v2-Neuerstellung
Some checks failed
release-tag / release-image (push) Failing after 1m48s

This commit is contained in:
2026-05-18 11:25:31 +02:00
parent a38c883450
commit b7edfdd544
15 changed files with 836 additions and 600 deletions

View File

@@ -1,8 +1,5 @@
// internal/article/load.go
package article
// internal/article/load.go (gekürzt)
import (
"bytes"
"encoding/json"
@@ -19,15 +16,24 @@ import (
"github.com/gomarkdown/markdown/html"
)
// LoadDir liest alle *.html und *.md unter root/content,
// parst FrontMatter (JSONKommentar in der 1. Zeile) und liefert []Article.
func LoadDir(root string) ([]Article, error) {
var out []Article
seen := make(map[string]bool) // DuplikatsCheck
type LoaderOptions struct {
// TrustedHTML erlaubt rohe .html-Inhalte aus deinem Content-Repository.
// Achtung: Nur aktivieren, wenn du dem Repository vertraust.
TrustedHTML bool
}
mdRenderer := html.NewRenderer(html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
})
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 {
@@ -38,37 +44,15 @@ func LoadDir(root string) ([]Article, error) {
}
ext := strings.ToLower(filepath.Ext(path))
if ext != ".md" && ext != ".html" {
return nil // uninteressante Datei
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
return nil
}
raw, err := os.ReadFile(path)
meta, body, format, err := readContentFile(path, opts)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
return err
}
parts := bytes.SplitN(raw, []byte("\n"), 2)
if len(parts) < 2 {
return fmt.Errorf("%s: missing frontmatter", path)
}
headerJSON, err := extractJSONHeader(parts[0])
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
var meta struct {
Title string `json:"title"`
Date string `json:"date"`
Slug string `json:"slug"`
Cover string `json:"cover"`
Description string `json:"description"`
}
if err := json.Unmarshal(headerJSON, &meta); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
// FallbackSlug aus Dateinamen
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(filepath.Base(path), ext)
}
@@ -77,11 +61,6 @@ func LoadDir(root string) ([]Article, error) {
}
seen[meta.Slug] = true
body := parts[1]
if ext == ".md" {
body = md.ToHTML(body, nil, mdRenderer) // frischer Parser pro Call
}
date, err := time.Parse("2006-01-02", meta.Date)
if err != nil {
return fmt.Errorf("%s: parse date: %w", path, err)
@@ -93,7 +72,9 @@ func LoadDir(root string) ([]Article, error) {
Date: date,
Cover: meta.Cover,
Description: meta.Description,
Body: template.HTML(body),
Body: body,
Format: format,
SourcePath: path,
})
return nil
})
@@ -105,34 +86,7 @@ func LoadDir(root string) ([]Article, error) {
return out, nil
}
// ---------- NEU ----------
type StaticPage struct {
Title string
Slug string
Description string
Body template.HTML
}
// helper.go einmal zentral verwenden
func extractJSONHeader(line []byte) ([]byte, error) {
s := strings.TrimSpace(string(line))
s = strings.TrimPrefix(s, "<!--")
// alles bis zum ersten "-->" abschneiden
if idx := strings.Index(s, "-->"); idx != -1 {
s = s[:idx]
}
s = strings.TrimSpace(s)
// optional: falls doch kein JSON, besserer Fehler
if !strings.HasPrefix(s, "{") {
return nil, fmt.Errorf("no JSON frontmatter found")
}
return []byte(s), nil
}
// LoadStatic liest alle .md/.htmlDateien unter dir und liefert sie als Map[slug]StaticPage.
func LoadStatic(dir string) (map[string]StaticPage, error) {
func LoadStatic(dir string, opts LoaderOptions) (map[string]StaticPage, error) {
pages := make(map[string]StaticPage)
entries, err := os.ReadDir(dir)
@@ -144,38 +98,17 @@ func LoadStatic(dir string) (map[string]StaticPage, error) {
if e.IsDir() {
continue
}
ext := filepath.Ext(e.Name())
if ext != ".md" && ext != ".html" {
continue // unbekanntes Format
ext := strings.ToLower(filepath.Ext(e.Name()))
if ext != ".md" && ext != ".markdown" && ext != ".html" && ext != ".htm" {
continue
}
path := filepath.Join(dir, e.Name())
raw, err := os.ReadFile(path)
meta, body, format, err := readContentFile(path, opts)
if err != nil {
return nil, fmt.Errorf("read %s: %w", path, err)
return nil, err
}
// FrontMatter (erste Zeile) herauslösen
parts := bytes.SplitN(raw, []byte("\n"), 2)
if len(parts) < 2 {
return nil, fmt.Errorf("%s: missing frontmatter", path)
}
headerJSON, err := extractJSONHeader(parts[0])
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
var meta struct {
Title string `json:"title"`
Slug string `json:"slug"`
Description string `json:"description"`
}
if err := json.Unmarshal(headerJSON, &meta); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
// Fallback: Slug aus Dateinamen ableiten, falls im Header leer
if meta.Slug == "" {
meta.Slug = strings.TrimSuffix(e.Name(), ext)
}
@@ -183,17 +116,88 @@ func LoadStatic(dir string) (map[string]StaticPage, error) {
return nil, fmt.Errorf("%s: duplicate slug %q", path, meta.Slug)
}
body := parts[1]
if ext == ".md" {
body = md.ToHTML(body, nil, nil) // eigener Parser je Aufruf
}
pages[meta.Slug] = StaticPage{
Title: meta.Title,
Slug: meta.Slug,
Description: meta.Description,
Body: template.HTML(body),
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, "<!--")
if idx := strings.Index(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
}

View File

@@ -1,4 +1,3 @@
// internal/article/model.go
package article
import (
@@ -6,6 +5,13 @@ import (
"time"
)
type Format string
const (
FormatMarkdown Format = "markdown"
FormatHTML Format = "html"
)
type Article struct {
Title string
Slug string
@@ -13,7 +19,9 @@ type Article struct {
Cover string
Body template.HTML
Description string
Counter string
Counter int64
Format Format
SourcePath string
}
type ListPage struct {
@@ -21,3 +29,12 @@ type ListPage struct {
Description string
Articles []Article
}
type StaticPage struct {
Title string
Slug string
Description string
Body template.HTML
Format Format
SourcePath string
}