@agence-zef/astro 0.0.0-stage → 0.2.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,210 @@
1
+ ---
2
+ /**
3
+ * Popups du site (C-11, C-12), sans style imposé : lues à l'exécution au CMS (activer ou
4
+ * modifier une popup ne demande pas de reconstruire le site). Ciblage (pages, langues,
5
+ * appareils), déclenchement, fréquence et priorité appliqués dans le navigateur ; une
6
+ * seule popup automatique à la fois. Les boutons `data-zef-popup` (destination « Popup »)
7
+ * ouvrent la leur. Rendu : `<dialog class="zef-popup" data-layout="…">` à habiller en CSS.
8
+ * À placer une fois dans le gabarit, avant `</body>`.
9
+ */
10
+ const cms = (process.env.ZEF_URL ?? "").replace(/\/$/, "");
11
+ const locale = Astro.currentLocale ?? "";
12
+ ---
13
+
14
+ {cms && <div data-zef-popups={`${cms}/api/public/popups`} data-locale={locale} hidden />}
15
+
16
+ <script>
17
+ type Button = {
18
+ label: string;
19
+ href: string | null;
20
+ target: string | null;
21
+ download: boolean;
22
+ popup: string | null;
23
+ variant: string | null;
24
+ };
25
+ type Popup = {
26
+ id: string;
27
+ layout: string;
28
+ trigger: { type: "load" | "delay" | "scroll" | "exit" | "click"; delay?: number; scroll?: number };
29
+ frequency: { type: "page" | "session" | "days" | "once"; days?: number };
30
+ devices: "all" | "desktop" | "mobile";
31
+ pages: { mode: "all" | "include" | "exclude"; urls: string[] };
32
+ locales: string[];
33
+ content: Record<
34
+ string,
35
+ { titre: string; texte: string; image: { url: string; alt: string } | null; boutons: Button[] }
36
+ >;
37
+ };
38
+
39
+ const KEY = "zef-popups";
40
+ const store = {
41
+ read(storage: Storage): Record<string, number> {
42
+ try {
43
+ return JSON.parse(storage.getItem(KEY) ?? "{}");
44
+ } catch {
45
+ return {};
46
+ }
47
+ },
48
+ write(storage: Storage, id: string) {
49
+ try {
50
+ storage.setItem(KEY, JSON.stringify({ ...store.read(storage), [id]: Date.now() }));
51
+ } catch {
52
+ // Navigation privée : la fréquence ne tient que pour la page.
53
+ }
54
+ },
55
+ };
56
+
57
+ /** La fréquence autorise-t-elle encore la popup ? */
58
+ function allowed(p: Popup) {
59
+ const f = p.frequency;
60
+ if (f.type === "page") return true;
61
+ if (f.type === "session") return !store.read(sessionStorage)[p.id];
62
+ const last = store.read(localStorage)[p.id];
63
+ if (!last) return true;
64
+ if (f.type === "once") return false;
65
+ return Date.now() - last > (f.days ?? 1) * 86_400_000;
66
+ }
67
+
68
+ function targeted(p: Popup, locale: string, path: string) {
69
+ const mobile = matchMedia("(max-width: 767px)").matches;
70
+ if (p.devices === "mobile" && !mobile) return false;
71
+ if (p.devices === "desktop" && mobile) return false;
72
+ if (p.locales.length && !p.locales.includes(locale)) return false;
73
+ const clean = (u: string) => u.replace(/\/$/, "") || "/";
74
+ const here = p.pages.urls.map(clean).includes(clean(path));
75
+ if (p.pages.mode === "include") return here;
76
+ if (p.pages.mode === "exclude") return !here;
77
+ return true;
78
+ }
79
+
80
+ function render(p: Popup, locale: string, openPopup: (id: string) => void) {
81
+ const c = p.content[locale] ?? Object.values(p.content)[0];
82
+ const dialog = document.createElement("dialog");
83
+ dialog.className = "zef-popup";
84
+ dialog.dataset.layout = p.layout;
85
+ dialog.dataset.popup = p.id;
86
+ if (c?.titre) dialog.setAttribute("aria-label", c.titre);
87
+ const close = document.createElement("button");
88
+ close.type = "button";
89
+ close.className = "zef-popup__close";
90
+ close.setAttribute("aria-label", locale === "en" ? "Close" : "Fermer");
91
+ close.textContent = "×";
92
+ close.addEventListener("click", () => dialog.close());
93
+ dialog.append(close);
94
+ if (c?.image) {
95
+ const img = document.createElement("img");
96
+ img.className = "zef-popup__image";
97
+ img.src = c.image.url;
98
+ img.alt = c.image.alt;
99
+ dialog.append(img);
100
+ }
101
+ const body = document.createElement("div");
102
+ body.className = "zef-popup__body";
103
+ if (c?.titre) {
104
+ const h = document.createElement("h2");
105
+ h.className = "zef-popup__title";
106
+ h.textContent = c.titre;
107
+ body.append(h);
108
+ }
109
+ if (c?.texte) {
110
+ const t = document.createElement("div");
111
+ t.className = "zef-popup__text";
112
+ // Texte riche nettoyé par l'API du CMS (mêmes balises que les sections).
113
+ t.innerHTML = c.texte;
114
+ body.append(t);
115
+ }
116
+ if (c?.boutons.length) {
117
+ const actions = document.createElement("div");
118
+ actions.className = "zef-popup__actions";
119
+ for (const b of c.boutons) {
120
+ const el = b.href ? document.createElement("a") : document.createElement("button");
121
+ el.textContent = b.label;
122
+ el.className = "zef-popup__button";
123
+ if (b.variant) el.dataset.variant = b.variant;
124
+ if (el instanceof HTMLAnchorElement && b.href) {
125
+ el.href = b.href;
126
+ if (b.target) {
127
+ el.target = b.target;
128
+ el.rel = "noopener";
129
+ }
130
+ if (b.download) el.download = "";
131
+ } else {
132
+ (el as HTMLButtonElement).type = "button";
133
+ // Sans destination (« Plus tard ») : ferme ; vers une popup : l'ouvre.
134
+ el.addEventListener("click", () => {
135
+ dialog.close();
136
+ if (b.popup) openPopup(b.popup);
137
+ });
138
+ }
139
+ actions.append(el);
140
+ }
141
+ body.append(actions);
142
+ }
143
+ dialog.append(body);
144
+ dialog.addEventListener("click", (e) => {
145
+ if (e.target === dialog) dialog.close();
146
+ });
147
+ dialog.addEventListener("close", () => dialog.remove());
148
+ return dialog;
149
+ }
150
+
151
+ async function start(root: HTMLElement) {
152
+ if (new URLSearchParams(location.search).get("zef-popups") === "off") return;
153
+ const locale = root.dataset.locale || document.documentElement.lang || "fr";
154
+ let popups: Popup[] = [];
155
+ try {
156
+ const res = await fetch(root.dataset.zefPopups ?? "");
157
+ popups = (await res.json()).popups ?? [];
158
+ } catch {
159
+ return;
160
+ }
161
+ const byId = new Map(popups.map((p) => [p.id, p]));
162
+ const open = (id: string) => {
163
+ const p = byId.get(id);
164
+ if (!p || document.querySelector(`dialog.zef-popup[data-popup="${p.id}"]`)) return;
165
+ const dialog = render(p, locale, open);
166
+ document.body.append(dialog);
167
+ if (p.layout === "centree" || p.layout === "plein-ecran") dialog.showModal();
168
+ else dialog.show();
169
+ store.write(sessionStorage, p.id);
170
+ store.write(localStorage, p.id);
171
+ };
172
+
173
+ // Boutons « Popup » du site (et ceux ajoutés plus tard).
174
+ document.addEventListener("click", (e) => {
175
+ const trigger = (e.target as Element | null)?.closest<HTMLElement>("[data-zef-popup]");
176
+ if (!trigger?.dataset.zefPopup) return;
177
+ e.preventDefault();
178
+ open(trigger.dataset.zefPopup);
179
+ });
180
+
181
+ // Une seule popup automatique : la première (priorité) qui vise cette page.
182
+ const auto = popups.find(
183
+ (p) => p.trigger.type !== "click" && targeted(p, locale, location.pathname) && allowed(p),
184
+ );
185
+ if (!auto) return;
186
+ const t = auto.trigger;
187
+ if (t.type === "load") open(auto.id);
188
+ else if (t.type === "delay") setTimeout(() => open(auto.id), (t.delay ?? 0) * 1000);
189
+ else if (t.type === "scroll") {
190
+ const onScroll = () => {
191
+ const max = document.documentElement.scrollHeight - innerHeight;
192
+ if (max <= 0 || (scrollY / max) * 100 >= (t.scroll ?? 50)) {
193
+ removeEventListener("scroll", onScroll);
194
+ open(auto.id);
195
+ }
196
+ };
197
+ addEventListener("scroll", onScroll, { passive: true });
198
+ } else if (t.type === "exit" && matchMedia("(pointer: fine)").matches) {
199
+ const onLeave = (e: MouseEvent) => {
200
+ if (e.clientY > 0 || e.relatedTarget) return;
201
+ document.removeEventListener("mouseout", onLeave);
202
+ open(auto.id);
203
+ };
204
+ document.addEventListener("mouseout", onLeave);
205
+ }
206
+ }
207
+
208
+ const root = document.querySelector<HTMLElement>("[data-zef-popups]");
209
+ if (root) start(root);
210
+ </script>
@@ -0,0 +1,11 @@
1
+ ---
2
+ /** Texte riche saisi dans le CMS (paragraphes, titres autorisés, listes, liens). */
3
+ interface Props {
4
+ html: string;
5
+ class?: string;
6
+ }
7
+
8
+ const { html, class: className } = Astro.props;
9
+ ---
10
+
11
+ {html && <div class={className} set:html={html} />}
@@ -0,0 +1,38 @@
1
+ ---
2
+ /**
3
+ * Vidéo de la médiathèque (champ `f.media({ accept: "video" })`), sans style imposé. Par défaut
4
+ * avec les contrôles ; `ambient` : lecture automatique, en boucle et sans le son (fond de section).
5
+ * `poster` : image affichée avant la lecture (une `Media` de la médiathèque).
6
+ */
7
+ import type { Media } from "@agence-zef/schema";
8
+
9
+ interface Props {
10
+ media: Media | null;
11
+ poster?: Media | null;
12
+ class?: string;
13
+ /** Fond animé : lecture automatique, en boucle, sans le son ni contrôles. */
14
+ ambient?: boolean;
15
+ }
16
+
17
+ const { media, poster, class: className, ambient = false } = Astro.props;
18
+ const isVideo = Boolean(media?.mime?.startsWith("video/"));
19
+ ---
20
+
21
+ {
22
+ media && isVideo && (
23
+ <video
24
+ src={media.url}
25
+ poster={poster?.url}
26
+ width={media.width}
27
+ height={media.height}
28
+ class={className}
29
+ aria-label={media.alt || undefined}
30
+ preload="metadata"
31
+ playsinline
32
+ controls={!ambient}
33
+ autoplay={ambient}
34
+ loop={ambient}
35
+ muted={ambient}
36
+ />
37
+ )
38
+ }
@@ -0,0 +1,68 @@
1
+ ---
2
+ import components from "virtual:zef/sections";
3
+ /**
4
+ * Rend les sections d'une page du CMS, dans l'ordre, avec le composant
5
+ * `src/sections/<type>/index.astro` du site. Props : champs de la section, `id`, `variant`, `anchor`,
6
+ * et `item` sur la page d'un élément de collection.
7
+ * Le bloc HTML de l'agence (`zef-html`) est rendu ici, sans composant ; le CSS personnalisé
8
+ * d'une section est ajouté juste avant elle, `&` désignant l'élément racine de la section.
9
+ */
10
+ import { HTML_BLOCK } from "@agence-zef/schema";
11
+ import type { ApiItem, ApiSection } from "@agence-zef/sdk";
12
+
13
+ interface Props {
14
+ sections: ApiSection[];
15
+ /** Page d'un élément de collection : l'élément, passé à chaque section (`item`). */
16
+ item?: ApiItem;
17
+ /** Aperçu de l'éditeur : chaque section est repérée pour l'overlay (`data-zef-section`). */
18
+ preview?: boolean;
19
+ }
20
+
21
+ const { sections, item, preview = false } = Astro.props;
22
+ const missing = sections
23
+ .filter((s) => s.type !== HTML_BLOCK && !components[s.type])
24
+ .map((s) => s.type);
25
+ if (missing.length) {
26
+ console.warn(
27
+ `[@agence-zef/astro] section(s) sans composant : ${[...new Set(missing)].join(", ")}`,
28
+ );
29
+ }
30
+
31
+ /** Le CSS ne peut pas refermer sa balise `<style>`. */
32
+ const safeCss = (css: string) => css.replace(/<\/?style/gi, "");
33
+ const scoped = (css: string, id: string) =>
34
+ safeCss(css).replaceAll("&", `:where([data-zef-css="${id}"]) > *`);
35
+ const text = (value: unknown) => (typeof value === "string" ? value : "");
36
+ ---
37
+
38
+ {
39
+ sections.map((s) => {
40
+ const Section = components[s.type];
41
+ if (s.type !== HTML_BLOCK && !Section) return null;
42
+ const rendered =
43
+ s.type === HTML_BLOCK ? (
44
+ <Fragment>
45
+ <section id={s.anchor ?? undefined} data-zef-html={s.id}>
46
+ <Fragment set:html={text(s.data.html)} />
47
+ </section>
48
+ {text(s.data.css) && <style set:html={safeCss(text(s.data.css))} />}
49
+ {text(s.data.js) && <script is:inline set:html={text(s.data.js).replace(/<\/script/gi, "<\\/script")} />}
50
+ </Fragment>
51
+ ) : (
52
+ <Section {...s.data} id={s.id} variant={s.variant} anchor={s.anchor} item={item} />
53
+ );
54
+ return preview || s.css ? (
55
+ <div
56
+ data-zef-section={preview ? s.id : undefined}
57
+ data-zef-type={preview ? s.type : undefined}
58
+ data-zef-css={s.css ? s.id : undefined}
59
+ style="display: contents"
60
+ >
61
+ {s.css && <style set:html={scoped(s.css, s.id)} />}
62
+ {rendered}
63
+ </div>
64
+ ) : (
65
+ rendered
66
+ );
67
+ })
68
+ }
@@ -0,0 +1,24 @@
1
+ ---
2
+ import config from "virtual:zef/config";
3
+ import Layout from "virtual:zef/layout";
4
+ /**
5
+ * Route attrape-tout injectée par `@agence-zef/astro` : pages du CMS et éléments des collections
6
+ * avec page, chacun avec les sections choisies dans l'éditeur, dans le gabarit du site.
7
+ * Sur un élément, chaque section reçoit aussi l'élément (`item`) pour afficher ses champs.
8
+ */
9
+ import { type RouteProps, zefStaticPaths } from "@agence-zef/astro";
10
+ import ZefSections from "../ZefSections.astro";
11
+
12
+ export async function getStaticPaths() {
13
+ return zefStaticPaths(config);
14
+ }
15
+
16
+ const props = Astro.props as RouteProps;
17
+ const { site, locale, title, alternates, header, footer } = props;
18
+ // Site en développement (§ 5.11) : aucune page n'est indexable.
19
+ const seo = site.indexable === false ? { ...props.seo, noindex: true } : props.seo;
20
+ ---
21
+
22
+ <Layout site={site} locale={locale} title={title} seo={seo} alternates={alternates} header={header} footer={footer}>
23
+ {props.kind === "page" ? <ZefSections sections={props.page.sections} /> : <ZefSections sections={props.item.sections ?? []} item={props.item} />}
24
+ </Layout>
@@ -0,0 +1,37 @@
1
+ ---
2
+ /**
3
+ * Re-rendu d'une seule section pour l'aperçu (changement de variante, élément ajouté…) :
4
+ * l'overlay envoie la section du brouillon, l'API la résout, le composant du site la rend.
5
+ * Sur la page d'un élément de collection, la section reçoit aussi l'élément (`item`).
6
+ */
7
+ import { type ApiItem, createClient } from "@agence-zef/sdk";
8
+ import ZefSections from "../ZefSections.astro";
9
+
10
+ export const prerender = false;
11
+
12
+ Astro.response.headers.set("Cache-Control", "no-store");
13
+
14
+ let section = null;
15
+ let item: ApiItem | undefined;
16
+ if (Astro.request.method === "POST") {
17
+ const body = (await Astro.request.json().catch(() => null)) as {
18
+ token?: string;
19
+ locale?: string;
20
+ doc?: string;
21
+ section?: { id: string; type: string };
22
+ } | null;
23
+ if (body?.token && body.section) {
24
+ const api = createClient({ url: process.env.ZEF_URL ?? "", apiKey: body.token });
25
+ const [resolved, document] = await Promise.all([
26
+ // Le document est passé à l'API : sur la page d'un élément, ses variables sont remplacées.
27
+ api.previewSection(body.section, { locale: body.locale, item: body.doc }).catch(() => null),
28
+ body.doc ? api.previewDocument(body.doc, { locale: body.locale }).catch(() => null) : null,
29
+ ]);
30
+ section = resolved;
31
+ if (document?.kind === "item") item = document.item;
32
+ }
33
+ }
34
+ if (!section) Astro.response.status = 400;
35
+ ---
36
+
37
+ {section && <ZefSections sections={[section]} item={item} preview />}
@@ -0,0 +1,72 @@
1
+ ---
2
+ import Layout from "virtual:zef/layout";
3
+ /**
4
+ * Vignette de la bibliothèque de sections (E-02) : une section avec son contenu d'exemple,
5
+ * dans le gabarit du site, mise en haut de la fenêtre. L'éditeur l'affiche réduite dans une
6
+ * iframe ; affichable seulement dans le CMS (`frame-ancestors`).
7
+ */
8
+ import { cmsOrigins } from "@agence-zef/astro";
9
+ import { createClient } from "@agence-zef/sdk";
10
+ import ZefSections from "../ZefSections.astro";
11
+
12
+ export const prerender = false;
13
+
14
+ const params = Astro.url.searchParams;
15
+ const token = params.get("token") ?? "";
16
+ const locale = params.get("locale") ?? undefined;
17
+
18
+ const ancestors = cmsOrigins().join(" ");
19
+ Astro.response.headers.set("Content-Security-Policy", `frame-ancestors ${ancestors || "'none'"}`);
20
+ Astro.response.headers.set("Cache-Control", "no-store");
21
+ Astro.response.headers.set("X-Robots-Tag", "noindex");
22
+
23
+ let raw: { id: string; type: string } | null = null;
24
+ try {
25
+ raw = JSON.parse(Buffer.from(params.get("section") ?? "", "base64url").toString("utf8"));
26
+ } catch {
27
+ raw = null;
28
+ }
29
+ const api = createClient({ url: process.env.ZEF_URL ?? "", apiKey: token });
30
+ const [site, section] = await Promise.all([
31
+ api.site().catch(() => null),
32
+ raw ? api.previewSection(raw, { locale }).catch(() => null) : null,
33
+ ]);
34
+ if (!site || !section) Astro.response.status = 404;
35
+ ---
36
+
37
+ {
38
+ site && section ? (
39
+ <Layout
40
+ site={site}
41
+ locale={locale ?? site.defaultLocale}
42
+ title={site.name}
43
+ seo={{ title: null, description: null, noindex: true, canonical: null, image: null }}
44
+ alternates={{}}
45
+ header={null}
46
+ footer={null}
47
+ >
48
+ <div id="zef-thumbnail">
49
+ <ZefSections sections={[section]} />
50
+ </div>
51
+ <style is:global>
52
+ html { overflow: hidden; scrollbar-width: none; }
53
+ body { pointer-events: none; }
54
+ astro-dev-toolbar { display: none !important; }
55
+ </style>
56
+ <script>
57
+ // La section seule : tout ce qui l'entoure dans le gabarit (header, footer…) est masqué.
58
+ let node = document.getElementById("zef-thumbnail");
59
+ while (node && node !== document.body) {
60
+ for (const sibling of node.parentElement?.children ?? []) {
61
+ if (sibling !== node && !["SCRIPT", "STYLE", "LINK"].includes(sibling.tagName)) {
62
+ (sibling as HTMLElement).style.display = "none";
63
+ }
64
+ }
65
+ node = node.parentElement;
66
+ }
67
+ </script>
68
+ </Layout>
69
+ ) : (
70
+ <p style="font-family: system-ui; padding: 1rem; color: #71717a">Vignette indisponible</p>
71
+ )
72
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ import Layout from "virtual:zef/layout";
3
+ /**
4
+ * Aperçu de l'éditeur (plan technique § 3.4) : la page ou l'élément de collection, brouillon
5
+ * compris, dans le gabarit du site, avec l'overlay de sélection. Lu avec le jeton signé que
6
+ * l'éditeur passe dans l'adresse ; affichable seulement dans le CMS (`frame-ancestors` :
7
+ * sous-domaine du client et espace agence `admin.`).
8
+ */
9
+ import { cmsOrigin, cmsOrigins } from "@agence-zef/astro";
10
+ import { createClient } from "@agence-zef/sdk";
11
+ import ZefSections from "../ZefSections.astro";
12
+
13
+ export const prerender = false;
14
+
15
+ const params = Astro.url.searchParams;
16
+ const token = params.get("token") ?? "";
17
+ const doc = params.get("doc") ?? "";
18
+ const locale = params.get("locale") ?? undefined;
19
+ const version = params.get("version") ?? undefined;
20
+ const cms = cmsOrigin(params.get("cms"));
21
+
22
+ const ancestors = cmsOrigins().join(" ");
23
+ Astro.response.headers.set("Content-Security-Policy", `frame-ancestors ${ancestors || "'none'"}`);
24
+ Astro.response.headers.set("Cache-Control", "no-store");
25
+ Astro.response.headers.set("X-Robots-Tag", "noindex");
26
+
27
+ const api = createClient({ url: process.env.ZEF_URL ?? "", apiKey: token });
28
+ const [site, document, layout] = await Promise.all([
29
+ api.site().catch(() => null),
30
+ doc ? api.previewDocument(doc, { locale, version }).catch(() => null) : null,
31
+ api.previewLayout({ locale }).catch(() => ({ header: null, footer: null })),
32
+ ]);
33
+ if (!site || !document) {
34
+ Astro.response.status = 404;
35
+ }
36
+ const page = document?.kind === "page" ? document.page : null;
37
+ const props = document &&
38
+ site && {
39
+ site,
40
+ locale: page?.locale ?? locale ?? site.defaultLocale,
41
+ title: page
42
+ ? page.title
43
+ : String(document.kind === "item" ? document.item[document.titleField] : ""),
44
+ seo: page?.seo ?? {
45
+ title: null,
46
+ description: null,
47
+ noindex: true,
48
+ canonical: null,
49
+ image: null,
50
+ },
51
+ alternates: page?.alternates ?? {},
52
+ header: layout.header,
53
+ footer: layout.footer,
54
+ preview: true,
55
+ };
56
+ ---
57
+
58
+ {
59
+ props ? (
60
+ <Layout {...props}>
61
+ <div data-zef-sections style="display: contents">
62
+ <ZefSections
63
+ sections={page ? page.sections : document?.kind === "item" ? (document.item.sections ?? []) : []}
64
+ item={document?.kind === "item" ? document.item : undefined}
65
+ preview
66
+ />
67
+ </div>
68
+ <div id="zef-preview" hidden data-cms={cms} data-token={token} data-locale={props.locale} data-doc={doc} />
69
+ <script>
70
+ import { startOverlay } from "@agence-zef/astro/overlay";
71
+ startOverlay();
72
+ </script>
73
+ </Layout>
74
+ ) : (
75
+ <p style="font-family: system-ui; padding: 2rem">Aperçu indisponible : le lien a expiré ou le contenu n'existe plus.</p>
76
+ )
77
+ }
@@ -0,0 +1,16 @@
1
+ import { getClient } from "@agence-zef/astro";
2
+ import type { APIRoute } from "astro";
3
+
4
+ /*
5
+ * `robots.txt` injecté par `@agence-zef/astro` (§ 5.11) : tout est bloqué tant que le site est en
6
+ * développement ; une fois en ligne, les moteurs peuvent tout indexer. Construit avec le site :
7
+ * la mise en ligne relance un build.
8
+ */
9
+
10
+ export const prerender = true;
11
+
12
+ export const GET: APIRoute = async () => {
13
+ const site = await getClient().site();
14
+ const body = site.indexable ? "User-agent: *\nAllow: /\n" : "User-agent: *\nDisallow: /\n";
15
+ return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
16
+ };
@@ -0,0 +1,12 @@
1
+ declare module "virtual:zef/config" {
2
+ const config: import("@agence-zef/schema").ZefConfig;
3
+ export default config;
4
+ }
5
+ declare module "virtual:zef/sections" {
6
+ const sections: Record<string, (props: Record<string, unknown>) => unknown>;
7
+ export default sections;
8
+ }
9
+ declare module "virtual:zef/layout" {
10
+ const layout: (props: Record<string, unknown>) => unknown;
11
+ export default layout;
12
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Charge ZEF_URL et ZEF_API_KEY depuis `.env` (écrit par `zef link`) dans `process.env`,
3
+ * sans écraser les variables déjà posées (Vercel). Astro ne le fait que pour `import.meta.env`.
4
+ */
5
+ export declare function loadZefEnv(root: string): void;
package/dist/env.js ADDED
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseEnv } from "node:util";
4
+ /**
5
+ * Charge ZEF_URL et ZEF_API_KEY depuis `.env` (écrit par `zef link`) dans `process.env`,
6
+ * sans écraser les variables déjà posées (Vercel). Astro ne le fait que pour `import.meta.env`.
7
+ */
8
+ export function loadZefEnv(root) {
9
+ const file = join(root, ".env");
10
+ if (!existsSync(file))
11
+ return;
12
+ const values = parseEnv(readFileSync(file, "utf8"));
13
+ for (const key of ["ZEF_URL", "ZEF_API_KEY"]) {
14
+ if (!process.env[key] && values[key])
15
+ process.env[key] = values[key];
16
+ }
17
+ }
@@ -0,0 +1,53 @@
1
+ import type { ZefConfig } from "@agence-zef/schema";
2
+ import { type ApiItem, type ApiPage, type ApiSection, type ApiSeo, type ApiSite, type ZefClient } from "@agence-zef/sdk";
3
+ /**
4
+ * Registre du site, déclaré une fois dans `src/env.d.ts` pour typer les helpers :
5
+ * `declare module "@agence-zef/astro" { interface Register { config: typeof import("../zef.config").default } }`
6
+ */
7
+ export interface Register {
8
+ }
9
+ export type SiteConfig = Register extends {
10
+ config: infer C extends ZefConfig;
11
+ } ? C : ZefConfig;
12
+ /** Client de l'API du site, configuré par ZEF_URL et ZEF_API_KEY (`zef link`). */
13
+ export declare function getClient(): ZefClient<SiteConfig>;
14
+ export declare const getSite: () => Promise<ApiSite>;
15
+ export declare const getPage: (url: string) => Promise<ApiPage | null>;
16
+ export declare const getCollection: ZefClient<SiteConfig>["collection"];
17
+ export declare const getItem: ZefClient<SiteConfig>["item"];
18
+ /** Props reçues par `src/layouts/Page.astro` : une page du CMS ou un élément de collection. */
19
+ export type LayoutProps = {
20
+ site: ApiSite;
21
+ locale: string;
22
+ title: string;
23
+ seo: ApiSeo;
24
+ /** Adresse dans chaque langue (sélecteur de langue, balises hreflang). */
25
+ alternates: Record<string, string>;
26
+ /** Header et footer (sections partagées), à rendre avec `<ZefSections>` ; `null` sans `layout`. */
27
+ header: ApiSection | null;
28
+ footer: ApiSection | null;
29
+ /** Aperçu de l'éditeur : à passer à `<ZefSections preview>` pour le header et le footer. */
30
+ preview?: boolean;
31
+ };
32
+ export type RouteProps = LayoutProps & ({
33
+ kind: "page";
34
+ page: ApiPage;
35
+ } | {
36
+ kind: "item";
37
+ collection: string;
38
+ item: ApiItem;
39
+ });
40
+ /** Toutes les routes du site : pages publiées, puis éléments des collections avec page. */
41
+ export declare function zefStaticPaths(config: ZefConfig): Promise<{
42
+ params: {
43
+ path: string | undefined;
44
+ };
45
+ props: RouteProps;
46
+ }[]>;
47
+ /**
48
+ * Origines du CMS autorisées à afficher l'aperçu : le sous-domaine du client (`ZEF_URL`) et
49
+ * l'espace agence (`admin.` du même domaine), d'où l'agence gère ses clients.
50
+ */
51
+ export declare function cmsOrigins(zefUrl?: string | undefined): string[];
52
+ /** Origine du CMS qui ouvre l'aperçu (`?cms=`), si elle est autorisée ; sinon celle du client. */
53
+ export declare function cmsOrigin(requested: string | null, zefUrl?: string | undefined): string;