/* ============================================================================ ROUTE CONFIG ---------------------------------------------------------------------------- URL shape - an explicit locale prefix on BOTH languages, never a ?lang=: / -> 302 to /en or /ar (Accept-Language) /:lang -> home /:lang/
-> one of the ten top-level sections /:lang/
/ -> one entry of a CMS-backed section /:lang/events-calendar/.ics -> that event as an RFC 5545 file anything else -> 404 ONE set of route modules serves both languages: the ":lang" layout wraps every page, so a page module is written once and rendered for /en/about and /ar/about alike. An unknown ":lang" is rejected by that layout's loader, and any other unmatched URL falls through to the catch-all, which renders the root ErrorBoundary's localised 404. LISTING AND DETAIL ARE SIBLINGS, NOT PARENT AND CHILD ----------------------------------------------------- A detail route is registered as a SIBLING of its listing route - both are children of the ":lang" layout - and deliberately not as a child of the listing. React Router only renders a child where its parent renders an , and a listing module renders a page, not an outlet: nesting would either hide the detail page entirely or paint it underneath the whole index (its filters, its cards, its pagination). "locale-layout.tsx" is the only module in this tree that renders an , so it is the only legitimate parent. The shared URL prefix is expressed in the path string ("events-calendar/:slug"), which costs nothing and keeps each page's route a leaf that owns its whole screen. ":slug.ics" DOES NOT SHADOW ":slug" ----------------------------------- Both patterns match /en/events-calendar/kickoff.ics, so ranking - not array order - has to decide. React Router scores a partial dynamic segment (":slug.ics") above a whole-segment one (":slug"), so the resource route wins the .ics URL and the page route keeps every other slug. The .ics module exports only a `loader`, which is what makes it a resource route: its Response is handed to the browser verbatim and no parent loader - not even the ":lang" layout's - runs, which is why that module validates the locale segment itself. ROUTE IDS --------- "root" | "routes/locale-redirect" | "locale" | "routes/not-found", one per section named after its key: "home", "about", "whatWeDo", "activities", "events", "applications", "news", "contact", "resources", "knowledgeHub" - and one per detail route, the section key plus "Detail" ("newsDetail", "eventsDetail", ...), plus "eventsIcs". So `useRouteLoaderData("locale")` reads the ":lang" layout's data anywhere. ADDING A SECTION PAGE --------------------- Add the section to `ROUTE_SLUGS`, name its module in `SECTION_MODULES` (and in `DETAIL_MODULES` if entries can be drilled into), and write the file. The tables below are declared, not discovered: every module named here is registered unconditionally, so a path that does not resolve fails the build instead of quietly falling through to the 404 route. Vite does not re-evaluate this file when a route module appears, so restart `pnpm dev` after adding one. ========================================================================= */ import { index, route, type RouteConfig, type RouteConfigEntry, } from "@react-router/dev/routes"; import { ROUTE_SLUGS, type RouteSlugKey } from "./lib/route-slugs"; /** * Section -> route module. The file name is always the slug, so the URL and * the module that serves it can be read off each other. */ const SECTION_MODULES = { home: "routes/home.tsx", about: "routes/about.tsx", whatWeDo: "routes/what-we-do.tsx", activities: "routes/activities-and-initiatives.tsx", events: "routes/events-calendar.tsx", applications: "routes/applications-and-opportunities.tsx", news: "routes/news-and-updates.tsx", contact: "routes/contact.tsx", resources: "routes/resources-and-publications.tsx", knowledgeHub: "routes/knowledge-hub.tsx", } as const satisfies Record; /** * Section -> the module that renders ONE entry of it, at * `
/:slug`. Only the six CMS-backed sections have one; "about", * "what-we-do", "contact" and the home page are single pages with nothing to * drill into. */ const DETAIL_MODULES = { activities: "routes/activities-and-initiatives.$slug.tsx", events: "routes/events-calendar.$slug.tsx", applications: "routes/applications-and-opportunities.$slug.tsx", news: "routes/news-and-updates.$slug.tsx", resources: "routes/resources-and-publications.$slug.tsx", knowledgeHub: "routes/knowledge-hub.$slug.tsx", } as const satisfies Partial>; /** * Resource routes: a module with a `loader` and no component, whose Response * is served as-is. `path` is relative to the ":lang" layout, like every other * entry here. */ const RESOURCE_ROUTES = [ { id: "eventsIcs", path: `${ROUTE_SLUGS.events}/:slug.ics`, file: "routes/events-calendar.$slug.ics.ts", }, ] as const; const detailKeys = Object.keys(DETAIL_MODULES) as (keyof typeof DETAIL_MODULES)[]; /** * Every page under ":lang": the ten sections, then the six detail routes, then * the .ics resource route. Flat on purpose - see "LISTING AND DETAIL ARE * SIBLINGS" above. */ const localeChildRoutes: RouteConfigEntry[] = [ ...(Object.keys(SECTION_MODULES) as RouteSlugKey[]).map((key) => key === "home" ? index(SECTION_MODULES.home, { id: "home" }) : route(ROUTE_SLUGS[key], SECTION_MODULES[key], { id: key }), ), ...detailKeys.map((key) => route(`${ROUTE_SLUGS[key]}/:slug`, DETAIL_MODULES[key], { id: `${key}Detail`, }), ), ...RESOURCE_ROUTES.map((entry) => route(entry.path, entry.file, { id: entry.id }), ), ]; export default [ // Bare "/" is not a page: it negotiates and redirects. index("routes/locale-redirect.tsx"), // Every page, in both languages, through one shared route module. route( ":lang", "routes/locale-layout.tsx", { id: "locale" }, localeChildRoutes, ), // Unknown locale segments are handled by the layout's loader; every other // unmatched URL lands here. route("*", "routes/not-found.tsx"), ] satisfies RouteConfig;