@agence-zef/astro 0.3.1 → 0.5.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/Seo.astro +24 -0
- package/components/routes/llms.ts +24 -0
- package/components/routes/page.astro +13 -2
- package/components/routes/robots.ts +6 -6
- package/components/routes/sitemap.ts +16 -0
- package/dist/helpers.d.ts +29 -1
- package/dist/helpers.js +64 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -0
- package/dist/seo.d.ts +103 -0
- package/dist/seo.js +258 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.js +33 -0
- package/package.json +22 -5
- package/styles.css +468 -0
|
@@ -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>
|
package/components/Image.astro
CHANGED
|
@@ -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'
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Référencement de la page (§ 5.13), à placer dans le `<head>` du gabarit :
|
|
4
|
+
* `<Seo {...Astro.props} organization={…} />`. Titre, description, robots, canonique, hreflang
|
|
5
|
+
* (avec `x-default`), Open Graph, Twitter et données structurées JSON-LD (WebSite, Organization
|
|
6
|
+
* ou LocalBusiness, WebPage, BreadcrumbList, Product pour la Boutique). Les champs viennent de
|
|
7
|
+
* l'onglet SEO de l'éditeur ; l'image de partage par défaut et l'adresse du site, du CMS.
|
|
8
|
+
*/
|
|
9
|
+
import { jsonLdScript, type SeoInput, seoTags } from "@agence-zef/astro";
|
|
10
|
+
|
|
11
|
+
type Props = Omit<SeoInput, "url" | "origin"> & { url?: string };
|
|
12
|
+
|
|
13
|
+
const props = Astro.props;
|
|
14
|
+
const tags = seoTags({
|
|
15
|
+
...props,
|
|
16
|
+
url: props.url ?? Astro.url.pathname,
|
|
17
|
+
origin: Astro.site?.origin ?? Astro.url.origin,
|
|
18
|
+
});
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<title>{tags.title}</title>
|
|
22
|
+
{tags.meta.map((m) => (m.name ? <meta name={m.name} content={m.content} /> : <meta property={m.property} content={m.content} />))}
|
|
23
|
+
{tags.links.map((l) => <link rel={l.rel} href={l.href} hreflang={l.hreflang} />)}
|
|
24
|
+
<script type="application/ld+json" is:inline set:html={jsonLdScript(tags.jsonLd)} />
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import config from "virtual:zef/config";
|
|
2
|
+
import { getClient, llmsTxt, siteOrigin, zefSiteIndex } from "@agence-zef/astro";
|
|
3
|
+
import type { APIRoute } from "astro";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* `llms.txt` injecté par `@agence-zef/astro` (§ 5.13, format llmstxt.org) : le site présenté aux
|
|
7
|
+
* assistants IA, avec ses pages et ses collections et leur description.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const prerender = true;
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async ({ site: astroSite }) => {
|
|
13
|
+
const site = await getClient().site();
|
|
14
|
+
const collections = (config.collections ?? [])
|
|
15
|
+
.filter((c) => c.page)
|
|
16
|
+
.map((c) => ({ name: c.name, label: c.label }));
|
|
17
|
+
const body = llmsTxt(
|
|
18
|
+
site,
|
|
19
|
+
await zefSiteIndex(config),
|
|
20
|
+
siteOrigin(site, astroSite?.origin),
|
|
21
|
+
collections,
|
|
22
|
+
);
|
|
23
|
+
return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
24
|
+
};
|
|
@@ -14,11 +14,22 @@ export async function getStaticPaths() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const props = Astro.props as RouteProps;
|
|
17
|
-
const { site, locale, title, alternates, header, footer } = props;
|
|
17
|
+
const { site, locale, title, alternates, header, footer, url, breadcrumbs } = props;
|
|
18
18
|
// Site en développement (§ 5.11) : aucune page n'est indexable.
|
|
19
19
|
const seo = site.indexable === false ? { ...props.seo, noindex: true } : props.seo;
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
<Layout
|
|
22
|
+
<Layout
|
|
23
|
+
site={site}
|
|
24
|
+
locale={locale}
|
|
25
|
+
title={title}
|
|
26
|
+
seo={seo}
|
|
27
|
+
alternates={alternates}
|
|
28
|
+
url={url}
|
|
29
|
+
breadcrumbs={breadcrumbs}
|
|
30
|
+
item={props.kind === "item" ? props.item : undefined}
|
|
31
|
+
header={header}
|
|
32
|
+
footer={footer}
|
|
33
|
+
>
|
|
23
34
|
{props.kind === "page" ? <ZefSections sections={props.page.sections} /> : <ZefSections sections={props.item.sections ?? []} item={props.item} />}
|
|
24
35
|
</Layout>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { getClient } from "@agence-zef/astro";
|
|
1
|
+
import { getClient, robotsTxt, siteOrigin } from "@agence-zef/astro";
|
|
2
2
|
import type { APIRoute } from "astro";
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
* `robots.txt` injecté par `@agence-zef/astro` (§ 5.11) : tout est bloqué tant que le site
|
|
6
|
-
* développement ; une fois en ligne,
|
|
7
|
-
* la mise en ligne relance un build.
|
|
5
|
+
* `robots.txt` injecté par `@agence-zef/astro` (§ 5.11, § 5.13) : tout est bloqué tant que le site
|
|
6
|
+
* est en développement ; une fois en ligne, moteurs et assistants IA peuvent tout lire, le
|
|
7
|
+
* sitemap est déclaré. Construit avec le site : la mise en ligne relance un build.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export const prerender = true;
|
|
11
11
|
|
|
12
|
-
export const GET: APIRoute = async () => {
|
|
12
|
+
export const GET: APIRoute = async ({ site: astroSite }) => {
|
|
13
13
|
const site = await getClient().site();
|
|
14
|
-
const body = site
|
|
14
|
+
const body = robotsTxt(site, siteOrigin(site, astroSite?.origin));
|
|
15
15
|
return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
16
16
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import config from "virtual:zef/config";
|
|
2
|
+
import { getClient, sitemapXml, siteOrigin, zefSiteIndex } from "@agence-zef/astro";
|
|
3
|
+
import type { APIRoute } from "astro";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* `sitemap.xml` injecté par `@agence-zef/astro` (§ 5.13) : pages et éléments publiés et
|
|
7
|
+
* indexables, chaque langue liée par `xhtml:link`, date de dernière publication.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const prerender = true;
|
|
11
|
+
|
|
12
|
+
export const GET: APIRoute = async ({ site: astroSite }) => {
|
|
13
|
+
const site = await getClient().site();
|
|
14
|
+
const body = sitemapXml(await zefSiteIndex(config), siteOrigin(site, astroSite?.origin));
|
|
15
|
+
return new Response(body, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
|
|
16
|
+
};
|
package/dist/helpers.d.ts
CHANGED
|
@@ -23,12 +23,22 @@ export type LayoutProps = {
|
|
|
23
23
|
seo: ApiSeo;
|
|
24
24
|
/** Adresse dans chaque langue (sélecteur de langue, balises hreflang). */
|
|
25
25
|
alternates: Record<string, string>;
|
|
26
|
+
/** Adresse de la page (`/la-cave`), base de la canonique et de `og:url`. */
|
|
27
|
+
url?: string;
|
|
28
|
+
/** Fil d'Ariane, de l'accueil à la page (données structurées BreadcrumbList). */
|
|
29
|
+
breadcrumbs?: Breadcrumb[];
|
|
30
|
+
/** Élément de collection affiché (données structurées : Product pour la Boutique). */
|
|
31
|
+
item?: ApiItem;
|
|
26
32
|
/** Header et footer (sections partagées), à rendre avec `<ZefSections>` ; `null` sans `layout`. */
|
|
27
33
|
header: ApiSection | null;
|
|
28
34
|
footer: ApiSection | null;
|
|
29
35
|
/** Aperçu de l'éditeur : à passer à `<ZefSections preview>` pour le header et le footer. */
|
|
30
36
|
preview?: boolean;
|
|
31
37
|
};
|
|
38
|
+
export type Breadcrumb = {
|
|
39
|
+
title: string;
|
|
40
|
+
url: string;
|
|
41
|
+
};
|
|
32
42
|
export type RouteProps = LayoutProps & ({
|
|
33
43
|
kind: "page";
|
|
34
44
|
page: ApiPage;
|
|
@@ -46,13 +56,31 @@ export declare function zefLayoutProps(opts: {
|
|
|
46
56
|
locale?: string;
|
|
47
57
|
noindex?: boolean;
|
|
48
58
|
}): Promise<LayoutProps>;
|
|
49
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Toutes les routes du site : pages publiées, puis éléments des collections avec page. Calculé
|
|
61
|
+
* une fois par build : la route des pages, le sitemap et `llms.txt` partagent le résultat.
|
|
62
|
+
*/
|
|
50
63
|
export declare function zefStaticPaths(config: ZefConfig): Promise<{
|
|
51
64
|
params: {
|
|
52
65
|
path: string | undefined;
|
|
53
66
|
};
|
|
54
67
|
props: RouteProps;
|
|
55
68
|
}[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Index du site pour le sitemap et `llms.txt` : une entrée par page et par élément avec page,
|
|
71
|
+
* dans chaque langue (titre, description, indexation, date de publication, langues).
|
|
72
|
+
*/
|
|
73
|
+
export declare function zefSiteIndex(config: ZefConfig): Promise<{
|
|
74
|
+
url: string;
|
|
75
|
+
locale: string;
|
|
76
|
+
title: string;
|
|
77
|
+
description: string | null;
|
|
78
|
+
noindex: boolean;
|
|
79
|
+
canonical: string | null;
|
|
80
|
+
updatedAt: string | null;
|
|
81
|
+
alternates: Record<string, string>;
|
|
82
|
+
collection: string | null;
|
|
83
|
+
}[]>;
|
|
56
84
|
/**
|
|
57
85
|
* Origines du CMS autorisées à afficher l'aperçu : le sous-domaine du client (`ZEF_URL`) et
|
|
58
86
|
* l'espace agence (`admin.` du même domaine), d'où l'agence gère ses clients.
|