internal/content/page.go
102 Zeilen · 2.6 KB · Go
| 1 | package content |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "log" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // Page represents a static site page (e.g. Impressum, Datenschutz, Projekte). |
| 12 | type Page struct { |
| 13 | Title string `yaml:"title"` |
| 14 | Description string `yaml:"description"` |
| 15 | |
| 16 | // Derived |
| 17 | Slug string // URL path relative to /sites/, e.g. "impressum" or "projekte/vm-tracker" |
| 18 | Content string // raw markdown |
| 19 | HTMLContent template.HTML // rendered HTML |
| 20 | } |
| 21 | |
| 22 | // ParsePage reads a markdown file and returns a Page. |
| 23 | func ParsePage(path string, basePath string) (*Page, error) { |
| 24 | data, err := os.ReadFile(path) |
| 25 | if err != nil { |
| 26 | return nil, err |
| 27 | } |
| 28 | |
| 29 | raw := strings.TrimLeft(string(data), " \t\n\r") |
| 30 | page := &Page{} |
| 31 | |
| 32 | if strings.HasPrefix(raw, "---") { |
| 33 | // Has frontmatter |
| 34 | rest := raw[3:] |
| 35 | idx := strings.Index(rest, "\n---") |
| 36 | if idx >= 0 { |
| 37 | frontmatter := rest[:idx] |
| 38 | body := rest[idx+4:] |
| 39 | if err := yamlUnmarshal([]byte(frontmatter), page); err != nil { |
| 40 | log.Printf("Warning: frontmatter parse error in %s: %v", path, err) |
| 41 | } |
| 42 | raw = body |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // Derive slug from relative path |
| 47 | rel, _ := filepath.Rel(basePath, path) |
| 48 | slug := strings.TrimSuffix(rel, filepath.Ext(rel)) |
| 49 | slug = strings.TrimSuffix(slug, ".md") // double .md handling |
| 50 | // Normalize: if file is named index.md, use parent dir as slug |
| 51 | if filepath.Base(slug) == "index" { |
| 52 | slug = filepath.Dir(slug) |
| 53 | if slug == "." { |
| 54 | slug = "" |
| 55 | } |
| 56 | } |
| 57 | page.Slug = slug |
| 58 | |
| 59 | // If no title from frontmatter, derive from slug |
| 60 | if page.Title == "" { |
| 61 | parts := strings.Split(slug, "/") |
| 62 | last := parts[len(parts)-1] |
| 63 | page.Title = strings.Title(strings.ReplaceAll(last, "-", " ")) |
| 64 | } |
| 65 | |
| 66 | // Render markdown |
| 67 | rawContent := preprocessContent(raw) |
| 68 | page.Content = rawContent |
| 69 | |
| 70 | var buf strings.Builder |
| 71 | if err := mdConvert([]byte(rawContent), &buf); err != nil { |
| 72 | log.Printf("Warning: markdown render error in %s: %v", path, err) |
| 73 | } |
| 74 | page.HTMLContent = template.HTML(buf.String()) |
| 75 | |
| 76 | return page, nil |
| 77 | } |
| 78 | |
| 79 | // LoadAllPages recursively reads all markdown files from the sites directory. |
| 80 | func LoadAllPages(sitesDir string) ([]*Page, error) { |
| 81 | if _, err := os.Stat(sitesDir); os.IsNotExist(err) { |
| 82 | return nil, nil // sites dir doesn't exist yet, that's ok |
| 83 | } |
| 84 | |
| 85 | var pages []*Page |
| 86 | err := filepath.WalkDir(sitesDir, func(path string, d os.DirEntry, err error) error { |
| 87 | if err != nil { |
| 88 | return err |
| 89 | } |
| 90 | if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") { |
| 91 | return nil |
| 92 | } |
| 93 | page, err := ParsePage(path, sitesDir) |
| 94 | if err != nil { |
| 95 | log.Printf("Warning: skipping page %s: %v", path, err) |
| 96 | return nil |
| 97 | } |
| 98 | pages = append(pages, page) |
| 99 | return nil |
| 100 | }) |
| 101 | return pages, err |
| 102 | } |