feat: redesign CMS and unify German admin UI
This commit is contained in:
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user