slidev-prerender 0.0.1-alpha.3 → 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) {
@@ -54,12 +70,12 @@ async function copyDir(src, dst) {
54
70
  function toAttrValue(unsafe) {
55
71
  return JSON.stringify(escapeHtml(String(unsafe)));
56
72
  }
57
- async function applyHead(html, opt) {
73
+ async function applyHead(html, slug, opt) {
58
74
  const extracted = parseHtmlForUnheadExtraction(html).input;
59
75
  const description = opt.description ? toAttrValue(opt.description) : null;
60
76
  return await transformHtmlTemplate(createHead({ init: [extracted, {
61
77
  htmlAttrs: opt.lang ? { lang: opt.lang } : void 0,
62
- title: opt.title,
78
+ title: opt.title ?? slug,
63
79
  link: [
64
80
  opt.favicon ? {
65
81
  rel: "icon",
@@ -140,10 +156,11 @@ async function loadConfig(cwd = process.cwd()) {
140
156
  //#endregion
141
157
  //#region src/build/getConfig.ts
142
158
  const DEFAULT_CONFIG = {
143
- slidevDist: "dist",
144
- outDir: "dist-prerender",
159
+ slidevDist: "./dist",
160
+ outDir: "./dist-prerender",
145
161
  pages: [],
146
- port: 4173
162
+ port: 4173,
163
+ plugins: []
147
164
  };
148
165
  async function getConfig() {
149
166
  const config = await loadConfig();
@@ -151,44 +168,87 @@ async function getConfig() {
151
168
  slidevDist: config.slidevDist ?? DEFAULT_CONFIG.slidevDist,
152
169
  outDir: config.outDir ?? DEFAULT_CONFIG.outDir,
153
170
  pages: config.pages ?? DEFAULT_CONFIG.pages,
154
- port: config.port ?? DEFAULT_CONFIG.port
171
+ port: config.port ?? DEFAULT_CONFIG.port,
172
+ plugins: config.plugins ?? DEFAULT_CONFIG.plugins
155
173
  };
156
174
  }
157
175
 
158
176
  //#endregion
159
177
  //#region src/build/index.ts
160
- async function run() {
161
- const { slidevDist, outDir, pages, port } = await getConfig();
162
- try {
163
- await fs.access(slidevDist);
164
- } catch {
165
- 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}`);
166
193
  }
194
+ log.start("Preparing output directory...");
167
195
  await removeDist(outDir);
168
- try {
169
- await copyDir(slidevDist, outDir);
170
- } catch {
171
- console.warn(`Assets directory not found: ${slidevDist}, skipping...`);
172
- }
196
+ await copyDir(slidevDist, outDir);
197
+ log.success("Prepared output directory");
198
+ consola.log("");
199
+ log.start("Starting local server...");
173
200
  const { origin, close: serverClose } = await serveDist(slidevDist, port);
174
- const browser = await chromium.launch();
175
- const page = await browser.newPage();
201
+ let browser;
202
+ log.success(`Server started: ${origin}`);
203
+ consola.log("");
204
+ let generatedPageCount = 0;
176
205
  try {
177
- for (const n of pages) {
178
- console.log(`[SSG] page ${n.fileName}`);
179
- const html = await applyHead(await getPageHtml(page, `${origin}/${n.fileName}`), n.meta ?? { title: `ページ${n.fileName}` });
180
- 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++;
181
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("");
182
230
  } finally {
183
- await browser.close();
184
- 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;
185
245
  }
186
- console.log("done");
246
+ return true;
187
247
  }
188
248
 
189
249
  //#endregion
190
250
  //#region src/run.ts
191
- await run();
251
+ await build();
192
252
 
193
253
  //#endregion
194
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.3",
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",