feat(seo): add sitemap, canonical URLs, structured data, and OG/Twitter tags

- Add dynamic sitemap.xml with static pages and market entries
- Add canonical URLs and base OG/Twitter meta tags in root layout
- Add JSON-LD WebSite schema with SearchAction on home page
- Add JSON-LD Event schema on market detail pages
- Add twitter:card summary_large_image for markets with images
- Add OG tags to impressum and datenschutz pages
- Add font preloading for critical rendering path fonts
- Add Sitemap directive to robots.txt
This commit is contained in:
2026-02-22 11:45:27 +01:00
parent 2c462144a7
commit df93f83fcd
8 changed files with 126 additions and 0 deletions

View File

@@ -9,6 +9,8 @@
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
<meta name="theme-color" content="#1a3d24" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0f2818" media="(prefers-color-scheme: dark)" />
<link rel="preload" href="%sveltekit.assets%/fonts/crimsonpro-400.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="%sveltekit.assets%/fonts/medievalsharp-400.woff2" as="font" type="font/woff2" crossorigin />
<script>
(function () {
var t = localStorage.getItem('marktvogt-theme');

View File

@@ -2,6 +2,7 @@
import '../app.css';
import Header from '$lib/components/layout/Header.svelte';
import Footer from '$lib/components/layout/Footer.svelte';
import { page } from '$app/stores';
import type { Snippet } from 'svelte';
interface Props {
@@ -10,11 +11,18 @@
}
let { data, children }: Props = $props();
const canonicalUrl = $derived(`https://marktvogt.de${$page.url.pathname}`);
</script>
<svelte:head>
<title>Marktvogt - Mittelaltermärkte finden</title>
<meta name="description" content="Finde Mittelaltermärkte, Ritterturniere und historische Feste in deiner Nähe." />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:site_name" content="Marktvogt" />
<meta property="og:locale" content="de_DE" />
<meta property="og:url" content={canonicalUrl} />
<meta name="twitter:card" content="summary" />
</svelte:head>
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>

View File

@@ -11,6 +11,21 @@
<svelte:head>
<title>Marktvogt - Mittelaltermärkte finden</title>
<meta property="og:title" content="Marktvogt - Mittelaltermärkte finden" />
<meta property="og:description" content="Finde Mittelaltermärkte, Ritterturniere und historische Feste in deiner Nähe. Suche nach Ort, Datum oder Stichwort." />
<meta property="og:type" content="website" />
{@html `<script type="application/ld+json">${JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Marktvogt",
"url": "https://marktvogt.de",
"description": "Verzeichnis für Mittelaltermärkte, Ritterturniere und historische Feste in Deutschland",
"potentialAction": {
"@type": "SearchAction",
"target": "https://marktvogt.de/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
})}</script>`}
</svelte:head>
<div class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">

View File

@@ -1,6 +1,9 @@
<svelte:head>
<title>Datenschutzerklärung - Marktvogt</title>
<meta name="description" content="Datenschutzerklärung für Marktvogt Informationen zur Verarbeitung personenbezogener Daten." />
<meta property="og:title" content="Datenschutzerklärung - Marktvogt" />
<meta property="og:description" content="Datenschutzerklärung für Marktvogt Informationen zur Verarbeitung personenbezogener Daten." />
<meta property="og:type" content="website" />
</svelte:head>
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">

View File

@@ -1,6 +1,9 @@
<svelte:head>
<title>Impressum - Marktvogt</title>
<meta name="description" content="Impressum und Angaben gemäß § 5 TMG für Marktvogt." />
<meta property="og:title" content="Impressum - Marktvogt" />
<meta property="og:description" content="Impressum und Angaben gemäß § 5 TMG für Marktvogt." />
<meta property="og:type" content="website" />
</svelte:head>
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">

View File

@@ -36,8 +36,46 @@
<meta property="og:description" content="{market.description?.slice(0, 200)}" />
{#if market.image_url}
<meta property="og:image" content={market.image_url} />
<meta name="twitter:card" content="summary_large_image" />
{/if}
<meta property="og:type" content="event" />
{@html `<script type="application/ld+json">${JSON.stringify({
"@context": "https://schema.org",
"@type": "Event",
"name": market.name,
"startDate": market.start_date,
"endDate": market.end_date,
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
...(market.description ? { "description": market.description.slice(0, 500) } : {}),
...(market.image_url ? { "image": market.image_url } : {}),
...(market.website ? { "url": market.website } : {}),
...(market.organizer_name ? { "organizer": { "@type": "Organization", "name": market.organizer_name } } : {}),
"location": {
"@type": "Place",
"name": market.name,
"address": {
"@type": "PostalAddress",
"streetAddress": market.street,
"addressLocality": market.city,
"postalCode": market.zip,
"addressRegion": market.state,
"addressCountry": market.country
},
"geo": {
"@type": "GeoCoordinates",
"latitude": market.latitude,
"longitude": market.longitude
}
},
...(admission && admission.adult_cents > 0 ? { "offers": {
"@type": "Offer",
"price": (admission.adult_cents / 100).toFixed(2),
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"validFrom": market.start_date
}} : {})
})}</script>`}
</svelte:head>
<div class="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8">

View File

@@ -0,0 +1,55 @@
import type { RequestHandler } from './$types.js';
import { apiFetch } from '$lib/api/client.js';
import type { MarketSummary } from '$lib/api/types.js';
const ORIGIN = 'https://marktvogt.de';
function escapeXml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export const GET: RequestHandler = async ({ fetch }) => {
const staticPages = [
{ path: '/', priority: '1.0', changefreq: 'daily' },
{ path: '/impressum', priority: '0.2', changefreq: 'yearly' },
{ path: '/datenschutz', priority: '0.2', changefreq: 'yearly' }
];
let markets: MarketSummary[] = [];
try {
const res = await apiFetch<MarketSummary[]>('/markets?per_page=1000', { fetch });
markets = res.data;
} catch {
// Backend unreachable — return static pages only
}
const urls = staticPages
.map(
(p) => ` <url>
<loc>${ORIGIN}${p.path}</loc>
<changefreq>${p.changefreq}</changefreq>
<priority>${p.priority}</priority>
</url>`
)
.concat(
markets.map(
(m) => ` <url>
<loc>${ORIGIN}/markt/${escapeXml(m.slug)}</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>`
)
);
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.join('\n')}
</urlset>`;
return new Response(xml, {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=3600'
}
});
};

View File

@@ -1,3 +1,5 @@
# allow crawling everything by default
User-agent: *
Disallow:
Sitemap: https://marktvogt.de/sitemap.xml