@slybridges/kiss 0.9.5 → 0.10.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/.claude/settings.local.json +10 -0
- package/CLAUDE.md +127 -0
- package/README.md +6 -5
- package/TODO.md +5 -0
- package/package.json +1 -1
- package/src/build.js +21 -0
- package/src/config/defaultConfig.js +3 -0
- package/src/helpers.js +119 -19
- package/src/indexing/buildPageIndexes.js +73 -0
- package/src/indexing/index.js +35 -0
- package/src/indexing/lookupHelpers.js +114 -0
- package/src/logger.js +29 -2
- package/src/transforms/atAttributesContentTransform.js +180 -115
- package/src/transforms/imageContextTransform.js +285 -137
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
KISS (Keep It Simple and Static) is a low-tech static site generator built with Node.js. It transforms markdown, HTML, JSON, and JavaScript content into static websites using Nunjucks templates.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
### Build the site
|
|
12
|
+
```bash
|
|
13
|
+
npx kiss build
|
|
14
|
+
# Production build:
|
|
15
|
+
NODE_ENV=production npx kiss build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Development server with auto-reload
|
|
19
|
+
```bash
|
|
20
|
+
npx kiss start
|
|
21
|
+
# Launches build, watches for changes, and auto-reloads browser
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Watch mode only (no server)
|
|
25
|
+
```bash
|
|
26
|
+
npx kiss watch
|
|
27
|
+
# Use --incremental flag for experimental incremental builds
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Serve only (no watch)
|
|
31
|
+
```bash
|
|
32
|
+
npx kiss serve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Linting and formatting
|
|
36
|
+
```bash
|
|
37
|
+
npx eslint .
|
|
38
|
+
npx prettier --write .
|
|
39
|
+
# Note: Prettier config uses no semicolons
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
### Build Pipeline
|
|
45
|
+
|
|
46
|
+
The build process (`src/build.js`) follows this lifecycle:
|
|
47
|
+
|
|
48
|
+
1. **Config Loading**: Loads `kiss.config.js` with defaults from `src/config/defaultConfig.js`
|
|
49
|
+
2. **Content Loading**: Scans `content/` directory recursively, loading files via loaders
|
|
50
|
+
3. **Data Cascade**: Parent data cascades to children, with dynamic computations at each level
|
|
51
|
+
4. **Content Transformation**: Applies transforms (Nunjucks, @attributes, images)
|
|
52
|
+
5. **Data Views**: Computes derived data (collections, categories, site metadata)
|
|
53
|
+
6. **Writing**: Outputs HTML, optimized images, RSS, sitemap, and JSON context
|
|
54
|
+
|
|
55
|
+
### Content Organization
|
|
56
|
+
|
|
57
|
+
- Content lives in `content/` directory
|
|
58
|
+
- Folder structure maps to URL structure
|
|
59
|
+
- Index files (`index.js/md/html`) provide data cascade to children
|
|
60
|
+
- Single files or directories with `post.md/html` become pages
|
|
61
|
+
|
|
62
|
+
### Key Data Flow
|
|
63
|
+
|
|
64
|
+
Pages have a data structure with:
|
|
65
|
+
- User content fields (title, description, created, etc.)
|
|
66
|
+
- Computed fields via dynamic functions (permalink, URL, image optimization)
|
|
67
|
+
- `_meta` object with internal metadata (id, parent, children, paths)
|
|
68
|
+
- `_html` populated by content transformers
|
|
69
|
+
|
|
70
|
+
### Dynamic Computations
|
|
71
|
+
|
|
72
|
+
Located in `src/data/`, these functions run during data cascade to compute values:
|
|
73
|
+
- `computePermalink`: Generates URL path from file location
|
|
74
|
+
- `computeTitle`: Auto-generates from filename if not provided
|
|
75
|
+
- `computeImage`: Handles responsive image generation
|
|
76
|
+
- `computeDescription`: Extracts from content if not provided
|
|
77
|
+
|
|
78
|
+
### Content Loaders
|
|
79
|
+
|
|
80
|
+
Located in `src/loaders/`:
|
|
81
|
+
- `markdownLoader`: Processes .md files with front-matter
|
|
82
|
+
- `jsLoader`: Executes .js files that export data
|
|
83
|
+
- `jsonLoader`: Loads .json data files
|
|
84
|
+
- `textLoader`: Loads .html and other text files
|
|
85
|
+
- `staticLoader`: Copies static assets as-is
|
|
86
|
+
|
|
87
|
+
### Content Transforms
|
|
88
|
+
|
|
89
|
+
Located in `src/transforms/`:
|
|
90
|
+
- `nunjucksContentTransform`: Processes Nunjucks templates in content
|
|
91
|
+
- `atAttributesContentTransform`: Handles @attribute syntax for dynamic data insertion
|
|
92
|
+
- `imageContextTransform`: Optimizes and creates responsive images
|
|
93
|
+
|
|
94
|
+
### Writers
|
|
95
|
+
|
|
96
|
+
Located in `src/writers/`:
|
|
97
|
+
- `htmlWriter`: Renders pages using Nunjucks templates
|
|
98
|
+
- `imageWriter`: Generates optimized image variants with Sharp
|
|
99
|
+
- `rssContextWriter`: Creates RSS feed
|
|
100
|
+
- `sitemapContextWriter`: Generates sitemap.xml
|
|
101
|
+
- `jsonContextWriter`: Dumps full site context for debugging
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
Main config file is `kiss.config.js`. Key settings:
|
|
106
|
+
|
|
107
|
+
- `context.site`: Site-wide metadata (url, title, description)
|
|
108
|
+
- `dirs`: Directory paths (content, public, theme, templates)
|
|
109
|
+
- `hooks`: Extensibility points for custom loaders, transforms, writers
|
|
110
|
+
- `defaults`: Default values and behaviors
|
|
111
|
+
- `dataViews`: Functions to compute derived data
|
|
112
|
+
|
|
113
|
+
## Template System
|
|
114
|
+
|
|
115
|
+
Uses Nunjucks templates in `theme/templates/`:
|
|
116
|
+
- Templates have access to full page data and site context
|
|
117
|
+
- Default templates: `default.njk`, `collection.njk`, `post.njk`
|
|
118
|
+
- Custom filters loaded via `src/libs/loadNunjucksFilters.js`
|
|
119
|
+
|
|
120
|
+
## Important Conventions
|
|
121
|
+
|
|
122
|
+
- Code style: No semicolons (per Prettier config)
|
|
123
|
+
- Node.js 20+ required
|
|
124
|
+
- Uses CommonJS modules (require/module.exports)
|
|
125
|
+
- Async/await for asynchronous operations
|
|
126
|
+
- Lodash for utility functions
|
|
127
|
+
- Fast-glob for file system operations
|
package/README.md
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
## What kiss is
|
|
13
13
|
|
|
14
14
|
- **Low-tech**: VanillaJS, small codebase, little abstractions.
|
|
15
|
-
- **Minimal**: No framework, no code transpiler,
|
|
16
|
-
- **Batteries included**: Comes out of the box with everything you need to make an SEO
|
|
15
|
+
- **Minimal**: No framework, no code transpiler, few dependencies.
|
|
16
|
+
- **Batteries included**: Comes out of the box with everything you need to make an SEO-friendly website.
|
|
17
|
+
- **Built for performance**: Optimized for large sites with thousands of pages (one site we run has 5,000+ pages and builds under 6 minutes in the cloud and 3 minutes locally).
|
|
17
18
|
- **Developer friendly**: `kiss start` will watch your changes and reload the browser after every build so that you can iterate quickly.
|
|
18
19
|
- **Powerful**: Dynamic data computations, page data cascade and derived content generation.
|
|
19
|
-
- **Extensible**: Easily add support for more content types, dynamic computations, writers, post
|
|
20
|
+
- **Extensible**: Easily add support for more content types, dynamic computations, writers, post-build commands via hooks, etc.
|
|
20
21
|
|
|
21
22
|
## How it works:
|
|
22
23
|
|
|
@@ -28,10 +29,10 @@
|
|
|
28
29
|
- Create custom pages derived from the main data (e.g list of articles by tags or articles by author's)
|
|
29
30
|
- Pre-compute derived data views based (e.g. compute the list of categories and subcategories for generating the navigation bar)
|
|
30
31
|
|
|
31
|
-
kiss will automatically make your site SEO
|
|
32
|
+
kiss will automatically make your site SEO-friendly by default:
|
|
32
33
|
|
|
33
34
|
- Optimize images and make them responsive
|
|
34
|
-
- Data cascade makes it trivial to generate
|
|
35
|
+
- Data cascade makes it trivial to generate different sections of your site with different layouts and data content
|
|
35
36
|
- Generate RSS feed
|
|
36
37
|
- Generate sitemap
|
|
37
38
|
- Generate a dump of your full site as JSON for debug or to implement actions via workers (like site search)
|
package/TODO.md
ADDED
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
getPageFromInputPath,
|
|
13
13
|
relativeToAbsoluteAttributes,
|
|
14
14
|
} = require("./helpers")
|
|
15
|
+
const { buildPageIndexes } = require("./indexing")
|
|
15
16
|
const { baseLoader } = require("./loaders")
|
|
16
17
|
const { setGlobalLogger } = require("./logger")
|
|
17
18
|
|
|
@@ -97,6 +98,26 @@ const build = async (options = {}, lastBuild = {}, version = 0) => {
|
|
|
97
98
|
context = computeAllPagesData(context, config, buildFlags)
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Build indexes for O(1) lookups during transforms after dynamic data is computed
|
|
102
|
+
if (config.defaults.enablePageIndexes) {
|
|
103
|
+
global.logger.info("Building page indexes for fast lookups")
|
|
104
|
+
context._pageIndexes = buildPageIndexes(context.pages)
|
|
105
|
+
global.logger.log(
|
|
106
|
+
`- Built ${Object.keys(context._pageIndexes).length} indexes for ${Object.keys(context.pages).length} pages`,
|
|
107
|
+
)
|
|
108
|
+
} else {
|
|
109
|
+
// Indexes disabled - transforms will use O(n) fallbacks
|
|
110
|
+
// This is supported but not recommended for large sites
|
|
111
|
+
global.logger.info(
|
|
112
|
+
"Page indexes disabled - transforms will use O(n) page searches",
|
|
113
|
+
)
|
|
114
|
+
if (Object.keys(context.pages).length > 1000) {
|
|
115
|
+
global.logger.warn(
|
|
116
|
+
"Consider enabling page indexes for better performance (set config.defaults.enablePageIndexes = true)",
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
if (buildFlags.dataViews) {
|
|
101
122
|
global.logger.section("Computing data views")
|
|
102
123
|
context = computeDataViews(context, config)
|
|
@@ -75,6 +75,9 @@ const defaultConfig = {
|
|
|
75
75
|
// https://moz.com/learn/seo/meta-description
|
|
76
76
|
// This settings only applies to description automatically generated. If you provide your own description, it will be used as is.
|
|
77
77
|
descriptionLength: 160,
|
|
78
|
+
// PERFORMANCE OPTIMIZATION: Enable centralized page indexes for O(1) lookups during transforms
|
|
79
|
+
// Set to false only for extremely memory-constrained environments
|
|
80
|
+
enablePageIndexes: true,
|
|
78
81
|
maxComputingRounds: 10,
|
|
79
82
|
pageData: initialPageData,
|
|
80
83
|
pagePublishedAttribute: "created",
|
package/src/helpers.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const _ = require("lodash")
|
|
2
2
|
const fs = require("fs")
|
|
3
3
|
const path = require("path")
|
|
4
|
+
const {
|
|
5
|
+
findPageByPermalink,
|
|
6
|
+
findPageByDerivative,
|
|
7
|
+
findParentByPermalink,
|
|
8
|
+
} = require("./indexing")
|
|
4
9
|
|
|
5
10
|
// @ attribute format: @<attribute>:<value><terminator>
|
|
6
11
|
// terminators: space, comma, newline, end of string, ', ",<, >, ), ], }, #, \
|
|
@@ -150,13 +155,13 @@ const getFullPath = (pathname, basePath, options = {}) => {
|
|
|
150
155
|
|
|
151
156
|
/** Computes the input path based on the permalink by checking if the parent
|
|
152
157
|
* had a permalink different than their input path */
|
|
153
|
-
const getInputPath = (permalink, pages, baseContentPath) => {
|
|
158
|
+
const getInputPath = (permalink, pages, baseContentPath, indexes) => {
|
|
154
159
|
const pathObject = path.parse(permalink)
|
|
160
|
+
const parentPermalink = pathObject.dir + "/"
|
|
161
|
+
|
|
155
162
|
// search if a have a parent corresponding to this permalink's dir
|
|
156
|
-
const parent =
|
|
157
|
-
|
|
158
|
-
(page) => page.permalink === pathObject.dir + "/",
|
|
159
|
-
)
|
|
163
|
+
const parent = findParentByPermalink(indexes, parentPermalink, pages)
|
|
164
|
+
|
|
160
165
|
if (!parent) {
|
|
161
166
|
// no result: assume inputPath same as permalink
|
|
162
167
|
return path.join(baseContentPath, permalink)
|
|
@@ -185,31 +190,29 @@ const getLocale = (context, sep = "-") => {
|
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
const getPageFromInputPath = (inputPath, pages) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
return page
|
|
193
|
+
// O(n) search - this is only used in build.js where indexes aren't available
|
|
194
|
+
return Object.values(pages).find((page) => {
|
|
195
|
+
return page._meta?.inputSources?.some((source) => source.path === inputPath)
|
|
196
|
+
})
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// Tries to find the page corresponding to the source
|
|
196
|
-
// @attributes should have been resolved by
|
|
200
|
+
// @attributes should have been resolved by now
|
|
197
201
|
// Supports absolute, and relative paths
|
|
198
202
|
const getPageFromSource = (source, parentPage, pages, config, options = {}) => {
|
|
199
203
|
// source may have URL entities encoded. Decode them
|
|
200
204
|
source = decodeURI(source)
|
|
201
|
-
const { throwIfNotFound = true } = options
|
|
205
|
+
const { throwIfNotFound = true, indexes } = options
|
|
202
206
|
// value is a path: compute the permalink in case it is a relative path
|
|
203
207
|
const permalink = getFullPath(source, parentPage.permalink, {
|
|
204
208
|
throwIfInvalid: true,
|
|
205
209
|
})
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
210
|
+
|
|
211
|
+
const page =
|
|
212
|
+
findPageByPermalink(indexes, permalink, pages) ||
|
|
213
|
+
// in incremental builds, we also search in derivatives in case the image source was already replaced during previous build
|
|
214
|
+
findPageByDerivative(indexes, permalink, pages)
|
|
215
|
+
|
|
213
216
|
if (!page) {
|
|
214
217
|
if (throwIfNotFound) {
|
|
215
218
|
throw new Error(
|
|
@@ -355,6 +358,101 @@ const sortPages = (pages, sortBy, { skipUndefinedSort } = {}) => {
|
|
|
355
358
|
return pages
|
|
356
359
|
}
|
|
357
360
|
|
|
361
|
+
// Placeholder constants used by jsonSafeStringify/jsonSafeParse
|
|
362
|
+
// These unique strings replace special JS values that JSON can't handle
|
|
363
|
+
const JSON_PLACEHOLDERS = {
|
|
364
|
+
FUNCTION: "__KISS_FUNCTION__",
|
|
365
|
+
UNDEFINED: "__KISS_UNDEFINED__",
|
|
366
|
+
DATE: "__KISS_DATE__",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Converts a JavaScript object to a JSON string while preserving special values
|
|
371
|
+
* that JSON.stringify normally can't handle (functions, dates, undefined).
|
|
372
|
+
*
|
|
373
|
+
* @param {Object} obj - The object to stringify
|
|
374
|
+
* @returns {Object} { jsonStr: string, specialValues: Map } - The JSON string and a map of special values
|
|
375
|
+
*/
|
|
376
|
+
const jsonSafeStringify = (obj) => {
|
|
377
|
+
const specialValues = new Map()
|
|
378
|
+
|
|
379
|
+
const replacer = (key, value) => {
|
|
380
|
+
// Handle functions - store them and replace with placeholder
|
|
381
|
+
if (typeof value === "function") {
|
|
382
|
+
const id = `${JSON_PLACEHOLDERS.FUNCTION}_${specialValues.size}`
|
|
383
|
+
specialValues.set(id, value)
|
|
384
|
+
return id
|
|
385
|
+
}
|
|
386
|
+
// Handle dates - store them and replace with placeholder
|
|
387
|
+
if (value instanceof Date) {
|
|
388
|
+
const id = `${JSON_PLACEHOLDERS.DATE}_${specialValues.size}`
|
|
389
|
+
specialValues.set(id, value)
|
|
390
|
+
return id
|
|
391
|
+
}
|
|
392
|
+
// Handle undefined - store it and replace with placeholder
|
|
393
|
+
if (value === undefined) {
|
|
394
|
+
const id = `${JSON_PLACEHOLDERS.UNDEFINED}_${specialValues.size}`
|
|
395
|
+
specialValues.set(id, undefined)
|
|
396
|
+
return id
|
|
397
|
+
}
|
|
398
|
+
return value
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const jsonStr = JSON.stringify(obj, replacer)
|
|
402
|
+
return { jsonStr, specialValues }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Parses a JSON string back to an object while restoring special values
|
|
407
|
+
* that were replaced with placeholders during jsonSafeStringify.
|
|
408
|
+
*
|
|
409
|
+
* JSON.parse's reviver function DELETES properties when undefined is returned
|
|
410
|
+
* This causes properties with undefined values to be lost entirely, so we don't use this feature.
|
|
411
|
+
* Instead, We use a two-phase approach to get around this issue:
|
|
412
|
+
* 1. Parse the JSON normally to get object structure
|
|
413
|
+
* 2. Walk the object and restore special values in-place
|
|
414
|
+
* This preserves properties that have undefined values.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} jsonStr - The JSON string to parse
|
|
417
|
+
* @param {Map} specialValues - Map of placeholders to their original values
|
|
418
|
+
* @returns {Object} - The restored JavaScript object with all special values intact
|
|
419
|
+
*/
|
|
420
|
+
const jsonSafeParse = (jsonStr, specialValues) => {
|
|
421
|
+
// First parse normally to get the object structure
|
|
422
|
+
const parsed = JSON.parse(jsonStr)
|
|
423
|
+
|
|
424
|
+
// Then walk through and restore special values
|
|
425
|
+
const restoreSpecialValues = (obj) => {
|
|
426
|
+
if (obj === null || typeof obj !== "object") {
|
|
427
|
+
return obj
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const key in obj) {
|
|
431
|
+
const value = obj[key]
|
|
432
|
+
|
|
433
|
+
if (typeof value === "string") {
|
|
434
|
+
// Check if this is a placeholder that needs restoration
|
|
435
|
+
if (
|
|
436
|
+
value.startsWith(JSON_PLACEHOLDERS.FUNCTION) ||
|
|
437
|
+
value.startsWith(JSON_PLACEHOLDERS.DATE) ||
|
|
438
|
+
value.startsWith(JSON_PLACEHOLDERS.UNDEFINED)
|
|
439
|
+
) {
|
|
440
|
+
if (specialValues.has(value)) {
|
|
441
|
+
obj[key] = specialValues.get(value)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (typeof value === "object" && value !== null) {
|
|
445
|
+
// Recursively process nested objects
|
|
446
|
+
restoreSpecialValues(value)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return obj
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return restoreSpecialValues(parsed)
|
|
454
|
+
}
|
|
455
|
+
|
|
358
456
|
module.exports = {
|
|
359
457
|
AT_FILE_ATTRIBUTE_REGEX,
|
|
360
458
|
AT_GENERIC_ATTRIBUTE_REGEX,
|
|
@@ -373,6 +471,8 @@ module.exports = {
|
|
|
373
471
|
getParentPage,
|
|
374
472
|
isChild,
|
|
375
473
|
isValidURL,
|
|
474
|
+
jsonSafeParse,
|
|
475
|
+
jsonSafeStringify,
|
|
376
476
|
omitDeep,
|
|
377
477
|
relativeToAbsoluteAttributes,
|
|
378
478
|
sortPageIds,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds centralized indexes for O(1) page lookups throughout the build process.
|
|
3
|
+
*
|
|
4
|
+
* FALLBACK SUPPORT:
|
|
5
|
+
* When indexes are disabled (config.defaults.enablePageIndexes = false),
|
|
6
|
+
* all lookups fall back to O(n) searches. This is supported but not recommended
|
|
7
|
+
* for sites with 1000+ pages.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} pages - All pages with resolved data (post-cascade)
|
|
10
|
+
* @returns {Object} Six specialized indexes for different lookup patterns:
|
|
11
|
+
* - byPermalink: page.permalink → page
|
|
12
|
+
* - byInputPath: page._meta.inputPath → page
|
|
13
|
+
* - byIdAndLang: "id:lang" → page
|
|
14
|
+
* - byDerivative: derivative.permalink → page
|
|
15
|
+
* - byParentPermalink: directory permalinks → page
|
|
16
|
+
* - byInputSource: source.path → page
|
|
17
|
+
*/
|
|
18
|
+
const buildPageIndexes = (pages) => {
|
|
19
|
+
const indexes = {
|
|
20
|
+
byPermalink: new Map(),
|
|
21
|
+
byInputPath: new Map(),
|
|
22
|
+
byIdAndLang: new Map(),
|
|
23
|
+
byDerivative: new Map(),
|
|
24
|
+
byParentPermalink: new Map(),
|
|
25
|
+
byInputSource: new Map(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build all indexes in a single pass through pages
|
|
29
|
+
for (const page of Object.values(pages)) {
|
|
30
|
+
// Permalink index
|
|
31
|
+
if (page.permalink) {
|
|
32
|
+
indexes.byPermalink.set(page.permalink, page)
|
|
33
|
+
|
|
34
|
+
// Parent permalink index for directory lookups
|
|
35
|
+
// Used by getInputPath to find parent directories
|
|
36
|
+
if (page.permalink.endsWith("/")) {
|
|
37
|
+
indexes.byParentPermalink.set(page.permalink, page)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// InputPath index - available after content loading
|
|
42
|
+
if (page._meta?.inputPath) {
|
|
43
|
+
indexes.byInputPath.set(page._meta.inputPath, page)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ID and language index for @id resolution
|
|
47
|
+
if (page.id && page.lang) {
|
|
48
|
+
indexes.byIdAndLang.set(`${page.id}:${page.lang}`, page)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Derivatives index for image permalinks
|
|
52
|
+
if (page.derivatives) {
|
|
53
|
+
for (const derivative of page.derivatives) {
|
|
54
|
+
if (derivative.permalink) {
|
|
55
|
+
indexes.byDerivative.set(derivative.permalink, page)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Input sources index for getPageFromInputPath
|
|
61
|
+
if (page._meta?.inputSources) {
|
|
62
|
+
for (const source of page._meta.inputSources) {
|
|
63
|
+
if (source.path) {
|
|
64
|
+
indexes.byInputSource.set(source.path, page)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return indexes
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = buildPageIndexes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized indexing module for O(1) page lookups.
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURE DECISION:
|
|
5
|
+
* On large sites, walking through and finding pages during the transform phase
|
|
6
|
+
* can be a major performance bottleneck. To address this, we build centralized
|
|
7
|
+
* indexes for common lookup patterns (by permalink, inputPath, id+lang, etc.)
|
|
8
|
+
*
|
|
9
|
+
* BENCHMARKS (5000 page site, 200 @attributes each):
|
|
10
|
+
* - No indexes: 5+ minutes
|
|
11
|
+
* - With indexes + helpers: 57 seconds
|
|
12
|
+
* - With indexes + direct access: 41 seconds (28% faster)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const buildPageIndexes = require("./buildPageIndexes")
|
|
16
|
+
const {
|
|
17
|
+
findInIndex,
|
|
18
|
+
findPageByPermalink,
|
|
19
|
+
findPageByInputPath,
|
|
20
|
+
findPageByIdAndLang,
|
|
21
|
+
findPageByDerivative,
|
|
22
|
+
findParentByPermalink,
|
|
23
|
+
findPageByInputSource,
|
|
24
|
+
} = require("./lookupHelpers")
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
buildPageIndexes,
|
|
28
|
+
findInIndex,
|
|
29
|
+
findPageByPermalink,
|
|
30
|
+
findPageByInputPath,
|
|
31
|
+
findPageByIdAndLang,
|
|
32
|
+
findPageByDerivative,
|
|
33
|
+
findParentByPermalink,
|
|
34
|
+
findPageByInputSource,
|
|
35
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for efficient index-based lookups with O(n) fallback.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT PERFORMANCE NOTE:
|
|
5
|
+
* These helpers add ~0.001ms overhead per call due to function invocation.
|
|
6
|
+
* For hot paths with 1M+ calls (like @attribute resolution), use direct Map.get() instead.
|
|
7
|
+
* Benchmark data: 41 seconds with direct access vs 57 seconds with these helpers (40% slower).
|
|
8
|
+
*
|
|
9
|
+
* WHEN TO USE THESE HELPERS:
|
|
10
|
+
* - One-off lookups during page processing
|
|
11
|
+
* - Non-performance-critical paths
|
|
12
|
+
* - When code clarity is more important than microsecond optimization
|
|
13
|
+
*
|
|
14
|
+
* WHEN NOT TO USE:
|
|
15
|
+
* - Inside tight loops processing thousands of items
|
|
16
|
+
* - @attribute resolution (uses inline helpers instead)
|
|
17
|
+
* - Any path that executes more than 10,000 times per build
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generic lookup with index (O(1)) and fallback (O(n)) support.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} indexes - The indexes object containing Maps for lookups
|
|
24
|
+
* @param {string} indexName - Name of the index to use (e.g., 'byPermalink')
|
|
25
|
+
* @param {*} key - The key to look up in the index
|
|
26
|
+
* @param {Function} fallbackFn - Optional fallback function for O(n) search
|
|
27
|
+
* @returns {*} The found item or null
|
|
28
|
+
*/
|
|
29
|
+
const findInIndex = (indexes, indexName, key, fallbackFn) => {
|
|
30
|
+
// Try index first (O(1))
|
|
31
|
+
if (indexes && indexes[indexName]) {
|
|
32
|
+
const result = indexes[indexName].get(key)
|
|
33
|
+
if (result) {
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fall back to O(n) search if provided
|
|
39
|
+
// This is expected during dynamic data cascade before indexes are built
|
|
40
|
+
if (fallbackFn) {
|
|
41
|
+
return fallbackFn()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find page by permalink using index or fallback.
|
|
49
|
+
*/
|
|
50
|
+
const findPageByPermalink = (indexes, permalink, pages) => {
|
|
51
|
+
return findInIndex(indexes, "byPermalink", permalink, () =>
|
|
52
|
+
Object.values(pages).find((p) => p.permalink === permalink),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find page by input path using index or fallback.
|
|
58
|
+
*/
|
|
59
|
+
const findPageByInputPath = (indexes, inputPath, pages) => {
|
|
60
|
+
return findInIndex(indexes, "byInputPath", inputPath, () =>
|
|
61
|
+
Object.values(pages).find((p) => p._meta?.inputPath === inputPath),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find page by ID and language using index or fallback.
|
|
67
|
+
*/
|
|
68
|
+
const findPageByIdAndLang = (indexes, id, lang, pages) => {
|
|
69
|
+
const key = `${id}:${lang}`
|
|
70
|
+
return findInIndex(indexes, "byIdAndLang", key, () =>
|
|
71
|
+
Object.values(pages).find((p) => p.id === id && p.lang === lang),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find page by derivative permalink using index or fallback.
|
|
77
|
+
*/
|
|
78
|
+
const findPageByDerivative = (indexes, derivativePermalink, pages) => {
|
|
79
|
+
return findInIndex(indexes, "byDerivative", derivativePermalink, () =>
|
|
80
|
+
Object.values(pages).find((p) =>
|
|
81
|
+
p.derivatives?.find((d) => d.permalink === derivativePermalink),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find parent page by directory permalink using index or fallback.
|
|
88
|
+
*/
|
|
89
|
+
const findParentByPermalink = (indexes, parentPermalink, pages) => {
|
|
90
|
+
return findInIndex(indexes, "byParentPermalink", parentPermalink, () =>
|
|
91
|
+
Object.values(pages).find((p) => p.permalink === parentPermalink),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find page by input source path using index or fallback.
|
|
97
|
+
*/
|
|
98
|
+
const findPageByInputSource = (indexes, inputSourcePath, pages) => {
|
|
99
|
+
return findInIndex(indexes, "byInputSource", inputSourcePath, () =>
|
|
100
|
+
Object.values(pages).find((p) =>
|
|
101
|
+
p._meta?.inputSources?.some((s) => s.path === inputSourcePath),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
findInIndex,
|
|
108
|
+
findPageByPermalink,
|
|
109
|
+
findPageByInputPath,
|
|
110
|
+
findPageByIdAndLang,
|
|
111
|
+
findPageByDerivative,
|
|
112
|
+
findParentByPermalink,
|
|
113
|
+
findPageByInputSource,
|
|
114
|
+
}
|