@terrymooreii/sia 2.1.13 → 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/bin/cli.js CHANGED
@@ -15,6 +15,7 @@ import { buildCommand } from '../lib/build.js';
15
15
  import { newCommand } from '../lib/new.js';
16
16
  import { initCommand } from '../lib/init.js';
17
17
  import { themeCommand } from '../lib/theme.js';
18
+ import { migrateCommand } from '../lib/migrate.js';
18
19
 
19
20
  program
20
21
  .name('sia')
@@ -54,5 +55,11 @@ program
54
55
  .option('-q, --quick', 'Skip prompts and use defaults')
55
56
  .action(themeCommand);
56
57
 
58
+ program
59
+ .command('migrate')
60
+ .description('Migrate standalone .md files to folder-based structure (folder/index.md)')
61
+ .option('--dry-run', 'Preview changes without applying them', false)
62
+ .action(migrateCommand);
63
+
57
64
  program.parse();
58
65
 
package/docs/README.md CHANGED
@@ -10,6 +10,8 @@ Welcome to the Sia documentation. Sia is a simple, powerful static site generato
10
10
  | [Markdown Guide](markdown-guide.md) | Markdown syntax and all supported plugins |
11
11
  | [Front Matter Reference](front-matter.md) | YAML front matter options for posts, pages, and notes |
12
12
  | [Creating Themes](creating-themes.md) | How to create and customize themes |
13
+ | [Plugin System](plugins.md) | Extend Sia with plugins - hooks, API, and configuration |
14
+ | [Creating Plugins](creating-plugins.md) | Guide to creating local and npm package plugins |
13
15
 
14
16
  ## Quick Links
15
17
 
@@ -58,6 +60,7 @@ npm run build
58
60
  - **Live Reload** - Development server with hot reloading
59
61
  - **Multiple Themes** - Built-in themes (main, minimal, developer, magazine) with light/dark mode
60
62
  - **Custom Theme Packages** - Create and share themes as npm packages (`sia-theme-*`)
63
+ - **Plugin System** - Extend functionality with local or npm plugins (`sia-plugin-*`)
61
64
  - **RSS Feed** - Automatic RSS feed generation
62
65
  - **SEO Ready** - Open Graph and Twitter Card meta tags included
63
66
 
@@ -68,8 +71,17 @@ my-site/
68
71
  ├── _config.yml # Site configuration
69
72
  ├── src/
70
73
  │ ├── posts/ # Blog posts (markdown)
74
+ │ │ └── 2024-12-17-my-post/
75
+ │ │ ├── index.md
76
+ │ │ └── (assets can go here)
71
77
  │ ├── pages/ # Static pages
78
+ │ │ └── about/
79
+ │ │ ├── index.md
80
+ │ │ └── (assets can go here)
72
81
  │ ├── notes/ # Short notes/tweets
82
+ │ │ └── 2024-12-17-note-1234567890/
83
+ │ │ ├── index.md
84
+ │ │ └── (assets can go here)
73
85
  │ └── images/ # Images
74
86
  ├── assets/ # Static assets (optional)
75
87
  ├── static/ # Static assets (optional)
@@ -77,10 +89,14 @@ my-site/
77
89
  ├── favicon.ico # Site favicon (optional)
78
90
  ├── _layouts/ # Custom layouts (optional)
79
91
  ├── _includes/ # Custom includes (optional)
92
+ ├── _plugins/ # Local plugins (optional)
93
+ │ └── my-plugin.js
80
94
  ├── styles/ # Custom CSS (optional)
81
95
  └── dist/ # Generated output
82
96
  ```
83
97
 
98
+ Each post, page, and note is created as a folder containing an `index.md` file. This allows you to organize assets (images, PDFs, etc.) alongside your content in the same folder.
99
+
84
100
  ## Configuration
85
101
 
86
102
  Edit `_config.yml` to customize your site:
@@ -125,6 +141,12 @@ server:
125
141
  assets:
126
142
  css: [] # Custom CSS files (paths relative to root)
127
143
  js: [] # Custom JavaScript files (paths relative to root)
144
+
145
+ plugins:
146
+ enabled: true # Master switch for plugins
147
+ strictMode: false # Fail build on plugin errors
148
+ order: [] # Explicit plugin execution order (optional)
149
+ config: {} # Plugin-specific configuration
128
150
  ```
