package article import ( "bytes" "encoding/json" "fmt" "html/template" "io/fs" "os" "path/filepath" "sort" "strings" "time" md "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" ) type LoaderOptions struct { // TrustedHTML erlaubt rohe .html-Inhalte aus deinem Content-Repository. // Achtung: Nur aktivieren, wenn du dem Repository vertraust. TrustedHTML bool } type frontMatter struct { Title string `json:"title"` Date string `json:"date"` Slug string `json:"slug"` Cover string `json:"cover"` Description string `json:"description"` Format string `json:"format"` } func LoadDir(root string, opts LoaderOptions) ([]Article, error) { var out []Article seen := make(map[string]bool) err := filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if d.IsDir() { return nil } ext := strings.ToLower(filepath.Ext(path)) if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" { return nil } meta, body, format, err := readContentFile(path, opts) if err != nil { return err } if meta.Slug == "" { meta.Slug = strings.TrimSuffix(filepath.Base(path), ext) } if seen[meta.Slug] { return fmt.Errorf("%s: duplicate slug %q", path, meta.Slug) } seen[meta.Slug] = true date, err := time.Parse("2006-01-02", meta.Date) if err != nil { return fmt.Errorf("%s: parse date: %w", path, err) } out = append(out, Article{ Title: meta.Title, Slug: meta.Slug, Date: date, Cover: meta.Cover, Description: meta.Description, Body: body, Format: format, SourcePath: path, }) return nil }) if err != nil { return nil, err } sort.Slice(out, func(i, j int) bool { return out[i].Date.After(out[j].Date) }) return out, nil } func LoadStatic(dir string, opts LoaderOptions) (map[string]StaticPage, error) { pages := make(map[string]StaticPage) entries, err := os.ReadDir(dir) if err != nil { return nil, err } for _, e := range entries { if e.IsDir() { continue } ext := strings.ToLower(filepath.Ext(e.Name())) if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" { continue } path := filepath.Join(dir, e.Name()) meta, body, format, err := readContentFile(path, opts) if err != nil { return nil, err } if meta.Slug == "" { meta.Slug = strings.TrimSuffix(e.Name(), ext) } if _, dup := pages[meta.Slug]; dup { return nil, fmt.Errorf("%s: duplicate slug %q", path, meta.Slug) } pages[meta.Slug] = StaticPage{ Title: meta.Title, Slug: meta.Slug, Description: meta.Description, Body: body, Format: format, SourcePath: path, } } return pages, nil } func readContentFile(path string, opts LoaderOptions) (frontMatter, template.HTML, Format, error) { raw, err := os.ReadFile(path) if err != nil { return frontMatter{}, "", "", fmt.Errorf("read %s: %w", path, err) } parts := bytes.SplitN(raw, []byte("\n"), 2) if len(parts) < 2 { return frontMatter{}, "", "", fmt.Errorf("%s: missing front-matter", path) } headerJSON, err := extractJSONHeader(parts[0]) if err != nil { return frontMatter{}, "", "", fmt.Errorf("%s: %w", path, err) } var meta frontMatter if err := json.Unmarshal(headerJSON, &meta); err != nil { return frontMatter{}, "", "", fmt.Errorf("%s: %w", path, err) } ext := strings.ToLower(filepath.Ext(path)) format := detectFormat(ext, meta.Format) body := renderBody(parts[1], format, opts) return meta, body, format, nil } func detectFormat(ext, explicit string) Format { switch strings.ToLower(strings.TrimSpace(explicit)) { case "html": return FormatHTML case "md", "markdown": return FormatMarkdown } switch ext { case ".html", ".htm": return FormatHTML default: return FormatMarkdown } } func renderBody(body []byte, format Format, opts LoaderOptions) template.HTML { if format == FormatMarkdown { renderer := html.NewRenderer(html.RendererOptions{ Flags: html.CommonFlags | html.HrefTargetBlank, }) return template.HTML(md.ToHTML(body, nil, renderer)) } if opts.TrustedHTML { return template.HTML(body) } // Sicherheits-Fallback: HTML-Dateien werden escaped, solange BLOG_TRUSTED_HTML=false ist. return template.HTML(template.HTMLEscapeString(string(body))) } func extractJSONHeader(line []byte) ([]byte, error) { s := strings.TrimSpace(string(line)) s = strings.TrimPrefix(s, ""); idx != -1 { s = s[:idx] } s = strings.TrimSpace(s) if !strings.HasPrefix(s, "{") { return nil, fmt.Errorf("no JSON front-matter found") } return []byte(s), nil }