@docubook/flame 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docu/lib/build.deno.js +4 -4
- package/.docu/lib/build.impl-N2MBT3CM.js +12 -0
- package/.docu/lib/build.node.js +4 -4
- package/.docu/lib/{chunk-J5NMYSBJ.js → chunk-4IQXHHPF.js} +5 -2
- package/.docu/lib/{chunk-YI76L7LB.js → chunk-IJFMQFWW.js} +69 -25
- package/.docu/lib/{chunk-YENO4SVR.js → chunk-JTH4JBBS.js} +38 -5
- package/.docu/lib/chunk-TI6M4643.js +204 -0
- package/.docu/lib/{chunk-EIKBL7SD.js → chunk-UUS45SUC.js} +2 -2
- package/.docu/lib/{chunk-WCMRAAWH.js → chunk-Y2TAEAUQ.js} +2 -2
- package/.docu/lib/{chunk-VX4HLVHE.js → chunk-ZA5QB5UK.js} +4 -4
- package/.docu/lib/clean.js +2 -2
- package/.docu/lib/deploy.deno.js +2 -2
- package/.docu/lib/deploy.node.js +2 -2
- package/.docu/lib/preview.deno.js +3 -3
- package/.docu/lib/preview.node.js +3 -3
- package/.docu/lib/server.deno.js +4 -4
- package/.docu/lib/server.node.js +4 -4
- package/.docu/node/build.impl.ts +15 -3
- package/.docu/node/build.ts +6 -0
- package/.docu/node/client.ts +38 -13
- package/.docu/node/deploy.shared.ts +147 -12
- package/.docu/node/deploy.ts +141 -30
- package/.docu/node/html.shared.ts +19 -0
- package/.docu/node/html.ts +17 -23
- package/.docu/node/hydrate.node.ts +74 -39
- package/.docu/node/hydrate.ts +74 -48
- package/.docu/node/paths.ts +7 -5
- package/.docu/node/seo.ts +42 -0
- package/.docu/node/server-routes.ts +2 -1
- package/.docu/node/types.ts +2 -0
- package/bin/cli.js +19 -6
- 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.impl-O3Z5XIGM.js +0 -12
- package/.docu/lib/chunk-7VXEOH7X.js +0 -79
package/.docu/lib/server.deno.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-ZA5QB5UK.js";
|
|
4
|
+
import "./chunk-IJFMQFWW.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-Y2TAEAUQ.js";
|
|
7
|
+
import "./chunk-4IQXHHPF.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/server.deno.ts
|
|
10
10
|
import { denoAdapter } from "@docubook/runt";
|
package/.docu/lib/server.node.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runServer
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-ZA5QB5UK.js";
|
|
4
|
+
import "./chunk-IJFMQFWW.js";
|
|
5
5
|
import "./chunk-EOK6KATZ.js";
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-Y2TAEAUQ.js";
|
|
7
|
+
import "./chunk-4IQXHHPF.js";
|
|
8
8
|
|
|
9
9
|
// .docu/node/server.node.ts
|
|
10
10
|
import { nodeAdapter } from "@docubook/runt";
|
package/.docu/node/build.impl.ts
CHANGED
|
@@ -31,8 +31,9 @@ import { loadPlugins } from "./plugin-loader";
|
|
|
31
31
|
import { BuildPluginBuilder } from "./plugin-builder";
|
|
32
32
|
import { scanMdxFiles } from "./utils";
|
|
33
33
|
import type { BuildCache, CliArgs } from "./types";
|
|
34
|
-
import { generateNonce } from "./security";
|
|
34
|
+
import { generateNonce, cspHeader } 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,15 @@ 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 || "");
|
|
152
|
+
const csp = cspHeader(nonce, process.env.NODE_ENV !== "production");
|
|
150
153
|
let html = htmlShell({
|
|
151
154
|
title,
|
|
152
155
|
description,
|
|
153
156
|
body,
|
|
154
157
|
favicon,
|
|
158
|
+
seo,
|
|
159
|
+
csp,
|
|
155
160
|
css: assetManifest.css,
|
|
156
161
|
js: assetManifest.js,
|
|
157
162
|
nonce,
|
|
@@ -345,14 +350,18 @@ export async function runBuild(): Promise<void> {
|
|
|
345
350
|
|
|
346
351
|
const landingPage = React.createElement(IndexPage);
|
|
347
352
|
const landingFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
353
|
+
const landingSeo = buildSeoMeta(docuConfig, docuConfig.meta as Record<string, unknown>, "");
|
|
354
|
+
const landingNonce = generateNonce();
|
|
348
355
|
const landingHtml = htmlShell({
|
|
349
356
|
title: docuConfig.meta?.title || "DocuBook",
|
|
350
357
|
description: docuConfig.meta?.description || "",
|
|
351
358
|
body: renderToString(landingPage),
|
|
352
359
|
favicon: landingFavicon,
|
|
360
|
+
seo: landingSeo,
|
|
361
|
+
csp: cspHeader(landingNonce, process.env.NODE_ENV !== "production"),
|
|
353
362
|
css: assetManifest.css,
|
|
354
363
|
js: assetManifest.js,
|
|
355
|
-
nonce:
|
|
364
|
+
nonce: landingNonce,
|
|
356
365
|
themeCss: inlineThemeCss,
|
|
357
366
|
});
|
|
358
367
|
await writeFile(join(DIST_DIR, "index.html"), landingHtml);
|
|
@@ -363,14 +372,17 @@ export async function runBuild(): Promise<void> {
|
|
|
363
372
|
React.createElement(NotFoundPage)
|
|
364
373
|
);
|
|
365
374
|
const notFoundFavicon = docuConfig.meta?.favicon || "/docs/assets/images/favicon.ico";
|
|
375
|
+
const notFoundNonce = generateNonce();
|
|
366
376
|
const notFoundHtml = htmlShell({
|
|
367
377
|
title: "404 - Not Found",
|
|
368
378
|
description: "",
|
|
369
379
|
body: renderToString(notFoundPage),
|
|
370
380
|
favicon: notFoundFavicon,
|
|
381
|
+
headExtra: ['<meta name="robots" content="noindex,follow">'],
|
|
382
|
+
csp: cspHeader(notFoundNonce, process.env.NODE_ENV !== "production"),
|
|
371
383
|
css: assetManifest.css,
|
|
372
384
|
js: assetManifest.js,
|
|
373
|
-
nonce:
|
|
385
|
+
nonce: notFoundNonce,
|
|
374
386
|
themeCss: inlineThemeCss,
|
|
375
387
|
});
|
|
376
388
|
await writeFile(join(DIST_DIR, "404.html"), notFoundHtml);
|
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(),
|
package/.docu/node/client.ts
CHANGED
|
@@ -25,6 +25,8 @@ function mountIsland(
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function mountIslands() {
|
|
28
|
+
// forceCreate: SSR sidebar renders <Menu> only; client renders full <Sidebar>
|
|
29
|
+
// (DesktopSidebar + MobileBar) — structure mismatch forces full createRoot.
|
|
28
30
|
mountIsland(
|
|
29
31
|
"sidebar-island",
|
|
30
32
|
(el) => {
|
|
@@ -38,18 +40,16 @@ function mountIslands() {
|
|
|
38
40
|
true
|
|
39
41
|
);
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
true
|
|
52
|
-
);
|
|
43
|
+
// mobile-bar-island SSR div is empty (data attributes only),
|
|
44
|
+
// so hydrateRoot child check falls through to createRoot automatically.
|
|
45
|
+
mountIsland("mobile-bar-island", (el) => {
|
|
46
|
+
const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
|
|
47
|
+
return React.createElement(MobileBar, {
|
|
48
|
+
tocs,
|
|
49
|
+
title: el.dataset.title || "",
|
|
50
|
+
repoUrl: el.dataset.repo || "",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
53
|
|
|
54
54
|
mountIsland("toc-island", (el) => {
|
|
55
55
|
const tocs: TocItem[] = safeParseTocs(el.dataset.tocs);
|
|
@@ -69,7 +69,8 @@ function hydrateMdxContent() {
|
|
|
69
69
|
try {
|
|
70
70
|
const compiledSource = JSON.parse(sourceEl.textContent || "");
|
|
71
71
|
const components = createMdxComponents();
|
|
72
|
-
|
|
72
|
+
hydrateRoot(
|
|
73
|
+
island,
|
|
73
74
|
React.createElement(MDXRemote, { compiledSource, scope: {}, frontmatter: {}, components })
|
|
74
75
|
);
|
|
75
76
|
} catch (e) {
|
|
@@ -82,3 +83,27 @@ if (document.readyState === "loading") {
|
|
|
82
83
|
} else {
|
|
83
84
|
mountIslands();
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
// ── Hash scroll compensation ──────────────────────────────────────
|
|
88
|
+
// After hydration, lazy-rendered content (Mermaid via IntersectionObserver) can
|
|
89
|
+
// shift layout and push the hash target (#section-2) off-screen.
|
|
90
|
+
// Poll with rAF for ~1s and re-scroll if the target is below viewport.
|
|
91
|
+
function scrollToHashOnLoad() {
|
|
92
|
+
const hash = window.location.hash;
|
|
93
|
+
if (!hash || hash === "#") return;
|
|
94
|
+
const id = hash.slice(1);
|
|
95
|
+
const deadline = performance.now() + 1000;
|
|
96
|
+
function tick() {
|
|
97
|
+
const el = document.getElementById(id);
|
|
98
|
+
if (el) {
|
|
99
|
+
const top = el.getBoundingClientRect().top;
|
|
100
|
+
if (top <= window.innerHeight - 100 && top >= 0) return; // already in view
|
|
101
|
+
el.scrollIntoView();
|
|
102
|
+
return; // scrolled once, done
|
|
103
|
+
}
|
|
104
|
+
if (performance.now() < deadline) requestAnimationFrame(tick);
|
|
105
|
+
}
|
|
106
|
+
requestAnimationFrame(tick);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
scrollToHashOnLoad();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime-neutral deploy —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Runtime-neutral deploy — 3 modes:
|
|
3
|
+
* 1. `flame deploy` → build + generate GitHub Actions workflow
|
|
4
|
+
* 2. `flame deploy --docker` → build + generate Docker deployment files
|
|
5
|
+
* 3. `flame deploy --docker --silent` → same as #2, minimal output
|
|
6
|
+
*
|
|
7
|
+
* Mirror of deploy.ts (Bun-only, protected) for Node.js and Deno.
|
|
8
|
+
* Runs the neutral build in-process, then prepares .docu/dist.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
@@ -12,26 +16,157 @@ import { DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
|
12
16
|
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
13
17
|
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
14
18
|
|
|
15
|
-
export
|
|
16
|
-
|
|
19
|
+
export const HEADERS_FILE = `/*
|
|
20
|
+
X-Frame-Options: DENY
|
|
21
|
+
X-Content-Type-Options: nosniff
|
|
22
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
23
|
+
|
|
24
|
+
/assets/*
|
|
25
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
|
|
29
|
+
const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
|
|
17
30
|
|
|
31
|
+
/** Logger that no-ops all non-error output in silent mode. */
|
|
32
|
+
const log = isSilent
|
|
33
|
+
? { info: () => {}, ok: () => {}, created: () => {}, out: () => {} }
|
|
34
|
+
: {
|
|
35
|
+
info: (m: string) => console.log(m),
|
|
36
|
+
ok: () => console.log("\n✅ Ready to deploy!"),
|
|
37
|
+
created: (m: string) => console.log(m),
|
|
38
|
+
out: (m: string) => console.log(m),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function runBuild() {
|
|
18
42
|
process.env.NODE_ENV = "production";
|
|
43
|
+
if (isSilent) {
|
|
44
|
+
process.env.FLAME_BUILD_SILENT = "1";
|
|
45
|
+
process.env.LOG_LEVEL = "error";
|
|
46
|
+
}
|
|
19
47
|
const { runBuildCli } = await import("./build.impl");
|
|
20
48
|
await runBuildCli();
|
|
49
|
+
}
|
|
21
50
|
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
async function writeDockerFiles() {
|
|
52
|
+
const dockerDir = PROJECT_ROOT;
|
|
53
|
+
|
|
54
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(dockerDir, "Dockerfile"),
|
|
57
|
+
`FROM node:22-alpine AS builder
|
|
58
|
+
WORKDIR /app
|
|
59
|
+
COPY package.json package-lock.json ./
|
|
60
|
+
RUN npm ci
|
|
61
|
+
COPY . .
|
|
62
|
+
RUN npm run build
|
|
63
|
+
|
|
64
|
+
FROM nginx:alpine
|
|
65
|
+
COPY --from=builder /app/.docu/dist /usr/share/nginx/html
|
|
66
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
67
|
+
USER nginx
|
|
68
|
+
EXPOSE 80
|
|
69
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
70
|
+
`
|
|
71
|
+
);
|
|
72
|
+
log.created("📄 Created Dockerfile");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!existsSync(join(dockerDir, "nginx.conf"))) {
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(dockerDir, "nginx.conf"),
|
|
78
|
+
`server {
|
|
79
|
+
listen 80;
|
|
80
|
+
server_name _;
|
|
81
|
+
root /usr/share/nginx/html;
|
|
82
|
+
index index.html;
|
|
83
|
+
|
|
84
|
+
gzip on;
|
|
85
|
+
gzip_types text/html text/css application/javascript image/svg+xml;
|
|
86
|
+
|
|
87
|
+
# Security headers (HSTS effective when HTTPS is terminated upstream)
|
|
88
|
+
add_header X-Frame-Options "DENY" always;
|
|
89
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
90
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
91
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
92
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
93
|
+
|
|
94
|
+
location /assets/ {
|
|
95
|
+
expires 1y;
|
|
96
|
+
add_header Cache-Control "public, immutable";
|
|
97
|
+
add_header X-Frame-Options "DENY" always;
|
|
98
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
99
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
100
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
101
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
location /docs/assets/ {
|
|
105
|
+
expires 7d;
|
|
106
|
+
add_header Cache-Control "public";
|
|
107
|
+
add_header X-Frame-Options "DENY" always;
|
|
108
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
109
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
110
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
111
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
location / {
|
|
115
|
+
try_files $uri $uri.html $uri/ =404;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
log.created("📄 Created nginx.conf");
|
|
121
|
+
}
|
|
24
122
|
|
|
25
|
-
|
|
123
|
+
if (!existsSync(join(dockerDir, ".dockerignore"))) {
|
|
124
|
+
await writeFile(
|
|
125
|
+
join(dockerDir, ".dockerignore"),
|
|
126
|
+
`node_modules
|
|
127
|
+
.git
|
|
128
|
+
*.DS_Store
|
|
129
|
+
.docu/dist
|
|
130
|
+
.docu/lib
|
|
131
|
+
.env
|
|
132
|
+
.env.*
|
|
133
|
+
.npmrc
|
|
134
|
+
*.log
|
|
135
|
+
`
|
|
136
|
+
);
|
|
137
|
+
log.created("📄 Created .dockerignore");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function writeGhaWorkflow() {
|
|
26
142
|
if (!existsSync(WORKFLOW_FILE)) {
|
|
27
143
|
await mkdir(WORKFLOW_DIR, { recursive: true });
|
|
28
144
|
await writeFile(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
|
|
29
|
-
|
|
145
|
+
log.created("📄 Created .github/workflows/deploy.yml");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runDeploy(): Promise<void> {
|
|
150
|
+
log.info("📦 Building for production...\n");
|
|
151
|
+
await runBuild();
|
|
152
|
+
|
|
153
|
+
// Common: .nojekyll + _headers
|
|
154
|
+
await writeFile(join(DIST_DIR, ".nojekyll"), "");
|
|
155
|
+
await writeFile(join(DIST_DIR, "_headers"), HEADERS_FILE);
|
|
156
|
+
|
|
157
|
+
if (isDocker) {
|
|
158
|
+
await writeDockerFiles();
|
|
159
|
+
} else {
|
|
160
|
+
await writeGhaWorkflow();
|
|
30
161
|
}
|
|
31
162
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
163
|
+
log.ok();
|
|
164
|
+
log.out(" Output: .docu/dist/");
|
|
165
|
+
if (isDocker) {
|
|
166
|
+
log.out(" Run: docker build -t my-docs . && docker run -p 80:80 my-docs");
|
|
167
|
+
} else {
|
|
168
|
+
log.out(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
|
|
169
|
+
}
|
|
35
170
|
}
|
|
36
171
|
|
|
37
172
|
const GITHUB_ACTIONS_WORKFLOW = `name: Deploy to GitHub Pages
|
package/.docu/node/deploy.ts
CHANGED
|
@@ -1,56 +1,165 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploy script
|
|
2
|
+
* Deploy script — 3 modes:
|
|
3
|
+
* 1. `flame deploy` → build + generate GitHub Actions workflow
|
|
4
|
+
* 2. `flame deploy --docker` → build + generate Docker deployment files
|
|
5
|
+
* 3. `flame deploy --docker --silent` → same as #2, minimal output
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
* Runs build, adds .nojekyll, and generates GitHub Actions workflow.
|
|
7
|
+
* Bun-native path — uses Bun.write() and Bun.spawn().
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
9
11
|
import { existsSync } from "node:fs";
|
|
10
12
|
import { join } from "node:path";
|
|
11
13
|
import { DIST_DIR, PROJECT_ROOT } from "./paths";
|
|
14
|
+
import { HEADERS_FILE } from "./deploy.shared";
|
|
15
|
+
|
|
16
|
+
export { HEADERS_FILE };
|
|
12
17
|
|
|
13
18
|
const WORKFLOW_DIR = join(PROJECT_ROOT, ".github/workflows");
|
|
14
19
|
const WORKFLOW_FILE = join(WORKFLOW_DIR, "deploy.yml");
|
|
15
20
|
|
|
16
|
-
const
|
|
17
|
-
|
|
21
|
+
const isDocker = !!process.env.FLAME_DEPLOY_DOCKER;
|
|
22
|
+
const isSilent = !!process.env.FLAME_DEPLOY_SILENT;
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
/** Logger that no-ops all non-error output in silent mode. */
|
|
25
|
+
const log = isSilent
|
|
26
|
+
? { info: () => {}, ok: () => {}, created: () => {}, out: () => {} }
|
|
27
|
+
: {
|
|
28
|
+
info: (m: string) => console.log(m),
|
|
29
|
+
ok: () => console.log("\n✅ Ready to deploy!"),
|
|
30
|
+
created: (m: string) => console.log(m),
|
|
31
|
+
out: (m: string) => console.log(m),
|
|
32
|
+
};
|
|
22
33
|
|
|
23
|
-
async function
|
|
24
|
-
console.log("📦 Building for production...\n");
|
|
25
|
-
|
|
26
|
-
// Run build
|
|
34
|
+
async function runBuild() {
|
|
27
35
|
const build = Bun.spawn(["bun", "run", "build"], {
|
|
28
|
-
stdout: "inherit",
|
|
29
|
-
stderr: "inherit",
|
|
36
|
+
stdout: isSilent ? "ignore" : "inherit",
|
|
37
|
+
stderr: isSilent ? "ignore" : "inherit",
|
|
30
38
|
});
|
|
31
39
|
const exitCode = await build.exited;
|
|
32
40
|
if (exitCode !== 0) {
|
|
33
41
|
console.error("\n❌ Build failed");
|
|
34
42
|
process.exit(1);
|
|
35
43
|
}
|
|
44
|
+
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
export const NGINX_CONF = `server {
|
|
47
|
+
listen 80;
|
|
48
|
+
server_name _;
|
|
49
|
+
root /usr/share/nginx/html;
|
|
50
|
+
index index.html;
|
|
51
|
+
|
|
52
|
+
gzip on;
|
|
53
|
+
gzip_types text/html text/css application/javascript image/svg+xml;
|
|
54
|
+
|
|
55
|
+
# Security headers (server-level, inherited by all locations)
|
|
56
|
+
# HSTS effective when HTTPS is terminated upstream (reverse proxy / LB)
|
|
57
|
+
add_header X-Frame-Options "DENY" always;
|
|
58
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
59
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
60
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
61
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
62
|
+
|
|
63
|
+
location /assets/ {
|
|
64
|
+
expires 1y;
|
|
65
|
+
add_header Cache-Control "public, immutable";
|
|
66
|
+
add_header X-Frame-Options "DENY" always;
|
|
67
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
68
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
69
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
70
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
71
|
+
}
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
location /docs/assets/ {
|
|
74
|
+
expires 7d;
|
|
75
|
+
add_header Cache-Control "public";
|
|
76
|
+
add_header X-Frame-Options "DENY" always;
|
|
77
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
78
|
+
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
|
79
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
80
|
+
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
81
|
+
}
|
|
43
82
|
|
|
44
|
-
|
|
83
|
+
location / {
|
|
84
|
+
try_files $uri $uri.html $uri/ =404;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export const DOCKERFILE_BUN = `FROM oven/bun:1 AS builder
|
|
90
|
+
WORKDIR /app
|
|
91
|
+
COPY package.json bun.lock ./
|
|
92
|
+
RUN bun install --frozen-lockfile
|
|
93
|
+
COPY . .
|
|
94
|
+
RUN bun run build
|
|
95
|
+
|
|
96
|
+
FROM nginx:alpine
|
|
97
|
+
COPY --from=builder /app/.docu/dist /usr/share/nginx/html
|
|
98
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
99
|
+
USER nginx
|
|
100
|
+
EXPOSE 80
|
|
101
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
export const DOCKERIGNORE = `node_modules
|
|
105
|
+
.git
|
|
106
|
+
*.DS_Store
|
|
107
|
+
.docu/dist
|
|
108
|
+
.docu/lib
|
|
109
|
+
.env
|
|
110
|
+
.env.*
|
|
111
|
+
.npmrc
|
|
112
|
+
*.log
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
async function writeDockerFiles() {
|
|
116
|
+
const dockerDir = PROJECT_ROOT;
|
|
117
|
+
|
|
118
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
119
|
+
await Bun.write(join(dockerDir, "Dockerfile"), DOCKERFILE_BUN);
|
|
120
|
+
log.created("📄 Created Dockerfile");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!existsSync(join(dockerDir, "nginx.conf"))) {
|
|
124
|
+
await Bun.write(join(dockerDir, "nginx.conf"), NGINX_CONF);
|
|
125
|
+
log.created("📄 Created nginx.conf");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!existsSync(join(dockerDir, ".dockerignore"))) {
|
|
129
|
+
await Bun.write(join(dockerDir, ".dockerignore"), DOCKERIGNORE);
|
|
130
|
+
log.created("📄 Created .dockerignore");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function writeGhaWorkflow() {
|
|
45
135
|
if (!existsSync(WORKFLOW_FILE)) {
|
|
46
136
|
await mkdir(WORKFLOW_DIR, { recursive: true });
|
|
47
|
-
await
|
|
48
|
-
|
|
137
|
+
await Bun.write(WORKFLOW_FILE, GITHUB_ACTIONS_WORKFLOW);
|
|
138
|
+
log.created("📄 Created .github/workflows/deploy.yml");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function deploy() {
|
|
143
|
+
log.info("📦 Building for production...\n");
|
|
144
|
+
await runBuild();
|
|
145
|
+
|
|
146
|
+
// Common: .nojekyll + _headers
|
|
147
|
+
await Bun.write(join(DIST_DIR, ".nojekyll"), "");
|
|
148
|
+
await Bun.write(join(DIST_DIR, "_headers"), HEADERS_FILE);
|
|
149
|
+
|
|
150
|
+
if (isDocker) {
|
|
151
|
+
await writeDockerFiles();
|
|
152
|
+
} else {
|
|
153
|
+
await writeGhaWorkflow();
|
|
49
154
|
}
|
|
50
155
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
156
|
+
log.ok();
|
|
157
|
+
log.out(" Output: .docu/dist/");
|
|
158
|
+
if (isDocker) {
|
|
159
|
+
log.out(" Run: docker build -t my-docs . && docker run -p 80:80 my-docs");
|
|
160
|
+
} else {
|
|
161
|
+
log.out(" Push to GitHub and enable Pages (Settings → Pages → Source: GitHub Actions)");
|
|
162
|
+
}
|
|
54
163
|
}
|
|
55
164
|
|
|
56
165
|
const GITHUB_ACTIONS_WORKFLOW = `name: Deploy to GitHub Pages
|
|
@@ -103,7 +212,9 @@ jobs:
|
|
|
103
212
|
uses: actions/deploy-pages@v4
|
|
104
213
|
`;
|
|
105
214
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
215
|
+
if (import.meta.main) {
|
|
216
|
+
deploy().catch((err) => {
|
|
217
|
+
console.error("Deploy failed:", err);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -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>
|
|
@@ -63,12 +79,15 @@ export function htmlShell(opts: HtmlShellOptions): string {
|
|
|
63
79
|
<title>${escapeHtml(title)}</title>
|
|
64
80
|
<meta name="description" content="${escapeHtml(description)}">
|
|
65
81
|
${favicon ? `<link rel="icon" type="image/x-icon" href="${escapeHtml(resolvePath(favicon))}">` : ""}${themeStyle}
|
|
82
|
+
<link rel="preload" href="${escapeHtml(assetPrefix + css)}" as="style">
|
|
66
83
|
<link rel="stylesheet" href="${escapeHtml(assetPrefix + css)}">
|
|
67
84
|
${csp ? `<meta http-equiv="Content-Security-Policy" content="${escapeHtml(csp)}">` : ""}
|
|
85
|
+
${seoTags}
|
|
68
86
|
<script${nonceAttr}>try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}</script>${headInjection}
|
|
69
87
|
</head>
|
|
70
88
|
<body>
|
|
71
89
|
<div id="root">${body}</div>
|
|
90
|
+
<link rel="modulepreload" href="${escapeHtml(assetPrefix + js)}">
|
|
72
91
|
<script type="module"${nonceAttr} src="${escapeHtml(assetPrefix + js)}"></script>${extraScripts ? `\n ${extraScripts}` : ""}${bodyInjection}
|
|
73
92
|
</body>
|
|
74
93
|
</html>`;
|