New blog posts should get short, human-readable URL slugs (e.g. /posts/2026/03/19/container-gov/) instead of today's long path derived from the title (e.g.
/posts/2026/03/19/open-elements-liefert-gehärtete-software-für-die-deutsche-verwaltung/).
At the same time, not a single existing link may break. To achieve this, every legacy post gets an explicit slug: field in its frontmatter that matches its current
URL exactly. Result: existing links (Google index, backlinks) stay valid, while new posts get clean URLs.
Background
The current URL structure is a Hugo legacy. The slug is derived from the title, not the filename:
generatePostPath() in src/lib/markdown.ts → YYYY/MM/DD/slugify(title)
slugify() deliberately keeps umlauts (\p{L}); code comment: "Keeps locale-specific characters (e.g. umlauts) to match Hugo URLs."
- Without an explicit
slug: field, the title-based derivation always applies (generatePostSlug()).
An explicit slug: field in the frontmatter already overrides the title-based derivation today — so the mechanism exists, it just needs to be used consistently.
Tasks
Acceptance criteria
- All post URLs reachable today remain reachable after the change (no 404, no redirect required).
- Newly created posts can get a clean URL via a short
slug: field.
- The convention is documented.
References
src/lib/markdown.ts — generatePostPath(), generatePostSlug(), slugify(), getAllPostSlugs()
src/components/blog/BlogCard.tsx — builds post links from post.slug
New blog posts should get short, human-readable URL slugs (e.g.
/posts/2026/03/19/container-gov/) instead of today's long path derived from the title (e.g./posts/2026/03/19/open-elements-liefert-gehärtete-software-für-die-deutsche-verwaltung/).At the same time, not a single existing link may break. To achieve this, every legacy post gets an explicit
slug:field in its frontmatter that matches its currentURL exactly. Result: existing links (Google index, backlinks) stay valid, while new posts get clean URLs.
Background
The current URL structure is a Hugo legacy. The slug is derived from the title, not the filename:
generatePostPath()insrc/lib/markdown.ts→YYYY/MM/DD/slugify(title)slugify()deliberately keeps umlauts (\p{L}); code comment: "Keeps locale-specific characters (e.g. umlauts) to match Hugo URLs."slug:field, the title-based derivation always applies (generatePostSlug()).An explicit
slug:field in the frontmatter already overrides the title-based derivation today — so the mechanism exists, it just needs to be used consistently.Tasks
content/posts/, set an explicitslug:field matching the current title-generated slug (onlythe final path segment, without
YYYY/MM/DD). This keeps existing post URLs unchanged.generatePostSlug()), not manually.CLAUDE.md/ the conventions that new posts should set a short, human-readableslug:field.slug:field become mandatory going forward (lint/CI check) to prevent accidental long paths from reappearing?getAllPostSlugs()additionally pushes the filename-based slug) is not affected by the change.*.de.md/*.md) are handled correctly.Acceptance criteria
slug:field.References
src/lib/markdown.ts—generatePostPath(),generatePostSlug(),slugify(),getAllPostSlugs()src/components/blog/BlogCard.tsx— builds post links frompost.slug