@hgdc/docs 0.1.1 → 0.3.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/docs.schema.json +25 -0
- package/package.json +3 -1
- package/src/build/auto-import.ts +5 -24
- package/src/build/base-links.ts +35 -0
- package/src/build/outputs.ts +17 -4
- package/src/core/components.ts +166 -0
- package/src/core/config.ts +50 -0
- package/src/core/feedback-api.ts +17 -0
- package/src/core/llms.ts +8 -5
- package/src/core/navigation.ts +24 -0
- package/src/core/validate.ts +276 -0
- package/src/integration.ts +71 -21
- package/src/overrides/Footer.astro +154 -0
- package/src/overrides/Head.astro +4 -2
- package/src/overrides/PageTitle.astro +3 -2
- package/src/overrides/Sidebar.astro +2 -2
- package/src/overrides/SocialIcons.astro +59 -0
- package/template/.astro/content-assets.mjs +4 -1
- package/template/.build/.well-known/llms.txt +5 -5
- package/template/.build/404.html +5 -5
- package/template/.build/_astro/{common.8GHCkW7i.css → common.CQYUsl_5.css} +2 -2
- package/template/.build/_hgdc/corpus.json +1 -1
- package/template/.build/demarrer/premiere-doc/index.html +12 -12
- package/template/.build/demarrer/premiere-doc.md +1 -1
- package/template/.build/explications/donnees/index.html +11 -11
- package/template/.build/explications/donnees.md +1 -1
- package/template/.build/explications/fonctionnement/index.html +12 -12
- package/template/.build/explications/fonctionnement.md +3 -3
- package/template/.build/guides/agents/index.html +15 -10
- package/template/.build/guides/agents.md +15 -1
- package/template/.build/guides/assistant/index.html +14 -12
- package/template/.build/guides/assistant.md +5 -1
- package/template/.build/guides/composants/index.html +15 -15
- package/template/.build/guides/ecrire/index.html +12 -12
- package/template/.build/guides/ecrire.md +1 -1
- package/template/.build/guides/elements/index.html +1 -0
- package/template/.build/guides/navigation/index.html +11 -11
- package/template/.build/guides/publier/index.html +16 -16
- package/template/.build/guides/publier.md +8 -8
- package/template/.build/guides/theme/index.html +11 -11
- package/template/.build/guides/theme.md +1 -1
- package/template/.build/index.html +11 -11
- package/template/.build/index.md +5 -5
- package/template/.build/llms-full.txt +69 -47
- package/template/.build/llms.txt +5 -5
- package/template/.build/pagefind/fragment/fr-ch_2a431a9.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_35f556a.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_456586a.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_48ca71e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_72b91f4.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_8f256dd.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_95a15cc.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_aa80c55.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_ab79738.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_c589c2a.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_dab8f5e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_de60b43.pf_fragment +0 -0
- package/template/.build/pagefind/index/fr-ch_c5ced52.pf_index +0 -0
- package/template/.build/pagefind/pagefind-entry.json +1 -1
- package/template/.build/pagefind/pagefind.fr-ch_8a246aa922758.pf_meta +0 -0
- package/template/.build/reference/api-assistant/index.html +11 -11
- package/template/.build/reference/api-assistant.md +3 -3
- package/template/.build/reference/cli/index.html +11 -11
- package/template/.build/reference/cli.md +5 -5
- package/template/.build/reference/docs-json/index.html +35 -11
- package/template/.build/reference/docs-json.md +19 -15
- package/template/.build/reference/mcp/index.html +10 -10
- package/template/node_modules/.vite/deps/_metadata.json +8 -8
- package/template/.build/pagefind/fragment/fr-ch_424fd16.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_5a4d77d.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_6c36edc.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_73d7887.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_8234ed3.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_8f8a19e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_9f1b981.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_a19039d.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_d72cc1e.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_d732215.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e78d58b.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e92f972.pf_fragment +0 -0
- package/template/.build/pagefind/index/fr-ch_f94c95d.pf_index +0 -0
- package/template/.build/pagefind/pagefind.fr-ch_fb78ab42961f4.pf_meta +0 -0
package/docs.schema.json
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"type": "string",
|
|
24
24
|
"enum": ["fr", "en", "de", "it"]
|
|
25
25
|
},
|
|
26
|
+
"base": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^(?:\\/[a-z0-9][a-z0-9_-]*)+$"
|
|
29
|
+
},
|
|
26
30
|
"colors": {
|
|
27
31
|
"type": "object",
|
|
28
32
|
"properties": {
|
|
@@ -244,6 +248,27 @@
|
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
},
|
|
251
|
+
"redirects": {
|
|
252
|
+
"maxItems": 500,
|
|
253
|
+
"type": "array",
|
|
254
|
+
"items": {
|
|
255
|
+
"type": "object",
|
|
256
|
+
"properties": {
|
|
257
|
+
"source": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"pattern": "^\\/(?!\\/)[A-Za-z0-9._~\\-/%]*$"
|
|
260
|
+
},
|
|
261
|
+
"destination": {
|
|
262
|
+
"type": "string",
|
|
263
|
+
"minLength": 1
|
|
264
|
+
},
|
|
265
|
+
"permanent": {
|
|
266
|
+
"type": "boolean"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"required": ["source", "destination"]
|
|
270
|
+
}
|
|
271
|
+
},
|
|
247
272
|
"assistant": {
|
|
248
273
|
"type": "object",
|
|
249
274
|
"properties": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Modèle de site de HGDC Docs : documentation MDX (docs.json) construite avec Astro Starlight, recherche, versions, llms.txt et assistant.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@astrojs/starlight": "0.42.6",
|
|
32
32
|
"@fontsource-variable/geist": "5.3.0",
|
|
33
33
|
"@fontsource-variable/inter": "5.3.0",
|
|
34
|
+
"@mdx-js/mdx": "3.1.1",
|
|
34
35
|
"astro": "7.3.8",
|
|
35
36
|
"github-slugger": "2.0.0",
|
|
36
37
|
"hast-util-select": "6.0.4",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"remark-stringify": "11.0.0",
|
|
42
43
|
"unified": "11.0.5",
|
|
43
44
|
"vfile": "6.0.3",
|
|
45
|
+
"yaml": "2.9.1",
|
|
44
46
|
"zod": "4.6.5"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
package/src/build/auto-import.ts
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
import type { Root } from "mdast"
|
|
2
2
|
import type { VFile } from "vfile"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
* Composants disponibles dans chaque page MDX sans `import`, comme chez Mintlify
|
|
6
|
-
* (`<Note>`, `<Steps>`, `<Card>`…). Le plugin ajoute en tête du fichier l'import des
|
|
7
|
-
* composants que la page n'importe pas déjà elle-même.
|
|
8
|
-
*/
|
|
9
|
-
export const MDX_COMPONENTS = [
|
|
10
|
-
"Accordion",
|
|
11
|
-
"AccordionGroup",
|
|
12
|
-
"Card",
|
|
13
|
-
"CardGroup",
|
|
14
|
-
"Check",
|
|
15
|
-
"CodeGroup",
|
|
16
|
-
"Columns",
|
|
17
|
-
"Danger",
|
|
18
|
-
"Frame",
|
|
19
|
-
"Info",
|
|
20
|
-
"Note",
|
|
21
|
-
"Step",
|
|
22
|
-
"Steps",
|
|
23
|
-
"Tab",
|
|
24
|
-
"Tabs",
|
|
25
|
-
"Tip",
|
|
26
|
-
"Warning",
|
|
27
|
-
] as const
|
|
4
|
+
import { MDX_COMPONENTS } from "../core/components.ts"
|
|
28
5
|
|
|
29
6
|
const SOURCE = "@hgdc/docs/components"
|
|
30
7
|
|
|
@@ -64,6 +41,10 @@ function importNode(name: string): EsmNode {
|
|
|
64
41
|
}
|
|
65
42
|
}
|
|
66
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Ajoute en tête de chaque page MDX l'import des composants (`core/components.ts`) que la
|
|
46
|
+
* page n'importe pas déjà elle-même : `<Note>`, `<Steps>`… s'écrivent sans `import`.
|
|
47
|
+
*/
|
|
67
48
|
export function remarkAutoImport() {
|
|
68
49
|
return (tree: Root, file: VFile) => {
|
|
69
50
|
if (!String(file.path ?? file.history.at(-1) ?? "").endsWith(".mdx")) return
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Root } from "mdast"
|
|
2
|
+
|
|
3
|
+
import { withBase } from "../core/navigation.ts"
|
|
4
|
+
|
|
5
|
+
type Node = { type: string; url?: unknown; attributes?: unknown; children?: Array<Node> }
|
|
6
|
+
type Attribute = { type: string; name?: string; value?: unknown }
|
|
7
|
+
|
|
8
|
+
/** Attributs JSX qui portent une adresse : `<Card href>`, `<img src>`, `<a href>`, `<video poster>`. */
|
|
9
|
+
const URL_ATTRIBUTES = new Set(["href", "src", "poster"])
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Site servi sous un sous-chemin (`base: "/docs"`) : les liens et médias écrits depuis la
|
|
13
|
+
* racine du site (`/guides/publier/`, `/images/x.png`) reçoivent le sous-chemin, dans le
|
|
14
|
+
* Markdown (`[…](/…)`, ``, définitions) comme dans les composants MDX. Le rédacteur
|
|
15
|
+
* écrit ses liens comme pour un site à la racine ; la doc reste déplaçable.
|
|
16
|
+
*/
|
|
17
|
+
export function remarkBaseLinks(options: { base: string }) {
|
|
18
|
+
const { base } = options
|
|
19
|
+
return (tree: Root) => {
|
|
20
|
+
if (!base) return
|
|
21
|
+
const walk = (node: Node) => {
|
|
22
|
+
if (typeof node.url === "string") node.url = withBase(base, node.url)
|
|
23
|
+
// Les directives (`:::note{…}`) portent un objet d'attributs : seuls ceux de JSX nous intéressent.
|
|
24
|
+
for (const attribute of Array.isArray(node.attributes) ? (node.attributes as Array<Attribute>) : [])
|
|
25
|
+
if (
|
|
26
|
+
attribute.type === "mdxJsxAttribute" &&
|
|
27
|
+
URL_ATTRIBUTES.has(attribute.name ?? "") &&
|
|
28
|
+
typeof attribute.value === "string"
|
|
29
|
+
)
|
|
30
|
+
attribute.value = withBase(base, attribute.value)
|
|
31
|
+
for (const child of node.children ?? []) walk(child)
|
|
32
|
+
}
|
|
33
|
+
walk(tree as unknown as Node)
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/build/outputs.ts
CHANGED
|
@@ -10,10 +10,18 @@ import { unified } from "unified"
|
|
|
10
10
|
import type { DocsConfig } from "../core/config.ts"
|
|
11
11
|
import { CORPUS_FORMAT, CORPUS_PATH, MAX_PAGE_CHARS, type Corpus } from "../core/corpus.ts"
|
|
12
12
|
import { llmsFullTxt, llmsTxt, markdownPath, type LlmsPage } from "../core/llms.ts"
|
|
13
|
-
import { allPages, locatePage, pageToPath, type NavTree } from "../core/navigation.ts"
|
|
13
|
+
import { allPages, locatePage, pageToPath, withBase, type NavTree } from "../core/navigation.ts"
|
|
14
14
|
import { htmlToMarkdown } from "./markdown.ts"
|
|
15
15
|
|
|
16
|
-
type Input = {
|
|
16
|
+
type Input = {
|
|
17
|
+
outDir: string
|
|
18
|
+
pathnames: Array<string>
|
|
19
|
+
config: DocsConfig
|
|
20
|
+
tree: NavTree
|
|
21
|
+
site: string | null
|
|
22
|
+
/** Sous-chemin du site (`/docs`) : porté par les chemins publiés, pas par les fichiers écrits ici. */
|
|
23
|
+
base?: string
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
/** `guides/publier/` (pathname d'Astro) → `/guides/publier/`. */
|
|
19
27
|
const normalize = (pathname: string) => `/${pathname}`.replace(/\/+/g, "/").replace(/([^/])$/, "$1/")
|
|
@@ -24,6 +32,7 @@ const normalize = (pathname: string) => `/${pathname}`.replace(/\/+/g, "/").repl
|
|
|
24
32
|
* navigation y entrent : une page hors navigation n'est pas annoncée aux agents.
|
|
25
33
|
*/
|
|
26
34
|
export async function writeMachineReadable(input: Input): Promise<{ pages: number }> {
|
|
35
|
+
const base = input.base ?? ""
|
|
27
36
|
const byPath = new Map(allPages(input.tree).map((page) => [pageToPath(page), page]))
|
|
28
37
|
const pages = new Map<string, LlmsPage>()
|
|
29
38
|
const corpusPages: Corpus["pages"] = []
|
|
@@ -45,7 +54,7 @@ export async function writeMachineReadable(input: Input): Promise<{ pages: numbe
|
|
|
45
54
|
pages.set(page, entry)
|
|
46
55
|
await write(input.outDir, markdownPath(path), `# ${title}\n\n${markdown}\n`)
|
|
47
56
|
corpusPages.push({
|
|
48
|
-
path,
|
|
57
|
+
path: withBase(base, path),
|
|
49
58
|
title,
|
|
50
59
|
description: entry.description,
|
|
51
60
|
version: locatePage(input.tree, page)?.version.id ?? null,
|
|
@@ -57,6 +66,7 @@ export async function writeMachineReadable(input: Input): Promise<{ pages: numbe
|
|
|
57
66
|
name: input.config.name,
|
|
58
67
|
description: input.config.description ?? null,
|
|
59
68
|
site: input.site,
|
|
69
|
+
base,
|
|
60
70
|
tree: input.tree,
|
|
61
71
|
pages,
|
|
62
72
|
}
|
|
@@ -75,7 +85,7 @@ export async function writeMachineReadable(input: Input): Promise<{ pages: numbe
|
|
|
75
85
|
builtAt: new Date().toISOString(),
|
|
76
86
|
theme: {
|
|
77
87
|
primary: input.config.colors?.primary ?? null,
|
|
78
|
-
logo: typeof logo === "string" ? logo : (logo?.light ?? null),
|
|
88
|
+
logo: withLogoBase(base, typeof logo === "string" ? logo : (logo?.light ?? null)),
|
|
79
89
|
},
|
|
80
90
|
versions: input.tree.versions.map(({ id, label, isDefault }) => ({ id, label, isDefault })),
|
|
81
91
|
pages: corpusPages,
|
|
@@ -84,6 +94,9 @@ export async function writeMachineReadable(input: Input): Promise<{ pages: numbe
|
|
|
84
94
|
return { pages: pages.size }
|
|
85
95
|
}
|
|
86
96
|
|
|
97
|
+
const withLogoBase = (base: string, logo: string | null) =>
|
|
98
|
+
logo ? withBase(base, logo.startsWith("/") || logo.includes("://") ? logo : `/${logo}`) : null
|
|
99
|
+
|
|
87
100
|
async function write(outDir: string, path: string, content: string): Promise<void> {
|
|
88
101
|
const target = join(outDir, path)
|
|
89
102
|
await mkdir(dirname(target), { recursive: true })
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composants disponibles dans chaque page MDX sans `import`, comme chez Mintlify
|
|
3
|
+
* (`<Note>`, `<Steps>`, `<Card>`…). Lus par le build (`build/auto-import.ts`) et par
|
|
4
|
+
* l'éditeur de la console (validation, insertion par « / » ou « < », mode visuel).
|
|
5
|
+
* Les props décrites ici sont celles des fichiers `src/components/*.astro`.
|
|
6
|
+
*/
|
|
7
|
+
export const MDX_COMPONENTS = [
|
|
8
|
+
"Accordion",
|
|
9
|
+
"AccordionGroup",
|
|
10
|
+
"Card",
|
|
11
|
+
"CardGroup",
|
|
12
|
+
"Check",
|
|
13
|
+
"CodeGroup",
|
|
14
|
+
"Columns",
|
|
15
|
+
"Danger",
|
|
16
|
+
"Frame",
|
|
17
|
+
"Info",
|
|
18
|
+
"Note",
|
|
19
|
+
"Step",
|
|
20
|
+
"Steps",
|
|
21
|
+
"Tab",
|
|
22
|
+
"Tabs",
|
|
23
|
+
"Tip",
|
|
24
|
+
"Warning",
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
export type MdxComponent = (typeof MDX_COMPONENTS)[number]
|
|
28
|
+
|
|
29
|
+
export function isMdxComponent(name: string): name is MdxComponent {
|
|
30
|
+
return (MDX_COMPONENTS as ReadonlyArray<string>).includes(name)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ComponentProp = {
|
|
34
|
+
name: string
|
|
35
|
+
label: string
|
|
36
|
+
type: "string" | "number" | "boolean"
|
|
37
|
+
required?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Props de chaque composant, pour les formulaires du mode visuel. */
|
|
41
|
+
export const COMPONENT_PROPS: Record<MdxComponent, ReadonlyArray<ComponentProp>> = {
|
|
42
|
+
Accordion: [
|
|
43
|
+
{ name: "title", label: "Titre", type: "string", required: true },
|
|
44
|
+
{ name: "defaultOpen", label: "Ouvert au départ", type: "boolean" },
|
|
45
|
+
],
|
|
46
|
+
AccordionGroup: [],
|
|
47
|
+
Card: [
|
|
48
|
+
{ name: "title", label: "Titre", type: "string", required: true },
|
|
49
|
+
{ name: "href", label: "Lien", type: "string" },
|
|
50
|
+
],
|
|
51
|
+
CardGroup: [{ name: "cols", label: "Colonnes", type: "number" }],
|
|
52
|
+
Check: [{ name: "title", label: "Titre", type: "string" }],
|
|
53
|
+
CodeGroup: [],
|
|
54
|
+
Columns: [{ name: "cols", label: "Colonnes", type: "number" }],
|
|
55
|
+
Danger: [{ name: "title", label: "Titre", type: "string" }],
|
|
56
|
+
Frame: [{ name: "caption", label: "Légende", type: "string" }],
|
|
57
|
+
Info: [{ name: "title", label: "Titre", type: "string" }],
|
|
58
|
+
Note: [{ name: "title", label: "Titre", type: "string" }],
|
|
59
|
+
Step: [{ name: "title", label: "Titre", type: "string", required: true }],
|
|
60
|
+
Steps: [],
|
|
61
|
+
Tab: [{ name: "title", label: "Titre", type: "string", required: true }],
|
|
62
|
+
Tabs: [{ name: "syncKey", label: "Clé de synchronisation", type: "string" }],
|
|
63
|
+
Tip: [{ name: "title", label: "Titre", type: "string" }],
|
|
64
|
+
Warning: [{ name: "title", label: "Titre", type: "string" }],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type BlockCategory = "Encadrés" | "Mise en page" | "Contenu"
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Blocs proposés à l'insertion, dans l'ordre du menu. `snippet` est le MDX inséré ; `$|`
|
|
71
|
+
* marque l'endroit où placer le curseur. Plusieurs blocs peuvent produire le même
|
|
72
|
+
* composant (colonnes à 2, 3 ou 4).
|
|
73
|
+
*/
|
|
74
|
+
export type Block = {
|
|
75
|
+
id: string
|
|
76
|
+
label: string
|
|
77
|
+
description: string
|
|
78
|
+
category: BlockCategory
|
|
79
|
+
keywords: ReadonlyArray<string>
|
|
80
|
+
snippet: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const callout = (name: string, label: string, description: string, keywords: Array<string>): Block => ({
|
|
84
|
+
id: name.toLowerCase(),
|
|
85
|
+
label,
|
|
86
|
+
description,
|
|
87
|
+
category: "Encadrés",
|
|
88
|
+
keywords: [name.toLowerCase(), ...keywords],
|
|
89
|
+
snippet: `<${name}>\n$|\n</${name}>`,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const columns = (cols: number): Block => ({
|
|
93
|
+
id: `columns-${cols}`,
|
|
94
|
+
label: `${cols} colonnes`,
|
|
95
|
+
description: `Cartes côte à côte, sur ${cols} colonnes.`,
|
|
96
|
+
category: "Mise en page",
|
|
97
|
+
keywords: ["colonnes", "columns", "grille", String(cols)],
|
|
98
|
+
snippet: `<Columns cols={${cols}}>\n${Array.from({ length: cols }, (_, i) => ` <Card title="Carte ${i + 1}">${i === 0 ? "$|" : ""}</Card>`).join("\n")}\n</Columns>`,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export const BLOCKS: ReadonlyArray<Block> = [
|
|
102
|
+
callout("Note", "Note", "Une information utile à côté du texte.", ["information", "remarque"]),
|
|
103
|
+
callout("Tip", "Conseil", "Une astuce qui fait gagner du temps.", ["astuce", "conseil"]),
|
|
104
|
+
callout("Info", "Info", "Un contexte ou une précision.", ["information"]),
|
|
105
|
+
callout("Warning", "Attention", "Un point à ne pas manquer.", ["attention", "avertissement"]),
|
|
106
|
+
callout("Danger", "Danger", "Une action risquée ou irréversible.", ["danger", "risque"]),
|
|
107
|
+
callout("Check", "Validé", "Une étape réussie, un résultat attendu.", ["validé", "succès", "ok"]),
|
|
108
|
+
columns(2),
|
|
109
|
+
columns(3),
|
|
110
|
+
columns(4),
|
|
111
|
+
{
|
|
112
|
+
id: "steps",
|
|
113
|
+
label: "Étapes",
|
|
114
|
+
description: "Une marche à suivre numérotée.",
|
|
115
|
+
category: "Mise en page",
|
|
116
|
+
keywords: ["étapes", "steps", "procédure"],
|
|
117
|
+
snippet:
|
|
118
|
+
'<Steps>\n <Step title="Première étape">\n $|\n </Step>\n <Step title="Deuxième étape">\n </Step>\n</Steps>',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "tabs",
|
|
122
|
+
label: "Onglets",
|
|
123
|
+
description: "Plusieurs variantes d'un même contenu.",
|
|
124
|
+
category: "Mise en page",
|
|
125
|
+
keywords: ["onglets", "tabs"],
|
|
126
|
+
snippet: '<Tabs>\n <Tab title="Premier">\n $|\n </Tab>\n <Tab title="Second">\n </Tab>\n</Tabs>',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "accordion",
|
|
130
|
+
label: "Accordéon",
|
|
131
|
+
description: "Un détail replié, ouvert au clic.",
|
|
132
|
+
category: "Mise en page",
|
|
133
|
+
keywords: ["accordéon", "accordion", "replié", "faq"],
|
|
134
|
+
snippet: '<AccordionGroup>\n <Accordion title="Question">\n $|\n </Accordion>\n</AccordionGroup>',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "card",
|
|
138
|
+
label: "Carte",
|
|
139
|
+
description: "Un lien mis en avant, avec un titre.",
|
|
140
|
+
category: "Contenu",
|
|
141
|
+
keywords: ["carte", "card", "lien"],
|
|
142
|
+
snippet: '<Card title="Titre" href="/">\n $|\n</Card>',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "code-group",
|
|
146
|
+
label: "Groupe de code",
|
|
147
|
+
description: "Le même exemple en plusieurs langages.",
|
|
148
|
+
category: "Contenu",
|
|
149
|
+
keywords: ["code", "codegroup", "exemple"],
|
|
150
|
+
snippet: "<CodeGroup>\n\n```bash npm\n$|\n```\n\n```bash bun\n\n```\n\n</CodeGroup>",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "frame",
|
|
154
|
+
label: "Cadre",
|
|
155
|
+
description: "Une image ou une capture, avec sa légende.",
|
|
156
|
+
category: "Contenu",
|
|
157
|
+
keywords: ["cadre", "frame", "image", "capture"],
|
|
158
|
+
snippet: '<Frame caption="Légende">\n \n</Frame>',
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
/** Texte à insérer et position du curseur dans ce texte. */
|
|
163
|
+
export function expandSnippet(snippet: string): { text: string; cursor: number } {
|
|
164
|
+
const cursor = snippet.indexOf("$|")
|
|
165
|
+
return cursor === -1 ? { text: snippet, cursor: snippet.length } : { text: snippet.replace("$|", ""), cursor }
|
|
166
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -16,6 +16,8 @@ const PAGE = /^(?!\/)(?!.*\.\.)[a-z0-9][a-z0-9/_-]*$/i
|
|
|
16
16
|
const PROJECT_ID_PATTERN = /^[a-z0-9]{8,64}$/
|
|
17
17
|
|
|
18
18
|
const hex = z.string().regex(HEX, "couleur attendue au format #rrggbb")
|
|
19
|
+
/** Sous-chemin du site (`/docs`) : un ou plusieurs segments, sans « / » final. */
|
|
20
|
+
const BASE = /^(?:\/[a-z0-9][a-z0-9_-]*)+$/i
|
|
19
21
|
|
|
20
22
|
export type NavPageRef = string
|
|
21
23
|
export type NavGroupInput = { group: string; pages: Array<NavPageRef | NavGroupInput>; expanded?: boolean }
|
|
@@ -54,6 +56,47 @@ const logo = z.union([
|
|
|
54
56
|
z.object({ light: z.string(), dark: z.string().optional(), href: z.string().optional() }),
|
|
55
57
|
])
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Réseaux du pied de page : clé de `footer.socials` (celles de Mintlify), libellé, icône de
|
|
61
|
+
* Starlight. Lu par le build et par le formulaire des réglages de la console.
|
|
62
|
+
*/
|
|
63
|
+
export const SOCIAL_NETWORKS = {
|
|
64
|
+
linkedin: { label: "LinkedIn", icon: "linkedin" },
|
|
65
|
+
github: { label: "GitHub", icon: "github" },
|
|
66
|
+
gitlab: { label: "GitLab", icon: "gitlab" },
|
|
67
|
+
x: { label: "X", icon: "x.com" },
|
|
68
|
+
youtube: { label: "YouTube", icon: "youtube" },
|
|
69
|
+
instagram: { label: "Instagram", icon: "instagram" },
|
|
70
|
+
facebook: { label: "Facebook", icon: "facebook" },
|
|
71
|
+
discord: { label: "Discord", icon: "discord" },
|
|
72
|
+
mastodon: { label: "Mastodon", icon: "mastodon" },
|
|
73
|
+
bluesky: { label: "Bluesky", icon: "blueSky" },
|
|
74
|
+
website: { label: "Site web", icon: "external" },
|
|
75
|
+
} as const
|
|
76
|
+
|
|
77
|
+
/** Ancienne écriture acceptée : `x.com` (Starlight) et `twitter` désignent X. */
|
|
78
|
+
const SOCIAL_ALIASES: Record<string, keyof typeof SOCIAL_NETWORKS> = { "x.com": "x", twitter: "x" }
|
|
79
|
+
|
|
80
|
+
export function socialNetwork(key: string): (typeof SOCIAL_NETWORKS)[keyof typeof SOCIAL_NETWORKS] | null {
|
|
81
|
+
const name = (SOCIAL_ALIASES[key] ?? key) as keyof typeof SOCIAL_NETWORKS
|
|
82
|
+
return SOCIAL_NETWORKS[name] ?? null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const REDIRECT_SOURCE = /^\/(?!\/)[A-Za-z0-9._~\-/%]*$/
|
|
86
|
+
|
|
87
|
+
/** Ancienne adresse vers la nouvelle (champ `redirects` de Mintlify), sans motif. */
|
|
88
|
+
const redirect = z.object({
|
|
89
|
+
source: z.string().regex(REDIRECT_SOURCE, "adresse source attendue, commençant par « / » (ex. /guides/ancien)"),
|
|
90
|
+
destination: z
|
|
91
|
+
.string()
|
|
92
|
+
.min(1)
|
|
93
|
+
.refine(
|
|
94
|
+
(value) => /^\/(?!\/)/.test(value) || value.startsWith("https://"),
|
|
95
|
+
"destination attendue : un chemin du site (« /… ») ou une adresse https://",
|
|
96
|
+
),
|
|
97
|
+
permanent: z.boolean().optional(),
|
|
98
|
+
})
|
|
99
|
+
|
|
57
100
|
export const docsConfigSchema = z.object({
|
|
58
101
|
$schema: z.string().optional(),
|
|
59
102
|
name: z.string().min(1).max(80),
|
|
@@ -64,6 +107,12 @@ export const docsConfigSchema = z.object({
|
|
|
64
107
|
.regex(PROJECT_ID_PATTERN, "identifiant de projet attendu, copié depuis la console docs.hgdc.ch")
|
|
65
108
|
.optional(),
|
|
66
109
|
language: z.enum(["fr", "en", "de", "it"]).default("fr"),
|
|
110
|
+
/**
|
|
111
|
+
* Sous-chemin où la doc est servie, quand un produit la publie derrière un proxy
|
|
112
|
+
* (`https://app.exemple.ch/docs`). Liens, médias, recherche, `llms.txt` et pages `.md`
|
|
113
|
+
* le portent ; le site est écrit dans `dist/<base>/`.
|
|
114
|
+
*/
|
|
115
|
+
base: z.string().regex(BASE, "sous-chemin attendu, sans « / » final (ex. /docs)").optional(),
|
|
67
116
|
colors: z.object({ primary: hex, light: hex.optional(), dark: hex.optional() }).optional(),
|
|
68
117
|
logo: logo.optional(),
|
|
69
118
|
favicon: z.string().optional(),
|
|
@@ -78,6 +127,7 @@ export const docsConfigSchema = z.object({
|
|
|
78
127
|
})
|
|
79
128
|
.optional(),
|
|
80
129
|
footer: z.object({ socials: z.record(z.string(), z.url()).optional() }).optional(),
|
|
130
|
+
redirects: z.array(redirect).max(500, "500 redirections au plus").optional(),
|
|
81
131
|
assistant: z
|
|
82
132
|
.object({
|
|
83
133
|
enabled: z.boolean().default(true),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contrat de `POST /api/v1/feedback` entre le bas de chaque page (« Cette page t'a aidé ? »)
|
|
5
|
+
* et la console. Aucune donnée personnelle : la page, la réponse, un commentaire facultatif.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_FEEDBACK_COMMENT = 1000
|
|
8
|
+
|
|
9
|
+
export const feedbackRequestSchema = z.object({
|
|
10
|
+
project: z.string().regex(/^[a-z0-9]{8,64}$/),
|
|
11
|
+
/** Chemin de la page, `base` compris (`/docs/guides/publier/`). */
|
|
12
|
+
page: z.string().startsWith("/").max(512),
|
|
13
|
+
helpful: z.boolean(),
|
|
14
|
+
comment: z.string().trim().max(MAX_FEEDBACK_COMMENT).optional(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export type FeedbackRequest = z.infer<typeof feedbackRequestSchema>
|
package/src/core/llms.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NavItem, NavTree } from "./navigation.ts"
|
|
2
|
-
import { pagesOf as pagesIn, pageToPath } from "./navigation.ts"
|
|
2
|
+
import { pagesOf as pagesIn, pageToPath, withBase } from "./navigation.ts"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `llms.txt` et `llms-full.txt` au format de llmstxt.org : un H1, un résumé en citation,
|
|
@@ -12,6 +12,8 @@ export type LlmsInput = {
|
|
|
12
12
|
name: string
|
|
13
13
|
description: string | null
|
|
14
14
|
site: string | null
|
|
15
|
+
/** Sous-chemin du site (`/docs`), `""` ou absent à la racine. */
|
|
16
|
+
base?: string
|
|
15
17
|
tree: NavTree
|
|
16
18
|
pages: ReadonlyMap<string, LlmsPage>
|
|
17
19
|
}
|
|
@@ -22,14 +24,15 @@ export function markdownPath(path: string): string {
|
|
|
22
24
|
return trimmed ? `${trimmed}.md` : "/index.md"
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
function url(
|
|
26
|
-
|
|
27
|
+
function url(input: Pick<LlmsInput, "site" | "base">, path: string): string {
|
|
28
|
+
const full = withBase(input.base ?? "", path)
|
|
29
|
+
return input.site ? new URL(full, input.site).toString() : full
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
function link(input: LlmsInput, page: string): string | null {
|
|
30
33
|
const data = input.pages.get(page)
|
|
31
34
|
if (!data) return null
|
|
32
|
-
const target = url(input
|
|
35
|
+
const target = url(input, markdownPath(pageToPath(page)))
|
|
33
36
|
return `- [${data.title}](${target})${data.description ? `: ${data.description}` : ""}`
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -89,7 +92,7 @@ export function llmsFullTxt(input: LlmsInput, order: Array<string>): string {
|
|
|
89
92
|
for (const page of order) {
|
|
90
93
|
const data = input.pages.get(page)
|
|
91
94
|
if (!data) continue
|
|
92
|
-
parts.push(`\n---\n\nSource : ${url(input
|
|
95
|
+
parts.push(`\n---\n\nSource : ${url(input, pageToPath(page))}\n\n# ${data.title}\n\n${data.markdown.trim()}`)
|
|
93
96
|
}
|
|
94
97
|
return `${parts.join("\n").trim()}\n`
|
|
95
98
|
}
|
package/src/core/navigation.ts
CHANGED
|
@@ -82,6 +82,30 @@ export function pageToPath(page: string): string {
|
|
|
82
82
|
return slug ? `/${slug}/` : "/"
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** Sous-chemin du site tel que docs.json le déclare, ou `""` pour un site à la racine. */
|
|
86
|
+
export function siteBase(config: Pick<DocsConfig, "base">): string {
|
|
87
|
+
return config.base ? config.base.replace(/\/+$/, "") : ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Chemin public d'un chemin du site : `/guides/publier/` → `/docs/guides/publier/` sous
|
|
92
|
+
* `base: "/docs"`. Un chemin qui porte déjà le sous-chemin, ou qui n'est pas absolu
|
|
93
|
+
* (`#ancre`, `https://…`, `//hôte`), reste tel quel.
|
|
94
|
+
*/
|
|
95
|
+
export function withBase(base: string, path: string): string {
|
|
96
|
+
if (!base || !path.startsWith("/") || path.startsWith("//")) return path
|
|
97
|
+
if (path === base || path.startsWith(`${base}/`) || path.startsWith(`${base}#`) || path.startsWith(`${base}?`))
|
|
98
|
+
return path
|
|
99
|
+
return `${base}${path}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Chemin du site sans son sous-chemin : `/docs/guides/` → `/guides/`, `/docs/` → `/`. */
|
|
103
|
+
export function withoutBase(base: string, pathname: string): string {
|
|
104
|
+
if (!base) return pathname
|
|
105
|
+
if (pathname === base) return "/"
|
|
106
|
+
return pathname.startsWith(`${base}/`) ? pathname.slice(base.length) : pathname
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/** Pages citées dans la navigation mais absentes du dossier : erreur de build lisible. */
|
|
86
110
|
export function missingPages(tree: NavTree, existing: ReadonlySet<string>): Array<string> {
|
|
87
111
|
return [...new Set(allPages(tree))].filter((page) => !existing.has(pageToSlug(page)))
|