@slybridges/kiss 0.6.6 → 0.8.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.
Files changed (35) hide show
  1. package/.eslintrc.js +1 -1
  2. package/README.md +1 -1
  3. package/package.json +11 -9
  4. package/src/build.js +154 -101
  5. package/src/cli.js +7 -1
  6. package/src/config/defaultConfig.js +39 -10
  7. package/src/config/loadConfig.js +3 -3
  8. package/src/data/computeCreated.js +19 -1
  9. package/src/data/computeDescription.js +8 -4
  10. package/src/data/computeImage.js +38 -11
  11. package/src/data/computeLayout.js +4 -4
  12. package/src/data/computeMeta.js +1 -1
  13. package/src/data/computeModified.js +18 -1
  14. package/src/data/computePermalink.js +39 -2
  15. package/src/data/initialPageData.js +5 -0
  16. package/src/devServer.js +3 -3
  17. package/src/helpers.js +132 -37
  18. package/src/libs/loadNunjucksFilters.js +9 -7
  19. package/src/loaders/baseLoader.js +1 -0
  20. package/src/loaders/computeCollectionLoader.js +5 -5
  21. package/src/loaders/staticLoader.js +0 -1
  22. package/src/loaders/textLoader.js +2 -2
  23. package/src/logger.js +1 -1
  24. package/src/transforms/atAttributesContentTransform.js +217 -0
  25. package/src/transforms/imageContextTransform.js +163 -114
  26. package/src/transforms/index.js +2 -0
  27. package/src/views/computeCategoriesDataView.js +2 -2
  28. package/src/views/computeCollectionDataView.js +2 -2
  29. package/src/views/computeIterableCollectionDataView.js +6 -6
  30. package/src/views/computeSiteLastUpdatedDataView.js +1 -1
  31. package/src/writers/htmlWriter.js +1 -1
  32. package/src/writers/imageWriter.js +8 -1
  33. package/src/writers/jsonContextWriter.js +1 -1
  34. package/src/writers/rssContextWriter.js +7 -7
  35. package/src/writers/sitemapContextWriter.js +2 -2
package/.eslintrc.js CHANGED
@@ -3,7 +3,7 @@ module.exports = {
3
3
  es6: true,
4
4
  node: true,
5
5
  },
6
- extends: "eslint:recommended",
6
+ extends: ["eslint:recommended", "prettier"],
7
7
  parserOptions: {
8
8
  ecmaVersion: 12,
9
9
  },
package/README.md CHANGED
@@ -42,7 +42,7 @@ Concept is being tuned until reaching v1. Things might break. Use at your own ri
42
42
 
43
43
  ## Requirements
44
44
 
45
- Node 12 or above.
45
+ Node 20 or above.
46
46
 
47
47
  ## Quick start
48
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slybridges/kiss",
3
- "version": "0.6.6",
3
+ "version": "0.8.0",
4
4
  "description": "Keep It Simple and Static site generator",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,24 +12,26 @@
12
12
  "author": "Sylvestre Dupont",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "browser-sync": "^2.29.3",
15
+ "browser-sync": "^3.0.2",
16
16
  "chalk": "^4.1.2",
17
17
  "cheerio": "^1.0.0-rc.12",
18
- "chokidar": "^3.5.3",
19
- "date-fns": "^2.30.0",
20
- "fast-glob": "^3.3.1",
18
+ "chokidar": "^3.6.0",
19
+ "date-fns": "^3.6.0",
20
+ "fast-glob": "^3.3.2",
21
21
  "front-matter": "^4.0.2",
22
- "fs-extra": "^11.1.1",
22
+ "fs-extra": "^11.2.0",
23
23
  "lodash": "^4.17.21",
24
- "marked": "^8.0.1",
24
+ "marked": "^12.0.1",
25
25
  "nunjucks": "^3.2.4",
26
- "sharp": "^0.32.5",
26
+ "sharp": "^0.33.3",
27
27
  "slugify": "^1.6.6",
