feat(seo): per-route html lang, QAPage/Breadcrumb/Event/WebSite schemas, sitemap lastmod

Set <html lang> from URL prefix via handle hook (was hardcoded "en" despite
mostly German content). Add Person + WebSite + SearchAction graph to root
layout — enables Google sitelinks search box and clusters identity across
git.bocken.org and github.com/AlexBocken via sameAs.

Build apologetikJsonLd.ts: contra args now emit QAPage with one suggestedAnswer
per voiced archetype, citations as CreativeWork. Build breadcrumbJsonLd.ts and
wire BreadcrumbList into recipe detail, contra args, prayer detail, and
calendar day. Calendar day also emits Event schema.

Sitemap now reads recipes directly from MongoDB to populate <lastmod> from
dateModified; static URLs use server-startup ISO date. English recipe URLs
only emitted when translation status is approved.
This commit is contained in:
2026-05-02 21:48:05 +02:00
parent 7e33ea833e
commit ecbd24d7a4
13 changed files with 268 additions and 24 deletions
+31
View File
@@ -6,6 +6,33 @@
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
let { children } = $props();
const websiteJsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebSite',
'@id': 'https://bocken.org/#website',
url: 'https://bocken.org/',
name: 'Bocken',
inLanguage: ['de', 'en', 'la'],
publisher: { '@id': 'https://bocken.org/#person' },
potentialAction: {
'@type': 'SearchAction',
target: { '@type': 'EntryPoint', urlTemplate: 'https://bocken.org/rezepte/search?q={search_term_string}' },
'query-input': 'required name=search_term_string'
}
},
{
'@type': 'Person',
'@id': 'https://bocken.org/#person',
name: 'Alexander Bocken',
url: 'https://bocken.org/',
image: 'https://bocken.org/static/user/full/alexander.webp',
sameAs: ['https://git.bocken.org', 'https://github.com/AlexBocken']
}
]
};
/** Refresh server data on resume — Tauri WebView and backgrounded browser tabs
* don't re-run SvelteKit load() otherwise. Throttled: at most once per 5 min. */
const REFRESH_MIN_GAP_MS = 5 * 60 * 1000;
@@ -43,6 +70,10 @@
});
</script>
<svelte:head>
{@html `<script type="application/ld+json">${JSON.stringify(websiteJsonLd)}</script>`}
</svelte:head>
{@render children()}
<Toast />
<ConfirmDialog />