@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/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 byId = new Map(entries.map((e) => [e.id, e]));
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: "@agence-zef/astro/routes/page.astro",
93
- prerender: true,
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: true,
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: true,
145
+ prerender: !cached,
105
146
  });
106
147
  injectRoute({
107
148
  pattern: "/llms.txt",
108
149
  entrypoint: "@agence-zef/astro/routes/llms.ts",
109
- prerender: true,
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.4.1",
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
- "@agence-zef/schema": "0.4.0",
71
- "@agence-zef/sdk": "0.4.1"
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": {