/* ============================================================================ ROOT ROUTE - the document and the application shell ---------------------------------------------------------------------------- `Layout` renders the whole document, which is why and are correct in the FIRST SSR byte: the locale is read from the URL, not from a cookie, a context provider or an effect, so the server and the first client render cannot disagree. The theme surface is the one deliberate exception to "no cookies" - it is a cookie read in the loader, but because the loader is what the server renders, the first byte still matches. The header and the footer live here rather than in the ":lang" layout so the ErrorBoundary keeps its chrome: a 404 is a normal page of the site, with the navigation and the mandatory EU visibility still present. ========================================================================= */ import { useMemo, type ReactNode } from "react"; import { isRouteErrorResponse, Link, Links, Meta, Outlet, Scripts, ScrollRestoration, useLocation, useRouteLoaderData, } from "react-router"; import type { Route } from "./+types/root"; import "./app.css"; import { ArrowIcon, Button } from "~/components/ui/Button"; import { Footer } from "~/components/ui/Footer"; import { Navigation } from "~/components/ui/Navigation"; import { useLocale, useRouteHandleValue } from "~/i18n"; import { DEFAULT_LOCALE, dirFor, type Locale } from "~/i18n/config"; import { localizedPath } from "~/i18n/paths"; import { createTranslator, getMessages } from "~/i18n/t"; import { getSiteUrl } from "~/lib/env.server"; import { readCanonicalPath, readFooterSurface, readNavSurface, readSelfAlternates, } from "~/lib/route-handle"; import { alternateUrls, buildMeta, canonicalUrl } from "~/lib/seo"; import { buildFooterLinkGroups, buildNavItems } from "~/lib/site-navigation"; import { DEFAULT_THEME, readTheme, themeCookie, type AppTheme } from "~/lib/theme"; /** * Last-resort origin for the canonical and hreflang links. The root loader * normally supplies the real one (`getSiteUrl`), but when IT is what threw, * the ErrorBoundary still renders through `Layout` with no loader data - and * a page with no canonical at all is worse than one pointing at the public * production origin. Mirrors DEFAULTS.SITE_URL in ~/lib/env.server, which * cannot be imported here because `Layout` also runs in the browser. */ const FALLBACK_SITE_URL = "https://sharper.ly"; /** * The faces each language actually renders in - every file listed here exists * in public/fonts/. Only the two weights above the fold are preloaded per * locale; Montserrat Arabic Medium is left to load normally. */ const FONT_PRELOADS: Readonly> = { en: ["/fonts/Panton-Regular.woff2", "/fonts/Panton-Bold.woff2"], ar: [ "/fonts/Montserrat-Arabic-Regular.woff2", "/fonts/Montserrat-Arabic-Bold.woff2", ], }; function createCspNonce(): string { const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)); return btoa(String.fromCharCode(...bytes)); } /** * Resolved once per request. Every route can read the origin and the locale * off this loader via `matches[0].data` in its own `meta`. */ export function loader({ request }: Route.LoaderArgs) { const { pathname } = new URL(request.url); const locale = pathnameLocale(pathname); return { locale, dir: dirFor(locale), pathname, siteUrl: getSiteUrl(request), /* Which surface the document stands on. Read from the theme cookie so the server paints the same surface the reader asked for - the browser never guesses, so there is no flash-of-wrong-theme and no hydration mismatch. `:root` is dark, so the absence of a cookie is a dark first visit. */ theme: readTheme(request.headers.get("Cookie")), nonce: createCspNonce(), }; } /** * The root action is the theme toggle's destination (the nav posts here at * `/`). It validates the requested theme and persists it with Set-Cookie; the * loader re-runs and the shell repaints the `data-surface` on . */ export async function action({ request }: Route.ActionArgs) { const formData = await request.formData(); const requested = formData.get("theme"); const theme: AppTheme = requested === "light" ? "light" : requested === "dark" ? "dark" : DEFAULT_THEME; return new Response(null, { status: 204, headers: { "Set-Cookie": themeCookie(theme) }, }); } function pathnameLocale(pathname: string): Locale { const first = pathname.replace(/^\/+/, "").split("/")[0]?.toLowerCase(); return first === "ar" ? "ar" : first === "en" ? "en" : DEFAULT_LOCALE; } /** * Site-level defaults. A page's own `meta` replaces this array wholesale, so * this is what renders for a route that declares none - and, because `error` * is passed here when a boundary catches, it is also the error page's meta. */ export function meta({ loaderData, error }: Route.MetaArgs) { const locale = loaderData?.locale ?? DEFAULT_LOCALE; const messages = getMessages(locale); const t = createTranslator(locale); const isError = error !== undefined; const isNotFound = isRouteErrorResponse(error) && error.status === 404; return buildMeta({ locale, pathname: loaderData?.pathname ?? localizedPath("/", locale), siteUrl: loaderData?.siteUrl ?? FALLBACK_SITE_URL, title: isNotFound ? t("errors.notFoundTitle") : isError ? t("errors.genericTitle") : messages.site.metaTitle, description: isError ? undefined : messages.site.description, appendSiteName: isError, // An error page must never enter the index, whatever its status code. noIndex: isError, }); } export function Layout({ children }: { children: ReactNode }) { const locale = useLocale(); const dir = dirFor(locale); const { pathname, search, hash } = useLocation(); const t = useMemo(() => createTranslator(locale), [locale]); const navItems = useMemo(() => buildNavItems(locale), [locale]); const footerLinkGroups = useMemo(() => buildFooterLinkGroups(locale), [locale]); /* The theme cookie decides which surface the DOCUMENT stands on. The header and footer follow it, unless the route's handle forces one (`navSurface` / `footerSurface`) - so a page can still draw a light nav on a dark surface in either theme. */ const rootData = useRouteLoaderData("root"); const theme: AppTheme = rootData?.theme ?? DEFAULT_THEME; const nonce = rootData?.nonce; const navSurface = useRouteHandleValue(readNavSurface) ?? theme; const footerSurface = useRouteHandleValue(readFooterSurface) ?? theme; /* Canonical and hreflang are emitted HERE, not from a route's `meta`: a leaf `meta` replaces its parents' wholesale, so a page that writes its own would silently drop them. `handle.canonicalPath` is the escape hatch. */ const siteUrl = rootData?.siteUrl ?? FALLBACK_SITE_URL; const canonicalPath = useRouteHandleValue(readCanonicalPath) ?? pathname; /* A detail route emits its own pair from the counterpart's real Strapi slug (see routes/_shared/alternates.ts). Emitting the shell's prefix-swapped set as well would ship TWO tags, and the second would point at an Arabic URL that does not exist. */ const ownsAlternates = useRouteHandleValue(readSelfAlternates) ?? false; return ( {!ownsAlternates && ( <> {alternateUrls(siteUrl, canonicalPath).map((alternate) => ( ))} )} {FONT_PRELOADS[locale].map((href) => ( ))}