Update public feature details and align SEO metadata

This commit is contained in:
t.indorf
2026-10-13 09:23:46 +02:00
parent 1472779781
commit abe73bb9ac
15 changed files with 683 additions and 234 deletions
+16 -17
View File
@@ -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() {
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700 ring-1 ring-emerald-100">
<Icon className="h-5 w-5" />
</div>
<h2 className="font-semibold tracking-tight">
{item.title}
</h2>
<h2 className="font-semibold tracking-tight">{item.title}</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
{item.text}
</p>
@@ -154,9 +153,9 @@ export default function DatenschutzPage() {
<p className="mt-3 leading-7 text-slate-700">
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].
</p>
</div>
@@ -180,9 +179,9 @@ export default function DatenschutzPage() {
<p className="mt-3 leading-7 text-slate-700">
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.
</p>
</div>
@@ -220,9 +219,9 @@ export default function DatenschutzPage() {
</h2>
<p className="mt-3 leading-7 text-slate-700">
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].
</p>
</div>
+25 -34
View File
@@ -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 (
<div className="min-h-screen bg-[#f8faf8] text-slate-950">
<StructuredData data={featureStructuredData(feature)} />
<header className="border-b border-slate-200 bg-white/95">
<div className="mx-auto flex h-20 w-full max-w-7xl items-center justify-between px-5 sm:px-6">
<Link href="/" className="flex items-center gap-3 text-slate-950">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-950 text-white">
<Link
href="/"
className="flex shrink-0 items-center gap-2 text-slate-950 sm:gap-3"
>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-950 text-white sm:h-9 sm:w-9">
<ShieldCheck className="h-5 w-5" />
</span>
<span className="text-base font-semibold tracking-tight">
<span className="whitespace-nowrap text-sm font-semibold tracking-tight sm:text-base">
Kita-Planer
</span>
</Link>
<nav className="flex items-center gap-2">
<Button asChild variant="ghost">
<nav className="flex items-center gap-1 sm:gap-2">
<Button
asChild
variant="ghost"
className="px-3 text-xs sm:px-4 sm:text-sm"
>
<Link href="/login">Login</Link>
</Button>
<Button asChild>
<Button asChild className="px-3 text-xs sm:px-4 sm:text-sm">
<Link href="/#kontakt">Demo anfragen</Link>
</Button>
</nav>
@@ -181,7 +172,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
<section className="mx-auto grid w-full max-w-7xl gap-6 px-5 py-20 sm:px-6 lg:grid-cols-2">
<Card className="border-slate-200 bg-white">
<CardHeader>
<CardTitle className="text-2xl tracking-tight">
<CardTitle as="h2" className="text-2xl tracking-tight">
{feature.problemTitle}
</CardTitle>
<CardDescription className="text-base leading-7 text-slate-600">
@@ -192,7 +183,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
<Card className="border-slate-200 bg-white">
<CardHeader>
<CardTitle className="text-2xl tracking-tight">
<CardTitle as="h2" className="text-2xl tracking-tight">
{feature.solutionTitle}
</CardTitle>
<CardDescription className="text-base leading-7 text-slate-600">
@@ -220,7 +211,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700 ring-1 ring-emerald-100">
<CheckCircle2 className="h-5 w-5" />
</div>
<CardTitle className="text-xl tracking-tight">
<CardTitle as="h3" className="text-xl tracking-tight">
{benefit.title}
</CardTitle>
<CardDescription className="text-sm leading-6 text-slate-600">
@@ -273,7 +264,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) {
<div className="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-slate-100 text-slate-700">
<HelpCircle className="h-5 w-5" />
</div>
<CardTitle className="text-lg leading-snug">
<CardTitle as="h3" className="text-lg leading-snug">
{faq.question}
</CardTitle>
<CardDescription className="text-sm leading-6 text-slate-600">
+9 -10
View File
@@ -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
</h2>
<p className="mt-3 leading-7 text-slate-700">
Umsatzsteuer-Identifikationsnummer gemäß § 27a
Umsatzsteuergesetz: -
Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:
-
</p>
</div>
@@ -126,9 +127,7 @@ export default function ImpressumPage() {
<h2 className="text-xl font-semibold tracking-tight">
Verantwortlich für den Inhalt
</h2>
<p className="mt-3 leading-7 text-slate-700">
Torben Indorf
</p>
<p className="mt-3 leading-7 text-slate-700">Torben Indorf</p>
</div>
<div>
+23 -29
View File
@@ -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 (
<html
lang="de"
className={`${manrope.variable} h-full antialiased`}
>
<html lang="de" className={`${manrope.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col">
{children}
<UmamiAnalytics nonce={nonce} enabled={!session?.user?.id} />
+111 -79
View File
@@ -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 (
<div className="min-h-screen bg-[#f8faf8] text-slate-950">
<StructuredData data={landingStructuredData(marketingFeatures)} />
<header className="absolute left-0 right-0 top-0 z-20">
<div className="mx-auto flex min-h-20 w-full max-w-7xl items-center justify-between gap-3 px-5 py-4 sm:px-6">
<Link href="/" className="flex items-center gap-3 text-white">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/12 ring-1 ring-white/30">
<Link
href="/"
className="flex shrink-0 items-center gap-2 text-white sm:gap-3"
>
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/12 ring-1 ring-white/30 sm:h-9 sm:w-9">
<ShieldCheck className="h-5 w-5" />
</span>
<span className="text-base font-semibold tracking-tight">
<span className="whitespace-nowrap text-sm font-semibold tracking-tight sm:text-base">
Kita-Planer
</span>
</Link>
<nav className="flex flex-wrap items-center justify-end gap-2">
<nav className="flex flex-wrap items-center justify-end gap-1 sm:gap-2">
<Button
asChild
variant="ghost"
className="text-white hover:bg-white/10 hover:text-white"
className="px-3 text-xs text-white hover:bg-white/10 hover:text-white sm:px-4 sm:text-sm"
>
<Link href="/login">Login</Link>
</Button>
<Button
asChild
className="bg-white text-slate-950 shadow-sm hover:bg-white/90"
className="bg-white px-3 text-xs text-slate-950 shadow-sm hover:bg-white/90 sm:px-4 sm:text-sm"
>
<Link href="/#kontakt">Demo anfragen</Link>
</Button>
@@ -245,8 +235,8 @@ export default function LandingPage() {
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-white/82 sm:text-xl">
Für Elterninitiativen, freie Kitas und Kita-Vereine, die
Dienste, Termine, Krankmeldungen und offizielle Kommunikation
ehrenamtlich organisieren.
Dienste, Termine, Abwesenheiten, Arbeitszeit und offizielle
Kommunikation gemeinsam organisieren.
</p>
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
@@ -294,9 +284,10 @@ export default function LandingPage() {
</h2>
</div>
<p className="max-w-2xl text-sm leading-6 text-slate-600 lg:justify-self-end">
Der Notdienst-Planer sammelt Verfügbarkeiten, zeigt offene
Familien und macht aus losen Antworten einen veröffentlichbaren
Monatsplan.
Der Notdienst-Planer sammelt Verfügbarkeiten, erinnert an fehlende
Einträge und macht aus Antworten einen veröffentlichbaren
Monatsplan. Bei manuellen Änderungen bleibt sichtbar, welche
Haushalte für den Tag verfügbar sind.
</p>
</div>
@@ -386,11 +377,14 @@ export default function LandingPage() {
</div>
</section>
<section className="mx-auto w-full max-w-7xl px-5 py-24 sm:px-6">
<section
id="funktionen"
className="mx-auto w-full max-w-7xl px-5 py-24 sm:px-6"
>
<div className="mb-12 flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<p className="mb-3 text-sm font-semibold uppercase tracking-wide text-emerald-700">
Die 5 Säulen
Die {marketingFeatures.length} Bereiche
</p>
<h2 className="text-balance text-3xl font-semibold leading-tight sm:text-4xl">
Plant Dienste, teilt Neuigkeiten und behaltet Termine gemeinsam
@@ -405,15 +399,12 @@ export default function LandingPage() {
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-6">
{marketingFeatures.map((feature, index) => {
{marketingFeatures.map((feature) => {
const Icon = feature.icon;
const isWide = index > 2;
return (
<Card
key={feature.title}
className={`group border-slate-200 bg-white/90 shadow-sm transition duration-200 hover:-translate-y-1 hover:border-slate-300 hover:shadow-lg ${
isWide ? "xl:col-span-3" : "xl:col-span-2"
}`}
className="group border-slate-200 bg-white/90 shadow-sm transition duration-200 hover:-translate-y-1 hover:border-slate-300 hover:shadow-lg xl:col-span-2"
>
<CardHeader className="space-y-5">
<div className="flex items-start justify-between gap-4">
@@ -440,7 +431,10 @@ export default function LandingPage() {
variant="link"
className="mt-4 h-auto p-0 text-slate-950"
>
<Link href={`/features/${feature.slug}`}>
<Link
href={`/features/${feature.slug}`}
aria-label={`Mehr über ${feature.shortTitle} erfahren`}
>
Mehr erfahren
<ArrowRight className="h-4 w-4" />
</Link>
@@ -508,9 +502,9 @@ export default function LandingPage() {
</h2>
<p className="mt-5 max-w-xl text-base leading-7 text-slate-300">
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.
</p>
</div>
@@ -644,10 +638,10 @@ function ProductBrowserMock() {
<span className="h-3 w-3 rounded-full bg-amber-300" />
<span className="h-3 w-3 rounded-full bg-emerald-300" />
</div>
<div className="rounded-full border border-slate-200 bg-white px-4 py-1 text-xs font-medium text-slate-500">
app.kita-planer.de/dashboard/notdienst
<div className="rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-medium text-slate-500 sm:px-4 sm:text-xs">
Kita-Planer · Notdienst
</div>
<div className="w-16" />
<div className="hidden w-16 sm:block" />
</div>
<div className="bg-[#fbf8ef] p-2 sm:p-4">
@@ -675,14 +669,23 @@ function FeaturePreview({ slug }: { slug: string }) {
</span>
</div>
<div className="grid grid-cols-5 gap-1.5">
{["", "bg-red-100", "", "bg-red-100", "", "bg-red-100", "", "", "bg-red-100", ""].map(
(cell, index) => (
<span
key={index}
className={`h-7 rounded-md border border-red-100 bg-white ${cell}`}
/>
),
)}
{[
"",
"bg-red-100",
"",
"bg-red-100",
"",
"bg-red-100",
"",
"",
"bg-red-100",
"",
].map((cell, index) => (
<span
key={index}
className={`h-7 rounded-md border border-red-100 bg-white ${cell}`}
/>
))}
</div>
</div>
);
@@ -748,11 +751,40 @@ function FeaturePreview({ slug }: { slug: string }) {
);
}
if (slug === "personal-arbeitszeit") {
return (
<div className="rounded-xl border border-teal-100 bg-[#fbf8ef] p-3">
<div className="mb-3 flex items-center justify-between gap-2 text-xs">
<span className="font-semibold text-slate-700">
Meine Arbeitswoche
</span>
<span className="rounded-full bg-teal-50 px-2 py-1 text-teal-700">
Eingereicht
</span>
</div>
<div className="grid grid-cols-3 gap-2 text-center text-xs">
{[
["Mo", "7:00 Std."],
["Di", "6:30 Std."],
["Mi", "Urlaub"],
].map(([day, hours]) => (
<div key={day} className="rounded-lg bg-white p-2">
<span className="block text-slate-500">{day}</span>
<span className="mt-1 block font-medium text-slate-700">
{hours}
</span>
</div>
))}
</div>
</div>
);
}
return (
<div className="rounded-xl border border-indigo-100 bg-[#fbf8ef] p-3">
<div className="mb-3 flex items-center gap-2 text-xs font-semibold text-slate-700">
<Bell className="h-4 w-4 text-indigo-700" />
Neue Mitteilung
Aktualisierte Mitteilung
</div>
<div className="space-y-2">
<span className="block h-2.5 w-4/5 rounded-full bg-slate-200" />
+8 -10
View File
@@ -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,
};
}
+8 -19
View File
@@ -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,
})),