@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.
- package/.eslintrc.js +1 -1
- package/README.md +1 -1
- package/package.json +11 -9
- package/src/build.js +154 -101
- package/src/cli.js +7 -1
- package/src/config/defaultConfig.js +39 -10
- package/src/config/loadConfig.js +3 -3
- package/src/data/computeCreated.js +19 -1
- package/src/data/computeDescription.js +8 -4
- package/src/data/computeImage.js +38 -11
- package/src/data/computeLayout.js +4 -4
- package/src/data/computeMeta.js +1 -1
- package/src/data/computeModified.js +18 -1
- package/src/data/computePermalink.js +39 -2
- package/src/data/initialPageData.js +5 -0
- package/src/devServer.js +3 -3
- package/src/helpers.js +132 -37
- package/src/libs/loadNunjucksFilters.js +9 -7
- package/src/loaders/baseLoader.js +1 -0
- package/src/loaders/computeCollectionLoader.js +5 -5
- package/src/loaders/staticLoader.js +0 -1
- package/src/loaders/textLoader.js +2 -2
- package/src/logger.js +1 -1
- package/src/transforms/atAttributesContentTransform.js +217 -0
- package/src/transforms/imageContextTransform.js +163 -114
- package/src/transforms/index.js +2 -0
- package/src/views/computeCategoriesDataView.js +2 -2
- package/src/views/computeCollectionDataView.js +2 -2
- package/src/views/computeIterableCollectionDataView.js +6 -6
- package/src/views/computeSiteLastUpdatedDataView.js +1 -1
- package/src/writers/htmlWriter.js +1 -1
- package/src/writers/imageWriter.js +8 -1
- package/src/writers/jsonContextWriter.js +1 -1
- package/src/writers/rssContextWriter.js +7 -7
- package/src/writers/sitemapContextWriter.js +2 -2
package/.eslintrc.js
CHANGED
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slybridges/kiss",
|
|
3
|
-
"version": "0.
|
|
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": "^
|
|
15
|
+
"browser-sync": "^3.0.2",
|
|
16
16
|
"chalk": "^4.1.2",
|
|
17
17
|
"cheerio": "^1.0.0-rc.12",
|
|
18
|
-
"chokidar": "^3.
|
|
19
|
-
"date-fns": "^
|
|
20
|
-
"fast-glob": "^3.3.
|
|
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.
|
|
22
|
+
"fs-extra": "^11.2.0",
|
|
23
23
|
"lodash": "^4.17.21",
|
|
24
|
-
"marked": "^
|
|
24
|
+
"marked": "^12.0.1",
|
|
25
25
|
"nunjucks": "^3.2.4",
|
|
26
|
-
"sharp": "^0.
|
|
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.
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 &&
|
|
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) =>
|
|
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`)
|
package/src/config/loadConfig.js
CHANGED
|
@@ -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
|
}
|