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:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
55
web/src/routes/sitemap.xml/+server.ts
Normal file
55
web/src/routes/sitemap.xml/+server.ts
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
||||
Sitemap: https://marktvogt.de/sitemap.xml
|
||||
|
||||
Reference in New Issue
Block a user