@terrymooreii/sia 2.3.1 → 2.3.3

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 CHANGED
@@ -71,17 +71,26 @@ my-site/
71
71
  ├── _config.yml # Site configuration
72
72
  ├── src/
73
73
  │ ├── posts/ # Blog posts (markdown)
74
- │ │ └── 2024-12-17-my-post/
75
- │ │ ├── index.md
76
- │ │ └── (assets can go here)
74
+ │ │ ├── 2024-12-17-my-post/ # Flat structure (default)
75
+ │ │├── index.md
76
+ │ │└── (assets can go here)
77
+ │ │ └── 2024/ # Or date-organized (if path: posts/:year/:month)
78
+ │ │ └── 12/
79
+ │ │ └── 2024-12-17-my-post/
80
+ │ │ ├── index.md
81
+ │ │ └── (assets can go here)
77
82
  │ ├── pages/ # Static pages
78
83
  │ │ └── about/
79
84
  │ │ ├── index.md
80
85
  │ │ └── (assets can go here)
81
86
  │ ├── notes/ # Short notes/tweets
82
- │ │ └── 2024-12-17-note-1234567890/
83
- │ │ ├── index.md
84
- │ │ └── (assets can go here)
87
+ │ │ ├── 2024-12-17-note-1234567890/ # Flat structure (default)
88
+ │ │├── index.md
89
+ │ │└── (assets can go here)
90
+ │ │ └── 2024/ # Or date-organized (if path: notes/:year)
91
+ │ │ └── 2024-12-17-note-1234567890/
92
+ │ │ ├── index.md
93
+ │ │ └── (assets can go here)
85
94
  │ └── images/ # Images
86
95
  ├── assets/ # Static assets (optional)
87
96
  ├── static/ # Static assets (optional)
@@ -97,10 +106,39 @@ my-site/
97
106
 
98
107
  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
108
 
