@terrymooreii/sia 2.1.13 → 2.2.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
@@ -68,8 +68,17 @@ my-site/
68
68
  ├── _config.yml # Site configuration
69
69
  ├── src/
70
70
  │ ├── posts/ # Blog posts (markdown)
71
+ │ │ └── 2024-12-17-my-post/
72
+ │ │ ├── index.md
73
+ │ │ └── (assets can go here)
71
74
  │ ├── pages/ # Static pages
75
+ │ │ └── about/
76
+ │ │ ├── index.md
77
+ │ │ └── (assets can go here)
72
78
  │ ├── notes/ # Short notes/tweets
79
+ │ │ └── 2024-12-17-note-1234567890/
80
+ │ │ ├── index.md
81
+ │ │ └── (assets can go here)
73
82
  │ └── images/ # Images
74
83
  ├── assets/ # Static assets (optional)
75
84
  ├── static/ # Static assets (optional)
@@ -81,6 +90,8 @@ my-site/
81
90
  └── dist/ # Generated output
82
91
  ```
83
92
 
93
+ 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.
94
+
84
95
  ## Configuration
85
96
 
86
97
  Edit `_config.yml` to customize your site:
@@ -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
 
package/lib/assets.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  unlinkSync,
10
10
  rmdirSync
11
11
  } from 'fs';
12
- import { join, dirname, relative, extname } from 'path';
12
+ import { join, dirname, relative, extname, basename } from 'path';
13
13
  import { resolveTheme, getBuiltInThemesDir } from './theme-resolver.js';
14
14
 
15
15
  // Supported asset extensions
@@ -115,6 +115,40 @@ export function copyImages(config) {
115
115
  return copied;
116
116
  }
117
117
 
118
+ /**
119
+ * Copy assets from a content folder to the output directory
120
+ * @param {string} contentFolderPath - Path to the content folder (e.g., src/posts/2024-12-17-my-post/)
121
+ * @param {string} outputDir - Output directory for this content item (e.g., dist/blog/my-post/)
122
+ * @returns {number} Number of assets copied
123
+ */
124
+ export function copyContentAssets(contentFolderPath, outputDir) {
125
+ if (!existsSync(contentFolderPath)) {
126
+ return 0;
127
+ }
128
+
129
+ const files = getAllFiles(contentFolderPath);
130
+ let copied = 0;
131
+
132
+ for (const file of files) {
133
+ // Skip the index.md file itself
134
+ const fileName = basename(file);
135
+ if (fileName === 'index.md' || fileName === 'index.markdown') {
136
+ continue;
137
+ }
138
+
139
+ // Only copy asset files
140
+ if (isAsset(file)) {
141
+ const relativePath = relative(contentFolderPath, file);
142
+ const destPath = join(outputDir, relativePath);
143
+
144
+ copyFile(file, destPath);
145
+ copied++;
146
+ }
147
+ }
148
+
149
+ return copied;
150
+ }
151
+
118
152
  /**
119
153
  * Check if directory has CSS files
120
154
  */
@@ -321,6 +355,7 @@ export default {
321
355
  copyFile,
322
356
  copyAssets,
323
357
  copyImages,
358
+ copyContentAssets,
324
359
  copyDefaultStyles,
325
360
  copyStaticAssets,
326
361
  copyCustomAssets,
package/lib/build.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { loadConfig } from './config.js';
5
5
  import { buildSiteData, paginate, getPaginationUrls } from './collections.js';
6
6
  import { createTemplateEngine, renderTemplate } from './templates.js';
7
- import { copyImages, copyDefaultStyles, copyStaticAssets, copyCustomAssets, writeFile, ensureDir } from './assets.js';
7
+ import { copyImages, copyDefaultStyles, copyStaticAssets, copyCustomAssets, copyContentAssets, writeFile, ensureDir } from './assets.js';
8
8
  import { resolveTheme } from './theme-resolver.js';
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url);
@@ -35,6 +35,11 @@ function renderContentItem(env, item, siteData) {
35
35
  });
36
36
 
37
37
  writeFile(item.outputPath, html);
38
+
39
+ // Copy assets from content folder to output directory
40
+ const contentFolder = dirname(item.filePath);
41
+ const outputDir = dirname(item.outputPath);
42
+ return copyContentAssets(contentFolder, outputDir);
38
43
  }
39
44
 
40
45
  /**
@@ -231,16 +236,22 @@ export async function build(options = {}) {
231
236
 
232
237
  // Render all content items
233
238
  let itemCount = 0;
239
+ let totalContentAssets = 0;
234
240
 
235
241
  for (const [collectionName, items] of Object.entries(siteData.collections)) {
236
242
  for (const item of items) {
237
- renderContentItem(env, item, siteData);
243
+ const assetsCopied = renderContentItem(env, item, siteData);
244
+ totalContentAssets += assetsCopied;
238
245
  itemCount++;
239
246
  }
240
247
  }
241
248
 
242
249
  console.log(`📄 Generated ${itemCount} content pages`);
243
250
 
251
+ if (totalContentAssets > 0) {
252
+ console.log(`📦 Copied ${totalContentAssets} asset(s) from content folders`);
253
+ }
254
+
244
255
  // Render listing pages
245
256
  renderHomepage(env, siteData);
246
257
  renderBlogListing(env, siteData);
package/lib/content.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
2
- import { join, basename, extname, relative } from 'path';
2
+ import { join, basename, extname, dirname } from 'path';
3
3
  import matter from 'gray-matter';
4
4
  import { Marked } from 'marked';
5
5
  import { markedHighlight } from 'marked-highlight';
@@ -326,11 +326,28 @@ export function slugify(str) {
326
326
 
327
327
  /**
328
328
  * Extract slug from filename (removes date prefix if present)
329
+ * If the file is index.md, extracts from parent directory name instead
329
330
  */
330
331
  export function getSlugFromFilename(filename) {
331
332
  // Remove extension
332
333
  const name = basename(filename, extname(filename));
333
334
 
335
+ // If file is index.md, extract from parent directory name
336
+ if (name === 'index') {
337
+ const parentDir = dirname(filename);
338
+ const parentDirName = basename(parentDir);
339
+
340
+ // Check for date prefix pattern: YYYY-MM-DD-slug
341
+ const datePattern = /^\d{4}-\d{2}-\d{2}-(.+)$/;
342
+ const match = parentDirName.match(datePattern);
343
+
344
+ if (match) {
345
+ return match[1];
346
+ }
347
+
348
+ return slugify(parentDirName);
349
+ }
350
+
334
351
  // Check for date prefix pattern: YYYY-MM-DD-slug
335
352
  const datePattern = /^\d{4}-\d{2}-\d{2}-(.+)$/;
336
353
  const match = name.match(datePattern);
@@ -344,9 +361,27 @@ export function getSlugFromFilename(filename) {
344
361
 
345
362
  /**
346
363
  * Extract date from filename if present
364
+ * If the file is index.md, extracts from parent directory name instead
347
365
  */
348
366
  export function getDateFromFilename(filename) {
349
367
  const name = basename(filename, extname(filename));
368
+
369
+ // If file is index.md, extract from parent directory name
370
+ if (name === 'index') {
371
+ const parentDir = dirname(filename);
372
+ const parentDirName = basename(parentDir);
373
+ const datePattern = /^(\d{4}-\d{2}-\d{2})/;
374
+ const match = parentDirName.match(datePattern);
375
+
376
+ if (match) {
377
+ // Parse as local date, not UTC
378
+ const [year, month, day] = match[1].split('-').map(Number);
379
+ return new Date(year, month - 1, day);
380
+ }
381
+
382
+ return null;
383
+ }
384
+
350
385
  const datePattern = /^(\d{4}-\d{2}-\d{2})/;
351
386
  const match = name.match(datePattern);
352
387
 
package/lib/migrate.js ADDED
@@ -0,0 +1,180 @@
1
+ import { readdirSync, statSync, existsSync, mkdirSync, renameSync } from 'fs';
2
+ import { join, dirname, basename, extname } from 'path';
3
+ import { loadConfig } from './config.js';
4
+ import { getMarkdownFiles } from './content.js';
5
+
6
+ /**
7
+ * Check if a file is already in folder-based structure
8
+ */
9
+ function isAlreadyMigrated(filePath) {
10
+ try {
11
+ const name = basename(filePath, extname(filePath));
12
+ const parentDir = dirname(filePath);
13
+ // Check if file is index.md and parent is a directory (not the collection root)
14
+ return name === 'index' && statSync(parentDir).isDirectory();
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Check if a file is a standalone .md file (needs migration)
22
+ * Returns true if file is directly in collection directory (not in subdirectory)
23
+ */
24
+ function needsMigration(filePath, collectionDir) {
25
+ // Normalize paths for comparison
26
+ const normalizedFilePath = filePath.replace(/\\/g, '/');
27
+ const normalizedCollectionDir = collectionDir.replace(/\\/g, '/');
28
+
29
+ // Get relative path from collection directory
30
+ const relativePath = normalizedFilePath.replace(normalizedCollectionDir + '/', '');
31
+
32
+ // If relative path contains no slashes, it's directly in collection dir
33
+ // Also check it's not already index.md in a folder
34
+ const isStandalone = !relativePath.includes('/');
35
+ const isNotIndex = basename(filePath, extname(filePath)) !== 'index';
36
+
37
+ return isStandalone && isNotIndex;
38
+ }
39
+
40
+ /**
41
+ * Migrate a single file to folder structure
42
+ */
43
+ function migrateFile(filePath, collectionDir, dryRun = false) {
44
+ const fileName = basename(filePath);
45
+ const nameWithoutExt = basename(filePath, extname(filePath));
46
+ const newFolder = join(collectionDir, nameWithoutExt);
47
+ const newFilePath = join(newFolder, 'index.md');
48
+
49
+ // Skip if already migrated
50
+ if (isAlreadyMigrated(filePath)) {
51
+ return { status: 'skipped', reason: 'already_migrated', file: filePath };
52
+ }
53
+
54
+ // Skip if target folder already exists
55
+ if (existsSync(newFolder)) {
56
+ return { status: 'skipped', reason: 'folder_exists', file: filePath, folder: newFolder };
57
+ }
58
+
59
+ if (dryRun) {
60
+ return { status: 'would_migrate', from: filePath, to: newFilePath };
61
+ }
62
+
63
+ try {
64
+ // Create folder
65
+ mkdirSync(newFolder, { recursive: true });
66
+
67
+ // Move file to new location
68
+ renameSync(filePath, newFilePath);
69
+
70
+ return { status: 'migrated', from: filePath, to: newFilePath };
71
+ } catch (error) {
72
+ return { status: 'error', file: filePath, error: error.message };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Migrate all content files to folder structure
78
+ */
79
+ export async function migrateContent(options = {}) {
80
+ const {
81
+ dryRun = false,
82
+ rootDir = process.cwd()
83
+ } = options;
84
+
85
+ const config = loadConfig(rootDir);
86
+ const collections = ['posts', 'pages', 'notes'];
87
+ const results = {
88
+ migrated: [],
89
+ skipped: [],
90
+ errors: []
91
+ };
92
+
93
+ console.log('\n🔄 Content Migration Tool\n');
94
+
95
+ if (dryRun) {
96
+ console.log('⚠️ DRY RUN MODE - No files will be modified\n');
97
+ }
98
+
99
+ for (const collectionName of collections) {
100
+ const collectionConfig = config.collections[collectionName];
101
+ if (!collectionConfig) {
102
+ continue;
103
+ }
104
+
105
+ const collectionDir = join(config.inputDir, collectionConfig.path);
106
+
107
+ if (!existsSync(collectionDir)) {
108
+ console.log(`⚠️ Collection directory not found: ${collectionDir}`);
109
+ continue;
110
+ }
111
+
112
+ console.log(`📁 Processing ${collectionName} collection...`);
113
+
114
+ // Get all markdown files in the collection directory
115
+ const allFiles = getMarkdownFiles(collectionDir);
116
+
117
+ // Filter to only standalone files (not already in folders)
118
+ const filesToMigrate = allFiles.filter(filePath => {
119
+ return needsMigration(filePath, collectionDir);
120
+ });
121
+
122
+ if (filesToMigrate.length === 0) {
123
+ console.log(` No files to migrate in ${collectionName} collection\n`);
124
+ continue;
125
+ }
126
+
127
+ console.log(` Found ${filesToMigrate.length} file(s) to migrate`);
128
+
129
+ for (const filePath of filesToMigrate) {
130
+ const result = migrateFile(filePath, collectionDir, dryRun);
131
+
132
+ if (result.status === 'migrated' || result.status === 'would_migrate') {
133
+ results.migrated.push(result);
134
+ const folderName = basename(dirname(result.to));
135
+ console.log(` ✓ ${basename(filePath)} → ${folderName}/index.md`);
136
+ } else if (result.status === 'skipped') {
137
+ results.skipped.push(result);
138
+ const reason = result.reason === 'already_migrated'
139
+ ? 'already in folder structure'
140
+ : 'target folder exists';
141
+ console.log(` ⊘ Skipped: ${basename(filePath)} (${reason})`);
142
+ } else {
143
+ results.errors.push(result);
144
+ console.error(` ✗ Error: ${basename(filePath)} - ${result.error}`);
145
+ }
146
+ }
147
+
148
+ console.log('');
149
+ }
150
+
151
+ // Print summary
152
+ console.log('='.repeat(50));
153
+ console.log('Migration Summary:');
154
+ console.log(` ✓ Migrated: ${results.migrated.length}`);
155
+ console.log(` ⊘ Skipped: ${results.skipped.length}`);
156
+ console.log(` ✗ Errors: ${results.errors.length}`);
157
+ console.log('='.repeat(50));
158
+
159
+ if (dryRun) {
160
+ console.log('\n⚠️ This was a dry run. No files were actually moved.');
161
+ console.log(' Run without --dry-run to apply changes.\n');
162
+ } else if (results.migrated.length > 0) {
163
+ console.log('\n✨ Migration complete! Your content is now in folder-based structure.\n');
164
+ }
165
+
166
+ return results;
167
+ }
168
+
169
+ /**
170
+ * Migration command handler for CLI
171
+ */
172
+ export async function migrateCommand(options = {}) {
173
+ await migrateContent({
174
+ dryRun: options.dryRun === true,
175
+ rootDir: process.cwd()
176
+ });
177
+ }
178
+
179
+ export default { migrateContent, migrateCommand };
180
+
package/lib/new.js CHANGED
@@ -51,14 +51,18 @@ function formatTags(tags) {
51
51
  function createPost(config, options) {
52
52
  const slug = slugify(options.title);
53
53
  const date = getDateString();
54
- const filename = `${date}-${slug}.md`;
54
+ const folderName = `${date}-${slug}`;
55
55
  const postsDir = join(config.inputDir, config.collections.posts?.path || 'posts');
56
- const filePath = join(postsDir, filename);
56
+ const postFolder = join(postsDir, folderName);
57
+ const filePath = join(postFolder, 'index.md');
57
58
 
58
- // Ensure directory exists
59
+ // Ensure directories exist
59
60
  if (!existsSync(postsDir)) {
60
61
  mkdirSync(postsDir, { recursive: true });
61
62
  }
63
+ if (!existsSync(postFolder)) {
64
+ mkdirSync(postFolder, { recursive: true });
65
+ }
62
66
 
63
67
  let frontMatter = `---
