radiant-docs 0.1.7 → 0.1.9

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 (78) hide show
  1. package/dist/index.js +28 -5
  2. package/package.json +5 -4
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -6,10 +6,10 @@
6
6
  "dev": "astro dev",
7
7
  "start": "tsx runner.ts",
8
8
  "prebuild": "rm -rf public/pagefind",
9
- "build": "astro build && pagefind --site dist",
9
+ "build": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/rewrite-static-asset-host.mjs",
10
10
  "preview": "astro preview",
11
11
  "astro": "astro",
12
- "search:index": "astro build && pagefind --site dist && cp -r dist/pagefind public/"
12
+ "search:index": "astro build && node scripts/generate-og-metadata.mjs && node scripts/generate-og-images.mjs && node scripts/stamp-og-image-versions.mjs && node scripts/stamp-image-versions.mjs && pagefind --site dist && node scripts/stamp-pagefind-runtime-version.mjs && node scripts/rewrite-static-asset-host.mjs && cp -r dist/pagefind public/"
13
13
  },
14
14
  "dependencies": {
15
15
  "@alpinejs/collapse": "^3.15.2",
@@ -18,10 +18,11 @@
18
18
  "@astrojs/alpinejs": "^0.4.9",
19
19
  "@astrojs/mdx": "^4.3.12",
20
20
  "@aws-sdk/client-s3": "^3.964.0",
21
- "@expressive-code/plugin-collapsible-sections": "^0.41.4",
22
- "@expressive-code/plugin-line-numbers": "^0.41.4",
21
+ "@fontsource/google-sans-flex": "^5.2.2",
23
22
  "@iconify-json/lucide": "^1.2.79",
23
+ "@iconify-json/simple-icons": "^1.2.69",
24
24
  "@readme/oas-to-snippet": "^29.3.0",
25
+ "@resvg/resvg-js": "^2.6.2",
25
26
  "@stoplight/spectral-core": "^1.20.0",
26
27
  "@stoplight/spectral-rulesets": "^1.22.0",
27
28
  "@tailwindcss/typography": "^0.5.19",
@@ -29,13 +30,14 @@
29
30
  "@xt0rted/expressive-code-file-icons": "^1.0.0",
30
31
  "alpinejs": "^3.15.2",
31
32
  "astro": "^5.16.4",
32
- "astro-expressive-code": "^0.41.4",
33
33
  "astro-icon": "^1.1.5",
34
34
  "fs-extra": "^11.3.3",
35
35
  "mime-types": "^3.0.2",
36
36
  "oas": "^28.7.0",
37
37
  "openapi-sampler": "^1.6.2",
38
38
  "prismjs": "^1.30.0",
39
+ "rehype-autolink-headings": "^7.1.0",
40
+ "rehype-slug": "^6.0.0",
39
41
  "simple-git": "^3.30.0",
40
42
  "tailwindcss": "^4.1.17",
41
43
  "yaml": "^2.8.2",
@@ -0,0 +1,335 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Resvg } from "@resvg/resvg-js";
4
+
5
+ const CWD = process.cwd();
6
+ const DIST_DIR = path.join(CWD, "dist");
7
+ const MANIFEST_PATH = path.join(DIST_DIR, "_og/meta.json");
8
+ const OUTPUT_DIR = path.join(DIST_DIR, "_og/images");
9
+
10
+ const OG_WIDTH = 1200;
11
+ const OG_HEIGHT = 630;
12
+ const OG_BACKGROUND = "#171717";
13
+ const OG_PRIMARY_COLOR = "#737373";
14
+ const OG_FONT_FAMILY = "DejaVu Sans, Arial, sans-serif";
15
+
16
+ function trimToUndefined(value) {
17
+ return typeof value === "string" && value.trim().length > 0
18
+ ? value.trim()
19
+ : undefined;
20
+ }
21
+
22
+ function decodeHtmlEntities(value) {
23
+ if (!value || typeof value !== "string") return "";
24
+
25
+ const named = {
26
+ amp: "&",
27
+ lt: "<",
28
+ gt: ">",
29
+ quot: '"',
30
+ apos: "'",
31
+ nbsp: " ",
32
+ };
33
+
34
+ return value
35
+ .replace(/&#(\d+);/g, (_, dec) => {
36
+ const code = Number.parseInt(dec, 10);
37
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
38
+ })
39
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
40
+ const code = Number.parseInt(hex, 16);
41
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
42
+ })
43
+ .replace(/&([a-zA-Z]+);/g, (entity, name) => {
44
+ const decoded = named[name.toLowerCase()];
45
+ return decoded ?? entity;
46
+ });
47
+ }
48
+
49
+ function normalizeRoutePath(routePath) {
50
+ if (!routePath || routePath === "/") return "/";
51
+ if (!routePath.startsWith("/")) routePath = `/${routePath}`;
52
+ return routePath.endsWith("/") ? routePath : `${routePath}/`;
53
+ }
54
+
55
+ function truncateText(value, maxLength) {
56
+ const text = trimToUndefined(value);
57
+ if (!text) return "";
58
+ if (text.length <= maxLength) return text;
59
+ return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
60
+ }
61
+
62
+ function stripSiteSuffixFromTitle(title, siteName) {
63
+ const trimmedTitle = trimToUndefined(title);
64
+ const trimmedSiteName = trimToUndefined(siteName);
65
+ if (!trimmedTitle || !trimmedSiteName) return trimmedTitle ?? "";
66
+
67
+ const suffixes = [` | ${trimmedSiteName}`, ` - ${trimmedSiteName}`];
68
+ for (const suffix of suffixes) {
69
+ if (trimmedTitle.endsWith(suffix)) {
70
+ return trimmedTitle.slice(0, -suffix.length).trim();
71
+ }
72
+ }
73
+
74
+ return trimmedTitle;
75
+ }
76
+
77
+ function routeToImageRelativePath(routePath) {
78
+ const normalizedRoutePath = normalizeRoutePath(routePath);
79
+ if (normalizedRoutePath === "/") return "index.png";
80
+ return `${normalizedRoutePath.slice(1, -1)}.png`;
81
+ }
82
+
83
+ function escapeXml(value) {
84
+ return value
85
+ .replace(/&/g, "&amp;")
86
+ .replace(/</g, "&lt;")
87
+ .replace(/>/g, "&gt;")
88
+ .replace(/"/g, "&quot;")
89
+ .replace(/'/g, "&apos;");
90
+ }
91
+
92
+ function wrapText(input, maxChars, maxLines) {
93
+ const text = trimToUndefined(input);
94
+ if (!text) return [];
95
+
96
+ const words = text.split(/\s+/).filter(Boolean);
97
+ const lines = [];
98
+ let current = "";
99
+ let index = 0;
100
+
101
+ while (index < words.length) {
102
+ const word = words[index];
103
+ const candidate = current ? `${current} ${word}` : word;
104
+
105
+ if (candidate.length <= maxChars) {
106
+ current = candidate;
107
+ index += 1;
108
+ continue;
109
+ }
110
+
111
+ if (!current) {
112
+ current = word.slice(0, maxChars);
113
+ index += 1;
114
+ }
115
+
116
+ lines.push(current);
117
+ current = "";
118
+
119
+ if (lines.length === maxLines) {
120
+ break;
121
+ }
122
+ }
123
+
124
+ if (lines.length < maxLines && current) {
125
+ lines.push(current);
126
+ }
127
+
128
+ const consumedAllWords = index >= words.length;
129
+ if (!consumedAllWords && lines.length > 0) {
130
+ lines[lines.length - 1] = truncateText(lines[lines.length - 1], maxChars);
131
+ }
132
+
133
+ return lines.slice(0, maxLines);
134
+ }
135
+
136
+ function mimeTypeFromFilePath(filePath) {
137
+ const extension = path.extname(filePath).toLowerCase();
138
+ if (extension === ".svg") return "image/svg+xml";
139
+ if (extension === ".png") return "image/png";
140
+ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
141
+ if (extension === ".webp") return "image/webp";
142
+ if (extension === ".gif") return "image/gif";
143
+ return undefined;
144
+ }
145
+
146
+ function safeDecodeURIComponent(value) {
147
+ try {
148
+ return decodeURIComponent(value);
149
+ } catch {
150
+ return value;
151
+ }
152
+ }
153
+
154
+ function resolveLocalLogoPath(logoPath) {
155
+ const stripped = logoPath.split("#")[0].split("?")[0];
156
+ const decoded = safeDecodeURIComponent(stripped);
157
+ const relativePath = decoded.startsWith("/") ? decoded.slice(1) : decoded;
158
+ const absolutePath = path.resolve(DIST_DIR, relativePath);
159
+ const distRoot = path.resolve(DIST_DIR) + path.sep;
160
+
161
+ if (!absolutePath.startsWith(distRoot) && absolutePath !== path.resolve(DIST_DIR)) {
162
+ return undefined;
163
+ }
164
+
165
+ return absolutePath;
166
+ }
167
+
168
+ function resolveLogoDataUri(logoPath) {
169
+ const resolvedLogoPath = trimToUndefined(logoPath);
170
+ if (!resolvedLogoPath) return undefined;
171
+ if (/^https?:\/\//i.test(resolvedLogoPath)) return undefined;
172
+
173
+ const localLogoPath = resolveLocalLogoPath(resolvedLogoPath);
174
+ if (!localLogoPath || !fs.existsSync(localLogoPath)) return undefined;
175
+
176
+ const stats = fs.statSync(localLogoPath);
177
+ if (!stats.isFile()) return undefined;
178
+
179
+ const mimeType = mimeTypeFromFilePath(localLogoPath);
180
+ if (!mimeType) return undefined;
181
+
182
+ const buffer = fs.readFileSync(localLogoPath);
183
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
184
+ }
185
+
186
+ function renderOgSvg({
187
+ title,
188
+ description,
189
+ siteName,
190
+ logoDataUri,
191
+ }) {
192
+ const resolvedTitle = trimToUndefined(title) ?? siteName;
193
+ const titleLines = wrapText(resolvedTitle, 24, 2);
194
+ if (titleLines.length === 0) {
195
+ titleLines.push(siteName);
196
+ }
197
+
198
+ const resolvedDescription = trimToUndefined(description) ?? "";
199
+ const descriptionLines = wrapText(resolvedDescription, 58, 2);
200
+
201
+ const titleLineHeight = 76;
202
+ const descriptionLineHeight = 38;
203
+ const spaceBetweenTitleAndDescription = 30;
204
+ const bottomPadding = 56;
205
+ const titleBlockHeight =
206
+ titleLines.length * titleLineHeight +
207
+ spaceBetweenTitleAndDescription +
208
+ descriptionLines.length * descriptionLineHeight;
209
+ const titleBlockTop = OG_HEIGHT - bottomPadding - titleBlockHeight;
210
+
211
+ let currentY = titleBlockTop;
212
+ const titleSvg = titleLines
213
+ .map((line) => {
214
+ const element = `<text x="56" y="${currentY}" fill="#FFFFFF" font-family="${OG_FONT_FAMILY}" font-size="72" font-weight="700" letter-spacing="-0.02em" dominant-baseline="hanging">${escapeXml(line)}</text>`;
215
+ currentY += titleLineHeight;
216
+ return element;
217
+ })
218
+ .join("");
219
+
220
+ currentY += spaceBetweenTitleAndDescription;
221
+ const descriptionSvg = descriptionLines
222
+ .map((line) => {
223
+ const element = `<text x="56" y="${currentY}" fill="#FFFFFF" fill-opacity="0.82" font-family="${OG_FONT_FAMILY}" font-size="32" font-weight="400" letter-spacing="0" dominant-baseline="hanging">${escapeXml(line)}</text>`;
224
+ currentY += descriptionLineHeight;
225
+ return element;
226
+ })
227
+ .join("");
228
+
229
+ const logoSvg = logoDataUri
230
+ ? `<image href="${escapeXml(logoDataUri)}" x="56" y="56" width="420" height="132" preserveAspectRatio="xMinYMid meet" />`
231
+ : `<text x="56" y="74" fill="#FFFFFF" font-family="${OG_FONT_FAMILY}" font-size="64" font-weight="700" letter-spacing="-0.02em" dominant-baseline="hanging">${escapeXml(siteName)}</text>`;
232
+
233
+ return `<?xml version="1.0" encoding="UTF-8"?>
234
+ <svg width="${OG_WIDTH}" height="${OG_HEIGHT}" viewBox="0 0 ${OG_WIDTH} ${OG_HEIGHT}" xmlns="http://www.w3.org/2000/svg">
235
+ <defs>
236
+ <radialGradient id="ogGlow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1540 -360) rotate(90) scale(1600)">
237
+ <stop offset="0" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0.56" />
238
+ <stop offset="0.74" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0" />
239
+ <stop offset="1" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0" />
240
+ </radialGradient>
241
+ </defs>
242
+ <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${OG_BACKGROUND}" />
243
+ <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="url(#ogGlow)" />
244
+ ${logoSvg}
245
+ ${titleSvg}
246
+ ${descriptionSvg}
247
+ </svg>`;
248
+ }
249
+
250
+ function writeImageForRoute({
251
+ routePath,
252
+ title,
253
+ description,
254
+ siteName,
255
+ logoDataUri,
256
+ }) {
257
+ const svg = renderOgSvg({
258
+ title,
259
+ description,
260
+ siteName,
261
+ logoDataUri,
262
+ });
263
+
264
+ const resvg = new Resvg(svg, {
265
+ font: {
266
+ loadSystemFonts: true,
267
+ defaultFontFamily: "DejaVu Sans",
268
+ sansSerifFamily: "DejaVu Sans",
269
+ },
270
+ });
271
+
272
+ const pngData = resvg.render().asPng();
273
+ const outputRelativePath = routeToImageRelativePath(routePath);
274
+ const outputPath = path.join(OUTPUT_DIR, ...outputRelativePath.split("/"));
275
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
276
+ fs.writeFileSync(outputPath, pngData);
277
+ }
278
+
279
+ function main() {
280
+ if (!fs.existsSync(DIST_DIR)) {
281
+ console.warn("Skipping OG image generation: dist directory not found.");
282
+ return;
283
+ }
284
+
285
+ if (!fs.existsSync(MANIFEST_PATH)) {
286
+ console.warn("Skipping OG image generation: OG metadata manifest not found.");
287
+ return;
288
+ }
289
+
290
+ const rawManifest = fs.readFileSync(MANIFEST_PATH, "utf8");
291
+ const manifest = JSON.parse(rawManifest);
292
+
293
+ const defaults = manifest?.defaults ?? {};
294
+ const pages = manifest?.pages ?? {};
295
+ const siteName = trimToUndefined(decodeHtmlEntities(defaults.siteName)) ??
296
+ "Documentation";
297
+ const logoDataUri = resolveLogoDataUri(defaults.logoDark);
298
+
299
+ const routes = Object.keys(pages).sort();
300
+ if (routes.length === 0) {
301
+ console.warn("Skipping OG image generation: no pages found in metadata.");
302
+ return;
303
+ }
304
+
305
+ let generatedCount = 0;
306
+ for (const routePath of routes) {
307
+ const page = pages[routePath] ?? {};
308
+ const decodedTitle = decodeHtmlEntities(page.title);
309
+ const decodedDescription = decodeHtmlEntities(page.description);
310
+
311
+ const cleanedTitle = stripSiteSuffixFromTitle(decodedTitle, siteName);
312
+ const title = truncateText(cleanedTitle || siteName, 80);
313
+ const fallbackDescription = `Learn about ${title} in the ${siteName} documentation.`;
314
+ const description = truncateText(
315
+ decodedDescription || fallbackDescription,
316
+ 150,
317
+ );
318
+
319
+ writeImageForRoute({
320
+ routePath,
321
+ title,
322
+ description,
323
+ siteName,
324
+ logoDataUri,
325
+ });
326
+
327
+ generatedCount += 1;
328
+ }
329
+
330
+ console.log(
331
+ `✅ Generated ${generatedCount} OG image${generatedCount === 1 ? "" : "s"} in ${OUTPUT_DIR}`,
332
+ );
333
+ }
334
+
335
+ main();
@@ -0,0 +1,173 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const CWD = process.cwd();
5
+ const DIST_DIR = path.join(CWD, "dist");
6
+ const DOCS_CONFIG_PATH = path.join(CWD, "src/content/docs/docs.json");
7
+ const OUTPUT_DIR = path.join(DIST_DIR, "_og");
8
+ const OUTPUT_PATH = path.join(OUTPUT_DIR, "meta.json");
9
+
10
+ function readDocsConfig() {
11
+ if (!fs.existsSync(DOCS_CONFIG_PATH)) {
12
+ return {};
13
+ }
14
+
15
+ try {
16
+ const raw = fs.readFileSync(DOCS_CONFIG_PATH, "utf8");
17
+ return JSON.parse(raw);
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function findHtmlFiles(dir, files = []) {
24
+ if (!fs.existsSync(dir)) return files;
25
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
26
+
27
+ for (const entry of entries) {
28
+ const fullPath = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ findHtmlFiles(fullPath, files);
31
+ continue;
32
+ }
33
+
34
+ if (entry.isFile() && entry.name.endsWith(".html")) {
35
+ files.push(fullPath);
36
+ }
37
+ }
38
+
39
+ return files;
40
+ }
41
+
42
+ function normalizeRoutePath(routePath) {
43
+ if (!routePath || routePath === "/") return "/";
44
+ if (!routePath.startsWith("/")) routePath = `/${routePath}`;
45
+ return routePath.endsWith("/") ? routePath : `${routePath}/`;
46
+ }
47
+
48
+ function routePathFromHtmlPath(filePath) {
49
+ const relative = path.relative(DIST_DIR, filePath).replace(/\\/g, "/");
50
+
51
+ if (relative === "index.html") return "/";
52
+ if (relative.endsWith("/index.html")) {
53
+ const base = relative.slice(0, -"/index.html".length);
54
+ return normalizeRoutePath(base);
55
+ }
56
+
57
+ if (relative.endsWith(".html")) {
58
+ return normalizeRoutePath(relative.slice(0, -".html".length));
59
+ }
60
+
61
+ return "/";
62
+ }
63
+
64
+ function matchMetaContent(html, attrName, attrValue) {
65
+ const regex = new RegExp(
66
+ `<meta\\s+[^>]*${attrName}\\s*=\\s*["']${attrValue}["'][^>]*content\\s*=\\s*["']([^"']*)["'][^>]*>`,
67
+ "i",
68
+ );
69
+ const reverseRegex = new RegExp(
70
+ `<meta\\s+[^>]*content\\s*=\\s*["']([^"']*)["'][^>]*${attrName}\\s*=\\s*["']${attrValue}["'][^>]*>`,
71
+ "i",
72
+ );
73
+ return html.match(regex)?.[1] ?? html.match(reverseRegex)?.[1] ?? "";
74
+ }
75
+
76
+ function decodeHtmlEntities(value) {
77
+ if (!value || typeof value !== "string") return "";
78
+
79
+ const named = {
80
+ amp: "&",
81
+ lt: "<",
82
+ gt: ">",
83
+ quot: '"',
84
+ apos: "'",
85
+ nbsp: " ",
86
+ };
87
+
88
+ return value
89
+ .replace(/&#(\d+);/g, (_, dec) => {
90
+ const code = Number.parseInt(dec, 10);
91
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
92
+ })
93
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
94
+ const code = Number.parseInt(hex, 16);
95
+ return Number.isFinite(code) ? String.fromCodePoint(code) : _;
96
+ })
97
+ .replace(/&([a-zA-Z]+);/g, (entity, name) => {
98
+ const decoded = named[name.toLowerCase()];
99
+ return decoded ?? entity;
100
+ });
101
+ }
102
+
103
+ function extractTitle(html) {
104
+ const ogTitle = decodeHtmlEntities(
105
+ matchMetaContent(html, "property", "og:title"),
106
+ ).trim();
107
+ if (ogTitle) return ogTitle;
108
+ return decodeHtmlEntities(
109
+ html.match(/<title>([^<]*)<\/title>/i)?.[1] ?? "",
110
+ ).trim();
111
+ }
112
+
113
+ function extractDescription(html) {
114
+ const description = decodeHtmlEntities(
115
+ matchMetaContent(html, "name", "description"),
116
+ ).trim();
117
+ if (description) return description;
118
+ return decodeHtmlEntities(
119
+ matchMetaContent(html, "property", "og:description"),
120
+ ).trim();
121
+ }
122
+
123
+ function main() {
124
+ if (!fs.existsSync(DIST_DIR)) {
125
+ console.warn("Skipping OG metadata generation: dist directory not found.");
126
+ return;
127
+ }
128
+
129
+ const docsConfig = readDocsConfig();
130
+ const logo = typeof docsConfig.logo === "object" && docsConfig.logo
131
+ ? docsConfig.logo
132
+ : {};
133
+
134
+ const htmlFiles = findHtmlFiles(DIST_DIR);
135
+ htmlFiles.sort();
136
+ const pages = {};
137
+
138
+ for (const filePath of htmlFiles) {
139
+ const routePath = routePathFromHtmlPath(filePath);
140
+ const html = fs.readFileSync(filePath, "utf8");
141
+
142
+ pages[routePath] = {
143
+ title: extractTitle(html),
144
+ description: extractDescription(html),
145
+ };
146
+ }
147
+
148
+ const metadata = {
149
+ defaults: {
150
+ siteName:
151
+ typeof docsConfig.title === "string" ? docsConfig.title.trim() : "",
152
+ logoLight:
153
+ typeof logo.light === "string" ? logo.light.trim() : "",
154
+ logoDark:
155
+ typeof logo.dark === "string" ? logo.dark.trim() : "",
156
+ background:
157
+ typeof docsConfig.ogBackground === "string"
158
+ ? docsConfig.ogBackground.trim()
159
+ : "",
160
+ primaryColor:
161
+ typeof docsConfig.primaryColor === "string"
162
+ ? docsConfig.primaryColor.trim()
163
+ : "",
164
+ },
165
+ pages,
166
+ };
167
+
168
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
169
+ fs.writeFileSync(OUTPUT_PATH, JSON.stringify(metadata, null, 2), "utf8");
170
+ console.log(`✅ Generated OG metadata at ${OUTPUT_PATH}`);
171
+ }
172
+
173
+ main();