109
+ **Note:** You can organize posts and notes by date using date variables in the `path` configuration (e.g., `posts/:year/:month`). See [Date Variables in Paths](#date-variables-in-paths) for details.
110
+
100
111
  ## Configuration
101
112
 
102
113
  Edit `_config.yml` to customize your site:
103
114
 
115
+ ### Date Variables in Paths
116
+
117
+ You can organize posts and notes by date using date variables in the `path` property:
118
+
119
+ ```yaml
120
+ collections:
121
+ posts:
122
+ path: posts/:year/:month # Organizes posts by year and month
123
+ # New posts will be created in: posts/2024/01/2024-01-15-slug/
124
+
125
+ notes:
126
+ path: notes/:year # Organizes notes by year only
127
+ # New notes will be created in: notes/2024/2024-01-15-note-1234567890/
128
+ ```
129
+
130
+ **Supported date variables:**
131
+ - `:year` - 4-digit year (e.g., `2024`)
132
+ - `:month` - 2-digit month (e.g., `01`, `12`)
133
+ - `:day` - 2-digit day (e.g., `01`, `31`)
134
+
135
+ **Examples:**
136
+ - `posts/:year/:month` → `posts/2024/01/`
137
+ - `posts/:year` → `posts/2024/`
138
+ - `notes/:year/:month/:day` → `notes/2024/01/15/`
139
+
140
+ When loading collections, Sia automatically searches recursively through all date-organized directories, so existing content will be found regardless of the path structure.
141
+
104
142
  ```yaml
105
143
  site:
106
144
  title: "My Blog"
@@ -117,7 +155,7 @@ output: dist
117
155
 
118
156
  collections:
119
157
  posts:
120
- path: posts
158
+ path: posts # Or use date variables: posts/:year/:month
121
159
  layout: post
122
160
  permalink: /blog/:slug/
123
161
  sortBy: date
@@ -127,7 +165,7 @@ collections:
127
165
  layout: page
128
166
  permalink: /:slug/
129
167
  notes:
130
- path: notes
168
+ path: notes # Or use date variables: notes/:year
131
169
  layout: note
132
170
  permalink: /notes/:slug/
133
171
 
@@ -165,13 +165,32 @@ Blog posts are stored in `src/posts/` (or your configured path).
165
165
  # From _config.yml
166
166
  collections:
167
167
  posts:
168
- path: posts
168
+ path: posts # Or use date variables: posts/:year/:month
169
169
  layout: post
170
170
  permalink: /blog/:slug/
171
171
  sortBy: date
172
172
  sortOrder: desc
173
173
  ```
174
174
 
175
+ **Date Variables in Paths:**
176
+
177
+ You can organize posts by date using date variables in the `path` property:
178
+
179
+ ```yaml
180
+ collections:
181
+ posts:
182
+ path: posts/:year/:month # Creates: posts/2024/01/2024-01-15-slug/
183
+ # Or
184
+ path: posts/:year # Creates: posts/2024/2024-01-15-slug/
185
+ ```
186
+
187
+ **Supported variables:**
188
+ - `:year` - 4-digit year (e.g., `2024`)
189
+ - `:month` - 2-digit month (e.g., `01`, `12`)
190
+ - `:day` - 2-digit day (e.g., `01`, `31`)
191
+
192
+ When you create a new post, it will be automatically placed in the correct date-organized directory based on the current date.
193
+
175
194
  ### Typical Post Front Matter
176
195
 
177
196
  ```yaml
@@ -268,13 +287,32 @@ Notes are short-form content stored in `src/notes/` (or your configured path). T
268
287
  # From _config.yml
269
288
  collections:
270
289
  notes:
271
- path: notes
290
+ path: notes # Or use date variables: notes/:year
272
291
  layout: note
273
292
  permalink: /notes/:slug/
274
293
  sortBy: date
275
294
  sortOrder: desc
276
295
  ```
277
296
 
297
+ **Date Variables in Paths:**
298
+
299
+ You can organize notes by date using date variables in the `path` property:
300
+
301
+ ```yaml
302
+ collections:
303
+ notes:
304
+ path: notes/:year # Creates: notes/2024/2024-01-15-note-1234567890/
305
+ # Or
306
+ path: notes/:year/:month # Creates: notes/2024/01/2024-01-15-note-1234567890/
307
+ ```
308
+
309
+ **Supported variables:**
310
+ - `:year` - 4-digit year (e.g., `2024`)
311
+ - `:month` - 2-digit month (e.g., `01`, `12`)
312
+ - `:day` - 2-digit day (e.g., `01`, `31`)
313
+
314
+ When you create a new note, it will be automatically placed in the correct date-organized directory based on the current date.
315
+
278
316
  ### Typical Note Front Matter
279
317
 
280
318
  Notes often have minimal front matter since they're short-form:
package/lib/content.js CHANGED
@@ -321,6 +321,49 @@ function truncateMarkdownSafely(text, maxLength) {
321
321
  return text.substring(0, truncateAt).trim() + '...';
322
322
  }
323
323
 
324
+ /**
325
+ * Expand date variables in a path template
326
+ * Supports :year, :month, :day variables
327
+ * @param {string} pathTemplate - Path template with date variables (e.g., "posts/:year/:month")
328
+ * @param {Date} date - Date to use for expansion
329
+ * @returns {string} Expanded path
330
+ */
331
+ export function expandDatePath(pathTemplate, date) {
332
+ if (!pathTemplate || typeof pathTemplate !== 'string') {
333
+ return pathTemplate;
334
+ }
335
+
336
+ const year = date.getFullYear();
337
+ const month = String(date.getMonth() + 1).padStart(2, '0');
338
+ const day = String(date.getDate()).padStart(2, '0');
339
+
340
+ return pathTemplate
341
+ .replace(/:year/g, year)
342
+ .replace(/:month/g, month)
343
+ .replace(/:day/g, day);
344
+ }
345
+
346
+ /**
347
+ * Get base path from a path template (removes date variables for recursive searching)
348
+ * @param {string} pathTemplate - Path template with date variables
349
+ * @returns {string} Base path (everything before the first date variable)
350
+ */
351
+ export function getBasePath(pathTemplate) {
352
+ if (!pathTemplate || typeof pathTemplate !== 'string') {
353
+ return pathTemplate;
354
+ }
355
+
356
+ // Find the first occurrence of a date variable (can be at start or after a slash)
357
+ const firstVariable = pathTemplate.match(/:(\w+)/);
358
+ if (!firstVariable) {
359
+ // No date variables, return as-is
360
+ return pathTemplate;
361
+ }
362
+
363
+ // Return everything before the first date variable
364
+ return pathTemplate.substring(0, firstVariable.index);
365
+ }
366
+
324
367
  /**
325
368
  * Generate a URL-friendly slug from a string
326
369
  */
@@ -402,10 +445,66 @@ export function getDateFromFilename(filename) {
402
445
  return null;
403
446
  }
404
447
 
448
+ /**
449
+ * Convert relative image and link paths to absolute paths based on base URL
450
+ * @param {string} html - HTML content with potential relative paths
451
+ * @param {string} baseUrl - Base URL for the content item (e.g., '/blog/my-post/')
452
+ * @returns {string} HTML with absolute paths
453
+ */
454
+ function fixRelativePaths(html, baseUrl) {
455
+ if (!html || !baseUrl) return html;
456
+
457
+ // Normalize baseUrl to ensure it ends with a slash
458
+ const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
459
+
460
+ // Helper function to convert relative path to absolute
461
+ const makeAbsolute = (path) => {
462
+ // Skip if it's already an absolute URL (http://, https://) or absolute path (/)
463
+ if (/^(https?:|\/|#|mailto:)/.test(path)) {
464
+ return path;
465
+ }
466
+
467
+ // Convert relative path to absolute path
468
+ // Remove leading ./ if present
469
+ const cleanPath = path.replace(/^\.\//, '');
470
+
471
+ // Combine base URL with path
472
+ return normalizedBaseUrl + cleanPath;
473
+ };
474
+
475
+ // Fix relative image paths
476
+ html = html.replace(
477
+ /<img\s+([^>]*?)(?:src\s*=\s*(["'])([^"']+)\2)([^>]*)>/gi,
478
+ (match, beforeAttrs, quote, src, afterAttrs) => {
479
+ const absolutePath = makeAbsolute(src);
480
+
481
+ // Reconstruct the img tag with the absolute path
482
+ const before = beforeAttrs ? beforeAttrs.trim() + ' ' : '';
483
+ const after = afterAttrs ? ' ' + afterAttrs.trim() : '';
484
+ return `<img ${before}src=${quote}${absolutePath}${quote}${after}>`;
485
+ }
486
+ );
487
+
488
+ // Fix relative link paths (but skip anchor links and external URLs)
489
+ html = html.replace(
490
+ /<a\s+([^>]*?)(?:href\s*=\s*(["'])([^"']+)\2)([^>]*)>/gi,
491
+ (match, beforeAttrs, quote, href, afterAttrs) => {
492
+ const absolutePath = makeAbsolute(href);
493
+
494
+ // Reconstruct the link tag with the absolute path
495
+ const before = beforeAttrs ? beforeAttrs.trim() + ' ' : '';
496
+ const after = afterAttrs ? ' ' + afterAttrs.trim() : '';
497
+ return `<a ${before}href=${quote}${absolutePath}${quote}${after}>`;
498
+ }
499
+ );
500
+
501
+ return html;
502
+ }
503
+
405
504
  /**
406
505
  * Parse a markdown file with front matter
407
506
  */
408
- export async function parseContent(filePath) {
507
+ export async function parseContent(filePath, options = {}) {
409
508
  let content = readFileSync(filePath, 'utf-8');
410
509
 
411
510
  // Execute beforeParse hook
@@ -521,7 +620,11 @@ export async function loadCollection(config, collectionName) {
521
620
  return [];
522
621
  }
523
622
 
524
- const collectionDir = join(config.inputDir, collectionConfig.path);
623
+ // If path contains date variables, use base path for recursive searching
624
+ // Otherwise use the path as-is
625
+ const pathTemplate = collectionConfig.path;
626
+ const basePath = getBasePath(pathTemplate);
627
+ const collectionDir = join(config.inputDir, basePath);
525
628
  const files = getMarkdownFiles(collectionDir);
526
629
 
527
630
  const items = await Promise.all(
@@ -546,6 +649,11 @@ export async function loadCollection(config, collectionName) {
546
649
  item.url = basePath + permalink;
547
650
  item.outputPath = join(config.outputDir, permalink, 'index.html');
548
651
 
652
+ // Fix relative image and link paths in content and excerptHtml
653
+ // This ensures images and links work on both individual pages and list pages
654
+ item.content = fixRelativePaths(item.content, item.url);
655
+ item.excerptHtml = fixRelativePaths(item.excerptHtml, item.url);
656
+
549
657
  return item;
550
658
  } catch (err) {
551
659
  console.error(`Error parsing ${filePath}:`, err.message);
@@ -610,6 +718,8 @@ export default {
610
718
  getMarkdownFiles,
611
719
  loadCollection,
612
720
  loadAllCollections,
613
- addMarkedExtension
721
+ addMarkedExtension,
722
+ expandDatePath,
723
+ getBasePath
614
724
  };
615
725
 
package/lib/migrate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdirSync, statSync, existsSync, mkdirSync, renameSync } from 'fs';
2
2
  import { join, dirname, basename, extname } from 'path';
3
3
  import { loadConfig } from './config.js';
4
- import { getMarkdownFiles } from './content.js';
4
+ import { getMarkdownFiles, getBasePath } from './content.js';
5
5
 
6
6
  /**
7
7
  * Check if a file is already in folder-based structure
@@ -102,7 +102,10 @@ export async function migrateContent(options = {}) {
102
102
  continue;
103
103
  }
104
104
 
105
- const collectionDir = join(config.inputDir, collectionConfig.path);
105
+ // If path contains date variables, use base path for recursive searching
106
+ const pathTemplate = collectionConfig.path;
107
+ const basePath = getBasePath(pathTemplate);
108
+ const collectionDir = join(config.inputDir, basePath);
106
109
 
107
110
  if (!existsSync(collectionDir)) {
108
111
  console.log(`⚠️ Collection directory not found: ${collectionDir}`);
package/lib/new.js CHANGED
@@ -2,7 +2,7 @@ import prompts from 'prompts';
2
2
  import { writeFileSync, existsSync, mkdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { loadConfig } from './config.js';
5
- import { slugify } from './content.js';
5
+ import { slugify, expandDatePath } from './content.js';
6
6
 
7
7
  /**
8
8
  * Get current date in YYYY-MM-DD format
@@ -52,7 +52,10 @@ function createPost(config, options) {
52
52
  const slug = slugify(options.title);
53
53
  const date = getDateString();
54
54
  const folderName = `${date}-${slug}`;
55
- const postsDir = join(config.inputDir, config.collections.posts?.path || 'posts');
55
+ const now = new Date();
56
+ const pathTemplate = config.collections.posts?.path || 'posts';
57
+ const expandedPath = expandDatePath(pathTemplate, now);
58
+ const postsDir = join(config.inputDir, expandedPath);
56
59
  const postFolder = join(postsDir, folderName);
57
60
  const filePath = join(postFolder, 'index.md');
58
61
 
@@ -129,7 +132,10 @@ function createNote(config, options) {
129
132
  const date = getDateString();
130
133
  const timestamp = Date.now();
131
134
  const folderName = `${date}-${slug}-${timestamp}`;
132
- const notesDir = join(config.inputDir, config.collections.notes?.path || 'notes');
135
+ const now = new Date();
136
+ const pathTemplate = config.collections.notes?.path || 'notes';
137
+ const expandedPath = expandDatePath(pathTemplate, now);
138
+ const notesDir = join(config.inputDir, expandedPath);
133
139
  const noteFolder = join(notesDir, folderName);
134
140
  const filePath = join(noteFolder, 'index.md');
135
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrymooreii/sia",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
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": {