@hgdc/docs 0.2.0 → 0.4.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.
Files changed (233) hide show
  1. package/docs.schema.json +21 -0
  2. package/package.json +3 -1
  3. package/src/build/auto-import.ts +5 -24
  4. package/src/build/content.ts +82 -0
  5. package/src/build/markdown.ts +10 -5
  6. package/src/components/Accordion.astro +3 -1
  7. package/src/components/Callout.astro +31 -0
  8. package/src/components/Card.astro +3 -1
  9. package/src/components/Check.astro +2 -3
  10. package/src/components/CodeGroup.astro +9 -1
  11. package/src/components/Danger.astro +2 -3
  12. package/src/components/Info.astro +2 -3
  13. package/src/components/Note.astro +2 -3
  14. package/src/components/Tip.astro +2 -3
  15. package/src/components/Warning.astro +2 -3
  16. package/src/core/components.ts +166 -0
  17. package/src/core/config.ts +42 -0
  18. package/src/core/feedback-api.ts +27 -0
  19. package/src/core/theme.ts +6 -3
  20. package/src/core/validate.ts +276 -0
  21. package/src/integration.ts +102 -30
  22. package/src/overrides/Assistant.astro +17 -39
  23. package/src/overrides/Footer.astro +107 -0
  24. package/src/overrides/Head.astro +14 -14
  25. package/src/overrides/Header.astro +92 -0
  26. package/src/overrides/Icon.astro +65 -0
  27. package/src/overrides/PageFrame.astro +55 -8
  28. package/src/overrides/PageSidebar.astro +7 -0
  29. package/src/overrides/PageTitle.astro +126 -95
  30. package/src/overrides/Search.astro +54 -0
  31. package/src/overrides/Sidebar.astro +105 -60
  32. package/src/overrides/SidebarList.astro +52 -0
  33. package/src/overrides/SiteFooter.astro +51 -0
  34. package/src/overrides/ThemeProvider.astro +23 -0
  35. package/src/overrides/ThemeToggle.astro +90 -0
  36. package/src/overrides/TwoColumnContent.astro +45 -0
  37. package/src/overrides/sections.ts +29 -0
  38. package/src/styles/theme.css +2137 -511
  39. package/src/widget/client.ts +51 -19
  40. package/src/widget/code.ts +218 -0
  41. package/src/widget/excerpt.ts +5 -0
  42. package/src/widget/search.ts +222 -0
  43. package/template/.astro/collections/docs.schema.json +3 -0
  44. package/template/.astro/content-modules.mjs +35 -15
  45. package/template/.astro/data-store.json +1 -0
  46. package/template/.astro/settings.json +5 -0
  47. package/template/.build/.well-known/llms.txt +51 -34
  48. package/template/.build/404.html +16 -42
  49. package/template/.build/_astro/Assistant.astro_astro_type_script_index_0_lang.C1pz7yK1.js +4 -0
  50. package/template/.build/_astro/CodeGroup.astro_astro_type_script_index_0_lang.-eM8maAX.js +1 -0
  51. package/template/.build/_astro/Footer.astro_astro_type_script_index_0_lang.DzCnn3OT.js +1 -0
  52. package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.D7EcezBU.js +1 -0
  53. package/template/.build/_astro/code.smqv0zhe.js +9 -0
  54. package/template/.build/_astro/common.Dx3aBIS0.css +3 -0
  55. package/template/.build/_astro/{ec.8nqtp.css → ec.t9qpe.css} +1 -1
  56. package/template/.build/_astro/logo.BPu921Tp.svg +1 -0
  57. package/template/.build/_hgdc/corpus.json +1 -1
  58. package/template/.build/concepts/confidentialite/index.html +50 -0
  59. package/template/.build/concepts/confidentialite.md +38 -0
  60. package/template/.build/concepts/deploiements/index.html +106 -0
  61. package/template/.build/concepts/deploiements.md +70 -0
  62. package/template/.build/concepts/organisations-et-roles/index.html +83 -0
  63. package/template/.build/concepts/organisations-et-roles.md +39 -0
  64. package/template/.build/concepts/preversion/index.html +49 -0
  65. package/template/.build/concepts/preversion.md +52 -0
  66. package/template/.build/concepts/secrets-et-coffre/index.html +70 -0
  67. package/template/.build/concepts/secrets-et-coffre.md +54 -0
  68. package/template/.build/demarrer/depuis-github/index.html +34 -0
  69. package/template/.build/demarrer/depuis-github.md +68 -0
  70. package/template/.build/demarrer/premier-site/index.html +28 -0
  71. package/template/.build/demarrer/premier-site.md +94 -0
  72. package/template/.build/guides/brancher-un-domaine/index.html +91 -0
  73. package/template/.build/guides/brancher-un-domaine.md +81 -0
  74. package/template/.build/guides/cle-d-api/index.html +62 -0
  75. package/template/.build/guides/cle-d-api.md +78 -0
  76. package/template/.build/guides/depannage/index.html +64 -0
  77. package/template/.build/guides/depannage.md +69 -0
  78. package/template/.build/guides/deployer-depuis-github/index.html +45 -0
  79. package/template/.build/guides/deployer-depuis-github.md +70 -0
  80. package/template/.build/guides/deployer-un-framework/index.html +81 -0
  81. package/template/.build/guides/deployer-un-framework.md +74 -0
  82. package/template/.build/guides/github-actions-sans-application/index.html +53 -0
  83. package/template/.build/guides/github-actions-sans-application.md +95 -0
  84. package/template/.build/guides/inviter-une-personne/index.html +28 -0
  85. package/template/.build/guides/inviter-une-personne.md +38 -0
  86. package/template/.build/guides/lire-les-logs/index.html +37 -0
  87. package/template/.build/guides/lire-les-logs.md +48 -0
  88. package/template/.build/guides/migrer-depuis-vercel/index.html +25 -0
  89. package/template/.build/guides/migrer-depuis-vercel.md +51 -0
  90. package/template/.build/guides/monorepo/index.html +40 -0
  91. package/template/.build/guides/monorepo.md +42 -0
  92. package/template/.build/guides/revenir-en-arriere/index.html +39 -0
  93. package/template/.build/guides/revenir-en-arriere.md +50 -0
  94. package/template/.build/guides/site-statique/index.html +41 -0
  95. package/template/.build/guides/site-statique.md +59 -0
  96. package/template/.build/guides/synchroniser-doppler/index.html +46 -0
  97. package/template/.build/guides/synchroniser-doppler.md +63 -0
  98. package/template/.build/guides/variables-d-environnement/index.html +73 -0
  99. package/template/.build/guides/variables-d-environnement.md +92 -0
  100. package/template/.build/images/famille/account.svg +1 -0
  101. package/template/.build/images/famille/analytics.svg +1 -0
  102. package/template/.build/images/famille/docs.svg +1 -0
  103. package/template/.build/images/famille/host.svg +1 -0
  104. package/template/.build/images/famille/mail.svg +1 -0
  105. package/template/.build/images/famille/portal.svg +1 -0
  106. package/template/.build/images/famille/status.svg +1 -0
  107. package/template/.build/images/logo.svg +1 -0
  108. package/template/.build/index.html +30 -86
  109. package/template/.build/index.md +59 -21
  110. package/template/.build/llms-full.txt +2330 -518
  111. package/template/.build/llms.txt +51 -34
  112. package/template/.build/pagefind/fragment/fr-ch_16987e2.pf_fragment +0 -0
  113. package/template/.build/pagefind/fragment/fr-ch_2b4c97b.pf_fragment +0 -0
  114. package/template/.build/pagefind/fragment/fr-ch_3294a5c.pf_fragment +0 -0
  115. package/template/.build/pagefind/fragment/fr-ch_34858c9.pf_fragment +0 -0
  116. package/template/.build/pagefind/fragment/fr-ch_39ddba1.pf_fragment +0 -0
  117. package/template/.build/pagefind/fragment/fr-ch_4ab1899.pf_fragment +0 -0
  118. package/template/.build/pagefind/fragment/fr-ch_4d3a52a.pf_fragment +0 -0
  119. package/template/.build/pagefind/fragment/fr-ch_506c187.pf_fragment +0 -0
  120. package/template/.build/pagefind/fragment/fr-ch_5aa255a.pf_fragment +0 -0
  121. package/template/.build/pagefind/fragment/fr-ch_6841c7d.pf_fragment +0 -0
  122. package/template/.build/pagefind/fragment/fr-ch_6dc7a2d.pf_fragment +0 -0
  123. package/template/.build/pagefind/fragment/fr-ch_847edff.pf_fragment +0 -0
  124. package/template/.build/pagefind/fragment/fr-ch_8745af7.pf_fragment +0 -0
  125. package/template/.build/pagefind/fragment/fr-ch_8aac5e6.pf_fragment +0 -0
  126. package/template/.build/pagefind/fragment/fr-ch_916b233.pf_fragment +0 -0
  127. package/template/.build/pagefind/fragment/fr-ch_9fd014a.pf_fragment +0 -0
  128. package/template/.build/pagefind/fragment/fr-ch_a0808f8.pf_fragment +0 -0
  129. package/template/.build/pagefind/fragment/fr-ch_a4e1f69.pf_fragment +0 -0
  130. package/template/.build/pagefind/fragment/fr-ch_a80a53d.pf_fragment +0 -0
  131. package/template/.build/pagefind/fragment/fr-ch_a9e725b.pf_fragment +0 -0
  132. package/template/.build/pagefind/fragment/fr-ch_ac3f64c.pf_fragment +0 -0
  133. package/template/.build/pagefind/fragment/fr-ch_af292f1.pf_fragment +0 -0
  134. package/template/.build/pagefind/fragment/fr-ch_af3e716.pf_fragment +0 -0
  135. package/template/.build/pagefind/fragment/fr-ch_b0689a4.pf_fragment +0 -0
  136. package/template/.build/pagefind/fragment/fr-ch_baed996.pf_fragment +0 -0
  137. package/template/.build/pagefind/fragment/fr-ch_c89e27a.pf_fragment +0 -0
  138. package/template/.build/pagefind/fragment/fr-ch_cd6ca13.pf_fragment +0 -0
  139. package/template/.build/pagefind/fragment/fr-ch_d09af09.pf_fragment +0 -0
  140. package/template/.build/pagefind/fragment/fr-ch_d4bcea8.pf_fragment +0 -0
  141. package/template/.build/pagefind/fragment/fr-ch_dc2a134.pf_fragment +0 -0
  142. package/template/.build/pagefind/fragment/fr-ch_ded7ddc.pf_fragment +0 -0
  143. package/template/.build/pagefind/fragment/fr-ch_e2528cb.pf_fragment +0 -0
  144. package/template/.build/pagefind/fragment/fr-ch_e3cbb4e.pf_fragment +0 -0
  145. package/template/.build/pagefind/fragment/fr-ch_e7902d9.pf_fragment +0 -0
  146. package/template/.build/pagefind/fragment/fr-ch_f48ca47.pf_fragment +0 -0
  147. package/template/.build/pagefind/index/fr-ch_8a5f905.pf_index +0 -0
  148. package/template/.build/pagefind/index/fr-ch_fbd9c96.pf_index +0 -0
  149. package/template/.build/pagefind/pagefind-entry.json +1 -1
  150. package/template/.build/pagefind/pagefind.fr-ch_211aacbfa7f0e.pf_meta +0 -0
  151. package/template/.build/reference/api/deploiements/index.html +326 -0
  152. package/template/.build/reference/api/deploiements.md +170 -0
  153. package/template/.build/reference/api/erreurs/index.html +102 -0
  154. package/template/.build/reference/api/erreurs.md +50 -0
  155. package/template/.build/reference/api/fichiers/index.html +82 -0
  156. package/template/.build/reference/api/fichiers.md +62 -0
  157. package/template/.build/reference/api/introduction/index.html +126 -0
  158. package/template/.build/reference/api/introduction.md +84 -0
  159. package/template/.build/reference/api/projets/index.html +184 -0
  160. package/template/.build/reference/api/projets.md +102 -0
  161. package/template/.build/reference/api/utilisateur/index.html +96 -0
  162. package/template/.build/reference/api/utilisateur.md +89 -0
  163. package/template/.build/reference/api/variables/index.html +75 -0
  164. package/template/.build/reference/api/variables.md +58 -0
  165. package/template/.build/reference/cli/index.html +165 -82
  166. package/template/.build/reference/cli.md +159 -23
  167. package/template/.build/reference/compatibilite/index.html +196 -0
  168. package/template/.build/reference/compatibilite.md +64 -0
  169. package/template/.build/reference/domaines/index.html +171 -0
  170. package/template/.build/reference/domaines.md +76 -0
  171. package/template/.build/reference/limites/index.html +159 -0
  172. package/template/.build/reference/limites.md +56 -0
  173. package/template/.build/reference/variables/index.html +164 -0
  174. package/template/.build/reference/variables.md +63 -0
  175. package/template/.build/reference/vercel-json/index.html +65 -0
  176. package/template/.build/reference/vercel-json.md +51 -0
  177. package/template/node_modules/.vite/deps/_metadata.json +8 -8
  178. package/template/src/content.config.ts +8 -2
  179. package/template/.build/_astro/Assistant.astro_astro_type_script_index_0_lang.DWgmlr85.js +0 -4
  180. package/template/.build/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.C3fM4Kra.js +0 -1
  181. package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.Cn3-p701.js +0 -1
  182. package/template/.build/_astro/TableOfContents.astro_astro_type_script_index_0_lang.BpJaGz7G.js +0 -1
  183. package/template/.build/_astro/common.C-MpX45O.css +0 -3
  184. package/template/.build/_astro/logo-clair.BqNh6JNi.svg +0 -1
  185. package/template/.build/_astro/logo-sombre.BkgsFXxh.svg +0 -1
  186. package/template/.build/_astro/starlight-toc.D4UDIxQL.js +0 -1
  187. package/template/.build/_astro/ui-core.CrG20ya_.js +0 -2
  188. package/template/.build/demarrer/premiere-doc/index.html +0 -84
  189. package/template/.build/demarrer/premiere-doc.md +0 -65
  190. package/template/.build/explications/donnees/index.html +0 -114
  191. package/template/.build/explications/donnees.md +0 -28
  192. package/template/.build/explications/fonctionnement/index.html +0 -93
  193. package/template/.build/explications/fonctionnement.md +0 -32
  194. package/template/.build/guides/agents/index.html +0 -118
  195. package/template/.build/guides/agents.md +0 -54
  196. package/template/.build/guides/assistant/index.html +0 -119
  197. package/template/.build/guides/assistant.md +0 -46
  198. package/template/.build/guides/composants/index.html +0 -97
  199. package/template/.build/guides/composants.md +0 -101
  200. package/template/.build/guides/ecrire/index.html +0 -116
  201. package/template/.build/guides/ecrire.md +0 -63
  202. package/template/.build/guides/navigation/index.html +0 -90
  203. package/template/.build/guides/navigation.md +0 -63
  204. package/template/.build/guides/publier/index.html +0 -95
  205. package/template/.build/guides/publier.md +0 -65
  206. package/template/.build/guides/theme/index.html +0 -87
  207. package/template/.build/guides/theme.md +0 -33
  208. package/template/.build/images/favicon.svg +0 -1
  209. package/template/.build/images/logo-clair.svg +0 -1
  210. package/template/.build/images/logo-sombre.svg +0 -1
  211. package/template/.build/pagefind/fragment/fr-ch_2a431a9.pf_fragment +0 -0
  212. package/template/.build/pagefind/fragment/fr-ch_456586a.pf_fragment +0 -0
  213. package/template/.build/pagefind/fragment/fr-ch_4f8575c.pf_fragment +0 -0
  214. package/template/.build/pagefind/fragment/fr-ch_68e734b.pf_fragment +0 -0
  215. package/template/.build/pagefind/fragment/fr-ch_72b91f4.pf_fragment +0 -0
  216. package/template/.build/pagefind/fragment/fr-ch_8f256dd.pf_fragment +0 -0
  217. package/template/.build/pagefind/fragment/fr-ch_95a15cc.pf_fragment +0 -0
  218. package/template/.build/pagefind/fragment/fr-ch_aa80c55.pf_fragment +0 -0
  219. package/template/.build/pagefind/fragment/fr-ch_ab79738.pf_fragment +0 -0
  220. package/template/.build/pagefind/fragment/fr-ch_cfa4336.pf_fragment +0 -0
  221. package/template/.build/pagefind/fragment/fr-ch_dab8f5e.pf_fragment +0 -0
  222. package/template/.build/pagefind/fragment/fr-ch_de60b43.pf_fragment +0 -0
  223. package/template/.build/pagefind/fragment/fr-ch_e02aa49.pf_fragment +0 -0
  224. package/template/.build/pagefind/fragment/fr-ch_e254a54.pf_fragment +0 -0
  225. package/template/.build/pagefind/fragment/fr-ch_e2c46e3.pf_fragment +0 -0
  226. package/template/.build/pagefind/index/fr-ch_a71411b.pf_index +0 -0
  227. package/template/.build/pagefind/pagefind.fr-ch_2f3ea8fccf961.pf_meta +0 -0
  228. package/template/.build/reference/api-assistant/index.html +0 -161
  229. package/template/.build/reference/api-assistant.md +0 -70
  230. package/template/.build/reference/docs-json/index.html +0 -192
  231. package/template/.build/reference/docs-json.md +0 -43
  232. package/template/.build/reference/mcp/index.html +0 -151
  233. package/template/.build/reference/mcp.md +0 -49
