poops 1.5.3 → 1.7.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/README.md +191 -9
- package/lib/exec.js +44 -0
- package/lib/markup/collections.js +240 -42
- package/lib/markup/engines/liquid.js +9 -3
- package/lib/markup/engines/nunjucks.js +9 -3
- package/lib/markup/helpers.js +314 -7
- package/lib/markup/indexer.js +371 -8
- package/lib/markups.js +57 -7
- package/package.json +2 -2
- package/poops.js +22 -9
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { slugify, humanize } from 'book-of-spells'
|
|
4
5
|
import log from '../utils/log.js'
|
|
5
6
|
import { mkDir, toPosix } from '../utils/helpers.js'
|
|
6
|
-
import { replaceOutExtensions, getRelativePathPrefix, getPageUrlRelativeToOutput, parseFrontMatter, wordcount } from './helpers.js'
|
|
7
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrlRelativeToOutput, parseFrontMatter, wordcount, excerpt, groupby } from './helpers.js'
|
|
7
8
|
|
|
8
9
|
export function getSingleCollectionData(markupInDir, collectionName) {
|
|
9
10
|
const collectionData = []
|
|
@@ -30,6 +31,7 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
30
31
|
log({ tag: 'markup', warn: true, text: 'No date in front matter, falling back to file mtime:', link: file })
|
|
31
32
|
}
|
|
32
33
|
frontMatter.wordcount = wordcount(content)
|
|
34
|
+
frontMatter.excerpt = excerpt(content)
|
|
33
35
|
frontMatter.fileName = path.basename(file)
|
|
34
36
|
frontMatter.filePath = path.relative(process.cwd(), file)
|
|
35
37
|
frontMatter.collection = collectionName
|
|
@@ -157,9 +159,75 @@ export function buildCollectionObject(markupInDir, collectionProtoObject) {
|
|
|
157
159
|
}
|
|
158
160
|
})
|
|
159
161
|
|
|
162
|
+
if (collectionProtoObject.taxonomies) {
|
|
163
|
+
collection._taxonomies = normalizeTaxonomies(collectionProtoObject.taxonomies, collection.paginate)
|
|
164
|
+
}
|
|
165
|
+
|
|
160
166
|
return collection
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
// Normalizes the `taxonomies` collection option into { name, path, paginate }.
|
|
170
|
+
// Accepts "tags" | ["tags", "category"] | [{ name, path, paginate }]. `name` is
|
|
171
|
+
// the front-matter field grouped on; `path` is the URL segment under the
|
|
172
|
+
// collection (defaults to `name`, so use `{ name: "tags", path: "tag" }` for a
|
|
173
|
+
// singular /tag/ URL); `paginate` falls back to the collection's page size.
|
|
174
|
+
function normalizeTaxonomies(config, defaultPaginate) {
|
|
175
|
+
const items = Array.isArray(config) ? config : [config]
|
|
176
|
+
const out = []
|
|
177
|
+
for (let item of items) {
|
|
178
|
+
if (typeof item === 'string') item = { name: item }
|
|
179
|
+
if (!item || !item.name) continue
|
|
180
|
+
const paginate = parseInt(item.paginate)
|
|
181
|
+
out.push({
|
|
182
|
+
name: item.name,
|
|
183
|
+
path: item.path || item.name,
|
|
184
|
+
paginate: !isNaN(paginate) ? paginate : defaultPaginate
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Chunks items into pages of `size`; no/invalid size → a single page holding all.
|
|
191
|
+
export function paginateItems(items, size) {
|
|
192
|
+
if (!size || isNaN(size) || size < 1) return [items]
|
|
193
|
+
const pages = []
|
|
194
|
+
for (let i = 0; i < items.length; i += size) pages.push(items.slice(i, i + size))
|
|
195
|
+
return pages.length ? pages : [items]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Groups each collection's items by every configured taxonomy field and attaches
|
|
199
|
+
// collection.taxonomies = [{ name, path, terms: [{ term, slug, url, count,
|
|
200
|
+
// totalPages, pages, items }] }] — consumed by both the listing template (to link
|
|
201
|
+
// tag/category pages) and generateTaxonomyPages. Must run before any collection
|
|
202
|
+
// template renders so page 1 of the index can already see the term links.
|
|
203
|
+
// ponytail: two terms that slugify to the same slug collide on one URL (last
|
|
204
|
+
// write wins). Add a per-slug de-dupe counter if real tag sets ever clash.
|
|
205
|
+
export function buildTaxonomyData(collectionData) {
|
|
206
|
+
if (!collectionData) return
|
|
207
|
+
for (const name of Object.keys(collectionData)) {
|
|
208
|
+
const collection = collectionData[name]
|
|
209
|
+
if (!collection._taxonomies) continue
|
|
210
|
+
collection.taxonomies = collection._taxonomies.map((tax) => {
|
|
211
|
+
const terms = groupby(collection.items, tax.name)
|
|
212
|
+
.filter((g) => g.key !== '')
|
|
213
|
+
.map((g) => {
|
|
214
|
+
const slug = slugify(g.key)
|
|
215
|
+
const pages = paginateItems(g.items, tax.paginate)
|
|
216
|
+
return {
|
|
217
|
+
term: g.key,
|
|
218
|
+
slug,
|
|
219
|
+
url: toPosix(path.join(collection.name, tax.path, slug)),
|
|
220
|
+
count: g.items.length,
|
|
221
|
+
totalPages: pages.length,
|
|
222
|
+
pages,
|
|
223
|
+
items: g.items
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
return { name: tax.name, path: tax.path, terms }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
163
231
|
export function buildCollectionPaginationData(collectionData) {
|
|
164
232
|
if (!collectionData) return
|
|
165
233
|
|
|
@@ -203,9 +271,83 @@ export function pruneStalePaginationDirs(collectionName, markupInDir, markupOutD
|
|
|
203
271
|
}
|
|
204
272
|
}
|
|
205
273
|
|
|
206
|
-
|
|
274
|
+
// Renders a collection template once per page in `pages`, writing page 1 to
|
|
275
|
+
// out/<urlBase>/index.html and page N (2..) to out/<urlBase>/N/index.html.
|
|
276
|
+
// Shared by plain collection pagination and per-term taxonomy pages: the only
|
|
277
|
+
// differences are urlBase and the `extra` context merged onto the collection
|
|
278
|
+
// snapshot (taxonomy pages add activeTerm/activeTaxonomy and a term-filtered
|
|
279
|
+
// items/pageItems). Returns a promise per page.
|
|
280
|
+
function renderPages({ pages, urlBase, collection, extra, pageProps, titleBase, titleAlways, titleFormat, collectionName, collectionData, file, markupOutDir, compileEntryFn, baseURL }) {
|
|
281
|
+
const promises = []
|
|
282
|
+
const totalPages = pages.length
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < totalPages; i++) {
|
|
285
|
+
const pageNumber = i + 1
|
|
286
|
+
const pageUrl = pageNumber === 1 ? urlBase : `${urlBase}/${pageNumber}`
|
|
287
|
+
const nextPage = pageNumber === totalPages ? null : pageNumber + 1
|
|
288
|
+
const nextPageUrl = nextPage === null ? null : `${urlBase}/${nextPage}`
|
|
289
|
+
const prevPage = pageNumber === 1 ? null : pageNumber - 1
|
|
290
|
+
let prevPageUrl = prevPage === null ? null : `${urlBase}/${prevPage}`
|
|
291
|
+
if (prevPage === 1) prevPageUrl = urlBase
|
|
292
|
+
|
|
293
|
+
// Snapshot per-page properties to avoid async mutation
|
|
294
|
+
const pageSnapshot = {
|
|
295
|
+
...collection,
|
|
296
|
+
...extra,
|
|
297
|
+
pageItems: pages[i],
|
|
298
|
+
pageNumber,
|
|
299
|
+
totalPages,
|
|
300
|
+
pageUrl,
|
|
301
|
+
nextPage,
|
|
302
|
+
nextPageUrl,
|
|
303
|
+
prevPage,
|
|
304
|
+
prevPageUrl
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const markupOut = path.resolve(process.cwd(), markupOutDir, pageUrl, 'index.html')
|
|
308
|
+
const fromPath = path.resolve(process.cwd(), markupOutDir)
|
|
309
|
+
const markupOutDirFull = path.dirname(markupOut)
|
|
310
|
+
|
|
311
|
+
const context = {
|
|
312
|
+
...collectionData,
|
|
313
|
+
[collectionName]: pageSnapshot,
|
|
314
|
+
relativePathPrefix: getRelativePathPrefix(markupOutDirFull, fromPath, baseURL),
|
|
315
|
+
// output-relative so page.url matches nav.json/index urls, same as compileDirectory
|
|
316
|
+
_url: getPageUrlRelativeToOutput(markupOut, markupOutDir)
|
|
317
|
+
}
|
|
318
|
+
// extra props stamped onto page.* (compileEntry honors _page) — taxonomy
|
|
319
|
+
// pages use it to feed the breadcrumb a correct term trail, and both paths
|
|
320
|
+
// set a distinct title so <title>/og/jsonld don't duplicate across pages.
|
|
321
|
+
// titleAlways overrides page 1 too (term pages want "Tag: Feature"); plain
|
|
322
|
+
// pagination keeps page 1's front-matter title and only suffixes 2..N.
|
|
323
|
+
const props = { ...pageProps }
|
|
324
|
+
if (titleBase && (titleAlways || pageNumber > 1)) {
|
|
325
|
+
props.title = pageNumber === 1
|
|
326
|
+
? titleBase
|
|
327
|
+
: (titleFormat || '{title} — Page {n}')
|
|
328
|
+
.replace('{title}', titleBase)
|
|
329
|
+
.replace('{n}', pageNumber)
|
|
330
|
+
.replace('{total}', totalPages)
|
|
331
|
+
}
|
|
332
|
+
if (Object.keys(props).length) context._page = props
|
|
333
|
+
|
|
334
|
+
// mkDir only when a page is actually written: no empty pagination dirs
|
|
335
|
+
// for index-less or unpublished (skipped) collection indexes
|
|
336
|
+
promises.push(compileEntryFn(file, context).then(({ result, skipped }) => {
|
|
337
|
+
if (skipped) return
|
|
338
|
+
mkDir(markupOutDirFull)
|
|
339
|
+
// async write so I/O overlaps rendering of the other pages
|
|
340
|
+
return fs.promises.writeFile(markupOut, result)
|
|
341
|
+
}))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return promises
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function generateCollectionPaginationPages(collectionData, markupInDir, markupOutDir, compileEntryFn, baseURL, site = {}) {
|
|
207
348
|
if (!collectionData) return []
|
|
208
349
|
|
|
350
|
+
const titleFormat = site.pagination && site.pagination.title
|
|
209
351
|
const compilePromises = []
|
|
210
352
|
|
|
211
353
|
for (const collectionName of Object.keys(collectionData)) {
|
|
@@ -222,50 +364,106 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
222
364
|
|
|
223
365
|
if (!file) continue
|
|
224
366
|
|
|
225
|
-
for
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
367
|
+
// base for "<title> — Page N" on pages 2..N (page 1 keeps its own title)
|
|
368
|
+
const indexTitle = parseFrontMatter(file).frontMatter.title || humanize(collection.name)
|
|
369
|
+
|
|
370
|
+
compilePromises.push(...renderPages({
|
|
371
|
+
pages: collection.pages,
|
|
372
|
+
urlBase: collection.name,
|
|
373
|
+
collection,
|
|
374
|
+
extra: {},
|
|
375
|
+
titleBase: indexTitle,
|
|
376
|
+
titleFormat,
|
|
377
|
+
collectionName,
|
|
378
|
+
collectionData,
|
|
379
|
+
file,
|
|
380
|
+
markupOutDir,
|
|
381
|
+
compileEntryFn,
|
|
382
|
+
baseURL
|
|
383
|
+
}))
|
|
384
|
+
}
|
|
235
385
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
...collection,
|
|
239
|
-
pageItems: collection.pages[i],
|
|
240
|
-
pageNumber,
|
|
241
|
-
pageUrl,
|
|
242
|
-
nextPage,
|
|
243
|
-
nextPageUrl,
|
|
244
|
-
prevPage,
|
|
245
|
-
prevPageUrl
|
|
246
|
-
}
|
|
386
|
+
return compilePromises
|
|
387
|
+
}
|
|
247
388
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
389
|
+
// Removes stale output under out/<collection>/<tax.path>/: whole term-slug dirs
|
|
390
|
+
// for terms that no longer exist, plus leftover page dirs (2..) inside a
|
|
391
|
+
// surviving term whose page count shrank. Guarded so a real source subdir
|
|
392
|
+
// mirrored at the same path is never deleted.
|
|
393
|
+
export function pruneStaleTaxonomyDirs(collectionName, tax, markupInDir, markupOutDir, terms) {
|
|
394
|
+
const taxDir = path.resolve(process.cwd(), markupOutDir, collectionName, tax.path)
|
|
395
|
+
if (!fs.existsSync(taxDir)) return
|
|
251
396
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// output-relative so page.url matches nav.json/index urls, same as compileDirectory
|
|
257
|
-
_url: getPageUrlRelativeToOutput(markupOut, markupOutDir)
|
|
258
|
-
}
|
|
397
|
+
// never touch a taxonomy path that shadows a real source directory
|
|
398
|
+
if (fs.existsSync(path.resolve(process.cwd(), markupInDir, collectionName, tax.path))) return
|
|
399
|
+
|
|
400
|
+
const bySlug = new Map(terms.map((t) => [t.slug, t]))
|
|
259
401
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
402
|
+
for (const entry of fs.readdirSync(taxDir)) {
|
|
403
|
+
const slugDir = path.join(taxDir, entry)
|
|
404
|
+
if (!fs.statSync(slugDir).isDirectory()) continue
|
|
405
|
+
|
|
406
|
+
const term = bySlug.get(entry)
|
|
407
|
+
if (!term) {
|
|
408
|
+
fs.rmSync(slugDir, { recursive: true, force: true })
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// surviving term: drop page-number dirs beyond its current totalPages
|
|
413
|
+
for (const sub of fs.readdirSync(slugDir)) {
|
|
414
|
+
const pageNumber = parseInt(sub, 10)
|
|
415
|
+
if (String(pageNumber) !== sub || pageNumber < 2 || pageNumber <= term.totalPages) continue
|
|
416
|
+
fs.rmSync(path.join(slugDir, sub), { recursive: true, force: true })
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Renders per-term taxonomy pages (e.g. changelog/tag/release/) using the
|
|
422
|
+
// collection's own index template. Each term page carries the term-filtered
|
|
423
|
+
// items/pageItems and activeTaxonomy/activeTerm so the shared template can show
|
|
424
|
+
// a heading and paginate. buildTaxonomyData must have run first.
|
|
425
|
+
export function generateTaxonomyPages(collectionData, markupInDir, markupOutDir, compileEntryFn, baseURL, site = {}) {
|
|
426
|
+
if (!collectionData) return []
|
|
427
|
+
|
|
428
|
+
const titleFormat = site.pagination && site.pagination.title
|
|
429
|
+
const compilePromises = []
|
|
430
|
+
|
|
431
|
+
for (const collectionName of Object.keys(collectionData)) {
|
|
432
|
+
const collection = collectionData[collectionName]
|
|
433
|
+
if (!collection.taxonomies) continue
|
|
434
|
+
|
|
435
|
+
const file = getCollectionIndexFile(markupInDir, collectionName)
|
|
436
|
+
|
|
437
|
+
for (const tax of collection.taxonomies) {
|
|
438
|
+
// prune before the index check so removed tags are cleaned even if the
|
|
439
|
+
// index template was deleted
|
|
440
|
+
pruneStaleTaxonomyDirs(collectionName, tax, markupInDir, markupOutDir, file ? tax.terms : [])
|
|
441
|
+
if (!file) continue
|
|
442
|
+
|
|
443
|
+
for (const term of tax.terms) {
|
|
444
|
+
compilePromises.push(...renderPages({
|
|
445
|
+
pages: term.pages,
|
|
446
|
+
urlBase: term.url,
|
|
447
|
+
collection,
|
|
448
|
+
// term-scope items so `.items` and `groupby(.items…)` also filter to
|
|
449
|
+
// the term on its page, not the whole collection
|
|
450
|
+
extra: { items: term.items, activeTaxonomy: tax.path, activeTerm: term.term, activeTermSlug: term.slug },
|
|
451
|
+
// breadcrumb hint: Home > collection > term (the tag/ path segment has
|
|
452
|
+
// no page, and page.title is the index's, not the term's)
|
|
453
|
+
pageProps: { _taxonomy: { collection: collectionName, path: tax.path, term: term.term, termUrl: term.url } },
|
|
454
|
+
// distinct <title>/og/jsonld per term ("Tag: Feature"), not the shared
|
|
455
|
+
// index title — matches the breadcrumb crumb label
|
|
456
|
+
titleBase: `${humanize(tax.path)}: ${humanize(term.term)}`,
|
|
457
|
+
titleAlways: true,
|
|
458
|
+
titleFormat,
|
|
459
|
+
collectionName,
|
|
460
|
+
collectionData,
|
|
461
|
+
file,
|
|
462
|
+
markupOutDir,
|
|
463
|
+
compileEntryFn,
|
|
464
|
+
baseURL
|
|
465
|
+
}))
|
|
466
|
+
}
|
|
269
467
|
}
|
|
270
468
|
}
|
|
271
469
|
|
|
@@ -3,9 +3,9 @@ import path from 'node:path'
|
|
|
3
3
|
import { Liquid } from 'liquidjs'
|
|
4
4
|
import { highlightCode } from '../highlight.js'
|
|
5
5
|
import { marked, renderMarkdownCached } from '../renderer.js'
|
|
6
|
-
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc } from '../helpers.js'
|
|
6
|
+
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
|
|
7
7
|
import { getImageExif, listImages } from '../image-cache.js'
|
|
8
|
-
import { slugify } from 'book-of-spells'
|
|
8
|
+
import { slugify, humanize } from 'book-of-spells'
|
|
9
9
|
import dayjs from 'dayjs'
|
|
10
10
|
|
|
11
11
|
// liquidjs parse-cache keys: "<lookupType>:<name>" (root/layouts/partials)
|
|
@@ -76,6 +76,7 @@ export default class LiquidEngine {
|
|
|
76
76
|
registerFilters({ timeDateFormat, markupOut }) {
|
|
77
77
|
const engine = this.engine
|
|
78
78
|
engine.registerFilter('slugify', (str) => slugify(str))
|
|
79
|
+
engine.registerFilter('humanize', (str) => humanize(str))
|
|
79
80
|
engine.registerFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
80
81
|
engine.registerFilter('markdown', (str) => marked.parse(String(str || '')))
|
|
81
82
|
engine.registerFilter('toc', (html) => renderToc(String(html || '')))
|
|
@@ -115,6 +116,10 @@ export default class LiquidEngine {
|
|
|
115
116
|
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
116
117
|
return listImages(dirPath, outputDir)
|
|
117
118
|
})
|
|
119
|
+
engine.registerFilter('jsonld', (page, site) => buildJsonLd(page, site))
|
|
120
|
+
engine.registerFilter('og', (page, site) => buildOpenGraph(page, site))
|
|
121
|
+
engine.registerFilter('canonical', (page, site) => buildCanonical(page, site))
|
|
122
|
+
engine.registerFilter('breadcrumb', (page, site, prefix) => buildBreadcrumb(page, site, prefix))
|
|
118
123
|
engine.registerFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
119
124
|
engine.registerFilter('highlight', (code, lang) => {
|
|
120
125
|
const highlighted = highlightCode(code, lang)
|
|
@@ -289,7 +294,8 @@ function registerPaginationTag(engine) {
|
|
|
289
294
|
* render(ctx) {
|
|
290
295
|
const pagination = yield this.liquid._evalValue(this.value, ctx)
|
|
291
296
|
const prefix = (yield ctx.get(['relativePathPrefix'])) || ''
|
|
292
|
-
|
|
297
|
+
const site = (yield ctx.get(['site'])) || {}
|
|
298
|
+
return buildPaginationTag(pagination, prefix, site.pagination)
|
|
293
299
|
}
|
|
294
300
|
})
|
|
295
301
|
}
|
|
@@ -2,12 +2,12 @@ import fs from 'node:fs'
|
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
3
|
import nunjucks from 'nunjucks'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc } from '../helpers.js'
|
|
5
|
+
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc, buildJsonLd, buildOpenGraph, buildCanonical, buildBreadcrumb } from '../helpers.js'
|
|
6
6
|
import { getImageExif, listImages } from '../image-cache.js'
|
|
7
7
|
import { toPosix } from '../../utils/helpers.js'
|
|
8
8
|
import { highlightCode } from '../highlight.js'
|
|
9
9
|
import { marked, renderMarkdownCached } from '../renderer.js'
|
|
10
|
-
import { slugify } from 'book-of-spells'
|
|
10
|
+
import { slugify, humanize } from 'book-of-spells'
|
|
11
11
|
import dayjs from 'dayjs'
|
|
12
12
|
import log from '../../utils/log.js'
|
|
13
13
|
|
|
@@ -133,6 +133,7 @@ export default class NunjucksEngine {
|
|
|
133
133
|
registerFilters({ timeDateFormat, markupOut }) {
|
|
134
134
|
const env = this.env
|
|
135
135
|
env.addFilter('slugify', slugify)
|
|
136
|
+
env.addFilter('humanize', humanize)
|
|
136
137
|
env.addFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
137
138
|
env.addFilter('markdown', (str) => marked.parse(String(str || '')))
|
|
138
139
|
env.addFilter('toc', (html) => {
|
|
@@ -177,6 +178,10 @@ export default class NunjucksEngine {
|
|
|
177
178
|
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
178
179
|
return listImages(dirPath, outputDir)
|
|
179
180
|
})
|
|
181
|
+
env.addFilter('jsonld', (page, site) => new nunjucks.runtime.SafeString(buildJsonLd(page, site)))
|
|
182
|
+
env.addFilter('og', (page, site) => new nunjucks.runtime.SafeString(buildOpenGraph(page, site)))
|
|
183
|
+
env.addFilter('canonical', (page, site) => new nunjucks.runtime.SafeString(buildCanonical(page, site)))
|
|
184
|
+
env.addFilter('breadcrumb', (page, site, prefix) => new nunjucks.runtime.SafeString(buildBreadcrumb(page, site, prefix)))
|
|
180
185
|
env.addFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
181
186
|
env.addFilter('highlight', (code, lang) => {
|
|
182
187
|
const highlighted = highlightCode(code, lang)
|
|
@@ -337,6 +342,7 @@ export class PaginationExtension {
|
|
|
337
342
|
|
|
338
343
|
run(context, pagination) {
|
|
339
344
|
const prefix = context.lookup('relativePathPrefix') || ''
|
|
340
|
-
|
|
345
|
+
const site = context.lookup('site') || {}
|
|
346
|
+
return new nunjucks.runtime.SafeString(buildPaginationTag(pagination, prefix, site.pagination))
|
|
341
347
|
}
|
|
342
348
|
}
|