diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
index 3957d4dd..b6eff739 100644
--- a/src/lib/markdown.ts
+++ b/src/lib/markdown.ts
@@ -398,6 +398,59 @@ function decorateExternalLinks(contentHtml: string): string {
);
}
+function rewriteInternalLinks(contentHtml: string, locale: string): string {
+ return contentHtml.replace(
+ /]*?)href=(["'])(.*?)\2([^>]*)>([\s\S]*?)<\/a>/gi,
+ (
+ fullMatch,
+ attributesBeforeHref: string,
+ quote: string,
+ href: string,
+ attributesAfterHref: string,
+ linkContent: string,
+ ) => {
+ const trimmedHref = href.trim();
+
+ // Only process root-relative paths (starting with '/' but not '//')
+ if (!trimmedHref.startsWith('/') || trimmedHref.startsWith('//')) {
+ return fullMatch;
+ }
+
+ // Leave asset paths untouched (path segment containing a file extension)
+ const pathPart = trimmedHref.split(/[?#]/)[0];
+ const lastSegment = pathPart.split('/').pop() || '';
+ if (lastSegment.includes('.')) {
+ return fullMatch;
+ }
+
+ // Check if it already has a locale prefix (e.g. /de/ or /de)
+ const hasLocalePrefix = /^\/(de|en)(?:[\/?#]|$)/.test(trimmedHref);
+
+ // Normalize trailing slash
+ let normalizedHref = trimmedHref;
+ const match = trimmedHref.match(/^([^?#]*)(.*)$/);
+ if (match) {
+ let pPart = match[1];
+ const queryAndHash = match[2];
+ if (pPart !== '/' && pPart.endsWith('/')) {
+ pPart = pPart.slice(0, -1);
+ }
+ normalizedHref = pPart + queryAndHash;
+ }
+
+ // Prepend locale prefix if needed
+ let newHref = normalizedHref;
+ if (!hasLocalePrefix && locale !== 'en') {
+ const prefix = `/${locale}`;
+ newHref =
+ normalizedHref === '/' ? prefix : `${prefix}${normalizedHref}`;
+ }
+
+ return `${linkContent}`;
+ },
+ );
+}
+
const STANDALONE_IMAGE_STYLE =
'display: block; max-width: 100%; height: auto; margin-left: auto; margin-right: auto;';
@@ -722,8 +775,11 @@ export async function getPostBySlug(
.process(transformedContent);
const contentHtml = highlightCodeBlocks(
decorateHeadlinesWithAnchors(
- decorateExternalLinks(
- centerStandaloneHtmlImages(processedContent.toString()),
+ rewriteInternalLinks(
+ decorateExternalLinks(
+ centerStandaloneHtmlImages(processedContent.toString()),
+ ),
+ locale,
),
),
);
diff --git a/tests/e2e/internal-links.spec.ts b/tests/e2e/internal-links.spec.ts
new file mode 100644
index 00000000..816d3221
--- /dev/null
+++ b/tests/e2e/internal-links.spec.ts
@@ -0,0 +1,45 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Locale-aware internal link rewriter', () => {
+ test('rewrites internal links in a German post to include /de prefix and normalizes trailing slashes', async ({ page }) => {
+ // Navigate to a German post
+ await page.goto('/de/posts/2025-12-15-cra');
+
+ const articleBody = page.locator('main .prose').first();
+
+ // /contact should be rewritten to /de/contact
+ const contactLink = articleBody.locator('a[href="/de/contact"]').first();
+ await expect(contactLink).toBeVisible();
+
+ // /newsletter should be rewritten to /de/newsletter
+ const newsletterLink = articleBody.locator('a[href="/de/newsletter"]').first();
+ await expect(newsletterLink).toBeVisible();
+
+ // External links should NOT be rewritten
+ const externalLink = articleBody.locator('a[href^="https://eur-lex.europa.eu"]');
+ await expect(externalLink).toBeVisible();
+ await expect(externalLink).not.toHaveAttribute('href', /^\/de/);
+ });
+
+ test('rewrites shortcode relref links in a German post to include /de prefix', async ({ page }) => {
+ // Navigate to the German review 2025 post which contains a relref to the 2024 post
+ await page.goto('/de/posts/2026-02-10-review-2025');
+
+ const articleBody = page.locator('main .prose').first();
+
+ // The relref posts/2025-01-16-open-elements-in-2024 should be rewritten to /de/posts/2025-01-16-open-elements-in-2024
+ const relrefLink = articleBody.locator('a[href="/de/posts/2025-01-16-open-elements-in-2024"]');
+ await expect(relrefLink).toBeVisible();
+ });
+
+ test('does not prefix internal links in an English post', async ({ page }) => {
+ // Navigate to the English review 2025 post
+ await page.goto('/posts/2026-02-10-review-2025');
+
+ const articleBody = page.locator('main .prose').first();
+
+ // In English post, relref should remain unprefixed /posts/2025-01-16-open-elements-in-2024
+ const relrefLink = articleBody.locator('a[href="/posts/2025-01-16-open-elements-in-2024"]');
+ await expect(relrefLink).toBeVisible();
+ });
+});