SEO groundwork; hide the empty 'Wie wir arbeiten' block
- robots.txt and sitemap.xml (both answered with the homepage before) - real 404 page; nginx returns 404 instead of the homepage for unknown paths - gzip and caching for fonts, images, videos - JSON-LD as one graph: ProfessionalService (+founders, offer), WebSite, FAQPage built from the FAQ answers - sharper title and a meta description under 160 characters, real descriptions for imprint and privacy pages - imprint cites § 5 DDG (TMG was replaced in May 2024) - empty kontakt.howWeWork hides the block instead of a red placeholder Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d035bbcd61
commit
275e479aba
10 changed files with 109 additions and 18 deletions
|
|
@ -240,10 +240,10 @@ module.exports = [
|
||||||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||||||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||||||
{ key: 'lead', label: 'Einleitungstext', type: 'richtext' },
|
{ key: 'lead', label: 'Einleitungstext', type: 'richtext' },
|
||||||
{ key: 'phone', label: 'Telefonnummer (leer = Platzhalter)', type: 'text' },
|
{ key: 'phone', label: 'Telefonnummer (leer = ausgeblendet, auch im Impressum)', type: 'text' },
|
||||||
{ key: 'hours', label: 'Telefonische Erreichbarkeit unter der Nummer (leer = ausgeblendet)', type: 'text' },
|
{ key: 'hours', label: 'Telefonische Erreichbarkeit unter der Nummer (leer = ausgeblendet)', type: 'text' },
|
||||||
{ key: 'email', label: 'E-Mail-Adresse', type: 'email' },
|
{ key: 'email', label: 'E-Mail-Adresse', type: 'email' },
|
||||||
{ key: 'howWeWork', label: '"Wie wir arbeiten" (leer = Platzhalter)', type: 'textarea' },
|
{ key: 'howWeWork', label: '"Wie wir arbeiten" (leer = ausgeblendet)', type: 'textarea' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"description": "Larpaso aus Hannover baut Websites für Inhaberbetriebe, Praxen und Handwerk: Angebot, Preise, Zeiten und Anfahrt auf einen Blick, gut bedienbar auf dem Handy, zum Festpreis."
|
"description": "Webdesign aus Hannover: Larpaso baut Websites für Inhaberbetriebe, Praxen und Handwerk in ganz Deutschland. Individuell, handyfreundlich, zum Festpreis."
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"logo": "/logo.svg",
|
"logo": "/logo.svg",
|
||||||
|
|
|
||||||
21
nginx.conf
21
nginx.conf
|
|
@ -8,14 +8,31 @@ server {
|
||||||
# (so steht es auch in der Datenschutzerklaerung). Fehler weiter melden.
|
# (so steht es auch in der Datenschutzerklaerung). Fehler weiter melden.
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|
||||||
# Saubere URLs + Fallback
|
# Komprimieren: HTML/CSS/JS/SVG kommen deutlich kleiner beim Besucher an
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 512;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types text/plain text/css text/xml application/xml application/javascript
|
||||||
|
application/json application/ld+json image/svg+xml;
|
||||||
|
|
||||||
|
# Saubere URLs. Unbekannte Adressen bekommen eine echte 404 statt der
|
||||||
|
# Startseite (sonst wertet Google sie als "Soft 404" / doppelte Inhalte).
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ $uri.html /index.html;
|
try_files $uri $uri/ $uri.html =404;
|
||||||
}
|
}
|
||||||
|
error_page 404 /404.html;
|
||||||
|
|
||||||
# Astro legt gehashte Assets unter /_astro ab -> lange cachen
|
# Astro legt gehashte Assets unter /_astro ab -> lange cachen
|
||||||
location /_astro/ {
|
location /_astro/ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Schriften, Bilder, Videos: aendern sich selten
|
||||||
|
location ~* \.(woff2|webp|jpg|jpeg|png|svg|ico|mp4)$ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
public/robots.txt
Normal file
4
public/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://larpaso.de/sitemap.xml
|
||||||
|
|
@ -7,15 +7,21 @@ import { site, theme, themeCss, fontFacesCss } from '../lib/content';
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** Zusätzliche schema.org-Objekte nur für diese Seite (z. B. FAQPage). */
|
||||||
|
schema?: Record<string, unknown>[];
|
||||||
|
/** Seite nicht in Suchmaschinen aufnehmen (404). */
|
||||||
|
noindex?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description = site.meta.description } = Astro.props;
|
const { title, description = site.meta.description, schema = [], noindex = false } = Astro.props;
|
||||||
const canonical = new URL(Astro.url.pathname, Astro.site).href;
|
const canonical = new URL(Astro.url.pathname, Astro.site).href;
|
||||||
const ogImage = new URL('/og-larpaso.jpg', Astro.site).href;
|
const ogImage = new URL('/og-larpaso.jpg', Astro.site).href;
|
||||||
|
const orgId = new URL('/#larpaso', Astro.site).href;
|
||||||
const orgData = {
|
const orgData = {
|
||||||
'@context': 'https://schema.org',
|
|
||||||
'@type': 'ProfessionalService',
|
'@type': 'ProfessionalService',
|
||||||
|
'@id': orgId,
|
||||||
name: 'Larpaso',
|
name: 'Larpaso',
|
||||||
|
alternateName: 'larpaso Webdesign',
|
||||||
description: site.meta.description,
|
description: site.meta.description,
|
||||||
url: Astro.site?.href,
|
url: Astro.site?.href,
|
||||||
logo: new URL('/icon-512.png', Astro.site).href,
|
logo: new URL('/icon-512.png', Astro.site).href,
|
||||||
|
|
@ -25,7 +31,28 @@ const orgData = {
|
||||||
address: { '@type': 'PostalAddress', addressLocality: 'Hannover', addressRegion: 'Niedersachsen', addressCountry: 'DE' },
|
address: { '@type': 'PostalAddress', addressLocality: 'Hannover', addressRegion: 'Niedersachsen', addressCountry: 'DE' },
|
||||||
areaServed: { '@type': 'Country', name: 'Deutschland' },
|
areaServed: { '@type': 'Country', name: 'Deutschland' },
|
||||||
priceRange: 'ab 500 €',
|
priceRange: 'ab 500 €',
|
||||||
|
founder: [
|
||||||
|
{ '@type': 'Person', name: `Lara ${site.footer.laraNachname}` },
|
||||||
|
{ '@type': 'Person', name: `Pascal ${site.footer.pascalNachname}` },
|
||||||
|
],
|
||||||
|
knowsAbout: ['Webdesign', 'Websites für kleine Betriebe', 'Responsive Webdesign', 'Suchmaschinenoptimierung', 'Barrierefreiheit'],
|
||||||
|
makesOffer: {
|
||||||
|
'@type': 'Offer',
|
||||||
|
name: 'Website zum Festpreis',
|
||||||
|
price: '500',
|
||||||
|
priceCurrency: 'EUR',
|
||||||
|
description: 'Individuelle Website mit Texten, Gestaltung und Veröffentlichung, einmaliger Festpreis.',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
const siteData = {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': new URL('/#website', Astro.site).href,
|
||||||
|
name: 'Larpaso',
|
||||||
|
url: Astro.site?.href,
|
||||||
|
inLanguage: 'de-DE',
|
||||||
|
publisher: { '@id': orgId },
|
||||||
|
};
|
||||||
|
const ldJson = { '@context': 'https://schema.org', '@graph': [orgData, siteData, ...schema] };
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -41,6 +68,7 @@ const orgData = {
|
||||||
<meta name="theme-color" content="#FCFAF6" />
|
<meta name="theme-color" content="#FCFAF6" />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<title>{title} · Larpaso</title>
|
<title>{title} · Larpaso</title>
|
||||||
|
{noindex && <meta name="robots" content="noindex" />}
|
||||||
<link rel="canonical" href={canonical} />
|
<link rel="canonical" href={canonical} />
|
||||||
|
|
||||||
<!-- Vorschau beim Teilen (WhatsApp, Signal, Facebook, LinkedIn, Mail) -->
|
<!-- Vorschau beim Teilen (WhatsApp, Signal, Facebook, LinkedIn, Mail) -->
|
||||||
|
|
@ -60,7 +88,7 @@ const orgData = {
|
||||||
<style set:html={themeCss(theme)} />
|
<style set:html={themeCss(theme)} />
|
||||||
|
|
||||||
<!-- Für Suchmaschinen: wer, was, Sitz Hannover (Kunden überall). Adresse bewusst ohne Straße. -->
|
<!-- Für Suchmaschinen: wer, was, Sitz Hannover (Kunden überall). Adresse bewusst ohne Straße. -->
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(orgData)} />
|
<script type="application/ld+json" set:html={JSON.stringify(ldJson)} />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>
|
<a href="#main-content" class="skip-link">Zum Inhalt springen</a>
|
||||||
|
|
|
||||||
14
src/pages/404.astro
Normal file
14
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Seite nicht gefunden" description="Diese Seite gibt es bei Larpaso nicht (mehr)." noindex>
|
||||||
|
<section class="section-tight">
|
||||||
|
<div class="container legal">
|
||||||
|
<p class="eyebrow">Fehler 404</p>
|
||||||
|
<h1>Diese Seite gibt es nicht.</h1>
|
||||||
|
<p>Vielleicht hat sich die Adresse geändert. Auf der Startseite finden Sie alles über uns, unsere Arbeiten und wie Sie uns erreichen.</p>
|
||||||
|
<p><a class="btn btn-primary" href="/">Zur Startseite</a></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
|
@ -7,7 +7,7 @@ const introFallback =
|
||||||
'[Diese Seite ist noch ein Platzhalter. Vor dem Livegang durch eine vollständige, rechtlich geprüfte Datenschutzerklärung ersetzen — u. a. mit Angaben zu: verantwortlicher Stelle, Kontaktformular/Mailto-Versand, ggf. eingesetzten Diensten (Hosting, Schriftarten, Analyse-Tools), Speicherdauer und Betroffenenrechten.]';
|
'[Diese Seite ist noch ein Platzhalter. Vor dem Livegang durch eine vollständige, rechtlich geprüfte Datenschutzerklärung ersetzen — u. a. mit Angaben zu: verantwortlicher Stelle, Kontaktformular/Mailto-Versand, ggf. eingesetzten Diensten (Hosting, Schriftarten, Analyse-Tools), Speicherdauer und Betroffenenrechten.]';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Datenschutz" description="Datenschutzerklärung von larpaso.">
|
<Layout title="Datenschutz" description="Datenschutzerklärung von Larpaso: welche Daten die Website verarbeitet, wo sie gehostet wird und welche Rechte Sie haben.">
|
||||||
<section class="section-tight">
|
<section class="section-tight">
|
||||||
<div class="container legal">
|
<div class="container legal">
|
||||||
<p class="eyebrow">Datenschutz</p>
|
<p class="eyebrow">Datenschutz</p>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import { site, richWithEmail } from '../lib/content';
|
||||||
const { impressum, kontakt } = site;
|
const { impressum, kontakt } = site;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Impressum" description="Impressum von larpaso.">
|
<Layout title="Impressum" description="Impressum von Larpaso, Webdesign aus Hannover: Anbieter, Kontakt und rechtliche Angaben.">
|
||||||
<section class="section-tight">
|
<section class="section-tight">
|
||||||
<div class="container legal">
|
<div class="container legal">
|
||||||
<p class="eyebrow">Impressum</p>
|
<p class="eyebrow">Impressum</p>
|
||||||
<h1>Angaben gemäß § 5 TMG</h1>
|
<h1>Angaben gemäß § 5 DDG</h1>
|
||||||
|
|
||||||
<p class={impressum.anbieterAngaben ? undefined : 'placeholder'} style="white-space: pre-line">
|
<p class={impressum.anbieterAngaben ? undefined : 'placeholder'} style="white-space: pre-line">
|
||||||
{impressum.anbieterAngaben ||
|
{impressum.anbieterAngaben ||
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,22 @@ const zoomFor = (item?: MappeItem) =>
|
||||||
: undefined;
|
: undefined;
|
||||||
const findItem = (src: string) =>
|
const findItem = (src: string) =>
|
||||||
mappe.items.find((i) => src && (i.image === src || i.mobileImage === src));
|
mappe.items.find((i) => src && (i.image === src || i.mobileImage === src));
|
||||||
|
|
||||||
|
// FAQ für Google als strukturierte Daten (nur beantwortete Fragen)
|
||||||
|
const plain = (html: string) => html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
|
||||||
|
const faqSchema = {
|
||||||
|
'@type': 'FAQPage',
|
||||||
|
mainEntity: faq.items
|
||||||
|
.filter((i) => i.question && i.answer)
|
||||||
|
.map((i) => ({
|
||||||
|
'@type': 'Question',
|
||||||
|
name: plain(i.question),
|
||||||
|
acceptedAnswer: { '@type': 'Answer', text: plain(i.answer) },
|
||||||
|
})),
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Webdesign aus Hannover">
|
<Layout title="Webdesign aus Hannover · Websites zum Festpreis" schema={[faqSchema]}>
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<section class="section hero">
|
<section class="section hero">
|
||||||
<div class="container hero-grid">
|
<div class="container hero-grid">
|
||||||
|
|
@ -291,12 +304,12 @@ const findItem = (src: string) =>
|
||||||
<p class="contact-hint">Jederzeit per E-Mail erreichbar, Antwort in der Regel am selben Tag.</p>
|
<p class="contact-hint">Jederzeit per E-Mail erreichbar, Antwort in der Regel am selben Tag.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="contact-detail">
|
{kontakt.howWeWork && (
|
||||||
<p class="contact-label">Wie wir arbeiten</p>
|
<div class="contact-detail">
|
||||||
<p class={kontakt.howWeWork ? undefined : 'placeholder'}>
|
<p class="contact-label">Wie wir arbeiten</p>
|
||||||
{kontakt.howWeWork || '[Wie wir arbeiten — noch zu ergänzen]'}
|
<p>{kontakt.howWeWork}</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="contact-form card" aria-label="Kontaktformular" id="contact-form">
|
<form class="contact-form card" aria-label="Kontaktformular" id="contact-form">
|
||||||
|
|
|
||||||
15
src/pages/sitemap.xml.ts
Normal file
15
src/pages/sitemap.xml.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
// Nur echte Seiten. /leistungen, /ueber-uns, /kontakt sind Weiterleitungen und gehören nicht hinein.
|
||||||
|
const seiten = ['/', '/impressum/', '/datenschutz/'];
|
||||||
|
|
||||||
|
export const GET: APIRoute = ({ site }) => {
|
||||||
|
const heute = new Date().toISOString().slice(0, 10);
|
||||||
|
const urls = seiten
|
||||||
|
.map((p) => ` <url><loc>${new URL(p, site).href}</loc><lastmod>${heute}</lastmod></url>`)
|
||||||
|
.join('\n');
|
||||||
|
return new Response(
|
||||||
|
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`,
|
||||||
|
{ headers: { 'Content-Type': 'application/xml; charset=utf-8' } },
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue