@terrymooreii/sia 1.0.1 → 2.0.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/_config.yml +33 -0
- package/bin/cli.js +51 -0
- package/defaults/includes/footer.njk +14 -0
- package/defaults/includes/header.njk +71 -0
- package/defaults/includes/pagination.njk +26 -0
- package/defaults/includes/tag-list.njk +11 -0
- package/defaults/layouts/base.njk +41 -0
- package/defaults/layouts/note.njk +25 -0
- package/defaults/layouts/page.njk +14 -0
- package/defaults/layouts/post.njk +43 -0
- package/defaults/pages/blog.njk +36 -0
- package/defaults/pages/feed.njk +28 -0
- package/defaults/pages/index.njk +60 -0
- package/defaults/pages/notes.njk +34 -0
- package/defaults/pages/tag.njk +41 -0
- package/defaults/pages/tags.njk +39 -0
- package/defaults/styles/main.css +1074 -0
- package/lib/assets.js +234 -0
- package/lib/build.js +260 -19
- package/lib/collections.js +191 -0
- package/lib/config.js +114 -0
- package/lib/content.js +323 -0
- package/lib/index.js +53 -18
- package/lib/init.js +555 -6
- package/lib/new.js +379 -41
- package/lib/server.js +257 -0
- package/lib/templates.js +249 -0
- package/package.json +30 -15
- package/readme.md +216 -52
- package/src/images/.gitkeep +3 -0
- package/src/notes/2024-12-17-first-note.md +6 -0
- package/src/pages/about.md +29 -0
- package/src/posts/2024-12-16-markdown-features.md +76 -0
- package/src/posts/2024-12-17-welcome-to-sia.md +78 -0
- package/src/posts/2024-12-17-welcome-to-static-forge.md +78 -0
- package/.github/workflows/main.yml +0 -33
- package/.prettierignore +0 -3
- package/.prettierrc +0 -8
- package/lib/helpers.js +0 -37
- package/lib/markdown.js +0 -33
- package/lib/parse.js +0 -94
- package/lib/readconfig.js +0 -16
- package/lib/rss.js +0 -63
- package/templates/siarc-template.js +0 -53
- package/templates/src/_partials/_footer.njk +0 -1
- package/templates/src/_partials/_head.njk +0 -35
- package/templates/src/_partials/_header.njk +0 -1
- package/templates/src/_partials/_layout.njk +0 -12
- package/templates/src/_partials/_nav.njk +0 -12
- package/templates/src/_partials/page.njk +0 -5
- package/templates/src/_partials/post.njk +0 -13
- package/templates/src/_partials/posts.njk +0 -19
- package/templates/src/assets/android-chrome-192x192.png +0 -0
- package/templates/src/assets/android-chrome-512x512.png +0 -0
- package/templates/src/assets/apple-touch-icon.png +0 -0
- package/templates/src/assets/favicon-16x16.png +0 -0
- package/templates/src/assets/favicon-32x32.png +0 -0
- package/templates/src/assets/favicon.ico +0 -0
- package/templates/src/assets/site.webmanifest +0 -19
- package/templates/src/content/index.md +0 -7
- package/templates/src/css/markdown.css +0 -1210
- package/templates/src/css/theme.css +0 -120
package/lib/assets.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readdirSync,
|
|
3
|
+
statSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
unlinkSync,
|
|
10
|
+
rmdirSync
|
|
11
|
+
} from 'fs';
|
|
12
|
+
import { join, dirname, relative, extname } from 'path';
|
|
13
|
+
|
|
14
|
+
// Supported asset extensions
|
|
15
|
+
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.avif'];
|
|
16
|
+
const STATIC_EXTENSIONS = ['.css', '.js', '.json', '.xml', '.txt', '.pdf', '.woff', '.woff2', '.ttf', '.eot'];
|
|
17
|
+
const ALL_ASSET_EXTENSIONS = [...IMAGE_EXTENSIONS, ...STATIC_EXTENSIONS];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a file is an asset
|
|
21
|
+
*/
|
|
22
|
+
export function isAsset(filePath) {
|
|
23
|
+
const ext = extname(filePath).toLowerCase();
|
|
24
|
+
return ALL_ASSET_EXTENSIONS.includes(ext);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a file is an image
|
|
29
|
+
*/
|
|
30
|
+
export function isImage(filePath) {
|
|
31
|
+
const ext = extname(filePath).toLowerCase();
|
|
32
|
+
return IMAGE_EXTENSIONS.includes(ext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively get all files in a directory
|
|
37
|
+
*/
|
|
38
|
+
function getAllFiles(dir, fileList = []) {
|
|
39
|
+
if (!existsSync(dir)) {
|
|
40
|
+
return fileList;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const items = readdirSync(dir);
|
|
44
|
+
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
const fullPath = join(dir, item);
|
|
47
|
+
const stat = statSync(fullPath);
|
|
48
|
+
|
|
49
|
+
if (stat.isDirectory()) {
|
|
50
|
+
getAllFiles(fullPath, fileList);
|
|
51
|
+
} else {
|
|
52
|
+
fileList.push(fullPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return fileList;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ensure a directory exists
|
|
61
|
+
*/
|
|
62
|
+
export function ensureDir(dirPath) {
|
|
63
|
+
if (!existsSync(dirPath)) {
|
|
64
|
+
mkdirSync(dirPath, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Copy a single file
|
|
70
|
+
*/
|
|
71
|
+
export function copyFile(src, dest) {
|
|
72
|
+
ensureDir(dirname(dest));
|
|
73
|
+
copyFileSync(src, dest);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Copy all assets from source to destination
|
|
78
|
+
*/
|
|
79
|
+
export function copyAssets(srcDir, destDir) {
|
|
80
|
+
const copied = [];
|
|
81
|
+
|
|
82
|
+
if (!existsSync(srcDir)) {
|
|
83
|
+
return copied;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const files = getAllFiles(srcDir);
|
|
87
|
+
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
if (isAsset(file)) {
|
|
90
|
+
const relativePath = relative(srcDir, file);
|
|
91
|
+
const destPath = join(destDir, relativePath);
|
|
92
|
+
|
|
93
|
+
copyFile(file, destPath);
|
|
94
|
+
copied.push(relativePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return copied;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Copy images from the images folder
|
|
103
|
+
*/
|
|
104
|
+
export function copyImages(config) {
|
|
105
|
+
const imagesDir = join(config.inputDir, 'images');
|
|
106
|
+
const outputImagesDir = join(config.outputDir, 'images');
|
|
107
|
+
|
|
108
|
+
const copied = copyAssets(imagesDir, outputImagesDir);
|
|
109
|
+
|
|
110
|
+
if (copied.length > 0) {
|
|
111
|
+
console.log(`🖼️ Copied ${copied.length} images`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return copied;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if directory has CSS files
|
|
119
|
+
*/
|
|
120
|
+
function hasCssFiles(dir) {
|
|
121
|
+
if (!existsSync(dir)) return false;
|
|
122
|
+
|
|
123
|
+
const files = getAllFiles(dir);
|
|
124
|
+
return files.some(f => extname(f).toLowerCase() === '.css');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Copy default styles to output
|
|
129
|
+
*/
|
|
130
|
+
export function copyDefaultStyles(config, defaultsDir) {
|
|
131
|
+
const defaultStylesDir = join(defaultsDir, 'styles');
|
|
132
|
+
const outputStylesDir = join(config.outputDir, 'styles');
|
|
133
|
+
|
|
134
|
+
// Check if user has custom styles (must actually have CSS files)
|
|
135
|
+
const userStylesDir = join(config.rootDir, 'styles');
|
|
136
|
+
|
|
137
|
+
if (hasCssFiles(userStylesDir)) {
|
|
138
|
+
// Copy user styles
|
|
139
|
+
const copied = copyAssets(userStylesDir, outputStylesDir);
|
|
140
|
+
console.log(`🎨 Copied ${copied.length} custom style files`);
|
|
141
|
+
return copied;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Copy default styles from the package
|
|
145
|
+
if (existsSync(defaultStylesDir)) {
|
|
146
|
+
const copied = copyAssets(defaultStylesDir, outputStylesDir);
|
|
147
|
+
console.log(`🎨 Copied ${copied.length} default style files`);
|
|
148
|
+
return copied;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('⚠️ No styles found');
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Copy all static assets (non-image files from root)
|
|
157
|
+
*/
|
|
158
|
+
export function copyStaticAssets(config) {
|
|
159
|
+
const staticDirs = ['assets', 'static', 'public'];
|
|
160
|
+
let totalCopied = 0;
|
|
161
|
+
|
|
162
|
+
for (const dirName of staticDirs) {
|
|
163
|
+
const srcDir = join(config.rootDir, dirName);
|
|
164
|
+
|
|
165
|
+
if (existsSync(srcDir)) {
|
|
166
|
+
const destDir = join(config.outputDir, dirName);
|
|
167
|
+
const copied = copyAssets(srcDir, destDir);
|
|
168
|
+
totalCopied += copied.length;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Also copy favicon if it exists in root
|
|
173
|
+
const faviconSrc = join(config.rootDir, 'favicon.ico');
|
|
174
|
+
if (existsSync(faviconSrc)) {
|
|
175
|
+
copyFile(faviconSrc, join(config.outputDir, 'favicon.ico'));
|
|
176
|
+
totalCopied++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (totalCopied > 0) {
|
|
180
|
+
console.log(`📁 Copied ${totalCopied} static assets`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return totalCopied;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Write a file with directory creation
|
|
188
|
+
*/
|
|
189
|
+
export function writeFile(filePath, content) {
|
|
190
|
+
ensureDir(dirname(filePath));
|
|
191
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clean a directory (remove all contents)
|
|
196
|
+
*/
|
|
197
|
+
export function cleanDir(dirPath) {
|
|
198
|
+
if (!existsSync(dirPath)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const items = readdirSync(dirPath);
|
|
203
|
+
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
const fullPath = join(dirPath, item);
|
|
206
|
+
const stat = statSync(fullPath);
|
|
207
|
+
|
|
208
|
+
if (stat.isDirectory()) {
|
|
209
|
+
cleanDir(fullPath);
|
|
210
|
+
// Remove empty directory
|
|
211
|
+
try {
|
|
212
|
+
rmdirSync(fullPath);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// Ignore if directory not empty
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
unlinkSync(fullPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export default {
|
|
223
|
+
isAsset,
|
|
224
|
+
isImage,
|
|
225
|
+
ensureDir,
|
|
226
|
+
copyFile,
|
|
227
|
+
copyAssets,
|
|
228
|
+
copyImages,
|
|
229
|
+
copyDefaultStyles,
|
|
230
|
+
copyStaticAssets,
|
|
231
|
+
writeFile,
|
|
232
|
+
cleanDir
|
|
233
|
+
};
|
|
234
|
+
|
package/lib/build.js
CHANGED
|
@@ -1,20 +1,261 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { buildSiteData, paginate, getPaginationUrls } from './collections.js';
|
|
6
|
+
import { createTemplateEngine, renderTemplate } from './templates.js';
|
|
7
|
+
import { copyImages, copyDefaultStyles, copyStaticAssets, writeFile, ensureDir } from './assets.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const defaultsDir = join(__dirname, '..', 'defaults');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Clean the output directory
|
|
15
|
+
*/
|
|
16
|
+
function cleanOutput(config) {
|
|
17
|
+
if (existsSync(config.outputDir)) {
|
|
18
|
+
rmSync(config.outputDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
mkdirSync(config.outputDir, { recursive: true });
|
|
21
|
+
console.log('🧹 Cleaned output directory');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render and write a single content item
|
|
26
|
+
*/
|
|
27
|
+
function renderContentItem(env, item, siteData) {
|
|
28
|
+
const templateName = `${item.layout}.njk`;
|
|
29
|
+
|
|
30
|
+
const html = renderTemplate(env, templateName, {
|
|
31
|
+
...siteData,
|
|
32
|
+
page: item,
|
|
33
|
+
content: item.content,
|
|
34
|
+
title: item.title
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
writeFile(item.outputPath, html);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render paginated listing pages
|
|
42
|
+
*/
|
|
43
|
+
function renderPaginatedPages(env, siteData, baseUrl, outputBase, templateName, extraData = {}) {
|
|
44
|
+
const { paginatedCollections } = siteData;
|
|
45
|
+
|
|
46
|
+
// Get all posts for the main blog listing
|
|
47
|
+
const posts = siteData.collections.posts || [];
|
|
48
|
+
const pages = paginate(posts, siteData.config.pagination.size);
|
|
49
|
+
|
|
50
|
+
for (const page of pages) {
|
|
51
|
+
const pagination = getPaginationUrls(baseUrl, page);
|
|
52
|
+
|
|
53
|
+
const html = renderTemplate(env, templateName, {
|
|
54
|
+
...siteData,
|
|
55
|
+
pagination,
|
|
56
|
+
posts: page.items,
|
|
57
|
+
...extraData
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const outputPath = page.pageNumber === 1
|
|
61
|
+
? join(outputBase, 'index.html')
|
|
62
|
+
: join(outputBase, 'page', String(page.pageNumber), 'index.html');
|
|
63
|
+
|
|
64
|
+
writeFile(outputPath, html);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return pages.length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render tag pages
|
|
72
|
+
*/
|
|
73
|
+
function renderTagPages(env, siteData) {
|
|
74
|
+
const { tags, allTags, config } = siteData;
|
|
75
|
+
|
|
76
|
+
// Render main tags listing page
|
|
77
|
+
const tagsHtml = renderTemplate(env, 'tags.njk', {
|
|
78
|
+
...siteData,
|
|
79
|
+
title: 'Tags'
|
|
80
|
+
});
|
|
81
|
+
writeFile(join(config.outputDir, 'tags', 'index.html'), tagsHtml);
|
|
82
|
+
|
|
83
|
+
// Render individual tag pages with pagination
|
|
84
|
+
for (const [tagSlug, tagData] of Object.entries(tags)) {
|
|
85
|
+
const tagPages = paginate(tagData.items, config.pagination.size);
|
|
86
|
+
const baseUrl = `/tags/${tagSlug}/`;
|
|
87
|
+
|
|
88
|
+
for (const page of tagPages) {
|
|
89
|
+
const pagination = getPaginationUrls(baseUrl, page);
|
|
90
|
+
|
|
91
|
+
const html = renderTemplate(env, 'tag.njk', {
|
|
92
|
+
...siteData,
|
|
93
|
+
tag: tagData,
|
|
94
|
+
pagination,
|
|
95
|
+
posts: page.items,
|
|
96
|
+
title: `Tagged: ${tagData.name}`
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const outputPath = page.pageNumber === 1
|
|
100
|
+
? join(config.outputDir, 'tags', tagSlug, 'index.html')
|
|
101
|
+
: join(config.outputDir, 'tags', tagSlug, 'page', String(page.pageNumber), 'index.html');
|
|
102
|
+
|
|
103
|
+
writeFile(outputPath, html);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`🏷️ Generated ${Object.keys(tags).length} tag pages`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Render the homepage
|
|
112
|
+
*/
|
|
113
|
+
function renderHomepage(env, siteData) {
|
|
114
|
+
const html = renderTemplate(env, 'index.njk', {
|
|
115
|
+
...siteData,
|
|
116
|
+
title: siteData.site.title
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
writeFile(join(siteData.config.outputDir, 'index.html'), html);
|
|
120
|
+
console.log('🏠 Generated homepage');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render the blog listing with pagination
|
|
125
|
+
*/
|
|
126
|
+
function renderBlogListing(env, siteData) {
|
|
127
|
+
const { config } = siteData;
|
|
128
|
+
const blogDir = join(config.outputDir, 'blog');
|
|
129
|
+
|
|
130
|
+
const pageCount = renderPaginatedPages(
|
|
131
|
+
env,
|
|
132
|
+
siteData,
|
|
133
|
+
'/blog/',
|
|
134
|
+
blogDir,
|
|
135
|
+
'blog.njk'
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
console.log(`📝 Generated ${pageCount} blog listing pages`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Render notes listing
|
|
143
|
+
*/
|
|
144
|
+
function renderNotesListing(env, siteData) {
|
|
145
|
+
const { config, collections } = siteData;
|
|
146
|
+
const notes = collections.notes || [];
|
|
147
|
+
|
|
148
|
+
if (notes.length === 0) return;
|
|
149
|
+
|
|
150
|
+
const notesDir = join(config.outputDir, 'notes');
|
|
151
|
+
const pages = paginate(notes, config.pagination.size);
|
|
152
|
+
|
|
153
|
+
for (const page of pages) {
|
|
154
|
+
const pagination = getPaginationUrls('/notes/', page);
|
|
155
|
+
|
|
156
|
+
const html = renderTemplate(env, 'notes.njk', {
|
|
157
|
+
...siteData,
|
|
158
|
+
pagination,
|
|
159
|
+
notes: page.items,
|
|
160
|
+
title: 'Notes'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const outputPath = page.pageNumber === 1
|
|
164
|
+
? join(notesDir, 'index.html')
|
|
165
|
+
: join(notesDir, 'page', String(page.pageNumber), 'index.html');
|
|
166
|
+
|
|
167
|
+
writeFile(outputPath, html);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`📋 Generated ${pages.length} notes listing pages`);
|
|
20
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate RSS feed
|
|
175
|
+
*/
|
|
176
|
+
function renderRSSFeed(env, siteData) {
|
|
177
|
+
const { config, collections } = siteData;
|
|
178
|
+
const posts = collections.posts || [];
|
|
179
|
+
|
|
180
|
+
const rss = renderTemplate(env, 'feed.njk', {
|
|
181
|
+
...siteData,
|
|
182
|
+
posts,
|
|
183
|
+
buildDate: new Date().toUTCString()
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
writeFile(join(config.outputDir, 'feed.xml'), rss);
|
|
187
|
+
console.log('📡 Generated RSS feed');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Main build function
|
|
192
|
+
*/
|
|
193
|
+
export async function build(options = {}) {
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
|
|
196
|
+
console.log('\n⚡ Sia - Building site...\n');
|
|
197
|
+
|
|
198
|
+
// Load configuration
|
|
199
|
+
const config = loadConfig(options.rootDir || process.cwd());
|
|
200
|
+
|
|
201
|
+
// Clean output directory if requested
|
|
202
|
+
if (options.clean !== false) {
|
|
203
|
+
cleanOutput(config);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build site data (collections, tags, etc.)
|
|
207
|
+
const siteData = buildSiteData(config);
|
|
208
|
+
|
|
209
|
+
// Create template engine
|
|
210
|
+
const env = createTemplateEngine(config);
|
|
211
|
+
|
|
212
|
+
// Render all content items
|
|
213
|
+
let itemCount = 0;
|
|
214
|
+
|
|
215
|
+
for (const [collectionName, items] of Object.entries(siteData.collections)) {
|
|
216
|
+
for (const item of items) {
|
|
217
|
+
renderContentItem(env, item, siteData);
|
|
218
|
+
itemCount++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`📄 Generated ${itemCount} content pages`);
|
|
223
|
+
|
|
224
|
+
// Render listing pages
|
|
225
|
+
renderHomepage(env, siteData);
|
|
226
|
+
renderBlogListing(env, siteData);
|
|
227
|
+
renderNotesListing(env, siteData);
|
|
228
|
+
renderTagPages(env, siteData);
|
|
229
|
+
|
|
230
|
+
// Generate RSS feed
|
|
231
|
+
renderRSSFeed(env, siteData);
|
|
232
|
+
|
|
233
|
+
// Copy assets
|
|
234
|
+
copyImages(config);
|
|
235
|
+
copyDefaultStyles(config, defaultsDir);
|
|
236
|
+
copyStaticAssets(config);
|
|
237
|
+
|
|
238
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
239
|
+
console.log(`\n✅ Build complete in ${duration}s`);
|
|
240
|
+
console.log(`📁 Output: ${config.outputDir}\n`);
|
|
241
|
+
|
|
242
|
+
return { config, siteData };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build command handler for CLI
|
|
247
|
+
*/
|
|
248
|
+
export async function buildCommand(options) {
|
|
249
|
+
try {
|
|
250
|
+
await build({
|
|
251
|
+
clean: options.clean !== false,
|
|
252
|
+
rootDir: process.cwd()
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error('❌ Build failed:', err.message);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default { build, buildCommand };
|
|
261
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { loadAllCollections } from './content.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build tag collections from all content
|
|
5
|
+
*/
|
|
6
|
+
export function buildTagCollections(collections) {
|
|
7
|
+
const tags = {};
|
|
8
|
+
|
|
9
|
+
// Iterate through all collections and gather tags
|
|
10
|
+
for (const [collectionName, items] of Object.entries(collections)) {
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
if (item.tags && Array.isArray(item.tags)) {
|
|
13
|
+
for (const tag of item.tags) {
|
|
14
|
+
const normalizedTag = tag.toLowerCase().trim();
|
|
15
|
+
|
|
16
|
+
if (!tags[normalizedTag]) {
|
|
17
|
+
tags[normalizedTag] = {
|
|
18
|
+
name: tag,
|
|
19
|
+
slug: normalizedTag.replace(/\s+/g, '-'),
|
|
20
|
+
items: [],
|
|
21
|
+
count: 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
tags[normalizedTag].items.push({
|
|
26
|
+
...item,
|
|
27
|
+
collection: collectionName
|
|
28
|
+
});
|
|
29
|
+
tags[normalizedTag].count++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sort items within each tag by date (newest first)
|
|
36
|
+
for (const tag of Object.values(tags)) {
|
|
37
|
+
tag.items.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return tags;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get all unique tags sorted by count
|
|
45
|
+
*/
|
|
46
|
+
export function getAllTags(tagCollections) {
|
|
47
|
+
return Object.values(tagCollections)
|
|
48
|
+
.sort((a, b) => b.count - a.count);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Paginate an array of items
|
|
53
|
+
*/
|
|
54
|
+
export function paginate(items, pageSize = 10) {
|
|
55
|
+
const pages = [];
|
|
56
|
+
const totalPages = Math.ceil(items.length / pageSize);
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < totalPages; i++) {
|
|
59
|
+
const start = i * pageSize;
|
|
60
|
+
const end = start + pageSize;
|
|
61
|
+
const pageItems = items.slice(start, end);
|
|
62
|
+
|
|
63
|
+
pages.push({
|
|
64
|
+
items: pageItems,
|
|
65
|
+
pageNumber: i + 1,
|
|
66
|
+
totalPages,
|
|
67
|
+
totalItems: items.length,
|
|
68
|
+
isFirst: i === 0,
|
|
69
|
+
isLast: i === totalPages - 1,
|
|
70
|
+
previousPage: i > 0 ? i : null,
|
|
71
|
+
nextPage: i < totalPages - 1 ? i + 2 : null,
|
|
72
|
+
startIndex: start,
|
|
73
|
+
endIndex: Math.min(end, items.length)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return pages;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate pagination URLs
|
|
82
|
+
*/
|
|
83
|
+
export function getPaginationUrls(baseUrl, pagination) {
|
|
84
|
+
return {
|
|
85
|
+
...pagination,
|
|
86
|
+
url: pagination.pageNumber === 1
|
|
87
|
+
? baseUrl
|
|
88
|
+
: `${baseUrl}page/${pagination.pageNumber}/`,
|
|
89
|
+
previousUrl: pagination.previousPage
|
|
90
|
+
? (pagination.previousPage === 1 ? baseUrl : `${baseUrl}page/${pagination.previousPage}/`)
|
|
91
|
+
: null,
|
|
92
|
+
nextUrl: pagination.nextPage
|
|
93
|
+
? `${baseUrl}page/${pagination.nextPage}/`
|
|
94
|
+
: null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the complete site data structure
|
|
100
|
+
*/
|
|
101
|
+
export function buildSiteData(config) {
|
|
102
|
+
// Load all content collections
|
|
103
|
+
const collections = loadAllCollections(config);
|
|
104
|
+
|
|
105
|
+
// Build tag collections
|
|
106
|
+
const tagCollections = buildTagCollections(collections);
|
|
107
|
+
const allTags = getAllTags(tagCollections);
|
|
108
|
+
|
|
109
|
+
console.log(`🏷️ Found ${allTags.length} unique tags`);
|
|
110
|
+
|
|
111
|
+
// Create paginated collections for listings
|
|
112
|
+
const paginatedCollections = {};
|
|
113
|
+
|
|
114
|
+
for (const [name, items] of Object.entries(collections)) {
|
|
115
|
+
paginatedCollections[name] = paginate(items, config.pagination.size);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create paginated tag pages
|
|
119
|
+
const paginatedTags = {};
|
|
120
|
+
|
|
121
|
+
for (const [tagSlug, tagData] of Object.entries(tagCollections)) {
|
|
122
|
+
paginatedTags[tagSlug] = paginate(tagData.items, config.pagination.size).map(page => ({
|
|
123
|
+
...page,
|
|
124
|
+
tag: tagData
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
config,
|
|
130
|
+
site: config.site,
|
|
131
|
+
collections,
|
|
132
|
+
paginatedCollections,
|
|
133
|
+
tags: tagCollections,
|
|
134
|
+
allTags,
|
|
135
|
+
paginatedTags
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get recent items across all collections
|
|
141
|
+
*/
|
|
142
|
+
export function getRecentItems(collections, limit = 10) {
|
|
143
|
+
const allItems = [];
|
|
144
|
+
|
|
145
|
+
for (const items of Object.values(collections)) {
|
|
146
|
+
allItems.push(...items);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return allItems
|
|
150
|
+
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
|
151
|
+
.slice(0, limit);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get related items based on shared tags
|
|
156
|
+
*/
|
|
157
|
+
export function getRelatedItems(item, collections, limit = 5) {
|
|
158
|
+
const related = [];
|
|
159
|
+
const itemTags = new Set(item.tags.map(t => t.toLowerCase()));
|
|
160
|
+
|
|
161
|
+
for (const items of Object.values(collections)) {
|
|
162
|
+
for (const candidate of items) {
|
|
163
|
+
if (candidate.slug === item.slug) continue;
|
|
164
|
+
|
|
165
|
+
const candidateTags = new Set(candidate.tags.map(t => t.toLowerCase()));
|
|
166
|
+
const sharedTags = [...itemTags].filter(t => candidateTags.has(t));
|
|
167
|
+
|
|
168
|
+
if (sharedTags.length > 0) {
|
|
169
|
+
related.push({
|
|
170
|
+
...candidate,
|
|
171
|
+
sharedTags: sharedTags.length
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return related
|
|
178
|
+
.sort((a, b) => b.sharedTags - a.sharedTags)
|
|
179
|
+
.slice(0, limit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default {
|
|
183
|
+
buildTagCollections,
|
|
184
|
+
getAllTags,
|
|
185
|
+
paginate,
|
|
186
|
+
getPaginationUrls,
|
|
187
|
+
buildSiteData,
|
|
188
|
+
getRecentItems,
|
|
189
|
+
getRelatedItems
|
|
190
|
+
};
|
|
191
|
+
|