- Customer accounts now require email verification (hashed, single-use, time-limited tokens) before they can request/confirm bookings, with resend flows on login/account/booking widget and rate limiting. - Admins get a private, rotatable iCalendar (ICS) subscription feed of their confirmed bookings and public events, timezone-correct for Europe/Berlin including DST, never exposing meeting passwords. - Adds a full SEO layer: per-page canonical/OG/Twitter metadata with CMS-editable overrides and content-derived fallbacks, a dynamic sitemap.xml and robots.txt driven by real published content, JSON-LD (Organization/LocalBusiness, WebSite, WebPage, BreadcrumbList, Service, Event, BlogPosting) that never fabricates data, and a CMS-managed redirect table for changed slugs. - Global ANOUMA-naming audit: the brand name is never used to label personal account/calendar areas anywhere in the app, CMS, or emails. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
30 lines
1.2 KiB
TypeScript
30 lines
1.2 KiB
TypeScript
/**
|
|
* 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
|
|
* expect. Uses Intl's timezone database rather than hardcoded +1/+2 math,
|
|
* so it's correct across the DST transition automatically.
|
|
*/
|
|
export function toBerlinOffsetISOString(date: Date): string {
|
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
timeZone: "Europe/Berlin",
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hourCycle: "h23",
|
|
timeZoneName: "shortOffset",
|
|
}).formatToParts(date);
|
|
|
|
const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "00";
|
|
const tzName = parts.find((p) => p.type === "timeZoneName")?.value ?? "GMT+0";
|
|
const match = /GMT([+-])(\d+)(?::(\d+))?/.exec(tzName);
|
|
const sign = match?.[1] ?? "+";
|
|
const offsetHours = (match?.[2] ?? "0").padStart(2, "0");
|
|
const offsetMinutes = (match?.[3] ?? "0").padStart(2, "0");
|
|
|
|
return `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}${sign}${offsetHours}:${offsetMinutes}`;
|
|
}
|