- vault → notes (PKM-exported content) - posts → articles (short-form, no TOC) - papers → essays (long-form, with TOC) - type: post → type: article (posts are just short articles) - layouts/paper → layouts/essay - 08a-paper.css → 08a-essay.css - CSS: fix redundant li resets, remove role="main" from article, replace <small> prev/next labels, add console layout - Update hugo.toml menus, internal URLs, front matter throughout - Add docs/context.md, docs/css-refactor-plan.md
2.5 KiB
Template Design: Handling the Markdown H1 / Front Matter Title Conflict
The Problem
Standard markdown convention — Obsidian, agent-written files, generic .md — opens
with a level-1 heading as the document title:
# My Note Title
Body text...
Hugo templates also render a title from front matter:
<h1>{{ .Title }}</h1>
When both exist, the page gets two <h1> elements: one from the template,
one from the rendered markdown content. The content one also carries an
auto-generated id attribute from Hugo's heading anchor renderer.
The Two Template Contracts
Default (_default/single.html) — bare markdown, minimal or no front matter.
The # Title in content IS the h1. The template header renders only metadata
(type, date, author, tags). No title rendered from front matter.
Vault (vault/single.html) — enriched front matter (title, type, date,
author, tags). Front matter title is authoritative. The # Title in content
is still present (markdown convention) but must be suppressed.
The Fix: Engine-Agnostic Regex Strip
When a template owns the title (renders <h1>{{ .Title }}</h1> from front matter),
strip the first h1 from the rendered content before outputting it.
Hugo:
{{ replaceRE "<h1[^>]*>.*?</h1>" "" .Content 1 | safeHTML }}
Jinja2 / Flask:
import re
content = re.sub(r'<h1[^>]*>.*?</h1>', '', content, count=1, flags=re.DOTALL)
Nunjucks / Liquid / any engine: equivalent string replace on the rendered HTML.
This is a string operation on already-rendered HTML, not a template-engine concept. It ports to any engine without modification.
Why Not Other Approaches
- Author convention (don't write
# Titlein vault files): breaks compatibility with the entire markdown ecosystem. - Hugo render hooks (
layouts/vault/_markup/render-heading.html): Hugo-specific, not portable. - CSS
display: none: h1 still exists in DOM — screen readers read it, search engines index it. Semantically wrong.
Engine-Agnostic Principle
ASW templates are prototyped in Hugo but must be portable to Flask/Jinja2 or any other engine. Template logic should express what, not how:
- What: "strip h1 from content if front matter title is present"
- How: engine-specific implementation of the same string operation
Hugo-specific features (render hooks, shortcodes) are acceptable as prototyping tools but should not become load-bearing parts of the template design.