@apleasantview/eleventy-plugin-baseline 0.1.0-next.39 → 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 (54) 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/index.js +80 -0
  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} +6 -6
  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 +52 -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/types.js +1 -1
  25. package/core/utils/add-trailing-slash.js +11 -0
  26. package/core/utils/ensure-dot-slash-dir.js +13 -0
  27. package/core/utils/normalize-languages.js +28 -0
  28. package/core/utils/resolve-field.js +9 -0
  29. package/core/utils/resolve-subdir.js +20 -0
  30. package/core/utils/slugify.js +15 -0
  31. package/core/utils/unique-by.js +25 -0
  32. package/core/virtual-dir.js +11 -10
  33. package/index.js +152 -115
  34. package/modules/assets/index.js +4 -2
  35. package/modules/assets/processors/esbuild-process.js +35 -2
  36. package/modules/assets/processors/postcss-process.js +36 -2
  37. package/modules/head/drivers/capo-adapter.js +26 -4
  38. package/modules/head/drivers/posthtml-head-elements.js +2 -4
  39. package/modules/head/index.js +7 -10
  40. package/modules/multilang/index.js +4 -2
  41. package/modules/navigator/index.js +33 -20
  42. package/modules/navigator/templates/navigator-core.html +1 -1
  43. package/modules/sitemap/index.js +7 -3
  44. package/package.json +4 -2
  45. package/core/filters/index.js +0 -4
  46. package/core/global-functions/index.js +0 -6
  47. package/core/logging.js +0 -32
  48. package/core/page-context.js +0 -310
  49. package/core/shortcodes/index.js +0 -2
  50. package/core/utils/helpers.js +0 -75
  51. /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
  52. /package/core/{filters → surface/filters}/isString.js +0 -0
  53. /package/core/{filters → surface/filters}/related-posts.js +0 -0
  54. /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,38 @@
1
1
  import * as esbuild from 'esbuild';
2
- import { createLogger } from '../../../core/logging.js';
2
+ import { createLogger } from '../../../core/logging/index.js';
3
+
4
+ /**
5
+ * esbuild processor (processor)
6
+ *
7
+ * Bundles a single JS entrypoint with esbuild and returns the text. Used
8
+ * both by the `js` template format compile guard and by the
9
+ * `inlineESbuild` filter in the assets module.
10
+ *
11
+ * Architecture layer:
12
+ * module
13
+ *
14
+ * System role:
15
+ * Stateless bundler called by `modules/assets/index.js`. The compile
16
+ * guard decides which files reach this processor; this file owns the
17
+ * esbuild call itself.
18
+ *
19
+ * Lifecycle:
20
+ * build-time → invoked per matching entrypoint during template compile,
21
+ * or per-call from the inline filter
22
+ *
23
+ * Why this exists:
24
+ * Eleventy treats every `.js` file as a template. A dedicated processor
25
+ * keeps esbuild configuration out of the template format wiring and lets
26
+ * the inline filter reuse the same defaults.
27
+ *
28
+ * Scope:
29
+ * Owns esbuild option defaults and the bundle call. Does not own the
30
+ * compile guard, the watch target, or markup wrapping; the assets
31
+ * module owns those.
32
+ *
33
+ * Data flow:
34
+ * entrypoint path + options → esbuild.build → bundled JS text
35
+ */
3
36
 
4
37
  const log = createLogger('assets-esbuild');
5
38
  const defaultOptions = { minify: true, target: 'es2020' };
@@ -28,7 +61,7 @@ export default async function assetsESbuild(jsFilePath, options = {}) {
28
61
  // Return raw JS; markup wrapping is handled by the plugin registration.
29
62
  return result.outputFiles[0].text;
30
63
  } catch (error) {
31
- log.error('esbuild failed:', error);
64
+ log.error('esbuild failed.', error);
32
65
  // Surface a safe JS comment so the caller can decide how to wrap it.
33
66
  return '/* Error processing JS */';
34
67
  }