129
151
 
130
152
  ## Custom CSS and JavaScript
@@ -162,6 +184,40 @@ This will:
162
184
 
163
185
  And inject them into all pages automatically.
164
186
 
187
+ ## Plugin System
188
+
189
+ Sia includes a powerful plugin system that allows you to extend functionality at key points in the build lifecycle. Plugins can:
190
+
191
+ - Transform content during parsing
192
+ - Generate additional files (search indexes, sitemaps, etc.)
193
+ - Add custom Marked extensions for markdown processing
194
+ - Register custom Nunjucks template filters and functions
195
+ - Modify site data before rendering
196
+ - Perform post-build tasks
197
+
198
+ ### Using Plugins
199
+
200
+ Plugins can be local (in `_plugins/` directory) or npm packages (with `sia-plugin-*` naming):
201
+
202
+ ```bash
203
+ # Install an npm plugin
204
+ npm install sia-plugin-search
205
+
206
+ # Or create a local plugin in _plugins/my-plugin.js
207
+ ```
208
+
209
+ Configure plugins in `_config.yml`:
210
+
211
+ ```yaml
212
+ plugins:
213
+ enabled: true
214
+ config:
215
+ sia-plugin-search:
216
+ outputPath: search-index.json
217
+ ```
218
+
219
+ See the [Plugin System documentation](plugins.md) for complete details and the [Creating Plugins guide](creating-plugins.md) for examples.
220
+
165
221
  ## Static Assets
166
222
 
167
223
  Sia automatically copies static assets during the build process. You can place static files in any of these locations:
@@ -225,6 +281,7 @@ dist/
225
281
  | `sia new page "Title"` | Create a new page |
226
282
  | `sia new note "Content"` | Create a new note |
227
283
  | `sia theme <name>` | Create a new theme package |
284
+ | `sia migrate` | Migrate standalone .md files to folder structure |
228
285
 
229
286
  ## License
230
287
 
