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:
@@ -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}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user