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: 'title', label: 'Überschrift', type: 'text' },
|
||||
{ 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: '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": {
|
||||
"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": {
|
||||
"logo": "/logo.svg",
|
||||
|
|
|
|||
21
nginx.conf
21
nginx.conf
|
|
@ -8,14 +8,31 @@ server {
|
|||
# (so steht es auch in der Datenschutzerklaerung). Fehler weiter melden.
|
||||
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 / {
|
||||
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
|
||||
location /_astro/ {
|
||||
expires 1y;
|
||||
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 {
|
||||
title: 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 ogImage = new URL('/og-larpaso.jpg', Astro.site).href;
|
||||
const orgId = new URL('/#larpaso', Astro.site).href;
|
||||
const orgData = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfessionalService',
|
||||
'@id': orgId,
|
||||
name: 'Larpaso',
|
||||
alternateName: 'larpaso Webdesign',
|
||||
description: site.meta.description,
|
||||
url: 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' },
|
||||
areaServed: { '@type': 'Country', name: 'Deutschland' },
|
||||
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>
|
||||
|
|
@ -41,6 +68,7 @@ const orgData = {
|
|||
<meta name="theme-color" content="#FCFAF6" />
|
||||
<meta name="description" content={description} />
|
||||
<title>{title} · Larpaso</title>
|
||||
{noindex && <meta name="robots" content="noindex" />}
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- Vorschau beim Teilen (WhatsApp, Signal, Facebook, LinkedIn, Mail) -->
|
||||
|
|
@ -60,7 +88,7 @@ const orgData = {
|
|||
<style set:html={themeCss(theme)} />
|
||||
|
||||
<!-- 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>
|
||||
<body>
|
||||
<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.]';
|
||||
---
|
||||
|
||||
<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">
|
||||
<div class="container legal">
|
||||
<p class="eyebrow">Datenschutz</p>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { site, richWithEmail } from '../lib/content';
|
|||
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">
|
||||
<div class="container legal">
|
||||
<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">
|
||||
{impressum.anbieterAngaben ||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,22 @@ const zoomFor = (item?: MappeItem) =>
|
|||
: undefined;
|
||||
const findItem = (src: string) =>
|
||||
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 -->
|
||||
<section class="section hero">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="contact-detail">
|
||||
<p class="contact-label">Wie wir arbeiten</p>
|
||||
<p class={kontakt.howWeWork ? undefined : 'placeholder'}>
|
||||
{kontakt.howWeWork || '[Wie wir arbeiten — noch zu ergänzen]'}
|
||||
</p>
|
||||
</div>
|
||||
{kontakt.howWeWork && (
|
||||
<div class="contact-detail">
|
||||
<p class="contact-label">Wie wir arbeiten</p>
|
||||
<p>{kontakt.howWeWork}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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