Files
chaosmith-site/blog/store.go
Pierre De Lancre c2e58aa9d7 Initial commit: chaosmith-site with blog
Static landing page + markdown blog served by Go.
Goldmark renders posts in-memory, no database.
Dockerfile + compose for podman deployment.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>
2026-06-24 14:58:29 +03:00

162 lines
3.1 KiB
Go

package blog
import (
"bytes"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/yuin/goldmark"
)
type Post struct {
Slug string
Title string
Date time.Time
HTML string
}
type Store struct {
posts []Post
index map[string]int
}
func NewStore() *Store {
return &Store{index: make(map[string]int)}
}
func (s *Store) Load(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
s.posts = s.posts[:0]
s.index = make(map[string]int)
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
path := filepath.Join(dir, e.Name())
raw, err := os.ReadFile(path)
if err != nil {
continue
}
slug := strings.TrimSuffix(e.Name(), ".md")
title, body, date := parseFrontMatter(raw, slug, path)
var buf bytes.Buffer
if err := goldmark.Convert(body, &buf); err != nil {
continue
}
s.index[slug] = len(s.posts)
s.posts = append(s.posts, Post{
Slug: slug,
Title: title,
Date: date,
HTML: buf.String(),
})
}
// sort newest first
sort.Slice(s.posts, func(i, j int) bool {
return s.posts[i].Date.After(s.posts[j].Date)
})
for i, p := range s.posts {
s.index[p.Slug] = i
}
return nil
}
func (s *Store) All() []Post {
return s.posts
}
func (s *Store) Get(slug string) (Post, bool) {
i, ok := s.index[slug]
if !ok {
return Post{}, false
}
return s.posts[i], true
}
func (s *Store) Count() int {
return len(s.posts)
}
var h1Re = regexp.MustCompile(`(?m)^#\s+(.+)$`)
// Date formats we recognize on the line after the title
var dateFormats = []string{
"2006-01-02",
"January 2, 2006",
"Jan 2, 2006",
"02/01/2006",
}
// parseFrontMatter extracts the title from the first `# heading`,
// strips that line from the body, and tries to parse a date from
// the line immediately after. Falls back to file mod time.
func parseFrontMatter(raw []byte, slug, path string) (title string, body []byte, date time.Time) {
body = raw
// Extract title
m := h1Re.FindSubmatch(raw)
if len(m) >= 2 {
title = strings.TrimSpace(string(m[1]))
} else {
title = slug
}
// Strip the first `# title` line from body so it's not rendered twice
if loc := h1Re.FindIndex(raw); loc != nil {
// Find end of that line
lineEnd := loc[1]
// advance past newline
for lineEnd < len(raw) && (raw[lineEnd] == '\n' || raw[lineEnd] == '\r') {
lineEnd++
}
body = raw[lineEnd:]
}
// Try to parse date from first remaining line
date = fileModTime(path)
rest := strings.TrimSpace(string(body))
if rest != "" {
firstLine := rest
if idx := strings.IndexByte(rest, '\n'); idx >= 0 {
firstLine = strings.TrimSpace(rest[:idx])
}
for _, fmt := range dateFormats {
if t, err := time.Parse(fmt, firstLine); err == nil {
date = t
// strip the date line from body too
if idx := strings.IndexByte(string(body), '\n'); idx >= 0 {
body = body[idx+1:]
} else {
body = nil
}
break
}
}
}
return title, body, date
}
func fileModTime(path string) time.Time {
info, err := os.Stat(path)
if err != nil {
return time.Now()
}
return info.ModTime()
}