@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,100 @@
1
+ import { createClient, } from "@agence-zef/sdk";
2
+ let client;
3
+ /** Client de l'API du site, configuré par ZEF_URL et ZEF_API_KEY (`zef link`). */
4
+ export function getClient() {
5
+ if (!client) {
6
+ const url = process.env.ZEF_URL;
7
+ const apiKey = process.env.ZEF_API_KEY;
8
+ if (!url || !apiKey) {
9
+ throw new Error("[@agence-zef/astro] ZEF_URL et ZEF_API_KEY manquants : lancez `zef link <client>`.");
10
+ }
11
+ client = createClient({ url, apiKey });
12
+ }
13
+ return client;
14
+ }
15
+ /* Helpers typés pour les besoins particuliers (liste filtrée, page sur mesure). */
16
+ export const getSite = () => getClient().site();
17
+ export const getPage = (url) => getClient().page(url);
18
+ export const getCollection = (name, opts) => getClient().collection(name, opts);
19
+ export const getItem = (name, slug, opts) => getClient().item(name, slug, opts);
20
+ const toParam = (url) => url.replace(/^\/+|\/+$/g, "") || undefined;
21
+ /** Toutes les routes du site : pages publiées, puis éléments des collections avec page. */
22
+ export async function zefStaticPaths(config) {
23
+ const api = getClient();
24
+ const site = await api.site();
25
+ const paths = [];
26
+ const layouts = new Map(await Promise.all(site.locales.map(async (l) => [l, await api.layout({ locale: l })])));
27
+ for (const entry of await api.pages()) {
28
+ for (const [locale, url] of Object.entries(entry.urls)) {
29
+ const page = await api.page(url);
30
+ if (!page)
31
+ continue;
32
+ paths.push({
33
+ params: { path: toParam(url) },
34
+ props: {
35
+ kind: "page",
36
+ page,
37
+ site,
38
+ locale,
39
+ title: page.seo.title ?? page.title,
40
+ seo: page.seo,
41
+ alternates: page.alternates,
42
+ header: layouts.get(locale)?.header ?? null,
43
+ footer: layouts.get(locale)?.footer ?? null,
44
+ },
45
+ });
46
+ }
47
+ }
48
+ const withPage = (config.collections ?? []).filter((c) => Boolean(c.page));
49
+ for (const def of withPage) {
50
+ const byLocale = await Promise.all(site.locales.map(async (locale) => ({
51
+ locale,
52
+ list: await api.collection(def.name, { locale }),
53
+ })));
54
+ for (const { locale, list } of byLocale) {
55
+ for (const item of list.items) {
56
+ if (!item.url)
57
+ continue;
58
+ const alternates = {};
59
+ for (const other of byLocale) {
60
+ const url = other.list.items.find((i) => i.id === item.id)?.url;
61
+ if (url)
62
+ alternates[other.locale] = url;
63
+ }
64
+ paths.push({
65
+ params: { path: toParam(item.url) },
66
+ props: {
67
+ kind: "item",
68
+ collection: def.name,
69
+ item,
70
+ site,
71
+ locale,
72
+ title: String(item[def.titleField] ?? ""),
73
+ seo: { title: null, description: null, noindex: false, canonical: null, image: null },
74
+ alternates,
75
+ header: layouts.get(locale)?.header ?? null,
76
+ footer: layouts.get(locale)?.footer ?? null,
77
+ },
78
+ });
79
+ }
80
+ }
81
+ }
82
+ return paths;
83
+ }
84
+ /**
85
+ * Origines du CMS autorisées à afficher l'aperçu : le sous-domaine du client (`ZEF_URL`) et
86
+ * l'espace agence (`admin.` du même domaine), d'où l'agence gère ses clients.
87
+ */
88
+ export function cmsOrigins(zefUrl = process.env.ZEF_URL) {
89
+ if (!zefUrl)
90
+ return [];
91
+ const client = new URL(zefUrl);
92
+ const agency = new URL(client.origin);
93
+ agency.hostname = client.hostname.replace(/^[^.]+/, "admin");
94
+ return [...new Set([client.origin, agency.origin])];
95
+ }
96
+ /** Origine du CMS qui ouvre l'aperçu (`?cms=`), si elle est autorisée ; sinon celle du client. */
97
+ export function cmsOrigin(requested, zefUrl = process.env.ZEF_URL) {
98
+ const allowed = cmsOrigins(zefUrl);
99
+ return requested && allowed.includes(requested) ? requested : (allowed[0] ?? "");
100
+ }
@@ -0,0 +1,18 @@
1
+ import type { AstroIntegration } from "astro";
2
+ export * from "./helpers.js";
3
+ export type ZefOptions = {
4
+ /** Gabarit des pages du CMS (en-tête, pied, styles du site). */
5
+ layout?: string;
6
+ /**
7
+ * Adresses publiques des médias, optimisés au build par `<Image>`. Par défaut le CDN de
8
+ * l'agence (`https://medias.agence-zef.fr`), plus `ZEF_MEDIA_URL` si elle est posée
9
+ * (MinIO en local : `http://localhost:9000`).
10
+ */
11
+ mediaUrls?: string[];
12
+ };
13
+ /**
14
+ * Intégration Astro du CMS ZEF (cahier des charges § 10) : route attrape-tout des pages
15
+ * et des collections avec page (les sections choisies dans l'éditeur, sans gabarit), rendu des
16
+ * sections (`src/sections/<nom>/index.astro`). Lit ZEF_URL et ZEF_API_KEY.
17
+ */
18
+ export default function zef(options?: ZefOptions): AstroIntegration;
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { loadZefEnv } from "./env.js";
5
+ import { getClient } from "./helpers.js";
6
+ export * from "./helpers.js";
7
+ const MEDIA_URL = "https://medias.agence-zef.fr";
8
+ /** Domaines autorisés pour l'optimisation des images distantes (`image.remotePatterns`). */
9
+ function mediaPatterns(urls) {
10
+ return urls.flatMap((raw) => {
11
+ try {
12
+ const url = new URL(raw);
13
+ return [
14
+ {
15
+ protocol: url.protocol.replace(":", ""),
16
+ hostname: url.hostname,
17
+ ...(url.port ? { port: url.port } : {}),
18
+ },
19
+ ];
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ });
25
+ }
26
+ const VIRTUAL = {
27
+ config: "virtual:zef/config",
28
+ sections: "virtual:zef/sections",
29
+ layout: "virtual:zef/layout",
30
+ };
31
+ /** `{ hero: "/…/src/sections/hero/index.astro" }` pour chaque dossier de `src/sections`. */
32
+ function sectionFiles(root) {
33
+ const dir = join(root, "src", "sections");
34
+ if (!existsSync(dir))
35
+ return {};
36
+ return Object.fromEntries(readdirSync(dir, { withFileTypes: true })
37
+ .filter((d) => d.isDirectory() && existsSync(join(dir, d.name, "index.astro")))
38
+ .map((d) => [d.name, join(dir, d.name, "index.astro")]));
39
+ }
40
+ const importMap = (files) => {
41
+ const entries = Object.entries(files);
42
+ return [
43
+ ...entries.map(([, file], i) => `import C${i} from ${JSON.stringify(file)};`),
44
+ `export default { ${entries.map(([name], i) => `${JSON.stringify(name)}: C${i}`).join(", ")} };`,
45
+ ].join("\n");
46
+ };
47
+ /**
48
+ * Intégration Astro du CMS ZEF (cahier des charges § 10) : route attrape-tout des pages
49
+ * et des collections avec page (les sections choisies dans l'éditeur, sans gabarit), rendu des
50
+ * sections (`src/sections/<nom>/index.astro`). Lit ZEF_URL et ZEF_API_KEY.
51
+ */
52
+ export default function zef(options = {}) {
53
+ return {
54
+ name: "@agence-zef/astro",
55
+ hooks: {
56
+ "astro:config:setup": async ({ config, command, injectRoute, updateConfig, addWatchFile, logger, }) => {
57
+ const root = fileURLToPath(config.root);
58
+ loadZefEnv(root);
59
+ // Redirections du CMS (C-17 : slugs modifiés, pages déplacées, redirections manuelles).
60
+ if ((command === "build" || command === "dev") &&
61
+ process.env.ZEF_URL &&
62
+ process.env.ZEF_API_KEY) {
63
+ try {
64
+ const list = await getClient().redirects();
65
+ updateConfig({
66
+ redirects: Object.fromEntries(list.map((r) => [r.from, { status: r.status, destination: r.to }])),
67
+ });
68
+ }
69
+ catch (error) {
70
+ logger.warn(`redirections non chargées : ${error instanceof Error ? error.message : error}`);
71
+ }
72
+ }
73
+ // Médias de la médiathèque : optimisés au build par `<Image>` (format, poids, srcset).
74
+ updateConfig({
75
+ image: {
76
+ remotePatterns: mediaPatterns([
77
+ ...(options.mediaUrls ?? [MEDIA_URL]),
78
+ ...(process.env.ZEF_MEDIA_URL ? [process.env.ZEF_MEDIA_URL] : []),
79
+ ]),
80
+ },
81
+ });
82
+ addWatchFile(join(root, ".env"));
83
+ const layout = join(root, options.layout ?? "src/layouts/Page.astro");
84
+ const configFile = join(root, "zef.config.ts");
85
+ injectRoute({
86
+ pattern: "/[...path]",
87
+ entrypoint: "@agence-zef/astro/routes/page.astro",
88
+ prerender: true,
89
+ });
90
+ injectRoute({
91
+ pattern: "/robots.txt",
92
+ entrypoint: "@agence-zef/astro/routes/robots.ts",
93
+ prerender: true,
94
+ });
95
+ // Aperçu de l'éditeur : rendu à la demande, donc avec un adaptateur (Vercel) ou en dev.
96
+ if (command === "dev" || config.adapter) {
97
+ injectRoute({
98
+ pattern: "/__zef/preview",
99
+ entrypoint: "@agence-zef/astro/routes/preview.astro",
100
+ prerender: false,
101
+ });
102
+ injectRoute({
103
+ pattern: "/__zef/preview/section",
104
+ entrypoint: "@agence-zef/astro/routes/preview-section.astro",
105
+ prerender: false,
106
+ });
107
+ injectRoute({
108
+ pattern: "/__zef/preview/thumbnail",
109
+ entrypoint: "@agence-zef/astro/routes/preview-thumbnail.astro",
110
+ prerender: false,
111
+ });
112
+ }
113
+ else {
114
+ logger.info("pas d'adaptateur : la route d'aperçu de l'éditeur n'est pas construite");
115
+ }
116
+ updateConfig({
117
+ vite: {
118
+ plugins: [
119
+ {
120
+ name: "zef:virtual",
121
+ resolveId(id) {
122
+ return Object.values(VIRTUAL).includes(id) ? `\0${id}` : undefined;
123
+ },
124
+ load(id) {
125
+ if (id === `\0${VIRTUAL.config}`) {
126
+ return `export { default } from ${JSON.stringify(configFile)};`;
127
+ }
128
+ if (id === `\0${VIRTUAL.layout}`) {
129
+ if (!existsSync(layout)) {
130
+ throw new Error(`[@agence-zef/astro] gabarit introuvable : ${layout}`);
131
+ }
132
+ return `export { default } from ${JSON.stringify(layout)};`;
133
+ }
134
+ if (id === `\0${VIRTUAL.sections}`)
135
+ return importMap(sectionFiles(root));
136
+ return undefined;
137
+ },
138
+ },
139
+ ],
140
+ },
141
+ });
142
+ },
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,110 @@
1
+ /** Messages échangés avec l'éditeur. */
2
+ export type FieldKind = "text" | "textarea" | "richtext" | "label" | "media" | "other";
3
+ export type FieldInfo = {
4
+ label: string;
5
+ kind: FieldKind;
6
+ };
7
+ export type ToPreview = {
8
+ zef: "labels";
9
+ sections: Record<string, string>;
10
+ fields: Record<string, Record<string, FieldInfo>>;
11
+ /** Sections fixes (header, footer) : ni barre d'outils ni « + Ajouter une section ». */
12
+ fixed?: string[];
13
+ /** Lecture seule (rôle, ou quelqu'un d'autre a la main) : sélection seulement. */
14
+ readOnly?: boolean;
15
+ } | {
16
+ zef: "select";
17
+ section: string | null;
18
+ path: string | null;
19
+ } | {
20
+ zef: "text";
21
+ section: string;
22
+ path: string;
23
+ value: string;
24
+ } | {
25
+ zef: "render";
26
+ section: {
27
+ id: string;
28
+ type: string;
29
+ };
30
+ after?: string | null;
31
+ } | {
32
+ zef: "order";
33
+ ids: string[];
34
+ } | {
35
+ zef: "remove";
36
+ id: string;
37
+ }
38
+ /** Historique (E-05) : sections modifiées ou ajoutées depuis une version, `null` pour effacer. */
39
+ | {
40
+ zef: "compare";
41
+ sections: Record<string, {
42
+ state: "modified" | "added";
43
+ label: string;
44
+ }> | null;
45
+ }
46
+ /** Commentaires (E-06) : épingles numérotées, mode « cliquez sur un élément », épingle à montrer. */
47
+ | {
48
+ zef: "pins";
49
+ pins: Pin[];
50
+ } | {
51
+ zef: "commentMode";
52
+ on: boolean;
53
+ } | {
54
+ zef: "showPin";
55
+ id: string;
56
+ }
57
+ /** Textes à ne pas éditer sur place (`<section>:<chemin>`) : ils contiennent des variables. */
58
+ | {
59
+ zef: "locked";
60
+ fields: string[];
61
+ };
62
+ export type Pin = {
63
+ id: string;
64
+ number: number;
65
+ section: string;
66
+ path: string | null;
67
+ };
68
+ export type Box = {
69
+ top: number;
70
+ left: number;
71
+ width: number;
72
+ height: number;
73
+ };
74
+ export type SectionAction = "up" | "down" | "duplicate" | "variants" | "delete";
75
+ export type FromPreview = {
76
+ zef: "ready";
77
+ } | {
78
+ zef: "select";
79
+ section: string | null;
80
+ path: string | null;
81
+ } | {
82
+ zef: "edit";
83
+ section: string;
84
+ path: string;
85
+ value: string;
86
+ } | {
87
+ zef: "action";
88
+ section: string;
89
+ action: SectionAction;
90
+ } | {
91
+ zef: "insert";
92
+ after: string;
93
+ } | {
94
+ zef: "pin";
95
+ id: string;
96
+ box: Box;
97
+ } | {
98
+ zef: "commentTarget";
99
+ section: string;
100
+ path: string | null;
101
+ box: Box;
102
+ }
103
+ /** Fichier glissé sur l'image d'un champ média : l'éditeur l'envoie et remplace l'image. */
104
+ | {
105
+ zef: "drop";
106
+ section: string;
107
+ path: string;
108
+ file: File;
109
+ };
110
+ export declare function startOverlay(): void;