radiant-docs 0.1.64 → 0.1.66

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 (29) hide show
  1. package/dist/index.js +53 -10
  2. package/package.json +4 -3
  3. package/template/package-lock.json +1991 -4096
  4. package/template/package.json +7 -19
  5. package/template/src/components/OpenApiPage.astro +107 -18
  6. package/template/src/components/Sidebar.astro +1 -0
  7. package/template/src/components/endpoint/PlaygroundBar.astro +1 -1
  8. package/template/src/components/endpoint/PlaygroundField.astro +433 -71
  9. package/template/src/components/endpoint/PlaygroundForm.astro +470 -87
  10. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  11. package/template/src/components/endpoint/ResponseDisplay.astro +109 -2
  12. package/template/src/components/endpoint/ResponseFieldTree.astro +1 -1
  13. package/template/src/components/endpoint/ResponseFields.astro +97 -28
  14. package/template/src/components/endpoint/ResponseSnippets.astro +19 -7
  15. package/template/src/layouts/Layout.astro +94 -0
  16. package/template/src/lib/openapi/operation-doc.ts +308 -70
  17. package/template/src/lib/openapi/response-content.ts +133 -0
  18. package/template/src/lib/openapi/schema-variant-labels.ts +213 -0
  19. package/template/.vscode/extensions.json +0 -4
  20. package/template/.vscode/launch.json +0 -11
  21. package/template/scripts/generate-og-images.mjs +0 -667
  22. package/template/scripts/generate-og-metadata.mjs +0 -206
  23. package/template/scripts/generate-proxy-allowed-origins.mjs +0 -48
  24. package/template/scripts/generate-robots-txt.mjs +0 -47
  25. package/template/scripts/publish-shiki-platform-assets.mjs +0 -1177
  26. package/template/scripts/remove-assistant-for-non-pro.mjs +0 -28
  27. package/template/scripts/stamp-image-versions.mjs +0 -355
  28. package/template/scripts/stamp-og-image-versions.mjs +0 -199
  29. package/template/scripts/stamp-pagefind-runtime-version.mjs +0 -140