@@ -0,0 +1,276 @@
1
+ import { compile } from "@mdx-js/mdx"
2
+ import type { Root } from "mdast"
3
+ import remarkGfm from "remark-gfm"
4
+ import { parse as parseYaml } from "yaml"
5
+
6
+ import { isMdxComponent } from "./components.ts"
7
+ import { parseDocsConfig } from "./config.ts"
8
+ import { missingPages, normalizeNavigation, pageToSlug } from "./navigation.ts"
9
+
10
+ /**
11
+ * Ce que le build refuserait, vérifié avant de publier : l'éditeur de la console bloque
12
+ * la publication sur la moindre erreur, avec la ligne à corriger. Même compilateur MDX
13
+ * que le build (`@mdx-js/mdx`, GFM), même schéma de `docs.json`, même règle de pages.
14
+ */
15
+ export type Problem = { file: string; line: number | null; column: number | null; message: string }
16
+
17
+ /** Une page de la doc : `.mdx` ou `.md` à l'intérieur du dossier. */
18
+ export const PAGE_FILE = /\.(mdx|md)$/i
19
+
20
+ const IMAGE_FILE = /\.(png|jpe?g|gif|svg|webp|avif|ico)$/i
21
+
22
+ /**
23
+ * Fichier que l'éditeur peut écrire, relatif au dossier de la doc : `docs.json`, pages et
24
+ * images. Jamais un dossier caché ni la configuration du build (même règle que Host,
25
+ * `isEditablePath` d'agenthost, qui revérifie).
26
+ */
27
+ export function isDocFile(path: string): boolean {
28
+ if (path.length === 0 || path.length > 300) return false
29
+ if (path.startsWith("/") || path.endsWith("/") || path.includes("\\")) return false
30
+ const segments = path.split("/")
31
+ if (segments.some((s) => s === "" || s === "." || s === ".." || s.startsWith("."))) return false
32
+ if (segments.includes("node_modules") || segments[0] === "dist") return false
33
+ if (!/^[\p{L}\p{N} ._/()+@-]+$/u.test(path)) return false
34
+ return path === "docs.json" || PAGE_FILE.test(path) || IMAGE_FILE.test(path)
35
+ }
36
+
37
+ export function isImageFile(path: string): boolean {
38
+ return IMAGE_FILE.test(path)
39
+ }
40
+
41
+ /** Fichiers Markdown du dossier qui ne sont pas des pages (`template/src/content.config.ts`). */
42
+ const NOT_PAGES = new Set(["README.md", "CHANGELOG.md"])
43
+
44
+ /**
45
+ * Une page du site : `.mdx` ou `.md`, sauf un extrait (`_nom.mdx`, inclus par d'autres pages)
46
+ * et le README ou le CHANGELOG à la racine, que le build ignore.
47
+ */
48
+ export function isPageFile(file: string): boolean {
49
+ return PAGE_FILE.test(file) && !NOT_PAGES.has(file) && !(file.split("/").at(-1) ?? "").startsWith("_")
50
+ }
51
+
52
+ /** `guides/publier.mdx` → `guides/publier` : le chemin de page de `docs.json` ; `null` pour un autre fichier. */
53
+ export function pageOfFile(file: string): string | null {
54
+ return isPageFile(file) ? file.replace(PAGE_FILE, "") : null
55
+ }
56
+
57
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n?---[ \t]*(?:\r?\n|$)/
58
+
59
+ export type Frontmatter = { raw: string | null; body: string; lines: number }
60
+
61
+ /** En-tête YAML d'une page et corps, avec le nombre de lignes de l'en-tête (pour garder les numéros de ligne). */
62
+ export function splitFrontmatter(source: string): Frontmatter {
63
+ const match = FRONTMATTER.exec(source)
64
+ if (!match) return { raw: null, body: source, lines: 0 }
65
+ const header = match[0]
66
+ const lines = header.split("\n").length - (header.endsWith("\n") ? 1 : 0)
67
+ return { raw: match[1] ?? "", body: source.slice(header.length), lines }
68
+ }
69
+
70
+ /** Lit l'en-tête d'une page : objet YAML, ou `null` s'il manque ou ne se lit pas. */
71
+ export function readFrontmatter(source: string): Record<string, unknown> | null {
72
+ const { raw } = splitFrontmatter(source)
73
+ if (raw === null) return null
74
+ try {
75
+ const data: unknown = parseYaml(raw)
76
+ return data && typeof data === "object" && !Array.isArray(data) ? (data as Record<string, unknown>) : null
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ type Position = { start?: { line?: number; column?: number } }
83
+ type MdxNode = { type: string; name?: string | null; position?: Position; children?: Array<MdxNode> }
84
+
85
+ /** Composants appelés par la page sans exister : le build échouerait au rendu. */
86
+ function unknownComponents(problems: Array<Problem>, file: string) {
87
+ return () => (tree: Root) => {
88
+ const declared = new Set<string>()
89
+ const walk = (node: MdxNode) => {
90
+ if (node.type === "mdxjsEsm") {
91
+ const value = (node as unknown as { value: string }).value
92
+ for (const match of value.matchAll(/import\s+(?:\{([^}]*)\}|([A-Za-z_$][\w$]*))/g)) {
93
+ for (const name of (match[1] ?? match[2] ?? "").split(","))
94
+ declared.add(
95
+ name
96
+ .split(/\s+as\s+/)
97
+ .pop()!
98
+ .trim(),
99
+ )
100
+ }
101
+ for (const match of value.matchAll(/export\s+(?:const|function)\s+([A-Za-z_$][\w$]*)/g)) declared.add(match[1]!)
102
+ }
103
+ for (const child of node.children ?? []) walk(child)
104
+ }
105
+ walk(tree as unknown as MdxNode)
106
+ const check = (node: MdxNode) => {
107
+ if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name) {
108
+ const root = node.name.split(".")[0]!
109
+ if (/^[A-Z]/.test(root) && !isMdxComponent(root) && !declared.has(root))
110
+ problems.push({
111
+ file,
112
+ line: node.position?.start?.line ?? null,
113
+ column: node.position?.start?.column ?? null,
114
+ message: `Le composant <${node.name}> n'existe pas. Composants disponibles : <Note>, <Tip>, <Card>, <Steps>…`,
115
+ })
116
+ }
117
+ for (const child of node.children ?? []) check(child)
118
+ }
119
+ check(tree as unknown as MdxNode)
120
+ }
121
+ }
122
+
123
+ const TRANSLATIONS: Array<[RegExp, (m: RegExpMatchArray) => string]> = [
124
+ [/Expected a closing tag for `<([^>]+)>`/, (m) => `La balise <${m[1]}> n'est jamais fermée.`],
125
+ [
126
+ /Unexpected closing slash `\/` in tag, expected an open tag first/,
127
+ () => "Balise fermante sans balise ouvrante correspondante.",
128
+ ],
129
+ [
130
+ /Unexpected closing tag `<\/([^>]+)>`, expected corresponding closing tag for `<([^>]+)>`/,
131
+ (m) => `</${m[1]}> ferme la mauvaise balise : <${m[2]}> est encore ouverte.`,
132
+ ],
133
+ [
134
+ /Expected the closing tag `<\/([^>]+)>` either after the end of/,
135
+ (m) =>
136
+ `</${m[1]}> est mal placée : ouvre et ferme <${m[1]}> sur leurs propres lignes, ou sur la même ligne que le texte.`,
137
+ ],
138
+ [
139
+ /Could not parse expression with acorn/,
140
+ () => "Expression entre accolades illisible : échappe « { » par « \\{ » si c'est du texte.",
141
+ ],
142
+ [/Could not parse import\/exports with acorn/, () => "Ligne import ou export illisible."],
143
+ [
144
+ /Unexpected end of file in expression, expected a corresponding closing brace for `\{`/,
145
+ () => "Accolade « { » jamais fermée : échappe-la par « \\{ » si c'est du texte.",
146
+ ],
147
+ [
148
+ /Unexpected character `(.)` \(U\+[0-9A-F]+\) before name/,
149
+ (m) => `Caractère « ${m[1]} » inattendu après « < » : écris « &lt; » pour un chevron dans le texte.`,
150
+ ],
151
+ [/Unexpected lazy line in/, () => "Ligne mal indentée à l'intérieur d'un bloc (citation, liste ou composant)."],
152
+ ]
153
+
154
+ function translate(reason: string): string {
155
+ for (const [pattern, render] of TRANSLATIONS) {
156
+ const match = reason.match(pattern)
157
+ if (match) return render(match)
158
+ }
159
+ return `Syntaxe MDX invalide : ${reason}`
160
+ }
161
+
162
+ type Point = { line?: number; column?: number }
163
+ type VFileMessageLike = {
164
+ reason?: string
165
+ message?: string
166
+ line?: number | null
167
+ column?: number | null
168
+ place?: (Point & { start?: Point }) | null
169
+ }
170
+
171
+ /** Position d'une erreur de MDX : son point, sinon celui cité dans le message (`(7:1-7:7)`). */
172
+ function positionOf(failure: VFileMessageLike, reason: string): { line: number | null; column: number | null } {
173
+ const point = failure.place?.start ?? failure.place
174
+ if (failure.line) return { line: failure.line, column: failure.column ?? null }
175
+ if (point?.line) return { line: point.line, column: point.column ?? null }
176
+ const cited = reason.match(/\((\d+):(\d+)/)
177
+ return cited ? { line: Number(cited[1]), column: Number(cited[2]) } : { line: null, column: null }
178
+ }
179
+
180
+ /** En-tête lu ; `undefined` (et une erreur notée) s'il ne se lit pas. */
181
+ function checkYaml(problems: Array<Problem>, file: string, raw: string): unknown {
182
+ try {
183
+ return parseYaml(raw)
184
+ } catch (error) {
185
+ const position = (error as { linePos?: Array<{ line: number; col: number }> }).linePos?.[0]
186
+ problems.push({
187
+ file,
188
+ line: position ? position.line + 1 : 1,
189
+ column: position?.col ?? null,
190
+ message: "En-tête illisible : vérifie les deux-points et les guillemets (YAML).",
191
+ })
192
+ return undefined
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Vérifie une page : en-tête YAML avec un `title` (Starlight l'exige), compilation MDX
198
+ * comme au build, composants connus. Les numéros de ligne sont ceux du fichier entier.
199
+ */
200
+ export async function checkPage(file: string, source: string): Promise<Array<Problem>> {
201
+ const problems: Array<Problem> = []
202
+ const front = splitFrontmatter(source)
203
+ // Un extrait ou le README n'est pas une page : seule la syntaxe compte, pas l'en-tête.
204
+ if (!isPageFile(file)) {
205
+ if (front.raw !== null) checkYaml(problems, file, front.raw)
206
+ } else if (front.raw === null) {
207
+ problems.push({
208
+ file,
209
+ line: 1,
210
+ column: 1,
211
+ message: "Il manque l'en-tête de la page : commence par ---, title: Le titre, puis ---.",
212
+ })
213
+ } else {
214
+ const data = checkYaml(problems, file, front.raw)
215
+ const title = data && typeof data === "object" ? (data as Record<string, unknown>).title : undefined
216
+ if (data !== undefined && (typeof title !== "string" || !title.trim()))
217
+ problems.push({
218
+ file,
219
+ line: 2,
220
+ column: 1,
221
+ message: "L'en-tête doit donner un titre : title: Le titre de la page.",
222
+ })
223
+ }
224
+
225
+ // Les lignes de l'en-tête restent, vides : les positions de MDX tombent sur la bonne ligne.
226
+ const body = "\n".repeat(front.lines) + front.body
227
+ try {
228
+ await compile(body, {
229
+ format: file.toLowerCase().endsWith(".md") ? "md" : "mdx",
230
+ remarkPlugins: [remarkGfm, unknownComponents(problems, file)],
231
+ })
232
+ } catch (error) {
233
+ const failure = error as VFileMessageLike
234
+ const reason = String(failure.reason ?? failure.message ?? error)
235
+ problems.push({ file, ...positionOf(failure, reason), message: translate(reason) })
236
+ }
237
+ return problems.sort((a, b) => (a.line ?? 0) - (b.line ?? 0))
238
+ }
239
+
240
+ /**
241
+ * Vérifie `docs.json` et sa navigation contre les fichiers de la doc : schéma, une seule
242
+ * forme de navigation, et chaque page citée présente dans le dossier.
243
+ */
244
+ export function checkSite(docsJson: string, files: Iterable<string>): Array<Problem> {
245
+ const parsed = parseDocsConfig(docsJson)
246
+ if (!parsed.ok) return parsed.errors.map((message) => ({ file: "docs.json", line: null, column: null, message }))
247
+ const existing = new Set<string>()
248
+ for (const file of files) {
249
+ const page = pageOfFile(file)
250
+ if (page) existing.add(pageToSlug(page))
251
+ }
252
+ const problems: Array<Problem> = missingPages(normalizeNavigation(parsed.config), existing).map((page) => ({
253
+ file: "docs.json",
254
+ line: null,
255
+ column: null,
256
+ message: `La navigation cite la page ${page}, mais ${page}.mdx n'existe pas.`,
257
+ }))
258
+ const slug = (path: string) =>
259
+ path
260
+ .split(/[?#]/)[0]!
261
+ .replace(/^\/+|\/+$/g, "")
262
+ .toLowerCase()
263
+ const sources = new Set<string>()
264
+ for (const redirect of parsed.config.redirects ?? []) {
265
+ const source = slug(redirect.source)
266
+ const say = (message: string) => problems.push({ file: "docs.json", line: null, column: null, message })
267
+ if (sources.has(source)) say(`Deux redirections partent de ${redirect.source}.`)
268
+ sources.add(source)
269
+ // Une redirection depuis une page qui existe la cacherait : le build écrirait les deux au même endroit.
270
+ if (existing.has(source))
271
+ say(`La redirection ${redirect.source} masque une page qui existe : retire-la ou supprime la page.`)
272
+ if (redirect.destination.startsWith("/") && !existing.has(slug(redirect.destination)))
273
+ say(`La redirection ${redirect.source} mène à ${redirect.destination}, une page qui n'existe pas.`)
274
+ }
275
+ return problems
276
+ }
@@ -8,9 +8,10 @@ import type { AstroIntegration, AstroUserConfig } from "astro"
8
8
 
9
9
  import { remarkAutoImport } from "./build/auto-import.ts"
10
10
  import { remarkBaseLinks } from "./build/base-links.ts"
11
+ import { rehypeDocsContent } from "./build/content.ts"
11
12
  import { publicFiles } from "./build/public-files.ts"
12
13
  import { writeMachineReadable } from "./build/outputs.ts"
13
- import { parseDocsConfig, withHostProject, type DocsConfig } from "./core/config.ts"
14
+ import { parseDocsConfig, socialNetwork, withHostProject, type DocsConfig } from "./core/config.ts"
14
15
  import {
15
16
  missingPages,
16
17
  normalizeNavigation,
@@ -95,9 +96,12 @@ function collectPublic(contentDir: string, declared: ReadonlyArray<string>): str
95
96
  return target
96
97
  }
97
98
 
98
- /** Pages du dépôt (`guides/publier`) et leur `sidebarTitle` éventuel (champ Mintlify). */
99
- function scanPages(contentDir: string): Map<string, string | null> {
100
- const pages = new Map<string, string | null>()
99
+ /** Ce que le frontmatter d'une page change dans la barre : `sidebarTitle` et `tag` (champs Mintlify). */
100
+ type PageMeta = { label: string | null; tag: string | null }
101
+
102
+ /** Pages du dépôt (`guides/publier`) et leurs réglages de barre latérale. */
103
+ function scanPages(contentDir: string): Map<string, PageMeta> {
104
+ const pages = new Map<string, PageMeta>()
101
105
  const walk = (dir: string) => {
102
106
  for (const name of readdirSync(dir)) {
103
107
  if (name.startsWith(".") || IGNORED_DIRS.has(name)) continue
@@ -108,8 +112,9 @@ function scanPages(contentDir: string): Map<string, string | null> {
108
112
  .replaceAll("\\", "/")
109
113
  .replace(/\.mdx?$/, "")
110
114
  const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync(path, "utf8"))?.[1] ?? ""
111
- const label = /^sidebarTitle:\s*["']?(.+?)["']?\s*$/m.exec(frontmatter)?.[1] ?? null
112
- pages.set(pageToSlug(page), label)
115
+ const field = (name: string) =>
116
+ new RegExp(`^${name}:\\s*["']?(.+?)["']?\\s*$`, "m").exec(frontmatter)?.[1] ?? null
117
+ pages.set(pageToSlug(page), { label: field("sidebarTitle"), tag: field("tag") })
113
118
  }
114
119
  }
115
120
  }
@@ -117,15 +122,21 @@ function scanPages(contentDir: string): Map<string, string | null> {
117
122
  return pages
118
123
  }
119
124
 
120
- type SidebarItem = { slug: string; label?: string } | { label: string; collapsed: boolean; items: Array<SidebarItem> }
125
+ type SidebarItem =
126
+ | { slug: string; label?: string; badge?: { text: string; variant: "note" } }
127
+ | { label: string; collapsed: boolean; items: Array<SidebarItem> }
121
128
 
122
- function sidebarItems(items: Array<NavItem>, labels: ReadonlyMap<string, string | null>): Array<SidebarItem> {
129
+ function sidebarItems(items: Array<NavItem>, labels: ReadonlyMap<string, PageMeta>): Array<SidebarItem> {
123
130
  return items.map((item) => {
124
131
  if (item.type === "group")
125
132
  return { label: item.label, collapsed: !item.expanded, items: sidebarItems(item.items, labels) }
126
133
  const slug = pageToSlug(item.page)
127
- const label = labels.get(slug)
128
- return label ? { slug, label } : { slug }
134
+ const meta = labels.get(slug)
135
+ return {
136
+ slug,
137
+ ...(meta?.label ? { label: meta.label } : {}),
138
+ ...(meta?.tag ? { badge: { text: meta.tag.slice(0, 16), variant: "note" as const } } : {}),
139
+ }
129
140
  })
130
141
  }
131
142
 
@@ -133,10 +144,7 @@ function sidebarItems(items: Array<NavItem>, labels: ReadonlyMap<string, string
133
144
  * Une entrée de premier niveau par couple (version, onglet), dans l'ordre de l'arbre ;
134
145
  * `route-data.ts` ne garde ensuite que celle de la page affichée.
135
146
  */
136
- export function starlightSidebar(
137
- tree: NavTree,
138
- labels: ReadonlyMap<string, string | null> = new Map(),
139
- ): Array<SidebarItem> {
147
+ export function starlightSidebar(tree: NavTree, labels: ReadonlyMap<string, PageMeta> = new Map()): Array<SidebarItem> {
140
148
  const sections = tree.versions.flatMap((version) => version.tabs)
141
149
  if (sections.length === 1) return sidebarItems(sections[0]!.items, labels)
142
150
  return tree.versions.flatMap((version) =>
@@ -148,17 +156,27 @@ export function starlightSidebar(
148
156
  )
149
157
  }
150
158
 
151
- const SOCIAL_ICONS = new Set([
152
- "github",
153
- "gitlab",
154
- "linkedin",
155
- "x.com",
156
- "mastodon",
157
- "discord",
158
- "youtube",
159
- "instagram",
160
- "bluesky",
161
- ])
159
+ /**
160
+ * `redirects` de docs.json en redirections d'Astro : une page qui renvoie à la nouvelle
161
+ * adresse (le site est statique). Chemins écrits comme les liens de la doc, `base` ajouté.
162
+ */
163
+ export function astroRedirects(
164
+ redirects: ReadonlyArray<{ source: string; destination: string; permanent?: boolean | undefined }>,
165
+ base: string,
166
+ ): Record<string, { status: 301 | 302; destination: string }> {
167
+ const slash = (path: string) => (path.endsWith("/") ? path : `${path}/`)
168
+ return Object.fromEntries(
169
+ redirects.map((item) => [
170
+ slash(item.source),
171
+ {
172
+ status: item.permanent === false ? 302 : 301,
173
+ destination: item.destination.startsWith("/")
174
+ ? `${base}${slash(item.destination.split("#")[0]!)}${item.destination.includes("#") ? `#${item.destination.split("#")[1]}` : ""}`
175
+ : item.destination,
176
+ },
177
+ ]),
178
+ )
179
+ }
162
180
 
163
181
  function virtualConfigPlugin(data: VirtualConfig) {
164
182
  const id = "virtual:hgdc-docs/config"
@@ -215,9 +233,10 @@ export function createAstroConfig(options: SiteOptions = readSiteOptions()): Ast
215
233
  const theme = themeFor(config.colors?.primary, config.colors?.dark)
216
234
  const logo = typeof config.logo === "string" ? { light: config.logo } : config.logo
217
235
  const asset = (path: string) => join(options.contentDir, path.replace(/^\//, ""))
218
- const social = Object.entries(config.footer?.socials ?? {})
219
- .filter(([icon]) => SOCIAL_ICONS.has(icon))
220
- .map(([icon, href]) => ({ icon: (icon === "x.com" ? "x.com" : icon) as "github", label: icon, href }))
236
+ const social = Object.entries(config.footer?.socials ?? {}).flatMap(([key, href]) => {
237
+ const network = socialNetwork(key)
238
+ return network ? [{ icon: network.icon as "github", label: network.label, href }] : []
239
+ })
221
240
 
222
241
  return {
223
242
  root: file("../template"),
@@ -231,7 +250,14 @@ export function createAstroConfig(options: SiteOptions = readSiteOptions()): Ast
231
250
  site: options.site ?? undefined,
232
251
  ...(base ? { base } : {}),
233
252
  trailingSlash: "always",
234
- markdown: { processor: unified({ remarkPlugins: [remarkAutoImport, [remarkBaseLinks, { base }]] }) },
253
+ devToolbar: { enabled: false },
254
+ redirects: astroRedirects(config.redirects ?? [], base),
255
+ markdown: {
256
+ processor: unified({
257
+ remarkPlugins: [remarkAutoImport, [remarkBaseLinks, { base }]],
258
+ rehypePlugins: [rehypeDocsContent],
259
+ }),
260
+ },
235
261
  vite: {
236
262
  plugins: [virtualConfigPlugin(data)],
237
263
  server: { fs: { allow: [options.contentDir, file("..")] } },
@@ -252,11 +278,57 @@ export function createAstroConfig(options: SiteOptions = readSiteOptions()): Ast
252
278
  routeMiddleware: file("./overrides/route-data.ts"),
253
279
  components: {
254
280
  Head: file("./overrides/Head.astro"),
281
+ Header: file("./overrides/Header.astro"),
282
+ ThemeProvider: file("./overrides/ThemeProvider.astro"),
283
+ Search: file("./overrides/Search.astro"),
255
284
  Sidebar: file("./overrides/Sidebar.astro"),
256
285
  PageTitle: file("./overrides/PageTitle.astro"),
257
286
  PageFrame: file("./overrides/PageFrame.astro"),
287
+ PageSidebar: file("./overrides/PageSidebar.astro"),
288
+ TwoColumnContent: file("./overrides/TwoColumnContent.astro"),
289
+ Footer: file("./overrides/Footer.astro"),
290
+ },
291
+ expressiveCode: {
292
+ // Clair d'abord (thème par défaut du site) ; le sombre suit `data-hd-theme` (ThemeProvider).
293
+ themes: ["github-light-default", "github-dark-default"],
294
+ useDarkModeMediaQuery: false,
295
+ themeCssSelector: (theme) => (theme.type === "dark" ? '[data-hd-theme="dark"]' : false),
296
+ // Blocs dessinés par theme.css (filet, en-tête, actions) : un cadre unique pour le
297
+ // shell comme pour le reste. Le bouton Copier d'Expressive Code reste dans le HTML,
298
+ // caché : son `data-code` porte le texte exact (build/markdown.ts, widget/code.ts).
299
+ defaultProps: { frame: "code" },
300
+ styleOverrides: {
301
+ borderRadius: "1rem",
302
+ borderColor: "var(--hd-border)",
303
+ borderWidth: "1px",
304
+ codeBackground: "var(--hd-code-bg)",
305
+ codeFontFamily: "var(--hd-font-mono)",
306
+ codeFontSize: "0.8125rem",
307
+ codeLineHeight: "1.7",
308
+ codePaddingBlock: "0.875rem",
309
+ codePaddingInline: "1rem",
310
+ uiFontFamily: "var(--hd-font-sans)",
311
+ uiFontSize: "0.8125rem",
312
+ frames: {
313
+ frameBoxShadowCssValue: "none",
314
+ editorTabBarBackground: "var(--hd-code-header)",
315
+ editorTabBarBorderBottomColor: "var(--hd-border)",
316
+ editorActiveTabBackground: "transparent",
317
+ editorActiveTabForeground: "var(--hd-fg)",
318
+ editorActiveTabBorderColor: "transparent",
319
+ editorActiveTabIndicatorTopColor: "transparent",
320
+ editorActiveTabIndicatorBottomColor: "var(--hd-accent)",
321
+ editorActiveTabIndicatorHeight: "2px",
322
+ editorTabBorderRadius: "0",
323
+ editorBackground: "var(--hd-code-bg)",
324
+ terminalBackground: "var(--hd-code-bg)",
325
+ terminalTitlebarBackground: "var(--hd-code-header)",
326
+ terminalTitlebarBorderBottomColor: "var(--hd-border)",
327
+ terminalTitlebarForeground: "var(--hd-muted-fg)",
328
+ terminalTitlebarDotsOpacity: "0",
329
+ },
330
+ },
258
331
  },
259
- expressiveCode: { themes: ["github-dark-default", "github-light-default"] },
260
332
  pagination: true,
261
333
  }),
262
334
  machineReadableOutputs(data, options),
@@ -1,39 +1,25 @@
1
1
  ---
2
2
  import data from "virtual:hgdc-docs/config"
3
3
 
4
- // Widget de l'assistant : panneau latéral non modal (la page reste lisible), plein écran
5
- // sur téléphone. Logique dans `../widget/client.ts`.
4
+ import Icon from "./Icon.astro"
5
+
6
+ // Assistant. Fermé : une barre « Pose une question… » au bas du contenu. Ouvert : un
7
+ // panneau à droite qui pousse la page (une feuille qui monte du bas sur téléphone).
8
+ // Logique dans `../widget/client.ts`.
6
9
  const endpoint = new URL("/api/v1/ask", data.config.assistant?.endpoint ?? data.apiEndpoint).toString()
7
10
  ---
8
11
 
9
12
  <hgdc-assistant data-endpoint={endpoint} data-project={data.config.project} data-name={data.config.name}>
10
- <button type="button" class="hgdc-launcher" data-open aria-haspopup="dialog" aria-expanded="false">
11
- <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18">
12
- <path d="M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9z" fill="currentColor"></path>
13
- <path d="M18.5 15l.8 1.9 1.9.8-1.9.8-.8 1.9-.8-1.9-1.9-.8 1.9-.8z" fill="currentColor"></path>
14
- </svg>
15
- <span>Demander à l'IA</span>
16
- <kbd class="hgdc-kbd" data-shortcut aria-hidden="true">Ctrl I</kbd>
17
- </button>
18
-
19
- <section class="hgdc-panel" role="dialog" aria-modal="false" aria-labelledby="hgdc-assistant-title" hidden>
13
+ <section class="hgdc-panel" role="dialog" aria-modal="false" aria-labelledby="hgdc-assistant-title" data-panel inert>
14
+ <div class="hgdc-panel__grip" aria-hidden="true"></div>
20
15
  <header class="hgdc-panel__header">
21
- <div>
22
- <h2 id="hgdc-assistant-title">Assistant</h2>
23
- <p>Répond à partir de cette documentation, avec ses sources.</p>
24
- </div>
16
+ <h2 id="hgdc-assistant-title"><Icon name="sparkles" />Assistant</h2>
25
17
  <div class="hgdc-panel__tools">
26
- <button type="button" class="hgdc-icon-button" data-reset aria-label="Nouvelle conversation" hidden>
27
- <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18"
28
- ><path d="M4 12a8 8 0 1 0 2.3-5.7M4 4v4h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
29
- ></path></svg
30
- >
18
+ <button type="button" class="hd-icon-button" data-reset aria-label="Nouvelle conversation" title="Nouvelle conversation" hidden>
19
+ <Icon name="rotate" />
31
20
  </button>
32
- <button type="button" class="hgdc-icon-button" data-close aria-label="Fermer l'assistant">
33
- <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18"
34
- ><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
35
- ></path></svg
36
- >
21
+ <button type="button" class="hd-icon-button" data-close aria-label="Fermer l'assistant" title="Fermer (Échap)">
22
+ <Icon name="x" />
37
23
  </button>
38
24
  </div>
39
25
  </header>
@@ -41,26 +27,18 @@ const endpoint = new URL("/api/v1/ask", data.config.assistant?.endpoint ?? data.
41
27
  <div class="hgdc-panel__body" data-log aria-live="polite">
42
28
  <div class="hgdc-empty" data-empty>
43
29
  <p class="hgdc-empty__title">Pose ta question sur {data.config.name}.</p>
44
- <p>
45
- Je cherche dans les pages de cette documentation et je cite mes sources. Si la réponse n'y est pas, je te le
46
- dis.
47
- </p>
30
+ <p>Je cherche dans les pages de cette documentation et je cite mes sources. Si la réponse n'y est pas, je te le dis.</p>
48
31
  </div>
49
32
  </div>
50
33
 
51
34
  <form class="hgdc-panel__form" data-form>
52
35
  <label class="sr-only" for="hgdc-question">Ta question</label>
53
- <textarea id="hgdc-question" data-input rows="1" maxlength="1000" placeholder="Comment publier ma doc ?" required
54
- ></textarea>
55
- <button type="submit" class="hgdc-send" data-send aria-label="Envoyer la question">
56
- <svg aria-hidden="true" viewBox="0 0 24 24" width="18" height="18"
57
- ><path d="M12 19V5M5 12l7-7 7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
58
- ></path></svg
59
- >
60
- </button>
36
+ <textarea id="hgdc-question" data-input rows="1" maxlength="1000" placeholder="Pose une question…" required></textarea>
37
+ <button type="submit" class="hd-send" data-send aria-label="Envoyer la question"><Icon name="arrow-up" size={16} /></button>
61
38
  </form>
62
39
  <p class="hgdc-panel__note">
63
- Réponses générées par IA (Claude, d'Anthropic), à vérifier. Les questions sont conservées 90 jours pour améliorer cette documentation.
40
+ Réponses générées par IA (Claude, d'Anthropic), à vérifier. Les questions sont conservées 90 jours pour améliorer
41
+ cette documentation.
64
42
  </p>
65
43
  </section>
66
44
  </hgdc-assistant>