@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.
@@ -0,0 +1,61 @@
1
+ ---
2
+ /**
3
+ * Accordéon natif (`<details>` / `<summary>`), sans JavaScript ni style imposé : classes
4
+ * `zef-accordion`, `zef-accordion__item`, `__summary`, `__content`, habillées par
5
+ * `@agence-zef/astro/styles.css` (ouverture animée) ou par le CSS du site.
6
+ *
7
+ * Deux usages : `items` (une liste du CMS : `{ summary, html }` ou `{ summary, text }`), ou
8
+ * des `<AccordionItem>` en contenu. `exclusive` : un seul élément ouvert à la fois (attribut
9
+ * `name` natif). `faq` : ajoute les données structurées FAQPage pour Google (avec `items`).
10
+ */
11
+ import RichText from "./RichText.astro";
12
+
13
+ type Item = { summary: string; html?: string | null; text?: string | null; open?: boolean };
14
+
15
+ interface Props {
16
+ items?: Item[];
17
+ exclusive?: boolean;
18
+ faq?: boolean;
19
+ /** Nom du groupe exclusif, à reprendre dans les `<AccordionItem name>` en contenu. */
20
+ name?: string;
21
+ class?: string;
22
+ }
23
+
24
+ const { items, exclusive = false, faq = false, class: className } = Astro.props;
25
+ const name =
26
+ Astro.props.name ?? (exclusive ? `zef-acc-${Math.random().toString(36).slice(2, 8)}` : undefined);
27
+ const strip = (html: string) =>
28
+ html
29
+ .replace(/<[^>]*>/g, " ")
30
+ .replace(/\s+/g, " ")
31
+ .trim();
32
+ const faqData =
33
+ faq && items?.length
34
+ ? {
35
+ "@context": "https://schema.org",
36
+ "@type": "FAQPage",
37
+ mainEntity: items
38
+ .filter((i) => i.summary)
39
+ .map((i) => ({
40
+ "@type": "Question",
41
+ name: i.summary,
42
+ acceptedAnswer: { "@type": "Answer", text: i.text ?? strip(i.html ?? "") },
43
+ })),
44
+ }
45
+ : null;
46
+ ---
47
+
48
+ <div class:list={["zef-accordion", className]}>
49
+ {
50
+ items?.map((item) => (
51
+ <details class="zef-accordion__item" name={name} open={item.open}>
52
+ <summary class="zef-accordion__summary">{item.summary}</summary>
53
+ <div class="zef-accordion__content">
54
+ {item.html ? <RichText html={item.html} /> : item.text ? <p>{item.text}</p> : null}
55
+ </div>
56
+ </details>
57
+ ))
58
+ }
59
+ <slot />
60
+ </div>
61
+ {faqData && <script type="application/ld+json" is:inline set:html={JSON.stringify(faqData).replace(/</g, "\\u003c")} />}
@@ -0,0 +1,23 @@
1
+ ---
2
+ /**
3
+ * Élément d'accordéon (`<details>`), à mettre dans `<Accordion>` ou seul. Titre : `summary`, ou
4
+ * le contenu du slot `summary` (icône, mise en forme). `name` : groupe exclusif (un seul ouvert).
5
+ */
6
+ interface Props {
7
+ summary?: string;
8
+ name?: string;
9
+ open?: boolean;
10
+ class?: string;
11
+ }
12
+
13
+ const { summary, name, open = false, class: className } = Astro.props;
14
+ ---
15
+
16
+ <details class:list={["zef-accordion__item", className]} name={name} open={open}>
17
+ <summary class="zef-accordion__summary">
18
+ {Astro.slots.has("summary") ? <slot name="summary" /> : summary}
19
+ </summary>
20
+ <div class="zef-accordion__content">
21
+ <slot />
22
+ </div>
23
+ </details>
@@ -0,0 +1,20 @@
1
+ ---
2
+ /**
3
+ * Case à cocher (ou bouton radio avec `type="radio"`) avec son libellé, classe `zef-check`.
4
+ * Le libellé : `label`, ou le contenu (lien vers les conditions, mise en forme).
5
+ */
6
+ import type { HTMLAttributes } from "astro/types";
7
+
8
+ type Props = Omit<HTMLAttributes<"input">, "class" | "type"> & {
9
+ label?: string;
10
+ type?: "checkbox" | "radio";
11
+ class?: string;
12
+ };
13
+
14
+ const { label, type = "checkbox", class: className, ...rest } = Astro.props;
15
+ ---
16
+
17
+ <label class:list={["zef-check", className]}>
18
+ <input {...rest} type={type} />
19
+ <span>{Astro.slots.has("default") ? <slot /> : label}</span>
20
+ </label>
@@ -0,0 +1,72 @@
1
+ ---
2
+ /**
3
+ * Fenêtre modale native (`<dialog>`), sans style imposé : classes `zef-dialog`,
4
+ * `zef-dialog__panel`, `zef-dialog__close`, habillées par `@agence-zef/astro/styles.css`
5
+ * (variables `--zef-*`) ou par le CSS du site.
6
+ *
7
+ * Ouverture : n'importe quel élément avec `data-zef-open="<id>"` (bouton, lien). Fermeture :
8
+ * Échap, clic sur le voile, ou un élément `data-zef-close` dans la fenêtre. La page en dessous
9
+ * ne défile plus pendant l'ouverture ; animations d'ouverture et de fermeture via
10
+ * `[open]` / `[data-closing]`, coupées si le visiteur réduit les animations.
11
+ */
12
+ interface Props {
13
+ id: string;
14
+ /** Titre lu par les lecteurs d'écran si la fenêtre n'a pas de titre visible. */
15
+ label?: string;
16
+ /** Bouton de fermeture en haut à droite (défaut : oui) et son libellé accessible. */
17
+ closeButton?: boolean;
18
+ closeLabel?: string;
19
+ class?: string;
20
+ }
21
+
22
+ const { id, label, closeButton = true, closeLabel = "Fermer", class: className } = Astro.props;
23
+ ---
24
+
25
+ <dialog id={id} class:list={["zef-dialog", className]} aria-label={label} data-zef-dialog>
26
+ <div class="zef-dialog__panel">
27
+ {
28
+ closeButton && (
29
+ <button type="button" class="zef-dialog__close" data-zef-close aria-label={closeLabel}>
30
+ <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
31
+ <path d="M18 6 6 18M6 6l12 12" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
32
+ </svg>
33
+ </button>
34
+ )
35
+ }
36
+ <slot />
37
+ </div>
38
+ </dialog>
39
+
40
+ <style is:global>
41
+ /* Comportement, pas habillage : la page ne défile plus derrière une fenêtre ouverte. */
42
+ html:has(dialog[data-zef-dialog][open]) {
43
+ overflow: hidden;
44
+ }
45
+ </style>
46
+
47
+ <script>
48
+ import { closeDialog } from "@agence-zef/astro/ui";
49
+
50
+ document.addEventListener("click", (event) => {
51
+ const target = event.target as Element;
52
+ const opener = target.closest<HTMLElement>("[data-zef-open]");
53
+ if (opener) {
54
+ const dialog = document.getElementById(opener.dataset.zefOpen ?? "");
55
+ if (dialog instanceof HTMLDialogElement) {
56
+ event.preventDefault();
57
+ dialog.showModal();
58
+ }
59
+ return;
60
+ }
61
+ const closer = target.closest("[data-zef-close]");
62
+ const dialog = (closer ?? target).closest("dialog[data-zef-dialog]");
63
+ // Un clic sur le voile atteint le <dialog> lui-même, pas son panneau.
64
+ if (dialog instanceof HTMLDialogElement && (closer || target === dialog)) closeDialog(dialog);
65
+ });
66
+ for (const dialog of document.querySelectorAll<HTMLDialogElement>("dialog[data-zef-dialog]")) {
67
+ dialog.addEventListener("cancel", (event) => {
68
+ event.preventDefault();
69
+ closeDialog(dialog);
70
+ });
71
+ }
72
+ </script>
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * Champ de formulaire codé dans le site : libellé, contrôle (slot), aide et erreur, avec les
4
+ * classes partagées par tous les formulaires de `@agence-zef/astro` (`zef-field`, `zef-label`,
5
+ * `zef-help`, `zef-error`). `for` : identifiant du contrôle.
6
+ */
7
+ interface Props {
8
+ label: string;
9
+ for?: string;
10
+ help?: string;
11
+ error?: string;
12
+ required?: boolean;
13
+ class?: string;
14
+ }
15
+
16
+ const { label, for: htmlFor, help, error, required = false, class: className } = Astro.props;
17
+ ---
18
+
19
+ <div class:list={["zef-field", className]} data-invalid={error ? "" : undefined}>
20
+ <label class="zef-label" for={htmlFor}>
21
+ {label}
22
+ {required && <span class="zef-required" aria-hidden="true"> *</span>}
23
+ </label>
24
+ <slot />
25
+ {help && <p class="zef-help">{help}</p>}
26
+ <p class="zef-error" role="alert" hidden={!error}>{error}</p>
27
+ </div>
@@ -10,18 +10,30 @@ import type { Media } from "@agence-zef/schema";
10
10
  interface Props {
11
11
  media: Media | null;
12
12
  class?: string;
13
- /** `eager` pour l'image principale de la page. */
13
+ /** `eager` pour une image visible dès l'ouverture de la page. */
14
14
  loading?: "lazy" | "eager";
15
+ /**
16
+ * Image principale de la page (LCP, en général celle du hero) : chargée tout de suite et en
17
+ * priorité (`fetchpriority="high"`). Une seule par page.
18
+ */
19
+ priority?: boolean;
15
20
  /** Largeur affichée (`(min-width: 48rem) 33vw, 100vw`) : active le `srcset`. */
16
21
  sizes?: string;
17
22
  }
18
23
 
19
- const { media, class: className, loading = "lazy", sizes } = Astro.props;
24
+ const { media, class: className, priority = false, sizes } = Astro.props;
25
+ const loading = priority ? "eager" : (Astro.props.loading ?? "lazy");
26
+ const fetchpriority = priority ? "high" : undefined;
27
+ // Proportions réservées même si le CSS du site force `width: auto` (pas de décalage au chargement).
28
+ const style =
29
+ media?.width && media.height ? `aspect-ratio: ${media.width} / ${media.height}` : undefined;
20
30
  const OPTIMIZED = ["image/jpeg", "image/png", "image/webp", "image/avif"];
21
31
  const optimize = Boolean(
22
32
  media?.width && media.height && media.mime && OPTIMIZED.includes(media.mime),
23
33
  );
24
- const STEPS = [480, 800, 1200, 1600, 2400];
34
+ // Largeurs générées pour le `srcset` : assez serrées pour qu'un mobile ne télécharge pas une
35
+ // image deux fois trop grande.
36
+ const STEPS = [320, 480, 640, 800, 960, 1200, 1600, 2000, 2400];
25
37
  const widths =
26
38
  sizes && media?.width
27
39
  ? [...STEPS.filter((w) => w < (media.width as number)), media.width]
@@ -37,10 +49,12 @@ const widths =
37
49
  width={media.width as number}
