@apleasantview/eleventy-plugin-baseline 0.1.0-next.40 → 0.1.0-next.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -23
  2. package/core/back-compat/options.js +69 -0
  3. package/core/content-graph/backlinks.js +65 -0
  4. package/core/content-graph/extractors.js +140 -0
  5. package/core/content-graph/graph.js +118 -0
  6. package/core/content-graph/index.js +2 -0
  7. package/core/content-graph/prepass.js +121 -0
  8. package/core/logging/banner.js +49 -0
  9. package/core/{logging.js → logging/index.js} +19 -2
  10. package/core/logging/quips.js +30 -0
  11. package/core/markdown/auto-heading-ids.js +86 -0
  12. package/core/markdown/index.js +5 -0
  13. package/core/markdown/safe-use.js +42 -0
  14. package/core/{wikilinks.js → markdown/wikilinks.js} +3 -3
  15. package/core/page-context/build.js +239 -0
  16. package/core/page-context/index.js +1 -0
  17. package/core/page-context/register.js +73 -0
  18. package/core/page-context/seo-helpers.js +56 -0
  19. package/core/schema.js +19 -1
  20. package/core/slug-index.js +2 -2
  21. package/core/state.js +73 -0
  22. package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
  23. package/core/surface/index.js +22 -0
  24. package/core/utils/add-trailing-slash.js +11 -0
  25. package/core/utils/ensure-dot-slash-dir.js +13 -0
  26. package/core/utils/normalize-languages.js +28 -0
  27. package/core/utils/resolve-field.js +9 -0
  28. package/core/utils/resolve-subdir.js +20 -0
  29. package/core/utils/slugify.js +15 -0
  30. package/core/utils/unique-by.js +25 -0
  31. package/core/virtual-dir.js +11 -10
  32. package/index.js +152 -115
  33. package/modules/assets/index.js +4 -2
  34. package/modules/assets/processors/esbuild-process.js +2 -2
  35. package/modules/assets/processors/postcss-process.js +2 -2
  36. package/modules/head/drivers/posthtml-head-elements.js +1 -3
  37. package/modules/head/index.js +7 -10
  38. package/modules/multilang/index.js +4 -2
  39. package/modules/navigator/index.js +33 -20
  40. package/modules/navigator/templates/navigator-core.html +1 -1
  41. package/modules/sitemap/index.js +7 -3
  42. package/package.json +4 -2
  43. package/core/filters/index.js +0 -4
  44. package/core/global-functions/index.js +0 -6
  45. package/core/page-context.js +0 -310
  46. package/core/shortcodes/index.js +0 -2
  47. package/core/utils/helpers.js +0 -75
  48. /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
  49. /package/core/{filters → surface/filters}/isString.js +0 -0
  50. /package/core/{filters → surface/filters}/related-posts.js +0 -0
  51. /package/core/{global-functions/date.js → surface/global-date-function.js} +0 -0
package/index.js CHANGED
@@ -3,36 +3,40 @@ import { createRequire } from 'node:module';
3
3
 
4
4
  import { HtmlBasePlugin } from '@11ty/eleventy';
5
5
  import { eleventyImageOnRequestDuringServePlugin } from '@11ty/eleventy-img';
6
+ import markdownItAttrs from 'markdown-it-attrs';
6
7
 
7
- import { createLogger } from './core/logging.js';
8
+ import { createLogger, printBannerOnce } from './core/logging/index.js';
9
+ import { isLegacyShape, normalizeLegacyShape } from './core/back-compat/options.js';
10
+ import { settingsSchema } from './core/schema.js';
11
+ import { deriveBaselineState } from './core/state.js';
12
+ import { runPrepass, PREPASS_SENTINEL } from './core/content-graph/index.js';
13
+ import { registerVirtualDir } from './core/virtual-dir.js';
8
14
  import { createContentMapStore } from './core/content-map-store.js';
9
15
  import { createTranslationMapStore } from './core/translation-map-store.js';
10
16
  import { createSlugIndex } from './core/slug-index.js';
