first commit

This commit is contained in:
Blake Ridgway
2026-03-07 21:16:51 -06:00
parent 21bd542469
commit 03fcf37beb
33 changed files with 3532 additions and 0 deletions

115
internal/blog/post.go Normal file
View File

@@ -0,0 +1,115 @@
package blog
import (
"bytes"
"html/template"
"strings"
"time"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"gopkg.in/yaml.v3"
)
// FrontMatter is the YAML block at the top of a post file.
type FrontMatter struct {
Title string `yaml:"title"`
Date string `yaml:"date"`
Tags []string `yaml:"tags"`
Slug string `yaml:"slug"`
Draft bool `yaml:"draft"`
Description string `yaml:"description"`
}
// Post is a parsed blog post with rendered HTML content.
type Post struct {
FrontMatter
ParsedDate time.Time
Content template.HTML
Slug string
}
// RenderMarkdown converts a markdown string to HTML.
func RenderMarkdown(src string) (template.HTML, error) {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Footnote,
extension.DefinitionList,
extension.Strikethrough,
extension.Table,
highlighting.NewHighlighting(
highlighting.WithFormatOptions(
chromahtml.WithClasses(true),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithXHTML(),
),
)
var buf bytes.Buffer
if err := md.Convert([]byte(src), &buf); err != nil {
return "", err
}
return template.HTML(buf.String()), nil
}
// ParsePost parses raw markdown bytes (with optional YAML frontmatter) into a Post.
// filename is the bare filename (e.g. "my-post.md") used to derive the slug if not set.
func ParsePost(raw []byte, filename string) (*Post, error) {
content := string(raw)
var fm FrontMatter
var markdownContent string
if strings.HasPrefix(content, "---") {
// Find closing ---
rest := content[3:]
idx := strings.Index(rest, "\n---")
if idx >= 0 {
fmRaw := rest[:idx]
if err := yaml.Unmarshal([]byte(fmRaw), &fm); err != nil {
return nil, err
}
markdownContent = strings.TrimSpace(rest[idx+4:])
} else {
markdownContent = content
}
} else {
markdownContent = content
}
htmlContent, err := RenderMarkdown(markdownContent)
if err != nil {
return nil, err
}
slug := fm.Slug
if slug == "" {
slug = strings.TrimSuffix(filename, ".md")
}
var parsedDate time.Time
if fm.Date != "" {
for _, layout := range []string{"2006-01-02", "2006-01-02T15:04:05Z07:00"} {
if t, err := time.Parse(layout, fm.Date); err == nil {
parsedDate = t
break
}
}
}
return &Post{
FrontMatter: fm,
ParsedDate: parsedDate,
Content: htmlContent,
Slug: slug,
}, nil
}