28
28
  "xml": "^1.0.1",
29
29
  "yargs": "^17.7.2"
30
30
  },
31
31
  "devDependencies": {
32
- "eslint": "^8.49.0"
32
+ "eslint": "^8.57.0",
33
+ "eslint-config-prettier": "^9.1.0",
34
+ "prettier": "^3.2.5"
33
35
  },
34
36
  "repository": {
35
37
  "type": "git",
package/src/build.js CHANGED
@@ -5,14 +5,14 @@ const fg = require("fast-glob")
5
5
  const path = require("path")
6
6
 
7
7
  const { loadConfig } = require("./config")
8
- const { getParentId } = require("./helpers")
8
+ const { getParentId, relativeToAbsoluteAttributes } = require("./helpers")
9
9
  const { baseLoader } = require("./loaders")
10
10
  const { setGlobalLogger } = require("./logger")
11
11
 
12
12
  const build = async (options = {}, config = null) => {
13
13
  console.time("Build time")
14
14
 
15
- const { configFile, verbosity, watchMode } = options
15
+ const { configFile, unsafeBuild, verbosity, watchMode } = options
16
16
 
17
17
  setGlobalLogger(verbosity)
18
18
 
@@ -37,7 +37,7 @@ const build = async (options = {}, config = null) => {
37
37
  }
38
38
 
39
39
  global.logger.section(
40
- `Loading content from '${config.dirs.content}' directory`
40
+ `Loading content from '${config.dirs.content}' directory`,
41
41
  )
42
42
  context.pages = await loadContent(config, context)
43
43
 
@@ -68,12 +68,18 @@ const build = async (options = {}, config = null) => {
68
68
  const warningCount = global.logger.counts.warn
69
69
  if (errorCount > 0) {
70
70
  global.logger.error(
71
- `${errorCount} error(s) and ${warningCount} warning(s) found.`
71
+ `${errorCount} error(s) and ${warningCount} warning(s) found.`,
72
72
  )
73
73
  if (!watchMode) {
74
- console.timeEnd("Build time")
75
- global.logger.info("Exiting build with errors.")
76
- process.exit(1)
74
+ if (unsafeBuild) {
75
+ global.logger.info(
76
+ "Unsafe build mode: exiting build with errors without raising exit(1)",
77
+ )
78
+ } else {
79
+ global.logger.info("Exiting build with errors.")
80
+ console.timeEnd("Build time")
81
+ process.exit(1)
82
+ }
77
83
  }
78
84
  } else if (warningCount > 0) {
79
85
  global.logger.warn(`${warningCount} warning(s) found.`)
@@ -95,7 +101,7 @@ const applyTransforms = async (context, config) => {
95
101
  const validScopes = [null, "PAGE", "CONTEXT"]
96
102
  for await (let transform of config.transforms) {
97
103
  const { scope, handler, outputType, namespace, ...rest } = transform
98
- const options = namespace ? _.get(config, namespace, {}) : rest
104
+ const options = getOptions(config, namespace, rest)
99
105
  if ("active" in options && !options.active) {
100
106
  global.logger.log(`- [${handler.name}]: transform not active. Skipping.`)
101
107
  continue
@@ -104,7 +110,7 @@ const applyTransforms = async (context, config) => {
104
110
  throw new Error(
105
111
  `[applyTransforms]: invalid scope for transform ${
106
112
  handler.name
107
- }, got '${scope}'. Valid choices are ${JSON.stringify(validScopes)}`
113
+ }, got '${scope}'. Valid choices are ${JSON.stringify(validScopes)}`,
108
114
  )
109
115
  }
110
116
  if (scope === "CONTEXT") {
@@ -117,7 +123,7 @@ const applyTransforms = async (context, config) => {
117
123
  } catch (err) {
118
124
  global.logger.error(
119
125
  `[${handler.name}] Error during transform:\n`,
120
- err.stack
126
+ err.stack,
121
127
  )
122
128
  }
123
129
  } else {
@@ -136,7 +142,7 @@ const applyTransforms = async (context, config) => {
136
142
  } catch (err) {
137
143
  global.logger.error(
138
144
  `[${handler.name}] Error during transform of page '${id}'\n`,
139
- err.stack
145
+ err.stack,
140
146
  )
141
147
  }
142
148
  }
@@ -157,7 +163,7 @@ const computeDataViews = (context, config) => {
157
163
  } catch (err) {
158
164
  global.logger.error(
159
165
  `[${handler.name}] Error during computing data view for '${attribute}'\n`,
160
- err.stack
166
+ err.stack,
161
167
  )
162
168
  }
163
169
  })
@@ -197,7 +203,7 @@ const computePageData = (data, config, context, options = {}) => {
197
203
  currentPending = countPendingDependencies(
198
204
  options.topLevelData,
199
205
  context.pages,
200
- value.kissDependencies
206
+ value.kissDependencies,
201
207
  )
202
208
  }
203
209
  if (currentPending == 0) {
@@ -236,7 +242,7 @@ const computeAllPagesData = (context, config) => {
236
242
  } catch (err) {
237
243
  global.logger.error(
238
244
  `[computePageData] Error during computing page data for page id '${page._meta.id}'\n`,
239
- err.stack
245
+ err.stack,
240
246
  )
241
247
  }
242
248
  context.pages[key] = computed.data
@@ -245,13 +251,13 @@ const computeAllPagesData = (context, config) => {
245
251
  if (pendingTotal > 0 && round + 1 > config.defaults.maxComputingRounds) {
246
252
  global.logger.error(
247
253
  ```Could not compute all data in ${config.defaults.maxComputingRounds} rounds. Check for circular
248
- dependencies or increase the 'maxComputingRounds' value.```
254
+ dependencies or increase the 'maxComputingRounds' value.```,
249
255
  )
250
256
  break
251
257
  }
252
258
  if (pendingTotal > 0) {
253
259
  global.logger.log(
254
- `- Round ${round}: ${pendingTotal} data points could not yet be computed. New round.`
260
+ `- Round ${round}: ${pendingTotal} data points could not yet be computed. New round.`,
255
261
  )
256
262
  } else {
257
263
  global.logger.log(`- Round ${round}: all data points computed.`)
@@ -282,8 +288,8 @@ const countPendingDependencies = (page, pages, deps = []) => {
282
288
  (pendingCount += countPendingDependencies(
283
289
  pages[id],
284
290
  pages,
285
- restDeps
286
- ))
291
+ restDeps,
292
+ )),
287
293
  )
288
294
  } else if (isComputableValue(depValue)) {
289
295
  pendingCount++
@@ -293,7 +299,7 @@ const countPendingDependencies = (page, pages, deps = []) => {
293
299
  }
294
300
  } else {
295
301
  throw new Error(
296
- `countPendingDependencies: dependency should either be a string or an array of strings: ${dep}`
302
+ `countPendingDependencies: dependency should either be a string or an array of strings: ${dep}`,
297
303
  )
298
304
  }
299
305
  })
@@ -321,7 +327,7 @@ const directoryCollectionLoader = (pathname, options, pages, config) => {
321
327
  { isDirectory: true, collectionGroup: "directory" },
322
328
  isTopLevel ? config.defaults.pageData : {},
323
329
  pages,
324
- config
330
+ config,
325
331
  )
326
332
 
327
333
  return _.merge({}, pages, {
@@ -333,72 +339,92 @@ const isComputableValue = (value) =>
333
339
  typeof value === "function" ||
334
340
  (_.isPlainObject(value) && value._kissCheckDependencies)
335
341
 
342
+ const getOptions = (config, namespace, options) => {
343
+ const nameSpaceOptions = _.get(config, namespace, {})
344
+ return { ...nameSpaceOptions, ...options }
345
+ }
346
+
336
347
  const loadContent = async (config, context) => {
337
348
  let pages = {}
338
- // file loaders
339
- await Promise.all(
340
- config.loaders
341
- .filter((loader) => !loader.source || loader.source === "file")
342
- .map(async ({ handler, namespace, ...loaderOptions }) => {
343
- const {
344
- description,
345
- match,
346
- matchOptions = {},
347
- ...options
348
- } = namespace ? _.get(config, namespace, {}) : loaderOptions
349
- if ("active" in options && !options.active) {
350
- global.logger.log(`- [${handler.name}]: loader not active. Skipping.`)
351
- return
352
- }
353
- let fgOptions = {
354
- cwd: config.dirs.content,
355
- markDirectories: true,
356
- stats: true,
357
- ...matchOptions,
358
- }
359
- const message =
360
- description ||
361
- `Loading files matching ${JSON.stringify(match)} using ${
362
- handler.name
363
- }`
364
- global.logger.info(message)
365
- let files = fg.sync(match, fgOptions)
366
- // sort files to make sure index files are loaded first
367
- // and respect the data cascade principle
368
- files = sortIndexFirst(files)
369
- for (const file of files) {
370
- let pathname = path.join(config.dirs.content, file.path)
371
- let page = {}
372
- try {
373
- // load parent folders, if any
374
- pages = directoryCollectionLoader(pathname, options, pages, config)
375
- // load stats
376
- page = {
377
- _meta: {
378
- fileCreated: file.stats.ctime,
379
- fileModified: file.stats.mtime,
380
- },
381
- }
382
- // load base data including _meta infos and based on ParentData
383
- page = baseLoader(pathname, options, page, pages, config)
384
- // load content specific data
385
- page = handler(pathname, options, page, context, config)
386
- pages[page._meta.id] = page
387
- global.logger.log(
388
- `- [${handler.name}] loaded '${page._meta.inputPath}'`
389
- )
390
- } catch (err) {
391
- global.logger.error(
392
- `- [${handler.name}] Error loading '${_.get(
393
- page,
394
- "_meta.inputPath"
395
- )}'\n`,
396
- err.stack
397
- )
398
- }
399
- }
400
- })
349
+ let files = []
350
+ // We first need to fetch all files that match all loaders
351
+ // So that we can sort them in the right order before loading them
352
+ // This is essential for the data cascade to work properly
353
+ config.loaders.forEach(
354
+ ({ handler, namespace, source, ...loaderOptions }, idx) => {
355
+ if (source && source !== "file") {
356
+ return
357
+ }
358
+ const allOptions = getOptions(config, namespace, loaderOptions)
359
+ const { match, matchOptions = {}, ...options } = allOptions
360
+ if ("active" in options && !options.active) {
361
+ global.logger.log(`[${handler.name}]: loader not active. Skipping.`)
362
+ return
363
+ }
364
+ let fgOptions = {
365
+ cwd: config.dirs.content,
366
+ markDirectories: true,
367
+ stats: true,
368
+ ...matchOptions,
369
+ }
370
+ let extraOptions = []
371
+ if (namespace !== handler.name) {
372
+ extraOptions.push(`namespace: '${namespace}'`)
373
+ }
374
+ if (loaderOptions && Object.keys(loaderOptions).length > 0) {
375
+ extraOptions.push(`options: ${JSON.stringify(loaderOptions)}`)
376
+ }
377
+ global.logger.info(
378
+ `Listing files matching ${JSON.stringify(match)} for ${handler.name}${extraOptions.length > 0 ? ` (${extraOptions.join(", ")})` : ""}`,
379
+ )
380
+ files = files.concat(
381
+ fg.sync(match, fgOptions).map((file) => ({ ...file, loaderIdx: idx })),
382
+ )
383
+ },
384
+ )
385
+ global.logger.info(
386
+ `Found ${files.length} files. Sorting them for the data cascade.`,
401
387
  )
388
+ // sorting and loading files
389
+ // sort files to make sure index.* files are loaded first and post.* files last
390
+ // in order to respect the data cascade principle
391
+ files = sortFiles(files)
392
+ global.logger.info(`Loading files...`)
393
+ for (const file of files) {
394
+ // finding the right loader for the file
395
+ const { handler, namespace, ...loaderOptions } =
396
+ config.loaders[file.loaderIdx]
397
+ const options = getOptions(config, namespace, loaderOptions)
398
+ let pathname = path.join(config.dirs.content, file.path)
399
+ let page = {}
400
+ try {
401
+ // load parent folders, if any
402
+ pages = directoryCollectionLoader(pathname, options, pages, config)
403
+ // load stats
404
+ page = {
405
+ _meta: {
406
+ fileCreated: file.stats.ctime,
407
+ fileModified: file.stats.mtime,
408
+ },
409
+ }
410
+ // load base data including _meta infos and based on ParentData
411
+ page = baseLoader(pathname, options, page, pages, config)
412
+ // load content specific data
413
+ page = await handler(pathname, options, page, context, config)
414
+ // relative @attributes to absolute
415
+ page = relativeToAbsoluteAttributes(page, options, config)
416
+ pages[page._meta.id] = page
417
+ global.logger.log(`- [${handler.name}] loaded '${page._meta.inputPath}'`)
418
+ } catch (err) {
419
+ global.logger.error(
420
+ `- [${handler.name}] Error loading '${_.get(
421
+ page,
422
+ "_meta.inputPath",
423
+ )}'\n`,
424
+ err.stack,
425
+ )
426
+ }
427
+ }
402
428
  // computed loaders
403
429
  _.filter(config.loaders, (loader) => loader.source === "computed").forEach(
404
430
  (loader) => {
@@ -407,7 +433,7 @@ const loadContent = async (config, context) => {
407
433
  description || `Loading computed pages using ${handler.name}`
408
434
  global.logger.info(message)
409
435
  pages = handler(pages, options, config)
410
- }
436
+ },
411
437
  )
412
438
  return pages
413
439
  }
@@ -452,7 +478,7 @@ const runCopyHook = ({ from, to, description }, config) => {
452
478
  } catch (e) {
453
479
  global.logger.error(
454
480
  `Error copying from '${from}' to '${publicTo}'\n`,
455
- e.stack
481
+ e.stack,
456
482
  )
457
483
  }
458
484
  }
@@ -468,16 +494,39 @@ const runExecHook = (command, options) => {
468
494
  }
469
495
  }
470
496
 
471
- const sortIndexFirst = (files) => {
497
+ // Sort files using those rules:
498
+ // Files higher in the list will be loaded first
499
+ // Inside the same directory load index first, all other files but post files after and post files last
500
+ const sortFiles = (files) => {
472
501
  return files.sort((fileA, fileB) => {
473
502
  const isAIndexFile = fileA.name.startsWith("index.")
474
503
  const isBIndexFile = fileB.name.startsWith("index.")
504
+ const isAPostFile = fileA.name.startsWith("post.")
505
+ const isBPostFile = fileB.name.startsWith("post.")
506
+
475
507
  if (isAIndexFile && isBIndexFile) {
476
508
  // both are indexes
477
509
  // return the shortest path first to respect data cascade
478
- return fileB.path.length < fileA.path.length ? 1 : -1
510
+ return fileA.path.length < fileB.path.length ? -1 : 1
511
+ }
512
+ if (isAIndexFile || isBIndexFile) {
513
+ // one of them is an index file
514
+ // index files first
515
+ return isAIndexFile ? -1 : 1
516
+ }
517
+ if (isAPostFile && isBPostFile) {
518
+ // both are post files
519
+ // we don't want higher up post files to impact lower post files
520
+ // so we return the longest path first
521
+ return fileA.path.length < fileB.path.length ? 1 : -1
479
522
  }
480
- return isBIndexFile && !isAIndexFile ? 1 : -1
523
+ if (isAPostFile || isBPostFile) {
524
+ // one of them is a post file
525
+ // post files last
526
+ return isAPostFile ? 1 : -1
527
+ }
528
+ // all other files
529
+ return 0
481
530
  })
482
531
  }
483
532
 
@@ -493,38 +542,42 @@ const runHandlerHook = (handler, options, config, data) => {
493
542
  }
494
543
 
495
544
  const writeStaticSite = async (context, config) => {
496
- global.logger.info("Writing individual pages")
545
+ global.logger.info("Writing individual pages and images")
497
546
  await Promise.all(
498
547
  _.map(context.pages, async (page) => {
499
- if (!page.permalink) {
548
+ if (page.excludeFromWrite) {
549
+ global.logger.log(
550
+ `- Page '${page._meta.id}' is marked as excludeFromWrite. Skipping.`,
551
+ )
552
+ } else if (!page.permalink) {
500
553
  global.logger.log(
501
- `- Page '${page._meta.id}' has no permalink. Skipping.`
554
+ `- Page '${page._meta.id}' has no permalink. Skipping.`,
502
555
  )
503
556
  } else {
504
557
  const writer = config.writers.find(
505
- (writer) => writer.outputType === page._meta.outputType
558
+ (writer) => writer.outputType === page._meta.outputType,
506
559
  )
507
560
  if (writer) {
508
561
  const { handler, namespace, ...rest } = writer
509
- const options = namespace ? _.get(config, namespace, {}) : rest
562
+ const options = getOptions(config, namespace, rest)
510
563
  try {
511
564
  await handler(page, options, config, context)
512
565
  global.logger.log(
513
- `- [${writer.handler.name}] wrote '${page._meta.outputPath}'`
566
+ `- [${writer.handler.name}] wrote '${page._meta.outputPath}'`,
514
567
  )
515
568
  } catch (err) {
516
569
  global.logger.error(
517
570
  `- [${writer.handler.name}] error writing '${page._meta.outputPath}'\n`,
518
- err.stack
571
+ err.stack,
519
572
  )
520
573
  }
521
574
  } else {
522
575
  global.logger.warn(
523
- `- no writer for type '${page._meta.outputType}' found for '${page._meta.inputPath}'. Skipping.`
576
+ `- no writer for type '${page._meta.outputType}' found for '${page._meta.inputPath}'. Skipping.`,
524
577
  )
525
578
  }
526
579
  }
527
- })
580
+ }),
528
581
  )
529
582
  const contextWriters = _.filter(config.writers, { scope: "CONTEXT" })
530
583
  if (contextWriters.length === 0) {
@@ -534,7 +587,7 @@ const writeStaticSite = async (context, config) => {
534
587
  return await Promise.all(
535
588
  contextWriters.map(async (writer) => {
536
589
  const { handler, namespace, ...rest } = writer
537
- const options = namespace ? _.get(config, namespace, {}) : rest
590
+ const options = getOptions(config, namespace, rest)
538
591
  if ("active" in options && !options.active) {
539
592
  global.logger.log(`- [${handler.name}]: writer not active. Skipping.`)
540
593
  return
@@ -542,14 +595,14 @@ const writeStaticSite = async (context, config) => {
542
595
  try {
543
596
  await handler(context, options, config)
544
597
  global.logger.log(
545
- `- [${handler.name}] wrote ${options.target || "file"}`
598
+ `- [${handler.name}] wrote ${options.target || "file"}`,
546
599
  )
547
600
  } catch (err) {
548
601
  global.logger.error(
549
602
  `- [${handler.name}] error writing ${options.target || "file"}\n`,
550
- err.stack
603
+ err.stack,
551
604
  )
552
605
  }
553
- })
606
+ }),
554
607
  )
555
608
  }
package/src/cli.js CHANGED
@@ -11,7 +11,7 @@ yargs(hideBin(process.argv))
11
11
  "start",
12
12
  "start server and rebuilds on changes (serve + watch)",
13
13
  () => {},
14
- start
14
+ start,
15
15
  )
16
16
  .command("serve", "start development server", () => {}, serve)
17
17
  .command("watch", "watch files and rebuild site on changes", () => {}, watch)
@@ -21,4 +21,10 @@ yargs(hideBin(process.argv))
21
21
  describe: "Verbosity level",
22
22
  choices: ["log", "info", "success", "warn", "error"],
23
23
  })
24
+ .option("u", {
25
+ alias: "unsafe-build",
26
+ boolean: true,
27
+ default: false,
28
+ describe: "Won't exit(1) on build errors",
29
+ })
24
30
  .demandCommand(1, "Enter a command").argv
@@ -14,6 +14,7 @@ const {
14
14
  textLoader,
15
15
  } = require("../loaders")
16
16
  const {
17
+ atAttributesContentTransform,
17
18
  imageContextTransform,
18
19
  nunjucksContentTransform,
19
20
  } = require("../transforms")
@@ -69,16 +70,21 @@ const defaultConfig = {
69
70
  defaults: {
70
71
  sortCollectionBy: "-created",
71
72
  dateFormat: "MMMM do, yyyy 'at' hh:mm aaa",
73
+ // descriptionLength: how many characters to use for the description field
74
+ // Meta descriptions can be any length, but Google generally truncates snippets to ~155–160 characters.
75
+ // https://moz.com/learn/seo/meta-description
76
+ // This settings only applies to description automatically generated. If you provide your own description, it will be used as is.
77
+ descriptionLength: 160,
72
78
  maxComputingRounds: 10,
73
79
  pageData: initialPageData,
74
80
  pagePublishedAttribute: "created",
75
81
  pageUpdatedAttribute: "modified",
76
82
  },
77
83
  dirs: {
78
- content: "content",
79
- public: "public",
80
- theme: "theme",
81
- template: "theme/templates",
84
+ content: "content", // where to load documents from
85
+ public: "public", // where to write the generated files
86
+ theme: "theme", // where to find templates and other design files
87
+ template: "theme/templates", // where to find templates
82
88
  watchExtra: [], // additional paths to watch for change in watch mode
83
89
  },
84
90
  env: env,
@@ -114,9 +120,11 @@ const defaultConfig = {
114
120
  loaders: [
115
121
  { handler: jsLoader, namespace: "jsLoader" },
116
122
  { handler: jsonLoader, namespace: "jsonLoader" },
117
- { handler: markdownLoader, namespace: "markdownLoader" },
118
123
  { handler: staticLoader, namespace: "staticLoader" },
119
124
  { handler: textLoader, namespace: "textLoader" },
125
+ // image is loaded in a separate staticLoader to allow for image optimization later
126
+ { handler: staticLoader, namespace: "image", outputType: "IMAGE" },
127
+ { handler: markdownLoader, namespace: "markdownLoader" },
120
128
  // Use the example below to create computed tag pages from the "tags" attribute found in pages
121
129
  // {
122
130
  // source: "computed",
@@ -131,10 +139,16 @@ const defaultConfig = {
131
139
  handler: nunjucksContentTransform,
132
140
  description: "Applying Nunjucks templates to content",
133
141
  },
142
+ {
143
+ outputType: "HTML",
144
+ handler: atAttributesContentTransform,
145
+ description: "Finding and resolving @attributes",
146
+ },
134
147
  {
135
148
  scope: "CONTEXT",
136
149
  namespace: "image",
137
150
  handler: imageContextTransform,
151
+ description: "Finding and preparing images to optimize",
138
152
  },
139
153
  ],
140
154
  writers: [
@@ -160,22 +174,33 @@ const defaultConfig = {
160
174
  blur: false, // turning to true requires https://github.com/verlok/vanilla-lazyload
161
175
  blurWidth: 32,
162
176
  defaultWidth: 1024,
163
- description: "Optimizing images",
164
177
  filename: defaultImageFilename,
165
- formats: ["jpeg"], // webp, avif
178
+ // output format of images
179
+ formats: ["original"],
180
+ // input path and format of images
181
+ match: ["**/*.jpg", "**/*.jpeg", "**/*.png", "**/*.webp"],
166
182
  overwrite: env === "production", // if false, won't regenerate the image if already in public dir
167
183
  sizes: ["(min-width: 1024px) 1024px", "100vw"],
168
184
  widths: [320, 640, 1024, 1366, "original"],
185
+ // defaultFormat: "jpeg", // if not present, will use formats[0]
169
186
  // resizeOptions: { /*... any option accepted by sharp.resize()*/ }
170
187
  // jpegOptions: { /*... any option accepted by sharp.jpeg()*/ }
171
188
  // webpOptions: { /*... any option accepted by sharp.webp()*/ }
172
189
  // avifOptions: { /*... any option accepted by sharp.avif()*/ }
173
190
  },
191
+ templates: {
192
+ collection: "collection.njk",
193
+ default: "default.njk",
194
+ post: "post.njk",
195
+ },
174
196
  rss: {
175
197
  active: true,
176
198
  target: "feed.xml",
177
199
  pageFilter: (page) =>
178
- page.url && page._meta.isPost && !page.excludeFromCollection,
200
+ page.url &&
201
+ page._meta.isPost &&
202
+ !page.excludeFromWrite &&
203
+ !page.excludeFromSitemap,
179
204
  xmlOptions: {
180
205
  declaration: true,
181
206
  indent: env === "production" ? null : " ",
@@ -200,7 +225,11 @@ const defaultConfig = {
200
225
  collection: 0.5,
201
226
  },
202
227
  target: "sitemap.xml",
203
- pageFilter: (page) => page.url && page._meta.outputType === "HTML",
228
+ pageFilter: (page) =>
229
+ page.url &&
230
+ page._meta.outputType === "HTML" &&
231
+ !page.excludeFromWrite &&
232
+ !page.excludeFromSitemap,
204
233
  xmlOptions: {
205
234
  declaration: true,
206
235
  indent: env === "production" ? null : " ",
@@ -216,7 +245,7 @@ module.exports = defaultConfig
216
245
  function addPlugin(pluginFunc, options) {
217
246
  if (typeof pluginFunc !== "function") {
218
247
  throw new Error(
219
- `config.addPlugin(): plugin argument should be a function, got: '${typeof pluginFunc}'`
248
+ `config.addPlugin(): plugin argument should be a function, got: '${typeof pluginFunc}'`,
220
249
  )
221
250
  }
222
251
  global.logger.info(`Loading '${pluginFunc.name}' plugin`)
@@ -32,17 +32,17 @@ const checkConfig = (config) => {
32
32
  const siteURL = _.get(config, "context.site.url")
33
33
  if (!isValidURL(siteURL)) {
34
34
  global.logger.error(
35
- `[checkConfig] 'context.site.url' is required and should be a valid URL (e.g. https://example.org), got ${siteURL}.`
35
+ `[checkConfig] 'context.site.url' is required and should be a valid URL (e.g. https://example.org), got ${siteURL}.`,
36
36
  )
37
37
  }
38
38
  if (!_.get(config, "context.site.title")) {
39
39
  global.logger.warn(
40
- `[checkConfig] No site title found. We highly recommend to set it in 'context.site.title'.`
40
+ `[checkConfig] No site title found. We highly recommend to set it in 'context.site.title'.`,
41
41
  )
42
42
  }
43
43
  if (!_.get(config, "context.site.image")) {
44
44
  global.logger.warn(
45
- `[checkConfig] No default image found. We highly recommend to set one in 'context.site.image'.`
45
+ `[checkConfig] No default image found. We highly recommend to set one in 'context.site.image'.`,
46
46
  )
47
47
  }
48
48
  }