From abe73bb9ac8a64ae9c565025cd5c693703040a67 Mon Sep 17 00:00:00 2001 From: "t.indorf" Date: Tue, 13 Oct 2026 09:23:46 +0200 Subject: [PATCH] Update public feature details and align SEO metadata --- docs/FUNKTIONSUEBERSICHT.md | 18 ++ package.json | 2 + src/app/datenschutz/page.tsx | 33 ++-- src/app/features/[slug]/page.tsx | 59 +++--- src/app/impressum/page.tsx | 19 +- src/app/layout.tsx | 52 +++-- src/app/page.tsx | 190 +++++++++++-------- src/app/robots.ts | 18 +- src/app/sitemap.ts | 27 +-- src/components/marketing/structured-data.tsx | 13 ++ src/lib/marketing/features.ts | 138 ++++++++++---- src/lib/marketing/seo.ts | 166 ++++++++++++++++ src/proxy.ts | 5 +- tests/marketing-seo-http.cjs | 57 ++++++ tests/marketing-seo.test.cjs | 120 ++++++++++++ 15 files changed, 683 insertions(+), 234 deletions(-) create mode 100644 src/components/marketing/structured-data.tsx create mode 100644 src/lib/marketing/seo.ts create mode 100644 tests/marketing-seo-http.cjs create mode 100644 tests/marketing-seo.test.cjs diff --git a/docs/FUNKTIONSUEBERSICHT.md b/docs/FUNKTIONSUEBERSICHT.md index 0947860..e6ffa64 100644 --- a/docs/FUNKTIONSUEBERSICHT.md +++ b/docs/FUNKTIONSUEBERSICHT.md @@ -1,5 +1,23 @@ # Kita-Planer – Funktionsübersicht und aktueller Stand +## Ergänzung vom 13.10.2026: Landingpage und SEO + +- Die öffentliche Landingpage beschreibt jetzt sechs Bereiche, einschließlich Personal/Arbeitszeit mit eigener Detailseite `/features/personal-arbeitszeit`. Rollenvergabe, Wiederherstellung des Kontozugangs, Kalender-Rückmeldungen, Mitbringkorrekturen, zukünftige Abwesenheitsänderungen, Notdienst-Erinnerungen/Veröffentlichung und Mitteilungskorrekturen samt Versandübersicht sind in den passenden Texten ergänzt. Die Darstellung behält das vorhandene Design bei und ist auf Desktop sowie bei 390 und 360 Pixeln geprüft; der mobile Logo-Umbruch wurde behoben. +- Veraltete Aussagen zu „1-Klick-Alarmierung“, Raumbuchungen und Lesebestätigungen wurden durch die tatsächlich vorhandenen Abläufe ersetzt. Der persönliche Ungelesen-Status wird nicht als Nachweis einer tatsächlichen Kenntnisnahme oder als Admin-Auswertung beworben. +- `src/lib/marketing/seo.ts` bündelt öffentliche Domain, Titel, Beschreibungen, Canonicals und Vorschaudaten. Öffentliche Seiten besitzen eigene absolute Canonicals und `og:url`; Titel wiederholen den Markennamen nicht. Open Graph und Twitter verwenden passende Beschreibungen und das vorhandene, tatsächlich 1200 × 630 Pixel große Bild. Sein Alternativtext beschreibt die sichtbaren Spielklötze. +- Die Startseite liefert JSON-LD für WebSite, WebPage und SoftwareApplication; Feature-Seiten ergänzen BreadcrumbList und ihre eigene WebPage. Die Daten stammen aus den vorhandenen Produktbeschreibungen, enthalten keine erfundenen Bewertungen, Preise oder Anbieterangaben und werden gegen eingeschleuste HTML-Script-Enden abgesichert. Die Scripts verwenden den vorhandenen CSP-Nonce. Dies ist keine Zusage besonderer Google-Suchergebnisdarstellungen. +- Die Sitemap enthält ausschließlich neun öffentliche Canonical-Seiten: Startseite, sechs Feature-Seiten, Impressum und Datenschutz. Das Änderungsdatum folgt der tatsächlichen Änderung der Marketinginhalte (`marketingLastModified`) und wechselt nicht mehr bei jedem Build. Für rechtliche Seiten wird kein unbekanntes Änderungsdatum erfunden. Bei weiteren Änderungen der Produkttexte ist dieses Datum gezielt fortzuschreiben. +- Indexierung ist im Root-Layout standardmäßig ausgeschaltet; nur die öffentlichen Seiten schalten sie ausdrücklich ein. Private Seiten behalten `noindex, nofollow` und `private, no-store` auch als HTTP-Header. Login, Registrierung und Wiederherstellungs-Anforderungsformular bleiben für Crawler erreichbar, damit sie die `noindex`-Markierung lesen können. Interne und Tokenrouten bleiben zusätzlich in robots.txt ausgeschlossen. robots.txt ist keine Zugriffskontrolle und eine blockierte URL allein garantiert keine Entfernung aus einem Suchindex. + +Prüfung: `npm run test:seo` (11 Tests gegen tatsächliche Metadaten-/Sitemap-/JSON-LD-Funktionen) sowie `TEST_SEO_ORIGIN=http://127.0.0.1:PORT npm run test:seo:http` (24 Tests gegen den lokal gestarteten Standalone-Produktionsbuild). Die HTTP-Prüfung umfasst alle neun öffentlichen HTML-Seiten, Social-Crawler-Metadaten direkt im Head, strukturierte Daten und CSP-Nonce, Sitemap, robots.txt, tatsächliche PNG-Bildmaße, private Seiten/Weiterleitungen und unbekannte Feature-URLs mit 404/noindex. Insgesamt 35 Tests erfolgreich, keine übersprungenen Tests. Build und TypeScript erfolgreich; ESLint ohne Fehler, mit fünf bereits bestehenden Warnungen. + +Die bestehende Produktions-Landingpage, robots.txt und Sitemap wurden zusätzlich lesend abgeglichen. Die neuen Änderungen wurden lokal geprüft und sind noch nicht veröffentlicht. Für diese SEO-Ergänzung gibt es keine weitere Datenbankmigration; der Branch baut auf der vorherigen Mitteilungs-Etappe mit deren eigener Migration auf. Es wurden keine produktiven Daten verändert, Anfragen versendet oder Analytics-Ereignisse ausgelöst. + +Offen: Impressum und Datenschutzhinweise enthalten im bestehenden Seiteninhalt weiterhin Platzhalter für Anbieter, Anschrift, Kontakt, verantwortliche Stelle und Dienstleister. Diese Angaben müssen von der verantwortlichen Person geliefert werden; die Metadaten-Ergänzung ersetzt sie nicht. Search-Console-Zugriff, tatsächlicher Google-Indexierungsstand und reale Nutzungs-/Performance-Messwerte waren nicht Teil der verfügbaren Prüfung. + +Grundlagen: [Google zu Canonical-URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls), [Sitemap-Änderungsdatum](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap) und [noindex](https://developers.google.com/search/docs/crawling-indexing/block-indexing); außerdem die mit Next.js 16.4 gelieferten Metadaten- und JSON-LD-Guides. + + ## Ergänzung vom 13.10.2026: Mitteilungen korrigieren und Versand verwalten - Unter `/dashboard/admin/news` können Kita-Admins alle eigenen Beiträge nach Titel/Text durchsuchen und in Seiten mit zwölf Meldungen durchblättern. „Bearbeiten & Versand“ öffnet die Korrektur und den Versandstatus. Eltern- und Koordinationskonten erhalten keinen Verwaltungszugriff. diff --git a/package.json b/package.json index 7251422..5f0b404 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "test:calendar:integration": "node --test tests/calendar-actions.integration.test.cjs", "test:notdienst:notifications": "node --test tests/notdienst-notifications.integration.test.cjs", "test:announcements": "node --test tests/announcements.integration.test.cjs", + "test:seo:http": "node --test tests/marketing-seo-http.cjs", + "test:seo": "node --test tests/marketing-seo.test.cjs", "test:mail": "node --test tests/mail-transport.test.cjs", "test:account": "node --test tests/password-management.integration.test.cjs", "test:roles": "node --test tests/kita-roles.integration.test.cjs tests/notdienst-assignment-view.test.cjs", diff --git a/src/app/datenschutz/page.tsx b/src/app/datenschutz/page.tsx index f637990..178e873 100644 --- a/src/app/datenschutz/page.tsx +++ b/src/app/datenschutz/page.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import { publicPageMetadata } from "@/lib/marketing/seo"; import Link from "next/link"; import { ArrowLeft, @@ -11,10 +11,11 @@ import { import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -export const metadata: Metadata = { - title: "Datenschutz | Kita-Planer", - description: "Datenschutzhinweise und Platzhalter für Kita-Planer.", -}; +export const metadata = publicPageMetadata( + "/datenschutz", + "Datenschutz", + "Datenschutzhinweise zu Konten, Kontaktfreigaben und der Nutzung von Kita-Planer.", +); const overviewItems = [ { @@ -77,9 +78,7 @@ export default function DatenschutzPage() {
-