11
- import { registerVirtualDir } from './core/virtual-dir.js';
12
- import { registerPageContext } from './core/page-context.js';
13
- import { wikilinks } from './core/wikilinks.js';
14
- import { settingsSchema } from './core/schema.js';
15
-
16
- import { registerGlobals } from './core/global-functions/index.js';
17
- import { markdownFilter, relatedPostsFilter, isStringFilter } from './core/filters/index.js';
18
- import { imageShortcode } from './core/shortcodes/index.js';
17
+ import { registerPageContext } from './core/page-context/index.js';
18
+ import { autoHeadingIds, safeUse, wikilinks } from './core/markdown/index.js';
19
+ import { slugify } from './core/utils/slugify.js';
19
20
  import { assetsCore, headCore, multilangCore, navigatorCore, sitemapCore } from './modules.js';
21
+ import {
22
+ registerGlobals,
23
+ markdownFilter,
24
+ relatedPostsFilter,
25
+ isStringFilter,
26
+ imageShortcode
27
+ } from './core/surface/index.js';
20
28
 
21
29
  const __require = createRequire(import.meta.url);
22
30
  const { name, version } = __require('./package.json');
31
+ const eleventyVersion = process.env.ELEVENTY_VERSION;
32
+ // const absoluteRoot = process.env.ELEVENTY_ROOT; -> Safekeeping.
23
33
 
24
34
  const mode = process.env.ELEVENTY_ENV;
35
+ // eslint-disable-next-line no-unused-vars
25
36
  const isDev = mode === 'development';
37
+ // eslint-disable-next-line no-unused-vars
26
38
  const isProd = mode === 'production';
27
39
 
28
- const LEGACY_OPTION_KEYS = [
29
- 'verbose',
30
- 'enableNavigatorTemplate',
31
- 'enableSitemapTemplate',
32
- 'assetsESBuild',
33
- 'multilingual'
34
- ];
35
-
36
40
  // Whitelist of reserved global data keys used internally across the plugin.
37
41
  // Positive side effect is they all get listed in order and merge data to the same key.
38
42
  // Also prevents name collision with filters.
@@ -44,37 +48,19 @@ const INTERNAL_KEYS = [
44
48
  '_navigator',
45
49
  '_sitemap',
46
50
  '_snapshot',
47
- '_pageContext'
51
+ 'eleventyComputed._pageContext',
52
+ 'eleventyComputed._node',
53
+ 'eleventyComputed._edges',
54
+ 'eleventyComputed._backlinks',
55
+ 'eleventyComputed._outgoing'
48
56
  ];
49
57
 
50
- /**
51
- * Detect legacy single-object plugin invocation.
52
- *
53
- * The original plugin API accepted a single merged configuration object.
54
- * This helper detects that shape and enables safe normalization into
55
- * the current (settings, options) contract.
56
- *
57
- * NOTE: arguments.length is required because default parameters mask arity.
58
- */
59
- function looksLikeLegacyOptions(firstArg, argsLength) {
60
- if (argsLength >= 2) return false;
61
- if (!firstArg || typeof firstArg !== 'object') return false;
62
- return LEGACY_OPTION_KEYS.some((key) => key in firstArg);
63
- }
58
+ // Base logger outputs regardless of options.
59
+ const baseLog = createLogger(null, { verbose: true });
64
60
 
65
- /**
66
- * Normalize legacy plugin input into the current structured contract.
67
- *
68
- * - settings → site identity (content + SEO concerns)
69
- * - options → runtime behavior flags
70
- */
71
- function splitLegacyOptions(legacy) {
72
- const { defaultLanguage, languages, ...rest } = legacy;
73
- return {
74
- settings: { defaultLanguage, languages },
75
- options: rest
76
- };
77
- }
61
+ printBannerOnce(baseLog, { version, eleventyVersion });
62
+
63
+ let contentGraph = null;
78
64
 
79
65
  /**
80
66
  * Baseline (composition root)
@@ -118,36 +104,29 @@ function splitLegacyOptions(legacy) {
118
104
  */
119
105
  export default function baseline(settings = {}, options = {}) {
120
106
  // --- Legacy compatibility layer ---
121
- const argsLength = arguments.length;
122
- const wasLegacy = looksLikeLegacyOptions(settings, argsLength);
123
-
124
- if (wasLegacy) {
125
- const split = splitLegacyOptions(settings);
126
- settings = split.settings;
127
- options = split.options;
128
- }
129
-
130
- // Base logger outputs regardless of options.
131
- const baseLog = createLogger(null, { verbose: true });
132
-
133
- // Scoped logging.
134
- function scopedLog(name) {
135
- return createLogger(name, { verbose: options.verbose });
136
- }
137
-
138
- if (wasLegacy) {
139
- baseLog.info('DEPRECATED: single-object plugin arg. Use baseline(settings, options) instead.');
107
+ if (isLegacyShape(settings, arguments.length)) {
108
+ const normalized = normalizeLegacyShape(settings);
109
+ settings = normalized.settings;
110
+ options = normalized.options;
111
+ baseLog.info('Single-object plugin arg is deprecated. Use baseline(settings, options).');
140
112
  }
141
113
 
142
114
  // Validate configuration shape (non-fatal).
143
115
  const parsed = settingsSchema.safeParse(settings);
144
116
  if (!parsed.success) {
145
117
  for (const issue of parsed.error.issues) {
146
- baseLog.info('settings:', `${issue.path.join('.')} ${issue.message}`);
118
+ baseLog.info('settings:', `${issue.path.join('.')}, ${issue.message}`);
147
119
  }
148
120
  }
149
121
 
150
- baseLog.info('Eleventy Baseline', version);
122
+ // Resolve state once, above the closure. Pure; no eleventyConfig.
123
+ const state = deriveBaselineState(settings, options, { mode });
124
+ baseLog.info('Settings and options resolved, modules loaded');
125
+
126
+ // Scoped logging.
127
+ function scopedLog(name) {
128
+ return createLogger(name, { verbose: state.options.verbose });
129
+ }
151
130
 
152
131
  /**
153
132
  * Eleventy plugin initializer.
@@ -160,26 +139,69 @@ export default function baseline(settings = {}, options = {}) {
160
139
  try {
161
140
  eleventyConfig.versionCheck('>=3.0');
162
141
  } catch (e) {
163
- baseLog.error('Eleventy version mismatch:', e.message);
142
+ baseLog.error('Eleventy version mismatch.', e.message);
143
+ }
144
+
145
+ // --- Pre-pass wiring ---
146
+ // One mechanic: the pre-pass runs at the start of every Eleventy
147
+ // build cycle via `eleventy.before`. Initial build, watch rebuild,
148
+ // production build — all the same path. Templates always render
149
+ // against a graph rebuilt from current source. The sentinel keeps
150
+ // the inner Eleventy from re-attaching the hook on re-entry.
151
+ if (process.env[PREPASS_SENTINEL] !== '1') {
152
+ const prepassLog = scopedLog('pre-pass');
153
+
154
+ // Origins HtmlBasePlugin may have rewritten internal hrefs to.
155
+ // Stripped during link extraction so backlinks key on path-only.
156
+ const knownOrigins = new Set(['http://localhost:8080']);
157
+ for (const candidate of [settings.url, process.env.URL]) {
158
+ if (!candidate) continue;
159
+ try {
160
+ knownOrigins.add(new URL(candidate).origin);
161
+ } catch {
162
+ prepassLog.info('No known origins, using localhost only');
163
+ }
164
+ }
165
+
166
+ eleventyConfig.on('eleventy.before', async () => {
167
+ contentGraph = await runPrepass(
168
+ eleventyConfig.directories?.input,
169
+ eleventyConfig.directories?.output,
170
+ scopedLog,
171
+ { quietMode: true, knownOrigins }
172
+ );
173
+ });
164
174
  }
165
175
 
166
176
  INTERNAL_KEYS.forEach((key) => {
177
+ // We leave eleventyComputed callback kEys alone, the rest are reserved-empty.
178
+ if (
179
+ key === 'eleventyComputed._pageContext' ||
180
+ key === 'eleventyComputed._node' ||
181
+ key === 'eleventyComputed._edges' ||
182
+ key === 'eleventyComputed._backlinks' ||
183
+ key === 'eleventyComputed._outgoing'
184
+ )
185
+ return;
167
186
  eleventyConfig.addGlobalData(key, {});
168
187
  });
169
188
 
170
189
  const env = {
171
- name: 'Eleventy Baseline',
172
- package: name,
173
190
  version,
174
- mode
191
+ name: 'Eleventy Baseline',
192
+ env: {
193
+ mode,
194
+ package: name
195
+ }
175
196
  };
176
197
 
177
198
  eleventyConfig.addGlobalData('_baseline', {
178
- env
199
+ ...env,
200
+ options: state.options
179
201
  });
180
202
 
181
203
  if (!settings.url) {
182
- baseLog.warn('settings.url missing canonical URLs will be relative');
204
+ baseLog.warn('settings.url missing, canonical URLs will be relative');
183
205
  }
184
206
 
185
207
  registerGlobals(eleventyConfig);
@@ -188,38 +210,12 @@ export default function baseline(settings = {}, options = {}) {
188
210
  baseHref: process.env.URL || eleventyConfig.pathPrefix
189
211
  });
190
212
 
191
- // --- State layer (authoritative configuration) ---
213
+ // --- Feature exposure to templates ---
192
214
  const hasImageTransformPlugin = eleventyConfig.hasPlugin('eleventyImageTransformPlugin');
193
215
 
194
- const state = {
195
- settings: {
196
- title: settings.title,
197
- tagline: settings.tagline,
198
- url: settings.url,
199
- noindex: settings.noindex ?? false,
200
- defaultLanguage: settings.defaultLanguage,
201
- languages: settings.languages,
202
- head: settings.head
203
- },
204
-
205
- options: {
206
- verbose: options.verbose ?? false,
207
- multilang: options.multilingual ?? false,
208
- sitemap: options.sitemap ?? options.enableSitemapTemplate ?? true,
209
- navigator: options.navigator ?? options.enableNavigatorTemplate ?? isDev,
210
- head: {
211
- titleSeparator: options.head?.titleSeparator,
212
- showGenerator: options.head?.showGenerator
213
- },
214
- assets: {
215
- esbuild: options.assets?.esbuild ?? options.assetsESBuild ?? {}
216
- }
217
- }
218
- };
219
-
220
216
  eleventyConfig.addGlobalData('_baseline', {
221
217
  features: {
222
- ...state.options,
218
+ ...state.features,
223
219
  hasImageTransformPlugin
224
220
  }
225
221
  });
@@ -234,6 +230,9 @@ export default function baseline(settings = {}, options = {}) {
234
230
  outputDir: ''
235
231
  });
236
232
 
233
+ const virtualDirLog = scopedLog('virtual-dir');
234
+ virtualDirLog.info('Virtual directories mounted');
235
+
237
236
  const directories = {
238
237
  input: eleventyConfig.directories?.input,
239
238
  output: eleventyConfig.directories?.output,
@@ -277,6 +276,9 @@ export default function baseline(settings = {}, options = {}) {
277
276
  get contentMap() {
278
277
  return contentMapStore.get();
279
278
  },
279
+ get contentGraph() {
280
+ return contentGraph;
281
+ },
280
282
  translationMap: translationMapStore,
281
283
  slugIndex
282
284
  },
@@ -287,34 +289,68 @@ export default function baseline(settings = {}, options = {}) {
287
289
  // Page context registry
288
290
  const pageContextRegistry = registerPageContext(eleventyConfig, coreContext);
289
291
 
290
- // Wikilinks: [[slug]] / [[slug | lang]] in body markdown.
292
+ // --- Content graph ---
293
+ // Cascade hookup for the content graph. Reads via the runtime getter so
294
+ // serve-mode rebuilds reassigning `contentGraph` are picked up.
295
+ function getNode(pageUrl) {
296
+ return coreContext.runtime.contentGraph?.nodes?.[pageUrl];
297
+ }
298
+
299
+ function getEdges() {
300
+ return coreContext.runtime.contentGraph?.edges ?? [];
301
+ }
302
+
303
+ eleventyConfig.addGlobalData('eleventyComputed._node', () => (data) => {
304
+ const pageUrl = data.page?.url;
305
+ if (!pageUrl) return undefined;
306
+
307
+ return getNode(pageUrl);
308
+ });
309
+
310
+ eleventyConfig.addGlobalData('eleventyComputed._backlinks', () => (data) => {
311
+ const edges = getEdges();
312
+
313
+ const pageUrl = data.page?.url;
314
+ if (!pageUrl) return [];
315
+
316
+ return edges.filter((edge) => edge.to === pageUrl);
317
+ });
318
+
319
+ eleventyConfig.addGlobalData('eleventyComputed._outgoing', () => (data) => {
320
+ const edges = getEdges();
321
+
322
+ const pageUrl = data.page?.url;
323
+ if (!pageUrl) return [];
324
+
325
+ return edges.filter((edge) => edge.from === pageUrl);
326
+ });
327
+
328
+ // --- Markdown engine ---
329
+ // Order matters: attrs first so manual ids are visible to auto-heading-ids'
330
+ // seed pass; wikilinks last since it parses inline tokens independently.
331
+ const mdLog = scopedLog('markdown');
291
332
  eleventyConfig.amendLibrary('md', (md) => {
292
- md.use(wikilinks, { slugIndex, pageContextRegistry, translationMapStore });
333
+ safeUse(md, 'curly_attributes', markdownItAttrs, undefined, mdLog);
334
+ safeUse(md, 'baseline_auto_heading_ids', autoHeadingIds, { slugify }, mdLog);
335
+ safeUse(md, 'baseline_wikilinks', wikilinks, { slugIndex, pageContextRegistry, translationMapStore }, mdLog);
293
336
  });
294
337
 
338
+ // --- Snapshots ---
295
339
  coreContext.snapshots = {
296
340
  contentMap: () => contentMapStore.snapshot(),
297
341
  pageContext: () => pageContextRegistry.snapshot()
298
342
  };
299
343
 
300
- // --- Module activation ---
301
- const features = {
302
- multilang: Boolean(state.options.multilang),
303
- sitemap: Boolean(state.options.sitemap),
304
- navigator: Boolean(state.options.navigator),
305
- head: true,
306
- assets: true
307
- };
308
-
309
344
  // --- Module registry ---
310
345
  const moduleRegistry = [
311
- { when: features.multilang, name: 'multilang', plugin: multilangCore },
312
- { when: features.sitemap, name: 'sitemap', plugin: sitemapCore },
346
+ { when: state.features.multilang, name: 'multilang', plugin: multilangCore },
347
+ { when: state.features.sitemap, name: 'sitemap', plugin: sitemapCore },
313
348
  { name: 'navigator', plugin: navigatorCore },
314
- { when: features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
315
- { when: features.assets, name: 'assets', plugin: assetsCore }
349
+ { when: state.features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
350
+ { when: state.features.assets, name: 'assets', plugin: assetsCore }
316
351
  ];
317
352
 
353
+ const active = [];
318
354
  for (const entry of moduleRegistry) {
319
355
  const { when = true, name, plugin, consumes = {} } = entry;
320
356
  if (!when) continue;
@@ -325,6 +361,7 @@ export default function baseline(settings = {}, options = {}) {
325
361
  };
326
362
 
327
363
  eleventyConfig.addPlugin(plugin, moduleContext);
364
+ active.push(name);
328
365
  }
329
366
 
330
367
  // --- Filters ---
@@ -49,7 +49,7 @@ export function assetsCore(eleventyConfig, moduleContext) {
49
49
  const parsed = optionsSchema.safeParse(options.assets);
50
50
  if (!parsed.success) {
51
51
  for (const issue of parsed.error.issues) {
52
- log.info('options:', `${issue.path.join('.')} ${issue.message}`);
52
+ log.info('options:', `${issue.path.join('.')}, ${issue.message}`);
53
53
  }
54
54
  }
55
55
 
@@ -63,13 +63,15 @@ export function assetsCore(eleventyConfig, moduleContext) {
63
63
  const watchGlob = TemplatePath.join(assetsDirectory, '**/*.{css,js,svg,png,jpeg,jpg,webp,gif,avif}');
64
64
 
65
65
  if (!assetsDirectory) {
66
- log.warn('eleventyConfig.directories.assets is unset; registerVirtualDir must run before this plugin.');
66
+ log.warn('directories.assets is unset, registerVirtualDir must run before this plugin');
67
67
  return;
68
68
  }
69
69
 
70
70
  // Watch common asset formats so edits trigger reloads during --serve.
71
71
  eleventyConfig.addWatchTarget(watchGlob);
72
72
 
73
+ log.info('Assets pipeline registered');
74
+
73
75
  // --- JS (esbuild) ---
74
76
  // Register js as a template format. Only index.js files under assets/js/
75
77
  // are compiled; everything else (11tydata.js, non-entry scripts) is skipped
@@ -1,5 +1,5 @@
1
1
  import * as esbuild from 'esbuild';
2
- import { createLogger } from '../../../core/logging.js';
2
+ import { createLogger } from '../../../core/logging/index.js';
3
3
 
4
4
  /**
5
5
  * esbuild processor (processor)
@@ -61,7 +61,7 @@ export default async function assetsESbuild(jsFilePath, options = {}) {
61
61
  // Return raw JS; markup wrapping is handled by the plugin registration.
62
62
  return result.outputFiles[0].text;
63
63
  } catch (error) {
64
- log.error('esbuild failed:', error);
64
+ log.error('esbuild failed.', error);
65
65
  // Surface a safe JS comment so the caller can decide how to wrap it.
66
66
  return '/* Error processing JS */';
67
67
  }
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
2
2
  import postcss from 'postcss';
3
3
  import loadPostCSSConfig from 'postcss-load-config';
4
4
  import fallbackPostCSSConfig from '../configs/postcss.config.js';
5
- import { createLogger } from '../../../core/logging.js';
5
+ import { createLogger } from '../../../core/logging/index.js';
6
6
 
7
7
  /**
8
8
  * PostCSS processor (processor)
@@ -79,7 +79,7 @@ export default async function assetsPostCSS(cssFilePath) {
79
79
  // Return raw CSS; markup wrapping is handled in the plugin registration.
80
80
  return result.css;
81
81
  } catch (error) {
82
- log.error('PostCSS failed:', error);
82
+ log.error('PostCSS failed.', error);
83
83
  // Surface a safe CSS string so the caller can decide how to wrap it.
84
84
  return '/* Error processing CSS */';
85
85
  }
