seo: enforce trailing slash on all URLs
- 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:
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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)}/`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)}/`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 ?? '' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user