38
50
  height={media.height as number}
39
51
  loading={loading}
40
- decoding="async"
52
+ fetchpriority={fetchpriority}
53
+ decoding={priority ? "sync" : "async"}
41
54
  sizes={sizes}
42
55
  widths={widths}
43
56
  class={className}
57
+ style={style}
44
58
  />
45
59
  ) : (
46
60
  <img
@@ -49,9 +63,11 @@ const widths =
49
63
  width={media.width}
50
64
  height={media.height}
51
65
  loading={loading}
52
- decoding="async"
66
+ fetchpriority={fetchpriority}
67
+ decoding={priority ? "sync" : "async"}
53
68
  sizes={sizes}
54
69
  class={className}
70
+ style={style}
55
71
  />
56
72
  ))
57
73
  }
@@ -0,0 +1,49 @@
1
+ ---
2
+ /**
3
+ * Zone de saisie (`<input>` ou `<textarea>` avec `multiline`), classe `zef-input`. Avec `label`,
4
+ * elle est rendue dans un `<Field>` (libellé, aide, erreur). Les autres attributs sont passés
5
+ * tels quels (`name`, `type`, `required`, `placeholder`, `autocomplete`…).
6
+ */
7
+ import type { HTMLAttributes } from "astro/types";
8
+ import Field from "./Field.astro";
9
+
10
+ type Props = Omit<HTMLAttributes<"input">, "class"> & {
11
+ label?: string;
12
+ help?: string;
13
+ error?: string;
14
+ multiline?: boolean;
15
+ rows?: number;
16
+ class?: string;
17
+ };
18
+
19
+ const {
20
+ label,
21
+ help,
22
+ error,
23
+ multiline = false,
24
+ rows = 5,
25
+ class: className,
26
+ id: givenId,
27
+ ...rest
28
+ } = Astro.props;
29
+ const id =
30
+ givenId ?? (label ? `zef-${rest.name ?? Math.random().toString(36).slice(2, 8)}` : undefined);
31
+ const attrs = {
32
+ ...rest,
33
+ id,
34
+ class: `zef-input${className ? ` ${className}` : ""}`,
35
+ "aria-invalid": error ? "true" : undefined,
36
+ };
37
+ ---
38
+
39
+ {
40
+ label ? (
41
+ <Field label={label} for={id} help={help} error={error} required={Boolean(rest.required)}>
42
+ {multiline ? <textarea {...(attrs as HTMLAttributes<"textarea">)} rows={rows}>{rest.value ?? ""}</textarea> : <input {...attrs} />}
43
+ </Field>
44
+ ) : multiline ? (
45
+ <textarea {...(attrs as HTMLAttributes<"textarea">)} rows={rows}>{rest.value ?? ""}</textarea>
46
+ ) : (
47
+ <input {...attrs} />
48
+ )
49
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ /**
3
+ * Popover natif (attribut `popover`), sans JavaScript ni style imposé : ouvert par un bouton
4
+ * `<button popovertarget="<id>">`, fermé au clic en dehors et par Échap. Placé sous son bouton
5
+ * par le positionnement d'ancre CSS (`styles.css`), au centre de l'écran en repli.
6
+ * Classe `zef-popover` ; `manual` : ne se ferme pas au clic en dehors.
7
+ */
8
+ interface Props {
9
+ id: string;
10
+ manual?: boolean;
11
+ /** Position par rapport au bouton : en dessous (défaut), au-dessus, à droite, à gauche. */
12
+ placement?: "bottom" | "top" | "right" | "left";
13
+ class?: string;
14
+ }
15
+
16
+ const { id, manual = false, placement = "bottom", class: className } = Astro.props;
17
+ ---
18
+
19
+ <div id={id} popover={manual ? "manual" : "auto"} class:list={["zef-popover", className]} data-placement={placement}>
20
+ <slot />
21
+ </div>
@@ -0,0 +1,55 @@
1
+ ---
2
+ /**
3
+ * Liste déroulante native, classe `zef-select`. Options : `options` (`{ value: label }` ou
4
+ * `[{ value, label }]`) ou `<option>` en contenu. Avec `label`, rendue dans un `<Field>`.
5
+ */
6
+ import type { HTMLAttributes } from "astro/types";
7
+ import Field from "./Field.astro";
8
+
9
+ type Option = { value: string; label: string };
10
+ type Props = Omit<HTMLAttributes<"select">, "class"> & {
11
+ label?: string;
12
+ help?: string;
13
+ error?: string;
14
+ options?: Record<string, string> | Option[];
15
+ /** Première option vide (« Choisir… »). */
16
+ placeholder?: string;
17
+ value?: string;
18
+ class?: string;
19
+ };
20
+
21
+ const {
22
+ label,
23
+ help,
24
+ error,
25
+ options,
26
+ placeholder,
27
+ value,
28
+ class: className,
29
+ id: givenId,
30
+ ...rest
31
+ } = Astro.props;
32
+ const id =
33
+ givenId ?? (label ? `zef-${rest.name ?? Math.random().toString(36).slice(2, 8)}` : undefined);
34
+ const list: Option[] = Array.isArray(options)
35
+ ? options
36
+ : Object.entries(options ?? {}).map(([v, l]) => ({ value: v, label: l }));
37
+ ---
38
+
39
+ {
40
+ label ? (
41
+ <Field label={label} for={id} help={help} error={error} required={Boolean(rest.required)}>
42
+ <select {...rest} id={id} class:list={["zef-select", className]} aria-invalid={error ? "true" : undefined}>
43
+ {placeholder !== undefined && <option value="">{placeholder}</option>}
44
+ {list.map((o) => <option value={o.value} selected={o.value === value}>{o.label}</option>)}
45
+ <slot />
46
+ </select>
47
+ </Field>
48
+ ) : (
49
+ <select {...rest} id={id} class:list={["zef-select", className]} aria-invalid={error ? "true" : undefined}>
50
+ {placeholder !== undefined && <option value="">{placeholder}</option>}
51
+ {list.map((o) => <option value={o.value} selected={o.value === value}>{o.label}</option>)}
52
+ <slot />
53
+ </select>
54
+ )
55
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * Rendu à la demande (`rendering: "cached"`) : règles de cache des pages du CMS. Le CDN garde
3
+ * une page un an ; une publication l'invalide par le tag `zef` (le CDN sert l'ancienne version
4
+ * le temps d'en rendre une nouvelle, `swr`).
5
+ */
6
+
7
+ /** Tag posé sur toutes les réponses du site rendues depuis le CMS. */
8
+ export const CACHE_TAG = "zef";
9
+
10
+ export const PAGE_CACHE = { maxAge: 60 * 60 * 24 * 365, swr: 60 * 60 * 24 * 7 };
@@ -1,6 +1,7 @@
1
1
  import config from "virtual:zef/config";
2
2
  import { getClient, llmsTxt, siteOrigin, zefSiteIndex } from "@agence-zef/astro";
3
3
  import type { APIRoute } from "astro";
4
+ import { CACHE_TAG, PAGE_CACHE } from "./cache";
4
5
 
5
6
  /*
6
7
  * `llms.txt` injecté par `@agence-zef/astro` (§ 5.13, format llmstxt.org) : le site présenté aux
@@ -9,7 +10,9 @@ import type { APIRoute } from "astro";
9
10
 
10
11
  export const prerender = true;
11
12
 
12
- export const GET: APIRoute = async ({ site: astroSite }) => {
13
+ export const GET: APIRoute = async ({ site: astroSite, cache }) => {
14
+ // Rendu à la demande (`rendering: "cached"`) : gardé en cache, invalidé par une publication.
15
+ cache.set({ ...PAGE_CACHE, tags: [CACHE_TAG] });
13
16
  const site = await getClient().site();
14
17
  const collections = (config.collections ?? [])
15
18
  .filter((c) => c.page)
@@ -0,0 +1,69 @@
1
+ ---
2
+ import Layout from "virtual:zef/layout";
3
+ /**
4
+ * Rendu à la demande (`defineConfig({ rendering: "cached" })`) : chaque page du CMS ou élément
5
+ * de collection est rendu à la première visite puis gardé en cache par le CDN (route caching
6
+ * d'Astro), avec le tag `zef`. Une publication invalide ce tag (`/__zef/revalidate`) : les pages
7
+ * sont rafraîchies à la visite suivante, sans build. Contenu réservé : jamais en cache.
8
+ * Adresse inconnue : redirection du CMS (C-17) si elle existe, sinon 404.
9
+ */
10
+ import { getClient, type RouteProps, zefRouteProps } from "@agence-zef/astro";
11
+ import { visitorOf } from "@agence-zef/astro/accounts";
12
+ import ReservedNotice from "../ReservedNotice.astro";
13
+ import ZefSections from "../ZefSections.astro";
14
+ import { CACHE_TAG, PAGE_CACHE } from "./cache";
15
+
16
+ const path = decodeURI(Astro.url.pathname);
17
+ let props: RouteProps | null;
18
+ try {
19
+ props = await zefRouteProps(path, visitorOf(Astro));
20
+ } catch {
21
+ // CMS injoignable : 503 sans cache, le CDN continue de servir la version qu'il a.
22
+ Astro.cache.set(false);
23
+ return new Response("Service momentanément indisponible.", { status: 503 });
24
+ }
25
+
26
+ if (!props) {
27
+ const redirect = (
28
+ await getClient()
29
+ .redirects()
30
+ .catch(() => [])
31
+ ).find((r) => r.from === path || r.from === path.replace(/\/$/, ""));
32
+ Astro.cache.set({ maxAge: 60, tags: [CACHE_TAG] });
33
+ if (redirect) return Astro.redirect(redirect.to, redirect.status === 302 ? 302 : 301);
34
+ return new Response(null, { status: 404 });
35
+ }
36
+
37
+ const doc = props.kind === "page" ? props.page : props.item;
38
+ const reserved = doc.access?.reserved === true;
39
+ // Le contenu réservé dépend du visiteur : jamais en cache.
40
+ if (reserved) Astro.cache.set(false);
41
+ else Astro.cache.set({ ...PAGE_CACHE, tags: [CACHE_TAG] });
42
+
43
+ // Site en développement (§ 5.11) : aucune page n'est indexable.
44
+ const seo =
45
+ props.site.indexable === false || reserved ? { ...props.seo, noindex: true } : props.seo;
46
+ ---
47
+
48
+ <Layout
49
+ site={props.site}
50
+ locale={props.locale}
51
+ title={props.title}
52
+ seo={seo}
53
+ alternates={props.alternates}
54
+ url={props.url}
55
+ breadcrumbs={props.breadcrumbs}
56
+ item={props.kind === "item" ? props.item : undefined}
57
+ header={props.header}
58
+ footer={props.footer}
59
+ >
60
+ {
61
+ reserved && !doc.access?.granted ? (
62
+ <ReservedNotice title={props.title} products={doc.access?.products ?? []} />
63
+ ) : props.kind === "page" ? (
64
+ <ZefSections sections={props.page.sections} />
65
+ ) : (
66
+ <ZefSections sections={props.item.sections ?? []} item={props.item} />
67
+ )
68
+ }
69
+ </Layout>
@@ -0,0 +1,33 @@
1
+ import { createClient } from "@agence-zef/sdk";
2
+ import type { APIRoute } from "astro";
3
+ import { CACHE_TAG } from "./cache";
4
+
5
+ /*
6
+ * Invalidation du cache après une publication (`rendering: "cached"`) : le CMS appelle
7
+ * `POST /__zef/revalidate` avec un jeton d'aperçu de ce site (`Authorization: Bearer …`). Le site
8
+ * vérifie le jeton auprès de son CMS (`ZEF_URL` désigne le client : un jeton accepté là est
9
+ * forcément le sien), puis invalide le tag `zef` : toutes les pages sont rafraîchies à leur
10
+ * prochaine visite.
11
+ */
12
+
13
+ export const prerender = false;
14
+
15
+ export const POST: APIRoute = async ({ request, cache }) => {
16
+ const token = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
17
+ const url = process.env.ZEF_URL;
18
+ if (!token || !url) return json(401, { ok: false, error: "Jeton manquant." });
19
+ const valid = await createClient({ url, apiKey: token })
20
+ .site()
21
+ .then(() => true)
22
+ .catch(() => false);
23
+ if (!valid) return json(403, { ok: false, error: "Jeton refusé." });
24
+ if (!cache.enabled) return json(200, { ok: true, invalidated: false });
25
+ await cache.invalidate({ tags: [CACHE_TAG] });
26
+ return json(200, { ok: true, invalidated: true });
27
+ };
28
+
29
+ const json = (status: number, body: unknown) =>
30
+ new Response(JSON.stringify(body), {
31
+ status,
32
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
33
+ });
@@ -1,5 +1,6 @@
1
1
  import { getClient, robotsTxt, siteOrigin } from "@agence-zef/astro";
2
2
  import type { APIRoute } from "astro";
3
+ import { CACHE_TAG, PAGE_CACHE } from "./cache";
3
4
 
4
5
  /*
5
6
  * `robots.txt` injecté par `@agence-zef/astro` (§ 5.11, § 5.13) : tout est bloqué tant que le site
@@ -9,7 +10,9 @@ import type { APIRoute } from "astro";
9
10
 
10
11
  export const prerender = true;
11
12
 
12
- export const GET: APIRoute = async ({ site: astroSite }) => {
13
+ export const GET: APIRoute = async ({ site: astroSite, cache }) => {
14
+ // Rendu à la demande (`rendering: "cached"`) : gardé en cache, invalidé par une publication.
15
+ cache.set({ ...PAGE_CACHE, tags: [CACHE_TAG] });
13
16
  const site = await getClient().site();
14
17
  const body = robotsTxt(site, siteOrigin(site, astroSite?.origin));
15
18
  return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
@@ -1,6 +1,7 @@
1
1
  import config from "virtual:zef/config";
2
2
  import { getClient, sitemapXml, siteOrigin, zefSiteIndex } from "@agence-zef/astro";
3
3
  import type { APIRoute } from "astro";
4
+ import { CACHE_TAG, PAGE_CACHE } from "./cache";
4
5
 
5
6
  /*
6
7
  * `sitemap.xml` injecté par `@agence-zef/astro` (§ 5.13) : pages et éléments publiés et
@@ -9,7 +10,9 @@ import type { APIRoute } from "astro";
9
10
 
10
11
  export const prerender = true;
11
12
 
12
- export const GET: APIRoute = async ({ site: astroSite }) => {
13
+ export const GET: APIRoute = async ({ site: astroSite, cache }) => {
14
+ // Rendu à la demande (`rendering: "cached"`) : gardé en cache, invalidé par une publication.
15
+ cache.set({ ...PAGE_CACHE, tags: [CACHE_TAG] });
13
16
  const site = await getClient().site();
14
17
  const body = sitemapXml(await zefSiteIndex(config), siteOrigin(site, astroSite?.origin));
15
18
  return new Response(body, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
package/dist/helpers.d.ts CHANGED
@@ -56,6 +56,12 @@ export declare function zefLayoutProps(opts: {
56
56
  locale?: string;
57
57
  noindex?: boolean;
58
58
  }): Promise<LayoutProps>;
59
+ /**
60
+ * Rendu à la demande (`rendering: "cached"`) : les props du gabarit pour une adresse, page ou
61
+ * élément de collection, sans parcourir tout le site. `null` si rien n'est publié à cette
62
+ * adresse ; une erreur du CMS remonte (la page en cache reste servie).
63
+ */
64
+ export declare function zefRouteProps(path: string, visitor?: Parameters<ZefClient["document"]>[1]): Promise<RouteProps | null>;
59
65
  /**
60
66
  * Toutes les routes du site : pages publiées, puis éléments des collections avec page. Calculé
61
67
  * une fois par build : la route des pages, le sitemap et `llms.txt` partagent le résultat.