fossbook 0.2.14 → 0.2.16

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/fossbook.js CHANGED
@@ -24,6 +24,7 @@ Options:
24
24
  --clean Remove output directory before build (default: true)
25
25
  --github (init) Also create a GitHub repo and push
26
26
  --lang (new) Comma-separated translation languages, e.g. ko,ja
27
+ --include-drafts (build, serve) Include posts with draft: true
27
28
  -m, --message (deploy) Custom commit message
28
29
  --no-wait (deploy) Push without waiting for CI status
29
30
  --draft (deploy) Commit locally without pushing
@@ -59,72 +60,85 @@ if (hasFlag("-h", "--help") || !command) {
59
60
 
60
61
  const configPath = getOption("-c", "--config");
61
62
 
62
- switch (command) {
63
- case "build": {
64
- const config = loadConfig(configPath);
65
- const outputOverride = getOption("-o", "--output");
66
- if (outputOverride) config.dev.outdir = outputOverride;
67
-
68
- const { build } = require("../lib/index");
69
- build(config);
70
- break;
71
- }
72
-
73
- case "serve": {
74
- const config = loadConfig(configPath);
75
- const outputOverride = getOption("-o", "--output");
76
- if (outputOverride) config.dev.outdir = outputOverride;
77
- const port = getOption("-p", "--port") || 3000;
63
+ async function run() {
64
+ switch (command) {
65
+ case "build": {
66
+ const config = loadConfig(configPath);
67
+ const outputOverride = getOption("-o", "--output");
68
+ if (outputOverride) config.dev.outdir = outputOverride;
69
+ config.includeDrafts = hasFlag("--include-drafts");
70
+
71
+ const { build } = require("../lib/index");
72
+ await build(config);
73
+ break;
74
+ }
78
75
 
79
- const { build } = require("../lib/index");
80
- build(config);
76
+ case "serve": {
77
+ const config = loadConfig(configPath);
78
+ const outputOverride = getOption("-o", "--output");
79
+ if (outputOverride) config.dev.outdir = outputOverride;
80
+ config.includeDrafts = hasFlag("--include-drafts");
81
+ const port = getOption("-p", "--port") || 3000;
81
82
 
82
- const { serve } = require("../lib/server");
83
- serve(config, Number(port));
84
- break;
85
- }
83
+ const { build } = require("../lib/index");
84
+ await build(config);
86
85
 
87
- case "new": {
88
- const title = args[1];
89
- if (!title) {
90
- console.error('Error: Please provide a post title. Usage: fossbook new "My Post Title"');
91
- process.exit(1);
86
+ const { serve } = require("../lib/server");
87
+ serve(config, Number(port));
88
+ break;
92
89
  }
93
- if (hasFlag("--lang") && !getOption("--lang")) {
94
- console.error("Error: Please provide at least one language after --lang");
95
- process.exit(1);
90
+
91
+ case "new": {
92
+ const title = args[1];
93
+ if (!title) {
94
+ console.error(
95
+ 'Error: Please provide a post title. Usage: fossbook new "My Post Title"',
96
+ );
97
+ process.exit(1);
98
+ }
99
+ if (hasFlag("--lang") && !getOption("--lang")) {
100
+ console.error(
101
+ "Error: Please provide at least one language after --lang",
102
+ );
103
+ process.exit(1);
104
+ }
105
+ const config = loadConfig(configPath);
106
+ const { createPost } = require("../lib/new");
107
+ try {
108
+ createPost(config, title, getOption("--lang"));
109
+ } catch (error) {
110
+ console.error(`Error: ${error.message}`);
111
+ process.exit(1);
112
+ }
113
+ break;
96
114
  }
97
- const config = loadConfig(configPath);
98
- const { createPost } = require("../lib/new");
99
- try {
100
- createPost(config, title, getOption("--lang"));
101
- } catch (error) {
102
- console.error(`Error: ${error.message}`);
103
- process.exit(1);
115
+
116
+ case "init": {
117
+ const { initProject } = require("../lib/init");
118
+ const projectName = !args[1] || args[1].startsWith("--") ? null : args[1];
119
+ initProject({ github: hasFlag("--github"), name: projectName });
120
+ break;
104
121
  }
105
- break;
106
- }
107
122
 
108
- case "init": {
109
- const { initProject } = require("../lib/init");
110
- const projectName = !args[1] || args[1].startsWith("--") ? null : args[1];
111
- initProject({ github: hasFlag("--github"), name: projectName });
112
- break;
113
- }
123
+ case "deploy": {
124
+ const config = loadConfig(configPath);
125
+ const { deploy } = require("../lib/deploy");
126
+ await deploy(config, {
127
+ message: getOption("-m", "--message"),
128
+ noWait: hasFlag("--no-wait"),
129
+ draft: hasFlag("--draft"),
130
+ });
131
+ break;
132
+ }
114
133
 
115
- case "deploy": {
116
- const config = loadConfig(configPath);
117
- const { deploy } = require("../lib/deploy");
118
- deploy(config, {
119
- message: getOption("-m", "--message"),
120
- noWait: hasFlag("--no-wait"),
121
- draft: hasFlag("--draft"),
122
- });
123
- break;
134
+ default:
135
+ console.error(`Unknown command: ${command}`);
136
+ printHelp();
137
+ process.exit(1);
124
138
  }
125
-
126
- default:
127
- console.error(`Unknown command: ${command}`);
128
- printHelp();
129
- process.exit(1);
130
139
  }
140
+
141
+ run().catch((error) => {
142
+ console.error(error);
143
+ process.exitCode = 1;
144
+ });
package/lib/deploy.js CHANGED
@@ -14,7 +14,8 @@ async function deploy(config, options = {}) {
14
14
 
15
15
  // 1. Build the site
16
16
  console.log("Building site...");
17
- build(config);
17
+ config.includeDrafts = false;
18
+ await build(config);
18
19
  console.log("Build complete.\n");
19
20
 
20
21
  // 2. Check git is initialized
package/lib/index.js CHANGED
@@ -6,8 +6,13 @@ const HomePagination = require("./home");
6
6
  const AllPostsPage = require("./all_posts");
7
7
  const Posts = require("./posts");
8
8
  const TagPages = require("./tag");
9
+ const {
10
+ beginImageBuild,
11
+ finishImageBuild,
12
+ publishImage,
13
+ } = require("./mod/image");
9
14
 
10
- function copyDirectoryRecursive(src, dest) {
15
+ async function copyDirectoryRecursive(src, dest, config) {
11
16
  fs.mkdirSync(dest, { recursive: true });
12
17
  const entries = fs.readdirSync(src, { withFileTypes: true });
13
18
 
@@ -16,9 +21,11 @@ function copyDirectoryRecursive(src, dest) {
16
21
  const destPath = path.join(dest, entry.name);
17
22
 
18
23
  if (entry.isDirectory()) {
19
- copyDirectoryRecursive(srcPath, destPath);
24
+ await copyDirectoryRecursive(srcPath, destPath, config);
25
+ } else if (path.extname(entry.name).toLowerCase() === ".png") {
26
+ await publishImage(srcPath, destPath, config, undefined, false);
20
27
  } else {
21
- fs.copyFileSync(srcPath, destPath);
28
+ await fs.promises.copyFile(srcPath, destPath);
22
29
  }
23
30
  }
24
31
  }
@@ -35,10 +42,16 @@ function createPostTranslationIndex(languageConfigs) {
35
42
 
36
43
  const translations = [];
37
44
  for (const config of languageConfigs) {
38
- const sourceFile = config.language === config.defaultLanguage
39
- ? "index.md"
40
- : `index.${config.language}.md`;
41
- if (!fs.existsSync(path.join(postsDir, entry.name, sourceFile))) continue;
45
+ const sourceFile =
46
+ config.language === config.defaultLanguage
47
+ ? "index.md"
48
+ : `index.${config.language}.md`;
49
+ const sourcePath = path.join(postsDir, entry.name, sourceFile);
50
+ if (!fs.existsSync(sourcePath)) continue;
51
+
52
+ const post = new Page(config);
53
+ post.readSource(sourcePath, entry.name);
54
+ if (post.draft && !config.includeDrafts) continue;
42
55
 
43
56
  const prefix = config.postsPath ? `${config.postsPath}/` : "";
44
57
  translations.push({
@@ -60,18 +73,21 @@ function createAboutTranslations(languageConfigs) {
60
73
 
61
74
  const contentDir = languageConfigs[0].dev.content;
62
75
  return languageConfigs.flatMap((config) => {
63
- const sourceFile = config.language === config.defaultLanguage
64
- ? "about.md"
65
- : `about.${config.language}.md`;
76
+ const sourceFile =
77
+ config.language === config.defaultLanguage
78
+ ? "about.md"
79
+ : `about.${config.language}.md`;
66
80
  if (!fs.existsSync(path.join(contentDir, sourceFile))) return [];
67
81
 
68
- return [{
69
- language: config.language,
70
- languageName: config.languageName,
71
- path: `${config.basePath || "/"}about/`,
72
- url: `${config.blogsite}/about/`,
73
- isDefault: config.language === config.defaultLanguage,
74
- }];
82
+ return [
83
+ {
84
+ language: config.language,
85
+ languageName: config.languageName,
86
+ path: `${config.basePath || "/"}about/`,
87
+ url: `${config.blogsite}/about/`,
88
+ isDefault: config.language === config.defaultLanguage,
89
+ },
90
+ ];
75
91
  });
76
92
  }
77
93
 
@@ -105,16 +121,16 @@ function createTagListTranslations(languageConfigs) {
105
121
  }));
106
122
  }
107
123
 
108
- function buildLanguage(config) {
124
+ async function buildLanguage(config) {
109
125
  fs.mkdirSync(config.dev.outdir, { recursive: true });
110
126
 
111
127
  // Create post pages in output directory
112
128
  const posts = new Posts(config);
113
129
  const postObjects = posts.createPostObjects();
114
130
 
115
- postObjects.forEach((post) => {
116
- post.generateContent("post.html");
117
- });
131
+ for (const post of postObjects) {
132
+ await post.generateContent("post.html");
133
+ }
118
134
 
119
135
  // Create home page and pagination in output/page directory
120
136
  const homePagination = new HomePagination(config);
@@ -132,37 +148,47 @@ function buildLanguage(config) {
132
148
  tagPages.generateContent(postObjects);
133
149
 
134
150
  // Create about page in output/about directory
135
- const aboutFile = config.language === config.defaultLanguage
136
- ? "about.md"
137
- : `about.${config.language}.md`;
151
+ const aboutFile =
152
+ config.language === config.defaultLanguage
153
+ ? "about.md"
154
+ : `about.${config.language}.md`;
138
155
  const aboutPath = path.resolve(config.dev.content, aboutFile);
139
156
  if (fs.existsSync(aboutPath)) {
140
157
  const aboutPage = new Page(config);
141
158
  aboutPage.readSource(aboutPath, "about");
142
159
  aboutPage.translations = config.aboutTranslations || [];
143
- aboutPage.generateContent("page.html", "about");
160
+ await aboutPage.generateContent("page.html", "about");
144
161
  }
145
162
 
146
163
  // Copy static directory to output directory
147
164
  const staticImagesDir = path.join(config.dev.staticDir, "images");
148
165
  if (fs.existsSync(staticImagesDir)) {
149
- copyDirectoryRecursive(staticImagesDir, path.join(config.dev.outdir, "images"));
166
+ await copyDirectoryRecursive(
167
+ staticImagesDir,
168
+ path.join(config.dev.outdir, "images"),
169
+ config,
170
+ );
150
171
  }
151
172
 
152
173
  // Copy theme assets to output directory
153
174
  const themeAssetsDir = path.join(config.themePath, "assets");
154
175
  if (fs.existsSync(themeAssetsDir)) {
155
- copyDirectoryRecursive(themeAssetsDir, config.dev.outdir);
176
+ await copyDirectoryRecursive(themeAssetsDir, config.dev.outdir, config);
156
177
  }
157
178
  }
158
179
 
159
- function build(config) {
180
+ async function build(config) {
181
+ beginImageBuild();
182
+
160
183
  // Remove the output directory
161
184
  if (fs.existsSync(config.dev.outdir))
162
185
  fs.rmSync(config.dev.outdir, { recursive: true });
163
186
  fs.mkdirSync(config.dev.outdir, { recursive: true });
164
187
 
165
188
  const languageConfigs = config.languageConfigs || [config];
189
+ languageConfigs.forEach((languageConfig) => {
190
+ languageConfig.includeDrafts = config.includeDrafts === true;
191
+ });
166
192
  const postTranslationIndex = createPostTranslationIndex(languageConfigs);
167
193
  const aboutTranslations = createAboutTranslations(languageConfigs);
168
194
  const homeTranslations = createHomeTranslations(languageConfigs);
@@ -175,7 +201,10 @@ function build(config) {
175
201
  languageConfig.allPostsTranslations = allPostsTranslations;
176
202
  languageConfig.tagListTranslations = tagListTranslations;
177
203
  });
178
- languageConfigs.forEach(buildLanguage);
204
+ for (const languageConfig of languageConfigs) {
205
+ await buildLanguage(languageConfig);
206
+ }
207
+ await finishImageBuild(config);
179
208
 
180
209
  // Create CNAME file for GitHub Pages
181
210
  if (config.githubCNAME)
package/lib/init.js CHANGED
@@ -2,6 +2,12 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
  const readline = require("readline");
5
+ const { CACHE_SCHEMA_VERSION } = require("./mod/image");
6
+
7
+ const IMAGE_CACHE_SCHEMA = `v${CACHE_SCHEMA_VERSION}`;
8
+ const IMAGE_CACHE_IGNORE_RULE = ".fossbook-cache/";
9
+ const IMAGE_CACHE_SAVE_CONDITION =
10
+ "steps.image-cache-restore.outputs.cache-hit != 'true' && steps.image-cache-status.outputs.new-entries == 'true'";
5
11
 
6
12
  const DEPLOY_WORKFLOW = `name: Deploy to GitHub Pages
7
13
 
@@ -36,9 +42,41 @@ jobs:
36
42
  - name: Install dependencies
37
43
  run: npm ci
38
44
 
45
+ - name: Resolve Fossbook image cache
46
+ id: image-cache-config
47
+ run: node -e "require('fossbook/lib/mod/github_cache').writeGitHubCacheOutputs()"
48
+
49
+ - name: Restore Fossbook image cache
50
+ id: image-cache-restore
51
+ uses: actions/cache/restore@v4
52
+ continue-on-error: true
53
+ with:
54
+ path: \${{ steps.image-cache-config.outputs.cache-path }}/images
55
+ key: \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-\${{ github.run_id }}-\${{ github.run_attempt }}
56
+ restore-keys: |
57
+ \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-
58
+ \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-
59
+
39
60
  - name: Build site
40
61
  run: npx fossbook build
41
62
 
63
+ - name: Read Fossbook image cache status
64
+ id: image-cache-status
65
+ continue-on-error: true
66
+ shell: bash
67
+ env:
68
+ FOSSBOOK_CACHE_STATUS: \${{ steps.image-cache-config.outputs.status-path }}
69
+ run: |
70
+ node -e "const fs=require('fs');let writes=0;try{writes=JSON.parse(fs.readFileSync(process.env.FOSSBOOK_CACHE_STATUS,'utf8')).writes||0}catch(error){console.warn(error.message)}fs.appendFileSync(process.env.GITHUB_OUTPUT, 'new-entries='+(writes>0)+'\\n')"
71
+
72
+ - name: Save Fossbook image cache
73
+ if: \${{ ${IMAGE_CACHE_SAVE_CONDITION} }}
74
+ uses: actions/cache/save@v4
75
+ continue-on-error: true
76
+ with:
77
+ path: \${{ steps.image-cache-config.outputs.cache-path }}/images
78
+ key: \${{ runner.os }}-\${{ runner.arch }}-fossbook-images-${IMAGE_CACHE_SCHEMA}-\${{ steps.image-cache-config.outputs.input-hash }}-\${{ github.run_id }}-\${{ github.run_attempt }}
79
+
42
80
  - name: Setup Pages
43
81
  uses: actions/configure-pages@v4
44
82
  with:
@@ -70,10 +108,7 @@ function initProject(options = {}) {
70
108
  }
71
109
 
72
110
  // Create directories
73
- const dirs = [
74
- "content/posts",
75
- "static/images",
76
- ];
111
+ const dirs = ["content/posts", "static/images"];
77
112
 
78
113
  dirs.forEach((dir) => {
79
114
  const dirPath = path.join(cwd, dir);
@@ -95,8 +130,13 @@ function initProject(options = {}) {
95
130
  // Create .gitignore
96
131
  const gitignorePath = path.join(cwd, ".gitignore");
97
132
  if (!fs.existsSync(gitignorePath)) {
98
- fs.writeFileSync(gitignorePath, "node_modules/\npublic/\n");
133
+ fs.writeFileSync(
134
+ gitignorePath,
135
+ `node_modules/\npublic/\n${IMAGE_CACHE_IGNORE_RULE}\n`,
136
+ );
99
137
  console.log("Created: .gitignore");
138
+ } else if (ensureGitignoreRule(gitignorePath, IMAGE_CACHE_IGNORE_RULE)) {
139
+ console.log(`Updated: .gitignore (${IMAGE_CACHE_IGNORE_RULE})`);
100
140
  }
101
141
 
102
142
  // Create fossbook.config.js
@@ -119,6 +159,14 @@ function initProject(options = {}) {
119
159
  image: "",
120
160
  theme: "archie",
121
161
 
162
+ // PNG publishing optimization
163
+ imageOptimization: true,
164
+ imageMaxWidth: 1400,
165
+ imageMaxWidthOverride: 1200,
166
+ imageWebP: true,
167
+ imageResponsiveWidths: [600, 800, 1000],
168
+ cacheDir: "./.fossbook-cache",
169
+
122
170
  // Languages (optional)
123
171
  defaultLanguage: "en",
124
172
  defaultLanguageInSubdir: false,
@@ -192,7 +240,9 @@ Welcome to my blog!
192
240
  execSync("npm install", { cwd, stdio: "inherit" });
193
241
  console.log("Created: package-lock.json");
194
242
  } catch {
195
- console.warn("Warning: npm install failed. You may need to run it manually.");
243
+ console.warn(
244
+ "Warning: npm install failed. You may need to run it manually.",
245
+ );
196
246
  }
197
247
  }
198
248
 
@@ -203,13 +253,13 @@ Welcome to my blog!
203
253
 
204
254
  console.log("\nSite initialized! Next steps:");
205
255
  if (!options.github) {
206
- console.log(' 1. Edit fossbook.config.js with your site settings');
256
+ console.log(" 1. Edit fossbook.config.js with your site settings");
207
257
  console.log(' 2. Run: fossbook new "My First Post"');
208
258
  console.log(" 3. Edit your post in content/posts/");
209
259
  console.log(" 4. Run: fossbook build");
210
260
  console.log(" 5. Run: fossbook serve");
211
261
  } else {
212
- console.log(' 1. Edit fossbook.config.js with your site settings');
262
+ console.log(" 1. Edit fossbook.config.js with your site settings");
213
263
  console.log(' 2. Run: fossbook new "My First Post"');
214
264
  console.log(" 3. Edit your post in content/posts/");
215
265
  console.log(" 4. Run: fossbook deploy");
@@ -223,10 +273,10 @@ function initGitHub(cwd) {
223
273
  } catch {
224
274
  console.error(
225
275
  "Error: GitHub CLI (gh) is not installed.\n" +
226
- "Install it: https://cli.github.com/\n" +
227
- " Linux: sudo apt install gh\n" +
228
- " macOS: brew install gh\n" +
229
- " Windows: winget install GitHub.cli"
276
+ "Install it: https://cli.github.com/\n" +
277
+ " Linux: sudo apt install gh\n" +
278
+ " macOS: brew install gh\n" +
279
+ " Windows: winget install GitHub.cli",
230
280
  );
231
281
  process.exit(1);
232
282
  }
@@ -256,7 +306,9 @@ function initGitHub(cwd) {
256
306
  });
257
307
  } catch {
258
308
  // Repo may already exist — ask user what to do
259
- console.warn(`\nA GitHub repository named "${repoName}" already exists on your account.`);
309
+ console.warn(
310
+ `\nA GitHub repository named "${repoName}" already exists on your account.`,
311
+ );
260
312
  const answer = promptSync("Push to the existing repo? (y/n): ");
261
313
  if (answer.toLowerCase() !== "y") {
262
314
  console.log("Cancelled. Your local site files are still available.");
@@ -275,11 +327,18 @@ function initGitHub(cwd) {
275
327
  stdio: ["pipe", "pipe", "ignore"],
276
328
  }).trim();
277
329
  const remoteUrl = `git@github.com:${ghUser}/${repoName}.git`;
278
- execSync(`git remote add origin ${remoteUrl}`, { cwd, stdio: "inherit" });
330
+ execSync(`git remote add origin ${remoteUrl}`, {
331
+ cwd,
332
+ stdio: "inherit",
333
+ });
279
334
  console.log(`Added remote: ${remoteUrl}`);
280
335
  } catch {
281
- console.warn("Warning: Could not add git remote. Add it manually with:");
282
- console.warn(` git remote add origin git@github.com:<user>/${repoName}.git`);
336
+ console.warn(
337
+ "Warning: Could not add git remote. Add it manually with:",
338
+ );
339
+ console.warn(
340
+ ` git remote add origin git@github.com:<user>/${repoName}.git`,
341
+ );
283
342
  }
284
343
  }
285
344
  }
@@ -288,14 +347,19 @@ function initGitHub(cwd) {
288
347
  console.log("Committing and pushing to GitHub...");
289
348
  execSync("git add -A", { cwd, stdio: "inherit" });
290
349
  try {
291
- execSync('git commit -m "Initial fossbook site"', { cwd, stdio: "inherit" });
350
+ execSync('git commit -m "Initial fossbook site"', {
351
+ cwd,
352
+ stdio: "inherit",
353
+ });
292
354
  } catch {
293
355
  // Nothing to commit
294
356
  }
295
357
  try {
296
358
  execSync("git push -u origin main", { cwd, stdio: "inherit" });
297
359
  } catch (e) {
298
- console.warn("Warning: Could not push to origin. You may need to push manually.");
360
+ console.warn(
361
+ "Warning: Could not push to origin. You may need to push manually.",
362
+ );
299
363
  }
300
364
 
301
365
  console.log("\nGitHub repository created and pushed!");
@@ -323,4 +387,24 @@ function promptSync(question) {
323
387
  return str.trim();
324
388
  }
325
389
 
326
- module.exports = { initProject };
390
+ function shouldSaveImageCache(cacheHit, newEntries) {
391
+ return cacheHit !== "true" && newEntries === true;
392
+ }
393
+
394
+ function ensureGitignoreRule(gitignorePath, rule) {
395
+ const contents = fs.readFileSync(gitignorePath, "utf8");
396
+ const rules = contents.split(/\r?\n/).map((line) => line.trim());
397
+ if (rules.includes(rule)) return false;
398
+
399
+ const separator = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
400
+ fs.appendFileSync(gitignorePath, `${separator}${rule}\n`);
401
+ return true;
402
+ }
403
+
404
+ module.exports = {
405
+ DEPLOY_WORKFLOW,
406
+ IMAGE_CACHE_SCHEMA,
407
+ ensureGitignoreRule,
408
+ initProject,
409
+ shouldSaveImageCache,
410
+ };
package/lib/mod/config.js CHANGED
@@ -19,9 +19,15 @@ const defaults = {
19
19
  defaultLanguage: "en",
20
20
  defaultLanguageInSubdir: false,
21
21
  languages: null,
22
- basePath: "", // auto-detected for GitHub Pages project sites, e.g. "/repo-name/"
23
- postsPath: "posts", // URL prefix for posts: "posts" serves articles at /posts/<slug>/; set "" for root
24
- comments: null, // { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" }
22
+ basePath: "", // auto-detected for GitHub Pages project sites, e.g. "/repo-name/"
23
+ postsPath: "posts", // URL prefix for posts: "posts" serves articles at /posts/<slug>/; set "" for root
24
+ comments: null, // { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" }
25
+ imageOptimization: true,
26
+ imageMaxWidth: 1400,
27
+ imageMaxWidthOverride: 1200,
28
+ imageWebP: true,
29
+ imageResponsiveWidths: [600, 800, 1000],
30
+ cacheDir: "./.fossbook-cache",
25
31
  content: "./content",
26
32
  postsDir: "./content/posts",
27
33
  outputDir: "./public",
@@ -44,7 +50,9 @@ function resolveThemePath(config) {
44
50
  if (fs.existsSync(builtinThemePath)) {
45
51
  return builtinThemePath;
46
52
  }
47
- console.error(`Theme "${themeName}" not found in ${userThemePath} or ${builtinThemePath}`);
53
+ console.error(
54
+ `Theme "${themeName}" not found in ${userThemePath} or ${builtinThemePath}`,
55
+ );
48
56
  process.exit(1);
49
57
  }
50
58
 
@@ -76,6 +84,7 @@ function loadConfig(configPath) {
76
84
  content: path.resolve(merged.content),
77
85
  about: path.resolve(merged.content, "about.md"),
78
86
  outdir: path.resolve(merged.outputDir),
87
+ cacheDir: path.resolve(merged.cacheDir),
79
88
  themePath: path.dirname(themePath), // e.g. /path/to/themes
80
89
  staticDir: path.resolve(merged.staticDir),
81
90
  };
@@ -105,23 +114,36 @@ function loadConfig(configPath) {
105
114
 
106
115
  function createLanguageConfigs(config, configPath) {
107
116
  const configuredLanguages = config.languages;
108
- const hasMultipleLanguages = configuredLanguages !== null && configuredLanguages !== undefined;
117
+ const hasMultipleLanguages =
118
+ configuredLanguages !== null && configuredLanguages !== undefined;
109
119
  const languages = hasMultipleLanguages
110
120
  ? configuredLanguages
111
121
  : { [config.defaultLanguage]: {} };
112
122
 
113
- if (typeof languages !== "object" || Array.isArray(languages) || Object.keys(languages).length === 0) {
123
+ if (
124
+ typeof languages !== "object" ||
125
+ Array.isArray(languages) ||
126
+ Object.keys(languages).length === 0
127
+ ) {
114
128
  throw new Error("languages must be a non-empty object");
115
129
  }
116
- if (!Object.prototype.hasOwnProperty.call(languages, config.defaultLanguage)) {
117
- throw new Error(`defaultLanguage "${config.defaultLanguage}" is not defined in languages`);
130
+ if (
131
+ !Object.prototype.hasOwnProperty.call(languages, config.defaultLanguage)
132
+ ) {
133
+ throw new Error(
134
+ `defaultLanguage "${config.defaultLanguage}" is not defined in languages`,
135
+ );
118
136
  }
119
137
 
120
138
  return Object.entries(languages).map(([language, overrides]) => {
121
139
  if (!/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(language)) {
122
140
  throw new Error(`Invalid language key "${language}"`);
123
141
  }
124
- if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
142
+ if (
143
+ !overrides ||
144
+ typeof overrides !== "object" ||
145
+ Array.isArray(overrides)
146
+ ) {
125
147
  throw new Error(`languages.${language} must be an object`);
126
148
  }
127
149
 
@@ -129,7 +151,8 @@ function createLanguageConfigs(config, configPath) {
129
151
  ? loadLanguageConfig(configPath, language)
130
152
  : {};
131
153
 
132
- const useSubdirectory = config.defaultLanguageInSubdir || language !== config.defaultLanguage;
154
+ const useSubdirectory =
155
+ config.defaultLanguageInSubdir || language !== config.defaultLanguage;
133
156
  const languagePrefix = useSubdirectory ? language : "";
134
157
  const basePath = appendPathSegment(config.basePath, languagePrefix);
135
158
  const blogsite = appendUrlSegment(config.blogsite, languagePrefix);
@@ -141,7 +164,8 @@ function createLanguageConfigs(config, configPath) {
141
164
  defaultLanguage: config.defaultLanguage,
142
165
  defaultLanguageInSubdir: config.defaultLanguageInSubdir,
143
166
  language,
144
- languageName: fileOverrides.languageName || overrides.languageName || language,
167
+ languageName:
168
+ fileOverrides.languageName || overrides.languageName || language,
145
169
  locale: fileOverrides.locale || overrides.locale || language,
146
170
  languagePrefix,
147
171
  basePath,
@@ -166,8 +190,14 @@ function loadLanguageConfig(configPath, language) {
166
190
  if (!fs.existsSync(languageConfigPath)) return {};
167
191
 
168
192
  const languageConfig = require(languageConfigPath);
169
- if (!languageConfig || typeof languageConfig !== "object" || Array.isArray(languageConfig)) {
170
- throw new Error(`${path.basename(languageConfigPath)} must export an object`);
193
+ if (
194
+ !languageConfig ||
195
+ typeof languageConfig !== "object" ||
196
+ Array.isArray(languageConfig)
197
+ ) {
198
+ throw new Error(
199
+ `${path.basename(languageConfigPath)} must export an object`,
200
+ );
171
201
  }
172
202
  return languageConfig;
173
203
  }