Website configuration
June 8, 2026 · View on GitHub
The website/hugo.toml file ships the Hugo site at
mdsmith.dev. Two surfaces edit it programmatically:
mdsmith-release stamp <version>rewrites[params].versionfrom the dev sentinel to the release tag beforehugo --minify. See release tooling.mdsmith-release sync-messagingrewrites[params].descriptionfromdocs/brand/messaging.md. See plan 210.
Both round-trip the file through a real TOML parser, so the file does not carry inline rationale comments. The rationale lives here.
[module] mounts
Hugo treats website/ as the site root. The canonical docs
live in the sibling docs/ tree at the repo root. The two
trees are not mounted directly. Hugo parses shortcodes before
Markdown. Literal {{< ... >}} patterns in files like
docs/background/markdown-linters.md would break the build.
Before the Hugo build, mdsmith-release build-website
snapshots the source tree into website/content/docs/. The
snapshot escapes the shortcode patterns. It drops
proto.md templates. It renames index.md to _index.md.
The output directory is .gitignored.
The synced tree lives on disk under content/docs/ so the
.mdsmith.yml lint globs that target
website/content/docs/** keep matching. It is mounted at the
content root rather than under a docs segment: every doc
page is served at /<section>/..., not /docs/<section>/....
The only index is the homepage
(content/_index.md, served at /).
The first mount excludes docs/** so the unsynced
content/docs/ directory is not also exposed under /docs/;
the second mount re-roots it.
Maintainer docs are not published
The public site is for users. SyncDocs skips four docs/
subtrees at the docs root — development, research,
security, and brand. They ship in the repo for
maintainers but never render at mdsmith.dev. The set lives
in nonPublishedDocDirs in internal/release/syncdocs.go.
The user-facing sections (background, features, guides,
reference) are all that reach the site.
A link from a published page into a pruned tree must still
work. The rewriter routes it to the file's GitHub source at
sync time. So the link resolves on the site rather than
breaking. The code is repoPrunedDocLink in
internal/release/website.go (package release). The four
pruned dirs are direct children of the docs root, so a
relative link points at one of them at any depth.
Two consumers of the maintainer pages were repointed so they
survive the prune. The homepage logos strip now reads the
generated channels.yaml data file. It no longer reads the
/development/release-channels/ pages. The footer links
contributors to the GitHub docs/development tree.
disableKinds
disableKinds = ["taxonomy", "term"] switches off Hugo's
auto-generated /categories/ and /tags/ listing pages.
No .md file under docs/ declares either taxonomy.
Without the setting Hugo would still emit the empty index
URLs. They would all share the site-level
<meta name="description">. That is the duplicated SEO
snippet that hit mdsmith.dev before this setting landed.
Re-enable the kinds only when a published doc starts using
either taxonomy.
[markup.goldmark.renderer] unsafe = false
Synced docs carry no raw HTML. sync-docs strips
<?...?> directive markers and the content uses Markdown
code spans (not literal <code> / <…> tags), so
Goldmark's default raw-HTML filtering loses nothing.
Keep unsafe = false. Raw HTML in any doc would silently
vanish, flagging the author to fix the source rather than
ship unsanitized markup to mdsmith.dev.
[markup.highlight] style
noClasses = true emits inline styles rather than CSS
classes. The site does not ship a Chroma stylesheet under
static/css/, so the class-only form would render code
blocks unstyled. Switch to noClasses = false (and vendor
chroma-<style>.css) only when we want overridable token
classes.
style = "github-dark" is a dark style chosen because every
code surface on the site renders on the steel-900 panel (see
pre / .codeblock pre in CSS). The old light "github"
style emitted near-black token colors on that dark panel —
illegible (the "white/grey on grey" bug). github-dark's
light token colors sit on the dark panel with full contrast.
CSS pins the panel background so Chroma's own background
does not introduce a second, mismatched shade.
[params].version
Tracked by mdsmith-release stamp (see
internal/release/version.go
TrackedManifests). Between releases the value is the dev
sentinel 0.0.0-dev. The pages-deploy workflow rewrites it
to the cleaned tag (no leading v) before running
hugo --minify. Templates that display this should prefix
v in the rendered markup.
[params].description
Tracked by mdsmith-release sync-messaging from the
tagline field of
docs/brand/messaging.md. Hand-edits
to this field are reverted on the next sync. To change the
text, edit the source file and run the sync.
Summary front-matter rendering
Each docs page carries a summary front-matter field.
The field holds inline Markdown. Templates render it
through Hugo's .RenderString so backticks become
<code> and [text](url) becomes <a>.
The classifier lives in
internal/templatecheck. It exports
Scan(path, content). The function parses each
template with Go's text/template/parse package.
SkipFuncCheck mode is set so undefined Hugo
helpers do not error. The walker then visits the AST
and classifies each .Params.summary reference by
node context.
The integration test in
internal/release/template_summary_test.go
walks website/layouts/**/*.html and calls
templatecheck.Scan on each file. No regex
tokenising. No exemption list. Comments, string
literals, and CRLF line endings are handled by the
parser.
Safe forms:
- A presence predicate —
{{ if .Params.summary }}, the negated form, compound shapes ({{ if and .Params.summary .X }},{{ if or .Params.summary .Other }}), theelse ifvariant, subfield access ({{ if .Params.summary.HTML }}), or any other comparison that does not produce output. - A
.RenderStringcall with the summary as a positional argument:{{ .RenderString (dict "display" "inline") .Params.summary }}. Qualified receivers (.Page.RenderString,$.RenderString) are recognised. - A pipeline that passes the summary through
.RenderStringand then any number of post-render filters:{{ .Params.summary | .RenderString }},{{ .Params.summary | strings.TrimSpace | .RenderString }},{{ $.RenderString (dict "display" "inline") .Params.summary | plainify }}. Once the value has rendered, downstream stages such asplainifyorsafeHTMLare fine. - A sub-pipeline argument whose output feeds
.RenderString:{{ .RenderString (dict) (printf "wrapper: %s" .Params.summary) }}.
Forbidden forms:
{{ with .Params.summary }}and{{ else with .Params.summary }}— these rebind.to the summary string and the body typically emits the value raw.{{ range .Params.summary }}— ranging over a string iterates rune-by-rune and emits each code point as an integer.{{ template "name" .Params.summary }}and{{ block "name" .Params.summary }}— these pass the summary as the sub-template's dot. The sub-template lives in a separate parse tree; the scanner cannot follow the rebinding across the boundary.- The bare
{{ .Params.summary }}action. - Variable assignment in any context —
{{ $s := .Params.summary }},{{ if $s := .Params.summary }},{{ range $i, $v := .Params.summary }}. The bound name escapes the per-action check. .Params.summaryreferenced in a value-emitting action whose pipe does not reach.RenderString:{{ printf "%s" .Params.summary }},{{ .Params.summary | print "x" .Page.RenderString }}(the second example references.RenderStringas a value passed toprint, not as a method call).
baseof.html reuses the same projection. Each
branch of its meta-description chain runs the
source value through $.RenderString then
plainify. The sources are .Description,
.Params.summary, .Params.description, and
.Site.Params.description. Meta content cannot
carry HTML. Backticks become <code> then plain
text. SEO snippets see clean prose.
The rule applies only to .Params.summary today. If
other front-matter scalars (e.g. lead, eyebrow,
tagline) gain inline-Markdown content in the future,
extend the AST classifier in
internal/templatecheck to accept the
new field name set.