seo: enforce trailing slash on all URLs
Deploy / verify (push) Successful in 37s
Deploy / deploy (push) Successful in 55s

- Set trailingSlash='always' globally via root layout.
- Update postHref, pageHref, Pagination, tag/breadcrumb hrefs, blog→posts
  redirects to emit trailing slash.
- Match old Astro canonical so indexed URLs hit 200 directly instead of
  308-redirect.
This commit is contained in:
Peter Meier
2026-04-17 22:23:16 +02:00
parent d28a55a70a
commit f328989aa8
12 changed files with 24 additions and 19 deletions
+1 -1
View File
@@ -302,7 +302,7 @@ const POST_BASE = "/post";
export function postHref(post: PostEntry): string { export function postHref(post: PostEntry): string {
const slug = post.slug ?? post._slug ?? ""; const slug = post.slug ?? post._slug ?? "";
const norm = (slug ?? "").replace(/^\//, "").trim(); const norm = (slug ?? "").replace(/^\//, "").trim();
return norm ? `${POST_BASE}/${encodeURIComponent(norm)}` : POST_BASE; return norm ? `${POST_BASE}/${encodeURIComponent(norm)}/` : `${POST_BASE}/`;
} }
export function formatPostDate(value: string | undefined): string { export function formatPostDate(value: string | undefined): string {
+2 -2
View File
@@ -108,8 +108,8 @@
} }
function tagHref(tagSlug: string | null): string { function tagHref(tagSlug: string | null): string {
if (!tagSlug) return basePath; if (!tagSlug) return `${basePath}/`;
return `${basePath}/tag/${encodeURIComponent(tagSlug)}`; return `${basePath}/tag/${encodeURIComponent(tagSlug)}/`;
} }
const totalCount = $derived(totalPosts); const totalCount = $derived(totalPosts);
+3 -3
View File
@@ -19,8 +19,8 @@
} = $props(); } = $props();
function tagHref(tagSlug: string | null): string { function tagHref(tagSlug: string | null): string {
if (!tagSlug) return basePath; if (!tagSlug) return `${basePath}/`;
return `${basePath}/tag/${encodeURIComponent(tagSlug)}`; return `${basePath}/tag/${encodeURIComponent(tagSlug)}/`;
} }
function pageHref(page: number): string { function pageHref(page: number): string {
@@ -28,7 +28,7 @@
const tagPart = activeTag const tagPart = activeTag
? `/tag/${encodeURIComponent(activeTag)}` ? `/tag/${encodeURIComponent(activeTag)}`
: ""; : "";
return `${basePath}${tagPart}/page/${page}`; return `${basePath}${tagPart}/page/${page}/`;
} }
const showPagination = $derived(totalPages > 1); const showPagination = $derived(totalPages > 1);
+1 -1
View File
@@ -51,7 +51,7 @@
<Tag <Tag
label={name} label={name}
variant={variant} variant={variant}
href={tagBase ? `${tagBase}/${slug(t)}` : null} href={tagBase ? `${tagBase}/${slug(t)}/` : null}
icon={tagIcon(t)} icon={tagIcon(t)}
customColor={tagColor(t)} customColor={tagColor(t)}
/> />
+6 -2
View File
@@ -1,4 +1,7 @@
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
export const trailingSlash = 'always';
import { import {
getNavigationByKey, getNavigationByKey,
NavigationKeys, NavigationKeys,
@@ -71,7 +74,7 @@ export const load: LayoutServerLoad = async ({ locals }) => {
for (const p of posts) { for (const p of posts) {
const label = p.linkName ?? p.headline ?? p.slug ?? p._slug ?? ''; const label = p.linkName ?? p.headline ?? p.slug ?? p._slug ?? '';
const urlSlug = normalizeSlug(p.slug ?? p._slug); const urlSlug = normalizeSlug(p.slug ?? p._slug);
const canonical = urlSlug ? `/post/${encodeURIComponent(urlSlug)}` : ''; const canonical = urlSlug ? `/post/${encodeURIComponent(urlSlug)}/` : '';
const keySlug = normalizeSlug(p.slug); const keySlug = normalizeSlug(p.slug);
const keyUnderscore = normalizeSlug(p._slug); const keyUnderscore = normalizeSlug(p._slug);
if (keySlug) { if (keySlug) {
@@ -111,7 +114,8 @@ export const load: LayoutServerLoad = async ({ locals }) => {
.split('/') .split('/')
.filter(Boolean) .filter(Boolean)
.map((seg) => encodeURIComponent(seg)) .map((seg) => encodeURIComponent(seg))
.join('/'); .join('/') +
'/';
const href = postHref ?? pageHref; const href = postHref ?? pageHref;
headerLinks.push({ href, label }); headerLinks.push({ href, label });
} }
+1 -1
View File
@@ -25,7 +25,7 @@ export type SearchHit = {
function pageHref(p: { slug?: string; _slug?: string }): string { function pageHref(p: { slug?: string; _slug?: string }): string {
const raw = (p.slug ?? p._slug ?? '').replace(/^\//, '').trim(); const raw = (p.slug ?? p._slug ?? '').replace(/^\//, '').trim();
if (!raw || raw === 'home') return '/'; if (!raw || raw === 'home') return '/';
return '/' + raw.split('/').filter(Boolean).map(encodeURIComponent).join('/'); return '/' + raw.split('/').filter(Boolean).map(encodeURIComponent).join('/') + '/';
} }
export const GET: RequestHandler = async () => { export const GET: RequestHandler = async () => {
+1 -1
View File
@@ -2,5 +2,5 @@ import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
redirect(301, `/posts/page/${encodeURIComponent(params.page)}`); redirect(301, `/posts/page/${encodeURIComponent(params.page)}/`);
}; };
+1 -1
View File
@@ -2,5 +2,5 @@ import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
redirect(301, `/posts/tag/${encodeURIComponent(params.tag)}`); redirect(301, `/posts/tag/${encodeURIComponent(params.tag)}/`);
}; };
+1 -1
View File
@@ -121,7 +121,7 @@ export const load: PageServerLoad = async ({ params, locals, url }) => {
socialImage: postImageUrl ?? undefined, socialImage: postImageUrl ?? undefined,
breadcrumbItems: [ breadcrumbItems: [
{ href: '/', label: 'Start' }, { href: '/', label: 'Start' },
{ href: '/posts', label: 'Beiträge' }, { href: '/posts/', label: 'Beiträge' },
{ label: post.headline ?? post.linkName ?? post.slug ?? post._slug ?? slug }, { label: post.headline ?? post.linkName ?? post.slug ?? post._slug ?? slug },
], ],
}; };
+1 -1
View File
@@ -13,7 +13,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
seoDescription: `Beiträge zum Thema ${result.tagName}.`, seoDescription: `Beiträge zum Thema ${result.tagName}.`,
breadcrumbItems: [ breadcrumbItems: [
{ href: '/', label: t(translations, T.nav_start) }, { href: '/', label: t(translations, T.nav_start) },
{ href: '/posts', label: t(translations, T.nav_posts) }, { href: '/posts/', label: t(translations, T.nav_posts) },
{ label: result.tagName ?? '' }, { label: result.tagName ?? '' },
], ],
}; };
@@ -14,8 +14,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
seoDescription: `Beiträge zum Thema ${result.tagName}.`, seoDescription: `Beiträge zum Thema ${result.tagName}.`,
breadcrumbItems: [ breadcrumbItems: [
{ href: '/', label: t(translations, T.nav_start) }, { href: '/', label: t(translations, T.nav_start) },
{ href: '/posts', label: t(translations, T.nav_posts) }, { href: '/posts/', label: t(translations, T.nav_posts) },
{ href: `/posts/tag/${encodeURIComponent(params.tag)}`, label: result.tagName ?? '' }, { href: `/posts/tag/${encodeURIComponent(params.tag)}/`, label: result.tagName ?? '' },
], ],
}; };
}; };
+4 -3
View File
@@ -15,7 +15,8 @@ function pageHref(canonical: string): string {
.split('/') .split('/')
.filter(Boolean) .filter(Boolean)
.map((seg) => encodeURIComponent(seg)) .map((seg) => encodeURIComponent(seg))
.join('/') .join('/') +
'/'
); );
} }
@@ -40,8 +41,8 @@ export const GET: RequestHandler = async ({ url }) => {
const urls: { loc: string; lastmod?: string; changefreq?: string; priority?: string }[] = [ const urls: { loc: string; lastmod?: string; changefreq?: string; priority?: string }[] = [
{ loc: `${site}/`, changefreq: 'daily', priority: '1.0' }, { loc: `${site}/`, changefreq: 'daily', priority: '1.0' },
{ loc: `${site}/posts`, changefreq: 'daily', priority: '0.8' }, { loc: `${site}/posts/`, changefreq: 'daily', priority: '0.8' },
{ loc: `${site}/blog`, changefreq: 'daily', priority: '0.8' }, { loc: `${site}/blog/`, changefreq: 'daily', priority: '0.8' },
]; ];
try { try {