@webround/core 0.1.8 → 0.3.0
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/dist/base-css.d.ts +8 -0
- package/dist/base-css.js +19 -0
- package/dist/components/WrAltcha.d.ts +5 -0
- package/dist/components/WrAltcha.js +7 -0
- package/dist/components/WrButton.js +19 -0
- package/dist/components/WrForm.d.ts +7 -0
- package/dist/components/WrForm.js +15 -0
- package/dist/components/WrFormPrivacy.d.ts +4 -0
- package/dist/components/WrFormPrivacy.js +6 -0
- package/dist/components/WrFormSuccess.d.ts +4 -0
- package/dist/components/WrFormSuccess.js +6 -0
- package/dist/components/WrIframe.d.ts +8 -0
- package/dist/components/WrIframe.js +13 -0
- package/dist/components/WrImage.d.ts +9 -0
- package/dist/components/WrImage.js +13 -0
- package/dist/components/WrInput.d.ts +8 -0
- package/dist/components/WrInput.js +10 -0
- package/dist/components/WrLangSelector.d.ts +17 -0
- package/dist/components/WrLangSelector.js +27 -0
- package/dist/components/WrLangSync.d.ts +6 -0
- package/dist/components/WrLangSync.js +12 -0
- package/dist/components/WrLink.js +23 -0
- package/dist/components/WrNav.d.ts +26 -0
- package/dist/components/WrNav.js +15 -0
- package/dist/{WrSection.d.ts → components/WrSection.d.ts} +3 -3
- package/dist/components/WrShared.d.ts +22 -0
- package/dist/components/WrShared.js +18 -0
- package/dist/components/WrText.d.ts +11 -0
- package/dist/components/WrText.js +14 -0
- package/dist/components/WrThemeToggle.d.ts +9 -0
- package/dist/components/WrThemeToggle.js +43 -0
- package/dist/components/WrVideo.js +61 -0
- package/dist/context/WrAppContext.d.ts +40 -0
- package/dist/context/WrAppContext.js +95 -0
- package/dist/context/WrLayout.d.ts +23 -0
- package/dist/context/WrLayout.js +18 -0
- package/dist/context/WrPageContext.d.ts +10 -0
- package/dist/context/WrPageContext.js +13 -0
- package/dist/form-react.d.ts +82 -0
- package/dist/form-react.js +165 -0
- package/dist/form.d.ts +83 -0
- package/dist/form.js +175 -0
- package/dist/index.css +651 -0
- package/dist/index.d.ts +20 -8
- package/dist/index.js +20 -8
- package/dist/kinds.d.ts +109 -0
- package/dist/kinds.js +35 -0
- package/dist/lang-codes.d.ts +366 -0
- package/dist/lang-codes.js +166 -0
- package/dist/localized.d.ts +36 -0
- package/dist/localized.js +71 -0
- package/dist/not-found.d.ts +9 -0
- package/dist/not-found.js +13 -0
- package/dist/routing.d.ts +122 -0
- package/dist/routing.js +198 -0
- package/dist/seo-react.d.ts +20 -0
- package/dist/seo-react.js +37 -0
- package/dist/seo.d.ts +167 -0
- package/dist/seo.js +221 -0
- package/dist/site-env.d.ts +25 -0
- package/dist/site-env.js +37 -0
- package/dist/style.d.ts +456 -0
- package/dist/style.js +1081 -0
- package/dist/text-react.d.ts +11 -0
- package/dist/text-react.js +5 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +45 -0
- package/dist/types.d.ts +37 -21
- package/package.json +93 -7
- package/dist/WrButton.d.ts.map +0 -1
- package/dist/WrButton.js +0 -7
- package/dist/WrButton.js.map +0 -1
- package/dist/WrImage.d.ts +0 -8
- package/dist/WrImage.d.ts.map +0 -1
- package/dist/WrImage.js +0 -7
- package/dist/WrImage.js.map +0 -1
- package/dist/WrLink.d.ts.map +0 -1
- package/dist/WrLink.js +0 -15
- package/dist/WrLink.js.map +0 -1
- package/dist/WrPageContext.d.ts +0 -8
- package/dist/WrPageContext.d.ts.map +0 -1
- package/dist/WrPageContext.js +0 -11
- package/dist/WrPageContext.js.map +0 -1
- package/dist/WrSection.d.ts.map +0 -1
- package/dist/WrSection.js.map +0 -1
- package/dist/WrText.d.ts +0 -11
- package/dist/WrText.d.ts.map +0 -1
- package/dist/WrText.js +0 -7
- package/dist/WrText.js.map +0 -1
- package/dist/WrVideo.d.ts.map +0 -1
- package/dist/WrVideo.js +0 -42
- package/dist/WrVideo.js.map +0 -1
- package/dist/bridge.d.ts +0 -2
- package/dist/bridge.d.ts.map +0 -1
- package/dist/bridge.js +0 -110
- package/dist/bridge.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- /package/dist/{WrButton.d.ts → components/WrButton.d.ts} +0 -0
- /package/dist/{WrLink.d.ts → components/WrLink.d.ts} +0 -0
- /package/dist/{WrSection.js → components/WrSection.js} +0 -0
- /package/dist/{WrVideo.d.ts → components/WrVideo.d.ts} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A value that may come per language: a plain string, or `{ default, it, en… }` (what the page JSON holds for text, alt…). */
|
|
2
|
+
export type WrLocalized = string | Record<string, string>;
|
|
3
|
+
/** The value in a language, falling back to `default`; undefined when there is nothing to show (or it is not a localized value at all). */
|
|
4
|
+
export declare function wrLocalized(value: unknown, lang: string): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* A localized value with one language written (or rewritten): the others are kept. A plain string (what a field held before it was
|
|
7
|
+
* translated) becomes the `default`. THE way a translation is written: the components of the editor and the operations an agent runs
|
|
8
|
+
* use this one, so the two cannot disagree on what a translated field looks like.
|
|
9
|
+
*/
|
|
10
|
+
export declare function wrSetLocalized(existing: unknown, lang: string, value: string): Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* `wrSetLocalized` for a field the user can empty: an empty text in a language other than the default takes that language's text away (the
|
|
13
|
+
* default one is shown instead, as for a language that was never written), instead of showing nothing. The default language keeps its empty text.
|
|
14
|
+
*/
|
|
15
|
+
export declare function wrWriteLocalized(existing: unknown, lang: string, value: string): Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* A value that exists in one language so far, written for the first time. The default language is never left empty (a link with no text in
|
|
18
|
+
* the site's own language is worse than one that is not yet translated): in another language the text is also the default one until someone
|
|
19
|
+
* writes it, which is what a visitor of the default language would read anyway.
|
|
20
|
+
*/
|
|
21
|
+
export declare function wrNewLocalized(lang: string, value: string): Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* The entries of a list (a menu) after a caller wrote their `translated` fields in ONE language: `incoming` has them as plain text, the entries
|
|
24
|
+
* already there (`existing`) keep what they say in the other languages. An incoming entry is the one that is already there when its other
|
|
25
|
+
* fields (the ones that are not translated: an `href`) are the same; one that is not there yet is new. A translated field that arrives as
|
|
26
|
+
* `{ default, en… }` is taken as it is. THE rule for writing translated entries: the panel and every operation use it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function wrLocalizeItems(existing: unknown, incoming: unknown, translated: readonly string[], lang: string): Record<string, unknown>[];
|
|
29
|
+
/**
|
|
30
|
+
* The `aria-label` of an element in a language: what the JSON says (it can be translated, so it wins) or else the one written in the
|
|
31
|
+
* code. `lang` is the language of the page being shown; without it, the default one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function wrAriaLabel(config: {
|
|
34
|
+
ariaLabel?: unknown;
|
|
35
|
+
} | undefined, fromCode: string | undefined, lang?: string): string | undefined;
|
|
36
|
+
//# sourceMappingURL=localized.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The value in a language, falling back to `default`; undefined when there is nothing to show (or it is not a localized value at all). */
|
|
2
|
+
export function wrLocalized(value, lang) {
|
|
3
|
+
if (typeof value === 'string')
|
|
4
|
+
return value;
|
|
5
|
+
if (typeof value !== 'object' || value === null)
|
|
6
|
+
return undefined;
|
|
7
|
+
const map = value;
|
|
8
|
+
const found = map[lang] ?? map['default'];
|
|
9
|
+
return typeof found === 'string' ? found : undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A localized value with one language written (or rewritten): the others are kept. A plain string (what a field held before it was
|
|
13
|
+
* translated) becomes the `default`. THE way a translation is written: the components of the editor and the operations an agent runs
|
|
14
|
+
* use this one, so the two cannot disagree on what a translated field looks like.
|
|
15
|
+
*/
|
|
16
|
+
export function wrSetLocalized(existing, lang, value) {
|
|
17
|
+
const current = typeof existing === 'string' ? { default: existing }
|
|
18
|
+
: typeof existing === 'object' && existing !== null && !Array.isArray(existing) ? { ...existing }
|
|
19
|
+
: {};
|
|
20
|
+
return { ...current, [lang]: value };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `wrSetLocalized` for a field the user can empty: an empty text in a language other than the default takes that language's text away (the
|
|
24
|
+
* default one is shown instead, as for a language that was never written), instead of showing nothing. The default language keeps its empty text.
|
|
25
|
+
*/
|
|
26
|
+
export function wrWriteLocalized(existing, lang, value) {
|
|
27
|
+
const written = wrSetLocalized(existing, lang, value);
|
|
28
|
+
if (value === '' && lang !== 'default')
|
|
29
|
+
delete written[lang];
|
|
30
|
+
return written;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A value that exists in one language so far, written for the first time. The default language is never left empty (a link with no text in
|
|
34
|
+
* the site's own language is worse than one that is not yet translated): in another language the text is also the default one until someone
|
|
35
|
+
* writes it, which is what a visitor of the default language would read anyway.
|
|
36
|
+
*/
|
|
37
|
+
export function wrNewLocalized(lang, value) {
|
|
38
|
+
return lang === 'default' ? { default: value } : { default: value, [lang]: value };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The entries of a list (a menu) after a caller wrote their `translated` fields in ONE language: `incoming` has them as plain text, the entries
|
|
42
|
+
* already there (`existing`) keep what they say in the other languages. An incoming entry is the one that is already there when its other
|
|
43
|
+
* fields (the ones that are not translated: an `href`) are the same; one that is not there yet is new. A translated field that arrives as
|
|
44
|
+
* `{ default, en… }` is taken as it is. THE rule for writing translated entries: the panel and every operation use it.
|
|
45
|
+
*/
|
|
46
|
+
export function wrLocalizeItems(existing, incoming, translated, lang) {
|
|
47
|
+
const before = Array.isArray(existing) ? existing.filter(item => typeof item === 'object' && item !== null) : [];
|
|
48
|
+
const claimed = new Set();
|
|
49
|
+
const sameEntry = (a, b) => Object.keys({ ...a, ...b }).filter(key => !translated.includes(key)).every(key => a[key] === b[key]);
|
|
50
|
+
return (Array.isArray(incoming) ? incoming : []).map((item) => {
|
|
51
|
+
const index = before.findIndex((known, i) => !claimed.has(i) && sameEntry(known, item));
|
|
52
|
+
if (index >= 0)
|
|
53
|
+
claimed.add(index);
|
|
54
|
+
const out = { ...item };
|
|
55
|
+
for (const field of translated) {
|
|
56
|
+
const value = item[field];
|
|
57
|
+
if (typeof value !== 'string')
|
|
58
|
+
continue;
|
|
59
|
+
out[field] = index >= 0 ? wrWriteLocalized(before[index][field], lang, value) : wrNewLocalized(lang, value);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The `aria-label` of an element in a language: what the JSON says (it can be translated, so it wins) or else the one written in the
|
|
66
|
+
* code. `lang` is the language of the page being shown; without it, the default one.
|
|
67
|
+
*/
|
|
68
|
+
export function wrAriaLabel(config, fromCode, lang = 'default') {
|
|
69
|
+
return wrLocalized(config?.ariaLabel, lang) ?? fromCode;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=localized.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type WrLangConfig } from './routing';
|
|
2
|
+
/**
|
|
3
|
+
* What an address that leads to no page shows: nothing, the visitor is taken to the home page of their language (the URL is replaced, so
|
|
4
|
+
* "back" does not bring them here again). Both packages use this one. Never redirects to where it already is (a site without a home page).
|
|
5
|
+
*/
|
|
6
|
+
export declare function WrNotFound({ langs }: {
|
|
7
|
+
langs?: WrLangConfig;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=not-found.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
3
|
+
import { wrHomeOf } from './routing';
|
|
4
|
+
/**
|
|
5
|
+
* What an address that leads to no page shows: nothing, the visitor is taken to the home page of their language (the URL is replaced, so
|
|
6
|
+
* "back" does not bring them here again). Both packages use this one. Never redirects to where it already is (a site without a home page).
|
|
7
|
+
*/
|
|
8
|
+
export function WrNotFound({ langs }) {
|
|
9
|
+
const { pathname } = useLocation();
|
|
10
|
+
const home = wrHomeOf(pathname, langs);
|
|
11
|
+
return home === pathname ? null : _jsx(Navigate, { to: home, replace: true });
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=not-found.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { WrPageConfig } from '@webround/core/types';
|
|
3
|
+
import { LangCode } from './lang-codes';
|
|
4
|
+
export interface WrRouteEntry {
|
|
5
|
+
component: string;
|
|
6
|
+
lazy?: boolean;
|
|
7
|
+
/** Header e footer del sito sono di default su ogni pagina: `false` li toglie da questa (una landing, per esempio). */
|
|
8
|
+
header?: boolean;
|
|
9
|
+
footer?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type WrRoutesFile = Record<string, WrRouteEntry>;
|
|
12
|
+
export interface WrResolvedRoute {
|
|
13
|
+
routePath: string;
|
|
14
|
+
pattern: string;
|
|
15
|
+
lazy: boolean;
|
|
16
|
+
component: string;
|
|
17
|
+
Component?: ComponentType;
|
|
18
|
+
loadComponent?: () => Promise<{
|
|
19
|
+
default: ComponentType;
|
|
20
|
+
}>;
|
|
21
|
+
/** Già risolti (default true): la pagina decide al primo render, senza effetti e senza stato. */
|
|
22
|
+
header: boolean;
|
|
23
|
+
footer: boolean;
|
|
24
|
+
resolvePageName: (params: Record<string, string>) => string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Header e footer sono condivisi tra le pagine: sono componenti scritti una volta (`src/layout/Header.tsx`, `Footer.tsx`) e il loro
|
|
28
|
+
* JSON (solo ciò che il codice non può dire: voci di menu, traduzioni) sta a parte, in `config/pages/_shared/`. Un ambito = il file
|
|
29
|
+
* del codice + il nome del suo JSON.
|
|
30
|
+
*/
|
|
31
|
+
export declare const WR_SHARED_SCOPES: {
|
|
32
|
+
readonly header: {
|
|
33
|
+
readonly file: "layout/Header.tsx";
|
|
34
|
+
readonly name: "_shared/header";
|
|
35
|
+
};
|
|
36
|
+
readonly footer: {
|
|
37
|
+
readonly file: "layout/Footer.tsx";
|
|
38
|
+
readonly name: "_shared/footer";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** A shared component's scope: the file of its code (relative to `src/`) and the name of its JSON in `config/pages/`. */
|
|
42
|
+
export interface WrSharedScope {
|
|
43
|
+
file: string;
|
|
44
|
+
name: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The shared components a site declares, next to its header and footer (`src/config/shared.json`): name → file, relative to `src/`
|
|
48
|
+
* (`{ "product-card": "components/ProductCard.tsx" }`). The component wraps what it returns in `<WrShared name="product-card">`; its JSON
|
|
49
|
+
* is `config/pages/_shared/product-card.json`. A shared component can live anywhere and be used anywhere, any number of times: it is one
|
|
50
|
+
* file and one JSON, like the header.
|
|
51
|
+
*/
|
|
52
|
+
export type WrSharedFile = Record<string, string>;
|
|
53
|
+
export declare const WR_SHARED_FILE = "config/shared.json";
|
|
54
|
+
/** The scope name (the JSON's) of the shared component called `name`. */
|
|
55
|
+
export declare const wrSharedName: (name: string) => string;
|
|
56
|
+
/** Why `name` → `file` cannot be a shared component (null when it can). */
|
|
57
|
+
export declare function wrSharedProblem(name: string, file: unknown): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* EVERY shared scope of a site: its header and footer, then the ones it declares (an entry that cannot be one, or that points at the file of
|
|
60
|
+
* another, is left out: `wrSharedProblem` says why). The ONE list the editor, the operations and the checks read.
|
|
61
|
+
*/
|
|
62
|
+
export declare function wrSharedScopes(declared?: WrSharedFile | null): WrSharedScope[];
|
|
63
|
+
export interface WrRegistry {
|
|
64
|
+
[componentName: string]: ComponentType;
|
|
65
|
+
}
|
|
66
|
+
export interface WrLazyRegistry {
|
|
67
|
+
[componentName: string]: () => Promise<{
|
|
68
|
+
default: ComponentType;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
export interface WrResolveErrors {
|
|
72
|
+
missingComponents: {
|
|
73
|
+
routePath: string;
|
|
74
|
+
component: string;
|
|
75
|
+
}[];
|
|
76
|
+
}
|
|
77
|
+
export interface WrResolveResult {
|
|
78
|
+
routes: WrResolvedRoute[];
|
|
79
|
+
errors: WrResolveErrors;
|
|
80
|
+
}
|
|
81
|
+
/** The path a route is served at: `home` is `/`, `about` is `/about`, `blog/[slug]` is `/blog/:slug`. */
|
|
82
|
+
export declare function wrRoutePattern(routePath: string): string;
|
|
83
|
+
export declare function wrResolveRoutes(routesFile: WrRoutesFile, registry: WrRegistry, lazyRegistry?: WrLazyRegistry): WrResolveResult;
|
|
84
|
+
type GlobModules = Record<string, {
|
|
85
|
+
default?: ComponentType;
|
|
86
|
+
}>;
|
|
87
|
+
type GlobLazyModules = Record<string, () => Promise<{
|
|
88
|
+
default: ComponentType;
|
|
89
|
+
}>>;
|
|
90
|
+
export declare function wrBuildRegistry(modules: GlobModules): WrRegistry;
|
|
91
|
+
export declare function wrBuildLazyRegistry(modules: GlobLazyModules): WrLazyRegistry;
|
|
92
|
+
export declare function wrPageConfigPath(pageName: string): string;
|
|
93
|
+
/** The file of a page's component, relative to `src/`: the one place that says where pages live. */
|
|
94
|
+
export declare function wrPageFilePath(component: string): string;
|
|
95
|
+
type PageConfigModules = Record<string, WrPageConfig | {
|
|
96
|
+
default: WrPageConfig;
|
|
97
|
+
}>;
|
|
98
|
+
export declare function wrBuildPageConfigMap(modules: PageConfigModules): Record<string, WrPageConfig>;
|
|
99
|
+
export interface WrLangConfig {
|
|
100
|
+
/** Lang code che non compare nel path */
|
|
101
|
+
default: LangCode;
|
|
102
|
+
/** Tutti i lang code disponibili, default incluso */
|
|
103
|
+
available: LangCode[];
|
|
104
|
+
}
|
|
105
|
+
/** Per ogni route risolta, registra anche la variante /:lang/... con lo stesso Component/resolvePageName. */
|
|
106
|
+
export declare function wrExpandRoutesForLangs(routes: WrResolvedRoute[], langs?: WrLangConfig): WrResolvedRoute[];
|
|
107
|
+
/** Deriva la lingua corrente da un pathname. 'default' per "/", "/it" (= il default), o prefissi non previsti. */
|
|
108
|
+
/**
|
|
109
|
+
* The key a language has in the JSON (`{ default: …, en: … }`): the site's default language is always `default`, the others are their
|
|
110
|
+
* code. What the page context calls `lang`, and what every translated field is written under.
|
|
111
|
+
*/
|
|
112
|
+
export declare function wrLangKey(code: string, langs?: WrLangConfig): string;
|
|
113
|
+
export declare function wrExtractLang(pathname: string, langs: WrLangConfig): LangCode;
|
|
114
|
+
/** Costruisce il path per una data lingua, mantenendo lo stesso slug/segmenti correnti. */
|
|
115
|
+
export declare function wrBuildLangPath(targetLang: LangCode, pathname: string, langs: WrLangConfig): string;
|
|
116
|
+
/** Where an address that leads to no page goes: the home page of the language it is in (`/en/nope` → `/en`, `/nope` → `/`). */
|
|
117
|
+
export declare function wrHomeOf(pathname: string, langs?: WrLangConfig): string;
|
|
118
|
+
/** An address that leaves the site: it has a scheme (`https:`, `mailto:`, `tel:`) or starts with `//`. Anything else is the site's own and is a router `Link`, whatever `target` it was given. */
|
|
119
|
+
export declare const wrIsExternalHref: (href: string) => boolean;
|
|
120
|
+
export declare function wrLocalizeHref(href: string, lang: string): string;
|
|
121
|
+
export {};
|
|
122
|
+
//# sourceMappingURL=routing.d.ts.map
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header e footer sono condivisi tra le pagine: sono componenti scritti una volta (`src/layout/Header.tsx`, `Footer.tsx`) e il loro
|
|
3
|
+
* JSON (solo ciò che il codice non può dire: voci di menu, traduzioni) sta a parte, in `config/pages/_shared/`. Un ambito = il file
|
|
4
|
+
* del codice + il nome del suo JSON.
|
|
5
|
+
*/
|
|
6
|
+
export const WR_SHARED_SCOPES = {
|
|
7
|
+
header: { file: 'layout/Header.tsx', name: '_shared/header' },
|
|
8
|
+
footer: { file: 'layout/Footer.tsx', name: '_shared/footer' },
|
|
9
|
+
};
|
|
10
|
+
export const WR_SHARED_FILE = 'config/shared.json';
|
|
11
|
+
/** The name a shared component can have: not the header's or the footer's. */
|
|
12
|
+
const SHARED_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
13
|
+
const SHARED_RESERVED = Object.keys(WR_SHARED_SCOPES);
|
|
14
|
+
/** The scope name (the JSON's) of the shared component called `name`. */
|
|
15
|
+
export const wrSharedName = (name) => `_shared/${name}`;
|
|
16
|
+
/** Why `name` → `file` cannot be a shared component (null when it can). */
|
|
17
|
+
export function wrSharedProblem(name, file) {
|
|
18
|
+
if (!SHARED_NAME.test(name))
|
|
19
|
+
return `"${name}" is not a name a shared component can have: lowercase words with dashes (product-card)`;
|
|
20
|
+
if (SHARED_RESERVED.includes(name))
|
|
21
|
+
return `"${name}" is the name of the shared ${name} that every site has`;
|
|
22
|
+
if (typeof file !== 'string' || !/^(?!\/)(?!.*\.\.)[\w./-]+\.tsx$/.test(file))
|
|
23
|
+
return `${name}: the file must be a .tsx under src/ (components/ProductCard.tsx)`;
|
|
24
|
+
if (Object.values(WR_SHARED_SCOPES).some(scope => scope.file === file))
|
|
25
|
+
return `${name}: ${file} is already the file of a shared component`;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* EVERY shared scope of a site: its header and footer, then the ones it declares (an entry that cannot be one, or that points at the file of
|
|
30
|
+
* another, is left out: `wrSharedProblem` says why). The ONE list the editor, the operations and the checks read.
|
|
31
|
+
*/
|
|
32
|
+
export function wrSharedScopes(declared) {
|
|
33
|
+
const scopes = Object.values(WR_SHARED_SCOPES).map(scope => ({ ...scope }));
|
|
34
|
+
if (!declared || typeof declared !== 'object')
|
|
35
|
+
return scopes;
|
|
36
|
+
for (const name of Object.keys(declared).sort()) {
|
|
37
|
+
const file = declared[name];
|
|
38
|
+
if (wrSharedProblem(name, file) !== null || scopes.some(scope => scope.file === file))
|
|
39
|
+
continue;
|
|
40
|
+
scopes.push({ file, name: wrSharedName(name) });
|
|
41
|
+
}
|
|
42
|
+
return scopes;
|
|
43
|
+
}
|
|
44
|
+
/** The path a route is served at: `home` is `/`, `about` is `/about`, `blog/[slug]` is `/blog/:slug`. */
|
|
45
|
+
export function wrRoutePattern(routePath) {
|
|
46
|
+
if (routePath === 'home')
|
|
47
|
+
return '/';
|
|
48
|
+
return `/${routePath.replace(/\[([^\]]+)\]/g, ':$1')}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The name of a page is its route, always: `product/[slug]` is ONE page (one JSON, `config/pages/product/[slug].json`, the one the editor and the
|
|
52
|
+
* operations write) whatever slug the address has. The slug is for the page's code (`useParams`), never for finding its values.
|
|
53
|
+
*/
|
|
54
|
+
function makePageNameResolver(routePath) {
|
|
55
|
+
return () => routePath;
|
|
56
|
+
}
|
|
57
|
+
export function wrResolveRoutes(routesFile, registry, lazyRegistry = {}) {
|
|
58
|
+
const routes = [];
|
|
59
|
+
const missingComponents = [];
|
|
60
|
+
for (const [routePath, entry] of Object.entries(routesFile)) {
|
|
61
|
+
const isLazy = entry.lazy === true;
|
|
62
|
+
if (isLazy) {
|
|
63
|
+
const loadComponent = lazyRegistry[entry.component];
|
|
64
|
+
if (!loadComponent) {
|
|
65
|
+
missingComponents.push({ routePath, component: entry.component });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
routes.push({
|
|
69
|
+
routePath, pattern: wrRoutePattern(routePath),
|
|
70
|
+
lazy: true, component: entry.component, loadComponent, resolvePageName: makePageNameResolver(routePath),
|
|
71
|
+
header: entry.header !== false, footer: entry.footer !== false,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const Component = registry[entry.component];
|
|
76
|
+
if (!Component) {
|
|
77
|
+
missingComponents.push({ routePath, component: entry.component });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
routes.push({
|
|
81
|
+
routePath, pattern: wrRoutePattern(routePath),
|
|
82
|
+
lazy: false, component: entry.component, Component, resolvePageName: makePageNameResolver(routePath),
|
|
83
|
+
header: entry.header !== false, footer: entry.footer !== false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return { routes, errors: { missingComponents } };
|
|
87
|
+
}
|
|
88
|
+
function nameFromPath(filePath) {
|
|
89
|
+
return filePath.replace(/^.*\/pages\//, '').replace(/\.tsx?$/, '');
|
|
90
|
+
}
|
|
91
|
+
function checkDuplicate(seen, name, filePath) {
|
|
92
|
+
if (seen[name] && seen[name] !== filePath) {
|
|
93
|
+
throw new Error(`wr: duplicate page component name "${name}" (${seen[name]} and ${filePath}). ` +
|
|
94
|
+
`Component filenames must be unique across src/pages.`);
|
|
95
|
+
}
|
|
96
|
+
seen[name] = filePath;
|
|
97
|
+
}
|
|
98
|
+
export function wrBuildRegistry(modules) {
|
|
99
|
+
const registry = {};
|
|
100
|
+
const seen = {};
|
|
101
|
+
for (const filePath in modules) {
|
|
102
|
+
const mod = modules[filePath];
|
|
103
|
+
if (!mod?.default)
|
|
104
|
+
continue;
|
|
105
|
+
const name = nameFromPath(filePath);
|
|
106
|
+
checkDuplicate(seen, name, filePath);
|
|
107
|
+
registry[name] = mod.default;
|
|
108
|
+
}
|
|
109
|
+
return registry;
|
|
110
|
+
}
|
|
111
|
+
export function wrBuildLazyRegistry(modules) {
|
|
112
|
+
const registry = {};
|
|
113
|
+
const seen = {};
|
|
114
|
+
for (const filePath in modules) {
|
|
115
|
+
const name = nameFromPath(filePath);
|
|
116
|
+
checkDuplicate(seen, name, filePath);
|
|
117
|
+
registry[name] = modules[filePath];
|
|
118
|
+
}
|
|
119
|
+
return registry;
|
|
120
|
+
}
|
|
121
|
+
export function wrPageConfigPath(pageName) {
|
|
122
|
+
return `config/pages/${pageName}.json`;
|
|
123
|
+
}
|
|
124
|
+
/** The file of a page's component, relative to `src/`: the one place that says where pages live. */
|
|
125
|
+
export function wrPageFilePath(component) {
|
|
126
|
+
return `pages/${component}.tsx`;
|
|
127
|
+
}
|
|
128
|
+
function unwrapPageConfig(mod) {
|
|
129
|
+
if (mod && typeof mod === 'object' && 'default' in mod && mod.default !== undefined) {
|
|
130
|
+
return mod.default;
|
|
131
|
+
}
|
|
132
|
+
return mod;
|
|
133
|
+
}
|
|
134
|
+
export function wrBuildPageConfigMap(modules) {
|
|
135
|
+
const map = {};
|
|
136
|
+
for (const filePath in modules) {
|
|
137
|
+
const pageName = filePath.replace(/^.*\/config\/pages\//, '').replace(/\.json$/, '');
|
|
138
|
+
map[pageName] = unwrapPageConfig(modules[filePath]);
|
|
139
|
+
}
|
|
140
|
+
return map;
|
|
141
|
+
}
|
|
142
|
+
/** Per ogni route risolta, registra anche la variante /:lang/... con lo stesso Component/resolvePageName. */
|
|
143
|
+
export function wrExpandRoutesForLangs(routes, langs) {
|
|
144
|
+
if (!langs)
|
|
145
|
+
return routes;
|
|
146
|
+
const langRoutes = routes.map((route) => ({
|
|
147
|
+
...route,
|
|
148
|
+
pattern: `/:lang${route.pattern === '/' ? '' : route.pattern}`,
|
|
149
|
+
}));
|
|
150
|
+
return [...routes, ...langRoutes];
|
|
151
|
+
}
|
|
152
|
+
/** Deriva la lingua corrente da un pathname. 'default' per "/", "/it" (= il default), o prefissi non previsti. */
|
|
153
|
+
/**
|
|
154
|
+
* The key a language has in the JSON (`{ default: …, en: … }`): the site's default language is always `default`, the others are their
|
|
155
|
+
* code. What the page context calls `lang`, and what every translated field is written under.
|
|
156
|
+
*/
|
|
157
|
+
export function wrLangKey(code, langs) {
|
|
158
|
+
return langs && code === langs.default ? 'default' : code;
|
|
159
|
+
}
|
|
160
|
+
export function wrExtractLang(pathname, langs) {
|
|
161
|
+
const [first] = pathname.split('/').filter(Boolean);
|
|
162
|
+
if (first === langs.default)
|
|
163
|
+
return 'default';
|
|
164
|
+
if (first && langs.available.includes(first))
|
|
165
|
+
return first;
|
|
166
|
+
return 'default';
|
|
167
|
+
}
|
|
168
|
+
/** Costruisce il path per una data lingua, mantenendo lo stesso slug/segmenti correnti. */
|
|
169
|
+
export function wrBuildLangPath(targetLang, pathname, langs) {
|
|
170
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
171
|
+
// A path may carry a language prefix — the default language's too (`/it/about` is `/about`): it is dropped, whichever it is
|
|
172
|
+
const prefixed = segments.length > 0 && langs.available.includes(segments[0]);
|
|
173
|
+
const rest = prefixed ? segments.slice(1) : segments;
|
|
174
|
+
const slugPart = rest.length ? `/${rest.join('/')}` : '';
|
|
175
|
+
return targetLang === 'default' ? (slugPart || '/') : `/${targetLang}${slugPart}`;
|
|
176
|
+
}
|
|
177
|
+
/** Where an address that leads to no page goes: the home page of the language it is in (`/en/nope` → `/en`, `/nope` → `/`). */
|
|
178
|
+
export function wrHomeOf(pathname, langs) {
|
|
179
|
+
return langs ? wrBuildLangPath(wrExtractLang(pathname, langs), '/', langs) : '/';
|
|
180
|
+
}
|
|
181
|
+
/** An address that leaves the site: it has a scheme (`https:`, `mailto:`, `tel:`) or starts with `//`. Anything else is the site's own and is a router `Link`, whatever `target` it was given. */
|
|
182
|
+
export const wrIsExternalHref = (href) => /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(href);
|
|
183
|
+
export function wrLocalizeHref(href, lang) {
|
|
184
|
+
if (!href)
|
|
185
|
+
return href;
|
|
186
|
+
if (lang === 'default')
|
|
187
|
+
return href;
|
|
188
|
+
if (/^([a-z][a-z0-9+.-]*:)?\/\//i.test(href))
|
|
189
|
+
return href; // http://, https://, //cdn...
|
|
190
|
+
if (/^(mailto|tel|sms):/i.test(href))
|
|
191
|
+
return href;
|
|
192
|
+
if (href.startsWith('#'))
|
|
193
|
+
return href;
|
|
194
|
+
if (href === '/')
|
|
195
|
+
return `/${lang}`;
|
|
196
|
+
return `/${lang}${href}`;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=routing.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type WrSeoPage, type WrSeoSite } from './seo.js';
|
|
2
|
+
import { type WrLangConfig } from './routing.js';
|
|
3
|
+
import type { WrPageConfig } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* The SEO of a page whose texts are not in its JSON but in data it loads (a product's name, description and picture), in the language shown. It goes through the same `WrSeoHead` as every
|
|
6
|
+
* page, so a tag is written once; what the page's JSON says is kept, and what is given here wins. Pass `undefined` while the data has not come.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useWrSeo(seo: WrSeoPage | undefined): void;
|
|
9
|
+
/**
|
|
10
|
+
* A page's tags for search engines and social networks, rendered where the page is: React puts `<title>`, `<meta>` and `<link>` in
|
|
11
|
+
* `<head>` by itself. Both packages use this one, so what the editor shows is what production serves.
|
|
12
|
+
*/
|
|
13
|
+
export declare function WrSeoHead({ site, config, pathname, lang, langs }: {
|
|
14
|
+
site?: WrSeoSite;
|
|
15
|
+
config: WrPageConfig | undefined;
|
|
16
|
+
pathname: string;
|
|
17
|
+
lang: string;
|
|
18
|
+
langs?: WrLangConfig;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=seo-react.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useSyncExternalStore } from 'react';
|
|
3
|
+
import { wrSeoPageOf, wrSeoTags } from './seo.js';
|
|
4
|
+
import { wrBuildLangPath } from './routing.js';
|
|
5
|
+
const PROP = { hreflang: 'hrefLang' };
|
|
6
|
+
// What a page whose texts come from data (a product) says about itself, on top of what its JSON says. One page is shown at a time, so it is one value: the page sets it while it is shown, `WrSeoHead` reads it.
|
|
7
|
+
let dynamicSeo;
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
const publish = (next) => { dynamicSeo = next; listeners.forEach((listener) => listener()); };
|
|
10
|
+
const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; };
|
|
11
|
+
/**
|
|
12
|
+
* The SEO of a page whose texts are not in its JSON but in data it loads (a product's name, description and picture), in the language shown. It goes through the same `WrSeoHead` as every
|
|
13
|
+
* page, so a tag is written once; what the page's JSON says is kept, and what is given here wins. Pass `undefined` while the data has not come.
|
|
14
|
+
*/
|
|
15
|
+
export function useWrSeo(seo) {
|
|
16
|
+
const key = JSON.stringify(seo ?? null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
publish(seo);
|
|
19
|
+
return () => publish(undefined);
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- `key` is the value of `seo`
|
|
21
|
+
}, [key]);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A page's tags for search engines and social networks, rendered where the page is: React puts `<title>`, `<meta>` and `<link>` in
|
|
25
|
+
* `<head>` by itself. Both packages use this one, so what the editor shows is what production serves.
|
|
26
|
+
*/
|
|
27
|
+
export function WrSeoHead({ site, config, pathname, lang, langs }) {
|
|
28
|
+
const path = langs ? wrBuildLangPath('default', pathname, langs) : pathname;
|
|
29
|
+
const dynamic = useSyncExternalStore(subscribe, () => dynamicSeo, () => undefined);
|
|
30
|
+
const { title, tags, jsonLd } = wrSeoTags({ site, page: { ...wrSeoPageOf(config), ...dynamic }, path, lang, langs });
|
|
31
|
+
return (_jsxs(_Fragment, { children: [title && _jsx("title", { children: title }), tags.map(({ tag, attrs }) => {
|
|
32
|
+
const props = Object.fromEntries(Object.entries(attrs).map(([key, value]) => [PROP[key] ?? key, value]));
|
|
33
|
+
const key = `${tag}:${attrs.name ?? attrs.property ?? `${attrs.rel}:${attrs.hreflang ?? ''}`}`;
|
|
34
|
+
return tag === 'meta' ? _jsx("meta", { ...props }, key) : _jsx("link", { ...props }, key);
|
|
35
|
+
}), jsonLd.map((body, index) => _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: body } }, `ld:${index}`))] }));
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=seo-react.js.map
|