@@ -1,667 +0,0 @@
1
- import fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- import { Resvg } from "@resvg/resvg-js";
5
- import { create as createFont } from "fontkit";
6
- import wawoff2 from "wawoff2";
7
- import {
8
- DEFAULT_TEXT_FONT_PRESET,
9
- FONT_PRESET_OPTIONS,
10
- } from "radiant-docs-validator";
11
-
12
- const require = createRequire(import.meta.url);
13
-
14
- const CWD = process.cwd();
15
- const DIST_DIR = path.join(CWD, "dist");
16
- const MANIFEST_PATH = path.join(DIST_DIR, "_og/meta.json");
17
- const OUTPUT_DIR = path.join(DIST_DIR, "_og/images");
18
-
19
- const OG_WIDTH = 1200;
20
- const OG_HEIGHT = 630;
21
- const OG_BACKGROUND = "#171717";
22
- const OG_PRIMARY_COLOR = "#737373";
23
- const DEFAULT_OG_FONT_FAMILY = "DejaVu Sans, Arial, sans-serif";
24
- const FONT_PRESET_ID_SET = new Set(FONT_PRESET_OPTIONS);
25
- const OG_FONT_SUBSETS = ["latin", "latin-ext"];
26
- const baseFontCache = new Map();
27
- const weightedFontCache = new Map();
28
-
29
- const FONT_PRESETS = {
30
- "google-sans": {
31
- family: "Google Sans Variable, DejaVu Sans, Arial, sans-serif",
32
- fontsource: {
33
- packageName: "@fontsource-variable/google-sans",
34
- fileId: "google-sans",
35
- },
36
- },
37
- inter: {
38
- family: "Inter Variable, DejaVu Sans, Arial, sans-serif",
39
- fontsource: {
40
- packageName: "@fontsource-variable/inter",
41
- fileId: "inter",
42
- },
43
- },
44
- "inter-tight": {
45
- family: "Inter Tight Variable, DejaVu Sans, Arial, sans-serif",
46
- fontsource: {
47
- packageName: "@fontsource-variable/inter-tight",
48
- fileId: "inter-tight",
49
- },
50
- },
51
- geist: {
52
- family: "Geist Variable, DejaVu Sans, Arial, sans-serif",
53
- fontsource: {
54
- packageName: "@fontsource-variable/geist",
55
- fileId: "geist",
56
- },
57
- },
58
- "source-sans-3": {
59
- family: "Source Sans 3 Variable, DejaVu Sans, Arial, sans-serif",
60
- fontsource: {
61
- packageName: "@fontsource-variable/source-sans-3",
62
- fileId: "source-sans-3",
63
- },
64
- },
65
- "source-serif-4": {
66
- family: "Source Serif 4 Variable, DejaVu Serif, Georgia, serif",
67
- fontsource: {
68
- packageName: "@fontsource-variable/source-serif-4",
69
- fileId: "source-serif-4",
70
- },
71
- },
72
- "public-sans": {
73
- family: "Public Sans Variable, DejaVu Sans, Arial, sans-serif",
74
- fontsource: {
75
- packageName: "@fontsource-variable/public-sans",
76
- fileId: "public-sans",
77
- },
78
- },
79
- "geist-mono": {
80
- family: "Geist Mono Variable, DejaVu Sans Mono, monospace",
81
- fontsource: {
82
- packageName: "@fontsource-variable/geist-mono",
83
- fileId: "geist-mono",
84
- },
85
- },
86
- "jetbrains-mono": {
87
- family: "JetBrains Mono Variable, DejaVu Sans Mono, monospace",
88
- fontsource: {
89
- packageName: "@fontsource-variable/jetbrains-mono",
90
- fileId: "jetbrains-mono",
91
- },
92
- },
93
- "google-sans-code": {
94
- family: "Google Sans Code Variable, DejaVu Sans Mono, monospace",
95
- fontsource: {
96
- packageName: "@fontsource-variable/google-sans-code",
97
- fileId: "google-sans-code",
98
- },
99
- },
100
- "system-sans": {
101
- family: DEFAULT_OG_FONT_FAMILY,
102
- },
103
- "system-serif": {
104
- family: "DejaVu Serif, Georgia, serif",
105
- },
106
- "system-mono": {
107
- family: "DejaVu Sans Mono, monospace",
108
- },
109
- };
110
-
111
- function trimToUndefined(value) {
112
- return typeof value === "string" && value.trim().length > 0
113
- ? value.trim()
114
- : undefined;
115
- }
116
-
117
- function normalizeFontPresetId(value) {
118
- if (typeof value !== "string") return undefined;
119
- const normalized = value.trim().toLowerCase();
120
- return FONT_PRESET_ID_SET.has(normalized) ? normalized : undefined;
121
- }
122
-
123
- function getFontPreset(presetId) {
124
- return FONT_PRESETS[presetId] ?? FONT_PRESETS[DEFAULT_TEXT_FONT_PRESET];
125
- }
126
-
127
- async function getBaseFontsourceFont(fontsource, subset) {
128
- const cacheKey = `${fontsource.packageName}:${subset}`;
129
- const cachedFont = baseFontCache.get(cacheKey);
130
- if (cachedFont) return cachedFont;
131
-
132
- const fontPath = require.resolve(
133
- `${fontsource.packageName}/files/${fontsource.fileId}-${subset}-wght-normal.woff2`,
134
- );
135
- const woff2Buffer = fs.readFileSync(fontPath);
136
- const ttfBuffer = Buffer.from(await wawoff2.decompress(woff2Buffer));
137
- const font = createFont(ttfBuffer);
138
- baseFontCache.set(cacheKey, font);
139
- return font;
140
- }
141
-
142
- function getWeightedFont(font, fontsource, subset, fontWeight) {
143
- const cacheKey = `${fontsource.packageName}:${subset}:${fontWeight}`;
144
- const cachedFont = weightedFontCache.get(cacheKey);
145
- if (cachedFont) return cachedFont;
146
-
147
- const weightAxis = font.variationAxes?.wght;
148
- const weightedFont = weightAxis
149
- ? font.getVariation({
150
- wght: Math.max(weightAxis.min, Math.min(weightAxis.max, fontWeight)),
151
- })
152
- : font;
153
-
154
- weightedFontCache.set(cacheKey, weightedFont);
155
- return weightedFont;
156
- }
157
-
158
- async function getFontsourceFonts(fontsource, fontWeight) {
159
- if (!fontsource) return [];
160
-
161
- const fonts = [];
162
- for (const subset of OG_FONT_SUBSETS) {
163
- const font = await getBaseFontsourceFont(fontsource, subset);
164
- fonts.push(getWeightedFont(font, fontsource, subset, fontWeight));
165
- }
166
-
167
- return fonts;
168
- }
169
-
170
- async function getFontRenderer(presetId, fontWeight) {
171
- const fontPreset = getFontPreset(presetId);
172
- return {
173
- family: fontPreset.family,
174
- fonts: await getFontsourceFonts(fontPreset.fontsource, fontWeight),
175
- };
176
- }
177
-
178
- async function resolveOgFontConfig(fonts) {
179
- const textFontId =
180
- normalizeFontPresetId(fonts?.text) ?? DEFAULT_TEXT_FONT_PRESET;
181
- const headingFontId = normalizeFontPresetId(fonts?.heading) ?? textFontId;
182
-
183
- return {
184
- titleFont: await getFontRenderer(headingFontId, 700),
185
- descriptionFont: await getFontRenderer(textFontId, 400),
186
- };
187
- }
188
-
189
- function decodeHtmlEntities(value) {
190
- if (!value || typeof value !== "string") return "";
191
-
192
- const named = {
193
- amp: "&",
194
- lt: "<",
195
- gt: ">",
196
- quot: '"',
197
- apos: "'",
198
- nbsp: " ",
199
- };
200
-
201
- return value
202
- .replace(/&#(\d+);/g, (_, dec) => {
203
- const code = Number.parseInt(dec, 10);
204
- return Number.isFinite(code) ? String.fromCodePoint(code) : _;
205
- })
206
- .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
207
- const code = Number.parseInt(hex, 16);
208
- return Number.isFinite(code) ? String.fromCodePoint(code) : _;
209
- })
210
- .replace(/&([a-zA-Z]+);/g, (entity, name) => {
211
- const decoded = named[name.toLowerCase()];
212
- return decoded ?? entity;
213
- });
214
- }
215
-
216
- function normalizeRoutePath(routePath) {
217
- if (!routePath || routePath === "/") return "/";
218
- if (!routePath.startsWith("/")) routePath = `/${routePath}`;
219
- return routePath.endsWith("/") ? routePath : `${routePath}/`;
220
- }
221
-
222
- function truncateText(value, maxLength) {
223
- const text = trimToUndefined(value);
224
- if (!text) return "";
225
- if (text.length <= maxLength) return text;
226
- return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
227
- }
228
-
229
- function stripSiteSuffixFromTitle(title, siteName) {
230
- const trimmedTitle = trimToUndefined(title);
231
- const trimmedSiteName = trimToUndefined(siteName);
232
- if (!trimmedTitle || !trimmedSiteName) return trimmedTitle ?? "";
233
-
234
- const suffixes = [` | ${trimmedSiteName}`, ` - ${trimmedSiteName}`];
235
- for (const suffix of suffixes) {
236
- if (trimmedTitle.endsWith(suffix)) {
237
- return trimmedTitle.slice(0, -suffix.length).trim();
238
- }
239
- }
240
-
241
- return trimmedTitle;
242
- }
243
-
244
- function routeToImageRelativePath(routePath) {
245
- const normalizedRoutePath = normalizeRoutePath(routePath);
246
- if (normalizedRoutePath === "/") return "index.png";
247
- return `${normalizedRoutePath.slice(1, -1)}.png`;
248
- }
249
-
250
- function escapeXml(value) {
251
- return value
252
- .replace(/&/g, "&amp;")
253
- .replace(/</g, "&lt;")
254
- .replace(/>/g, "&gt;")
255
- .replace(/"/g, "&quot;")
256
- .replace(/'/g, "&apos;");
257
- }
258
-
259
- function findGlyphFont(fonts, codePoint) {
260
- for (const font of fonts) {
261
- if (font.hasGlyphForCodePoint(codePoint)) {
262
- return font;
263
- }
264
- }
265
- return fonts[0];
266
- }
267
-
268
- function splitTextIntoFontRuns(line, fonts) {
269
- const runs = [];
270
- for (const character of Array.from(line)) {
271
- const codePoint = character.codePointAt(0);
272
- if (codePoint === undefined) continue;
273
-
274
- const font = findGlyphFont(fonts, codePoint);
275
- const previousRun = runs[runs.length - 1];
276
- if (previousRun?.font === font) {
277
- previousRun.text += character;
278
- } else {
279
- runs.push({ font, text: character });
280
- }
281
- }
282
-
283
- return runs;
284
- }
285
-
286
- function renderPathTextLine({
287
- line,
288
- x,
289
- top,
290
- fontRenderer,
291
- fontSize,
292
- fill,
293
- fillOpacity,
294
- letterSpacingEm = 0,
295
- }) {
296
- const fonts = fontRenderer.fonts ?? [];
297
- const primaryFont = fonts[0];
298
- if (!primaryFont) {
299
- return "";
300
- }
301
-
302
- const primaryScale = fontSize / primaryFont.unitsPerEm;
303
- const baselineY = top + primaryFont.ascent * primaryScale;
304
- const letterSpacingPx = fontSize * letterSpacingEm;
305
- const pathData = [];
306
- let cursorX = x;
307
-
308
- for (const run of splitTextIntoFontRuns(line, fonts)) {
309
- const font = run.font;
310
- const scale = fontSize / font.unitsPerEm;
311
- const glyphRun = font.layout(run.text);
312
-
313
- for (let index = 0; index < glyphRun.glyphs.length; index += 1) {
314
- const glyph = glyphRun.glyphs[index];
315
- const position = glyphRun.positions[index] ?? {};
316
- const glyphPath = glyph.path;
317
- const xOffset = (position.xOffset ?? 0) * scale;
318
- const yOffset = (position.yOffset ?? 0) * scale;
319
-
320
- if (glyphPath.commands.length > 0) {
321
- pathData.push(
322
- glyphPath
323
- .transform(
324
- scale,
325
- 0,
326
- 0,
327
- -scale,
328
- cursorX + xOffset,
329
- baselineY - yOffset,
330
- )
331
- .toSVG(),
332
- );
333
- }
334
-
335
- cursorX +=
336
- ((position.xAdvance ?? glyph.advanceWidth ?? 0) * scale) +
337
- letterSpacingPx;
338
- }
339
- }
340
-
341
- if (pathData.length === 0) {
342
- return "";
343
- }
344
-
345
- const opacityAttribute =
346
- fillOpacity === undefined ? "" : ` fill-opacity="${fillOpacity}"`;
347
-
348
- return `<path d="${pathData.join(" ")}" fill="${fill}"${opacityAttribute} />`;
349
- }
350
-
351
- function renderTextLine({
352
- line,
353
- x,
354
- top,
355
- fontRenderer,
356
- fontSize,
357
- fontWeight,
358
- fill,
359
- fillOpacity,
360
- letterSpacingEm = 0,
361
- }) {
362
- const pathText = renderPathTextLine({
363
- line,
364
- x,
365
- top,
366
- fontRenderer,
367
- fontSize,
368
- fill,
369
- fillOpacity,
370
- letterSpacingEm,
371
- });
372
- if (pathText) return pathText;
373
-
374
- const opacityAttribute =
375
- fillOpacity === undefined ? "" : ` fill-opacity="${fillOpacity}"`;
376
- const letterSpacing = letterSpacingEm === 0 ? 0 : `${letterSpacingEm}em`;
377
- return `<text x="${x}" y="${top}" fill="${fill}"${opacityAttribute} font-family="${escapeXml(
378
- fontRenderer.family,
379
- )}" font-size="${fontSize}" font-weight="${fontWeight}" letter-spacing="${letterSpacing}" dominant-baseline="hanging">${escapeXml(
380
- line,
381
- )}</text>`;
382
- }
383
-
384
- function wrapText(input, maxChars, maxLines) {
385
- const text = trimToUndefined(input);
386
- if (!text) return [];
387
-
388
- const words = text.split(/\s+/).filter(Boolean);
389
- const lines = [];
390
- let current = "";
391
- let index = 0;
392
-
393
- while (index < words.length) {
394
- const word = words[index];
395
- const candidate = current ? `${current} ${word}` : word;
396
-
397
- if (candidate.length <= maxChars) {
398
- current = candidate;
399
- index += 1;
400
- continue;
401
- }
402
-
403
- if (!current) {
404
- current = word.slice(0, maxChars);
405
- index += 1;
406
- }
407
-
408
- lines.push(current);
409
- current = "";
410
-
411
- if (lines.length === maxLines) {
412
- break;
413
- }
414
- }
415
-
416
- if (lines.length < maxLines && current) {
417
- lines.push(current);
418
- }
419
-
420
- const consumedAllWords = index >= words.length;
421
- if (!consumedAllWords && lines.length > 0) {
422
- lines[lines.length - 1] = truncateText(lines[lines.length - 1], maxChars);
423
- }
424
-
425
- return lines.slice(0, maxLines);
426
- }
427
-
428
- function mimeTypeFromFilePath(filePath) {
429
- const extension = path.extname(filePath).toLowerCase();
430
- if (extension === ".svg") return "image/svg+xml";
431
- if (extension === ".png") return "image/png";
432
- if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
433
- if (extension === ".webp") return "image/webp";
434
- if (extension === ".gif") return "image/gif";
435
- return undefined;
436
- }
437
-
438
- function safeDecodeURIComponent(value) {
439
- try {
440
- return decodeURIComponent(value);
441
- } catch {
442
- return value;
443
- }
444
- }
445
-
446
- function resolveLocalLogoPath(logoPath) {
447
- const stripped = logoPath.split("#")[0].split("?")[0];
448
- const decoded = safeDecodeURIComponent(stripped);
449
- const relativePath = decoded.startsWith("/") ? decoded.slice(1) : decoded;
450
- const absolutePath = path.resolve(DIST_DIR, relativePath);
451
- const distRoot = path.resolve(DIST_DIR) + path.sep;
452
-
453
- if (!absolutePath.startsWith(distRoot) && absolutePath !== path.resolve(DIST_DIR)) {
454
- return undefined;
455
- }
456
-
457
- return absolutePath;
458
- }
459
-
460
- function resolveLogoDataUri(logoPath) {
461
- const resolvedLogoPath = trimToUndefined(logoPath);
462
- if (!resolvedLogoPath) return undefined;
463
- if (/^https?:\/\//i.test(resolvedLogoPath)) return undefined;
464
-
465
- const localLogoPath = resolveLocalLogoPath(resolvedLogoPath);
466
- if (!localLogoPath || !fs.existsSync(localLogoPath)) return undefined;
467
-
468
- const stats = fs.statSync(localLogoPath);
469
- if (!stats.isFile()) return undefined;
470
-
471
- const mimeType = mimeTypeFromFilePath(localLogoPath);
472
- if (!mimeType) return undefined;
473
-
474
- const buffer = fs.readFileSync(localLogoPath);
475
- return `data:${mimeType};base64,${buffer.toString("base64")}`;
476
- }
477
-
478
- function renderOgSvg({
479
- title,
480
- description,
481
- siteName,
482
- logoDataUri,
483
- titleFont,
484
- descriptionFont,
485
- }) {
486
- const resolvedTitle = trimToUndefined(title) ?? siteName;
487
- const titleLines = wrapText(resolvedTitle, 24, 2);
488
- if (titleLines.length === 0) {
489
- titleLines.push(siteName);
490
- }
491
-
492
- const resolvedDescription = trimToUndefined(description) ?? "";
493
- const descriptionLines = wrapText(resolvedDescription, 58, 2);
494
-
495
- const titleLineHeight = 76;
496
- const descriptionLineHeight = 38;
497
- const spaceBetweenTitleAndDescription = 30;
498
- const bottomPadding = 56;
499
- const titleBlockHeight =
500
- titleLines.length * titleLineHeight +
501
- spaceBetweenTitleAndDescription +
502
- descriptionLines.length * descriptionLineHeight;
503
- const titleBlockTop = OG_HEIGHT - bottomPadding - titleBlockHeight;
504
-
505
- let currentY = titleBlockTop;
506
- const titleSvg = titleLines
507
- .map((line) => {
508
- const element = renderTextLine({
509
- line,
510
- x: 56,
511
- top: currentY,
512
- fontRenderer: titleFont,
513
- fontSize: 72,
514
- fontWeight: 700,
515
- fill: "#FFFFFF",
516
- letterSpacingEm: -0.02,
517
- });
518
- currentY += titleLineHeight;
519
- return element;
520
- })
521
- .join("");
522
-
523
- currentY += spaceBetweenTitleAndDescription;
524
- const descriptionSvg = descriptionLines
525
- .map((line) => {
526
- const element = renderTextLine({
527
- line,
528
- x: 56,
529
- top: currentY,
530
- fontRenderer: descriptionFont,
531
- fontSize: 32,
532
- fontWeight: 400,
533
- fill: "#FFFFFF",
534
- fillOpacity: 0.82,
535
- });
536
- currentY += descriptionLineHeight;
537
- return element;
538
- })
539
- .join("");
540
-
541
- const logoSvg = logoDataUri
542
- ? `<image href="${escapeXml(logoDataUri)}" x="56" y="56" width="420" height="132" preserveAspectRatio="xMinYMid meet" />`
543
- : renderTextLine({
544
- line: siteName,
545
- x: 56,
546
- top: 74,
547
- fontRenderer: titleFont,
548
- fontSize: 64,
549
- fontWeight: 700,
550
- fill: "#FFFFFF",
551
- letterSpacingEm: -0.02,
552
- });
553
-
554
- return `<?xml version="1.0" encoding="UTF-8"?>
555
- <svg width="${OG_WIDTH}" height="${OG_HEIGHT}" viewBox="0 0 ${OG_WIDTH} ${OG_HEIGHT}" xmlns="http://www.w3.org/2000/svg">
556
- <defs>
557
- <radialGradient id="ogGlow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1540 -360) rotate(90) scale(1600)">
558
- <stop offset="0" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0.56" />
559
- <stop offset="0.74" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0" />
560
- <stop offset="1" stop-color="${OG_PRIMARY_COLOR}" stop-opacity="0" />
561
- </radialGradient>
562
- </defs>
563
- <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="${OG_BACKGROUND}" />
564
- <rect width="${OG_WIDTH}" height="${OG_HEIGHT}" fill="url(#ogGlow)" />
565
- ${logoSvg}
566
- ${titleSvg}
567
- ${descriptionSvg}
568
- </svg>`;
569
- }
570
-
571
- function writeImageForRoute({
572
- routePath,
573
- title,
574
- description,
575
- siteName,
576
- logoDataUri,
577
- ogFontConfig,
578
- }) {
579
- const svg = renderOgSvg({
580
- title,
581
- description,
582
- siteName,
583
- logoDataUri,
584
- titleFont: ogFontConfig.titleFont,
585
- descriptionFont: ogFontConfig.descriptionFont,
586
- });
587
-
588
- const resvg = new Resvg(svg, {
589
- font: {
590
- loadSystemFonts: true,
591
- defaultFontFamily: "DejaVu Sans",
592
- sansSerifFamily: "DejaVu Sans",
593
- serifFamily: "DejaVu Serif",
594
- monospaceFamily: "DejaVu Sans Mono",
595
- },
596
- });
597
-
598
- const pngData = resvg.render().asPng();
599
- const outputRelativePath = routeToImageRelativePath(routePath);
600
- const outputPath = path.join(OUTPUT_DIR, ...outputRelativePath.split("/"));
601
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
602
- fs.writeFileSync(outputPath, pngData);
603
- }
604
-
605
- async function main() {
606
- if (!fs.existsSync(DIST_DIR)) {
607
- console.warn("Skipping OG image generation: dist directory not found.");
608
- return;
609
- }
610
-
611
- if (!fs.existsSync(MANIFEST_PATH)) {
612
- console.warn("Skipping OG image generation: OG metadata manifest not found.");
613
- return;
614
- }
615
-
616
- const rawManifest = fs.readFileSync(MANIFEST_PATH, "utf8");
617
- const manifest = JSON.parse(rawManifest);
618
-
619
- const defaults = manifest?.defaults ?? {};
620
- const pages = manifest?.pages ?? {};
621
- const siteName = trimToUndefined(decodeHtmlEntities(defaults.siteName)) ??
622
- "Documentation";
623
- const logoDataUri = resolveLogoDataUri(defaults.logoDark);
624
- const ogFontConfig = await resolveOgFontConfig(defaults.fonts);
625
-
626
- const routes = Object.keys(pages).sort();
627
- if (routes.length === 0) {
628
- console.warn("Skipping OG image generation: no pages found in metadata.");
629
- return;
630
- }
631
-
632
- let generatedCount = 0;
633
- for (const routePath of routes) {
634
- const page = pages[routePath] ?? {};
635
- const decodedTitle = decodeHtmlEntities(page.title);
636
- const decodedDescription = decodeHtmlEntities(page.description);
637
-
638
- const cleanedTitle = stripSiteSuffixFromTitle(decodedTitle, siteName);
639
- const title = truncateText(cleanedTitle || siteName, 80);
640
- const fallbackDescription = `Learn about ${title} in the ${siteName} documentation.`;
641
- const description = truncateText(
642
- decodedDescription || fallbackDescription,
643
- 150,
644
- );
645
-
646
- writeImageForRoute({
647
- routePath,
648
- title,
649
- description,
650
- siteName,
651
- logoDataUri,
652
- ogFontConfig,
653
- });
654
-
655
- generatedCount += 1;
656
- }
657
-
658
- console.log(
659
- `✅ Generated ${generatedCount} OG image${generatedCount === 1 ? "" : "s"} in ${OUTPUT_DIR}`,
660
- );
661
- }
662
-
663
- main().catch((error) => {
664
- console.error("Failed to generate OG images.");
665
- console.error(error);
666
- process.exitCode = 1;
667
- });