@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.
Files changed (84) hide show
  1. package/docs.schema.json +4 -0
  2. package/package.json +3 -2
  3. package/src/build/base-links.ts +35 -0
  4. package/src/build/outputs.ts +17 -4
  5. package/src/core/config.ts +8 -0
  6. package/src/core/llms.ts +8 -5
  7. package/src/core/navigation.ts +24 -0
  8. package/src/integration.ts +42 -6
  9. package/src/overrides/Head.astro +14 -12
  10. package/src/overrides/PageTitle.astro +95 -94
  11. package/src/overrides/Sidebar.astro +60 -60
  12. package/template/.astro/content-modules.mjs +15 -15
  13. package/template/.build/.well-known/llms.txt +17 -17
  14. package/template/.build/404.html +5 -5
  15. package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.Cn3-p701.js +1 -0
  16. package/template/.build/_astro/common.C-MpX45O.css +3 -0
  17. package/template/.build/_astro/{ui-core.Clh5jUUO.js → ui-core.CrG20ya_.js} +1 -1
  18. package/template/.build/_hgdc/corpus.json +1 -1
  19. package/template/.build/demarrer/premiere-doc/index.html +15 -15
  20. package/template/.build/demarrer/premiere-doc.md +3 -3
  21. package/template/.build/explications/donnees/index.html +11 -11
  22. package/template/.build/explications/donnees.md +1 -1
  23. package/template/.build/explications/fonctionnement/index.html +13 -12
  24. package/template/.build/explications/fonctionnement.md +3 -3
  25. package/template/.build/guides/agents/index.html +16 -11
  26. package/template/.build/guides/agents.md +16 -2
  27. package/template/.build/guides/assistant/index.html +14 -12
  28. package/template/.build/guides/assistant.md +5 -1
  29. package/template/.build/guides/composants/index.html +19 -19
  30. package/template/.build/guides/composants.md +8 -10
  31. package/template/.build/guides/ecrire/index.html +13 -12
  32. package/template/.build/guides/ecrire.md +1 -1
  33. package/template/.build/guides/navigation/index.html +13 -12
  34. package/template/.build/guides/publier/index.html +26 -17
  35. package/template/.build/guides/publier.md +9 -9
  36. package/template/.build/guides/theme/index.html +13 -12
  37. package/template/.build/guides/theme.md +1 -1
  38. package/template/.build/index.html +12 -11
  39. package/template/.build/index.md +11 -11
  40. package/template/.build/llms-full.txt +95 -69
  41. package/template/.build/llms.txt +17 -17
  42. package/template/.build/pagefind/fragment/fr-ch_2a431a9.pf_fragment +0 -0
  43. package/template/.build/pagefind/fragment/fr-ch_456586a.pf_fragment +0 -0
  44. package/template/.build/pagefind/fragment/fr-ch_68e734b.pf_fragment +0 -0
  45. package/template/.build/pagefind/fragment/fr-ch_72b91f4.pf_fragment +0 -0
  46. package/template/.build/pagefind/fragment/fr-ch_8f256dd.pf_fragment +0 -0
  47. package/template/.build/pagefind/fragment/fr-ch_95a15cc.pf_fragment +0 -0
  48. package/template/.build/pagefind/fragment/fr-ch_aa80c55.pf_fragment +0 -0
  49. package/template/.build/pagefind/fragment/fr-ch_ab79738.pf_fragment +0 -0
  50. package/template/.build/pagefind/fragment/fr-ch_cfa4336.pf_fragment +0 -0
  51. package/template/.build/pagefind/fragment/fr-ch_dab8f5e.pf_fragment +0 -0
  52. package/template/.build/pagefind/fragment/fr-ch_de60b43.pf_fragment +0 -0
  53. package/template/.build/pagefind/fragment/fr-ch_e02aa49.pf_fragment +0 -0
  54. package/template/.build/pagefind/fragment/fr-ch_e254a54.pf_fragment +0 -0
  55. package/template/.build/pagefind/index/fr-ch_a71411b.pf_index +0 -0
  56. package/template/.build/pagefind/pagefind-entry.json +1 -1
  57. package/template/.build/pagefind/pagefind.fr-ch_2f3ea8fccf961.pf_meta +0 -0
  58. package/template/.build/reference/api-assistant/index.html +12 -12
  59. package/template/.build/reference/api-assistant.md +13 -3
  60. package/template/.build/reference/cli/index.html +11 -11
  61. package/template/.build/reference/cli.md +5 -5
  62. package/template/.build/reference/docs-json/index.html +11 -11
  63. package/template/.build/reference/docs-json.md +2 -2
  64. package/template/.build/reference/mcp/index.html +11 -11
  65. package/template/node_modules/.vite/deps/_metadata.json +8 -8
  66. package/src/build/public-files.test.ts +0 -29
  67. package/src/core/core.test.ts +0 -253
  68. package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.g9hoxVJg.js +0 -1
  69. package/template/.build/_astro/common.8GHCkW7i.css +0 -3
  70. package/template/.build/pagefind/fragment/fr-ch_148313b.pf_fragment +0 -0
  71. package/template/.build/pagefind/fragment/fr-ch_424fd16.pf_fragment +0 -0
  72. package/template/.build/pagefind/fragment/fr-ch_5a4d77d.pf_fragment +0 -0
  73. package/template/.build/pagefind/fragment/fr-ch_6c36edc.pf_fragment +0 -0
  74. package/template/.build/pagefind/fragment/fr-ch_73d7887.pf_fragment +0 -0
  75. package/template/.build/pagefind/fragment/fr-ch_8234ed3.pf_fragment +0 -0
  76. package/template/.build/pagefind/fragment/fr-ch_8f8a19e.pf_fragment +0 -0
  77. package/template/.build/pagefind/fragment/fr-ch_9f1b981.pf_fragment +0 -0
  78. package/template/.build/pagefind/fragment/fr-ch_a19039d.pf_fragment +0 -0
  79. package/template/.build/pagefind/fragment/fr-ch_d72cc1e.pf_fragment +0 -0
  80. package/template/.build/pagefind/fragment/fr-ch_d732215.pf_fragment +0 -0
  81. package/template/.build/pagefind/fragment/fr-ch_e78d58b.pf_fragment +0 -0
  82. package/template/.build/pagefind/fragment/fr-ch_e92f972.pf_fragment +0 -0
  83. package/template/.build/pagefind/index/fr-ch_f94c95d.pf_index +0 -0
  84. 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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/docs",
