initial commit
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
/* ============================================================================
|
||||
ROOT ROUTE - the document and the application shell
|
||||
----------------------------------------------------------------------------
|
||||
`Layout` renders the whole document, which is why <html lang> and <html dir>
|
||||
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 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";
|
||||
|
||||
/**
|
||||
* 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<Record<Locale, readonly string[]>> = {
|
||||
en: ["/fonts/Panton-Regular.woff2", "/fonts/Panton-Bold.woff2"],
|
||||
ar: [
|
||||
"/fonts/Montserrat-Arabic-Regular.woff2",
|
||||
"/fonts/Montserrat-Arabic-Bold.woff2",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 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),
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
const navSurface = useRouteHandleValue(readNavSurface) ?? "dark";
|
||||
const footerSurface = useRouteHandleValue(readFooterSurface) ?? "dark";
|
||||
|
||||
/* 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 rootData = useRouteLoaderData<typeof loader>("root");
|
||||
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 <link rel="canonical"> tags, and the second
|
||||
would point at an Arabic URL that does not exist. */
|
||||
const ownsAlternates = useRouteHandleValue(readSelfAlternates) ?? false;
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={dir} data-surface="dark">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
{!ownsAlternates && (
|
||||
<>
|
||||
<link rel="canonical" href={canonicalUrl(siteUrl, canonicalPath)} />
|
||||
{alternateUrls(siteUrl, canonicalPath).map((alternate) => (
|
||||
<link
|
||||
key={alternate.hrefLang}
|
||||
rel="alternate"
|
||||
hrefLang={alternate.hrefLang}
|
||||
href={alternate.href}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{FONT_PRELOADS[locale].map((href) => (
|
||||
<link
|
||||
key={href}
|
||||
rel="preload"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
href={href}
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
))}
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="flex min-h-dvh flex-col">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only body-sm-bold focus:not-sr-only focus:absolute focus:top-md focus:start-md focus:z-[var(--layer-header)] focus:rounded-[var(--radius-control)] focus:bg-[var(--surface-accent)] focus:px-md focus:py-sm focus:text-[var(--text-on-accent)]"
|
||||
>
|
||||
{t("nav.skipToContent")}
|
||||
</a>
|
||||
|
||||
<Navigation
|
||||
locale={locale}
|
||||
pathname={pathname}
|
||||
search={search}
|
||||
hash={hash}
|
||||
items={navItems}
|
||||
surface={navSurface}
|
||||
/>
|
||||
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<Footer
|
||||
locale={locale}
|
||||
surface={footerSurface}
|
||||
linkGroups={footerLinkGroups}
|
||||
/>
|
||||
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
/**
|
||||
* The site's 404 and 500 page. Localised like any other page, and rendered
|
||||
* inside `Layout`, so <html lang>/<html dir>, the navigation and the footer
|
||||
* are all still correct.
|
||||
*/
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
const locale = useLocale();
|
||||
const t = useMemo(() => createTranslator(locale), [locale]);
|
||||
|
||||
const routeError = isRouteErrorResponse(error) ? error : null;
|
||||
const status = routeError?.status ?? 500;
|
||||
const isNotFound = status === 404;
|
||||
|
||||
const title = isNotFound
|
||||
? t("errors.notFoundTitle")
|
||||
: t("errors.genericTitle");
|
||||
const body = isNotFound ? t("errors.notFoundBody") : t("errors.genericBody");
|
||||
|
||||
// Stack traces are development-only: a production reader gets the message,
|
||||
// never the server's internals.
|
||||
const stack =
|
||||
import.meta.env.DEV && error instanceof Error ? error.stack : undefined;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[var(--layout-prose-max)] flex-col items-start gap-lg px-[var(--layout-gutter)] py-4xl">
|
||||
<p className="label-md text-[var(--text-tertiary)]">
|
||||
{t("errors.statusLabel", { status })}
|
||||
</p>
|
||||
<h1 className="display-sm text-[var(--text-primary)]">{title}</h1>
|
||||
<p className="body-lg text-[var(--text-secondary)]">{body}</p>
|
||||
|
||||
<Button
|
||||
as={Link}
|
||||
to={localizedPath("/", locale)}
|
||||
variant="primary"
|
||||
iconEnd={<ArrowIcon />}
|
||||
>
|
||||
{t("actions.backToHome")}
|
||||
</Button>
|
||||
|
||||
{stack ? (
|
||||
<details className="w-full">
|
||||
<summary className="body-sm-bold cursor-pointer text-[var(--text-tertiary)]">
|
||||
{t("errors.detailsLabel")}
|
||||
</summary>
|
||||
<pre className="mt-md w-full overflow-x-auto rounded-[var(--radius-card)] bg-[var(--surface-raised)] p-md text-[var(--text-secondary)]">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user