@@ -41,10 +41,9 @@ import { dedupeMeta, dedupeLink } from '../utils/dedupe.js';
41
41
  * @param {Object} args.options - Head options (titleSeparator, showGenerator).
42
42
  * @param {string} args.placeholderTag - Placeholder element to replace.
43
43
  * @param {string} args.eol - End-of-line separator interleaved between nodes.
44
- * @param {Object} args.log - Scoped logger.
45
44
  * @returns {(tree: Object) => Object} PostHTML plugin function.
46
45
  */
47
- export function renderHead({ seeds, alternates, options, placeholderTag, eol, log }) {
46
+ export function renderHead({ seeds, alternates, options, placeholderTag, eol }) {
48
47
  const defaults = emitMeta(seeds.meta, seeds.render, options);
49
48
  const extras = emitExtras(seeds.head, alternates);
50
49
 
@@ -52,7 +51,6 @@ export function renderHead({ seeds, alternates, options, placeholderTag, eol, lo
52
51
  const sorted = capoSort(deduped);
53
52
 
54
53
  return function rendererPlugin(tree) {
55
- // log.info('injecting head for', seeds.page.inputPath || seeds.page.url);
56
54
  tree.match({ tag: placeholderTag }, () => ({
57
55
  tag: 'head',
58
56
  content: interleaveEOL(sorted, eol)
@@ -2,6 +2,8 @@ import { renderHead } from './drivers/posthtml-head-elements.js';
2
2
  import { buildAlternates } from './utils/alternates.js';
3
3
  import { optionsSchema } from './schema.js';
4
4
 
5
+ import chalk from 'kleur';
6
+
5
7
  // Internal constants — not user-facing.
6
8
  const PLACEHOLDER_TAG = 'baseline-head';
7
9
  const EOL = '\n';
@@ -52,7 +54,7 @@ export function headCore(eleventyConfig, moduleContext) {
52
54
  const parsed = optionsSchema.safeParse(options.head);
53
55
  if (!parsed.success) {
54
56
  for (const issue of parsed.error.issues) {
55
- log.info('options:', `${issue.path.join('.')} ${issue.message}`);
57
+ log.info('options:', `${issue.path.join('.')}, ${issue.message}`);
56
58
  }
57
59
  }
58
60
 
@@ -68,23 +70,19 @@ export function headCore(eleventyConfig, moduleContext) {
68
70
  const headStats = { pages: new Set() };
69
71
 
70
72
  eleventyConfig.on('eleventy.after', () => {
71
- log.info({
72
- message: 'Head injection summary',
73
- totalPages: headStats.pages.size,
74
- sample: Array.from(headStats.pages).slice(0, 10)
75
- });
73
+ log.info(chalk.green(`Head injected into ${headStats.pages.size} pages`));
76
74
  headStats.pages.clear();
77
75
  });
78
76
 
79
77
  // --- Transform-time: compose and inject. ---
80
- log.info('Injecting heads to pages');
78
+ log.info('Injecting heads');
81
79
  eleventyConfig.htmlTransformer.addPosthtmlPlugin('html', function (context) {
82
80
  headStats.pages.add(context?.page?.inputPath || context?.outputPath);
83
81
 
84
82
  const key = context?.page?.url ?? context?.page?.inputPath;
85
83
  const seeds = pageContextRegistry?.getByKey(key);
86
84
  if (!seeds) {
87
- log.warn('no head seeds for', context?.page?.inputPath || context?.outputPath);
85
+ log.warn('No head seeds for', context?.page?.inputPath || context?.outputPath);
88
86
  return (tree) => tree;
89
87
  }
90
88
 
@@ -99,8 +97,7 @@ export function headCore(eleventyConfig, moduleContext) {
99
97
  alternates,
100
98
  options: headOptions,
101
99
  placeholderTag: PLACEHOLDER_TAG,
102
- eol: EOL,
103
- log
100
+ eol: EOL
104
101
  });
105
102
  });
106
103
  }
@@ -1,6 +1,6 @@
1
1
  import { I18nPlugin } from '@11ty/eleventy';
2
2
  import { DeepCopy } from '@11ty/eleventy-utils';
3
- import { normalizeLanguages } from '../../core/utils/helpers.js';
3
+ import { normalizeLanguages } from '../../core/utils/normalize-languages.js';
4
4
  import i18nTranslationsFor from './filters/i18n-translations-for.js';
5
5
  import i18nTranslationIn from './filters/i18n-translation-in.js';
6
6
  import i18nDefaultTranslation from './filters/i18n-default-translation.js';
@@ -63,10 +63,12 @@ export function multilangCore(eleventyConfig, moduleContext) {
63
63
  const isMultilingual = options.multilang === true && defaultLanguage && hasLanguages;
64
64
 
65
65
  if (!isMultilingual) {
66
- log.info('inactive: requires options.multilingual + settings.defaultLanguage + languages');
66
+ log.info('Multilang inactive, needs options.multilang, settings.defaultLanguage, and languages');
67
67
  return;
68
68
  }
69
69
 
70
+ log.info(`Multilang active: ${Object.keys(languages).join('/')} (default: ${defaultLanguage})`);
71
+
70
72
  // Register Eleventy's built-in I18nPlugin for locale-aware URL resolution.
71
73
  eleventyConfig.addPlugin(I18nPlugin, {
72
74
  defaultLanguage: defaultLanguage,
@@ -10,39 +10,42 @@ const __dirname = path.dirname(__filename);
10
10
  /**
11
11
  * Navigator (module)
12
12
  *
13
- * Debug surface. Exposes Eleventy and Baseline runtime state to templates so
14
- * developers can inspect data shape, scope contents, and lifecycle output
15
- * without leaving the page.
13
+ * Two roles, one module: the public read surface for plugin-produced
14
+ * cross-page data (the content graph and its enriched backlinks), and the
15
+ * debug surface for inspecting Eleventy and Baseline runtime state.
16
16
  *
17
17
  * Architecture layer:
18
18
  * module
19
19
  *
20
20
  * System role:
21
- * Read-only window into the runtime substrate. Pulls snapshots from the
22
- * page-context registry and content-map store via the module context;
23
- * does not write back.
21
+ * Read-only window over the runtime substrate. Surfaces the content graph
22
+ * for templates that need cross-page reads, and snapshots from the
23
+ * page-context registry and content-map store for debugging. Writes
24
+ * nothing back.
24
25
  *
25
26
  * Lifecycle:
26
- * build-time → register Nunjucks globals, debug filters, and the
27
- * optional virtual debug page
27
+ * build-time → register `_navigator` ({ nodes, edges, backlinks }),
28
+ * debug globals, filters, and the optional virtual debug page
28
29
  * cascade-time → eleventyComputed `_snapshot` resolves contentMap and
29
30
  * pageContext on each page
30
31
  *
31
32
  * Why this exists:
32
- * Render-time inspection of cascade state has no built-in surface.
33
- * Centralising globals and filters under a debug-only module keeps the
34
- * inspection vocabulary stable and out of feature modules.
33
+ * Templates need an addressable cross-page surface for graph reads, and
34
+ * render-time inspection of cascade state has no built-in equivalent.
35
+ * One module owns both vocabularies so feature modules stay narrow.
35
36
  *
36
37
  * Scope:
37
- * Owns the `_runtime` and `_ctx` Nunjucks globals, computed `_snapshot`,
38
+ * Owns the `_navigator` global (`{ nodes, edges, backlinks }`, the public
39
+ * read surface), the debug globals `_runtime` and `_ctx`, computed `_snapshot`,
38
40
  * debug filters (`_inspect`, `_json`, `_keys`), and the optional virtual
39
41
  * page at /navigator-core.html.
40
- * Does not own the data it surfaces (page-context registry, content-map
41
- * store).
42
+ * Does not own the data it surfaces (content graph, page-context registry,
43
+ * content-map store).
42
44
  *
43
45
  * Data flow:
44
- * snapshots (contentMap, pageContext) + this.ctx → globals + computed
45
- * `_snapshot` + virtual page → developer
46
+ * runtime.contentGraph + snapshots + this.ctx → `_navigator` + debug
47
+ * globals + computed `_snapshot` + virtual page → templates and
48
+ * developers
46
49
  *
47
50
  * Note: `_snapshot.contentMap` is null on the navigator template itself
48
51
  * because it renders before `eleventy.contentMap` fires. Read `_snapshot`
@@ -51,23 +54,25 @@ const __dirname = path.dirname(__filename);
51
54
  * @param {import("@11ty/eleventy").UserConfig} eleventyConfig
52
55
  * @param {Object} moduleContext
53
56
  * @param {Object} moduleContext.state - Resolved plugin state.
57
+ * @param {Object} moduleContext.runtime - Lazy access layer; reads contentGraph.
54
58
  * @param {Object} moduleContext.snapshots - Thunks: { contentMap, pageContext }.
55
59
  */
56
60
  export function navigatorCore(eleventyConfig, moduleContext) {
57
- const { state, snapshots, log, env } = moduleContext;
61
+ const { state, runtime, snapshots, log, env } = moduleContext;
58
62
  const { settings, options } = state;
59
63
 
60
64
  // Structural-only options check: log on mismatch, do not throw.
61
65
  const parsed = optionsSchema.safeParse(options.navigator);
62
66
  if (!parsed.success) {
63
67
  for (const issue of parsed.error.issues) {
64
- log.info('options:', `${issue.path.join('.')} ${issue.message}`);
68
+ log.info('options:', `${issue.path.join('.')}, ${issue.message}`);
65
69
  }
66
70
  }
67
71
 
68
72
  // Boolean shorthand activates the virtual page; object form lets users tune.
69
73
  const navigatorOpts = options.navigator && typeof options.navigator === 'object' ? options.navigator : {};
70
- const renderTemplate = env.mode === 'development' ?? navigatorOpts.template ?? Boolean(options.navigator);
74
+ const renderTemplate =
75
+ navigatorOpts.template ?? (typeof options.navigator === 'boolean' ? options.navigator : env.mode === 'development');
71
76
  const inspectorDepth = navigatorOpts.inspectorDepth ?? 4;
72
77
 
73
78
  eleventyConfig.addGlobalData('eleventyComputed._snapshot', () => {
@@ -77,6 +82,14 @@ export function navigatorCore(eleventyConfig, moduleContext) {
77
82
  });
78
83
  });
79
84
 
85
+ // Public read surface for plugin-produced cross-page data. Templates can
86
+ // paginate over `_navigator.backlinks` or read `_navigator.graph` directly.
87
+ eleventyConfig.addGlobalData('_navigator', () => ({
88
+ nodes: runtime.contentGraph?.nodes ?? {},
89
+ edges: runtime.contentGraph?.edges ?? {},
90
+ backlinks: runtime.contentGraph?.backlinks ?? {}
91
+ }));
92
+
80
93
  /**
81
94
  * Nunjucks Global: _runtime
82
95
  *
@@ -125,7 +138,7 @@ export function navigatorCore(eleventyConfig, moduleContext) {
125
138
  inspectorDepth
126
139
  });
127
140
 
128
- log.info('Navigator template registered at /navigator-core.html');
141
+ log.info('Navigator mounted at /navigator-core.html');
129
142
  }
130
143
 
131
144
  /**
@@ -41,7 +41,7 @@ permalink: /navigator-core.html
41
41
  <details>
42
42
  <summary><strong>Page Context</strong></summary>
43
43
  <pre>
44
- {{- _snapshot.pageContext | _inspect({ depth: null }) -}}
44
+ {{- _pageContext | _inspect({ depth: null }) -}}
45
45
  </pre>
46
46
  </details>
47
47
  {% for key, value in _runtime() %}