package main import ( "cmp" "embed" "encoding/json" "errors" "fmt" "io/fs" "log" "net/http" "os" "regexp" "slices" "strings" "sync" "time" "github.com/a-h/templ" "gopkg.in/yaml.v3" ) //go:embed public blogs var embedded embed.FS func Assert(ok bool) { if !ok { panic("assertion failure") } } type Theme struct { primary string secondary string accent string extra string text string alt string background string } type FontPack struct { Monospace string Normal string } type Style struct { theme Theme fontPack FontPack } var ( mu sync.RWMutex themes = map[string]Theme{} fontPacks = map[string]FontPack{} ) func SetFontPack(name string, fontPack FontPack) { mu.Lock() defer mu.Unlock() fontPacks[name] = fontPack } func GetFontPack(name string) (FontPack, bool) { mu.RLock() defer mu.RUnlock() fontPack, ok := fontPacks[name] return fontPack, ok } func SetTheme(name string, theme Theme) { mu.Lock() defer mu.Unlock() themes[name] = theme } func GetTheme(name string) (Theme, bool) { mu.RLock() defer mu.RUnlock() theme, ok := themes[name] return theme, ok } func currentStyle(r *http.Request) Style { theme, ok := GetTheme("default") Assert(ok) fontPack, ok := GetFontPack("default") Assert(ok) style := Style{ theme: theme, fontPack: fontPack, } if cookie, err := r.Cookie("theme"); err == nil { if theme, ok := GetTheme(cookie.Value); ok { style.theme = theme } } if cookie, err := r.Cookie("font-pack"); err == nil { if fontPack, ok := GetFontPack(cookie.Value); ok { style.fontPack = fontPack } } return style } func (theme Theme) Vars() map[string]string { return map[string]string{ "primary": theme.primary, "secondary": theme.secondary, "accent": theme.accent, "extra": theme.extra, "text": theme.text, "alt": theme.alt, "background": theme.background, } } func (fontPack FontPack) Vars() map[string]string { return map[string]string{ "font-mono": fontPack.Monospace, "font-normal": fontPack.Normal, } } func (style Style) Vars() map[string]string { vars := style.theme.Vars() for k, v := range style.fontPack.Vars() { vars[k] = v } return vars } func styleToCss(style Style) templ.SafeCSS { var b strings.Builder for k, v := range style.Vars() { fmt.Fprintf(&b, "--%s:%s;", k, v) } return templ.SafeCSS(b.String()) } type Blog struct { Slug string Title string Desc string Date time.Time Body string // raw markdown for rendering } type frontmatter struct { Title string `yaml:"title"` Desc string `yaml:"description"` Date time.Time `yaml:"date"` } var datePrefix = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})-(.+)\.md$`) func parseBlog(name, raw string) (Blog, error) { raw = strings.ReplaceAll(raw, "\r\n", "\n") body := raw var frontmatter frontmatter if rest, ok := strings.CutPrefix(raw, "---\n"); ok { if head, after, found := strings.Cut(rest, "\n---\n"); found { if err := yaml.Unmarshal([]byte(head), &frontmatter); err != nil { return Blog{}, err } body = after } } slug := strings.TrimSuffix(name, ".md") date := frontmatter.Date // frontmatter wins if match := datePrefix.FindStringSubmatch(name); match != nil { slug = match[2] if date.IsZero() { date, _ = time.Parse(time.DateOnly, match[1]) } } if date.IsZero() { return Blog{}, errors.New("no date in frontmatter or filename") } title := frontmatter.Title if title == "" { title = slug } return Blog{ Slug: slug, Title: title, Date: date, Body: body, Desc: frontmatter.Desc, }, nil } func LoadBlogs(fsys fs.FS) ([]Blog, error) { entries, err := fs.ReadDir(fsys, ".") if err != nil { return nil, err } var blogs []Blog for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") || strings.HasPrefix(e.Name(), "_") { continue } raw, err := fs.ReadFile(fsys, e.Name()) if err != nil { return nil, err } blog, err := parseBlog(e.Name(), string(raw)) if err != nil { return nil, fmt.Errorf("%s: %w", e.Name(), err) } blogs = append(blogs, blog) } slices.SortFunc(blogs, func(a, b Blog) int { // newest first return cmp.Or(b.Date.Compare(a.Date), strings.Compare(a.Slug, b.Slug)) }) return blogs, nil } func loadBlogs() ([]Blog, error) { blogFS, err := fs.Sub(embedded, "blogs") if err != nil { return nil, err } return LoadBlogs(blogFS) } var ( devMode = os.Getenv("DEV") != "" cachedBlogs []Blog ) func getBlogs() []Blog { if devMode || cachedBlogs == nil { var err error cachedBlogs, err = loadBlogs() if err != nil { log.Println("reloading blogs failed:", err) } } return cachedBlogs } func latestBlog(blogs []Blog) *Blog { if len(blogs) == 0 { return nil } return &blogs[0] } type Project struct { Name string Type string Desc []string Cmd string Link string } var projects = []Project{ { Name: "CPU vs AI", Type: "Game (made with Zig)", Desc: []string{ "A tower defense game where an AI chip on your motherboard", "generates bugs! Place CPUs to destroy the bugs!", "Don't let them corrupt your RAM!", }, }, { Name: "Eko", Type: "Social Media Platform", Desc: []string{ "A terminal-native alternative", "to Discord for Terminal", "nerds!", }, }, { Name: "Trident", Type: "IntelliJ Plugin", Desc: []string{ "A blazingly fast way to", "Navigate to your mosta used", "files. Minimize keystrokes", "and maximize productivity!", }, }, { Name: "Dotfiles", Type: "Configuration", Desc: []string{ "My Linux configuration,", "includes Neovim, tmux and", "other programs.", }, }, } func styleHandler(cookie string, lookup func(string) (map[string]string, bool)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") vars, ok := lookup(name) if !ok { http.NotFound(w, r) return } http.SetCookie(w, &http.Cookie{ Name: cookie, Value: name, Path: "/", MaxAge: 31536000, SameSite: http.SameSiteLaxMode, }) w.Header().Set("Cache-Control", "no-store") json.NewEncoder(w).Encode(vars) } } func notFoundHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) NotFoundPage(currentStyle(r), r.URL.Path).Render(r.Context(), w) } type MyUptime struct { mu sync.RWMutex Uptime string LastUpdate time.Time } var myUptime MyUptime // GetMyUptime returns the current status string (handles offline state) func GetMyUptime() string { myUptime.mu.RLock() defer myUptime.mu.RUnlock() // If no ping received in 3 minutes, treat as offline if time.Since(myUptime.LastUpdate) > 1*time.Minute { return "Asleep" } uptime := myUptime.Uptime uptime, _ = strings.CutPrefix(uptime, "up ") uptime = "Awake for " + uptime return uptime } func handleUptimePing(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "uptime": GetMyUptime(), }) } else if r.Method == http.MethodPost { if devMode == false { secretToken := os.Getenv("MY_UPTIME_TOKEN") if r.Header.Get("Authorization") != "Bearer "+secretToken { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } var req struct { Uptime string `json:"uptime"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } myUptime.mu.Lock() myUptime.Uptime = req.Uptime myUptime.LastUpdate = time.Now() myUptime.mu.Unlock() w.WriteHeader(http.StatusOK) } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } } func main() { carbonightCat := Theme{ primary: "#94e2d5", // secondary: "#b4befe", secondary: "#BB91F0", accent: "#74c7ec", extra: "#a6e3a1", text: "#cdd6f4", alt: "#585b70", background: "#1e1e2e", } darkness := Theme{ primary: "#000000", secondary: "#000000", accent: "#000000", extra: "#000000", text: "#000000", alt: "#000000", background: "#000000", } dark := Theme{ primary: "#54d7a9", secondary: "#9b7eca", accent: "#7ed4fb", extra: "#8fcc75", text: "#ffffff", alt: "#999999", background: "#000000", } light := Theme{ primary: "#009399", secondary: "#440b8e", accent: "#177282", extra: "#40a02b", text: "#000000", alt: "#4d4d4d", background: "#ffffff", } banana := Theme{ primary: "#ffdd44", secondary: "#9b7eca", accent: "#7ed4fb", extra: "#000000", text: "#ffffff", alt: "#999999", background: "#000000", } jetbrains := FontPack{ Monospace: "JetBrains Mono", Normal: `system-ui, sans-serif`, } minecraft := FontPack{ Monospace: `"Monocraft", "JetBrains Mono", monospace`, Normal: `"Monocraft", system-ui, sans-serif`, } SetTheme("carbonight-cat", carbonightCat) SetTheme("banana", banana) SetTheme("black", dark) SetTheme("dark", darkness) SetTheme("light", light) SetTheme("white", light) SetTheme("sun", light) SetTheme("sunlight", light) SetTheme("flashbang", light) SetTheme("default", carbonightCat) SetFontPack("jetbrains", jetbrains) SetFontPack("minecraft", minecraft) SetFontPack("default", jetbrains) mux := http.NewServeMux() mux.Handle("GET /public/", http.FileServerFS(embedded)) mux.HandleFunc("GET /api/my-uptime", handleUptimePing) mux.HandleFunc("POST /api/my-uptime", handleUptimePing) mux.HandleFunc("/", notFoundHandler) // matches everything mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { HomePage(currentStyle(r), latestBlog(getBlogs())).Render(r.Context(), w) }) mux.HandleFunc("GET /skills", func(w http.ResponseWriter, r *http.Request) { SkillsPage(currentStyle(r)).Render(r.Context(), w) }) mux.HandleFunc("GET /projects", func(w http.ResponseWriter, r *http.Request) { ProjectsPage(currentStyle(r), projects).Render(r.Context(), w) }) mux.HandleFunc("GET /blogs", func(w http.ResponseWriter, r *http.Request) { BlogsPage(currentStyle(r), getBlogs()).Render(r.Context(), w) }) mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) { theme, ok := GetTheme(n) return theme.Vars(), ok })) mux.HandleFunc("GET /font-pack/{name}", styleHandler("font-pack", func(n string) (map[string]string, bool) { fontPack, ok := GetFontPack(n) return fontPack.Vars(), ok })) fmt.Println("Server running at http://localhost:8080") if err := http.ListenAndServe(":8080", mux); err != nil { panic(err) } }