Skip to content

Introduce short, human-readable post slugs — with backward compatibility for legacy URLs #362

Description

@hendrikebbers

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.tsYYYY/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

  • Backward compatibility: For each of the currently ~188 posts in content/posts/, set an explicit slug: field matching the current title-generated slug (only
    the final path segment, without YYYY/MM/DD). This keeps existing post URLs unchanged.
    • Should be done via a script (derived from generatePostSlug()), not manually.
  • Document the new convention: Record in CLAUDE.md / the conventions that new posts should set a short, human-readable slug: field.
  • Optional / discussion: Should the slug: field become mandatory going forward (lint/CI check) to prevent accidental long paths from reappearing?
  • Verify that the legacy alias route (getAllPostSlugs() additionally pushes the filename-based slug) is not affected by the change.
  • Ensure both DE and EN variants (*.de.md / *.md) are handled correctly.

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.tsgeneratePostPath(), generatePostSlug(), slugify(), getAllPostSlugs()
  • src/components/blog/BlogCard.tsx — builds post links from post.slug

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: triageIssue status label: status: triage

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions