feat(seo): sitemap, OG/canonical/hreflang, JSON-LD i18n
Add sitemap.xml route enumerating recipes, apologetik args, prayers, and faith hubs. Drop /static/ from robots.txt — was blocking JSON-LD recipe images from Google. Add reusable Seo component (OG/Twitter/canonical) and wire into homepage, faith hub, recipes hub, and apologetik index. Faith and recipe layouts now emit canonical + hreflang automatically by swapping known lang slugs; deeper paths whose inner segments aren't safely translatable (recipe [name], prayer [prayer], apologetik [argId]) are skipped at the layout and may opt-in per page. Recipe JSON-LD HowToStep names and baking instructions now resolve via the recipes i18n table (jsonld_step / jsonld_bake / jsonld_for_duration + existing at_temp) instead of being hardcoded German — English /recipes/ pages were emitting "Schritt N" in their schema.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
type Alternate = { hreflang: string; href: string };
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
ogImage?: string;
|
||||
ogImageAlt?: string;
|
||||
ogType?: 'website' | 'article';
|
||||
siteName?: string;
|
||||
lang?: 'de' | 'en' | 'la';
|
||||
alternates?: Alternate[];
|
||||
twitterCard?: 'summary' | 'summary_large_image';
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
canonical,
|
||||
ogImage,
|
||||
ogImageAlt,
|
||||
ogType = 'website',
|
||||
siteName = 'Bocken',
|
||||
lang,
|
||||
alternates = [],
|
||||
twitterCard = 'summary_large_image',
|
||||
}: Props = $props();
|
||||
|
||||
const localeMap = { de: 'de_DE', en: 'en_US', la: 'la' } as const;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
{#if description}<meta name="description" content={description} />{/if}
|
||||
{#if canonical}<link rel="canonical" href={canonical} />{/if}
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
{#if description}<meta property="og:description" content={description} />{/if}
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:site_name" content={siteName} />
|
||||
{#if canonical}<meta property="og:url" content={canonical} />{/if}
|
||||
{#if lang}<meta property="og:locale" content={localeMap[lang]} />{/if}
|
||||
{#if ogImage}
|
||||
<meta property="og:image" content={ogImage} />
|
||||
{#if ogImageAlt}<meta property="og:image:alt" content={ogImageAlt} />{/if}
|
||||
{/if}
|
||||
|
||||
<meta name="twitter:card" content={twitterCard} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
{#if description}<meta name="twitter:description" content={description} />{/if}
|
||||
{#if ogImage}<meta name="twitter:image" content={ogImage} />{/if}
|
||||
|
||||
{#each alternates as a (a.hreflang)}
|
||||
<link rel="alternate" hreflang={a.hreflang} href={a.href} />
|
||||
{/each}
|
||||
</svelte:head>
|
||||
@@ -181,6 +181,11 @@ export const de = {
|
||||
instructions_label: 'Zubereitung',
|
||||
at_temp: 'bei',
|
||||
|
||||
// JSON-LD recipe schema labels (HowToStep names, baking instruction text)
|
||||
jsonld_step: 'Schritt',
|
||||
jsonld_bake: 'Backen',
|
||||
jsonld_for_duration: 'für',
|
||||
|
||||
// CreateStepList baking
|
||||
not_set: 'Nicht gesetzt',
|
||||
duration: 'Dauer',
|
||||
|
||||
@@ -181,6 +181,11 @@ export const en = {
|
||||
instructions_label: 'Instructions',
|
||||
at_temp: 'at',
|
||||
|
||||
// JSON-LD recipe schema labels (HowToStep names, baking instruction text)
|
||||
jsonld_step: 'Step',
|
||||
jsonld_bake: 'Bake',
|
||||
jsonld_for_duration: 'for',
|
||||
|
||||
// CreateStepList baking
|
||||
not_set: 'Not set',
|
||||
duration: 'Duration',
|
||||
|
||||
@@ -27,6 +27,7 @@ function parseTimeToISO8601(timeString: string | undefined): string | undefined
|
||||
}
|
||||
|
||||
import type { RecipeModelType, NutritionMapping } from '$types/types';
|
||||
import { m, type RecipesLang } from '$lib/js/recipesI18n';
|
||||
|
||||
interface HowToStep {
|
||||
"@type": "HowToStep";
|
||||
@@ -62,7 +63,8 @@ type ReferencedNutrition = {
|
||||
baseMultiplier: number;
|
||||
};
|
||||
|
||||
export function generateRecipeJsonLd(data: RecipeModelType, referencedNutrition?: ReferencedNutrition[]) {
|
||||
export function generateRecipeJsonLd(data: RecipeModelType, referencedNutrition?: ReferencedNutrition[], lang: RecipesLang = 'de') {
|
||||
const t = m[lang];
|
||||
const jsonLd: RecipeJsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Recipe",
|
||||
@@ -126,7 +128,7 @@ export function generateRecipeJsonLd(data: RecipeModelType, referencedNutrition?
|
||||
for (let i = 0; i < instructionGroup.steps.length; i++) {
|
||||
jsonLd.recipeInstructions.push({
|
||||
"@type": "HowToStep",
|
||||
"name": `Schritt ${i + 1}`,
|
||||
"name": `${t.jsonld_step} ${i + 1}`,
|
||||
"text": instructionGroup.steps[i]
|
||||
});
|
||||
}
|
||||
@@ -137,16 +139,16 @@ export function generateRecipeJsonLd(data: RecipeModelType, referencedNutrition?
|
||||
// Add baking instructions if available
|
||||
if (data.baking?.temperature || data.baking?.length) {
|
||||
const bakingText = [
|
||||
data.baking.temperature ? `bei ${data.baking.temperature}` : '',
|
||||
data.baking.length ? `für ${data.baking.length}` : '',
|
||||
data.baking.temperature ? `${t.at_temp} ${data.baking.temperature}` : '',
|
||||
data.baking.length ? `${t.jsonld_for_duration} ${data.baking.length}` : '',
|
||||
data.baking.mode || ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
|
||||
if (bakingText) {
|
||||
jsonLd.recipeInstructions.push({
|
||||
"@type": "HowToStep",
|
||||
"name": "Backen",
|
||||
"text": `Backen ${bakingText}`
|
||||
"name": t.jsonld_bake,
|
||||
"text": `${t.jsonld_bake} ${bakingText}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user