@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.
@@ -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
  /**
@@ -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
- return {
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
- const marked = new Marked(
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
- const content = readFileSync(filePath, 'utf-8');
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(markdown);
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
- return {
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 = files
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
- .filter(item => {
533
- if (item === null) return false;
534
- // Include drafts if showDrafts is enabled in server config
535
- if (item.draft && config.server?.showDrafts) {
536
- return true;
537
- }
538
- // Otherwise, exclude drafts
539
- return !item.draft;
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
- items.sort((a, b) => {
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 items;
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