@terrymooreii/sia 2.2.0 → 2.3.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/docs/README.md +46 -0
- package/docs/creating-plugins.md +509 -0
- package/docs/plugins.md +320 -0
- package/lib/build.js +69 -4
- package/lib/collections.js +9 -3
- package/lib/config.js +7 -0
- package/lib/content.js +49 -23
- package/lib/hooks.js +188 -0
- package/lib/index.js +19 -0
- package/lib/plugins.js +308 -0
- package/lib/templates.js +7 -1
- package/package.json +1 -1
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Plugin System
|
|
2
|
+
|
|
3
|
+
Sia includes a powerful plugin system that allows you to extend functionality at key points in the build lifecycle. Plugins can be created locally in your project or published as npm packages.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Plugins are JavaScript modules that export a plugin object with hooks that execute at specific points during the build process. They can:
|
|
8
|
+
|
|
9
|
+
- Transform content before or after parsing
|
|
10
|
+
- Generate additional files (search indexes, sitemaps, etc.)
|
|
11
|
+
- Add custom Marked extensions for markdown processing
|
|
12
|
+
- Register custom Nunjucks template filters and functions
|
|
13
|
+
- Modify site data before rendering
|
|
14
|
+
- Perform post-build tasks
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
Plugins are configured in your `_config.yml` or `_config.json` file:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
plugins:
|
|
22
|
+
enabled: true # Master switch (default: true)
|
|
23
|
+
strictMode: false # Fail build on plugin errors (default: false)
|
|
24
|
+
order: # Explicit plugin execution order (optional)
|
|
25
|
+
- sia-search-plugin
|
|
26
|
+
- sia-sitemap-plugin
|
|
27
|
+
plugins: [] # Explicit list of plugins to load (optional, empty = all)
|
|
28
|
+
config: # Plugin-specific configuration
|
|
29
|
+
sia-search-plugin:
|
|
30
|
+
outputPath: search-index.json
|
|
31
|
+
includeContent: false
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Configuration Options
|
|
35
|
+
|
|
36
|
+
- **enabled**: Set to `false` to disable all plugins
|
|
37
|
+
- **strictMode**: If `true`, the build will fail if any plugin has an error. If `false` (default), errors are logged but the build continues
|
|
38
|
+
- **order**: Array of plugin names specifying execution order. Plugins not in this list will execute after, in dependency order
|
|
39
|
+
- **plugins**: Array of plugin names to explicitly load. If empty or omitted, all discovered plugins are loaded
|
|
40
|
+
- **config**: Object containing plugin-specific configuration, keyed by plugin name
|
|
41
|
+
|
|
42
|
+
## Available Hooks
|
|
43
|
+
|
|
44
|
+
Hooks are functions that plugins can register to execute at specific points in the build process.
|
|
45
|
+
|
|
46
|
+
### Build Lifecycle Hooks
|
|
47
|
+
|
|
48
|
+
#### `beforeBuild(config, api)`
|
|
49
|
+
Executes before the build starts, after configuration is loaded.
|
|
50
|
+
|
|
51
|
+
**Parameters:**
|
|
52
|
+
- `config`: Site configuration object
|
|
53
|
+
- `api`: Plugin API object (see Plugin API section)
|
|
54
|
+
|
|
55
|
+
**Use cases:** Initialize plugin state, validate configuration, set up external services
|
|
56
|
+
|
|
57
|
+
#### `afterConfigLoad(config, api)`
|
|
58
|
+
Executes immediately after configuration is loaded, before any build steps.
|
|
59
|
+
|
|
60
|
+
**Parameters:**
|
|
61
|
+
- `config`: Site configuration object
|
|
62
|
+
- `api`: Plugin API object
|
|
63
|
+
|
|
64
|
+
**Use cases:** Modify configuration, validate plugin requirements
|
|
65
|
+
|
|
66
|
+
#### `afterContentLoad(siteData, config, api)`
|
|
67
|
+
Executes after all content collections are loaded and parsed.
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
- `siteData`: Complete site data object with collections, tags, etc.
|
|
71
|
+
- `config`: Site configuration object
|
|
72
|
+
- `api`: Plugin API object
|
|
73
|
+
|
|
74
|
+
**Use cases:** Analyze content, generate metadata, modify collections
|
|
75
|
+
|
|
76
|
+
#### `afterTagCollections(tags, context, api)`
|
|
77
|
+
Executes after tag collections are built.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `tags`: Tag collections object
|
|
81
|
+
- `context`: Object with `config` and `collections`
|
|
82
|
+
- `api`: Plugin API object
|
|
83
|
+
|
|
84
|
+
**Use cases:** Modify tag data, generate tag statistics
|
|
85
|
+
|
|
86
|
+
#### `beforeSiteData(siteData, config, api)`
|
|
87
|
+
Executes before final siteData is created, allowing modification.
|
|
88
|
+
|
|
89
|
+
**Parameters:**
|
|
90
|
+
- `siteData`: Site data object (can be modified)
|
|
91
|
+
- `config`: Site configuration object
|
|
92
|
+
- `api`: Plugin API object
|
|
93
|
+
|
|
94
|
+
**Use cases:** Add custom data to siteData, modify collections
|
|
95
|
+
|
|
96
|
+
#### `beforeRender(siteData, config, api)`
|
|
97
|
+
Executes before templates are rendered.
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
- `siteData`: Site data object
|
|
101
|
+
- `config`: Site configuration object
|
|
102
|
+
- `api`: Plugin API object
|
|
103
|
+
|
|
104
|
+
**Use cases:** Prepare data for rendering, modify siteData for templates
|
|
105
|
+
|
|
106
|
+
#### `afterRender(siteData, config, api)`
|
|
107
|
+
Executes after all templates are rendered but before assets are copied.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
- `siteData`: Site data object
|
|
111
|
+
- `config`: Site configuration object
|
|
112
|
+
- `api`: Plugin API object
|
|
113
|
+
|
|
114
|
+
**Use cases:** Post-process rendered HTML, generate additional files
|
|
115
|
+
|
|
116
|
+
#### `afterBuild(siteData, config, api)`
|
|
117
|
+
Executes after the build completes, including asset copying.
|
|
118
|
+
|
|
119
|
+
**Parameters:**
|
|
120
|
+
- `siteData`: Site data object
|
|
121
|
+
- `config`: Site configuration object
|
|
122
|
+
- `api`: Plugin API object
|
|
123
|
+
|
|
124
|
+
**Use cases:** Generate search indexes, create sitemaps, upload files, send notifications
|
|
125
|
+
|
|
126
|
+
### Content Processing Hooks
|
|
127
|
+
|
|
128
|
+
#### `beforeContentParse(rawContent, filePath)`
|
|
129
|
+
Executes before a markdown file is parsed. Can modify the raw content.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
- `rawContent`: Raw file content (string)
|
|
133
|
+
- `filePath`: Path to the file
|
|
134
|
+
|
|
135
|
+
**Returns:** Modified content string (or original if unchanged)
|
|
136
|
+
|
|
137
|
+
**Use cases:** Pre-process markdown, inject content, transform syntax
|
|
138
|
+
|
|
139
|
+
#### `afterContentParse(item, filePath)`
|
|
140
|
+
Executes after a content item is parsed. Can modify the item.
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `item`: Parsed content item object
|
|
144
|
+
- `filePath`: Path to the file
|
|
145
|
+
|
|
146
|
+
**Returns:** Modified item object (or original if unchanged)
|
|
147
|
+
|
|
148
|
+
**Use cases:** Add custom metadata, transform content, modify front matter
|
|
149
|
+
|
|
150
|
+
#### `beforeMarkdown(markdown, context)`
|
|
151
|
+
Executes before markdown is converted to HTML. Can modify the markdown.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
- `markdown`: Markdown content (string)
|
|
155
|
+
- `context`: Object with `filePath` and `frontMatter`
|
|
156
|
+
|
|
157
|
+
**Returns:** Modified markdown string (or original if unchanged)
|
|
158
|
+
|
|
159
|
+
**Use cases:** Transform markdown syntax, inject content
|
|
160
|
+
|
|
161
|
+
#### `afterMarkdown(html, context)`
|
|
162
|
+
Executes after markdown is converted to HTML. Can modify the HTML.
|
|
163
|
+
|
|
164
|
+
**Parameters:**
|
|
165
|
+
- `html`: Generated HTML (string)
|
|
166
|
+
- `context`: Object with `filePath` and `frontMatter`
|
|
167
|
+
|
|
168
|
+
**Returns:** Modified HTML string (or original if unchanged)
|
|
169
|
+
|
|
170
|
+
**Use cases:** Post-process HTML, inject scripts, modify structure
|
|
171
|
+
|
|
172
|
+
### Template Hooks
|
|
173
|
+
|
|
174
|
+
#### `addTemplateFilter(env, config)`
|
|
175
|
+
Allows plugins to register custom Nunjucks filters.
|
|
176
|
+
|
|
177
|
+
**Parameters:**
|
|
178
|
+
- `env`: Nunjucks environment object
|
|
179
|
+
- `config`: Site configuration object
|
|
180
|
+
|
|
181
|
+
**Use cases:** Add custom template filters for data transformation
|
|
182
|
+
|
|
183
|
+
**Example:**
|
|
184
|
+
```javascript
|
|
185
|
+
hooks: {
|
|
186
|
+
addTemplateFilter: (env, config) => {
|
|
187
|
+
env.addFilter('uppercase', (str) => str.toUpperCase());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### `addTemplateFunction(env, config)`
|
|
193
|
+
Allows plugins to register custom Nunjucks functions.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
- `env`: Nunjucks environment object
|
|
197
|
+
- `config`: Site configuration object
|
|
198
|
+
|
|
199
|
+
**Use cases:** Add custom template functions for complex operations
|
|
200
|
+
|
|
201
|
+
**Example:**
|
|
202
|
+
```javascript
|
|
203
|
+
hooks: {
|
|
204
|
+
addTemplateFunction: (env, config) => {
|
|
205
|
+
env.addGlobal('getApiData', async (url) => {
|
|
206
|
+
// Fetch and return data
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Marked Extension Hook
|
|
213
|
+
|
|
214
|
+
Plugins can add custom Marked extensions using the `addMarkedExtension` function from the plugin API or by using the `beforeBuild` hook to call it.
|
|
215
|
+
|
|
216
|
+
**Example:**
|
|
217
|
+
```javascript
|
|
218
|
+
import { addMarkedExtension } from '@terrymooreii/sia';
|
|
219
|
+
|
|
220
|
+
hooks: {
|
|
221
|
+
beforeBuild: (config, api) => {
|
|
222
|
+
addMarkedExtension({
|
|
223
|
+
renderer: {
|
|
224
|
+
// Custom renderer
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Plugin API
|
|
232
|
+
|
|
233
|
+
The `api` object passed to hooks provides utilities for plugins:
|
|
234
|
+
|
|
235
|
+
### `api.config`
|
|
236
|
+
Full site configuration object (read-only recommended)
|
|
237
|
+
|
|
238
|
+
### `api.writeFile(path, content)`
|
|
239
|
+
Write a file to the output directory.
|
|
240
|
+
|
|
241
|
+
**Parameters:**
|
|
242
|
+
- `path`: File path relative to output directory
|
|
243
|
+
- `content`: File content (string)
|
|
244
|
+
|
|
245
|
+
### `api.readFile(path)`
|
|
246
|
+
Read a file from the filesystem.
|
|
247
|
+
|
|
248
|
+
**Parameters:**
|
|
249
|
+
- `path`: Absolute file path
|
|
250
|
+
|
|
251
|
+
**Returns:** File content (string)
|
|
252
|
+
|
|
253
|
+
### `api.joinPath(...paths)`
|
|
254
|
+
Join path segments (wrapper around Node.js `path.join`).
|
|
255
|
+
|
|
256
|
+
**Parameters:**
|
|
257
|
+
- `...paths`: Path segments
|
|
258
|
+
|
|
259
|
+
**Returns:** Joined path (string)
|
|
260
|
+
|
|
261
|
+
### `api.log(message, level)`
|
|
262
|
+
Log a message.
|
|
263
|
+
|
|
264
|
+
**Parameters:**
|
|
265
|
+
- `message`: Log message (string)
|
|
266
|
+
- `level`: Log level - `'info'`, `'warn'`, or `'error'` (default: `'info'`)
|
|
267
|
+
|
|
268
|
+
## Plugin Structure
|
|
269
|
+
|
|
270
|
+
A plugin must export an object with the following structure:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
export default {
|
|
274
|
+
name: 'plugin-name', // Required: Unique plugin identifier
|
|
275
|
+
version: '1.0.0', // Required: Plugin version
|
|
276
|
+
dependencies: [], // Optional: Array of plugin names this depends on
|
|
277
|
+
configSchema: {}, // Optional: Configuration schema
|
|
278
|
+
hooks: { // Optional: Hook functions
|
|
279
|
+
afterBuild: async (siteData, config, api) => {
|
|
280
|
+
// Plugin logic
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Required Fields
|
|
287
|
+
|
|
288
|
+
- **name**: Unique identifier for the plugin (string)
|
|
289
|
+
- **version**: Plugin version (string)
|
|
290
|
+
|
|
291
|
+
### Optional Fields
|
|
292
|
+
|
|
293
|
+
- **dependencies**: Array of plugin names that must load before this plugin
|
|
294
|
+
- **configSchema**: Object describing plugin configuration options (for documentation)
|
|
295
|
+
- **hooks**: Object mapping hook names to hook functions
|
|
296
|
+
|
|
297
|
+
## Plugin Discovery
|
|
298
|
+
|
|
299
|
+
Sia discovers plugins from two sources:
|
|
300
|
+
|
|
301
|
+
1. **Local plugins**: Files in the `_plugins/` directory (`.js` or `.mjs` files)
|
|
302
|
+
2. **NPM packages**: Packages in `node_modules` matching the pattern `sia-plugin-*`
|
|
303
|
+
|
|
304
|
+
Plugins are loaded in dependency order, or in the order specified in `config.plugins.order`.
|
|
305
|
+
|
|
306
|
+
## Error Handling
|
|
307
|
+
|
|
308
|
+
By default, plugin errors are logged but don't stop the build. Set `plugins.strictMode: true` in your config to fail the build on plugin errors.
|
|
309
|
+
|
|
310
|
+
Errors include:
|
|
311
|
+
- Plugin loading failures
|
|
312
|
+
- Hook execution errors
|
|
313
|
+
- Validation errors
|
|
314
|
+
|
|
315
|
+
Error messages include the plugin name and detailed error information.
|
|
316
|
+
|
|
317
|
+
## Examples
|
|
318
|
+
|
|
319
|
+
See [Creating Plugins](./creating-plugins.md) for detailed examples of creating local and npm package plugins.
|
|
320
|
+
|
package/lib/build.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
@@ -6,6 +6,8 @@ import { buildSiteData, paginate, getPaginationUrls } from './collections.js';
|
|
|
6
6
|
import { createTemplateEngine, renderTemplate } from './templates.js';
|
|
7
7
|
import { copyImages, copyDefaultStyles, copyStaticAssets, copyCustomAssets, copyContentAssets, writeFile, ensureDir } from './assets.js';
|
|
8
8
|
import { resolveTheme } from './theme-resolver.js';
|
|
9
|
+
import { loadPlugins } from './plugins.js';
|
|
10
|
+
import { initializeHooks, registerPluginHooks, executeHook } from './hooks.js';
|
|
9
11
|
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
13
|
const __dirname = dirname(__filename);
|
|
@@ -195,6 +197,28 @@ function renderRSSFeed(env, siteData) {
|
|
|
195
197
|
console.log('📡 Generated RSS feed');
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Create plugin API object for hooks
|
|
202
|
+
*/
|
|
203
|
+
function createPluginAPI(config) {
|
|
204
|
+
return {
|
|
205
|
+
config,
|
|
206
|
+
writeFile: (path, content) => {
|
|
207
|
+
writeFile(path, content);
|
|
208
|
+
},
|
|
209
|
+
readFile: (path) => {
|
|
210
|
+
return readFileSync(path, 'utf-8');
|
|
211
|
+
},
|
|
212
|
+
joinPath: (...paths) => {
|
|
213
|
+
return join(...paths);
|
|
214
|
+
},
|
|
215
|
+
log: (message, level = 'info') => {
|
|
216
|
+
const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : 'ℹ️';
|
|
217
|
+
console.log(`${prefix} ${message}`);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
/**
|
|
199
223
|
* Main build function
|
|
200
224
|
*/
|
|
@@ -206,6 +230,32 @@ export async function build(options = {}) {
|
|
|
206
230
|
// Load configuration
|
|
207
231
|
const config = loadConfig(options.rootDir || process.cwd());
|
|
208
232
|
|
|
233
|
+
// Initialize plugin system
|
|
234
|
+
initializeHooks();
|
|
235
|
+
|
|
236
|
+
// Load plugins
|
|
237
|
+
let plugins = [];
|
|
238
|
+
try {
|
|
239
|
+
plugins = await loadPlugins(config);
|
|
240
|
+
|
|
241
|
+
// Register plugin hooks
|
|
242
|
+
if (plugins.length > 0) {
|
|
243
|
+
registerPluginHooks(plugins);
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
if (config.plugins?.strictMode) {
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
console.warn(`⚠️ Plugin loading error: ${err.message}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Execute afterConfigLoad hook
|
|
253
|
+
const pluginAPI = createPluginAPI(config);
|
|
254
|
+
await executeHook('afterConfigLoad', config, pluginAPI);
|
|
255
|
+
|
|
256
|
+
// Execute beforeBuild hook
|
|
257
|
+
await executeHook('beforeBuild', config, pluginAPI);
|
|
258
|
+
|
|
209
259
|
// Only show drafts in dev mode if explicitly enabled in config
|
|
210
260
|
// For production builds, always disable showDrafts
|
|
211
261
|
if (!options.devMode) {
|
|
@@ -225,14 +275,23 @@ export async function build(options = {}) {
|
|
|
225
275
|
const resolvedTheme = resolveTheme(themeName, config.rootDir);
|
|
226
276
|
|
|
227
277
|
// Build site data (collections, tags, etc.)
|
|
228
|
-
const siteData = buildSiteData(config);
|
|
278
|
+
const siteData = await buildSiteData(config);
|
|
279
|
+
|
|
280
|
+
// Execute afterContentLoad hook
|
|
281
|
+
await executeHook('afterContentLoad', siteData, config, pluginAPI);
|
|
282
|
+
|
|
283
|
+
// Execute beforeSiteData hook (allows modification of siteData)
|
|
284
|
+
await executeHook('beforeSiteData', siteData, config, pluginAPI);
|
|
229
285
|
|
|
230
286
|
// Copy custom assets and add to siteData
|
|
231
287
|
const customAssets = copyCustomAssets(config);
|
|
232
288
|
siteData.customAssets = customAssets;
|
|
233
289
|
|
|
234
290
|
// Create template engine with resolved theme
|
|
235
|
-
const env = createTemplateEngine(config, resolvedTheme);
|
|
291
|
+
const env = await createTemplateEngine(config, resolvedTheme);
|
|
292
|
+
|
|
293
|
+
// Execute beforeRender hook
|
|
294
|
+
await executeHook('beforeRender', siteData, config, pluginAPI);
|
|
236
295
|
|
|
237
296
|
// Render all content items
|
|
238
297
|
let itemCount = 0;
|
|
@@ -261,17 +320,23 @@ export async function build(options = {}) {
|
|
|
261
320
|
// Generate RSS feed
|
|
262
321
|
renderRSSFeed(env, siteData);
|
|
263
322
|
|
|
323
|
+
// Execute afterRender hook
|
|
324
|
+
await executeHook('afterRender', siteData, config, pluginAPI);
|
|
325
|
+
|
|
264
326
|
// Copy assets
|
|
265
327
|
copyImages(config);
|
|
266
328
|
copyDefaultStyles(config, resolvedTheme);
|
|
267
329
|
// Custom assets already copied above and added to siteData
|
|
268
330
|
copyStaticAssets(config);
|
|
269
331
|
|
|
332
|
+
// Execute afterBuild hook
|
|
333
|
+
await executeHook('afterBuild', siteData, config, pluginAPI);
|
|
334
|
+
|
|
270
335
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
271
336
|
console.log(`\n✅ Build complete in ${duration}s`);
|
|
272
337
|
console.log(`📁 Output: ${config.outputDir}\n`);
|
|
273
338
|
|
|
274
|
-
return { config, siteData };
|
|
339
|
+
return { config, siteData, plugins };
|
|
275
340
|
}
|
|
276
341
|
|
|
277
342
|
/**
|
package/lib/collections.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAllCollections } from './content.js';
|
|
2
|
+
import { executeHook } from './hooks.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build tag collections from all content
|
|
@@ -102,9 +103,9 @@ export function getPaginationUrls(baseUrl, pagination, basePath = '') {
|
|
|
102
103
|
/**
|
|
103
104
|
* Build the complete site data structure
|
|
104
105
|
*/
|
|
105
|
-
export function buildSiteData(config) {
|
|
106
|
+
export async function buildSiteData(config) {
|
|
106
107
|
// Load all content collections
|
|
107
|
-
const collections = loadAllCollections(config);
|
|
108
|
+
const collections = await loadAllCollections(config);
|
|
108
109
|
|
|
109
110
|
// Build tag collections
|
|
110
111
|
const tagCollections = buildTagCollections(collections);
|
|
@@ -112,6 +113,9 @@ export function buildSiteData(config) {
|
|
|
112
113
|
|
|
113
114
|
console.log(`🏷️ Found ${allTags.length} unique tags`);
|
|
114
115
|
|
|
116
|
+
// Execute afterTagCollections hook
|
|
117
|
+
await executeHook('afterTagCollections', tagCollections, { config, collections });
|
|
118
|
+
|
|
115
119
|
// Create paginated collections for listings
|
|
116
120
|
const paginatedCollections = {};
|
|
117
121
|
|
|
@@ -129,7 +133,7 @@ export function buildSiteData(config) {
|
|
|
129
133
|
}));
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
const siteData = {
|
|
133
137
|
config,
|
|
134
138
|
site: config.site,
|
|
135
139
|
collections,
|
|
@@ -138,6 +142,8 @@ export function buildSiteData(config) {
|
|
|
138
142
|
allTags,
|
|
139
143
|
paginatedTags
|
|
140
144
|
};
|
|
145
|
+
|
|
146
|
+
return siteData;
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/**
|
package/lib/config.js
CHANGED
|
@@ -48,6 +48,13 @@ const defaultConfig = {
|
|
|
48
48
|
assets: {
|
|
49
49
|
css: [], // Array of custom CSS file paths (relative to root)
|
|
50
50
|
js: [] // Array of custom JavaScript file paths (relative to root)
|
|
51
|
+
},
|
|
52
|
+
plugins: {
|
|
53
|
+
enabled: true, // Master switch for plugins
|
|
54
|
+
strictMode: false, // Fail build on plugin errors (default: continue)
|
|
55
|
+
order: [], // Explicit plugin execution order (optional)
|
|
56
|
+
plugins: [], // Explicit list of plugins to load (optional, empty = all)
|
|
57
|
+
config: {} // Plugin-specific configuration
|
|
51
58
|
}
|
|
52
59
|
};
|
|
53
60
|
|
package/lib/content.js
CHANGED
|
@@ -10,6 +10,7 @@ import { markedSmartypants } from 'marked-smartypants';
|
|
|
10
10
|
import markedAlert from 'marked-alert';
|
|
11
11
|
import markedLinkifyIt from 'marked-linkify-it';
|
|
12
12
|
import hljs from 'highlight.js';
|
|
13
|
+
import { executeHook, executeHookWithResult, getHookRegistry } from './hooks.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Default emoji map for shortcode support
|
|
@@ -84,7 +85,7 @@ const emojis = {
|
|
|
84
85
|
/**
|
|
85
86
|
* Configure marked with syntax highlighting, emoji support, and enhanced markdown features
|
|
86
87
|
*/
|
|
87
|
-
|
|
88
|
+
let marked = new Marked(
|
|
88
89
|
markedHighlight({
|
|
89
90
|
langPrefix: 'hljs language-',
|
|
90
91
|
highlight(code, lang) {
|
|
@@ -120,6 +121,13 @@ marked.setOptions({
|
|
|
120
121
|
breaks: false
|
|
121
122
|
});
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Add a Marked extension (for plugins)
|
|
126
|
+
*/
|
|
127
|
+
export function addMarkedExtension(extension) {
|
|
128
|
+
marked.use(extension);
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
/**
|
|
124
132
|
* Extract YouTube video ID from various URL formats
|
|
125
133
|
* Supports:
|
|
@@ -397,12 +405,22 @@ export function getDateFromFilename(filename) {
|
|
|
397
405
|
/**
|
|
398
406
|
* Parse a markdown file with front matter
|
|
399
407
|
*/
|
|
400
|
-
export function parseContent(filePath) {
|
|
401
|
-
|
|
408
|
+
export async function parseContent(filePath) {
|
|
409
|
+
let content = readFileSync(filePath, 'utf-8');
|
|
410
|
+
|
|
411
|
+
// Execute beforeParse hook
|
|
412
|
+
content = await executeHookWithResult('beforeContentParse', content, filePath);
|
|
413
|
+
|
|
402
414
|
const { data: frontMatter, content: markdown } = matter(content);
|
|
403
415
|
|
|
416
|
+
// Execute beforeMarkdown hook
|
|
417
|
+
let processedMarkdown = await executeHookWithResult('beforeMarkdown', markdown, { filePath, frontMatter });
|
|
418
|
+
|
|
404
419
|
// Parse markdown to HTML
|
|
405
|
-
let html = marked.parse(
|
|
420
|
+
let html = marked.parse(processedMarkdown);
|
|
421
|
+
|
|
422
|
+
// Execute afterMarkdown hook
|
|
423
|
+
html = await executeHookWithResult('afterMarkdown', html, { filePath, frontMatter });
|
|
406
424
|
|
|
407
425
|
// Convert any remaining YouTube/Giphy links to embeds (handles autolinked URLs)
|
|
408
426
|
html = embedYouTubeVideos(html);
|
|
@@ -447,7 +465,7 @@ export function parseContent(filePath) {
|
|
|
447
465
|
tags = tags.split(',').map(t => t.trim());
|
|
448
466
|
}
|
|
449
467
|
|
|
450
|
-
|
|
468
|
+
const item = {
|
|
451
469
|
...frontMatter,
|
|
452
470
|
slug,
|
|
453
471
|
date,
|
|
@@ -459,6 +477,11 @@ export function parseContent(filePath) {
|
|
|
459
477
|
filePath,
|
|
460
478
|
draft: frontMatter.draft || false
|
|
461
479
|
};
|
|
480
|
+
|
|
481
|
+
// Execute afterContentParse hook (allows modification of item)
|
|
482
|
+
const modifiedItem = await executeHookWithResult('afterContentParse', item, filePath);
|
|
483
|
+
|
|
484
|
+
return modifiedItem;
|
|
462
485
|
}
|
|
463
486
|
|
|
464
487
|
/**
|
|
@@ -490,7 +513,7 @@ export function getMarkdownFiles(dir) {
|
|
|
490
513
|
/**
|
|
491
514
|
* Load all content from a collection directory
|
|
492
515
|
*/
|
|
493
|
-
export function loadCollection(config, collectionName) {
|
|
516
|
+
export async function loadCollection(config, collectionName) {
|
|
494
517
|
const collectionConfig = config.collections[collectionName];
|
|
495
518
|
|
|
496
519
|
if (!collectionConfig) {
|
|
@@ -501,10 +524,10 @@ export function loadCollection(config, collectionName) {
|
|
|
501
524
|
const collectionDir = join(config.inputDir, collectionConfig.path);
|
|
502
525
|
const files = getMarkdownFiles(collectionDir);
|
|
503
526
|
|
|
504
|
-
const items =
|
|
505
|
-
.map(filePath => {
|
|
527
|
+
const items = await Promise.all(
|
|
528
|
+
files.map(async (filePath) => {
|
|
506
529
|
try {
|
|
507
|
-
const item = parseContent(filePath);
|
|
530
|
+
const item = await parseContent(filePath);
|
|
508
531
|
|
|
509
532
|
// Add collection-specific metadata
|
|
510
533
|
item.collection = collectionName;
|
|
@@ -529,21 +552,23 @@ export function loadCollection(config, collectionName) {
|
|
|
529
552
|
return null;
|
|
530
553
|
}
|
|
531
554
|
})
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const filteredItems = items.filter(item => {
|
|
558
|
+
if (item === null) return false;
|
|
559
|
+
// Include drafts if showDrafts is enabled in server config
|
|
560
|
+
if (item.draft && config.server?.showDrafts) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
// Otherwise, exclude drafts
|
|
564
|
+
return !item.draft;
|
|
565
|
+
});
|
|
541
566
|
|
|
542
567
|
// Sort items
|
|
543
568
|
const sortBy = collectionConfig.sortBy || 'date';
|
|
544
569
|
const sortOrder = collectionConfig.sortOrder || 'desc';
|
|
545
570
|
|
|
546
|
-
|
|
571
|
+
filteredItems.sort((a, b) => {
|
|
547
572
|
const aVal = a[sortBy];
|
|
548
573
|
const bVal = b[sortBy];
|
|
549
574
|
|
|
@@ -560,17 +585,17 @@ export function loadCollection(config, collectionName) {
|
|
|
560
585
|
return 0;
|
|
561
586
|
});
|
|
562
587
|
|
|
563
|
-
return
|
|
588
|
+
return filteredItems;
|
|
564
589
|
}
|
|
565
590
|
|
|
566
591
|
/**
|
|
567
592
|
* Load all collections defined in config
|
|
568
593
|
*/
|
|
569
|
-
export function loadAllCollections(config) {
|
|
594
|
+
export async function loadAllCollections(config) {
|
|
570
595
|
const collections = {};
|
|
571
596
|
|
|
572
597
|
for (const name of Object.keys(config.collections)) {
|
|
573
|
-
collections[name] = loadCollection(config, name);
|
|
598
|
+
collections[name] = await loadCollection(config, name);
|
|
574
599
|
console.log(`📚 Loaded ${collections[name].length} items from "${name}" collection`);
|
|
575
600
|
}
|
|
576
601
|
|
|
@@ -584,6 +609,7 @@ export default {
|
|
|
584
609
|
getDateFromFilename,
|
|
585
610
|
getMarkdownFiles,
|
|
586
611
|
loadCollection,
|
|
587
|
-
loadAllCollections
|
|
612
|
+
loadAllCollections,
|
|
613
|
+
addMarkedExtension
|
|
588
614
|
};
|
|
589
615
|
|