feat(seo): noindex hook, recipe self-canonical, list-page metadata
CI / update (push) Successful in 37s

Add X-Robots-Tag noindex,nofollow handler in hooks.server.ts for /api,
/login, /logout, /register, /settings, /tasks, /fitness, /cospend,
/expenses, and the recipe admin/edit/add/search/favorites/to-try paths.
Header-based so the rule lives in one place and covers JSON responses.

Recipe detail pages now emit a self-canonical pointing at the bare slug —
the layout helper deliberately skipped detail pages, leaving query-param
variants (?multiplier=2, ?utm=…) as duplicate URLs in Google's index.

Per-page Seo on list pages so each ranks for its category-level query:
- Apologetik contra/pro indices now use localized heading + lede instead
  of hardcoded English descriptions
- Calendar month view title includes month + rite ("April 2026 ·
  Liturgical Calendar (Vetus Ordo) — Bocken")
- Recipe /category, /tag, /icon, /season hub + detail pages get
  descriptions via new *_meta_description and *_meta_prefix i18n keys
  (added in both DE and EN locales)
This commit is contained in:
2026-05-02 22:23:15 +02:00
parent d59cc0a732
commit 4623d7a1f7
16 changed files with 112 additions and 37 deletions
+27
View File
@@ -25,6 +25,32 @@ async function htmlLang({ event, resolve }: Parameters<Handle>[0]) {
});
}
/** Routes that must never appear in search-engine indexes. Search-results pages
* are thin/duplicate content; admin/edit/auth-walled pages have no public value
* and shouldn't burn crawl budget. Sets X-Robots-Tag rather than per-page meta
* so the rule lives in one place and also covers JSON/API responses.
*/
const NOINDEX_PATTERNS: RegExp[] = [
/^\/api(\/|$)/,
/^\/(rezepte|recipes)\/(search|admin|administration|add|edit|favorites|to-try)(\/|$)/,
/^\/login$/,
/^\/logout$/,
/^\/register(\/|$)/,
/^\/settings(\/|$)/,
/^\/tasks(\/|$)/,
/^\/fitness(\/|$)/,
/^\/cospend(\/|$)/,
/^\/expenses(\/|$)/,
];
async function noindex({ event, resolve }: Parameters<Handle>[0]) {
const response = await resolve(event);
if (NOINDEX_PATTERNS.some((p) => p.test(event.url.pathname))) {
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
}
return response;
}
async function timing({ event, resolve }: Parameters<Handle>[0]) {
const marks: Record<string, number> = {};
event.locals.timing = {
@@ -162,6 +188,7 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
export const handle: Handle = sequence(
timing,
htmlLang,
noindex,
auth.handle,
authorization
);