64
68
  title: "${options.title}"
@@ -87,14 +91,18 @@ ${options.content || 'Write your post content here...'}
87
91
  */
88
92
  function createPage(config, options) {
89
93
  const slug = slugify(options.title);
90
- const filename = `${slug}.md`;
94
+ const folderName = slug;
91
95
  const pagesDir = join(config.inputDir, config.collections.pages?.path || 'pages');
92
- const filePath = join(pagesDir, filename);
96
+ const pageFolder = join(pagesDir, folderName);
97
+ const filePath = join(pageFolder, 'index.md');
93
98
 
94
- // Ensure directory exists
99
+ // Ensure directories exist
95
100
  if (!existsSync(pagesDir)) {
96
101
  mkdirSync(pagesDir, { recursive: true });
97
102
  }
103
+ if (!existsSync(pageFolder)) {
104
+ mkdirSync(pageFolder, { recursive: true });
105
+ }
98
106
 
99
107
  let frontMatter = `---
100
108
  title: "${options.title}"
@@ -120,14 +128,18 @@ function createNote(config, options) {
120
128
  const slug = options.title ? slugify(options.title) : 'note';
121
129
  const date = getDateString();
122
130
  const timestamp = Date.now();
123
- const filename = `${date}-${slug}-${timestamp}.md`;
131
+ const folderName = `${date}-${slug}-${timestamp}`;
124
132
  const notesDir = join(config.inputDir, config.collections.notes?.path || 'notes');
125
- const filePath = join(notesDir, filename);
133
+ const noteFolder = join(notesDir, folderName);
134
+ const filePath = join(noteFolder, 'index.md');
126
135
 
127
- // Ensure directory exists
136
+ // Ensure directories exist
128
137
  if (!existsSync(notesDir)) {
129
138
  mkdirSync(notesDir, { recursive: true });
130
139
  }
140
+ if (!existsSync(noteFolder)) {
141
+ mkdirSync(noteFolder, { recursive: true });
142
+ }
131
143
 
132
144
  const content = `---
133
145
  date: ${getISODate()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrymooreii/sia",
3
- "version": "2.1.13",
3
+ "version": "2.2.0",
4
4
  "description": "A simple, powerful static site generator with markdown, front matter, and Nunjucks templates",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -66,7 +66,7 @@ The output will be in the `dist/` folder, ready to deploy to any static hosting.
66
66
  npx sia new post "My Post Title"
67
67
  ```
68
68
 
69
- Creates a new markdown file in `src/posts/` with front matter template.
69
+ Creates a new folder `src/posts/YYYY-MM-DD-my-post-title/` with an `index.md` file inside. You can organize assets (images, PDFs, etc.) in the same folder.
70
70
 
71
71
  ### New Page
72
72
 
@@ -74,7 +74,7 @@ Creates a new markdown file in `src/posts/` with front matter template.
74
74
  npx sia new page "About Me"
75
75
  ```
76
76
 
77
- Creates a new page in `src/pages/`.
77
+ Creates a new folder `src/pages/about-me/` with an `index.md` file inside. You can organize assets in the same folder.
78
78
 
79
79
  ### New Note
80
80
 
@@ -82,7 +82,7 @@ Creates a new page in `src/pages/`.
82
82
  npx sia new note "Quick thought"
83
83
  ```
84
84
 
85
- Creates a short note in `src/notes/`.
85
+ Creates a new folder `src/notes/YYYY-MM-DD-quick-thought-TIMESTAMP/` with an `index.md` file inside. You can organize assets in the same folder.
86
86
 
87
87
  ## Project Structure
88
88
 
@@ -91,8 +91,17 @@ my-site/
91
91
  ├── _config.yml # Site configuration
92
92
  ├── src/
93
93
  │ ├── posts/ # Blog posts (markdown)
94
+ │ │ └── 2024-12-17-my-post/
95
+ │ │ ├── index.md
96
+ │ │ └── (assets can go here)
94
97
  │ ├── pages/ # Static pages
98
+ │ │ └── about/
99
+ │ │ ├── index.md
100
+ │ │ └── (assets can go here)
95
101
  │ ├── notes/ # Short notes/tweets
102
+ │ │ └── 2024-12-17-note-1234567890/
103
+ │ │ ├── index.md
104
+ │ │ └── (assets can go here)
96
105
  │ └── images/ # Images
97
106
  ├── assets/ # Static assets (optional)
98
107
  ├── static/ # Static assets (optional)
@@ -104,6 +113,8 @@ my-site/
104
113
  └── dist/ # Generated output
105
114
  ```
106
115
 
116
+ 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.
117
+
107
118
  ## Configuration
108
119
 
109
120
  Edit `_config.yml` to customize your site: