Skip to content

Locale-aware internal link rewriter for Markdown content #363

Description

@hendrikebbers

Internal links written in Markdown post content are not rewritten to the current locale. A raw link like /posts/2025/12/15/... or /support-care on a German
(*.de.md) post resolves to the English default locale. If the target only exists in German (e.g. a *.de.md-only post), this produces a 404.

Concrete example that broke on a deploy preview:

  • Written in a .de.md post: /posts/2025/12/15/cyber-resilience-act-übersicht-und-auswirkungen
  • Result: …/posts/2025/12/15/…404
  • Correct target: …/de/posts/2025/12/15/…

Current workaround: authors must hard-code the /de prefix in every internal link inside German posts. This is error-prone and easy to forget, and it couples content to the
routing scheme.

Goal

Internal links in Markdown content should automatically resolve to the locale of the post being rendered. Authors write locale-neutral links (e.g. /posts/2025/12/15/...,
/support-care) and the renderer prefixes the active locale where needed.

Where

The Markdown pipeline already post-processes <a> tags in src/lib/markdown.ts:

  • isExternalContentLink() — distinguishes external vs. internal hrefs
  • addExternalLinkIcon() + the <a …href=…> regex (~line 372) — currently only appends an external-link icon
  • The render entry point (remark().use(remarkGfm).use(html, { sanitize: false })…, ~line 712) needs access to the current locale so it can rewrite internal hrefs.

Tasks

  • Pass the current locale into the Markdown render function (it is rendered per-locale already; thread the locale through).
  • Add an internal-link rewriter step: for hrefs that are internal (root-relative, not external per isExternalContentLink()) and not already locale-prefixed, prepend the
    locale path (en → no prefix, de/de) — mirroring the localePath logic used in BlogCard.tsx / rss.ts.
  • Do not double-prefix links that already contain a locale prefix (idempotent).
  • Leave external links, anchors (#…), mailto:/tel: and asset paths untouched.
  • Decide on trailing-slash normalization (Next.js default = no trailing slash; keep consistent).
  • Once shipped, existing hard-coded /de/... links in content can be simplified back to locale-neutral form (optional cleanup).

Acceptance criteria

  • A locale-neutral internal link in a *.de.md post renders with the /de prefix and resolves without a 404.
  • The same link in an English post renders without a prefix.
  • External links, anchors, and assets are unchanged; no double locale prefixes.

References

  • src/lib/markdown.tsisExternalContentLink(), addExternalLinkIcon(), <a> post-processing regex (~line 372), render entry (~line 712)
  • src/components/blog/BlogCard.tsx, src/lib/rss.ts — existing localePath logic (locale === 'en' ? '' : '/${locale}')

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