- {item.title} -

+

{item.title}

{item.text}

@@ -154,9 +153,9 @@ export default function DatenschutzPage() {

Zugriff auf Daten erhalten nur berechtigte Nutzerinnen und Nutzer innerhalb der jeweiligen Kita beziehungsweise des jeweiligen - Elternvereins. Für Hosting, E-Mail-Versand oder technische - Dienste können Auftragsverarbeiter eingesetzt werden: - [Hosting-Anbieter], [E-Mail-Anbieter], [weitere Dienstleister]. + Elternvereins. Für Hosting, E-Mail-Versand oder technische Dienste + können Auftragsverarbeiter eingesetzt werden: [Hosting-Anbieter], + [E-Mail-Anbieter], [weitere Dienstleister].

@@ -180,9 +179,9 @@ export default function DatenschutzPage() {

Kita-Planer verwendet technisch notwendige Cookies oder vergleichbare Technologien, um Login-Sitzungen sicher - bereitzustellen. Für die öffentliche Website kann zusätzlich - Umami Analytics eingesetzt werden, um Seitenaufrufe anonymisiert - und ohne Cookies auszuwerten. + bereitzustellen. Für die öffentliche Website kann zusätzlich Umami + Analytics eingesetzt werden, um Seitenaufrufe anonymisiert und + ohne Cookies auszuwerten.

@@ -220,9 +219,9 @@ export default function DatenschutzPage() {

Betroffene Personen können sich bei einer - Datenschutzaufsichtsbehörde beschweren. Zuständig ist - insbesondere die Aufsichtsbehörde am Sitz der verantwortlichen - Stelle: [zuständige Datenschutzaufsichtsbehörde]. + Datenschutzaufsichtsbehörde beschweren. Zuständig ist insbesondere + die Aufsichtsbehörde am Sitz der verantwortlichen Stelle: + [zuständige Datenschutzaufsichtsbehörde].

diff --git a/src/app/features/[slug]/page.tsx b/src/app/features/[slug]/page.tsx index f51df8b..75e3712 100644 --- a/src/app/features/[slug]/page.tsx +++ b/src/app/features/[slug]/page.tsx @@ -1,3 +1,5 @@ +import { StructuredData } from "@/components/marketing/structured-data"; +import { featureStructuredData, publicPageMetadata } from "@/lib/marketing/seo"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -48,30 +50,11 @@ export async function generateMetadata({ }; } - return { - title: feature.seoTitle, - description: feature.seoDescription, - openGraph: { - title: feature.seoTitle, - description: feature.seoDescription, - type: "website", - locale: "de_DE", - images: [ - { - url: "/og-image.png", - width: 1200, - height: 630, - alt: "Ein Kind spielt mit bunten Holzklötzen.", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: feature.seoTitle, - description: feature.seoDescription, - images: ["/og-image.png"], - }, - }; + return publicPageMetadata( + `/features/${feature.slug}`, + feature.seoTitle, + feature.seoDescription, + ); } export default async function FeaturePage({ params }: FeaturePageProps) { @@ -86,22 +69,30 @@ export default async function FeaturePage({ params }: FeaturePageProps) { return (
+
- - + + - + Kita-Planer - @@ -181,7 +172,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
- + {feature.problemTitle} @@ -192,7 +183,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) { - + {feature.solutionTitle} @@ -220,7 +211,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
- + {benefit.title} @@ -273,7 +264,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
- + {faq.question} diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx index 827e47a..634ce5e 100644 --- a/src/app/impressum/page.tsx +++ b/src/app/impressum/page.tsx @@ -1,14 +1,15 @@ -import type { Metadata } from "next"; +import { publicPageMetadata } from "@/lib/marketing/seo"; import Link from "next/link"; import { ArrowLeft, Building2, Mail, MapPin, ShieldCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -export const metadata: Metadata = { - title: "Impressum | Kita-Planer", - description: "Impressum und Anbieterkennzeichnung von Kita-Planer.", -}; +export const metadata = publicPageMetadata( + "/impressum", + "Impressum", + "Impressum und Anbieterkennzeichnung von Kita-Planer.", +); const contactItems = [ { @@ -117,8 +118,8 @@ export default function ImpressumPage() { Umsatzsteuer-ID

- Umsatzsteuer-Identifikationsnummer gemäß § 27a - Umsatzsteuergesetz: - + Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz: + -

@@ -126,9 +127,7 @@ export default function ImpressumPage() {

Verantwortlich für den Inhalt

-

- Torben Indorf -

+

Torben Indorf

diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2d6303d..d9df761 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,12 @@ import { getSession } from "@/lib/auth-utils"; import { headers } from "next/headers"; import type { Metadata } from "next"; +import { + getPublicSiteOrigin, + marketingDescription, + marketingTitle, + socialImage, +} from "@/lib/marketing/seo"; import { Manrope } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/sonner"; @@ -13,36 +19,24 @@ const manrope = Manrope({ }); export const metadata: Metadata = { - metadataBase: new URL( - process.env.NEXT_PUBLIC_SITE_URL ?? "https://mein-kitaplaner.de", - ), - title: { - default: "Der digitale Kita-Planer für Elternvereine", - template: "%s | Kita-Planer", - }, - description: - "Kita-Planer bündelt Dienste, Termine, Notdienste, Abwesenheiten und sichere Kommunikation für Elternvereine an einem Ort.", + metadataBase: new URL(getPublicSiteOrigin()), + title: { default: marketingTitle, template: "%s | Kita-Planer" }, + description: marketingDescription, + // Public marketing pages opt in explicitly; account and internal pages stay out. + robots: { index: false, follow: false }, openGraph: { - title: "Der digitale Kita-Planer für Elternvereine", - description: - "Organisiert Dienste, teilt Neuigkeiten und behaltet Termine gemeinsam im Blick.", + title: marketingTitle, + description: marketingDescription, type: "website", locale: "de_DE", - images: [ - { - url: "/og-image.png", - width: 1200, - height: 630, - alt: "Ein Kind spielt mit bunten Holzklötzen.", - }, - ], + siteName: "Kita-Planer", + images: [socialImage], }, twitter: { card: "summary_large_image", - title: "Der digitale Kita-Planer für Elternvereine", - description: - "Organisiert Dienste, teilt Neuigkeiten und behaltet Termine gemeinsam im Blick.", - images: ["/og-image.png"], + title: marketingTitle, + description: marketingDescription, + images: [{ url: socialImage.url, alt: socialImage.alt }], }, }; @@ -51,13 +45,13 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const [requestHeaders, session] = await Promise.all([headers(), getSession()]); + const [requestHeaders, session] = await Promise.all([ + headers(), + getSession(), + ]); const nonce = requestHeaders.get("x-nonce") ?? undefined; return ( - + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index c0f5dac..250472c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,3 @@ -import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; import { @@ -35,38 +34,20 @@ import { CardTitle, } from "@/components/ui/card"; import { marketingFeatures } from "@/lib/marketing/features"; +import { StructuredData } from "@/components/marketing/structured-data"; +import { + landingStructuredData, + marketingDescription, + marketingTitle, + publicPageMetadata, +} from "@/lib/marketing/seo"; import { ContactForm } from "./contact-form"; -export const metadata: Metadata = { - title: "Kita-Planer für Elterninitiativen und Kita-Vereine", - description: - "Die einfache Plattform für Elterninitiativen, freie Kitas und Kita-Vereine: organisiert Dienste, Termine, Krankmeldungen und offizielle Kommunikation an einem Ort.", - alternates: { - canonical: "/", - }, - openGraph: { - title: "Kita-Planer für Elterninitiativen und Kita-Vereine", - description: - "Weniger Chat-Chaos, weniger Excel-Listen, mehr Ruhe im ehrenamtlichen Kita-Alltag.", - type: "website", - locale: "de_DE", - images: [ - { - url: "/og-image.png", - width: 1200, - height: 630, - alt: "Ein Kind spielt mit bunten Holzklötzen.", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Kita-Planer für Elterninitiativen und Kita-Vereine", - description: - "Organisiert Dienste, Termine, Krankmeldungen und offizielle Kommunikation an einem Ort.", - images: ["/og-image.png"], - }, -}; +export const metadata = publicPageMetadata( + "/", + marketingTitle, + marketingDescription, +); const heroImage = "https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?auto=format&fit=crop&w=2400&q=86"; @@ -74,13 +55,13 @@ const heroImage = const privacyPoints = [ { icon: DatabaseZap, - title: "Strikt isolierte Mandanten", - text: "Jede Kita arbeitet technisch streng getrennt pro Kita. Rollen und Datenabfragen bleiben konsequent tenant-isoliert.", + title: "Getrennte Daten je Kita", + text: "Die Daten eurer Kita sind von anderen Einrichtungen getrennt. Zugriff erhalten nur die berechtigten Konten eurer Kita.", }, { icon: Fingerprint, title: "Kontaktfreigabe per Opt-in", - text: "Adressbuchdaten erscheinen nur nach aktivem Opt-In pro Elternteil. Nicht freigegebene Kontaktdaten verlassen den Server nicht.", + text: "Jeder Elternteil entscheidet über die Freigabe der eigenen Kontaktdaten. Im internen Adressbuch erscheinen nur freigegebene Angaben.", }, { icon: Trash2, @@ -90,23 +71,23 @@ const privacyPoints = [ { icon: LockKeyhole, title: "Geschützte Anhänge", - text: "Dokumente vom Schwarzen Brett liegen nicht öffentlich und werden nur nach Session- und Kita-Prüfung ausgeliefert.", + text: "Anhänge sind nur nach Anmeldung und für die gewählte Empfängergruppe eurer Kita abrufbar.", }, { icon: UserCog, title: "Rollenrechte pro Kita", - text: "Vorstand, Koordination, Eltern und Team sehen nur die Bereiche, die zu ihrer Aufgabe und Kita-Zugehörigkeit passen.", + text: "Kita-Admins vergeben Rechte für Verwaltung, Notdienst-Koordination, Eltern und Team. Ämter wie Vorstand sind von Zugriffsrechten getrennt.", }, { icon: FileStack, - title: "Klare Datenpfade", - text: "Löschungen sind bewusst vorgesehen; Fragen zu Export und Übergabe werden im Setup sauber geklärt.", + title: "Kontozugang selbst verwalten", + text: "Aktivierte Konten können ihr Passwort im Profil ändern oder einen zeitlich begrenzten Wiederherstellungslink anfordern.", }, ]; const proofPoints = [ "Für Elterninitiativen, freie Kitas und Kita-Vereine", - "Dienste, Kalender, Abwesenheiten und offizielle Infos in einem System", + "Dienste, Kalender, Arbeitszeit und offizielle Infos in einem System", "Gebaut für Vorstände, Eltern, Koordination und Team", ]; @@ -114,13 +95,13 @@ const replacedTools = [ { icon: MessagesSquare, title: "WhatsApp für offizielle Infos", - text: "Ankündigungen, Anhänge und Lesestatus gehören in einen verbindlichen Kanal, nicht zwischen spontane Rückfragen.", + text: "Ankündigungen mit Anhängen, sichtbaren Korrekturen und optionalen E-Mails bleiben auch später auffindbar.", accent: "bg-emerald-50 text-emerald-700 ring-emerald-100", }, { icon: Table2, title: "Excel-Listen für Dienste", - text: "Verfügbarkeiten, Mindesttage und Dienstpläne werden strukturierter als in Tabellen mit Version 17-final-final.", + text: "Verfügbarkeiten, Erinnerungen und veröffentlichte Dienstpläne bleiben nachvollziehbar. Bei Bedarf lässt sich die Einteilung manuell anpassen.", accent: "bg-lime-50 text-lime-700 ring-lime-100", }, { @@ -147,17 +128,22 @@ const audienceRoles = [ { icon: UserCog, title: "Vorstand & Admin", - text: "Kita, Familien, Termine, Einladungen und Rechte verwalten, ohne dass alles an einer privaten Ablage hängt.", + text: "Familien einladen, Zugriffsrechte vergeben, Mitteilungen korrigieren und Versandfehler gezielt erneut versuchen.", }, { icon: UsersRound, title: "Eltern", - text: "Termine sehen, Kinder krankmelden, Verfügbarkeiten eintragen und freigegebene Kontakte finden.", + text: "Verfügbarkeiten eintragen, zu Terminen zusagen, Mitbringbeiträge verwalten und zukünftige Abwesenheiten korrigieren.", }, { icon: HeartHandshake, - title: "ErzieherInnen & Koordination", - text: "Tagesüberblick, Abwesenheiten, Dienste und relevante Informationen an einem Ort sehen.", + title: "Kita-Team", + text: "Arbeitszeit erfassen, Wochen einreichen und Urlaub oder Freizeitausgleich beantragen. Abwesenheiten und Teammitteilungen bleiben im Blick.", + }, + { + icon: HeartHandshake, + title: "Notdienst-Koordination", + text: "Verfügbarkeiten prüfen, Monatspläne veröffentlichen und bei Ausfällen mit Rückmeldungen aus den Haushalten weiterplanen.", }, ]; @@ -190,28 +176,32 @@ const onboardingSteps = [ export default function LandingPage() { return (
+
- - + + - + Kita-Planer -
@@ -386,11 +377,14 @@ export default function LandingPage() {
-
+

- Die 5 Säulen + Die {marketingFeatures.length} Bereiche

Plant Dienste, teilt Neuigkeiten und behaltet Termine gemeinsam @@ -405,15 +399,12 @@ export default function LandingPage() {

- {marketingFeatures.map((feature, index) => { + {marketingFeatures.map((feature) => { const Icon = feature.icon; - const isWide = index > 2; return (
@@ -440,7 +431,10 @@ export default function LandingPage() { variant="link" className="mt-4 h-auto p-0 text-slate-950" > - + Mehr erfahren @@ -508,9 +502,9 @@ export default function LandingPage() {

Kita-Vereine verwalten besonders sensible Daten. Deshalb ist - Kita-Planer so gebaut, dass Sichtbarkeit, Rollenrechte, - Löschung und Mandantentrennung nicht vom guten Willen einzelner - Chats abhängen. + Kita-Planer so gebaut, dass Sichtbarkeit, Rollenrechte, Löschung + und Mandantentrennung nicht vom guten Willen einzelner Chats + abhängen.

@@ -644,10 +638,10 @@ function ProductBrowserMock() {
-
- app.kita-planer.de/dashboard/notdienst +
+ Kita-Planer · Notdienst
-
+
@@ -675,14 +669,23 @@ function FeaturePreview({ slug }: { slug: string }) {
- {["", "bg-red-100", "", "bg-red-100", "", "bg-red-100", "", "", "bg-red-100", ""].map( - (cell, index) => ( - - ), - )} + {[ + "", + "bg-red-100", + "", + "bg-red-100", + "", + "bg-red-100", + "", + "", + "bg-red-100", + "", + ].map((cell, index) => ( + + ))}
); @@ -748,11 +751,40 @@ function FeaturePreview({ slug }: { slug: string }) { ); } + if (slug === "personal-arbeitszeit") { + return ( +
+
+ + Meine Arbeitswoche + + + Eingereicht + +
+
+ {[ + ["Mo", "7:00 Std."], + ["Di", "6:30 Std."], + ["Mi", "Urlaub"], + ].map(([day, hours]) => ( +
+ {day} + + {hours} + +
+ ))} +
+
+ ); + } + return (
- Neue Mitteilung + Aktualisierte Mitteilung
diff --git a/src/app/robots.ts b/src/app/robots.ts index 37259c1..a338069 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -1,28 +1,26 @@ import type { MetadataRoute } from "next"; -function getSiteUrl() { - return process.env.NEXT_PUBLIC_SITE_URL ?? "https://mein-kitaplaner.de"; -} +import { getPublicSiteOrigin, publicUrl } from "@/lib/marketing/seo"; export default function robots(): MetadataRoute.Robots { - const siteUrl = getSiteUrl(); + const siteUrl = getPublicSiteOrigin(); return { rules: { userAgent: "*", allow: ["/", "/features/", "/impressum", "/datenschutz"], + // Login/registration/recovery forms stay crawlable so bots can read noindex. disallow: [ "/api/", - "/dashboard/", + "/dashboard", + "/admin", "/invite/", "/alert/", - "/login", - "/register", - "/onboarding/", - "/forbidden", + "/onboarding", + "/neues-passwort/", ], }, - sitemap: `${siteUrl}/sitemap.xml`, + sitemap: publicUrl("/sitemap.xml"), host: siteUrl, }; } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 0167d81..10ddcc6 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -2,36 +2,25 @@ import type { MetadataRoute } from "next"; import { marketingFeatures } from "@/lib/marketing/features"; -function getSiteUrl() { - return process.env.NEXT_PUBLIC_SITE_URL ?? "https://mein-kitaplaner.de"; -} +import { marketingLastModified, publicUrl } from "@/lib/marketing/seo"; export default function sitemap(): MetadataRoute.Sitemap { - const siteUrl = getSiteUrl(); - const lastModified = new Date(); - return [ { - url: siteUrl, - lastModified, - changeFrequency: "weekly", + url: publicUrl(), + lastModified: marketingLastModified, + changeFrequency: "monthly", priority: 1, }, + { url: publicUrl("/impressum"), changeFrequency: "yearly", priority: 0.2 }, { - url: `${siteUrl}/impressum`, - lastModified, - changeFrequency: "yearly", - priority: 0.2, - }, - { - url: `${siteUrl}/datenschutz`, - lastModified, + url: publicUrl("/datenschutz"), changeFrequency: "yearly", priority: 0.2, }, ...marketingFeatures.map((feature) => ({ - url: `${siteUrl}/features/${feature.slug}`, - lastModified, + url: publicUrl(`/features/${feature.slug}`), + lastModified: marketingLastModified, changeFrequency: "monthly" as const, priority: 0.8, })), diff --git a/src/components/marketing/structured-data.tsx b/src/components/marketing/structured-data.tsx new file mode 100644 index 0000000..ab919f6 --- /dev/null +++ b/src/components/marketing/structured-data.tsx @@ -0,0 +1,13 @@ +import { headers } from "next/headers"; +import { serializeStructuredData } from "@/lib/marketing/seo"; + +export async function StructuredData({ data }: { data: unknown }) { + const nonce = (await headers()).get("x-nonce") ?? undefined; + return ( + ' }; + assert.ok(!seo.serializeStructuredData(value).includes('<')); assert.deepEqual(JSON.parse(seo.serializeStructuredData(value)), value); + const component = load('src/components/marketing/structured-data.tsx').StructuredData; + const html = renderToStaticMarkup(await component({ data: value })); + assert.ok(html.includes('nonce="synthetic-seo-test-nonce"')); assert.equal((html.match(/