@hgdc/docs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs.schema.json +4 -0
- package/package.json +3 -2
- package/src/build/base-links.ts +35 -0
- package/src/build/outputs.ts +17 -4
- package/src/core/config.ts +8 -0
- package/src/core/llms.ts +8 -5
- package/src/core/navigation.ts +24 -0
- package/src/integration.ts +42 -6
- package/src/overrides/Head.astro +14 -12
- package/src/overrides/PageTitle.astro +95 -94
- package/src/overrides/Sidebar.astro +60 -60
- package/template/.astro/content-modules.mjs +15 -15
- package/template/.build/.well-known/llms.txt +17 -17
- package/template/.build/404.html +5 -5
- package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.Cn3-p701.js +1 -0
- package/template/.build/_astro/common.C-MpX45O.css +3 -0
- package/template/.build/_astro/{ui-core.Clh5jUUO.js → ui-core.CrG20ya_.js} +1 -1
- package/template/.build/_hgdc/corpus.json +1 -1
- package/template/.build/demarrer/premiere-doc/index.html +15 -15
- package/template/.build/demarrer/premiere-doc.md +3 -3
- package/template/.build/explications/donnees/index.html +11 -11
- package/template/.build/explications/donnees.md +1 -1
- package/template/.build/explications/fonctionnement/index.html +13 -12
- package/template/.build/explications/fonctionnement.md +3 -3
- package/template/.build/guides/agents/index.html +16 -11
- package/template/.build/guides/agents.md +16 -2
- package/template/.build/guides/assistant/index.html +14 -12
- package/template/.build/guides/assistant.md +5 -1
- package/template/.build/guides/composants/index.html +19 -19
- package/template/.build/guides/composants.md +8 -10
- package/template/.build/guides/ecrire/index.html +13 -12
- package/template/.build/guides/ecrire.md +1 -1
- package/template/.build/guides/navigation/index.html +13 -12
- package/template/.build/guides/publier/index.html +26 -17
- package/template/.build/guides/publier.md +9 -9
- package/template/.build/guides/theme/index.html +13 -12
- package/template/.build/guides/theme.md +1 -1
- package/template/.build/index.html +12 -11
- package/template/.build/index.md +11 -11
- package/template/.build/llms-full.txt +95 -69
- package/template/.build/llms.txt +17 -17
- package/template/.build/pagefind/fragment/fr-ch_2a431a9.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_456586a.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_68e734b.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_cfa4336.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/fragment/fr-ch_e02aa49.pf_fragment +0 -0
- package/template/.build/pagefind/fragment/fr-ch_e254a54.pf_fragment +0 -0
- package/template/.build/pagefind/index/fr-ch_a71411b.pf_index +0 -0
- package/template/.build/pagefind/pagefind-entry.json +1 -1
- package/template/.build/pagefind/pagefind.fr-ch_2f3ea8fccf961.pf_meta +0 -0
- package/template/.build/reference/api-assistant/index.html +12 -12
- package/template/.build/reference/api-assistant.md +13 -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 +11 -11
- package/template/.build/reference/docs-json.md +2 -2
- package/template/.build/reference/mcp/index.html +11 -11
- package/template/node_modules/.vite/deps/_metadata.json +8 -8
- package/src/build/public-files.test.ts +0 -29
- package/src/core/core.test.ts +0 -253
- package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.g9hoxVJg.js +0 -1
- package/template/.build/_astro/common.8GHCkW7i.css +0 -3
- package/template/.build/pagefind/fragment/fr-ch_148313b.pf_fragment +0 -0
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"template",
|
|
14
|
-
"docs.schema.json"
|
|
14
|
+
"docs.schema.json",
|
|
15
|
+
"!src/**/*.test.ts"
|
|
15
16
|
],
|
|
16
17
|
"exports": {
|
|
17
18
|
"./core/*": "./src/core/*.ts",
|
|
@@ -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 })
|
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 }
|
|
@@ -64,6 +66,12 @@ export const docsConfigSchema = z.object({
|
|
|
64
66
|
.regex(PROJECT_ID_PATTERN, "identifiant de projet attendu, copié depuis la console docs.hgdc.ch")
|
|
65
67
|
.optional(),
|
|
66
68
|
language: z.enum(["fr", "en", "de", "it"]).default("fr"),
|
|
69
|
+
/**
|
|
70
|
+
* Sous-chemin où la doc est servie, quand un produit la publie derrière un proxy
|
|
71
|
+
* (`https://app.exemple.ch/docs`). Liens, médias, recherche, `llms.txt` et pages `.md`
|
|
72
|
+
* le portent ; le site est écrit dans `dist/<base>/`.
|
|
73
|
+
*/
|
|
74
|
+
base: z.string().regex(BASE, "sous-chemin attendu, sans « / » final (ex. /docs)").optional(),
|
|
67
75
|
colors: z.object({ primary: hex, light: hex.optional(), dark: hex.optional() }).optional(),
|
|
68
76
|
logo: logo.optional(),
|
|
69
77
|
favicon: z.string().optional(),
|
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)))
|
package/src/integration.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
4
|
|
|
@@ -7,10 +7,18 @@ import starlight from "@astrojs/starlight"
|
|
|
7
7
|
import type { AstroIntegration, AstroUserConfig } from "astro"
|
|
8
8
|
|
|
9
9
|
import { remarkAutoImport } from "./build/auto-import.ts"
|
|
10
|
+
import { remarkBaseLinks } from "./build/base-links.ts"
|
|
10
11
|
import { publicFiles } from "./build/public-files.ts"
|
|
11
12
|
import { writeMachineReadable } from "./build/outputs.ts"
|
|
12
13
|
import { parseDocsConfig, withHostProject, type DocsConfig } from "./core/config.ts"
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
missingPages,
|
|
16
|
+
normalizeNavigation,
|
|
17
|
+
pageToSlug,
|
|
18
|
+
siteBase,
|
|
19
|
+
type NavItem,
|
|
20
|
+
type NavTree,
|
|
21
|
+
} from "./core/navigation.ts"
|
|
14
22
|
import { themeCss, themeFor } from "./core/theme.ts"
|
|
15
23
|
|
|
16
24
|
/** Où lire la doc du client et où écrire le site : posé par la CLI `hgdc-docs`. */
|
|
@@ -35,12 +43,27 @@ export function readSiteOptions(env: NodeJS.ProcessEnv = process.env): SiteOptio
|
|
|
35
43
|
return {
|
|
36
44
|
contentDir,
|
|
37
45
|
outDir: resolve(env.HGDC_DOCS_OUT ?? join(contentDir, "dist")),
|
|
38
|
-
site: env.HGDC_DOCS_SITE ?? vercelUrl,
|
|
46
|
+
site: siteOrigin(env.HGDC_DOCS_SITE ?? vercelUrl),
|
|
39
47
|
apiEndpoint: env.HGDC_DOCS_API ?? DEFAULT_API,
|
|
40
48
|
project: env.HGDC_DOCS_PROJECT ?? null,
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Origine du site seulement : le sous-chemin vient de `base` dans docs.json, pas de
|
|
54
|
+
* l'adresse (`https://app.ch/docs` et `https://app.ch` donnent le même site).
|
|
55
|
+
*/
|
|
56
|
+
function siteOrigin(value: string | null): string | null {
|
|
57
|
+
if (!value) return null
|
|
58
|
+
try {
|
|
59
|
+
return new URL(value).origin
|
|
60
|
+
} catch {
|
|
61
|
+
throw new DocsConfigError(
|
|
62
|
+
`HGDC_DOCS_SITE : adresse invalide (${value.slice(0, 100)}). Exemple : https://docs.exemple.ch`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
export class DocsConfigError extends Error {}
|
|
45
68
|
|
|
46
69
|
export function loadDocsConfig(contentDir: string, hostProject: string | null = null): DocsConfig {
|
|
@@ -58,6 +81,8 @@ export type VirtualConfig = {
|
|
|
58
81
|
tree: NavTree
|
|
59
82
|
apiEndpoint: string
|
|
60
83
|
site: string | null
|
|
84
|
+
/** Sous-chemin du site (`/docs`), `""` à la racine. */
|
|
85
|
+
base: string
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
const IGNORED_DIRS = new Set(["node_modules", "dist", ".hgdc-docs", ".git", ".github", ".vercel"])
|
|
@@ -144,6 +169,10 @@ function virtualConfigPlugin(data: VirtualConfig) {
|
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
|
|
172
|
+
/** Site sous un sous-chemin : la racine renvoie vers la doc, pour une visite directe de l'hôte. */
|
|
173
|
+
const rootRedirect = (base: string) =>
|
|
174
|
+
`<!doctype html><meta charset="utf-8"><meta name="robots" content="noindex"><meta http-equiv="refresh" content="0; url=${base}/"><link rel="canonical" href="${base}/"><a href="${base}/">Documentation</a>\n`
|
|
175
|
+
|
|
147
176
|
function machineReadableOutputs(data: VirtualConfig, options: SiteOptions): AstroIntegration {
|
|
148
177
|
return {
|
|
149
178
|
name: "hgdc-docs-outputs",
|
|
@@ -155,12 +184,17 @@ function machineReadableOutputs(data: VirtualConfig, options: SiteOptions): Astr
|
|
|
155
184
|
config: data.config,
|
|
156
185
|
tree: data.tree,
|
|
157
186
|
site: options.site,
|
|
187
|
+
base: data.base,
|
|
158
188
|
})
|
|
159
189
|
logger.info(`llms.txt, llms-full.txt, ${written.pages} pages Markdown et le corpus de l'assistant écrits.`)
|
|
160
190
|
// Le build tourne dans le paquet, où ses dépendances se résolvent quel que soit le
|
|
161
191
|
// gestionnaire du client (npm, pnpm, bun isolé), puis le site seul est copié chez lui.
|
|
192
|
+
// Sous un sous-chemin, le site va dans `dist/<base>/` : servi tel quel par Host, il
|
|
193
|
+
// répond aux mêmes adresses que derrière le proxy du produit (`/docs/_astro/…`).
|
|
162
194
|
rmSync(options.outDir, { recursive: true, force: true })
|
|
163
|
-
|
|
195
|
+
const target = join(options.outDir, ...data.base.split("/").filter(Boolean))
|
|
196
|
+
cpSync(fileURLToPath(dir), target, { recursive: true, filter: (path) => !path.includes(".prerender") })
|
|
197
|
+
if (data.base) writeFileSync(join(options.outDir, "index.html"), rootRedirect(data.base))
|
|
164
198
|
logger.info(`Site écrit dans ${options.outDir}`)
|
|
165
199
|
},
|
|
166
200
|
},
|
|
@@ -176,7 +210,8 @@ export function createAstroConfig(options: SiteOptions = readSiteOptions()): Ast
|
|
|
176
210
|
throw new DocsConfigError(
|
|
177
211
|
`docs.json cite des pages absentes du dossier :\n- ${missing.map((page) => `${page}.mdx`).join("\n- ")}`,
|
|
178
212
|
)
|
|
179
|
-
const
|
|
213
|
+
const base = siteBase(config)
|
|
214
|
+
const data: VirtualConfig = { config, tree, apiEndpoint: options.apiEndpoint, site: options.site, base }
|
|
180
215
|
const theme = themeFor(config.colors?.primary, config.colors?.dark)
|
|
181
216
|
const logo = typeof config.logo === "string" ? { light: config.logo } : config.logo
|
|
182
217
|
const asset = (path: string) => join(options.contentDir, path.replace(/^\//, ""))
|
|
@@ -194,8 +229,9 @@ export function createAstroConfig(options: SiteOptions = readSiteOptions()): Ast
|
|
|
194
229
|
outDir: file("../template/.build"),
|
|
195
230
|
cacheDir: join(options.contentDir, ".hgdc-docs", "cache"),
|
|
196
231
|
site: options.site ?? undefined,
|
|
232
|
+
...(base ? { base } : {}),
|
|
197
233
|
trailingSlash: "always",
|
|
198
|
-
markdown: { processor: unified({ remarkPlugins: [remarkAutoImport] }) },
|
|
234
|
+
markdown: { processor: unified({ remarkPlugins: [remarkAutoImport, [remarkBaseLinks, { base }]] }) },
|
|
199
235
|
vite: {
|
|
200
236
|
plugins: [virtualConfigPlugin(data)],
|
|
201
237
|
server: { fs: { allow: [options.contentDir, file("..")] } },
|
package/src/overrides/Head.astro
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Default from "@astrojs/starlight/components/Head.astro"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
1
|
+
---
|
|
2
|
+
import Default from "@astrojs/starlight/components/Head.astro"
|
|
3
|
+
import data from "virtual:hgdc-docs/config"
|
|
4
|
+
|
|
5
|
+
import { markdownPath } from "../core/llms.ts"
|
|
6
|
+
import { withBase, withoutBase } from "../core/navigation.ts"
|
|
7
|
+
|
|
8
|
+
// llmstxt.org : chaque page annonce sa version Markdown et l'index qui la décrit.
|
|
9
|
+
const markdown = withBase(data.base, markdownPath(withoutBase(data.base, Astro.url.pathname)))
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<Default><slot /></Default>
|
|
13
|
+
<link rel="alternate" type="text/markdown" href={markdown} />
|
|
14
|
+
<link rel="describedby" type="text/markdown" href={withBase(data.base, "/llms.txt")} />
|
|
@@ -1,94 +1,95 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Default from "@astrojs/starlight/components/PageTitle.astro"
|
|
3
|
-
import data from "virtual:hgdc-docs/config"
|
|
4
|
-
|
|
5
|
-
import { markdownPath } from "../core/llms.ts"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
></path
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<li><a
|
|
31
|
-
<li><a data-hgdc-open="
|
|
32
|
-
<li><a href="/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
button.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
link.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
menu.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
---
|
|
2
|
+
import Default from "@astrojs/starlight/components/PageTitle.astro"
|
|
3
|
+
import data from "virtual:hgdc-docs/config"
|
|
4
|
+
|
|
5
|
+
import { markdownPath } from "../core/llms.ts"
|
|
6
|
+
import { withBase, withoutBase } from "../core/navigation.ts"
|
|
7
|
+
|
|
8
|
+
const markdown = withBase(data.base, markdownPath(withoutBase(data.base, Astro.url.pathname)))
|
|
9
|
+
const assistant = Boolean(data.config.project) && data.config.assistant?.enabled !== false
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="hgdc-title">
|
|
13
|
+
<Default><slot /></Default>
|
|
14
|
+
<div class="hgdc-page-actions not-content" data-hgdc-skip data-pagefind-ignore>
|
|
15
|
+
<button type="button" class="hgdc-action" data-hgdc-copy={markdown}>
|
|
16
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16"
|
|
17
|
+
><rect x="8" y="8" width="12" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
18
|
+
></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
19
|
+
></path></svg
|
|
20
|
+
>
|
|
21
|
+
<span data-label>Copier pour un LLM</span>
|
|
22
|
+
</button>
|
|
23
|
+
<details class="hgdc-menu">
|
|
24
|
+
<summary class="hgdc-action hgdc-action--icon" aria-label="Autres formats de la page">
|
|
25
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16"
|
|
26
|
+
><path d="m6 9 6 6 6-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path></svg
|
|
27
|
+
>
|
|
28
|
+
</summary>
|
|
29
|
+
<ul class="hgdc-menu__list">
|
|
30
|
+
<li><a href={markdown}>Voir en Markdown</a></li>
|
|
31
|
+
<li><a data-hgdc-open="claude" href="https://claude.ai/new" target="_blank" rel="noopener">Ouvrir dans Claude</a></li>
|
|
32
|
+
<li><a data-hgdc-open="chatgpt" href="https://chatgpt.com/" target="_blank" rel="noopener">Ouvrir dans ChatGPT</a></li>
|
|
33
|
+
<li><a href={withBase(data.base, "/llms.txt")}>Index pour les agents (llms.txt)</a></li>
|
|
34
|
+
</ul>
|
|
35
|
+
</details>
|
|
36
|
+
{
|
|
37
|
+
assistant && (
|
|
38
|
+
<button type="button" class="hgdc-action" data-hgdc-ask>
|
|
39
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16">
|
|
40
|
+
<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" />
|
|
41
|
+
</svg>
|
|
42
|
+
<span>Demander à l'assistant</span>
|
|
43
|
+
</button>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
<span class="sr-only" role="status" aria-live="polite" data-hgdc-copy-status></span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<script>
|
|
51
|
+
const status = document.querySelector<HTMLElement>("[data-hgdc-copy-status]")
|
|
52
|
+
|
|
53
|
+
for (const button of document.querySelectorAll<HTMLButtonElement>("[data-hgdc-copy]")) {
|
|
54
|
+
const label = button.querySelector<HTMLElement>("[data-label]")!
|
|
55
|
+
button.addEventListener("click", async () => {
|
|
56
|
+
const done = (text: string) => {
|
|
57
|
+
label.textContent = text
|
|
58
|
+
if (status) status.textContent = text
|
|
59
|
+
setTimeout(() => (label.textContent = "Copier pour un LLM"), 2000)
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(button.dataset.hgdcCopy!)
|
|
63
|
+
if (!response.ok) throw new Error(String(response.status))
|
|
64
|
+
await navigator.clipboard.writeText(await response.text())
|
|
65
|
+
done("Page copiée")
|
|
66
|
+
} catch {
|
|
67
|
+
done("Copie impossible")
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// « Ouvrir dans… » : l'assistant externe lit la version Markdown de cette page.
|
|
73
|
+
const markdownUrl = new URL(document.querySelector<HTMLElement>("[data-hgdc-copy]")!.dataset.hgdcCopy!, location.href)
|
|
74
|
+
const prompt = `Lis ${markdownUrl.href} et réponds à mes questions sur cette page.`
|
|
75
|
+
for (const link of document.querySelectorAll<HTMLAnchorElement>("[data-hgdc-open]")) {
|
|
76
|
+
const base = link.dataset.hgdcOpen === "claude" ? "https://claude.ai/new" : "https://chatgpt.com/"
|
|
77
|
+
link.href = `${base}?q=${encodeURIComponent(prompt)}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Le menu se ferme au clic extérieur et à Échap, comme un menu natif.
|
|
81
|
+
for (const menu of document.querySelectorAll<HTMLDetailsElement>(".hgdc-menu")) {
|
|
82
|
+
document.addEventListener("click", (event) => {
|
|
83
|
+
if (menu.open && !menu.contains(event.target as Node)) menu.open = false
|
|
84
|
+
})
|
|
85
|
+
menu.addEventListener("keydown", (event) => {
|
|
86
|
+
if (event.key === "Escape" && menu.open) {
|
|
87
|
+
menu.open = false
|
|
88
|
+
menu.querySelector("summary")?.focus()
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const button of document.querySelectorAll("[data-hgdc-ask]"))
|
|
94
|
+
button.addEventListener("click", () => document.dispatchEvent(new CustomEvent("hgdc:assistant-open")))
|
|
95
|
+
</script>
|