slidev-prerender 0.0.1-alpha.2 → 0.0.1-beta.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/README.md CHANGED
@@ -52,7 +52,6 @@ By default, this will:
52
52
 
53
53
  - Read from `./dist` (your Slidev build output)
54
54
  - Generate pre-rendered pages in `./dist-prerender`
55
- - Create one HTML file per slide (1.html, 2.html, 3.html, etc.)
56
55
 
57
56
  ## ⚙️ Configuration
58
57
 
@@ -74,7 +73,7 @@ export default defineConfig({
74
73
  // Configuration for individual pages
75
74
  pages: [
76
75
  {
77
- fileName: "1",
76
+ slug: "1",
78
77
  meta: {
79
78
  title: "Welcome to My Presentation",
80
79
  description: "An introduction to the main topics",
@@ -87,7 +86,7 @@ export default defineConfig({
87
86
  },
88
87
  },
89
88
  {
90
- fileName: "2",
89
+ slug: "2",
91
90
  meta: {
92
91
  title: "Understanding the Key Concepts",
93
92
  description: "Deep dive into the core ideas",
@@ -112,12 +111,13 @@ export default defineConfig({
112
111
  | `outDir` | `string` | `"./dist-prerender"` | Output directory for pre-rendered pages |
113
112
  | `port` | `number` | `4173` | Port for the local server during rendering |
114
113
  | `pages` | `PageConfig[]` | `[]` | Configuration for individual slides |
114
+ | `plugins` | `PluginFunction[]` | `[]` | Array of plugins to transform HTML output |
115
115
 
116
116
  #### `PageConfig`
117
117
 
118
118
  | Option | Type | Description |
119
119
  | ---------- | ------------------ | ------------------------------------------------------------ |
120
- | `fileName` | `string` | Slide file name without extension (e.g., "1", "2", "3") |
120
+ | `slug` | `string` (required) | Slide file name without extension (e.g., "1", "2", "3") |
121
121
  | `meta` | `BuildHeadOptions` | Metadata configuration for the slide (optional) |
122
122
 
123
123
  #### `BuildHeadOptions`
@@ -148,6 +148,56 @@ export default defineConfig({
148
148
  | `twitterImage` | `string` | Twitter image URL |
149
149
  | `twitterUrl` | `string` | Twitter URL |
150
150
 
151
+ ### Plugins
152
+
153
+ Plugins allow you to transform the generated HTML for each slide. You can use plugins to inject custom scripts, modify content, or add analytics tracking.
154
+
155
+ ```typescript
156
+ import { defineConfig } from "slidev-prerender";
157
+
158
+ export default defineConfig({
159
+ plugins: [
160
+ // Example: Add Google Analytics
161
+ async (html, pageConfig, pageIndex, logger) => {
162
+ logger.info(`Processing slide ${pageIndex + 1}: ${pageConfig.slug}`);
163
+
164
+ const analyticsScript = `
165
+ <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
166
+ <script>
167
+ window.dataLayer = window.dataLayer || [];
168
+ function gtag(){dataLayer.push(arguments);}
169
+ gtag('js', new Date());
170
+ gtag('config', 'GA_MEASUREMENT_ID');
171
+ </script>
172
+ `;
173
+
174
+ return html.replace('</head>', `${analyticsScript}</head>`);
175
+ },
176
+
177
+ // Example: Add custom meta tag
178
+ (html) => {
179
+ return html.replace(
180
+ '</head>',
181
+ '<meta name="custom-tag" content="custom-value" /></head>'
182
+ );
183
+ },
184
+ ],
185
+ });
186
+ ```
187
+
188
+ #### Plugin Function Signature
189
+
190
+ ```typescript
191
+ type PluginFunction = (
192
+ html: string, // Current HTML content
193
+ pageConfig: PageConfig, // Configuration for the current page
194
+ pageIndex: number, // Zero-based index of the page
195
+ logger: ConsolaInstance // Logger instance for output
196
+ ) => string | Promise<string>;
197
+ ```
198
+
199
+ For more details on plugins, see the [Plugins Guide](./docs/guide/plugins.md) and [Plugins Reference](./docs/reference/plugins.md).
200
+
151
201
  ## 🌐 Deployment
152
202
 
153
203
  Deploy the generated `dist-prerender` folder like any static site:
package/dist/index.d.mts CHANGED
@@ -1,28 +1,31 @@
1
+ import { ConsolaInstance } from "consola";
1
2
  import { SeoMeta } from "@slidev/types";
2
3
  import { ResolvableLink } from "unhead/types";
3
4
 
4
5
  //#region src/build/handle-head.d.ts
5
6
  type BuildHeadOptions = {
6
7
  lang?: string;
7
- title: string;
8
- description?: string | null;
9
- canonicalUrl?: string | null;
10
- ogImage?: string | null;
11
- twitterCard?: string | null;
12
- favicon?: string | null;
8
+ title?: string;
9
+ description?: string;
10
+ canonicalUrl?: string;
11
+ ogImage?: string;
12
+ twitterCard?: string;
13
+ favicon?: string;
13
14
  webFonts?: ResolvableLink[];
14
15
  seoMeta?: Partial<SeoMeta>;
15
16
  };
16
17
  //#endregion
17
18
  //#region src/config/loadConfig.d.ts
19
+ type Page = {
20
+ slug: string;
21
+ meta?: BuildHeadOptions;
22
+ };
18
23
  type UserConfig = {
19
24
  slidevDist?: string;
20
25
  outDir?: string;
21
- pages?: {
22
- fileName: string;
23
- meta?: BuildHeadOptions;
24
- }[];
26
+ pages?: Page[];
25
27
  port?: number;
28
+ plugins?: ((html: string, currentPageConfig: Page, pageIndex: number, log: ConsolaInstance) => string | Promise<string>)[];
26
29
  };
27
30
  //#endregion
28
31
  //#region src/config/defineConfig.d.ts
package/dist/run.mjs CHANGED
@@ -8,6 +8,7 @@ import { createHead, transformHtmlTemplate } from "unhead/server";
8
8
  import { parseHtmlForUnheadExtraction } from "unhead/parser";
9
9
  import { pathToFileURL } from "node:url";
10
10
  import { existsSync } from "node:fs";
11
+ import consola from "consola";
11
12
 
12
13
  //#region src/build/handle-dist.ts
13
14
  async function removeDist(outDir) {
@@ -24,12 +25,27 @@ async function serveDist(distPath, port) {
24
25
  });
25
26
  const server = http.createServer((req, res) => serve(req, res));
26
27
  await new Promise((resolve$1, reject) => {
27
- server.once("error", reject);
28
- server.listen(port, resolve$1);
28
+ const onError = (err) => reject(err);
29
+ server.once("error", onError);
30
+ server.listen(port, () => {
31
+ server.off("error", onError);
32
+ resolve$1();
33
+ });
29
34
  });
35
+ let disposed = false;
30
36
  return {
31
37
  origin: `http://localhost:${port}`,
32
- close: () => new Promise((resolve$1) => server.close(() => resolve$1()))
38
+ close: async () => {
39
+ if (disposed) return;
40
+ disposed = true;
41
+ new Promise((resolve$1, reject) => {
42
+ try {
43
+ server.close((err) => err ? reject(err) : resolve$1());
44
+ } catch (e) {
45
+ reject(e);
46
+ }
47
+ });
48
+ }
33
49
  };
34
50
  }
35
51
  async function getPageHtml(page, pageUrl) {
@@ -40,6 +56,7 @@ async function getPageHtml(page, pageUrl) {
40
56
  //#endregion
41
57
  //#region src/utils/file.ts
42
58
  async function copyDir(src, dst) {
59
+ await fs.access(src);
43
60
  await fs.mkdir(dst, { recursive: true });
44
61
  for (const e of await fs.readdir(src, { withFileTypes: true })) {
45
62
  const s = path.join(src, e.name);
@@ -53,12 +70,12 @@ async function copyDir(src, dst) {
53
70
  function toAttrValue(unsafe) {
54
71
  return JSON.stringify(escapeHtml(String(unsafe)));
55
72
  }
56
- async function applyHead(html, opt) {
73
+ async function applyHead(html, slug, opt) {
57
74
  const extracted = parseHtmlForUnheadExtraction(html).input;
58
75
  const description = opt.description ? toAttrValue(opt.description) : null;
59
76
  return await transformHtmlTemplate(createHead({ init: [extracted, {
60
77
  htmlAttrs: opt.lang ? { lang: opt.lang } : void 0,
61
- title: opt.title,
78
+ title: opt.title ?? slug,
62
79
  link: [
63
80
  opt.favicon ? {
64
81
  rel: "icon",
@@ -139,10 +156,11 @@ async function loadConfig(cwd = process.cwd()) {
139
156
  //#endregion
140
157
  //#region src/build/getConfig.ts
141
158
  const DEFAULT_CONFIG = {
142
- slidevDist: "dist",
143
- outDir: "dist-prerender",
159
+ slidevDist: "./dist",
160
+ outDir: "./dist-prerender",
144
161
  pages: [],
145
- port: 4173
162
+ port: 4173,
163
+ plugins: []
146
164
  };
147
165
  async function getConfig() {
148
166
  const config = await loadConfig();
@@ -150,46 +168,87 @@ async function getConfig() {
150
168
  slidevDist: config.slidevDist ?? DEFAULT_CONFIG.slidevDist,
151
169
  outDir: config.outDir ?? DEFAULT_CONFIG.outDir,
152
170
  pages: config.pages ?? DEFAULT_CONFIG.pages,
153
- port: config.port ?? DEFAULT_CONFIG.port
171
+ port: config.port ?? DEFAULT_CONFIG.port,
172
+ plugins: config.plugins ?? DEFAULT_CONFIG.plugins
154
173
  };
155
174
  }
156
175
 
157
176
  //#endregion
158
177
  //#region src/build/index.ts
159
- async function run() {
160
- const { slidevDist, outDir, pages, port } = await getConfig();
161
- try {
162
- await fs.access(slidevDist);
163
- } catch {
164
- throw new Error(`Slidev dist directory not found: ${slidevDist}\nPlease run 'slidev build' first.`);
178
+ const log = consola.withTag("slidev-prerender");
179
+ async function build() {
180
+ const startedAt = performance.now();
181
+ log.start("Generating pages...");
182
+ consola.log("");
183
+ const { slidevDist, outDir, pages, port, plugins } = await getConfig();
184
+ log.info(`Input (slidevDist): ${slidevDist}`);
185
+ log.info(`Output (outDir): ${outDir}`);
186
+ log.info(`Pages: ${pages.length}`);
187
+ log.info(`Port: ${port}`);
188
+ consola.log("");
189
+ if (!await checkExistSlidevDist(slidevDist)) {
190
+ log.fatal(`Slidev dist directory not found: ${slidevDist}`);
191
+ log.fatal(`Run: slidev build`);
192
+ throw new Error(`Slidev dist directory not found: ${slidevDist}`);
165
193
  }
194
+ log.start("Preparing output directory...");
166
195
  await removeDist(outDir);
167
- const assetsPath = path.join(slidevDist, "assets");
168
- try {
169
- await fs.access(assetsPath);
170
- await copyDir(assetsPath, path.join(outDir, "assets"));
171
- } catch {
172
- console.warn(`Assets directory not found: ${assetsPath}, skipping...`);
173
- }
196
+ await copyDir(slidevDist, outDir);
197
+ log.success("Prepared output directory");
198
+ consola.log("");
199
+ log.start("Starting local server...");
174
200
  const { origin, close: serverClose } = await serveDist(slidevDist, port);
175
- const browser = await chromium.launch();
176
- const page = await browser.newPage();
201
+ let browser;
202
+ log.success(`Server started: ${origin}`);
203
+ consola.log("");
204
+ let generatedPageCount = 0;
177
205
  try {
178
- for (const n of pages) {
179
- console.log(`[SSG] page ${n.fileName}`);
180
- const html = await applyHead(await getPageHtml(page, `${origin}/${n.fileName}`), n.meta ?? { title: `ページ${n.fileName}` });
181
- await fs.writeFile(path.join(outDir, `${n.fileName}.html`), html);
206
+ log.start("Launching browser...");
207
+ browser = await chromium.launch();
208
+ const page = await browser.newPage();
209
+ log.success("Browser launched");
210
+ consola.log("");
211
+ log.start("Rendering pages...");
212
+ for (const [i, pageConfig] of pages.entries()) {
213
+ log.debug(`Render [${i + 1}/${pages.length}]: ${pageConfig.slug}`);
214
+ const originalHtml = await getPageHtml(page, `${origin}/${pageConfig.slug}`);
215
+ const head = pageConfig.meta ?? {};
216
+ const html = await applyHead(originalHtml, pageConfig.slug, head);
217
+ const processedHtml = await plugins.reduce(async (prevPromise, plugin) => {
218
+ const pluginLog = log.withTag(`plugin-${i}`);
219
+ return plugin(await prevPromise, pageConfig, i, pluginLog);
220
+ }, Promise.resolve(html));
221
+ await fs.writeFile(path.join(outDir, `${pageConfig.slug}.html`), processedHtml);
222
+ generatedPageCount++;
182
223
  }
224
+ log.success(`Rendered ${generatedPageCount} pages`);
225
+ consola.log("");
226
+ } catch (err) {
227
+ log.error(`Build failed after generating ${generatedPageCount}/${pages.length} pages`);
228
+ log.error(err);
229
+ consola.log("");
183
230
  } finally {
184
- await browser.close();
185
- serverClose();
231
+ log.start("Closing browser and stopping local server...");
232
+ await browser?.close();
233
+ await serverClose();
234
+ log.success("Cleanup done");
235
+ consola.log("");
236
+ }
237
+ const ms = performance.now() - startedAt;
238
+ log.success(`Done in ${(ms / 1e3).toFixed(2)}s`);
239
+ }
240
+ async function checkExistSlidevDist(slidevDist) {
241
+ try {
242
+ await fs.access(slidevDist);
243
+ } catch {
244
+ return false;
186
245
  }
187
- console.log("done");
246
+ return true;
188
247
  }
189
248
 
190
249
  //#endregion
191
250
  //#region src/run.ts
192
- await run();
251
+ await build();
193
252
 
194
253
  //#endregion
195
254
  export { };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "slidev-prerender",
3
3
  "type": "module",
4
- "version": "0.0.1-alpha.2",
4
+ "version": "0.0.1-beta.0",
5
5
  "description": "",
6
6
  "author": "petaxa <soccer.i_y@icloud.com>",
7
7
  "license": "MIT",
@@ -31,7 +31,8 @@
31
31
  "@playwright/test": "^1.57.0",
32
32
  "markdown-it": "^14.1.0",
33
33
  "unhead": "^2.1.1",
34
- "sirv": "^3.0.2"
34
+ "sirv": "^3.0.2",
35
+ "consola": "^3.4.2"
35
36
  },
36
37
  "scripts": {
37
38
  "build": "tsdown",