@@ -2,7 +2,41 @@ 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
+
7
+ /**
8
+ * PostCSS processor (processor)
9
+ *
10
+ * Processes a single CSS entrypoint through PostCSS and returns the text.
11
+ * Used both by the `css` template format compile guard and by the
12
+ * `inlinePostCSS` filter in the assets module.
13
+ *
14
+ * Architecture layer:
15
+ * module
16
+ *
17
+ * System role:
18
+ * Stateless processor called by `modules/assets/index.js`. Resolves the
19
+ * user's PostCSS config from the project root, falling back to the
20
+ * bundled Baseline config when none is found. Cached for the lifetime
21
+ * of the process.
22
+ *
23
+ * Lifecycle:
24
+ * build-time → invoked per matching entrypoint during template compile,
25
+ * or per-call from the inline filter
26
+ *
27
+ * Why this exists:
28
+ * Eleventy has no PostCSS hook of its own. A dedicated processor lets
29
+ * user configs win when present and keeps the bundled fallback out of
30
+ * the consumer's `node_modules` resolution path.
31
+ *
32
+ * Scope:
33
+ * Owns config resolution, caching, and the PostCSS call. Does not own
34
+ * the compile guard, the watch target, or markup wrapping; the assets
35
+ * module owns those.
36
+ *
37
+ * Data flow:
38
+ * entrypoint path → PostCSS pipeline → processed CSS text
39
+ */
6
40
 
7
41
  const log = createLogger('assets-postcss');
8
42
 
@@ -45,7 +79,7 @@ export default async function assetsPostCSS(cssFilePath) {
45
79
  // Return raw CSS; markup wrapping is handled in the plugin registration.
46
80
  return result.css;
47
81
  } catch (error) {
48
- log.error('PostCSS failed:', error);
82
+ log.error('PostCSS failed.', error);
49
83
  // Surface a safe CSS string so the caller can decide how to wrap it.
50
84
  return '/* Error processing CSS */';
51
85
  }
@@ -1,9 +1,31 @@
1
1
  /**
2
- * capo.js adapter for PostHTML AST nodes.
2
+ * Capo PostHTML adapter (driver)
3
3
  *
4
- * Implements the HTMLAdapter interface capo.js v2 uses to compute element
5
- * weights (src/adapters/adapter.js in @rviscomi/capo.js). Only getWeight is
6
- * consumed downstream; the rest are shimmed to satisfy the shape.
4
+ * Implements the `HTMLAdapter` interface capo.js v2 expects, against
5
+ * PostHTML's `{ tag, attrs, content }` node shape. Only `getWeight` is
6
+ * exercised downstream; the rest are shimmed to satisfy the contract.
7
+ *
8
+ * Architecture layer:
9
+ * module
10
+ *
11
+ * System role:
12
+ * Translation shim between the head driver's PostHTML tree and the
13
+ * capo.js sort. Used by `posthtml-head-elements.js` when ordering the
14
+ * composed `<head>` element list.
15
+ *
16
+ * Lifecycle:
17
+ * transform-time → invoked per node while capo.js scores element weights
18
+ *
19
+ * Why this exists:
20
+ * capo.js is DOM-shaped; PostHTML is not. Without an adapter the driver
21
+ * would have to walk the tree twice or hand-roll element weighting.
22
+ *
23
+ * Scope:
24
+ * Owns attribute lookup, tag name resolution, and text extraction over
25
+ * PostHTML nodes. Does not own weighting logic; capo.js owns that.
26
+ *
27
+ * Data flow:
28
+ * PostHTML node → adapter accessor → capo.js getWeight
7
29
  *
8
30
  * A PostHTML element node looks like `{ tag, attrs, content }` where attrs
9
31
  * is either undefined or a plain object. Boolean attributes appear with an
@@ -10,7 +10,7 @@ import { dedupeMeta, dedupeLink } from '../utils/dedupe.js';
10
10
  * <baseline-head> placeholder with the result.
11
11
  *
12
12
  * Architecture layer:
13
- * module (driver inside head)
13
+ * module
14
14
  *
15
15
  * System role:
16
16
  * The seam between head's pipeline and the renderer choice. Alternate
@@ -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,