feat: redesign CMS and unify German admin UI

This commit is contained in:
2026-09-17 16:51:24 +02:00
parent a2b3e99174
commit 7ec0c51ff5
129 changed files with 952 additions and 550 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ function bump(map: Record<string, number>, key: string | null | undefined) {
map[key] = (map[key] ?? 0) + 1;
}
/** Sorted (descending) [key, count] pairs, capped at `limit` — for dashboard tables. */
/** Sorted (descending) [key, count] pairs, capped at `limit` - for dashboard tables. */
export function topEntries(map: Record<string, number>, limit = 10): [string, number][] {
return Object.entries(map)
.sort(([, a], [, b]) => b - a)
@@ -26,7 +26,7 @@ export function topEntries(map: Record<string, number>, limit = 10): [string, nu
}
/**
* Pure aggregation over a set of raw AnalyticsEvents rows (any date range —
* Pure aggregation over a set of raw AnalyticsEvents rows (any date range -
* one day for the cleanup rollup, or a whole dashboard-selected range for
* live queries). Kept side-effect free and DB-agnostic so both callers
* (lib/analytics/cleanup.ts and lib/analytics/dashboardQueries.ts) share
+3 -3
View File
@@ -14,7 +14,7 @@ function addDays(date: Date, days: number): Date {
}
/**
* Rolls up every not-yet-aggregated, fully-completed calendar day (UTC —
* Rolls up every not-yet-aggregated, fully-completed calendar day (UTC -
* matching the UTC-day salt rotation in lib/analytics/salt.ts) from raw
* analytics-events into analytics-daily-stats, THEN purges raw events older
* than the configured retention, THEN purges old daily-stats rows. Order
@@ -22,7 +22,7 @@ function addDays(date: Date, days: number): Date {
* deleted, or that history is gone for good.
*
* Only aggregates days that don't already have a daily-stats row (each day
* is aggregated exactly once, ever) — safe to call this daily via
* is aggregated exactly once, ever) - safe to call this daily via
* app/(payload)/api/cron/analytics-cleanup/route.ts, and safe to re-run.
*/
export async function runAnalyticsCleanup(payload: Payload) {
@@ -60,7 +60,7 @@ export async function runAnalyticsCleanup(payload: Payload) {
{ createdAt: { less_than: dayEnd.toISOString() } },
],
},
limit: 0, // no limit — pull the whole day, small-site scale
limit: 0, // no limit - pull the whole day, small-site scale
depth: 0,
});
+3 -3
View File
@@ -2,15 +2,15 @@
/**
* Public developer API + consent storage for the analytics system. Importing
* this module has zero side effects — no network call, nothing initialized
* — until `track()` is actually called, and even then it's a no-op unless
* this module has zero side effects - no network call, nothing initialized
* - until `track()` is actually called, and even then it's a no-op unless
* consent was actually granted. Callers never check consent themselves:
*
* track("booking_started");
*
* The CMS-configured part of consent (whether the "statistics" category is
* enabled at all, and the current consent version) is pushed in once by
* components/Analytics.tsx via setAnalyticsConfig() — see that file. Until
* components/Analytics.tsx via setAnalyticsConfig() - see that file. Until
* that happens, `cmsConfig` is null and track() fails safe (no-op), so a
* component whose effects happen to run before Analytics.tsx's can never
* accidentally send data before the live config is known.
+3 -3
View File
@@ -1,7 +1,7 @@
/**
* Central, extensible registry of known analytics event names. Adding a new
* event elsewhere in the app never requires a migration — the database
* column (collections/AnalyticsEvents.ts#eventName) is free-text — this
* event elsewhere in the app never requires a migration - the database
* column (collections/AnalyticsEvents.ts#eventName) is free-text - this
* list exists purely so `track()` calls get autocomplete/type-checking and
* the dashboard can show a nice German label instead of a raw event name.
*
@@ -18,7 +18,7 @@ export const ANALYTICS_EVENTS = {
export type KnownAnalyticsEvent = keyof typeof ANALYTICS_EVENTS;
/** Any string works (see the collection comment above) — known ones just get a label. */
/** Any string works (see the collection comment above) - known ones just get a label. */
export type AnalyticsEventName = KnownAnalyticsEvent | (string & {});
export function analyticsEventLabel(eventName: string): string {
+1 -1
View File
@@ -27,7 +27,7 @@ function parseDateParam(value: string | null): Date | null {
/**
* Resolves the dashboard's period filter (?range=&from=&to=) into an actual
* date range, clamped to `maxDaysBack` (the configured raw-event retention —
* date range, clamped to `maxDaysBack` (the configured raw-event retention -
* see globals/AnalyticsSettings.ts) so the UI never claims to show data that
* has already been purged. Also computes the matching "previous period" for
* comparison (same length, immediately preceding).
+3 -3
View File
@@ -2,14 +2,14 @@ import { createHmac } from "node:crypto";
/**
* Derives a daily-rotating, non-reversible pseudonymous visitor identifier
* for session grouping — the same approach used by privacy-friendly
* for session grouping - the same approach used by privacy-friendly
* analytics tools (Plausible/Fathom-style): hash(ip + userAgent + host)
* salted with a value that changes every calendar day. Raw IP/UA are never
* persisted anywhere, only this hash — and since the salt rotates daily,
* persisted anywhere, only this hash - and since the salt rotates daily,
* the same visitor hashes differently tomorrow, so there is no cross-day
* linkage and no long-term individual profile.
*
* ANALYTICS_SALT_SECRET is a dedicated secret (not PAYLOAD_SECRET) — this
* ANALYTICS_SALT_SECRET is a dedicated secret (not PAYLOAD_SECRET) - this
* codebase keeps one secret per purpose on purpose (see
* MEETING_SESSION_SECRET's comment in .env.example).
*/
+1 -1
View File
@@ -7,7 +7,7 @@ const SESSION_IDLE_WINDOW_MS = 30 * 60 * 1000;
* Groups events into sessions without any client-side identifier: looks up
* the most recent event from the same `visitorHash` within the last 30
* minutes and reuses its sessionId, or mints a new one. One extra indexed
* read per tracked request — acceptable at this site's traffic volume, and
* read per tracked request - acceptable at this site's traffic volume, and
* simpler/safer than trying to hold a lock across an unauthenticated,
* high-frequency public endpoint (unlike lib/booking/lock.ts's advisory
* lock, an occasional extra spurious session here has no correctness
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* Coarse device/browser classification from the User-Agent header. No
* library, no raw UA string ever stored — just one bucket per request,
* library, no raw UA string ever stored - just one bucket per request,
* computed on the fly and discarded after classification. Not meant to be
* precise (e.g. WebView vs. real browser); good enough for the "device
* type" / "browser" breakdown in the Analytics dashboard.
@@ -19,7 +19,7 @@ export function classifyDevice(userAgent: string): DeviceType {
export function classifyBrowser(userAgent: string): BrowserFamily {
const ua = userAgent.toLowerCase();
// Order matters: Edge/Chrome UAs also contain "safari"; Chrome UAs also
// contain "chrome" but so do Edge/Opera — check the most specific tokens first.
// contain "chrome" but so do Edge/Opera - check the most specific tokens first.
if (/edg\//.test(ua)) return "edge";
if (/chrome\/|crios\//.test(ua)) return "chrome";
if (/firefox\/|fxios\//.test(ua)) return "firefox";
+2 -2
View File
@@ -12,7 +12,7 @@ export type ImageMood =
| "peach";
// Purely a presentation fallback for offers/events without an uploaded photo
// yet — content itself always comes from the CMS.
// yet - content itself always comes from the CMS.
const moodBySlug: Record<string, ImageMood> = {
prozessbegleitung: "plum",
"doula-begleitung": "dustyrose",
@@ -50,7 +50,7 @@ const categoryHref: Record<string, string> = {
/**
* Groups the flat `offers` collection into the four nav/overview sections.
* Private offers (e.g. "Einzelbegleitung") are deliberately excluded — they
* Private offers (e.g. "Einzelbegleitung") are deliberately excluded - they
* stay reachable only via their own direct booking link.
*/
export function groupOffersByCategory(offers: Offer[]): OfferCategoryGroup[] {
+3 -3
View File
@@ -1,6 +1,6 @@
/**
* Minimal in-memory fixed-window rate limiter for a single Node process
* (this app always runs as one process — see server.ts). Good enough to
* (this app always runs as one process - see server.ts). Good enough to
* blunt abuse of the email-verification resend endpoint without adding an
* external store; resets on deploy/restart, which is an acceptable
* trade-off for this use case.
@@ -34,12 +34,12 @@ export function checkRateLimit(key: string, opts: { max: number; windowMs: numbe
export function getClientIp(request: { headers: Headers }): string {
// Deployment is always behind exactly one trusted reverse proxy (see
// DEPLOYMENT.md — Nginx Proxy Manager; the app port isn't published
// DEPLOYMENT.md - Nginx Proxy Manager; the app port isn't published
// directly to the host). nginx's default `$proxy_add_x_forwarded_for`
// *appends* the real client IP to any X-Forwarded-For it already
// received rather than replacing it, so a directly-connecting client
// could prepend arbitrary spoofed entries of their own. Only the
// right-most (last) entry — the one the trusted proxy itself appended —
// right-most (last) entry - the one the trusted proxy itself appended -
// is trustworthy; taking the first entry would let a client bypass this
// rate limit entirely by sending a fake X-Forwarded-For header.
const forwarded = request.headers.get("x-forwarded-for");
+2 -2
View File
@@ -3,13 +3,13 @@ import { createHash, randomBytes } from "node:crypto";
/** How long a freshly issued email-verification link stays valid. */
export const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000;
/** 256 bits of randomness, hex-encoded — not guessable, never derived from user data. */
/** 256 bits of randomness, hex-encoded - not guessable, never derived from user data. */
export function generateVerificationToken(): string {
return randomBytes(32).toString("hex");
}
/**
* Only this hash is ever persisted (see collections/Customers.ts) — the
* Only this hash is ever persisted (see collections/Customers.ts) - the
* plaintext token exists only in the URL sent by email and briefly in
* memory while that email is being sent.
*/
+1 -1
View File
@@ -4,7 +4,7 @@ import { customerBookingView, type CustomerBooking } from "./customerView";
/**
* These are called only after the caller has already verified who the
* current customer is (see lib/auth/customer.ts) — the `where: { user }`
* current customer is (see lib/auth/customer.ts) - the `where: { user }`
* filter below is what actually scopes results to that one customer.
*/
export const getCustomerBookings = cache(async (customerId: number): Promise<CustomerBooking[]> => {
+3 -3
View File
@@ -19,7 +19,7 @@ const WEEKDAY_BY_INDEX = [
// and writes here go through Intl's Berlin timezone data (not local Date
// accessors, and not manual UTC+1/+2 arithmetic), so the server process's
// own timezone (this app runs in UTC in production) never leaks into the
// computed availability — see lib/calendar/timezone.ts for the same pattern.
// computed availability - see lib/calendar/timezone.ts for the same pattern.
function dateKey(date: Date): string {
const { year, month, day } = berlinParts(date);
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
@@ -31,7 +31,7 @@ function calendarDate(date: Date): CalendarDate {
}
function addCalendarDays({ year, month, day }: CalendarDate, delta: number): CalendarDate {
// Pure calendar-rollover arithmetic (month/year boundaries) — no timezone
// Pure calendar-rollover arithmetic (month/year boundaries) - no timezone
// conversion happens here, so using the UTC constructor is safe and simple.
const utc = new Date(Date.UTC(year, month - 1, day + delta));
return { year: utc.getUTCFullYear(), month: utc.getUTCMonth() + 1, day: utc.getUTCDate() };
@@ -58,7 +58,7 @@ function overlaps(aStart: Date, aEnd: Date, bStart: Date, bEnd: Date): boolean {
/**
* Computes bookable slots for one offer across a date range, from the
* configured weekly Availability (plus per-date AvailabilityOverrides),
* minus anything already pending/confirmed on Anna's schedule — she can
* minus anything already pending/confirmed on Anna's schedule - she can
* only do one thing at a time, regardless of which offer it's for.
*/
export async function getAvailableSlots(
+3 -3
View File
@@ -11,13 +11,13 @@ const WINDOW_FUTURE_MS = 400 * 24 * 60 * 60 * 1000;
/**
* Builds the calendar entries for Anna's personal ICS feed: confirmed
* bookings (never pending ones) plus standalone public events — onsite and
* bookings (never pending ones) plus standalone public events - onsite and
* online alike. Deliberately never includes the meeting password anywhere
* in the output, only a link to the protected join page.
*
* Local API is used with the default overrideAccess: true because this is
* only ever reached after the caller already verified the feed's secret
* token (see app/(frontend)/calendar/[tokenFile]/route.ts) — equivalent
* token (see app/(frontend)/calendar/[tokenFile]/route.ts) - equivalent
* trust to the existing admin calendar view.
*/
export async function getAdminFeedICSEvents(payload: Payload): Promise<ICSEvent[]> {
@@ -83,7 +83,7 @@ export async function getAdminFeedICSEvents(payload: Payload): Promise<ICSEvent[
end: new Date(b.endTime),
summary: offer?.title ?? "Termin",
description: description || undefined,
// Vor Ort: vollständige Adresse. Online: literal "Online" — never the
// Vor Ort: vollständige Adresse. Online: literal "Online" - never the
// meeting password, which is intentionally not part of this feed.
location: isOnline ? "Online" : fullAddress || undefined,
url,
+1 -1
View File
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
/**
* Long-lived, random, rotatable secret for a personal calendar-subscription
* URL (/calendar/<token>.ics) — treated like an API key, not a
* URL (/calendar/<token>.ics) - treated like an API key, not a
* short-lived link: only its hash is stored, so a database leak alone
* can't be used to subscribe to anyone's calendar, and rotating it
* (overwriting the stored hash) immediately invalidates the old URL.
+3 -3
View File
@@ -68,7 +68,7 @@ export function berlinDayBounds(date: Date): { key: string; start: Date; end: Da
* representation for iCalendar's local `TZID` date-time form
* (`DTSTART;TZID=Europe/Berlin:YYYYMMDDTHHMMSS`). Uses the ICU timezone
* database via Intl rather than manual UTC+1/+2 arithmetic, so daylight
* saving transitions are always handled correctly — no naive UTC offset
* saving transitions are always handled correctly - no naive UTC offset
* math that would drift by an hour half the year.
*/
export function toBerlinICSDateTime(date: Date): string {
@@ -87,7 +87,7 @@ export function toBerlinICSDateTime(date: Date): string {
return `${get("year")}${get("month")}${get("day")}T${get("hour")}${get("minute")}${get("second")}`;
}
/** UTC timestamp form (`YYYYMMDDTHHMMSSZ`) — used only for DTSTAMP, which marks generation time, not a local event time. */
/** UTC timestamp form (`YYYYMMDDTHHMMSSZ`) - used only for DTSTAMP, which marks generation time, not a local event time. */
export function toUTCICSStamp(date: Date): string {
return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
}
@@ -95,7 +95,7 @@ export function toUTCICSStamp(date: Date): string {
/**
* Standard IANA Europe/Berlin VTIMEZONE block (the same one commonly
* embedded by calendar exporters): CEST from the last Sunday in March,
* CET from the last Sunday in October — the actual EU DST rule, not an
* CET from the last Sunday in October - the actual EU DST rule, not an
* approximation.
*/
export const BERLIN_VTIMEZONE_LINES = [
+1 -1
View File
@@ -98,7 +98,7 @@ export function bookingRejectedEmail(args: BaseArgs): { subject: string; html: s
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
]),
paragraph("Melde dich gerne für einen neuen Termin – schau einfach wieder in deinem Konto vorbei.", { small: true }),
paragraph("Melde dich gerne für einen neuen Termin - schau einfach wieder in deinem Konto vorbei.", { small: true }),
].join("\n"),
});
return { subject: "Deine Terminanfrage bei ANOUMA", html };
+2 -2
View File
@@ -39,9 +39,9 @@ export function renderFactBox(rows: { label: string; value: string }[]): string
</table>`;
}
/** Shared table-based, inline-styled email shell — same design language across all ANOUMA emails. */
/** Shared table-based, inline-styled email shell - same design language across all ANOUMA emails. */
export function renderEmailShell(args: { preheader?: string; bodyHtml: string; footerText?: string }): string {
const { bodyHtml, footerText = "Räume für Verbindung – mit dir selbst, miteinander und mit der Natur." } = args;
const { bodyHtml, footerText = "Räume für Verbindung - mit dir selbst, miteinander und mit der Natur." } = args;
return `<!doctype html>
<html lang="de">
+1 -1
View File
@@ -76,7 +76,7 @@ export function renderReminderEmailHtml(data: ReminderEmailData): string {
</tr>
<tr>
<td style="padding:20px 32px;background:${colors.cream};text-align:center;">
<span style="color:${colors.plum};font-size:12px;">Räume für Verbindung – mit dir selbst, miteinander und mit der Natur.</span>
<span style="color:${colors.plum};font-size:12px;">Räume für Verbindung - mit dir selbst, miteinander und mit der Natur.</span>
</td>
</tr>
</table>
+1 -1
View File
@@ -2,7 +2,7 @@ import nodemailer from "nodemailer";
let cached: nodemailer.Transporter | null = null;
/** SMTP transporter for the existing ANOUMA mail system — no external newsletter service. */
/** SMTP transporter for the existing ANOUMA mail system - no external newsletter service. */
export function getMailTransport(): nodemailer.Transporter {
if (cached) return cached;
+2 -2
View File
@@ -1,6 +1,6 @@
import { formatBerlinDate, formatBerlinTime } from "@/lib/calendar/timezone";
/** Day + short month, e.g. { day: "15", month: "SEP" } — for the date badge on event cards. */
/** Day + short month, e.g. { day: "15", month: "SEP" } - for the date badge on event cards. */
export function formatEventDateParts(iso: string) {
const date = new Date(iso);
return {
@@ -23,7 +23,7 @@ export function formatTime(iso: string) {
}
export function formatTimeRange(start?: string | null, end?: string | null) {
if (start && end) return `${formatTime(start)} – ${formatTime(end)} Uhr`;
if (start && end) return `${formatTime(start)} - ${formatTime(end)} Uhr`;
if (start) return `${formatTime(start)} Uhr`;
return null;
}
+2 -2
View File
@@ -1,11 +1,11 @@
import { randomInt } from "node:crypto";
// Unambiguous alphabet (no 0/O/1/I/l) — easier to read aloud/type correctly.
// Unambiguous alphabet (no 0/O/1/I/l) - easier to read aloud/type correctly.
const ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
/**
* Cryptographically random meeting password, e.g. "K7mQ-42Px". Independent
* of the event's id/slug — never derive it from the meeting identifier.
* of the event's id/slug - never derive it from the meeting identifier.
*/
export function generateMeetingPassword(): string {
const chars = Array.from({ length: 8 }, () => ALPHABET[randomInt(ALPHABET.length)]);
+4 -4
View File
@@ -20,7 +20,7 @@ function formatTime(date: Date): string {
/**
* Checks all upcoming online events for due 60/30-minute reminders and sends
* them — to registered, non-cancelled participants and (optionally) to the
* them - to registered, non-cancelled participants and (optionally) to the
* admin host. Idempotent: every send is guarded by a "...Sent" flag so a
* cron job that runs every few minutes never double-sends.
*/
@@ -74,7 +74,7 @@ export async function runEventReminders() {
dateLabel,
timeLabel,
joinUrl,
meetingPassword: event.meetingPassword || "—",
meetingPassword: event.meetingPassword || "-",
minutesBefore: threshold.minutes,
});
results.emailsSent++;
@@ -82,7 +82,7 @@ export async function runEventReminders() {
await payload.update({ collection: "events", id: event.id, data: { [threshold.hostField]: true } });
}
// Participant reminders — only registered (not cancelled), not yet sent.
// Participant reminders - only registered (not cancelled), not yet sent.
const { docs: registrations } = await payload.find({
collection: "event-registrations",
where: {
@@ -102,7 +102,7 @@ export async function runEventReminders() {
dateLabel,
timeLabel,
joinUrl,
meetingPassword: event.meetingPassword || "—",
meetingPassword: event.meetingPassword || "-",
minutesBefore: threshold.minutes,
});
results.emailsSent++;
+1 -1
View File
@@ -4,7 +4,7 @@ export type MeetingStatus = "scheduled" | "open" | "live" | "ended";
/**
* Payload's "time only" date fields still store a full ISO timestamp, just
* with an arbitrary date portion — this combines the real event date with
* with an arbitrary date portion - this combines the real event date with
* the hour/minute from a time-only field into one correct Date.
*/
export function combineDateAndTime(dateISO: string, timeISO?: string | null): Date {
+2 -2
View File
@@ -6,13 +6,13 @@ export type MeetingTokenPayload = {
eventSlug: string;
participantId: string;
// Self-declared, persisted client-side (localStorage, not sessionStorage)
// identity for participants — participantId is fresh per /join call, so
// identity for participants - participantId is fresh per /join call, so
// without this a kick could always be undone by simply rejoining. Only
// used to make a kick stick across a rejoin (see rooms.ts kickedClientIds);
// never used to key live room membership, so a client choosing an
// arbitrary/colliding value can only affect their own future kick status,
// never another participant's connection. Still bypassable by clearing
// localStorage or joining from a different browser — there's no real
// localStorage or joining from a different browser - there's no real
// identity to anchor to for an unauthenticated participant.
clientId?: string;
name: string;
+2 -2
View File
@@ -7,7 +7,7 @@ import type { Event, Offer, Post } from "@/payload-types";
//
// IMPORTANT: Payload's Local API defaults `overrideAccess` to `true`, which
// SKIPS each collection's `access.read` check entirely (see
// node_modules/payload/dist/collections/operations/find.js) — it does not
// node_modules/payload/dist/collections/operations/find.js) - it does not
// automatically hide drafts. Every fetcher that feeds a *public* page must
// therefore pass `overrideAccess: false` so the collection's own
// `publishedOrAdmin` access rule actually filters out unpublished documents.
@@ -83,7 +83,7 @@ export const getEventBySlug = cache(async (slug: string): Promise<Event | null>
* Unlike `getEventBySlug`, this intentionally does NOT filter by publish
* status. Confirmed private bookings create an unpublished/private linked
* Event (see collections/BookingRequests.ts) that must still be reachable
* for the one customer who received its join link + password — "unlisted",
* for the one customer who received its join link + password - "unlisted",
* not "hidden". Only used by the join/beitreten flow, never for listings.
*/
export const getEventForJoin = cache(async (slug: string): Promise<Event | null> => {
+3 -3
View File
@@ -9,8 +9,8 @@ function stripTrailingSlash(url: string): string {
* The canonical base URL for the whole site. Prefers the CMS override
* (SEO-Einstellungen → Website-URL) when it's a valid absolute URL, then
* NEXT_PUBLIC_SERVER_URL (the single source of truth used everywhere else
* in this app — emails, meeting links, Payload's own serverURL), then the
* hardcoded siteConfig fallback. Never throws — SEO metadata must never
* in this app - emails, meeting links, Payload's own serverURL), then the
* hardcoded siteConfig fallback. Never throws - SEO metadata must never
* break a page render.
*/
export async function getSiteUrl(): Promise<string> {
@@ -20,7 +20,7 @@ export async function getSiteUrl(): Promise<string> {
return stripTrailingSlash(settings.siteUrl);
}
} catch {
// CMS/DB unreachable — fall through to the env-based default.
// CMS/DB unreachable - fall through to the env-based default.
}
return stripTrailingSlash(process.env.NEXT_PUBLIC_SERVER_URL || siteConfig.domain);
}
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Formats a JS Date as an ISO 8601 string carrying its actual Europe/Berlin
* UTC offset (e.g. "2026-06-15T09:00:00+02:00" in summer,
* "...+01:00" in winter) — what schema.org's Event startDate/endDate
* "...+01:00" in winter) - what schema.org's Event startDate/endDate
* expect. Uses Intl's timezone database rather than hardcoded +1/+2 math,
* so it's correct across the DST transition automatically.
*/
+6 -6
View File
@@ -2,7 +2,7 @@ import { siteConfig } from "@/lib/site";
import { toBerlinOffsetISOString } from "./dates";
import type { BookingSetting, Contact, Event, Offer, Post, SeoSetting } from "@/payload-types";
// Server-only builders — every function here returns `null` instead of
// Server-only builders - every function here returns `null` instead of
// guessing when the real data needed for a valid/honest result isn't
// present. Nothing here ever invents a review, price, address or phone
// number (see section 27/35 of the SEO brief).
@@ -34,7 +34,7 @@ function postalAddress(booking: BookingSetting) {
}
/**
* Organization (always safe — just a name and a URL) upgraded to
* Organization (always safe - just a name and a URL) upgraded to
* ProfessionalService/LocalBusiness once a real, complete street address
* has actually been entered in the CMS. Phone/opening hours/geo are added
* only when present; nothing is fabricated to "complete" the schema.
@@ -107,7 +107,7 @@ export function breadcrumbJsonLd(items: { name: string; url?: string }[]): JsonL
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
// The last item's "item" URL is deliberately omitted when absent —
// The last item's "item" URL is deliberately omitted when absent -
// that's the current page, and per Google's guidelines a BreadcrumbList
// entry doesn't need a URL for the page the visitor is already on.
itemListElement: items.map((item, index) => ({
@@ -137,7 +137,7 @@ export function serviceJsonLd(args: {
};
if (args.imageUrl) result.image = args.imageUrl;
if (args.areaServed) result.areaServed = args.areaServed;
// Deliberately no "offers"/price — the CMS "price" field is free text
// Deliberately no "offers"/price - the CMS "price" field is free text
// (e.g. "auf Anfrage") and can't be turned into a valid structured price
// without risking incorrect data.
return result;
@@ -154,7 +154,7 @@ export function eventJsonLd(args: {
}): JsonLdValue | null {
const { event } = args;
// Private single-session bookings must never surface as a public Event
// (see section 18) — defense in depth alongside the page-level checks
// (see section 18) - defense in depth alongside the page-level checks
// that already keep them out of public listings entirely.
if (event.isPrivateBooking) return null;
if (!event.date) return null;
@@ -186,7 +186,7 @@ export function eventJsonLd(args: {
address: postalAddress(args.booking),
};
} else if (event.location) {
// No structured address on file, but the event itself names a place —
// No structured address on file, but the event itself names a place -
// still valid schema.org (address is optional on Place).
result.location = { "@type": "Place", name: event.location };
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { getSiteUrl, isSiteIndexable } from "./config";
/**
* Single shared Metadata builder used by every public page's
* generateMetadata — guarantees every page gets its own canonical, robots,
* generateMetadata - guarantees every page gets its own canonical, robots,
* Open Graph and Twitter/X card data instead of copy-pasted boilerplate
* (and the accidental duplicate-description bugs that come with that).
*
+1 -1
View File
@@ -4,7 +4,7 @@ import { getCMS } from "@/lib/payload/getPayload";
/**
* Looks up a CMS-configured redirect for a changed slug (see
* collections/Redirects.ts). Only called from the three dynamic [slug]
* detail pages right before they'd otherwise 404 — not a catch-all
* detail pages right before they'd otherwise 404 - not a catch-all
* middleware, so this never adds a database lookup to the other ~30 routes
* that never need it.
*/
+2 -2
View File
@@ -1,9 +1,9 @@
export const siteConfig = {
name: "Anouma",
domain: "https://anouma.org",
title: "Anouma – Prozessbegleitung, Doula & Gemeinschaft",
title: "Anouma - Prozessbegleitung, Doula & Gemeinschaft",
description:
"Anouma begleitet dich in Lebensphasen, Schwangerschaft und Geburt sowie in Kinder- und Singkreisen – mit Naturverbundenheit, Vertrauen und einem geschützten Raum für deinen eigenen Weg.",
"Anouma begleitet dich in Lebensphasen, Schwangerschaft und Geburt sowie in Kinder- und Singkreisen - mit Naturverbundenheit, Vertrauen und einem geschützten Raum für deinen eigenen Weg.",
locale: "de_DE",
} as const;
+43 -43
View File
@@ -1,70 +1,70 @@
/**
* Original Anouma copy, transcribed verbatim from texte.txt and grouped by
* topic. Do not paraphrase content sourced from texte.txt — extend this file
* topic. Do not paraphrase content sourced from texte.txt - extend this file
* instead of duplicating strings inline in pages/components.
*/
export const maedchenkreis = {
title: "Mädchenkreis – verbunden mit dir, der Natur und dem Kreis",
title: "Mädchenkreis - verbunden mit dir, der Natur und dem Kreis",
paragraphs: [
"Der Übergang vom Mädchen zur jungen Frau ist eine besondere Zeit. Vieles beginnt sich zu verändern – im Körper, in den Gefühlen und in der Art, sich selbst und die Welt wahrzunehmen.",
"Der Übergang vom Mädchen zur jungen Frau ist eine besondere Zeit. Vieles beginnt sich zu verändern - im Körper, in den Gefühlen und in der Art, sich selbst und die Welt wahrzunehmen.",
"Der Mädchenkreis für Mädchen von 10 bis 14 Jahren schenkt Raum, diesen Weg bewusst und in Gemeinschaft zu erleben. Einen Raum, in dem jede so sein darf, wie sie ist, Fragen ihren Platz haben und die Mädchen sich selbst und einander auf neue Weise kennenlernen können.",
"Gemeinsam beschäftigen wir uns mit Themen wie körperlichen Veränderungen, Weiblichkeit, dem eigenen Frau-Werden und der Frage: Was bedeutet es eigentlich, eine Frau zu sein? Dabei geht es nicht darum, fertige Antworten zu finden, sondern die eigene Wahrnehmung zu stärken und den ganz persönlichen Weg zu entdecken.",
"Kreativität und Naturverbundenheit begleiten uns durch unsere gemeinsame Zeit. Wir arbeiten mit unterschiedlichen Materialien – mit Ton, Farben und Naturmaterialien, malen, weben, gestalten und lassen unseren Händen und unserer Fantasie freien Raum. Wir sind draußen in der Natur, nehmen die Jahreszeiten bewusst wahr und feiern gemeinsam Jahreskreisfeste.",
"Kreativität und Naturverbundenheit begleiten uns durch unsere gemeinsame Zeit. Wir arbeiten mit unterschiedlichen Materialien - mit Ton, Farben und Naturmaterialien, malen, weben, gestalten und lassen unseren Händen und unserer Fantasie freien Raum. Wir sind draußen in der Natur, nehmen die Jahreszeiten bewusst wahr und feiern gemeinsam Jahreskreisfeste.",
"Mit der Zeit darf so ein vertrauter und geschützter Raum entstehen, in dem die Mädchen sich als Teil einer Gemeinschaft erleben und sicher fühlen können. Ein Kreis, in dem jede mit allem da sein darf, was zu ihr gehört: mit ihren ganz eigenen Gaben und Fähigkeiten, ihrer Lebendigkeit und Freude, aber ebenso mit ihren Unsicherheiten, Ängsten und Schattenseiten. Die Mädchen dürfen erfahren, dass all diese Facetten Teil von ihnen sind und ihren Platz haben.",
"Der Mädchenkreis möchte sie darin bestärken, sich selbst kennenzulernen, sich mit anderen Mädchen verbunden zu fühlen und dem eigenen Frau-Werden mit Neugier und Vertrauen zu begegnen. Frau-Sein darf dabei als etwas Schönes, Lebendiges und Kraftvolles erfahren werden.",
],
} as const;
export const erdenkinder = {
title: "Erdenkinder – draußen zuhause",
title: "Erdenkinder - draußen zuhause",
paragraphs: [
"Die Erdenkinder sind eine Naturgruppe für Kinder von 5 bis 10 Jahren. Wir treffen uns im Wald – und wir sind draußen, bei Sonne, Wind und Regen. Die Natur ist dabei nicht nur der Ort, an dem wir uns treffen. Sie ist unser Zuhause, unsere Lehrerin und unsere Begleiterin. Wir möchten erfahren: Wir sind nicht nur in der Natur – wir sind ein Teil von ihr. Verbunden mit den Tieren, den Pflanzen, der Erde und miteinander.",
"Die Erdenkinder sind eine Naturgruppe für Kinder von 5 bis 10 Jahren. Wir treffen uns im Wald - und wir sind draußen, bei Sonne, Wind und Regen. Die Natur ist dabei nicht nur der Ort, an dem wir uns treffen. Sie ist unser Zuhause, unsere Lehrerin und unsere Begleiterin. Wir möchten erfahren: Wir sind nicht nur in der Natur - wir sind ein Teil von ihr. Verbunden mit den Tieren, den Pflanzen, der Erde und miteinander.",
"Gemeinsam ziehen wir los, entdecken und beobachten, sammeln und staunen. Wir singen, bauen, basteln und gestalten mit dem, was wir finden. Wir machen Feuer, kochen gemeinsam und lernen, uns draußen immer vertrauter zu bewegen. Dabei orientieren wir uns am Lauf der Jahreszeiten und erleben, wie sich der Wald und seine Bewohner im Laufe des Jahres verändern.",
"Es geht dabei nicht darum, möglichst viel Wissen über die Natur anzusammeln. Vielmehr dürfen die Kinder mit allen Sinnen erleben: Erde unter den Händen spüren, dem Wind und den Vögeln lauschen, den Duft des Waldes wahrnehmen, Feuer entzünden, gemeinsam etwas erschaffen und immer wieder Neues entdecken. Aus diesen unmittelbaren Erfahrungen kann eine tiefe und selbstverständliche Verbindung zur Natur wachsen.",
"Genauso wichtig wie die Verbindung zur Natur ist uns die Gemeinschaft untereinander. Mit der Zeit darf aus der Gruppe ein kleiner, vertrauter „Stamm“ entstehen. Ein Ort, an dem jedes Kind mit seinen ganz eigenen Gaben, Fähigkeiten und Ideen willkommen und wichtig ist. Jedes Kind darf seinen Platz finden, sich einbringen und erfahren: Ich gehöre dazu und ich kann etwas zu unserer Gemeinschaft beitragen.",
"In dieser gemeinsamen Zeit können Freundschaften wachsen, Vertrauen entstehen und die Kinder erleben, wie schön es ist, miteinander unterwegs zu sein, füreinander da zu sein und voneinander zu lernen – verbunden mit sich selbst, miteinander und mit der Erde.",
"In dieser gemeinsamen Zeit können Freundschaften wachsen, Vertrauen entstehen und die Kinder erleben, wie schön es ist, miteinander unterwegs zu sein, füreinander da zu sein und voneinander zu lernen - verbunden mit sich selbst, miteinander und mit der Erde.",
],
} as const;
export const singkreiseIntro = {
title: "Singkreise – miteinander singen, klingen und sein",
title: "Singkreise - miteinander singen, klingen und sein",
paragraphs: [
"Singen verbindet. Mit uns selbst, mit den Menschen um uns herum und mit etwas, das über uns hinausgeht. In meinen Singkreisen geht es nicht darum, „schön“ oder „richtig“ zu singen. Es braucht keine Vorkenntnisse. Im Mittelpunkt steht die Freude daran, die eigene Stimme erklingen zu lassen, miteinander zu schwingen und Gemeinschaft zu erleben.",
],
} as const;
export const singenImKreis = {
title: "Offener Singkreis – Lieder für die Erde und das Leben",
title: "Offener Singkreis - Lieder für die Erde und das Leben",
paragraphs: [
"Dieser Singkreis ist offen für alle Menschen und findet auf Spendenbasis statt. Wir kommen zusammen und singen Herzenslieder – für die Erde, für die Liebe und für all die verschiedenen Facetten unseres Lebens.",
"Das gemeinsame Singen darf dabei mehr sein als Musik. Es kann eine Form der Begegnung und Kommunikation sein, ein gemeinsames Schwingen und Klingen – und manchmal vielleicht auch eine Art Gebet. Wir erleben, was entsteht, wenn viele einzelne Stimmen zu einem gemeinsamen Klang werden.",
"Dieser Singkreis ist offen für alle Menschen und findet auf Spendenbasis statt. Wir kommen zusammen und singen Herzenslieder - für die Erde, für die Liebe und für all die verschiedenen Facetten unseres Lebens.",
"Das gemeinsame Singen darf dabei mehr sein als Musik. Es kann eine Form der Begegnung und Kommunikation sein, ein gemeinsames Schwingen und Klingen - und manchmal vielleicht auch eine Art Gebet. Wir erleben, was entsteht, wenn viele einzelne Stimmen zu einem gemeinsamen Klang werden.",
"Vorkenntnisse braucht es keine. Wer ein Instrument spielt, ist herzlich eingeladen, es mitzubringen und unseren Kreis damit zu bereichern.",
],
} as const;
export const singenFuerSchwangere = {
title: "Singkreis für Schwangere – Zeit für dich und dein Baby",
title: "Singkreis für Schwangere - Zeit für dich und dein Baby",
paragraphs: [
"Die Schwangerschaft ist eine ganz besondere Zeit des Wandels und der Verbindung. Dieser Kreis lädt dich dazu ein, dir ganz bewusst Zeit für dich und dein Baby zu nehmen, innezuhalten und diese besondere Lebensphase wahrzunehmen.",
"Wir singen gemeinsam einfache Lieder, Herzens- und Kinderlieder, die dich schon während der Schwangerschaft begleiten können – und vielleicht auch später zu vertrauten Liedern für dich und dein Kind werden.",
"Wir singen gemeinsam einfache Lieder, Herzens- und Kinderlieder, die dich schon während der Schwangerschaft begleiten können - und vielleicht auch später zu vertrauten Liedern für dich und dein Kind werden.",
"Gleichzeitig gibt es Raum, andere schwangere Frauen kennenzulernen, sich auszutauschen und miteinander zu teilen, was euch in dieser Zeit bewegt. Kleine Meditationen und wohltuende Momente des Innehaltens können den Kreis ergänzen.",
"Ein Raum zum Singen, Begegnen, Kraftschöpfen und Verbinden – mit dir selbst, deinem Baby und anderen Frauen.",
"Ein Raum zum Singen, Begegnen, Kraftschöpfen und Verbinden - mit dir selbst, deinem Baby und anderen Frauen.",
],
} as const;
export const mamaBabySingkreis = {
title: "Mama-Baby-Singkreis – Zeit für euch",
title: "Mama-Baby-Singkreis - Zeit für euch",
paragraphs: [
"Dieser Singkreis ist für Mamas mit Babys bis zum Krabbelalter. Er möchte euch einen liebevollen Raum schenken, in dem ihr für eine Weile aus dem Alltag aussteigen und euch ganz bewusst Zeit füreinander nehmen könnt.",
"Wir singen einfache Lieder und Kinderlieder, spielen kleine Spiele und entdecken gemeinsam Klänge, Bewegungen und Berührungen. Dabei darf das Singen zu etwas Verbindendem werden – die vertraute Stimme der Mama, die Nähe, das gemeinsame Lauschen und Schwingen. Es braucht nichts weiter als euch und die Freude daran, miteinander zu sein.",
"Und auch für dich als Mama soll dieser Kreis ein wohltuender Ort sein. Ein Ort, an dem du einfach ankommen darfst – mit allem, was gerade da ist. Es gibt Zeit für Begegnung und Austausch mit anderen Mamas, für gemeinsames Lachen, Teilen und Kennenlernen. Vielleicht entstehen daraus neue Verbindungen, die auch über den Singkreis hinaus tragen.",
"So darf eine kleine Gemeinschaft wachsen, in der Mamas und Babys sich willkommen und getragen fühlen – eine besondere gemeinsame Zeit voller Lieder, Nähe, Begegnung und kleiner Momente, die im Alltag manchmal viel zu schnell vorüberziehen.",
"Wir singen einfache Lieder und Kinderlieder, spielen kleine Spiele und entdecken gemeinsam Klänge, Bewegungen und Berührungen. Dabei darf das Singen zu etwas Verbindendem werden - die vertraute Stimme der Mama, die Nähe, das gemeinsame Lauschen und Schwingen. Es braucht nichts weiter als euch und die Freude daran, miteinander zu sein.",
"Und auch für dich als Mama soll dieser Kreis ein wohltuender Ort sein. Ein Ort, an dem du einfach ankommen darfst - mit allem, was gerade da ist. Es gibt Zeit für Begegnung und Austausch mit anderen Mamas, für gemeinsames Lachen, Teilen und Kennenlernen. Vielleicht entstehen daraus neue Verbindungen, die auch über den Singkreis hinaus tragen.",
"So darf eine kleine Gemeinschaft wachsen, in der Mamas und Babys sich willkommen und getragen fühlen - eine besondere gemeinsame Zeit voller Lieder, Nähe, Begegnung und kleiner Momente, die im Alltag manchmal viel zu schnell vorüberziehen.",
],
} as const;
export const prozessbegleitung = {
title: "Prozessbegleitung – den eigenen Weg wiederfinden",
title: "Prozessbegleitung - den eigenen Weg wiederfinden",
intro: [
"Manchmal kommen wir im Leben an einen Punkt, an dem wir nicht mehr weiterwissen. Etwas verändert sich, eine Entscheidung steht an, alte Ängste oder Muster zeigen sich oder das, was bisher getragen hat, fühlt sich plötzlich nicht mehr stimmig an.",
"In solchen Zeiten kann es hilfreich sein, ein Stück des Weges begleitet zu werden.",
@@ -75,25 +75,25 @@ export const prozessbegleitung = {
{
heading: "Individuelle Begleitung",
paragraphs: [
"Vielleicht gibt es gerade etwas in deinem Leben, das dich bewegt, verunsichert oder nicht zur Ruhe kommen lässt. Vielleicht stehst du an einer Schwelle, fühlst dich festgefahren, begegnest immer wieder den gleichen Ängsten oder Mustern – oder du spürst einfach, dass sich etwas verändern möchte, ohne schon genau zu wissen, was.",
"Vielleicht gibt es gerade etwas in deinem Leben, das dich bewegt, verunsichert oder nicht zur Ruhe kommen lässt. Vielleicht stehst du an einer Schwelle, fühlst dich festgefahren, begegnest immer wieder den gleichen Ängsten oder Mustern - oder du spürst einfach, dass sich etwas verändern möchte, ohne schon genau zu wissen, was.",
"Du musst damit nicht alleine sein.",
"In der Einzelbegleitung öffne und halte ich einen geschützten Raum für dich und das, was gerade da ist. Einen Raum, in dem du dich zeigen, aussprechen, fühlen und dir selbst begegnen darfst – ohne etwas leisten oder bereits eine Antwort haben zu müssen.",
"In der Einzelbegleitung öffne und halte ich einen geschützten Raum für dich und das, was gerade da ist. Einen Raum, in dem du dich zeigen, aussprechen, fühlen und dir selbst begegnen darfst - ohne etwas leisten oder bereits eine Antwort haben zu müssen.",
"Ich begleite dich intuitiv und mit unterschiedlichen Zugängen. Manchmal braucht es vor allem ein Gespräch und ein Gegenüber, das wirklich zuhört. Manchmal dürfen wir tiefer schauen, wahrnehmen, was hinter einem Thema liegt, oder über Aufstellungsarbeit, Körperwahrnehmung und andere Impulse neue Zugänge entstehen lassen.",
"Ein wesentlicher Teil meiner Arbeit ist für mich das Vertrauen in die Selbstheilungskräfte und die innere Weisheit jedes Menschen. Ich möchte nicht für dich wissen, wohin dein Weg gehen soll. Vielmehr halte und begleite ich einen Raum, in dem du dich wieder mit dir selbst verbinden und deinen eigenen Impulsen, deiner Kraft und deinen Möglichkeiten begegnen kannst.",
"Denn manchmal geht es weniger darum, etwas im Außen zu finden – sondern darum, sich an das zu erinnern, was in einem selbst längst da ist.",
"Denn manchmal geht es weniger darum, etwas im Außen zu finden - sondern darum, sich an das zu erinnern, was in einem selbst längst da ist.",
],
},
{
heading: "Die Natur als Begleiterin",
paragraphs: [
"Manche Prozesse führen uns nach draußen. Die Natur kann einen besonderen Raum eröffnen, um stiller zu werden, wahrzunehmen und dem eigenen Inneren auf eine andere Weise zu begegnen.",
"Dabei arbeite ich unter anderem mit Schwellengängen und dem Medizinrad. Übergänge können bewusst begangen, Fragen mit hinaus in die Natur genommen und neue Blickwinkel entdeckt werden. Nicht immer muss eine Antwort mit dem Verstand gefunden werden – manchmal entsteht etwas, indem wir wahrnehmen, lauschen und uns wieder mit uns selbst und unserer Umgebung verbinden.",
"Dabei arbeite ich unter anderem mit Schwellengängen und dem Medizinrad. Übergänge können bewusst begangen, Fragen mit hinaus in die Natur genommen und neue Blickwinkel entdeckt werden. Nicht immer muss eine Antwort mit dem Verstand gefunden werden - manchmal entsteht etwas, indem wir wahrnehmen, lauschen und uns wieder mit uns selbst und unserer Umgebung verbinden.",
],
},
{
heading: "Aufstellungsarbeit",
paragraphs: [
"Auch die Aufstellungsarbeit kann Teil der Prozessbegleitung sein – sowohl im Einzelsetting als auch in der Gruppe. Sie bietet die Möglichkeit, Beziehungen, innere Dynamiken und wiederkehrende Muster sichtbar zu machen und aus einer anderen Perspektive zu betrachten.",
"Auch die Aufstellungsarbeit kann Teil der Prozessbegleitung sein - sowohl im Einzelsetting als auch in der Gruppe. Sie bietet die Möglichkeit, Beziehungen, innere Dynamiken und wiederkehrende Muster sichtbar zu machen und aus einer anderen Perspektive zu betrachten.",
"Welche Form der Begleitung passend ist, muss nicht vorher feststehen. Wir schauen gemeinsam, was dein Anliegen gerade braucht und welcher Weg sich daraus entwickeln darf.",
],
},
@@ -101,15 +101,15 @@ export const prozessbegleitung = {
} as const;
export const doula = {
title: "Doula-Begleitung – getragen durch die Zeit des Werdens",
title: "Doula-Begleitung - getragen durch die Zeit des Werdens",
intro: [
"Mit dem Wunsch nach einem Kind beginnt eine ganz besondere Reise. Eine Zeit, in der Neues wachsen und entstehen darf – getragen von Hoffnung, Vorfreude und der leisen Ahnung von all dem, was kommen mag. Ein Weg voller Wandlung, auf dem du dir selbst, deiner Kraft und dem Wunder des Lebens auf ganz neue Weise begegnen darfst.",
"Mit dem Wunsch nach einem Kind beginnt eine ganz besondere Reise. Eine Zeit, in der Neues wachsen und entstehen darf - getragen von Hoffnung, Vorfreude und der leisen Ahnung von all dem, was kommen mag. Ein Weg voller Wandlung, auf dem du dir selbst, deiner Kraft und dem Wunder des Lebens auf ganz neue Weise begegnen darfst.",
"Kinderwunsch, Schwangerschaft, Geburt und Wochenbett sind Zeiten tiefgreifender Veränderung. Nicht nur ein Kind wird geboren. Auch eine Frau wandelt sich, eine Mutter wird geboren und eine Familie findet neu zueinander.",
"Als Doula möchte ich dich auf diesem Weg begleiten – von Frau zu Frau, achtsam, liebevoll und in tiefem Vertrauen in die natürlichen Kräfte, die in dir wirken.",
"Als Doula möchte ich dich auf diesem Weg begleiten - von Frau zu Frau, achtsam, liebevoll und in tiefem Vertrauen in die natürlichen Kräfte, die in dir wirken.",
],
sections: [
{
heading: "Schwangerschaft – eine Zeit des Wachsens",
heading: "Schwangerschaft - eine Zeit des Wachsens",
paragraphs: [
"Schwangerschaft ist eine Zeit zwischen den Welten. Während in deinem Körper neues Leben heranwächst, darf auch in dir etwas Neues entstehen.",
"Ich möchte dir Raum schenken, langsamer zu werden, nach innen zu lauschen und die Verbindung zu dir und deinem Kind bewusst wahrzunehmen.",
@@ -119,23 +119,23 @@ export const doula = {
],
},
{
heading: "Geburt – in deiner weiblichen Kraft",
heading: "Geburt - in deiner weiblichen Kraft",
paragraphs: [
"Gebären ist eine uralte weibliche Kraft.",
"Dein Körper trägt ein tiefes Wissen in sich. Als Doula möchte ich dich darin bestärken, diesem Wissen wieder zu lauschen und deinen ganz eigenen Weg durch die Geburt zu gehen.",
"Während der Geburt bin ich an deiner Seite. Ich halte den Raum, bleibe bei dir, erinnere dich an deine Kraft und unterstütze dich darin, mit dir und deinem Körper verbunden zu bleiben.",
"Jede Geburt ist so einzigartig wie die Frau und das Kind, die sich gemeinsam auf diesen Weg machen. Selbstbestimmte Geburt bedeutet für mich, diesem ganz eigenen Weg Raum zu geben – im Vertrauen auf deinen Körper, deine Wahrnehmung und deine Bedürfnisse und mit Menschen an deiner Seite, die dich darin achten und stärken.",
"Jede Geburt ist so einzigartig wie die Frau und das Kind, die sich gemeinsam auf diesen Weg machen. Selbstbestimmte Geburt bedeutet für mich, diesem ganz eigenen Weg Raum zu geben - im Vertrauen auf deinen Körper, deine Wahrnehmung und deine Bedürfnisse und mit Menschen an deiner Seite, die dich darin achten und stärken.",
"Selbstbestimmung bedeutet für mich, dich selbst in diesem Geschehen nicht zu verlieren. Wahrgenommen und gehört zu werden, deine Bedürfnisse und Grenzen spüren und ausdrücken zu dürfen und Entscheidungen bewusst mitzutragen.",
"So darf Geburt – in welcher Form sie sich auch zeigt – möglichst aus einem Gefühl von Verbundenheit, Vertrauen und eigener Kraft heraus erlebt werden.",
"So darf Geburt - in welcher Form sie sich auch zeigt - möglichst aus einem Gefühl von Verbundenheit, Vertrauen und eigener Kraft heraus erlebt werden.",
],
},
{
heading: "Wochenbett – Zeit zum Ankommen",
heading: "Wochenbett - Zeit zum Ankommen",
paragraphs: [
"Nach der Geburt beginnt eine leise und kostbare Zeit.",
"Ein Kind ist angekommen. Eine Mutter ist geboren. Eine Familie ordnet sich neu.",
"Das Wochenbett darf eine Zeit des Rückzugs, der Wärme und des Behütetseins sein. Eine Zeit, in der nicht das Funktionieren im Vordergrund stehen muss, sondern das Ankommen, Kennenlernen und Zusammenwachsen.",
"Auch hier begleite ich dich und eure Familie. Ich höre zu, bin da, stärke und halte Raum – damit neben deinem Baby auch du als Frau und Mutter gesehen und umsorgt sein darfst.",
"Auch hier begleite ich dich und eure Familie. Ich höre zu, bin da, stärke und halte Raum - damit neben deinem Baby auch du als Frau und Mutter gesehen und umsorgt sein darfst.",
],
},
{
@@ -144,7 +144,7 @@ export const doula = {
"Nicht jedes Kind, das sich auf den Weg zu uns macht, bleibt.",
"Wenn eine Schwangerschaft früh endet oder ihr euer Kind still gebären müsst, kann eine ganz andere Form der Begleitung notwendig werden.",
"Auch auf diesem Weg möchte ich Frauen und Familien nicht alleine lassen. Ich halte einen geschützten Raum für Abschied und Trauer, für Liebe und Verbundenheit, für Stille, Tränen und all das, wofür manchmal noch keine Worte da sind.",
"Vielleicht darf auch ein kleines Ritual entstehen, das hilft, dieses Kind willkommen zu heißen und zugleich wieder gehen zu lassen – und ihm einen Platz in der eigenen Geschichte und im Herzen zu geben.",
"Vielleicht darf auch ein kleines Ritual entstehen, das hilft, dieses Kind willkommen zu heißen und zugleich wieder gehen zu lassen - und ihm einen Platz in der eigenen Geschichte und im Herzen zu geben.",
"Diese Begleitung bei stillen Geburten und frühen Verlusten biete ich kostenfrei an.",
],
},
@@ -155,29 +155,29 @@ export const doula = {
"Ich möchte dich nicht führen und dir nicht sagen, wie dein Weg aussehen soll. Ich möchte dir zuhören, dich erinnern, dich stärken und einen Raum halten, in dem du deinen eigenen Antworten begegnen kannst.",
"Meine Begleitung kann emotional, körperorientiert und praktisch sein und sich ganz danach richten, was du und deine Familie in diesem Moment brauchen.",
"Dabei führe ich keine medizinischen Tätigkeiten durch und ersetze weder Hebamme noch ärztliche Begleitung. Meine Arbeit versteht sich als ergänzende Begleitung.",
"Im Mittelpunkt steht für mich das Vertrauen in das Leben und in die Kräfte des Werdens – und die Erinnerung daran, dass dein Weg zu deinem Kind dein ganz eigener sein darf.",
"Im Mittelpunkt steht für mich das Vertrauen in das Leben und in die Kräfte des Werdens - und die Erinnerung daran, dass dein Weg zu deinem Kind dein ganz eigener sein darf.",
],
},
],
} as const;
export const ueberMich = {
title: "Über mich – mein Weg",
title: "Über mich - mein Weg",
paragraphs: [
"Ich bin Waldorfpädagogin, Musikerin, Prozessbegleiterin, Doula und Mama von drei Kindern. Und doch erzählen all diese Bezeichnungen nur einen kleinen Teil davon, wer ich bin.",
"Mein Leben empfinde ich als eine Reise – eine Reise, die mich über viele Wege und Wandlungen immer wieder ein Stück näher zu mir selbst geführt hat. Es gab Zeiten des Wachsens und Aufblühens, des Suchens und Loslassens, des Innehaltens und Neuwerdens. Und mit jedem dieser Schritte durfte ich lernen, immer tiefer in eine ehrliche und liebevolle Beziehung mit mir selbst zu finden – meiner inneren Stimme zu lauschen, mir selbst zu vertrauen und zu spüren, was wirklich zu mir gehört.",
"Vielleicht ist genau das für mich der eigentliche Weg: immer mehr die zu werden, die ich im Innersten bin. Mich mit allem anzunehmen, was zu mir gehört – mit meiner Kraft und meiner Verletzlichkeit, mit meinen Gaben, meinen Fragen, meinem Licht und meinen Schatten. Und aus dieser Verbundenheit heraus den eigenen Weg immer klarer erkennen und mit Vertrauen gehen zu können.",
"Eine meiner wichtigsten Begleiterinnen auf dieser Reise ist die Natur. In ihr finde ich immer wieder zurück zu etwas Ursprünglichem und Wesentlichem. Der Rhythmus der Jahreszeiten, das Werden und Vergehen, die Stille des Waldes, die Erde unter meinen Füßen erinnern mich daran, dass auch wir Menschen Teil dieser lebendigen Kreisläufe sind. Dass es Zeiten des Wachsens gibt und Zeiten des Rückzugs – und dass beides seinen Platz haben darf.",
"Auch die Musik und das Singen begleiten mich schon lange. Im gemeinsamen Klang entsteht für mich eine besondere Form von Verbindung. Manchmal braucht es keine vielen Worte. Wir singen, lauschen, schwingen miteinander – und etwas darf sich öffnen und lebendig werden.",
"Und immer wieder waren es auch Menschen, die mich auf meinem Weg begleitet haben. Menschen, die mich gesehen, gehalten, inspiriert und ermutigt haben. Durch sie durfte ich erfahren, wie wertvoll es ist zu erkennen, dass wir alle gemeinsam und doch jeder für sich auf dem Weg sind – und was für ein Geschenk es ist, wenn wir das miteinander teilen können. Aus dieser Erfahrung ist in mir der Wunsch gewachsen, auch für andere Menschen solche Räume zu öffnen und ein Stück ihres Weges mitzugehen.",
"Mein Leben empfinde ich als eine Reise - eine Reise, die mich über viele Wege und Wandlungen immer wieder ein Stück näher zu mir selbst geführt hat. Es gab Zeiten des Wachsens und Aufblühens, des Suchens und Loslassens, des Innehaltens und Neuwerdens. Und mit jedem dieser Schritte durfte ich lernen, immer tiefer in eine ehrliche und liebevolle Beziehung mit mir selbst zu finden - meiner inneren Stimme zu lauschen, mir selbst zu vertrauen und zu spüren, was wirklich zu mir gehört.",
"Vielleicht ist genau das für mich der eigentliche Weg: immer mehr die zu werden, die ich im Innersten bin. Mich mit allem anzunehmen, was zu mir gehört - mit meiner Kraft und meiner Verletzlichkeit, mit meinen Gaben, meinen Fragen, meinem Licht und meinen Schatten. Und aus dieser Verbundenheit heraus den eigenen Weg immer klarer erkennen und mit Vertrauen gehen zu können.",
"Eine meiner wichtigsten Begleiterinnen auf dieser Reise ist die Natur. In ihr finde ich immer wieder zurück zu etwas Ursprünglichem und Wesentlichem. Der Rhythmus der Jahreszeiten, das Werden und Vergehen, die Stille des Waldes, die Erde unter meinen Füßen erinnern mich daran, dass auch wir Menschen Teil dieser lebendigen Kreisläufe sind. Dass es Zeiten des Wachsens gibt und Zeiten des Rückzugs - und dass beides seinen Platz haben darf.",
"Auch die Musik und das Singen begleiten mich schon lange. Im gemeinsamen Klang entsteht für mich eine besondere Form von Verbindung. Manchmal braucht es keine vielen Worte. Wir singen, lauschen, schwingen miteinander - und etwas darf sich öffnen und lebendig werden.",
"Und immer wieder waren es auch Menschen, die mich auf meinem Weg begleitet haben. Menschen, die mich gesehen, gehalten, inspiriert und ermutigt haben. Durch sie durfte ich erfahren, wie wertvoll es ist zu erkennen, dass wir alle gemeinsam und doch jeder für sich auf dem Weg sind - und was für ein Geschenk es ist, wenn wir das miteinander teilen können. Aus dieser Erfahrung ist in mir der Wunsch gewachsen, auch für andere Menschen solche Räume zu öffnen und ein Stück ihres Weges mitzugehen.",
"Besonders tief geprägt haben mich meine eigenen Schwangerschaften und Geburten. Sie haben mich meinem Körper, meinem Frau-Sein und meiner eigenen Kraft auf eine neue Weise begegnen lassen. Ich durfte erfahren, wie sich Ängste und alte Erfahrungen wandeln können und wie heilsam es sein kann, wieder Vertrauen in sich selbst, den eigenen Körper und das Leben zu finden.",
"Daraus ist mein Wunsch gewachsen, auch andere Frauen zu begleiten – sie zu stärken und sie daran zu erinnern, dass ihr eigener Weg bereits in ihnen lebt. Nicht indem ich ihnen sage, wohin sie gehen sollen, sondern indem ich einen Raum halte, in dem sie sich selbst wieder hören, ihrer eigenen Wahrnehmung vertrauen und ihre Kraft spüren können.",
"Daraus ist mein Wunsch gewachsen, auch andere Frauen zu begleiten - sie zu stärken und sie daran zu erinnern, dass ihr eigener Weg bereits in ihnen lebt. Nicht indem ich ihnen sage, wohin sie gehen sollen, sondern indem ich einen Raum halte, in dem sie sich selbst wieder hören, ihrer eigenen Wahrnehmung vertrauen und ihre Kraft spüren können.",
"Und auch den Kindern und Mädchen, die ich begleite, möchte ich etwas davon mitgeben. Ich wünsche mir, dass sie sich mit der Natur verbunden erleben, Gemeinschaft erfahren und zugleich spüren dürfen: Ich bin willkommen, so wie ich bin. Meine Gaben und Fähigkeiten gehören zu mir. Ich darf meine eigene Stimme hören und meinen eigenen Weg entdecken.",
],
closing: {
lead: "Vielleicht ist es genau das, was all die verschiedenen Bereiche meiner Arbeit miteinander verbindet:",
highlight: "Räume zu schaffen, in denen wir uns erinnern dürfen, wer wir sind.",
paragraph:
"Räume für Verbindung – mit uns selbst, miteinander und mit der Natur. Räume, in denen wir wachsen und uns wandeln dürfen und dabei immer mehr bei uns selbst ankommen. Und Räume, aus denen heraus der Mut wachsen darf, das eigene Leben verbunden, liebevoll und wahrhaftig zu leben.",
"Räume für Verbindung - mit uns selbst, miteinander und mit der Natur. Räume, in denen wir wachsen und uns wandeln dürfen und dabei immer mehr bei uns selbst ankommen. Und Räume, aus denen heraus der Mut wachsen darf, das eigene Leben verbunden, liebevoll und wahrhaftig zu leben.",
},
} as const;