fimo-react-router 0.21.0-experimental.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/AppResetScrollOnNavigate.d.ts +2 -0
- package/dist/AppResetScrollOnNavigate.js +12 -0
- package/dist/AppRoutesProvider.d.ts +25 -0
- package/dist/AppRoutesProvider.js +108 -0
- package/dist/FimoProviders.d.ts +30 -0
- package/dist/FimoProviders.js +187 -0
- package/dist/FimoScripts.d.ts +13 -0
- package/dist/FimoScripts.js +45 -0
- package/dist/OptionalTracking.d.ts +13 -0
- package/dist/OptionalTracking.js +28 -0
- package/dist/Pageview.d.ts +9 -0
- package/dist/Pageview.js +118 -0
- package/dist/active-routes.d.ts +12 -0
- package/dist/active-routes.js +60 -0
- package/dist/fixtures/virtual-fimo-config.d.ts +3 -0
- package/dist/fixtures/virtual-fimo-config.js +3 -0
- package/dist/fixtures/virtual-translations.d.ts +5 -0
- package/dist/fixtures/virtual-translations.js +9 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/navigation-utils.d.ts +8 -0
- package/dist/navigation-utils.js +36 -0
- package/dist/navigation.d.ts +12 -0
- package/dist/navigation.js +28 -0
- package/dist/prefetch.d.ts +9 -0
- package/dist/prefetch.js +39 -0
- package/dist/preset.d.ts +30 -0
- package/dist/preset.js +33 -0
- package/dist/routes.d.ts +35 -0
- package/dist/routes.js +85 -0
- package/dist/seo.d.ts +93 -0
- package/dist/seo.js +74 -0
- package/dist/vite.d.ts +20 -0
- package/dist/vite.js +104 -0
- package/package.json +76 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const normalizeRoutePath = (path) => {
|
|
2
|
+
const withSlash = path.startsWith('/') ? path : `/${path}`;
|
|
3
|
+
const normalized = withSlash.replace(/\/{2,}/g, '/');
|
|
4
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
|
|
5
|
+
};
|
|
6
|
+
const joinRoutePath = (parentPath, childPath) => {
|
|
7
|
+
if (!childPath) {
|
|
8
|
+
return parentPath;
|
|
9
|
+
}
|
|
10
|
+
if (childPath.startsWith('/')) {
|
|
11
|
+
return normalizeRoutePath(childPath);
|
|
12
|
+
}
|
|
13
|
+
return normalizeRoutePath(`${parentPath}/${childPath}`);
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Flatten React Router's framework manifest into the route graph the active
|
|
17
|
+
* router actually loaded. This is intentionally separate from fimo-config:
|
|
18
|
+
* metadata describes what Studio expects, while this manifest describes what
|
|
19
|
+
* the iframe can render after its route modules have converged.
|
|
20
|
+
*/
|
|
21
|
+
export function getActiveRouterRoutes(manifest) {
|
|
22
|
+
const routeRecord = manifest?.routes;
|
|
23
|
+
if (!routeRecord) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const entries = Object.values(routeRecord).filter(isActiveRouteManifestEntry);
|
|
27
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
28
|
+
const pathCache = new Map();
|
|
29
|
+
const resolvePath = (entry, resolving = new Set()) => {
|
|
30
|
+
const cached = pathCache.get(entry.id);
|
|
31
|
+
if (cached !== undefined) {
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
if (resolving.has(entry.id)) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const nextResolving = new Set(resolving).add(entry.id);
|
|
38
|
+
const parentPath = entry.parentId
|
|
39
|
+
? resolvePath(byId.get(entry.parentId) ?? { id: entry.parentId }, nextResolving)
|
|
40
|
+
: '';
|
|
41
|
+
const path = entry.index ? parentPath || '/' : joinRoutePath(parentPath, entry.path ?? '');
|
|
42
|
+
pathCache.set(entry.id, path);
|
|
43
|
+
return path;
|
|
44
|
+
};
|
|
45
|
+
return entries.flatMap((entry) => {
|
|
46
|
+
// A pathless/root layout is part of the framework manifest, but it is not
|
|
47
|
+
// a URL the router can navigate to. Treat only an explicit index route as
|
|
48
|
+
// the root path; including the layout would add a phantom `/` entry and
|
|
49
|
+
// make exact route-graph convergence impossible for apps without a home
|
|
50
|
+
// page.
|
|
51
|
+
if ((entry.path === undefined || entry.path === '') && !entry.index) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const path = normalizeRoutePath(resolvePath(entry));
|
|
55
|
+
return [{ id: entry.id, path }];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function isActiveRouteManifestEntry(value) {
|
|
59
|
+
return Boolean(value && typeof value === 'object' && typeof value.id === 'string');
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Static stand-in for the `virtual:translations` Vite module in vitest runs.
|
|
2
|
+
// `fimo-react`'s root barrel imports it, and this package renders against that
|
|
3
|
+
// barrel, so the alias has to exist here too.
|
|
4
|
+
export const dictionaries = {
|
|
5
|
+
en: { 'hero.title': 'Welcome' },
|
|
6
|
+
};
|
|
7
|
+
export const defaultLocale = 'en';
|
|
8
|
+
export const signature = 'fixture';
|
|
9
|
+
export const updatedAt = null;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { FimoProviders } from './FimoProviders.js';
|
|
2
|
+
export { FimoScripts } from './FimoScripts.js';
|
|
3
|
+
export { useLocale, formClient } from 'fimo-react';
|
|
4
|
+
export { localizeFimoPath, resolveFimoLocale } from 'fimo/config';
|
|
5
|
+
export { FimoLink, useFimoHref, useFimoNavigate } from './navigation.js';
|
|
6
|
+
export { localizeFimoTo } from './navigation-utils.js';
|
|
7
|
+
export { AppRoutesProvider } from './AppRoutesProvider.js';
|
|
8
|
+
export type { AppRoute, SlugInfo } from './AppRoutesProvider.js';
|
|
9
|
+
export type { DisplayRoute, FimoProvidersProps, ParamInfo } from './FimoProviders.js';
|
|
10
|
+
export type { FimoLocaleContextValue } from 'fimo-react';
|
|
11
|
+
export type { FimoLocaleResolution } from 'fimo/config';
|
|
12
|
+
export type { FimoLinkProps, FimoNavigationOptions } from './navigation.js';
|
|
13
|
+
export { Pageview } from './Pageview.js';
|
|
14
|
+
export * from './seo.js';
|
|
15
|
+
export * from 'fimo/seo';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `fimo-react-router` — the React Router adapter surface.
|
|
2
|
+
//
|
|
3
|
+
// Everything here needs React Router: the providers that read its location and
|
|
4
|
+
// framework manifest, locale-aware links, the route `meta` helpers, and the
|
|
5
|
+
// legacy `AppRoutesProvider` SPA shell. Content clients, labels, and editable
|
|
6
|
+
// primitives come from `fimo-react`, which has no router dependency.
|
|
7
|
+
export { FimoProviders } from './FimoProviders.js';
|
|
8
|
+
export { FimoScripts } from './FimoScripts.js';
|
|
9
|
+
export { useLocale, formClient } from 'fimo-react';
|
|
10
|
+
export { localizeFimoPath, resolveFimoLocale } from 'fimo/config';
|
|
11
|
+
export { FimoLink, useFimoHref, useFimoNavigate } from './navigation.js';
|
|
12
|
+
export { localizeFimoTo } from './navigation-utils.js';
|
|
13
|
+
export { AppRoutesProvider } from './AppRoutesProvider.js';
|
|
14
|
+
export { Pageview } from './Pageview.js';
|
|
15
|
+
export * from './seo.js';
|
|
16
|
+
export * from 'fimo/seo';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FimoConfig } from 'fimo/config';
|
|
2
|
+
export type FimoNavigationTo = string | {
|
|
3
|
+
pathname?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
hash?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function localizeFimoTo(to: FimoNavigationTo, locale: string, config: Pick<FimoConfig, 'i18n'> | undefined): FimoNavigationTo;
|
|
8
|
+
//# sourceMappingURL=navigation-utils.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { localizeFimoPath } from 'fimo/config';
|
|
2
|
+
export function localizeFimoTo(to, locale, config) {
|
|
3
|
+
if (typeof to === 'string') {
|
|
4
|
+
return localizeStringTo(to, locale, config);
|
|
5
|
+
}
|
|
6
|
+
if (!to.pathname) {
|
|
7
|
+
return to;
|
|
8
|
+
}
|
|
9
|
+
const localizedPathname = localizePathname(to.pathname, locale, config);
|
|
10
|
+
return localizedPathname === to.pathname ? to : { ...to, pathname: localizedPathname };
|
|
11
|
+
}
|
|
12
|
+
function localizeStringTo(to, locale, config) {
|
|
13
|
+
if (!shouldLocalizeStringPath(to)) {
|
|
14
|
+
return to;
|
|
15
|
+
}
|
|
16
|
+
const match = /^(?<pathname>[^?#]*)(?<search>\?[^#]*)?(?<hash>#.*)?$/.exec(to);
|
|
17
|
+
const pathname = match?.groups?.pathname || '/';
|
|
18
|
+
const search = match?.groups?.search ?? '';
|
|
19
|
+
const hash = match?.groups?.hash ?? '';
|
|
20
|
+
return `${localizePathname(pathname, locale, config)}${search}${hash}`;
|
|
21
|
+
}
|
|
22
|
+
function shouldLocalizeStringPath(to) {
|
|
23
|
+
if (to.startsWith('#') || to.startsWith('?') || to.startsWith('//')) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(to)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return to.startsWith('/');
|
|
30
|
+
}
|
|
31
|
+
function localizePathname(pathname, locale, config) {
|
|
32
|
+
if (!pathname.startsWith('/')) {
|
|
33
|
+
return pathname;
|
|
34
|
+
}
|
|
35
|
+
return localizeFimoPath(pathname, locale, config);
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LinkProps, NavigateOptions, To } from 'react-router';
|
|
2
|
+
export interface FimoNavigationOptions {
|
|
3
|
+
locale?: string;
|
|
4
|
+
preserveLocale?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface FimoLinkProps extends Omit<LinkProps, 'to'>, FimoNavigationOptions {
|
|
7
|
+
to: To;
|
|
8
|
+
}
|
|
9
|
+
export declare function FimoLink({ to, locale, preserveLocale, ...props }: FimoLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function useFimoHref(to: To, options?: FimoNavigationOptions): To;
|
|
11
|
+
export declare function useFimoNavigate(): (to: To | number, options?: NavigateOptions & FimoNavigationOptions) => void;
|
|
12
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLocale } from 'fimo-react';
|
|
3
|
+
import { Link, useNavigate } from 'react-router';
|
|
4
|
+
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo-vite
|
|
5
|
+
import fimoConfig from 'virtual:fimo-config';
|
|
6
|
+
import { localizeFimoTo } from './navigation-utils.js';
|
|
7
|
+
export function FimoLink({ to, locale, preserveLocale = true, ...props }) {
|
|
8
|
+
const href = useFimoHref(to, { locale, preserveLocale });
|
|
9
|
+
return _jsx(Link, { ...props, to: href });
|
|
10
|
+
}
|
|
11
|
+
export function useFimoHref(to, options = {}) {
|
|
12
|
+
const active = useLocale();
|
|
13
|
+
const locale = options.locale ?? (options.preserveLocale === false ? undefined : active.locale);
|
|
14
|
+
return locale ? localizeFimoTo(to, locale, fimoConfig) : to;
|
|
15
|
+
}
|
|
16
|
+
export function useFimoNavigate() {
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const active = useLocale();
|
|
19
|
+
return (to, options = {}) => {
|
|
20
|
+
if (typeof to === 'number') {
|
|
21
|
+
navigate(to);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const locale = options.locale ?? (options.preserveLocale === false ? undefined : active.locale);
|
|
25
|
+
const localizedTo = locale ? localizeFimoTo(to, locale, fimoConfig) : to;
|
|
26
|
+
navigate(localizedTo, options);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function resolveFimoLocaleForUrl(url: string): import("fimo-react").FimoLocaleResolution;
|
|
2
|
+
/**
|
|
3
|
+
* Fetch route data for SEO meta resolution.
|
|
4
|
+
*
|
|
5
|
+
* Only entry params are fetched: a locale param is resolved from the URL and
|
|
6
|
+
* never corresponds to a CMS schema, so it must not produce a request.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveMetaEntries(url: string): Promise<Record<string, unknown> | undefined>;
|
|
9
|
+
//# sourceMappingURL=prefetch.d.ts.map
|
package/dist/prefetch.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveFimoLocale } from 'fimo/config';
|
|
2
|
+
import { matchFimoRoute } from 'fimo/routes';
|
|
3
|
+
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo-vite
|
|
4
|
+
import fimoConfig from 'virtual:fimo-config';
|
|
5
|
+
export function resolveFimoLocaleForUrl(url) {
|
|
6
|
+
return resolveFimoLocale(url, fimoConfig);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fetch route data for SEO meta resolution.
|
|
10
|
+
*
|
|
11
|
+
* Only entry params are fetched: a locale param is resolved from the URL and
|
|
12
|
+
* never corresponds to a CMS schema, so it must not produce a request.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveMetaEntries(url) {
|
|
15
|
+
// @ts-ignore – import.meta.env is a Vite feature, replaced at build time for both client and SSR
|
|
16
|
+
const base = import.meta.env.VITE_API_URL;
|
|
17
|
+
if (!base) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const match = matchFimoRoute(url, fimoConfig);
|
|
21
|
+
if (!match || match.contract.entryParams.length === 0) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const data = {};
|
|
25
|
+
await Promise.all(match.contract.entryParams.map(async (param) => {
|
|
26
|
+
const value = match.entryParams[param.name];
|
|
27
|
+
if (!value) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const entryUrl = new URL(`${base}/entries/${param.schema}/field/${encodeURIComponent(param.queryField)}/${encodeURIComponent(value)}`);
|
|
31
|
+
entryUrl.searchParams.set('locale', match.locale);
|
|
32
|
+
const res = await fetch(entryUrl);
|
|
33
|
+
if (res.ok) {
|
|
34
|
+
const json = await res.json();
|
|
35
|
+
data[param.name] = json.data;
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
return Object.keys(data).length > 0 ? data : undefined;
|
|
39
|
+
}
|
package/dist/preset.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
interface ReactRouterUserConfig {
|
|
2
|
+
readonly ssr?: boolean;
|
|
3
|
+
}
|
|
4
|
+
interface ReactRouterBuildEndArgs {
|
|
5
|
+
readonly buildManifest: unknown;
|
|
6
|
+
readonly reactRouterConfig: unknown;
|
|
7
|
+
readonly viteConfig: {
|
|
8
|
+
readonly root: string;
|
|
9
|
+
readonly build: {
|
|
10
|
+
readonly assetsDir?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Connect a client-rendered React Router build to Fimo hosting.
|
|
16
|
+
*
|
|
17
|
+
* The hosting-provider build contract stays inside Fimo so applications do not
|
|
18
|
+
* install or import provider packages. SSR needs a deeper server-runtime
|
|
19
|
+
* adapter and is rejected until Fimo supports that contract explicitly.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fimoPreset(): {
|
|
22
|
+
name: string;
|
|
23
|
+
reactRouterConfig({ reactRouterUserConfig }: {
|
|
24
|
+
reactRouterUserConfig: ReactRouterUserConfig;
|
|
25
|
+
}): {
|
|
26
|
+
buildEnd({ buildManifest, reactRouterConfig, viteConfig }: ReactRouterBuildEndArgs): void;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=preset.d.ts.map
|
package/dist/preset.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const REACT_ROUTER_BUILD_RESULT = '.vercel/react-router-build-result.json';
|
|
4
|
+
/**
|
|
5
|
+
* Connect a client-rendered React Router build to Fimo hosting.
|
|
6
|
+
*
|
|
7
|
+
* The hosting-provider build contract stays inside Fimo so applications do not
|
|
8
|
+
* install or import provider packages. SSR needs a deeper server-runtime
|
|
9
|
+
* adapter and is rejected until Fimo supports that contract explicitly.
|
|
10
|
+
*/
|
|
11
|
+
export function fimoPreset() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'fimo',
|
|
14
|
+
reactRouterConfig({ reactRouterUserConfig }) {
|
|
15
|
+
if (reactRouterUserConfig.ssr !== false) {
|
|
16
|
+
throw new Error('fimoPreset() currently supports React Router apps configured with ssr: false');
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
buildEnd({ buildManifest, reactRouterConfig, viteConfig }) {
|
|
20
|
+
const buildResultPath = join(viteConfig.root, REACT_ROUTER_BUILD_RESULT);
|
|
21
|
+
const assetsDir = viteConfig.build.assetsDir;
|
|
22
|
+
const result = {
|
|
23
|
+
buildManifest,
|
|
24
|
+
reactRouterConfig,
|
|
25
|
+
viteConfig: assetsDir ? { build: { assetsDir } } : undefined,
|
|
26
|
+
};
|
|
27
|
+
mkdirSync(dirname(buildResultPath), { recursive: true });
|
|
28
|
+
writeFileSync(buildResultPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FimoConfig } from 'fimo/config';
|
|
2
|
+
export interface FimoReactRouterRoute {
|
|
3
|
+
id?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
file?: string;
|
|
6
|
+
index?: boolean;
|
|
7
|
+
children?: FimoReactRouterRoute[];
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Structural minimum a route entry must satisfy. Deliberately WITHOUT an
|
|
12
|
+
* index signature so framework types like React Router's `RouteConfigEntry`
|
|
13
|
+
* (which has none) remain assignable, and generic so callers get their own
|
|
14
|
+
* entry type back — `fimoRoutes(routes) satisfies RouteConfig` must keep
|
|
15
|
+
* compiling in project code.
|
|
16
|
+
*/
|
|
17
|
+
interface FimoRouteShape {
|
|
18
|
+
id?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
file?: string;
|
|
21
|
+
index?: boolean;
|
|
22
|
+
children?: FimoRouteShape[];
|
|
23
|
+
}
|
|
24
|
+
export interface FimoRoutesOptions {
|
|
25
|
+
config?: Pick<FimoConfig, 'i18n'>;
|
|
26
|
+
rootDir?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @deprecated New React Router projects should declare optional `:locale?`
|
|
30
|
+
* segments directly. This compatibility adapter may be removed in a future
|
|
31
|
+
* breaking release.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fimoRoutes<T extends FimoRouteShape>(routes: readonly T[], options?: FimoRoutesOptions): T[];
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=routes.d.ts.map
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getConfigServer, getFimoDefaultLocale, getFimoLocales } from 'fimo/config';
|
|
2
|
+
import { hasLocaleSegment } from 'fimo/routes';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated New React Router projects should declare optional `:locale?`
|
|
5
|
+
* segments directly. This compatibility adapter may be removed in a future
|
|
6
|
+
* breaking release.
|
|
7
|
+
*/
|
|
8
|
+
export function fimoRoutes(routes, options = {}) {
|
|
9
|
+
const config = options.config ?? getConfigServer(options.rootDir ?? process.cwd());
|
|
10
|
+
const strategy = config.i18n?.routing?.strategy ?? 'path-prefix';
|
|
11
|
+
if (strategy !== 'path-prefix') {
|
|
12
|
+
return [...routes];
|
|
13
|
+
}
|
|
14
|
+
const defaultLocale = getFimoDefaultLocale(config);
|
|
15
|
+
// The default locale renders at the canonical unprefixed path, so it never
|
|
16
|
+
// gets an alias.
|
|
17
|
+
const prefixedLocales = getFimoLocales(config).filter((locale) => locale !== defaultLocale);
|
|
18
|
+
// A route that declares its own `:locale` segment already carries the locale
|
|
19
|
+
// in its path; prefixing it again would produce `/es/:locale/...`.
|
|
20
|
+
const aliasable = routes.filter((route) => !declaresLocaleSegment(route));
|
|
21
|
+
if (prefixedLocales.length === 0 || aliasable.length === 0) {
|
|
22
|
+
return [...routes];
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
...routes,
|
|
26
|
+
...prefixedLocales.flatMap((locale) => aliasable.map((route, index) => localizeRouteEntry(route, {
|
|
27
|
+
locale,
|
|
28
|
+
prefixThisEntry: true,
|
|
29
|
+
routeKey: String(index),
|
|
30
|
+
}))),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
function declaresLocaleSegment(route) {
|
|
34
|
+
if (route.path && hasLocaleSegment(route.path)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return (route.children ?? []).some(declaresLocaleSegment);
|
|
38
|
+
}
|
|
39
|
+
function localizeRouteEntry(route, options) {
|
|
40
|
+
const clone = {
|
|
41
|
+
...route,
|
|
42
|
+
id: localizedRouteId(route, options.locale, options.routeKey),
|
|
43
|
+
};
|
|
44
|
+
let prefixAppliedHere = false;
|
|
45
|
+
if (options.prefixThisEntry) {
|
|
46
|
+
prefixAppliedHere = applyLocalePrefix(clone, options.locale);
|
|
47
|
+
}
|
|
48
|
+
if (route.children) {
|
|
49
|
+
clone.children = route.children.map((child, index) => localizeRouteEntry(child, {
|
|
50
|
+
locale: options.locale,
|
|
51
|
+
prefixThisEntry: !prefixAppliedHere,
|
|
52
|
+
routeKey: `${options.routeKey}.${index}`,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
return clone;
|
|
56
|
+
}
|
|
57
|
+
function applyLocalePrefix(route, locale) {
|
|
58
|
+
if (route.index) {
|
|
59
|
+
delete route.index;
|
|
60
|
+
route.path = locale;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (typeof route.path === 'string') {
|
|
64
|
+
route.path = joinRoutePath(locale, route.path);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function joinRoutePath(prefix, routePath) {
|
|
70
|
+
const cleanPrefix = prefix.replace(/^\/+|\/+$/g, '');
|
|
71
|
+
const cleanPath = routePath.replace(/^\/+|\/+$/g, '');
|
|
72
|
+
return cleanPath ? `${cleanPrefix}/${cleanPath}` : cleanPrefix;
|
|
73
|
+
}
|
|
74
|
+
function localizedRouteId(route, locale, routeKey) {
|
|
75
|
+
const base = route.id ?? route.path ?? (route.index ? 'index' : route.file) ?? `route-${routeKey}`;
|
|
76
|
+
return `fimo.${sanitizeRouteId(locale)}.${sanitizeRouteId(base)}`;
|
|
77
|
+
}
|
|
78
|
+
function sanitizeRouteId(value) {
|
|
79
|
+
const clean = value
|
|
80
|
+
.replace(/^\.?\//, '')
|
|
81
|
+
.replace(/\.[tj]sx?$/i, '')
|
|
82
|
+
.replace(/[^A-Za-z0-9_$-]+/g, '.')
|
|
83
|
+
.replace(/^\.+|\.+$/g, '');
|
|
84
|
+
return clean || 'route';
|
|
85
|
+
}
|
package/dist/seo.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type FimoSEO } from 'fimo/config';
|
|
2
|
+
export declare function getSeoForPath(pathname: string, routeData?: unknown): FimoSEO | undefined;
|
|
3
|
+
export declare function buildMetaForPath(pathname: string, routeData?: unknown): ({
|
|
4
|
+
title: string;
|
|
5
|
+
name?: undefined;
|
|
6
|
+
property?: undefined;
|
|
7
|
+
content?: undefined;
|
|
8
|
+
tagName?: undefined;
|
|
9
|
+
rel?: undefined;
|
|
10
|
+
href?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
title?: undefined;
|
|
13
|
+
name: string;
|
|
14
|
+
content: string;
|
|
15
|
+
property?: undefined;
|
|
16
|
+
tagName?: undefined;
|
|
17
|
+
rel?: undefined;
|
|
18
|
+
href?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
title?: undefined;
|
|
21
|
+
name?: undefined;
|
|
22
|
+
property: string;
|
|
23
|
+
content: string;
|
|
24
|
+
tagName?: undefined;
|
|
25
|
+
rel?: undefined;
|
|
26
|
+
href?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
title?: undefined;
|
|
29
|
+
name?: undefined;
|
|
30
|
+
property?: undefined;
|
|
31
|
+
content?: undefined;
|
|
32
|
+
tagName: string;
|
|
33
|
+
rel: string;
|
|
34
|
+
href: string;
|
|
35
|
+
})[];
|
|
36
|
+
export declare function buildFimoMeta(args: {
|
|
37
|
+
location: {
|
|
38
|
+
pathname: string;
|
|
39
|
+
};
|
|
40
|
+
matches: readonly unknown[];
|
|
41
|
+
}): ({
|
|
42
|
+
title: string;
|
|
43
|
+
name?: undefined;
|
|
44
|
+
property?: undefined;
|
|
45
|
+
content?: undefined;
|
|
46
|
+
tagName?: undefined;
|
|
47
|
+
rel?: undefined;
|
|
48
|
+
href?: undefined;
|
|
49
|
+
} | {
|
|
50
|
+
title?: undefined;
|
|
51
|
+
name: string;
|
|
52
|
+
content: string;
|
|
53
|
+
property?: undefined;
|
|
54
|
+
tagName?: undefined;
|
|
55
|
+
rel?: undefined;
|
|
56
|
+
href?: undefined;
|
|
57
|
+
} | {
|
|
58
|
+
title?: undefined;
|
|
59
|
+
name?: undefined;
|
|
60
|
+
property: string;
|
|
61
|
+
content: string;
|
|
62
|
+
tagName?: undefined;
|
|
63
|
+
rel?: undefined;
|
|
64
|
+
href?: undefined;
|
|
65
|
+
} | {
|
|
66
|
+
title?: undefined;
|
|
67
|
+
name?: undefined;
|
|
68
|
+
property?: undefined;
|
|
69
|
+
content?: undefined;
|
|
70
|
+
tagName: string;
|
|
71
|
+
rel: string;
|
|
72
|
+
href: string;
|
|
73
|
+
})[];
|
|
74
|
+
/**
|
|
75
|
+
* Returns the attributes Fimo injects onto the root `<html>` element.
|
|
76
|
+
* Currently: `lang` derived from the active locale or `i18n.defaultLocale`.
|
|
77
|
+
* May grow to include `dir`, `data-theme`, or other a11y/theming attributes
|
|
78
|
+
* without requiring changes to root.tsx.
|
|
79
|
+
*/
|
|
80
|
+
export declare function fimoHtmlProps(localeOrOptions?: string | {
|
|
81
|
+
locale?: string;
|
|
82
|
+
}): {
|
|
83
|
+
lang: string;
|
|
84
|
+
dir: 'ltr' | 'rtl';
|
|
85
|
+
};
|
|
86
|
+
/** Root-loader helper: per-route SEO entry data plus the resolved locale. */
|
|
87
|
+
export declare function loadFimoMetaData({ request }: {
|
|
88
|
+
request: Request;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
fimoMetaData: Record<string, unknown> | undefined;
|
|
91
|
+
fimoLocale: import("fimo-react").FimoLocaleResolution;
|
|
92
|
+
}>;
|
|
93
|
+
//# sourceMappingURL=seo.d.ts.map
|
package/dist/seo.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getFimoTextDirection, toHtmlLang } from 'fimo/config';
|
|
2
|
+
import { normalizePathname, resolveSeoForPath } from 'fimo/seo';
|
|
3
|
+
// @ts-ignore - virtual module provided by fimoConfigPlugin in fimo-vite
|
|
4
|
+
import rawConfig from 'virtual:fimo-config';
|
|
5
|
+
import { resolveFimoLocaleForUrl, resolveMetaEntries } from './prefetch.js';
|
|
6
|
+
const config = rawConfig;
|
|
7
|
+
export function getSeoForPath(pathname, routeData) {
|
|
8
|
+
return resolveSeoForPath(pathname, config, routeData);
|
|
9
|
+
}
|
|
10
|
+
function buildCanonicalUrl(seoUrl, pathname) {
|
|
11
|
+
if (!seoUrl) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return new URL(normalizePathname(pathname), seoUrl).toString();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function seoToMeta(seo, pathname) {
|
|
22
|
+
const canonical = buildCanonicalUrl(seo.url, pathname);
|
|
23
|
+
return [
|
|
24
|
+
...(seo.title ? [{ title: seo.title }] : []),
|
|
25
|
+
...(seo.description ? [{ name: 'description', content: seo.description }] : []),
|
|
26
|
+
...(seo.siteName ? [{ property: 'og:site_name', content: seo.siteName }] : []),
|
|
27
|
+
...(seo.title ? [{ property: 'og:title', content: seo.title }] : []),
|
|
28
|
+
...(seo.description ? [{ property: 'og:description', content: seo.description }] : []),
|
|
29
|
+
...(canonical ? [{ property: 'og:url', content: canonical }] : []),
|
|
30
|
+
...(seo.image ? [{ property: 'og:image', content: seo.image }] : []),
|
|
31
|
+
...(seo.locale ? [{ property: 'og:locale', content: seo.locale }] : []),
|
|
32
|
+
...(seo.title ? [{ name: 'twitter:title', content: seo.title }] : []),
|
|
33
|
+
...(seo.description ? [{ name: 'twitter:description', content: seo.description }] : []),
|
|
34
|
+
...(seo.image ? [{ name: 'twitter:image', content: seo.image }] : []),
|
|
35
|
+
...(canonical ? [{ tagName: 'link', rel: 'canonical', href: canonical }] : []),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
export function buildMetaForPath(pathname, routeData) {
|
|
39
|
+
const seo = getSeoForPath(pathname, routeData);
|
|
40
|
+
if (!seo) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return seoToMeta(seo, pathname);
|
|
44
|
+
}
|
|
45
|
+
export function buildFimoMeta(args) {
|
|
46
|
+
const pathname = args.location.pathname;
|
|
47
|
+
const firstMatch = args.matches[0];
|
|
48
|
+
const rootData = firstMatch && typeof firstMatch === 'object' && 'data' in firstMatch
|
|
49
|
+
? firstMatch.data
|
|
50
|
+
: undefined;
|
|
51
|
+
const routeData = rootData && typeof rootData === 'object' ? rootData.fimoMetaData : undefined;
|
|
52
|
+
const seo = getSeoForPath(pathname, routeData);
|
|
53
|
+
if (!seo) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return seoToMeta(seo, pathname);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the attributes Fimo injects onto the root `<html>` element.
|
|
60
|
+
* Currently: `lang` derived from the active locale or `i18n.defaultLocale`.
|
|
61
|
+
* May grow to include `dir`, `data-theme`, or other a11y/theming attributes
|
|
62
|
+
* without requiring changes to root.tsx.
|
|
63
|
+
*/
|
|
64
|
+
export function fimoHtmlProps(localeOrOptions) {
|
|
65
|
+
const explicitLocale = typeof localeOrOptions === 'string' ? localeOrOptions : localeOrOptions?.locale;
|
|
66
|
+
const locale = explicitLocale ?? config.i18n?.defaultLocale ?? 'en';
|
|
67
|
+
return { lang: toHtmlLang(locale), dir: getFimoTextDirection(locale) };
|
|
68
|
+
}
|
|
69
|
+
/** Root-loader helper: per-route SEO entry data plus the resolved locale. */
|
|
70
|
+
export async function loadFimoMetaData({ request }) {
|
|
71
|
+
const fimoMetaData = await resolveMetaEntries(request.url);
|
|
72
|
+
const fimoLocale = resolveFimoLocaleForUrl(request.url);
|
|
73
|
+
return { fimoMetaData, fimoLocale };
|
|
74
|
+
}
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type FimoViteOptions } from 'fimo-vite';
|
|
2
|
+
import type { PluginOption } from 'vite';
|
|
3
|
+
export type { FimoViteOptions } from 'fimo-vite';
|
|
4
|
+
/**
|
|
5
|
+
* Whether the project at `root` is a React Router framework-mode app.
|
|
6
|
+
*
|
|
7
|
+
* Gates the one framework-mode-only line below, so a plain-Vite React project
|
|
8
|
+
* that uses `BrowserRouter` never inherits it. Checks the manifest first: the
|
|
9
|
+
* config file can carry any of four extensions, but framework mode is
|
|
10
|
+
* impossible without `@react-router/dev`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isReactRouterProject(root: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The Fimo Vite plugin for React Router projects: `fimo-vite`'s neutral
|
|
15
|
+
* plugins plus the React/React-Router dependency pre-bundling those projects
|
|
16
|
+
* need. Options are `fimo-vite`'s; `seo` applies to a plain Vite index.html,
|
|
17
|
+
* while React Router framework mode owns SEO through route `meta` exports.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fimo(opts?: FimoViteOptions): PluginOption[];
|
|
20
|
+
//# sourceMappingURL=vite.d.ts.map
|