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:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="%lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
||||
|
||||
@@ -7,6 +7,24 @@ import { dbConnect } from "./utils/db"
|
||||
import { errorWithVerse, getRandomVerse } from "$lib/server/errorQuote"
|
||||
import { warmLiturgicalCache } from "$lib/server/liturgicalCalendar"
|
||||
|
||||
/** Map URL path to BCP 47 lang tag. Mirrors the [recipeLang] / [faithLang]
|
||||
* param matchers — keep in sync if new locale slugs are added.
|
||||
* @returns 'de' | 'en' | 'la'
|
||||
*/
|
||||
function langFromPath(pathname: string): 'de' | 'en' | 'la' {
|
||||
const first = pathname.split('/').filter(Boolean)[0] ?? '';
|
||||
if (first === 'recipes' || first === 'faith') return 'en';
|
||||
if (first === 'fides') return 'la';
|
||||
return 'de';
|
||||
}
|
||||
|
||||
async function htmlLang({ event, resolve }: Parameters<Handle>[0]) {
|
||||
const lang = langFromPath(event.url.pathname);
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%lang%', lang),
|
||||
});
|
||||
}
|
||||
|
||||
async function timing({ event, resolve }: Parameters<Handle>[0]) {
|
||||
const marks: Record<string, number> = {};
|
||||
event.locals.timing = {
|
||||
@@ -143,6 +161,7 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
|
||||
|
||||
export const handle: Handle = sequence(
|
||||
timing,
|
||||
htmlLang,
|
||||
auth.handle,
|
||||
authorization
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Argument, Archetype } from '$lib/data/apologetik';
|
||||
import type { FaithLang } from '$lib/js/faithI18n';
|
||||
import { faithSlugFromLang, apologetikSlug } from '$lib/js/faithI18n';
|
||||
|
||||
const SITE = 'https://bocken.org';
|
||||
|
||||
type CreativeWorkRef = { '@type': 'CreativeWork'; name: string };
|
||||
type Answer = {
|
||||
'@type': 'Answer';
|
||||
text: string;
|
||||
author: { '@type': 'Person'; name: string };
|
||||
citation?: CreativeWorkRef[];
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface ContraQaJsonLd {
|
||||
'@context': 'https://schema.org';
|
||||
'@type': 'QAPage';
|
||||
mainEntity: {
|
||||
'@type': 'Question';
|
||||
name: string;
|
||||
text: string;
|
||||
inLanguage: FaithLang;
|
||||
answerCount: number;
|
||||
suggestedAnswer: Answer[];
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a QAPage JSON-LD for a contra argument: one Question, many voiced Answers. */
|
||||
export function generateContraQaJsonLd(
|
||||
arg: Argument,
|
||||
archetypes: Record<string, Archetype>,
|
||||
lang: FaithLang
|
||||
): ContraQaJsonLd {
|
||||
const faithSeg = faithSlugFromLang(lang);
|
||||
const apolSeg = apologetikSlug(lang === 'la' ? 'en' : lang);
|
||||
const baseUrl = `${SITE}/${faithSeg}/${apolSeg}/contra/${arg.id}`;
|
||||
|
||||
const archIds = Object.keys(arg.counters);
|
||||
const answers: Answer[] = archIds.map((archId) => {
|
||||
const counter = arg.counters[archId];
|
||||
const archetype = archetypes[archId];
|
||||
const text = [counter.lede, ...(counter.body ?? [])].filter(Boolean).join('\n\n');
|
||||
const ans: Answer = {
|
||||
'@type': 'Answer',
|
||||
text,
|
||||
author: { '@type': 'Person', name: archetype?.name ?? archId },
|
||||
url: `${baseUrl}/${archId}`,
|
||||
};
|
||||
if (counter.cites?.length) {
|
||||
ans.citation = counter.cites.map((name) => ({ '@type': 'CreativeWork', name }));
|
||||
}
|
||||
return ans;
|
||||
});
|
||||
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'QAPage',
|
||||
mainEntity: {
|
||||
'@type': 'Question',
|
||||
name: arg.title,
|
||||
text: arg.steel,
|
||||
inLanguage: lang,
|
||||
answerCount: answers.length,
|
||||
suggestedAnswer: answers,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
const SITE = 'https://bocken.org';
|
||||
|
||||
export type Crumb = { name: string; path: string };
|
||||
|
||||
export interface BreadcrumbListJsonLd {
|
||||
'@context': 'https://schema.org';
|
||||
'@type': 'BreadcrumbList';
|
||||
itemListElement: Array<{
|
||||
'@type': 'ListItem';
|
||||
position: number;
|
||||
name: string;
|
||||
item: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** Build a BreadcrumbList. Pass crumbs in order from root → current page.
|
||||
* Last crumb's `item` is omitted per Google guidance (current page).
|
||||
* Paths are relative ("/rezepte"); SITE is prepended. */
|
||||
export function generateBreadcrumbJsonLd(crumbs: Crumb[]): BreadcrumbListJsonLd {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: crumbs.map((c, i) => ({
|
||||
'@type': 'ListItem',
|
||||
position: i + 1,
|
||||
name: c.name,
|
||||
item: `${SITE}${c.path}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -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 />
|
||||
+2
@@ -96,6 +96,8 @@
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(data.qaJsonLd)}</script>`}
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(data.breadcrumbJsonLd)}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
|
||||
|
||||
+19
-2
@@ -1,9 +1,13 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { ALEX_PICKS, findArgumentLang, getArchetypes, getArguments } from '$lib/data/apologetik';
|
||||
import { generateContraQaJsonLd } from '$lib/js/apologetikJsonLd';
|
||||
import { generateBreadcrumbJsonLd } from '$lib/js/breadcrumbJsonLd';
|
||||
import { m as faithM, faithSlugFromLang, apologetikSlug, type FaithLang } from '$lib/js/faithI18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { lang } = await parent();
|
||||
const parentData = await parent();
|
||||
const lang = parentData.lang as FaithLang;
|
||||
const [arg, archetypes, args] = await Promise.all([
|
||||
findArgumentLang(params.argId, lang),
|
||||
getArchetypes(lang),
|
||||
@@ -17,11 +21,24 @@ export const load: PageLoad = async ({ params, parent }) => {
|
||||
error(404, 'Voice not found');
|
||||
}
|
||||
const initialArchId = params.archId ?? null;
|
||||
const qaJsonLd = generateContraQaJsonLd(arg, archetypes, lang);
|
||||
const tFaith = faithM[lang];
|
||||
const faithSeg = faithSlugFromLang(lang);
|
||||
const apolSeg = apologetikSlug(lang === 'la' ? 'en' : lang);
|
||||
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
|
||||
{ name: 'Bocken', path: '/' },
|
||||
{ name: tFaith.title, path: `/${faithSeg}` },
|
||||
{ name: tFaith.apologetics, path: `/${faithSeg}/${apolSeg}` },
|
||||
{ name: tFaith.objections, path: `/${faithSeg}/${apolSeg}/contra` },
|
||||
{ name: arg.title, path: `/${faithSeg}/${apolSeg}/contra/${arg.id}` }
|
||||
]);
|
||||
return {
|
||||
argument: arg,
|
||||
archetypes,
|
||||
args,
|
||||
alexPicks: ALEX_PICKS[params.argId] ?? [],
|
||||
initialArchId
|
||||
initialArchId,
|
||||
qaJsonLd,
|
||||
breadcrumbJsonLd
|
||||
};
|
||||
};
|
||||
|
||||
+32
@@ -5,6 +5,8 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { formatLongDate, getMonthName, properLabel, type CalendarLang, m, m1962 } from '../../../../../calendarI18n';
|
||||
import HeroCard from '../../../../../HeroCard.svelte';
|
||||
import { generateBreadcrumbJsonLd } from '$lib/js/breadcrumbJsonLd';
|
||||
import { m as faithM, faithSlugFromLang, calendarSlug } from '$lib/js/faithI18n';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -78,11 +80,41 @@
|
||||
const nextHref = $derived(shiftDay(1));
|
||||
|
||||
const monthTitle = $derived(`${getMonthName(month, lang)} ${year}`);
|
||||
|
||||
const breadcrumbJsonLd = $derived.by(() => {
|
||||
// faith calendar i18n only has 'de' | 'en' | 'la'; CalendarLang has the same keys.
|
||||
const fLang = lang as 'de' | 'en' | 'la';
|
||||
const tFaith = faithM[fLang];
|
||||
const faithSeg = faithSlugFromLang(fLang);
|
||||
const calSeg = calendarSlug(fLang);
|
||||
return generateBreadcrumbJsonLd([
|
||||
{ name: 'Bocken', path: '/' },
|
||||
{ name: tFaith.title, path: `/${faithSeg}` },
|
||||
{ name: tFaith.calendar, path: `/${faithSeg}/${calSeg}` },
|
||||
{ name: monthTitle, path: backHref.split('?')[0] },
|
||||
{ name: `${day.name} — ${formatLongDate(iso, lang)}`, path: page.url.pathname }
|
||||
]);
|
||||
});
|
||||
|
||||
const eventJsonLd = $derived({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Event',
|
||||
name: day.name,
|
||||
startDate: iso,
|
||||
endDate: iso,
|
||||
eventAttendanceMode: 'https://schema.org/MixedEventAttendanceMode',
|
||||
eventStatus: 'https://schema.org/EventScheduled',
|
||||
location: { '@type': 'VirtualLocation', url: `https://bocken.org${page.url.pathname}` },
|
||||
organizer: { '@type': 'Person', name: 'Alexander Bocken' },
|
||||
description: day.name
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{day.name} — {formatLongDate(iso, lang)}</title>
|
||||
<meta name="description" content={day.name} />
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(breadcrumbJsonLd)}</script>`}
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(eventJsonLd)}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<main class="detail-wrap">
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
import ReginaCaeli from "$lib/components/faith/prayers/ReginaCaeli.svelte";
|
||||
import StickyImage from "$lib/components/faith/StickyImage.svelte";
|
||||
import AngelusStreakCounter from "$lib/components/faith/AngelusStreakCounter.svelte";
|
||||
import { m } from '$lib/js/faithI18n';
|
||||
import { m, faithSlugFromLang, prayersSlug } from '$lib/js/faithI18n';
|
||||
import { generateBreadcrumbJsonLd } from '$lib/js/breadcrumbJsonLd';
|
||||
/** @typedef {import('$lib/js/faithI18n').FaithLang} FaithLang */
|
||||
|
||||
let { data } = $props();
|
||||
@@ -99,6 +100,13 @@
|
||||
// Toggle href for no-JS fallback (navigates to opposite latin state)
|
||||
const latinToggleHref = $derived(data.initialLatin ? '?latin=0' : '?');
|
||||
|
||||
const breadcrumbJsonLd = $derived(generateBreadcrumbJsonLd([
|
||||
{ name: 'Bocken', path: '/' },
|
||||
{ name: t.title, path: `/${faithSlugFromLang(lang)}` },
|
||||
{ name: t.prayers, path: `/${faithSlugFromLang(lang)}/${prayersSlug(lang)}` },
|
||||
{ name: prayerName, path: `/${faithSlugFromLang(lang)}/${prayersSlug(lang)}/${data.prayer}` }
|
||||
]));
|
||||
|
||||
onMount(() => {
|
||||
// Clean up URL params after hydration (state is now in component state)
|
||||
if (window.location.search) {
|
||||
@@ -109,6 +117,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{prayerName} - Bocken</title>
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(breadcrumbJsonLd)}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -266,6 +266,7 @@ h2{
|
||||
<meta property="og:image:type" content="image/webp" />
|
||||
<meta property="og:image:alt" content="{data.strippedName}" />
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(data.recipeJsonLd)}</script>`}
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(data.breadcrumbJsonLd)}</script>`}
|
||||
<!-- SEO: hreflang tags -->
|
||||
<link rel="alternate" hreflang="de" href="https://bocken.org/rezepte/{data.germanShortName}" />
|
||||
{#if isEnglish || data.hasEnglishTranslation}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { generateRecipeJsonLd } from '$lib/js/recipeJsonLd';
|
||||
import { generateBreadcrumbJsonLd } from '$lib/js/breadcrumbJsonLd';
|
||||
import { m as recipesM } from '$lib/js/recipesI18n';
|
||||
import { isOffline, canUseOfflineData } from '$lib/offline/helpers';
|
||||
import { getFullRecipe, isOfflineDataAvailable } from '$lib/offline/db';
|
||||
import { stripHtmlTags } from '$lib/js/stripHtmlTags';
|
||||
@@ -174,6 +176,13 @@ export const load: PageLoad = async ({ fetch, params, url, data }) => {
|
||||
|
||||
// Generate JSON-LD server-side
|
||||
const recipeJsonLd = generateRecipeJsonLd(item, undefined, isEnglish ? 'en' : 'de');
|
||||
const tRecipes = recipesM[isEnglish ? 'en' : 'de'];
|
||||
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
|
||||
{ name: 'Bocken', path: '/' },
|
||||
{ name: tRecipes.all_recipes, path: `/${params.recipeLang}` },
|
||||
...(item.category ? [{ name: item.category, path: `/${params.recipeLang}/category/${item.category}` }] : []),
|
||||
{ name: stripHtmlTags(item.name || ''), path: `/${params.recipeLang}/${item.short_name}` },
|
||||
]);
|
||||
|
||||
// For German page: check if English translation exists
|
||||
// For English page: germanShortName is already in item (from API)
|
||||
@@ -194,6 +203,7 @@ export const load: PageLoad = async ({ fetch, params, url, data }) => {
|
||||
isFavorite,
|
||||
multiplier,
|
||||
recipeJsonLd,
|
||||
breadcrumbJsonLd,
|
||||
hasEnglishTranslation,
|
||||
englishShortName,
|
||||
germanShortName,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { ARGUMENTS, POS_ARGUMENTS } from '$lib/data/apologetik';
|
||||
import { validPrayerSlugs } from '$lib/data/prayerSlugs';
|
||||
import { Recipe } from '$models/Recipe';
|
||||
import { dbConnect } from '$utils/db';
|
||||
|
||||
const SITE = 'https://bocken.org';
|
||||
const BUILD_LASTMOD = new Date().toISOString().slice(0, 10);
|
||||
|
||||
type Url = { loc: string; changefreq?: string; priority?: number; alternates?: { hreflang: string; href: string }[] };
|
||||
type Url = { loc: string; lastmod?: string; changefreq?: string; priority?: number; alternates?: { hreflang: string; href: string }[] };
|
||||
|
||||
function xmlEscape(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
@@ -17,6 +20,7 @@ function renderUrl(u: Url): string {
|
||||
const parts = [
|
||||
` <url>`,
|
||||
` <loc>${xmlEscape(u.loc)}</loc>`,
|
||||
u.lastmod ? ` <lastmod>${u.lastmod}</lastmod>` : '',
|
||||
u.changefreq ? ` <changefreq>${u.changefreq}</changefreq>` : '',
|
||||
u.priority !== undefined ? ` <priority>${u.priority.toFixed(1)}</priority>` : '',
|
||||
alt,
|
||||
@@ -29,7 +33,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
const urls: Url[] = [];
|
||||
|
||||
// Home
|
||||
urls.push({ loc: `${SITE}/`, changefreq: 'monthly', priority: 1.0 });
|
||||
urls.push({ loc: `${SITE}/`, lastmod: BUILD_LASTMOD, changefreq: 'monthly', priority: 1.0 });
|
||||
|
||||
// Faith hubs (de/en) — Latin route exists but content sparse, skip.
|
||||
for (const [de, en] of [
|
||||
@@ -40,6 +44,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
]) {
|
||||
urls.push({
|
||||
loc: `${SITE}${de}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: [
|
||||
@@ -50,6 +55,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
});
|
||||
urls.push({
|
||||
loc: `${SITE}${en}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: [
|
||||
@@ -67,6 +73,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
]) {
|
||||
urls.push({
|
||||
loc: `${SITE}${de}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'weekly',
|
||||
priority: 0.8,
|
||||
alternates: [
|
||||
@@ -77,6 +84,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
});
|
||||
urls.push({
|
||||
loc: `${SITE}${en}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'weekly',
|
||||
priority: 0.8,
|
||||
alternates: [
|
||||
@@ -91,6 +99,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
for (const [de, en] of [['/glaube/apologetik', '/faith/apologetics']]) {
|
||||
urls.push({
|
||||
loc: `${SITE}${de}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: [
|
||||
@@ -101,6 +110,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
});
|
||||
urls.push({
|
||||
loc: `${SITE}${en}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
alternates: [
|
||||
@@ -117,6 +127,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
const enPath = `/faith/apologetics/contra/${arg.id}`;
|
||||
urls.push({
|
||||
loc: `${SITE}${dePath}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'yearly',
|
||||
priority: 0.6,
|
||||
alternates: [
|
||||
@@ -127,6 +138,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
});
|
||||
urls.push({
|
||||
loc: `${SITE}${enPath}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'yearly',
|
||||
priority: 0.6,
|
||||
alternates: [
|
||||
@@ -143,6 +155,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
const enPath = `/faith/apologetics/pro/${arg.id}`;
|
||||
urls.push({
|
||||
loc: `${SITE}${dePath}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'yearly',
|
||||
priority: 0.6,
|
||||
alternates: [
|
||||
@@ -153,6 +166,7 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
});
|
||||
urls.push({
|
||||
loc: `${SITE}${enPath}`,
|
||||
lastmod: BUILD_LASTMOD,
|
||||
changefreq: 'yearly',
|
||||
priority: 0.6,
|
||||
alternates: [
|
||||
@@ -165,30 +179,41 @@ export const GET: RequestHandler = async ({ fetch, setHeaders }) => {
|
||||
|
||||
// Prayers — slugs include both de+en variants in one set; emit each as its own URL.
|
||||
for (const slug of validPrayerSlugs) {
|
||||
urls.push({ loc: `${SITE}/glaube/gebete/${slug}`, changefreq: 'yearly', priority: 0.5 });
|
||||
urls.push({ loc: `${SITE}/faith/prayers/${slug}`, changefreq: 'yearly', priority: 0.5 });
|
||||
urls.push({ loc: `${SITE}/glaube/gebete/${slug}`, lastmod: BUILD_LASTMOD, changefreq: 'yearly', priority: 0.5 });
|
||||
urls.push({ loc: `${SITE}/faith/prayers/${slug}`, lastmod: BUILD_LASTMOD, changefreq: 'yearly', priority: 0.5 });
|
||||
}
|
||||
|
||||
// Recipes — fetch from internal API; tolerate failure so sitemap still ships static URLs.
|
||||
// Recipes — direct DB read so we get dateModified for <lastmod>. Tolerate
|
||||
// failure so sitemap still ships the static URLs above.
|
||||
try {
|
||||
const [deRes, enRes] = await Promise.all([
|
||||
fetch('/api/rezepte/items/all_brief'),
|
||||
fetch('/api/recipes/items/all_brief'),
|
||||
]);
|
||||
if (deRes.ok) {
|
||||
const items: Array<{ short_name?: string }> = await deRes.json();
|
||||
for (const r of items) {
|
||||
if (r.short_name) urls.push({ loc: `${SITE}/rezepte/${encodeURIComponent(r.short_name)}`, changefreq: 'monthly', priority: 0.7 });
|
||||
await dbConnect();
|
||||
const recipes = await Recipe.find(
|
||||
{},
|
||||
'short_name dateModified translations.en.short_name translations.en.translationStatus'
|
||||
).lean();
|
||||
for (const r of recipes) {
|
||||
const lastmod = r.dateModified ? new Date(r.dateModified).toISOString().slice(0, 10) : BUILD_LASTMOD;
|
||||
if (r.short_name) {
|
||||
urls.push({
|
||||
loc: `${SITE}/rezepte/${encodeURIComponent(r.short_name)}`,
|
||||
lastmod,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (enRes.ok) {
|
||||
const items: Array<{ short_name?: string }> = await enRes.json();
|
||||
for (const r of items) {
|
||||
if (r.short_name) urls.push({ loc: `${SITE}/recipes/${encodeURIComponent(r.short_name)}`, changefreq: 'monthly', priority: 0.7 });
|
||||
const enShort = r.translations?.en?.short_name;
|
||||
const approved = r.translations?.en?.translationStatus === 'approved';
|
||||
if (enShort && approved) {
|
||||
urls.push({
|
||||
loc: `${SITE}/recipes/${encodeURIComponent(enShort)}`,
|
||||
lastmod,
|
||||
changefreq: 'monthly',
|
||||
priority: 0.7,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[sitemap] recipe fetch failed:', e);
|
||||
console.error('[sitemap] recipe DB query failed:', e);
|
||||
}
|
||||
|
||||
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
Reference in New Issue
Block a user