@docubook/flame 1.5.3 → 1.5.4
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/.docu/lib/build.deno.js +3 -3
- package/.docu/lib/{build.impl-O3Z5XIGM.js → build.impl-CPFOQ7JW.js} +3 -3
- package/.docu/lib/build.node.js +3 -3
- package/.docu/lib/{chunk-VX4HLVHE.js → chunk-BDWN6AHQ.js} +2 -2
- package/.docu/lib/{chunk-YI76L7LB.js → chunk-C67LPMQA.js} +18 -0
- package/.docu/lib/{chunk-WCMRAAWH.js → chunk-GXQB4ETN.js} +1 -1
- package/.docu/lib/{chunk-YENO4SVR.js → chunk-NYCIAZTI.js} +28 -2
- package/.docu/lib/{chunk-7VXEOH7X.js → chunk-VKMZLLG5.js} +1 -1
- package/.docu/lib/{chunk-EIKBL7SD.js → chunk-XLH7Z4QO.js} +1 -1
- package/.docu/lib/clean.js +1 -1
- package/.docu/lib/deploy.deno.js +1 -1
- package/.docu/lib/deploy.node.js +1 -1
- package/.docu/lib/preview.deno.js +2 -2
- package/.docu/lib/preview.node.js +2 -2
- package/.docu/lib/server.deno.js +3 -3
- package/.docu/lib/server.node.js +3 -3
- package/.docu/node/build.impl.ts +6 -0
- package/.docu/node/build.ts +6 -0
- package/.docu/node/html.shared.ts +17 -0
- package/.docu/node/html.ts +17 -0
- package/.docu/node/seo.ts +42 -0
- package/.docu/node/types.ts +2 -0
- package/docu.schema.json +5 -1
- package/package.json +4 -4
- package/template/docs/assets/images/og.png +0 -0
- package/template/docu.json +2 -1
package/.docu/lib/build.deno.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runBuildCli
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-NYCIAZTI.js";
|
|
4
|
+
import "./chunk-C67LPMQA.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-GXQB4ETN.js";
|
|
7
7
|
import "./chunk-J5NMYSBJ.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/build.deno.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runBuild,
|
|
3
3
|
runBuildCli
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-NYCIAZTI.js";
|
|
5
|
+
import "./chunk-C67LPMQA.js";
|
|
6
6
|
import "./chunk-EOK6KATZ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-GXQB4ETN.js";
|
|
8
8
|
import "./chunk-J5NMYSBJ.js";
|
|
9
9
|
export {
|
|
10
10
|
runBuild,
|
package/.docu/lib/build.node.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runBuildCli
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-NYCIAZTI.js";
|
|
4
|
+
import "./chunk-C67LPMQA.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-GXQB4ETN.js";
|
|
7
7
|
import "./chunk-J5NMYSBJ.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/build.node.ts
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
htmlShell,
|
|
15
15
|
initSentry,
|
|
16
16
|
loadPlugins
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-C67LPMQA.js";
|
|
18
18
|
import {
|
|
19
19
|
SECURITY_HEADERS,
|
|
20
20
|
generateNonce,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./chunk-EOK6KATZ.js";
|
|
28
28
|
import {
|
|
29
29
|
logger
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-GXQB4ETN.js";
|
|
31
31
|
import {
|
|
32
32
|
DIST_DIR,
|
|
33
33
|
DOCS_DIR,
|
|
@@ -2336,6 +2336,23 @@ function htmlShell(opts) {
|
|
|
2336
2336
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
2337
2337
|
const assetPrefix = depthPrefix + "assets/";
|
|
2338
2338
|
const resolvePath = (path) => path.startsWith("/") ? depthPrefix + path.slice(1) : path;
|
|
2339
|
+
let seoTags = "";
|
|
2340
|
+
if (opts.seo) {
|
|
2341
|
+
const s = opts.seo;
|
|
2342
|
+
const e = escapeHtml;
|
|
2343
|
+
seoTags = `
|
|
2344
|
+
<meta property="og:title" content="${e(title)}" />
|
|
2345
|
+
<meta property="og:description" content="${e(description)}" />
|
|
2346
|
+
<meta property="og:url" content="${e(s.url)}" />
|
|
2347
|
+
<meta property="og:type" content="website" />
|
|
2348
|
+
<meta property="og:site_name" content="${e(s.siteName)}" />
|
|
2349
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
2350
|
+
<link rel="canonical" href="${e(s.url)}" />`;
|
|
2351
|
+
if (s.image) {
|
|
2352
|
+
seoTags += `
|
|
2353
|
+
<meta property="og:image" content="${e(s.image)}" />`;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2339
2356
|
return `<!DOCTYPE html>
|
|
2340
2357
|
<html lang="en">
|
|
2341
2358
|
<head>
|
|
@@ -2346,6 +2363,7 @@ function htmlShell(opts) {
|
|
|
2346
2363
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${escapeHtml(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
2347
2364
|
<link rel="stylesheet" href="${escapeHtml(assetPrefix + css)}">
|
|
2348
2365
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">` : ""}
|
|
2366
|
+
${seoTags}
|
|
2349
2367
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
2350
2368
|
</head>
|
|
2351
2369
|
<body>
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
htmlShell,
|
|
14
14
|
initSentry,
|
|
15
15
|
loadPlugins
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-C67LPMQA.js";
|
|
17
17
|
import {
|
|
18
18
|
generateNonce,
|
|
19
19
|
scanMdxFiles
|
|
20
20
|
} from "./chunk-EOK6KATZ.js";
|
|
21
21
|
import {
|
|
22
22
|
logger
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-GXQB4ETN.js";
|
|
24
24
|
import {
|
|
25
25
|
ASSETS_DIR,
|
|
26
26
|
CACHE_FILE,
|
|
@@ -38,6 +38,27 @@ import { createHash } from "node:crypto";
|
|
|
38
38
|
import { join, dirname } from "node:path";
|
|
39
39
|
import React from "react";
|
|
40
40
|
import { renderToString } from "react-dom/server";
|
|
41
|
+
|
|
42
|
+
// .docu/node/seo.ts
|
|
43
|
+
function buildSeoMeta(config, frontmatter, slug) {
|
|
44
|
+
const baseURL = config.meta?.baseURL?.replace(/\/+$/, "") || "";
|
|
45
|
+
const url = slug ? `${baseURL}/docs/${slug}` : `${baseURL}/`;
|
|
46
|
+
const result = {
|
|
47
|
+
url,
|
|
48
|
+
siteName: config.meta?.title || ""
|
|
49
|
+
};
|
|
50
|
+
const image = typeof frontmatter.image === "string" && frontmatter.image || config.meta?.ogImage;
|
|
51
|
+
if (image) {
|
|
52
|
+
try {
|
|
53
|
+
result.image = new URL(image, image.startsWith("/") ? baseURL : `${baseURL}/docs/`).href;
|
|
54
|
+
} catch {
|
|
55
|
+
result.image = image;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// .docu/node/build.impl.ts
|
|
41
62
|
function parseArgs() {
|
|
42
63
|
const args = process.argv.slice(2);
|
|
43
64
|
return {
|
|
@@ -123,11 +144,13 @@ ${msg}`, { cause: err });
|
|
|
123
144
|
const bodyExtra = builder?.collectBody(ctx);
|
|
124
145
|
const depth = slug ? slug.split("/").length : 1;
|
|
125
146
|
const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
147
|
+
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
126
148
|
let html = htmlShell({
|
|
127
149
|
title,
|
|
128
150
|
description,
|
|
129
151
|
body,
|
|
130
152
|
favicon,
|
|
153
|
+
seo,
|
|
131
154
|
css: assetManifest.css,
|
|
132
155
|
js: assetManifest.js,
|
|
133
156
|
nonce,
|
|
@@ -298,11 +321,13 @@ async function runBuild() {
|
|
|
298
321
|
}
|
|
299
322
|
const landingPage = React.createElement(IndexPage);
|
|
300
323
|
const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
324
|
+
const landingSeo = buildSeoMeta(docuConfig, docuConfig.meta, "");
|
|
301
325
|
const landingHtml = htmlShell({
|
|
302
326
|
title: docuConfig.meta?.title || "DocuBook",
|
|
303
327
|
description: docuConfig.meta?.description || "",
|
|
304
328
|
body: renderToString(landingPage),
|
|
305
329
|
favicon: landingFavicon,
|
|
330
|
+
seo: landingSeo,
|
|
306
331
|
css: assetManifest.css,
|
|
307
332
|
js: assetManifest.js,
|
|
308
333
|
nonce: generateNonce(),
|
|
@@ -320,6 +345,7 @@ async function runBuild() {
|
|
|
320
345
|
description: "",
|
|
321
346
|
body: renderToString(notFoundPage),
|
|
322
347
|
favicon: notFoundFavicon,
|
|
348
|
+
headExtra: ['<meta name="robots" content="noindex,follow">'],
|
|
323
349
|
css: assetManifest.css,
|
|
324
350
|
js: assetManifest.js,
|
|
325
351
|
nonce: generateNonce(),
|
|
@@ -12,7 +12,7 @@ var WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
|
12
12
|
async function runDeploy() {
|
|
13
13
|
console.log("\u{1F4E6} Building for production...\n");
|
|
14
14
|
process.env.NODE_ENV = "production";
|
|
15
|
-
const { runBuildCli } = await import("./build.impl-
|
|
15
|
+
const { runBuildCli } = await import("./build.impl-CPFOQ7JW.js");
|
|
16
16
|
await runBuildCli();
|
|
17
17
|
await writeFile(join(DIST_DIR, ".nojekyll"), "");
|
|
18
18
|
if (!existsSync(WORKFLOW_FILE)) {
|
package/.docu/lib/clean.js
CHANGED
package/.docu/lib/deploy.deno.js
CHANGED
package/.docu/lib/deploy.node.js
CHANGED
package/.docu/lib/server.deno.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-BDWN6AHQ.js";
|
|
4
|
+
import "./chunk-C67LPMQA.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-GXQB4ETN.js";
|
|
7
7
|
import "./chunk-J5NMYSBJ.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/server.deno.ts
|
package/.docu/lib/server.node.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-BDWN6AHQ.js";
|
|
4
|
+
import "./chunk-C67LPMQA.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-GXQB4ETN.js";
|
|
7
7
|
import "./chunk-J5NMYSBJ.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/server.node.ts
|
package/.docu/node/build.impl.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { scanMdxFiles } from "./utils";
|
|
|
33
33
|
import type { BuildCache, CliArgs } from "./types";
|
|
34
34
|
import { generateNonce } from "./security";
|
|
35
35
|
import type { PageMeta, PageContext } from "./plugin";
|
|
36
|
+
import { buildSeoMeta } from "./seo";
|
|
36
37
|
import DocsPage from "../pages/docs/[[...slug]]";
|
|
37
38
|
import IndexPage from "../pages/index";
|
|
38
39
|
import NotFoundPage from "../pages/404";
|
|
@@ -147,11 +148,13 @@ async function renderDocsPage(
|
|
|
147
148
|
|
|
148
149
|
const depth = slug ? slug.split("/").length : 1;
|
|
149
150
|
const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
151
|
+
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
150
152
|
let html = htmlShell({
|
|
151
153
|
title,
|
|
152
154
|
description,
|
|
153
155
|
body,
|
|
154
156
|
favicon,
|
|
157
|
+
seo,
|
|
155
158
|
css: assetManifest.css,
|
|
156
159
|
js: assetManifest.js,
|
|
157
160
|
nonce,
|
|
@@ -345,11 +348,13 @@ export async function runBuild(): Promise<void> {
|
|
|
345
348
|
|
|
346
349
|
const landingPage = React.createElement(IndexPage);
|
|
347
350
|
const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
351
|
+
const landingSeo = buildSeoMeta(docuConfig, docuConfig.meta as Record<string, unknown>, "");
|
|
348
352
|
const landingHtml = htmlShell({
|
|
349
353
|
title: docuConfig.meta?.title || "DocuBook",
|
|
350
354
|
description: docuConfig.meta?.description || "",
|
|
351
355
|
body: renderToString(landingPage),
|
|
352
356
|
favicon: landingFavicon,
|
|
357
|
+
seo: landingSeo,
|
|
353
358
|
css: assetManifest.css,
|
|
354
359
|
js: assetManifest.js,
|
|
355
360
|
nonce: generateNonce(),
|
|
@@ -368,6 +373,7 @@ export async function runBuild(): Promise<void> {
|
|
|
368
373
|
description: "",
|
|
369
374
|
body: renderToString(notFoundPage),
|
|
370
375
|
favicon: notFoundFavicon,
|
|
376
|
+
headExtra: ['<meta name="robots" content="noindex,follow">'],
|
|
371
377
|
css: assetManifest.css,
|
|
372
378
|
js: assetManifest.js,
|
|
373
379
|
nonce: generateNonce(),
|
package/.docu/node/build.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { scanMdxFiles } from "./utils";
|
|
|
25
25
|
import type { BuildCache, CliArgs } from "./types";
|
|
26
26
|
import { generateNonce } from "./security";
|
|
27
27
|
import type { PageMeta, PageContext } from "./plugin";
|
|
28
|
+
import { buildSeoMeta } from "./seo";
|
|
28
29
|
import DocsPage from "../pages/docs/[[...slug]]";
|
|
29
30
|
import IndexPage from "../pages/index";
|
|
30
31
|
import NotFoundPage from "../pages/404";
|
|
@@ -140,11 +141,13 @@ async function renderDocsPage(
|
|
|
140
141
|
|
|
141
142
|
const depth = slug ? slug.split("/").length : 1;
|
|
142
143
|
const favicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
144
|
+
const seo = buildSeoMeta(docuConfig, frontmatter, slug || "");
|
|
143
145
|
let html = htmlShell({
|
|
144
146
|
title,
|
|
145
147
|
description,
|
|
146
148
|
body,
|
|
147
149
|
favicon,
|
|
150
|
+
seo,
|
|
148
151
|
css: assetManifest.css,
|
|
149
152
|
js: assetManifest.js,
|
|
150
153
|
nonce,
|
|
@@ -335,11 +338,13 @@ async function build() {
|
|
|
335
338
|
|
|
336
339
|
const landingPage = React.createElement(IndexPage);
|
|
337
340
|
const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
341
|
+
const landingSeo = buildSeoMeta(docuConfig, {}, "");
|
|
338
342
|
const landingHtml = htmlShell({
|
|
339
343
|
title: docuConfig.meta?.title || "DocuBook",
|
|
340
344
|
description: docuConfig.meta?.description || "",
|
|
341
345
|
body: renderToString(landingPage),
|
|
342
346
|
favicon: landingFavicon,
|
|
347
|
+
seo: landingSeo,
|
|
343
348
|
css: assetManifest.css,
|
|
344
349
|
js: assetManifest.js,
|
|
345
350
|
nonce: generateNonce(),
|
|
@@ -358,6 +363,7 @@ async function build() {
|
|
|
358
363
|
description: "",
|
|
359
364
|
body: renderToString(notFoundPage),
|
|
360
365
|
favicon: notFoundFavicon,
|
|
366
|
+
headExtra: ['<meta name="robots" content="noindex,follow">'],
|
|
361
367
|
css: assetManifest.css,
|
|
362
368
|
js: assetManifest.js,
|
|
363
369
|
nonce: generateNonce(),
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { escapeHtml } from "./escapeHtml";
|
|
10
10
|
|
|
11
|
+
import type { SeoMeta } from "./seo";
|
|
12
|
+
|
|
11
13
|
export interface HtmlShellOptions {
|
|
12
14
|
title: string;
|
|
13
15
|
description: string;
|
|
@@ -30,6 +32,8 @@ export interface HtmlShellOptions {
|
|
|
30
32
|
headExtra?: string[];
|
|
31
33
|
/** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
|
|
32
34
|
bodyExtra?: string[];
|
|
35
|
+
/** SEO meta tags derived from config + frontmatter */
|
|
36
|
+
seo?: SeoMeta;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
@@ -55,6 +59,18 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
55
59
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
56
60
|
const assetPrefix = depthPrefix + "assets/";
|
|
57
61
|
const resolvePath = (path: string) => (path.startsWith("/") ? depthPrefix + path.slice(1) : path);
|
|
62
|
+
|
|
63
|
+
// Build SEO meta tags (OG, Twitter, canonical)
|
|
64
|
+
let seoTags = "";
|
|
65
|
+
if (opts.seo) {
|
|
66
|
+
const s = opts.seo;
|
|
67
|
+
const e = escapeHtml;
|
|
68
|
+
seoTags = `\n <meta property="og:title" content="${e(title)}" />\n <meta property="og:description" content="${e(description)}" />\n <meta property="og:url" content="${e(s.url)}" />\n <meta property="og:type" content="website" />\n <meta property="og:site_name" content="${e(s.siteName)}" />\n <meta name="twitter:card" content="summary_large_image" />\n <link rel="canonical" href="${e(s.url)}" />`;
|
|
69
|
+
if (s.image) {
|
|
70
|
+
seoTags += `\n <meta property="og:image" content="${e(s.image)}" />`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
return `<!DOCTYPE html>
|
|
59
75
|
<html lang="en">
|
|
60
76
|
<head>
|
|
@@ -65,6 +81,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
65
81
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${escapeHtml(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
66
82
|
<link rel="stylesheet" href="${escapeHtml(assetPrefix + css)}">
|
|
67
83
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">` : ""}
|
|
84
|
+
${seoTags}
|
|
68
85
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
69
86
|
</head>
|
|
70
87
|
<body>
|
package/.docu/node/html.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SeoMeta } from "./seo";
|
|
2
|
+
|
|
1
3
|
export interface HtmlShellOptions {
|
|
2
4
|
title: string;
|
|
3
5
|
description: string;
|
|
@@ -20,6 +22,8 @@ export interface HtmlShellOptions {
|
|
|
20
22
|
headExtra?: string[];
|
|
21
23
|
/** HTML strings to inject before `</body>`, after the main script (from plugin `injectBody` hooks). */
|
|
22
24
|
bodyExtra?: string[];
|
|
25
|
+
/** SEO meta tags derived from config + frontmatter */
|
|
26
|
+
seo?: SeoMeta;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function htmlShell(opts: HtmlShellOptions): string {
|
|
@@ -45,6 +49,18 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
45
49
|
const depthPrefix = depth === 0 ? "" : "../".repeat(depth);
|
|
46
50
|
const assetPrefix = depthPrefix + "assets/";
|
|
47
51
|
const resolvePath = (path: string) => (path.startsWith("/") ? depthPrefix + path.slice(1) : path);
|
|
52
|
+
|
|
53
|
+
// Build SEO meta tags (OG, Twitter, canonical)
|
|
54
|
+
let seoTags = "";
|
|
55
|
+
if (opts.seo) {
|
|
56
|
+
const s = opts.seo;
|
|
57
|
+
const e = Bun.escapeHTML;
|
|
58
|
+
seoTags = `\n <meta property="og:title" content="${e(title)}" />\n <meta property="og:description" content="${e(description)}" />\n <meta property="og:url" content="${e(s.url)}" />\n <meta property="og:type" content="website" />\n <meta property="og:site_name" content="${e(s.siteName)}" />\n <meta name="twitter:card" content="summary_large_image" />\n <link rel="canonical" href="${e(s.url)}" />`;
|
|
59
|
+
if (s.image) {
|
|
60
|
+
seoTags += `\n <meta property="og:image" content="${e(s.image)}" />`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
return `<!DOCTYPE html>
|
|
49
65
|
<html lang="en">
|
|
50
66
|
<head>
|
|
@@ -55,6 +71,7 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
55
71
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${Bun.escapeHTML(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
56
72
|
<link rel="stylesheet" href="${Bun.escapeHTML(assetPrefix + css)}">
|
|
57
73
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${Bun.escapeHTML(csp)}">` : ""}
|
|
74
|
+
${seoTags}
|
|
58
75
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
59
76
|
</head>
|
|
60
77
|
<body>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DocuConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface SeoMeta {
|
|
4
|
+
/** Absolute canonical URL */
|
|
5
|
+
url: string;
|
|
6
|
+
/** Site name for og:site_name */
|
|
7
|
+
siteName: string;
|
|
8
|
+
/** Absolute OG image URL (from frontmatter.image, if set) */
|
|
9
|
+
image?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build SEO metadata from config and per-page frontmatter.
|
|
14
|
+
* All fields are derived from existing data — no extra config required.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSeoMeta(
|
|
17
|
+
config: DocuConfig,
|
|
18
|
+
frontmatter: Record<string, unknown>,
|
|
19
|
+
slug: string
|
|
20
|
+
): SeoMeta {
|
|
21
|
+
const baseURL = config.meta?.baseURL?.replace(/\/+$/, "") || "";
|
|
22
|
+
const url = slug ? `${baseURL}/docs/${slug}` : `${baseURL}/`;
|
|
23
|
+
|
|
24
|
+
const result: SeoMeta = {
|
|
25
|
+
url,
|
|
26
|
+
siteName: config.meta?.title || "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Per-page image from frontmatter, fallback to global default from config
|
|
30
|
+
const image =
|
|
31
|
+
(typeof frontmatter.image === "string" && frontmatter.image) || config.meta?.ogImage;
|
|
32
|
+
if (image) {
|
|
33
|
+
// Resolve using URL constructor — handles absolute, root-relative, and relative paths
|
|
34
|
+
try {
|
|
35
|
+
result.image = new URL(image, image.startsWith("/") ? baseURL : `${baseURL}/docs/`).href;
|
|
36
|
+
} catch {
|
|
37
|
+
result.image = image;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
}
|
package/.docu/node/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface DocuMeta {
|
|
|
20
20
|
description: string;
|
|
21
21
|
baseURL: string;
|
|
22
22
|
favicon?: string;
|
|
23
|
+
/** Default OG image path (e.g. /docs/assets/images/og.png). Used when page frontmatter has no image. */
|
|
24
|
+
ogImage?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface SocialLink {
|
package/docu.schema.json
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"title": { "type": "string", "description": "Site title" },
|
|
16
16
|
"description": { "type": "string", "description": "Site description" },
|
|
17
17
|
"baseURL": { "type": "string", "description": "Base URL for the site" },
|
|
18
|
-
"favicon": { "type": "string", "description": "Path to favicon" }
|
|
18
|
+
"favicon": { "type": "string", "description": "Path to favicon" },
|
|
19
|
+
"ogImage": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Default OG image path (e.g. /docs/assets/images/og.png)"
|
|
22
|
+
}
|
|
19
23
|
},
|
|
20
24
|
"required": ["title"]
|
|
21
25
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docubook/flame",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "A blazing-fast React + MDX framework powered by Bun, built for modern documentation experiences.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"react": "^19.2.7",
|
|
55
55
|
"react-dom": "^19.2.7",
|
|
56
56
|
"unified": "^11.0.0",
|
|
57
|
-
"@docubook/mdx-content": "^3.4.3",
|
|
58
57
|
"@docubook/core": "^1.8.2",
|
|
58
|
+
"@docubook/mdx-content": "^3.4.4",
|
|
59
|
+
"@docubook/runt": "^1.0.0",
|
|
59
60
|
"@docubook/themes-colors": "^1.0.2",
|
|
60
|
-
"@docubook/ui-react": "^1.0.0"
|
|
61
|
-
"@docubook/runt": "^1.0.0"
|
|
61
|
+
"@docubook/ui-react": "^1.0.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@sentry/bun": "^10.0.0"
|
|
Binary file
|
package/template/docu.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"title": "My Docs",
|
|
5
5
|
"description": "Documentation powered by DocuBook Flame",
|
|
6
6
|
"baseURL": "http://localhost:3000",
|
|
7
|
-
"favicon": "/docs/assets/images/favicon.ico"
|
|
7
|
+
"favicon": "/docs/assets/images/favicon.ico",
|
|
8
|
+
"ogImage": "/docs/assets/images/og.png"
|
|
8
9
|
},
|
|
9
10
|
"themes": {
|
|
10
11
|
"colors": "default"
|