3
- "version": "0.1.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
+ }
@@ -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 = { outDir: string; pathnames: Array<string>; config: DocsConfig; tree: NavTree; site: string | null }
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 })
@@ -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(site: string | null, path: string): string {
26
- return site ? new URL(path, site).toString() : path
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.site, markdownPath(pageToPath(page)))
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.site, pageToPath(page))}\n\n# ${data.title}\n\n${data.markdown.trim()}`)
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
  }
@@ -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)))
@@ -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 { missingPages, normalizeNavigation, pageToSlug, type NavItem, type NavTree } from "./core/navigation.ts"
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
- cpSync(fileURLToPath(dir), options.outDir, { recursive: true, filter: (path) => !path.includes(".prerender") })
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 data: VirtualConfig = { config, tree, apiEndpoint: options.apiEndpoint, site: options.site }
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("..")] } },
@@ -1,12 +1,14 @@
1
- ---
2
- import Default from "@astrojs/starlight/components/Head.astro"
3
-
4
- import { markdownPath } from "../core/llms.ts"
5
-
6
- // llmstxt.org : chaque page annonce sa version Markdown et l'index qui la décrit.
7
- const markdown = markdownPath(Astro.url.pathname)
8
- ---
9
-
10
- <Default><slot /></Default>
11
- <link rel="alternate" type="text/markdown" href={markdown} />
12
- <link rel="describedby" type="text/markdown" href="/llms.txt" />
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
- const markdown = markdownPath(Astro.url.pathname)
8
- const assistant = Boolean(data.config.project) && data.config.assistant?.enabled !== false
9
- ---
10
-
11
- <div class="hgdc-title">
12
- <Default><slot /></Default>
13
- <div class="hgdc-page-actions not-content" data-hgdc-skip data-pagefind-ignore>
14
- <button type="button" class="hgdc-action" data-hgdc-copy={markdown}>
15
- <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16"
16
- ><rect x="8" y="8" width="12" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.8"
17
- ></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"
18
- ></path></svg
19
- >
20
- <span data-label>Copier pour un LLM</span>
21
- </button>
22
- <details class="hgdc-menu">
23
- <summary class="hgdc-action hgdc-action--icon" aria-label="Autres formats de la page">
24
- <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16"
25
- ><path d="m6 9 6 6 6-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path></svg
26
- >
27
- </summary>
28
- <ul class="hgdc-menu__list">
29
- <li><a href={markdown}>Voir en Markdown</a></li>
30
- <li><a data-hgdc-open="claude" href="https://claude.ai/new" target="_blank" rel="noopener">Ouvrir dans Claude</a></li>
31
- <li><a data-hgdc-open="chatgpt" href="https://chatgpt.com/" target="_blank" rel="noopener">Ouvrir dans ChatGPT</a></li>
32
- <li><a href="/llms.txt">Index pour les agents (llms.txt)</a></li>
33
- </ul>
34
- </details>
35
- {
36
- assistant && (
37
- <button type="button" class="hgdc-action" data-hgdc-ask>
38
- <svg aria-hidden="true" viewBox="0 0 24 24" width="16" height="16">
39
- <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" />
40
- </svg>
41
- <span>Demander à l'assistant</span>
42
- </button>
43
- )
44
- }
45
- <span class="sr-only" role="status" aria-live="polite" data-hgdc-copy-status></span>
46
- </div>
47
- </div>
48
-
49
- <script>
50
- const status = document.querySelector<HTMLElement>("[data-hgdc-copy-status]")
51
-
52
- for (const button of document.querySelectorAll<HTMLButtonElement>("[data-hgdc-copy]")) {
53
- const label = button.querySelector<HTMLElement>("[data-label]")!
54
- button.addEventListener("click", async () => {
55
- const done = (text: string) => {
56
- label.textContent = text
57
- if (status) status.textContent = text
58
- setTimeout(() => (label.textContent = "Copier pour un LLM"), 2000)
59
- }
60
- try {
61
- const response = await fetch(button.dataset.hgdcCopy!)
62
- if (!response.ok) throw new Error(String(response.status))
63
- await navigator.clipboard.writeText(await response.text())
64
- done("Page copiée")
65
- } catch {
66
- done("Copie impossible")
67
- }
68
- })
69
- }
70
-
71
- // « Ouvrir dans… » : l'assistant externe lit la version Markdown de cette page.
72
- const markdownUrl = new URL(document.querySelector<HTMLElement>("[data-hgdc-copy]")!.dataset.hgdcCopy!, location.href)
73
- const prompt = `Lis ${markdownUrl.href} et réponds à mes questions sur cette page.`
74
- for (const link of document.querySelectorAll<HTMLAnchorElement>("[data-hgdc-open]")) {
75
- const base = link.dataset.hgdcOpen === "claude" ? "https://claude.ai/new" : "https://chatgpt.com/"
76
- link.href = `${base}?q=${encodeURIComponent(prompt)}`
77
- }
78
-
79
- // Le menu se ferme au clic extérieur et à Échap, comme un menu natif.
80
- for (const menu of document.querySelectorAll<HTMLDetailsElement>(".hgdc-menu")) {
81
- document.addEventListener("click", (event) => {
82
- if (menu.open && !menu.contains(event.target as Node)) menu.open = false
83
- })
84
- menu.addEventListener("keydown", (event) => {
85
- if (event.key === "Escape" && menu.open) {
86
- menu.open = false
87
- menu.querySelector("summary")?.focus()
88
- }
89
- })
90
- }
91
-
92
- for (const button of document.querySelectorAll("[data-hgdc-ask]"))
93
- button.addEventListener("click", () => document.dispatchEvent(new CustomEvent("hgdc:assistant-open")))
94
- </script>
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>