72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
/* ============================================================================
|
|||
|
|
ROOT LOADER DATA, READ FROM `meta`
|
||
|
|
----------------------------------------------------------------------------
|
||
|
|
`buildMeta` needs the locale, the pathname and the site origin, and all
|
||
|
|
three are resolved once per request by the root loader. A `meta` function
|
||
|
|
reaches them through its `matches`.
|
||
|
|
|
||
|
|
React Router 8 names that property `loaderData` on every match (see
|
||
|
|
`CreateMetaArgs` in react-router/lib/types/route-module-annotations.d.ts and
|
||
|
|
the `Meta` component that builds the array). Version 7 called it `data`, and
|
||
|
|
plenty of examples still do, so both are read here.
|
||
|
|
|
||
|
|
Defensive rather than indexed: `matches[0]` has no data at all while the
|
||
|
|
root loader is in flight or on an error render, and a `meta` that throws
|
||
|
|
takes the whole document down with it.
|
||
|
|
========================================================================= */
|
||
|
|
|
||
|
|
import { DEFAULT_LOCALE, isLocale, localizedPath, type Locale } from "~/i18n";
|
||
|
|
|
||
|
|
export interface RootSeo {
|
||
|
|
readonly locale: Locale;
|
||
|
|
/** Pathname INCLUDING the locale prefix. */
|
||
|
|
readonly pathname: string;
|
||
|
|
/** Site origin without a trailing slash. */
|
||
|
|
readonly siteUrl: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Matches the default in ~/lib/env.server, for the case root data is absent. */
|
||
|
|
const FALLBACK_SITE_URL = "https://sharper.ly";
|
||
|
|
|
||
|
|
function readMatchData(match: unknown): Record<string, unknown> | undefined {
|
||
|
|
if (!match || typeof match !== "object") return undefined;
|
||
|
|
|
||
|
|
const { id, loaderData, data } = match as {
|
||
|
|
id?: unknown;
|
||
|
|
loaderData?: unknown;
|
||
|
|
data?: unknown;
|
||
|
|
};
|
||
|
|
if (id !== "root") return undefined;
|
||
|
|
|
||
|
|
const payload = loaderData ?? data;
|
||
|
|
return payload && typeof payload === "object"
|
||
|
|
? (payload as Record<string, unknown>)
|
||
|
|
: undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The root loader's `{ locale, pathname, siteUrl }`, with safe fallbacks. */
|
||
|
|
export function readRootSeo(matches: readonly unknown[]): RootSeo {
|
||
|
|
for (const match of matches) {
|
||
|
|
const record = readMatchData(match);
|
||
|
|
if (!record) continue;
|
||
|
|
|
||
|
|
const locale = isLocale(record.locale) ? record.locale : DEFAULT_LOCALE;
|
||
|
|
|
||
|
|
return {
|
||
|
|
locale,
|
||
|
|
pathname:
|
||
|
|
typeof record.pathname === "string"
|
||
|
|
? record.pathname
|
||
|
|
: localizedPath("/", locale),
|
||
|
|
siteUrl:
|
||
|
|
typeof record.siteUrl === "string" ? record.siteUrl : FALLBACK_SITE_URL,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
locale: DEFAULT_LOCALE,
|
||
|
|
pathname: localizedPath("/", DEFAULT_LOCALE),
|
||
|
|
siteUrl: FALLBACK_SITE_URL,
|
||
|
|
};
|
||
|
|
}
|