@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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npx eslint:*)",
5
+ "Bash(grep:*)"
6
+ ],
7
+ "deny": [],
8
+ "ask": []
9
+ }
10
+ }
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, little dependencies.
16
- - **Batteries included**: Comes out of the box with everything you need to make an SEO friendly website
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 build commands via hooks, etc.
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 friendly by default:
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 meta and Open Graph tags
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
@@ -0,0 +1,5 @@
1
+ TODO
2
+ - review code
3
+ - try to get read of findBy duplication in atAttributesContentTransform.js
4
+ - test deployment on cloudflare pages
5
+ - test batch writes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slybridges/kiss",
3
- "version": "0.9.5",
3
+ "version": "0.10.0",
4
4
  "description": "Keep It Simple and Static site generator",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 = _.find(
157
- pages,
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
- const pageValues = Object.values(pages)
189
- let page = pageValues.find((p) =>
190
- p._meta.inputSources.map((s) => s.path).includes(inputPath),
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 mow
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
- const page = Object.values(pages).find(
207
- (p) =>
208
- p.permalink === permalink ||
209
- // in incremental builds,
210
- // also search in derivatives in case the image source was already replaced during previous build
211
- p.derivatives?.find((d) => d.permalink === permalink),
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
+ }