asw/docs/template-h1-title.md
Ludo 15a6db9c0e
refactor: rename content types to semantic taxonomy
- 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
2026-04-11 13:36:58 +02:00

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 # Title in 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.