@agence-zef/astro 0.4.1 → 0.6.0-pilot.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/components/Accordion.astro +61 -0
- package/components/AccordionItem.astro +23 -0
- package/components/Checkbox.astro +20 -0
- package/components/Dialog.astro +72 -0
- package/components/Field.astro +27 -0
- package/components/Image.astro +21 -5
- package/components/Input.astro +49 -0
- package/components/Popover.astro +21 -0
- package/components/Select.astro +55 -0
- package/components/routes/cache.ts +10 -0
- package/components/routes/llms.ts +4 -1
- package/components/routes/page-cached.astro +69 -0
- package/components/routes/revalidate.ts +33 -0
- package/components/routes/robots.ts +4 -1
- package/components/routes/sitemap.ts +4 -1
- package/dist/helpers.d.ts +6 -0
- package/dist/helpers.js +81 -22
- package/dist/index.js +54 -6
- package/dist/seo.d.ts +2 -0
- package/dist/seo.js +3 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.js +33 -0
- package/package.json +22 -5
- package/styles.css +468 -0
package/dist/helpers.js
CHANGED
|
@@ -43,6 +43,85 @@ export async function zefLayoutProps(opts) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
const toParam = (url) => url.replace(/^\/+|\/+$/g, "") || undefined;
|
|
46
|
+
/** Arborescence des pages : fil d'Ariane d'une page, accueil de chaque langue. */
|
|
47
|
+
function pageTree(entries, site) {
|
|
48
|
+
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
49
|
+
const homeOf = (locale) => entries.find((e) => e.urls[locale] === "/" || e.urls[locale] === `/${locale}`);
|
|
50
|
+
/** Accueil, puis les pages parentes, puis la page elle-même. */
|
|
51
|
+
const trail = (id, locale) => {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (let e = byId.get(id); e; e = e.parentId ? byId.get(e.parentId) : undefined) {
|
|
54
|
+
const url = e.urls[locale];
|
|
55
|
+
if (url)
|
|
56
|
+
out.unshift({ title: e.titles[locale] || e.titles[site.defaultLocale] || "", url });
|
|
57
|
+
}
|
|
58
|
+
const home = homeOf(locale);
|
|
59
|
+
if (home && out[0]?.url !== home.urls[locale]) {
|
|
60
|
+
out.unshift({ title: home.titles[locale] || site.name, url: home.urls[locale] });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
return { trail, homeOf };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Rendu à la demande (`rendering: "cached"`) : les props du gabarit pour une adresse, page ou
|
|
68
|
+
* élément de collection, sans parcourir tout le site. `null` si rien n'est publié à cette
|
|
69
|
+
* adresse ; une erreur du CMS remonte (la page en cache reste servie).
|
|
70
|
+
*/
|
|
71
|
+
export async function zefRouteProps(path, visitor) {
|
|
72
|
+
const api = getClient();
|
|
73
|
+
const [found, site, entries] = await Promise.all([
|
|
74
|
+
api.document(path, visitor).catch((error) => {
|
|
75
|
+
if (error.status === 404)
|
|
76
|
+
return null;
|
|
77
|
+
throw error;
|
|
78
|
+
}),
|
|
79
|
+
api.site(),
|
|
80
|
+
api.pages(),
|
|
81
|
+
]);
|
|
82
|
+
if (!found)
|
|
83
|
+
return null;
|
|
84
|
+
const { trail, homeOf } = pageTree(entries, site);
|
|
85
|
+
if (found.kind === "page") {
|
|
86
|
+
const { page } = found;
|
|
87
|
+
const layout = await api.layout({ locale: page.locale });
|
|
88
|
+
const entry = entries.find((e) => e.id === page.id);
|
|
89
|
+
return {
|
|
90
|
+
kind: "page",
|
|
91
|
+
page,
|
|
92
|
+
site,
|
|
93
|
+
locale: page.locale,
|
|
94
|
+
title: page.seo.title ?? page.title,
|
|
95
|
+
seo: page.seo,
|
|
96
|
+
alternates: page.alternates,
|
|
97
|
+
url: page.url,
|
|
98
|
+
breadcrumbs: entry ? trail(entry.id, page.locale) : [],
|
|
99
|
+
header: layout.header,
|
|
100
|
+
footer: layout.footer,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const { item, collection, titleField } = found;
|
|
104
|
+
const locale = Object.entries(item.alternates ?? {}).find(([, url]) => url === item.url)?.[0] ??
|
|
105
|
+
site.defaultLocale;
|
|
106
|
+
const layout = await api.layout({ locale });
|
|
107
|
+
const title = String(item[titleField] ?? "");
|
|
108
|
+
const listingPath = item.url?.replace(/\/[^/]+\/?$/, "") ?? "";
|
|
109
|
+
const listing = entries.find((e) => e.urls[locale] === listingPath) ?? homeOf(locale);
|
|
110
|
+
return {
|
|
111
|
+
kind: "item",
|
|
112
|
+
collection,
|
|
113
|
+
item,
|
|
114
|
+
site,
|
|
115
|
+
locale,
|
|
116
|
+
title: item.seo?.title ?? title,
|
|
117
|
+
seo: item.seo,
|
|
118
|
+
alternates: item.alternates ?? {},
|
|
119
|
+
url: item.url ?? path,
|
|
120
|
+
breadcrumbs: [...(listing ? trail(listing.id, locale) : []), { title, url: item.url ?? path }],
|
|
121
|
+
header: layout.header,
|
|
122
|
+
footer: layout.footer,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
46
125
|
let staticPaths;
|
|
47
126
|
/**
|
|
48
127
|
* Toutes les routes du site : pages publiées, puis éléments des collections avec page. Calculé
|
|
@@ -61,22 +140,7 @@ async function buildStaticPaths(config) {
|
|
|
61
140
|
const paths = [];
|
|
62
141
|
const layouts = new Map(await Promise.all(site.locales.map(async (l) => [l, await api.layout({ locale: l })])));
|
|
63
142
|
const entries = await api.pages();
|
|
64
|
-
const
|
|
65
|
-
const homeOf = (locale) => entries.find((e) => e.urls[locale] === "/" || e.urls[locale] === `/${locale}`);
|
|
66
|
-
/** Accueil, puis les pages parentes, puis la page elle-même. */
|
|
67
|
-
const trail = (id, locale) => {
|
|
68
|
-
const out = [];
|
|
69
|
-
for (let e = byId.get(id); e; e = e.parentId ? byId.get(e.parentId) : undefined) {
|
|
70
|
-
const url = e.urls[locale];
|
|
71
|
-
if (url)
|
|
72
|
-
out.unshift({ title: e.titles[locale] || e.titles[site.defaultLocale] || "", url });
|
|
73
|
-
}
|
|
74
|
-
const home = homeOf(locale);
|
|
75
|
-
if (home && out[0]?.url !== home.urls[locale]) {
|
|
76
|
-
out.unshift({ title: home.titles[locale] || site.name, url: home.urls[locale] });
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
};
|
|
143
|
+
const { trail, homeOf } = pageTree(entries, site);
|
|
80
144
|
for (const entry of entries) {
|
|
81
145
|
for (const [locale, url] of Object.entries(entry.urls)) {
|
|
82
146
|
const page = await api.page(url);
|
|
@@ -111,12 +175,7 @@ async function buildStaticPaths(config) {
|
|
|
111
175
|
for (const item of list.items) {
|
|
112
176
|
if (!item.url || item.access?.reserved)
|
|
113
177
|
continue;
|
|
114
|
-
const alternates = {};
|
|
115
|
-
for (const other of byLocale) {
|
|
116
|
-
const url = other.list.items.find((i) => i.id === item.id)?.url;
|
|
117
|
-
if (url)
|
|
118
|
-
alternates[other.locale] = url;
|
|
119
|
-
}
|
|
178
|
+
const alternates = { ...item.alternates };
|
|
120
179
|
// Fil d'Ariane : accueil, page de la liste si elle existe (`/vins`), puis l'élément.
|
|
121
180
|
const listing = entries.find((e) => e.urls[locale] === def.page.path || e.urls[locale] === `/${locale}${def.page.path}`) ?? homeOf(locale);
|
|
122
181
|
const title = String(item[def.titleField] ?? "");
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,20 @@ export * from "./helpers.js";
|
|
|
8
8
|
export * from "./seo.js";
|
|
9
9
|
export * from "./shop.js";
|
|
10
10
|
const MEDIA_URL = "https://medias.agence-zef.fr";
|
|
11
|
+
/** `rendering` de `zef.config.ts` (lu comme la CLI, avec jiti) ; `static` en cas de doute. */
|
|
12
|
+
async function readRendering(file) {
|
|
13
|
+
if (!existsSync(file))
|
|
14
|
+
return "static";
|
|
15
|
+
try {
|
|
16
|
+
const { createJiti } = await import("jiti");
|
|
17
|
+
const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false });
|
|
18
|
+
const config = await jiti.import(file, { default: true });
|
|
19
|
+
return config?.rendering === "cached" ? "cached" : "static";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "static";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
11
25
|
/** Domaines autorisés pour l'optimisation des images distantes (`image.remotePatterns`). */
|
|
12
26
|
function mediaPatterns(urls) {
|
|
13
27
|
return urls.flatMap((raw) => {
|
|
@@ -84,30 +98,64 @@ export default function zef(options = {}) {
|
|
|
84
98
|
]),
|
|
85
99
|
},
|
|
86
100
|
});
|
|
101
|
+
// Performance : le CSS (quelques Ko par page) est écrit dans la page plutôt que chargé à
|
|
102
|
+
// part, ce qui retire une requête bloquante du premier affichage. Un site peut choisir
|
|
103
|
+
// « never » dans son `astro.config`.
|
|
104
|
+
if (config.build.inlineStylesheets === "auto") {
|
|
105
|
+
updateConfig({ build: { inlineStylesheets: "always" } });
|
|
106
|
+
}
|
|
87
107
|
addWatchFile(join(root, ".env"));
|
|
88
108
|
const layout = join(root, options.layout ?? "src/layouts/Page.astro");
|
|
89
109
|
const configFile = join(root, "zef.config.ts");
|
|
110
|
+
// Rendu des pages (`defineConfig({ rendering })`) : construites au build (`static`), ou
|
|
111
|
+
// rendues à la demande et gardées en cache par le CDN, invalidé par une publication
|
|
112
|
+
// (`cached`, demande un adaptateur).
|
|
113
|
+
const rendering = await readRendering(configFile);
|
|
114
|
+
const cached = rendering === "cached" && Boolean(config.adapter || command === "dev");
|
|
115
|
+
if (rendering === "cached" && !cached) {
|
|
116
|
+
logger.warn("rendering « cached » sans adaptateur : pages construites au build");
|
|
117
|
+
}
|
|
118
|
+
if (cached && !config.cache?.provider) {
|
|
119
|
+
// Cache du CDN de l'hébergeur : Vercel (tags, invalidation), sinon en mémoire.
|
|
120
|
+
const vercelProvider = config.adapter?.name === "@astrojs/vercel";
|
|
121
|
+
updateConfig({
|
|
122
|
+
cache: {
|
|
123
|
+
provider: vercelProvider
|
|
124
|
+
? { name: "vercel", entrypoint: "@astrojs/vercel/cache/provider" }
|
|
125
|
+
: { name: "memory", entrypoint: "astro/cache/memory" },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
90
129
|
injectRoute({
|
|
91
130
|
pattern: "/[...path]",
|
|
92
|
-
entrypoint:
|
|
93
|
-
|
|
131
|
+
entrypoint: cached
|
|
132
|
+
? "@agence-zef/astro/routes/page-cached.astro"
|
|
133
|
+
: "@agence-zef/astro/routes/page.astro",
|
|
134
|
+
prerender: !cached,
|
|
94
135
|
});
|
|
95
136
|
injectRoute({
|
|
96
137
|
pattern: "/robots.txt",
|
|
97
138
|
entrypoint: "@agence-zef/astro/routes/robots.ts",
|
|
98
|
-
prerender:
|
|
139
|
+
prerender: !cached,
|
|
99
140
|
});
|
|
100
141
|
// Référencement prêt à l'emploi (§ 5.13) : sitemap et présentation pour les IA.
|
|
101
142
|
injectRoute({
|
|
102
143
|
pattern: "/sitemap.xml",
|
|
103
144
|
entrypoint: "@agence-zef/astro/routes/sitemap.ts",
|
|
104
|
-
prerender:
|
|
145
|
+
prerender: !cached,
|
|
105
146
|
});
|
|
106
147
|
injectRoute({
|
|
107
148
|
pattern: "/llms.txt",
|
|
108
149
|
entrypoint: "@agence-zef/astro/routes/llms.ts",
|
|
109
|
-
prerender:
|
|
150
|
+
prerender: !cached,
|
|
110
151
|
});
|
|
152
|
+
if (cached) {
|
|
153
|
+
injectRoute({
|
|
154
|
+
pattern: "/__zef/revalidate",
|
|
155
|
+
entrypoint: "@agence-zef/astro/routes/revalidate.ts",
|
|
156
|
+
prerender: false,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
111
159
|
// Aperçu de l'éditeur : rendu à la demande, donc avec un adaptateur (Vercel) ou en dev.
|
|
112
160
|
if (command === "dev" || config.adapter) {
|
|
113
161
|
injectRoute({
|
|
@@ -131,7 +179,7 @@ export default function zef(options = {}) {
|
|
|
131
179
|
}
|
|
132
180
|
// Contenu réservé (lot 14e5c) : les adresses que la route statique n'a pas générées
|
|
133
181
|
// (pages et éléments réservés) sont rendues à la demande, avec la session du compte.
|
|
134
|
-
if (options.shop && (command === "dev" || config.adapter)) {
|
|
182
|
+
if (options.shop && !cached && (command === "dev" || config.adapter)) {
|
|
135
183
|
injectRoute({
|
|
136
184
|
pattern: "/[...zefReserved]",
|
|
137
185
|
entrypoint: "@agence-zef/astro/routes/reserved.astro",
|
package/dist/seo.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export type SeoInput = {
|
|
|
46
46
|
type?: string;
|
|
47
47
|
/** Données structurées en plus (article, évènement, FAQ…). */
|
|
48
48
|
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
49
|
+
/** Icône de l'onglet (`/favicon.svg`) ; à défaut, le logo du client dans le CMS. */
|
|
50
|
+
icon?: string | null;
|
|
49
51
|
};
|
|
50
52
|
export type SeoTags = {
|
|
51
53
|
title: string;
|
package/dist/seo.js
CHANGED
|
@@ -73,6 +73,9 @@ export function seoTags(input) {
|
|
|
73
73
|
if (image)
|
|
74
74
|
add("name", "twitter:image", absolute(origin, image.url));
|
|
75
75
|
const links = [{ rel: "canonical", href: canonical }];
|
|
76
|
+
const icon = input.icon ?? site.logo;
|
|
77
|
+
if (icon)
|
|
78
|
+
links.push({ rel: "icon", href: icon });
|
|
76
79
|
const langs = Object.entries(alternates);
|
|
77
80
|
if (langs.length > 1) {
|
|
78
81
|
for (const [lang, href] of langs)
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Ouvre une fenêtre `<Dialog>` par son identifiant. */
|
|
2
|
+
export declare function openDialog(id: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Ferme une fenêtre en laissant jouer son animation de sortie (`[data-closing]`), puis la
|
|
5
|
+
* ferme vraiment ; tout de suite si le visiteur réduit les animations ou sans animation CSS.
|
|
6
|
+
*/
|
|
7
|
+
export declare function closeDialog(target: HTMLDialogElement | string): void;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Comportements des composants d'interface (`<Dialog>`, `<Accordion>`) partagés par leurs
|
|
3
|
+
* scripts : exportés pour qu'un site les déclenche lui-même.
|
|
4
|
+
*/
|
|
5
|
+
const reduced = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6
|
+
/** Ouvre une fenêtre `<Dialog>` par son identifiant. */
|
|
7
|
+
export function openDialog(id) {
|
|
8
|
+
const dialog = document.getElementById(id);
|
|
9
|
+
if (dialog instanceof HTMLDialogElement && !dialog.open)
|
|
10
|
+
dialog.showModal();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ferme une fenêtre en laissant jouer son animation de sortie (`[data-closing]`), puis la
|
|
14
|
+
* ferme vraiment ; tout de suite si le visiteur réduit les animations ou sans animation CSS.
|
|
15
|
+
*/
|
|
16
|
+
export function closeDialog(target) {
|
|
17
|
+
const dialog = typeof target === "string" ? document.getElementById(target) : target;
|
|
18
|
+
if (!(dialog instanceof HTMLDialogElement) || !dialog.open || dialog.hasAttribute("data-closing"))
|
|
19
|
+
return;
|
|
20
|
+
const finish = () => {
|
|
21
|
+
dialog.removeAttribute("data-closing");
|
|
22
|
+
dialog.close();
|
|
23
|
+
};
|
|
24
|
+
if (reduced())
|
|
25
|
+
return finish();
|
|
26
|
+
dialog.setAttribute("data-closing", "");
|
|
27
|
+
const animated = getComputedStyle(dialog).animationName !== "none";
|
|
28
|
+
if (!animated)
|
|
29
|
+
return finish();
|
|
30
|
+
dialog.addEventListener("animationend", finish, { once: true });
|
|
31
|
+
// Filet : si l'animation ne se termine pas (onglet en arrière-plan), on ferme quand même.
|
|
32
|
+
setTimeout(() => dialog.hasAttribute("data-closing") && finish(), 400);
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agence-zef/astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-pilot.0",
|
|
4
4
|
"description": "Intégration Astro du CMS ZEF : routes, rendu des sections, aperçu, composants réutilisables sans style.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,11 +50,27 @@
|
|
|
50
50
|
"./VideoPlayer.astro": "./components/VideoPlayer.astro",
|
|
51
51
|
"./Subscriptions.astro": "./components/Subscriptions.astro",
|
|
52
52
|
"./ReservedNotice.astro": "./components/ReservedNotice.astro",
|
|
53
|
-
"./routes/shop.ts": "./components/routes/shop.ts"
|
|
53
|
+
"./routes/shop.ts": "./components/routes/shop.ts",
|
|
54
|
+
"./Dialog.astro": "./components/Dialog.astro",
|
|
55
|
+
"./Accordion.astro": "./components/Accordion.astro",
|
|
56
|
+
"./AccordionItem.astro": "./components/AccordionItem.astro",
|
|
57
|
+
"./Popover.astro": "./components/Popover.astro",
|
|
58
|
+
"./Field.astro": "./components/Field.astro",
|
|
59
|
+
"./Input.astro": "./components/Input.astro",
|
|
60
|
+
"./Select.astro": "./components/Select.astro",
|
|
61
|
+
"./Checkbox.astro": "./components/Checkbox.astro",
|
|
62
|
+
"./styles.css": "./styles.css",
|
|
63
|
+
"./ui": {
|
|
64
|
+
"types": "./dist/ui.d.ts",
|
|
65
|
+
"import": "./dist/ui.js"
|
|
66
|
+
},
|
|
67
|
+
"./routes/page-cached.astro": "./components/routes/page-cached.astro",
|
|
68
|
+
"./routes/revalidate.ts": "./components/routes/revalidate.ts"
|
|
54
69
|
},
|
|
55
70
|
"files": [
|
|
56
71
|
"dist",
|
|
57
|
-
"components"
|
|
72
|
+
"components",
|
|
73
|
+
"styles.css"
|
|
58
74
|
],
|
|
59
75
|
"publishConfig": {
|
|
60
76
|
"access": "public"
|
|
@@ -67,8 +83,9 @@
|
|
|
67
83
|
"@types/node": "^24.0.0"
|
|
68
84
|
},
|
|
69
85
|
"dependencies": {
|
|
70
|
-
"
|
|
71
|
-
"@agence-zef/
|
|
86
|
+
"jiti": "^2.6.1",
|
|
87
|
+
"@agence-zef/schema": "0.6.0-pilot.0",
|
|
88
|
+
"@agence-zef/sdk": "0.6.0-pilot.0"
|
|
72
89
|
},
|
|
73
90
|
"homepage": "https://agence-zef.fr",
|
|
74
91
|
"scripts": {
|