@@ -0,0 +1,509 @@
1
+ # Creating Plugins
2
+
3
+ This guide explains how to create plugins for Sia, both as local plugins in your project and as npm packages that can be shared with others.
4
+
5
+ ## Plugin Types
6
+
7
+ Sia supports two types of plugins:
8
+
9
+ 1. **Local plugins**: JavaScript files in your project's `_plugins/` directory
10
+ 2. **NPM package plugins**: Published npm packages with the `sia-plugin-*` naming convention
11
+
12
+ ## Local Plugins
13
+
14
+ Local plugins are perfect for project-specific functionality that you don't need to share.
15
+
16
+ ### Creating a Local Plugin
17
+
18
+ 1. Create a `_plugins/` directory in your project root (if it doesn't exist)
19
+ 2. Create a JavaScript file (`.js` or `.mjs`) in that directory
20
+ 3. Export a plugin object
21
+
22
+ **Example: `_plugins/search-index.js`**
23
+
24
+ ```javascript
25
+ export default {
26
+ name: 'search-index',
27
+ version: '1.0.0',
28
+ hooks: {
29
+ afterBuild: async (siteData, config, api) => {
30
+ // Generate search index
31
+ const searchData = {
32
+ pages: siteData.collections.pages.map(item => ({
33
+ title: item.title,
34
+ url: item.url,
35
+ excerpt: item.excerpt,
36
+ tags: item.tags
37
+ })),
38
+ posts: siteData.collections.posts.map(item => ({
39
+ title: item.title,
40
+ url: item.url,
41
+ date: item.date.toISOString(),
42
+ excerpt: item.excerpt,
43
+ tags: item.tags
44
+ })),
45
+ notes: siteData.collections.notes.map(item => ({
46
+ title: item.title,
47
+ url: item.url,
48
+ date: item.date.toISOString(),
49
+ excerpt: item.excerpt
50
+ }))
51
+ };
52
+
53
+ // Write search index file
54
+ const outputPath = api.joinPath(config.outputDir, 'search-index.json');
55
+ api.writeFile(outputPath, JSON.stringify(searchData, null, 2));
56
+ api.log('Generated search index', 'info');
57
+ }
58
+ }
59
+ };
60
+ ```
61
+
62
+ ### Local Plugin with Configuration
63
+
64
+ You can access plugin-specific configuration from `config.plugins.config`:
65
+
66
+ ```javascript
67
+ export default {
68
+ name: 'search-index',
69
+ version: '1.0.0',
70
+ configSchema: {
71
+ outputPath: { type: 'string', default: 'search-index.json' },
72
+ includeContent: { type: 'boolean', default: false }
73
+ },
74
+ hooks: {
75
+ afterBuild: async (siteData, config, api) => {
76
+ const pluginConfig = config.plugins?.config?.['search-index'] || {};
77
+ const outputPath = pluginConfig.outputPath || 'search-index.json';
78
+ const includeContent = pluginConfig.includeContent || false;
79
+
80
+ const searchData = {
81
+ pages: siteData.collections.pages.map(item => ({
82
+ title: item.title,
83
+ url: item.url,
84
+ excerpt: item.excerpt,
85
+ ...(includeContent && { content: item.content })
86
+ })),
87
+ // ... posts, notes
88
+ };
89
+
90
+ api.writeFile(
91
+ api.joinPath(config.outputDir, outputPath),
92
+ JSON.stringify(searchData, null, 2)
93
+ );
94
+ }
95
+ }
96
+ };
97
+ ```
98
+
99
+ Configure in `_config.yml`:
100
+
101
+ ```yaml
102
+ plugins:
103
+ config:
104
+ search-index:
105
+ outputPath: search-index.json
106
+ includeContent: false
107
+ ```
108
+
109
+ ## NPM Package Plugins
110
+
111
+ NPM package plugins can be shared with the community and installed via npm.
112
+
113
+ ### Creating an NPM Package Plugin
114
+
115
+ 1. Create a new npm package with name starting with `sia-plugin-`
116
+ 2. Set up the package structure
117
+ 3. Export the plugin object
118
+
119
+ ### Package Structure
120
+
121
+ ```
122
+ sia-plugin-example/
123
+ ├── package.json
124
+ ├── index.js
125
+ ├── README.md
126
+ └── LICENSE
127
+ ```
128
+
129
+ ### package.json
130
+
131
+ ```json
132
+ {
133
+ "name": "sia-plugin-example",
134
+ "version": "1.0.0",
135
+ "description": "Example Sia plugin",
136
+ "main": "index.js",
137
+ "type": "module",
138
+ "keywords": [
139
+ "sia",
140
+ "sia-plugin",
141
+ "static-site-generator"
142
+ ],
143
+ "author": "Your Name",
144
+ "license": "MIT",
145
+ "engines": {
146
+ "node": ">=18.0.0"
147
+ },
148
+ "peerDependencies": {
149
+ "@terrymooreii/sia": "^2.0.0"
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### index.js
155
+
156
+ ```javascript
157
+ export default {
158
+ name: 'sia-plugin-example',
159
+ version: '1.0.0',
160
+ configSchema: {
161
+ enabled: { type: 'boolean', default: true },
162
+ outputFile: { type: 'string', default: 'example-output.json' }
163
+ },
164
+ hooks: {
165
+ afterBuild: async (siteData, config, api) => {
166
+ const pluginConfig = config.plugins?.config?.['sia-plugin-example'] || {};
167
+
168
+ if (pluginConfig.enabled === false) {
169
+ return;
170
+ }
171
+
172
+ const output = {
173
+ buildDate: new Date().toISOString(),
174
+ totalPages: siteData.collections.pages.length,
175
+ totalPosts: siteData.collections.posts.length,
176
+ totalNotes: siteData.collections.notes.length,
177
+ totalTags: siteData.allTags.length
178
+ };
179
+
180
+ api.writeFile(
181
+ api.joinPath(config.outputDir, pluginConfig.outputFile || 'example-output.json'),
182
+ JSON.stringify(output, null, 2)
183
+ );
184
+
185
+ api.log(`Generated ${pluginConfig.outputFile}`, 'info');
186
+ }
187
+ }
188
+ };
189
+ ```
190
+
191
+ ### Installing and Using
192
+
193
+ Users install your plugin:
194
+
195
+ ```bash
196
+ npm install sia-plugin-example
197
+ ```
198
+
199
+ Then configure it in `_config.yml`:
200
+
201
+ ```yaml
202
+ plugins:
203
+ config:
204
+ sia-plugin-example:
205
+ enabled: true
206
+ outputFile: stats.json
207
+ ```
208
+
209
+ ## Advanced Examples
210
+
211
+ ### Content Transformation Plugin
212
+
213
+ Transform content during parsing:
214
+
215
+ ```javascript
216
+ export default {
217
+ name: 'content-transformer',
218
+ version: '1.0.0',
219
+ hooks: {
220
+ beforeMarkdown: (markdown, context) => {
221
+ // Replace custom syntax
222
+ return markdown.replace(/\[TOC\]/g, '<!-- Table of Contents -->');
223
+ },
224
+ afterMarkdown: (html, context) => {
225
+ // Inject custom HTML
226
+ return html.replace(
227
+ '<!-- Table of Contents -->',
228
+ '<nav class="toc">...</nav>'
229
+ );
230
+ }
231
+ }
232
+ };
233
+ ```
234
+
235
+ ### Custom Template Filter Plugin
236
+
237
+ Add custom Nunjucks filters:
238
+
239
+ ```javascript
240
+ export default {
241
+ name: 'custom-filters',
242
+ version: '1.0.0',
243
+ hooks: {
244
+ addTemplateFilter: (env, config) => {
245
+ // Add a filter to format numbers
246
+ env.addFilter('formatNumber', (num) => {
247
+ return new Intl.NumberFormat().format(num);
248
+ });
249
+
250
+ // Add a filter to truncate text
251
+ env.addFilter('truncate', (str, length = 50) => {
252
+ if (str.length <= length) return str;
253
+ return str.substring(0, length) + '...';
254
+ });
255
+ }
256
+ }
257
+ };
258
+ ```
259
+
260
+ Use in templates:
261
+
262
+ ```nunjucks
263
+ {{ post.wordCount | formatNumber }}
264
+ {{ post.excerpt | truncate(100) }}
265
+ ```
266
+
267
+ ### Custom Marked Extension Plugin
268
+
269
+ Add custom markdown syntax:
270
+
271
+ ```javascript
272
+ import { addMarkedExtension } from '@terrymooreii/sia';
273
+
274
+ export default {
275
+ name: 'custom-markdown',
276
+ version: '1.0.0',
277
+ hooks: {
278
+ beforeBuild: (config, api) => {
279
+ addMarkedExtension({
280
+ renderer: {
281
+ // Custom blockquote renderer
282
+ blockquote(quote) {
283
+ return `<blockquote class="custom-quote">${quote}</blockquote>`;
284
+ }
285
+ }
286
+ });
287
+ }
288
+ }
289
+ };
290
+ ```
291
+
292
+ ### Sitemap Generator Plugin
293
+
294
+ Generate a sitemap.xml:
295
+
296
+ ```javascript
297
+ export default {
298
+ name: 'sitemap-generator',
299
+ version: '1.0.0',
300
+ hooks: {
301
+ afterBuild: async (siteData, config, api) => {
302
+ const siteUrl = config.site.url.replace(/\/$/, '');
303
+ const basePath = config.site.basePath || '';
304
+
305
+ const urls = [];
306
+
307
+ // Add homepage
308
+ urls.push({
309
+ loc: `${siteUrl}${basePath}/`,
310
+ lastmod: new Date().toISOString().split('T')[0],
311
+ changefreq: 'daily',
312
+ priority: '1.0'
313
+ });
314
+
315
+ // Add all content items
316
+ for (const [collectionName, items] of Object.entries(siteData.collections)) {
317
+ for (const item of items) {
318
+ urls.push({
319
+ loc: `${siteUrl}${item.url}`,
320
+ lastmod: item.date.toISOString().split('T')[0],
321
+ changefreq: 'monthly',
322
+ priority: collectionName === 'pages' ? '0.8' : '0.6'
323
+ });
324
+ }
325
+ }
326
+
327
+ // Generate XML
328
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
329
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
330
+ ${urls.map(url => ` <url>
331
+ <loc>${url.loc}</loc>
332
+ <lastmod>${url.lastmod}</lastmod>
333
+ <changefreq>${url.changefreq}</changefreq>
334
+ <priority>${url.priority}</priority>
335
+ </url>`).join('\n')}
336
+ </urlset>`;
337
+
338
+ api.writeFile(
339
+ api.joinPath(config.outputDir, 'sitemap.xml'),
340
+ sitemap
341
+ );
342
+
343
+ api.log('Generated sitemap.xml', 'info');
344
+ }
345
+ }
346
+ };
347
+ ```
348
+
349
+ ### Plugin with Dependencies
350
+
351
+ Plugins can depend on other plugins:
352
+
353
+ ```javascript
354
+ export default {
355
+ name: 'enhanced-search',
356
+ version: '1.0.0',
357
+ dependencies: ['search-index'], // Requires search-index plugin
358
+ hooks: {
359
+ afterBuild: async (siteData, config, api) => {
360
+ // This plugin enhances the search index created by search-index plugin
361
+ // It runs after search-index because of the dependency
362
+ }
363
+ }
364
+ };
365
+ ```
366
+
367
+ ## Best Practices
368
+
369
+ ### 1. Error Handling
370
+
371
+ Always handle errors gracefully:
372
+
373
+ ```javascript
374
+ hooks: {
375
+ afterBuild: async (siteData, config, api) => {
376
+ try {
377
+ // Plugin logic
378
+ } catch (err) {
379
+ api.log(`Plugin error: ${err.message}`, 'error');
380
+ // Don't throw - let the build continue
381
+ }
382
+ }
383
+ }
384
+ ```
385
+
386
+ ### 2. Configuration Validation
387
+
388
+ Validate plugin configuration:
389
+
390
+ ```javascript
391
+ hooks: {
392
+ beforeBuild: (config, api) => {
393
+ const pluginConfig = config.plugins?.config?.['my-plugin'] || {};
394
+
395
+ if (pluginConfig.requiredOption === undefined) {
396
+ throw new Error('my-plugin: requiredOption is required');
397
+ }
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### 3. Async Operations
403
+
404
+ Use async/await for asynchronous operations:
405
+
406
+ ```javascript
407
+ hooks: {
408
+ afterBuild: async (siteData, config, api) => {
409
+ const data = await fetchExternalData();
410
+ // Process data
411
+ }
412
+ }
413
+ ```
414
+
415
+ ### 4. Logging
416
+
417
+ Use the API's logging function:
418
+
419
+ ```javascript
420
+ api.log('Processing complete', 'info');
421
+ api.log('Warning: something unusual', 'warn');
422
+ api.log('Error occurred', 'error');
423
+ ```
424
+
425
+ ### 5. Documentation
426
+
427
+ Document your plugin:
428
+
429
+ - Explain what it does
430
+ - List configuration options
431
+ - Provide usage examples
432
+ - Include version compatibility
433
+
434
+ ### 6. Testing
435
+
436
+ Test your plugin:
437
+
438
+ 1. Create a test Sia site
439
+ 2. Add your plugin
440
+ 3. Run `sia build`
441
+ 4. Verify the output
442
+
443
+ ### 7. Version Compatibility
444
+
445
+ Specify Sia version requirements in your plugin's README:
446
+
447
+ ```markdown
448
+ ## Requirements
449
+
450
+ - Sia >= 2.0.0
451
+ - Node.js >= 18.0.0
452
+ ```
453
+
454
+ ## Publishing NPM Plugins
455
+
456
+ 1. **Prepare your package:**
457
+ - Write a clear README
458
+ - Add a LICENSE file
459
+ - Test thoroughly
460
+
461
+ 2. **Publish to npm:**
462
+ ```bash
463
+ npm login
464
+ npm publish
465
+ ```
466
+
467
+ 3. **Tag your releases:**
468
+ ```bash
469
+ git tag v1.0.0
470
+ git push --tags
471
+ ```
472
+
473
+ 4. **Update documentation:**
474
+ - Add to Sia's plugin list (if applicable)
475
+ - Share on social media/forums
476
+
477
+ ## Troubleshooting
478
+
479
+ ### Plugin Not Loading
480
+
481
+ - Check that the plugin file is in `_plugins/` or installed via npm
482
+ - Verify the plugin exports a default object with `name` and `version`
483
+ - Check console output for error messages
484
+
485
+ ### Hook Not Executing
486
+
487
+ - Verify the hook name is correct
488
+ - Check that the hook function is properly defined
489
+ - Ensure the plugin is enabled (`plugins.enabled: true`)
490
+
491
+ ### Configuration Not Working
492
+
493
+ - Verify configuration is in `config.plugins.config[pluginName]`
494
+ - Check that plugin name matches exactly (case-sensitive)
495
+ - Validate configuration structure matches `configSchema`
496
+
497
+ ### Build Failing
498
+
499
+ - Set `plugins.strictMode: false` to see detailed error messages
500
+ - Check plugin dependencies are installed
501
+ - Verify Node.js version compatibility
502
+
503
+ ## Resources
504
+
505
+ - [Plugin System Documentation](./plugins.md) - Complete hook reference
506
+ - [Sia GitHub Repository](https://github.com/terrymooreii/sia) - Source code and issues
507
+ - [Marked Documentation](https://marked.js.org/) - For custom markdown extensions
508
+ - [Nunjucks Documentation](https://mozilla.github.io/nunjucks/) - For custom template filters/functions
509
+
@@ -366,19 +366,27 @@ permalink: /featured/special-post/
366
366
 
367
367
  ## Date from Filename
368
368
 
369
- Sia can extract dates from filenames using this pattern:
369
+ Sia can extract dates from filenames (or folder names when using `index.md`) using this pattern:
370
370
 
371
371
  ```
372
372
  YYYY-MM-DD-slug.md
373
373
  ```
374
374
 
375
+ or for folder-based content:
376
+
377
+ ```
378
+ YYYY-MM-DD-slug/index.md
379
+ ```
380
+
375
381
  ### Examples
376
382
 
377
- | Filename | Extracted Date | Extracted Slug |
378
- |----------|----------------|----------------|
379
- | `2024-12-17-my-post.md` | December 17, 2024 | `my-post` |
380
- | `2024-01-05-new-year.md` | January 5, 2024 | `new-year` |
381
- | `about.md` | Current date | `about` |
383
+ | Filename/Folder | Extracted Date | Extracted Slug |
384
+ |-----------------|----------------|----------------|
385
+ | `2024-12-17-my-post/index.md` | December 17, 2024 | `my-post` |
386
+ | `2024-01-05-new-year/index.md` | January 5, 2024 | `new-year` |
387
+ | `about/index.md` | Current date | `about` |
388
+ | `2024-12-17-my-post.md` | December 17, 2024 | `my-post` (backward compatible) |
389
+ | `about.md` | Current date | `about` (backward compatible) |
382
390
 
383
391
  ### Priority
384
392
 
@@ -391,8 +399,8 @@ Date resolution follows this priority:
391
399
  Slug resolution:
392
400
 
393
401
  1. `slug` in front matter (highest priority)
394
- 2. Slug extracted from filename (after date prefix)
395
- 3. Slugified filename
402
+ 2. Slug extracted from filename or folder name (after date prefix if present)
403
+ 3. Slugified filename or folder name
396
404
 
397
405
  ---
398
406