/* ============================================================================ EVENT -> iCALENDAR (resource route) ---------------------------------------------------------------------------- /{locale}/events-calendar/{slug}.ics This module exports a `loader` and nothing else - no component, no ErrorBoundary - which is what makes React Router treat it as a resource route and hand the loader's Response straight to the browser. Because a resource route is served on its own, the ":lang" layout above it does NOT run: its loader is not called for this request, so the locale segment is validated here as well. `requireLocale` throws the same 404 the layout would have. The file itself is built by ./_events-opportunities/ics.ts, which follows RFC 5545 rather than one client's tolerance - CRLF endings, 75-octet folding on character boundaries (an Arabic title is two bytes per letter), escaped TEXT values and an exclusive DTEND for an all-day event. ========================================================================= */ import { redirect } from "react-router"; import { isLocale, sectionPath, translate } from "~/i18n"; import { getSiteUrl } from "~/lib/env.server"; import { canonicalUrl } from "~/lib/seo"; import { requireEntity } from "~/lib/strapi"; import type { Route } from "./+types/events-calendar.$slug.ics"; import { getRouteCopy } from "./_events-opportunities/copy"; import { fetchEvent } from "./_events-opportunities/events.server"; import { buildIcsCalendar, icsResponse } from "./_events-opportunities/ics"; import { requireLocale } from "./_shared/listing"; /** Only used to name the file; the CMS slug supplies the rest. */ const FILENAME_PREFIX = "sharp-er"; export async function loader({ params, request }: Route.LoaderArgs) { /* The ":lang" layout normally 301s "/EN/..." to "/en/...", but its loader does not run for a resource route, so the same normalisation happens here rather than answering a 404 for a URL the rest of the site accepts. */ const segment = params.lang; const normalized = segment?.toLowerCase(); if (segment && normalized !== segment && isLocale(normalized)) { const url = new URL(request.url); throw redirect( `${url.pathname.replace(/^\/[^/]+/, `/${normalized}`)}${url.search}`, 301, ); } const locale = requireLocale(normalized); const slug = params.slug ?? ""; const { event } = requireEntity(await fetchEvent(slug, locale)); const siteUrl = getSiteUrl(request); const pathname = `${sectionPath("events", locale)}/${encodeURIComponent(slug)}`; const url = canonicalUrl(siteUrl, pathname); const copy = getRouteCopy(locale); const location = event.place ?? (event.format === "online" ? translate(locale, "events.online") : undefined); /* The joining link belongs in the calendar entry, not only on the page: by the time the reminder fires, the page is not what is in front of anyone. */ const description = [ event.summary, event.onlineUrl ? `${copy.eventDetail.joinOnlineLabel}: ${event.onlineUrl}` : undefined, url, ] .filter(Boolean) .join("\n\n"); const calendar = buildIcsCalendar( [ { /* documentId is shared by an entry's translations in Strapi 5, so the locale is part of the UID - importing the Arabic file must not overwrite the English entry already in someone's calendar. */ uid: `${event.documentId}.${locale}@sharper.ly`, start: event.start, // Only an explicit end is written: inventing a duration would put a // wrong-length block in a reader's calendar. end: event.end, allDay: event.allDay, summary: event.title, description, location, url, status: "CONFIRMED", }, ], { name: translate(locale, "nav.sections.events") }, ); return icsResponse(calendar, `${FILENAME_PREFIX}-${slug}.ics`); }