summaryrefslogtreecommitdiff
path: root/website.go
diff options
context:
space:
mode:
authorKyren223 <contact@kyren.codes>2026-10-08 19:44:46 +0300
committerKyren223 <contact@kyren.codes>2026-10-08 19:44:46 +0300
commit9b03d2790b8b8db1b5a6755b6130f37e5f7f2455 (patch)
treea84f6e9cfe2b8aaf087d55830c938443f6be683e /website.go
parent6e70fe447ec9504757fbdc152b5b3697c9af8def (diff)
WIP
Diffstat (limited to 'website.go')
-rw-r--r--website.go205
1 files changed, 201 insertions, 4 deletions
diff --git a/website.go b/website.go
index 8802bd1..b8c0e76 100644
--- a/website.go
+++ b/website.go
@@ -1,15 +1,28 @@
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")
@@ -128,6 +141,117 @@ func styleToCss(style Style) templ.SafeCSS {
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
@@ -193,10 +317,76 @@ func styleHandler(cookie string, lookup func(string) (map[string]string, bool))
}
}
+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: "#b4befe",
+ secondary: "#BB91F0",
accent: "#74c7ec",
extra: "#a6e3a1",
text: "#cdd6f4",
@@ -269,11 +459,14 @@ func main() {
mux := http.NewServeMux()
- fileServer := http.FileServer(http.Dir("./public"))
- mux.Handle("GET /public/", http.StripPrefix("/public/", fileServer))
+ 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)).Render(r.Context(), w)
+ HomePage(currentStyle(r), latestBlog(getBlogs())).Render(r.Context(), w)
})
mux.HandleFunc("GET /skills", func(w http.ResponseWriter, r *http.Request) {
@@ -284,6 +477,10 @@ func main() {
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