aa36b2905a
Single-binary Go service that renders markdown pages from a runtime volume mount. Targeted at public, no-auth, no-WAF deployment behind a TLS ingress; security posture is defense-in-depth at every layer: - goldmark with no WithUnsafe — raw HTML in author markdown is stripped - CSP without 'unsafe-inline', plus HSTS, COOP, CORP, Permissions-Policy - static handler rejects non-GET/HEAD, directory listings, dotfiles, traversal - content loader rejects symlinks that escape the content root, dotfiles, and .md files larger than 1 MiB - per-page template trees (cloned from layout) so define-blocks don't collide between home/category/page - SIGHUP triggers atomic library swap — live edits on volume, no rebuild Locale layout content/<locale>/<category>/<slug>.md. Categories without _index.md still appear on the home page with a humanized name. Search is a ~70-line vanilla JS scan over /search.json?lang=<locale>; swap for a real indexer if the corpus ever balloons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
36 lines
985 B
Go
36 lines
985 B
Go
// Package render converts markdown to HTML using goldmark.
|
|
//
|
|
// Zero-trust posture: raw HTML in markdown is escaped. Even though content comes from
|
|
// an operator-mounted volume (not a request), defense-in-depth keeps a stray <script>
|
|
// or <iframe> in a tutorial from running in a visitor's browser. Authors get goldmark
|
|
// + GFM + footnotes; raw HTML in .md is rendered as text.
|
|
package render
|
|
|
|
import (
|
|
"bytes"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/parser"
|
|
)
|
|
|
|
var md = goldmark.New(
|
|
goldmark.WithExtensions(
|
|
extension.GFM, // tables, strikethrough, autolink, tasklist
|
|
extension.Footnote,
|
|
),
|
|
goldmark.WithParserOptions(
|
|
parser.WithAutoHeadingID(),
|
|
),
|
|
// No WithUnsafe() — raw HTML stays escaped.
|
|
)
|
|
|
|
// Markdown renders a markdown body to HTML.
|
|
func Markdown(source []byte) (string, error) {
|
|
var buf bytes.Buffer
|
|
if err := md.Convert(source, &buf); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|