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

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 (73) hide show
  1. package/README.md +30 -32
  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 +185 -0
  5. package/core/content-graph/graph.js +121 -0
  6. package/core/content-graph/index.js +2 -0
  7. package/core/content-graph/prepass.js +121 -0
  8. package/core/dates/git-date.js +71 -0
  9. package/core/dates/index.js +55 -0
  10. package/core/locale/derive-lang.js +19 -0
  11. package/core/locale/index.js +6 -0
  12. package/core/locale/normalize-lang.js +13 -0
  13. package/core/locale/normalize-locale.js +20 -0
  14. package/core/locale/open-graph-locale.js +14 -0
  15. package/core/locale/resolve-default.js +27 -0
  16. package/core/locale/resolve-locale.js +16 -0
  17. package/core/logging/banner.js +49 -0
  18. package/core/{logging.js → logging/index.js} +19 -2
  19. package/core/logging/quips.js +30 -0
  20. package/core/markdown/auto-heading-ids.js +86 -0
  21. package/core/markdown/index.js +5 -0
  22. package/core/markdown/safe-use.js +42 -0
  23. package/core/{wikilinks.js → markdown/wikilinks.js} +4 -4
  24. package/core/page-context/build.js +336 -0
  25. package/core/page-context/index.js +1 -0
  26. package/core/page-context/register.js +73 -0
  27. package/core/page-context/seo-helpers.js +56 -0
  28. package/core/schema.js +22 -2
  29. package/core/seo-graph/adapter.js +246 -0
  30. package/core/seo-graph/build.js +87 -0
  31. package/core/seo-graph/index.js +1 -0
  32. package/core/seo-graph/open-graph.js +130 -0
  33. package/core/seo-graph/register.js +42 -0
  34. package/core/seo-graph/schema.js +18 -0
  35. package/core/slug-index.js +2 -2
  36. package/core/state.js +75 -0
  37. package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
  38. package/core/surface/index.js +22 -0
  39. package/core/types.js +3 -0
  40. package/core/utils/add-trailing-slash.js +11 -0
  41. package/core/utils/ensure-dot-slash-dir.js +13 -0
  42. package/core/utils/normalize-language-map.js +37 -0
  43. package/core/utils/resolve-field.js +9 -0
  44. package/core/utils/resolve-subdir.js +20 -0
  45. package/core/utils/slugify.js +15 -0
  46. package/core/utils/title-case-slug.js +15 -0
  47. package/core/utils/unique-by.js +25 -0
  48. package/core/virtual-dir.js +11 -10
  49. package/index.js +161 -118
  50. package/modules/assets/index.js +4 -2
  51. package/modules/assets/processors/esbuild-process.js +2 -2
  52. package/modules/assets/processors/postcss-process.js +2 -2
  53. package/modules/head/drivers/posthtml-head-elements.js +92 -12
  54. package/modules/head/index.js +23 -19
  55. package/modules/head/schema.js +7 -3
  56. package/modules/multilang/filters/i18n-default-translation.js +2 -4
  57. package/modules/multilang/filters/i18n-translation-in.js +2 -2
  58. package/modules/multilang/filters/i18n-translations-for.js +2 -2
  59. package/modules/multilang/index.js +80 -39
  60. package/modules/navigator/index.js +39 -25
  61. package/modules/navigator/templates/navigator-core.html +1 -1
  62. package/modules/sitemap/index.js +8 -4
  63. package/modules/sitemap/templates/sitemap-core.html +1 -1
  64. package/package.json +5 -2
  65. package/core/filters/index.js +0 -4
  66. package/core/global-functions/index.js +0 -6
  67. package/core/page-context.js +0 -310
  68. package/core/shortcodes/index.js +0 -2
  69. package/core/utils/helpers.js +0 -75
  70. /package/core/{global-functions/date.js → dates/date-global.js} +0 -0
  71. /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
  72. /package/core/{filters → surface/filters}/isString.js +0 -0
  73. /package/core/{filters → surface/filters}/related-posts.js +0 -0
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Helper function to add trailing slash to a path
3
+ * @param {string} path
4
+ * @returns {string}
5
+ */
6
+ export function addTrailingSlash(path) {
7
+ if (path.slice(-1) === '/') {
8
+ return path;
9
+ }
10
+ return path + '/';
11
+ }
@@ -0,0 +1,13 @@
1
+ import { TemplatePath } from '@11ty/eleventy-utils';
2
+
3
+ /**
4
+ * Normalise a directory path to a `./`-prefixed form, defaulting empty/missing
5
+ * input to the current directory. Thin wrapper over
6
+ * `TemplatePath.addLeadingDotSlash` that bakes in the empty-string fallback.
7
+ *
8
+ * @param {string | undefined} dir
9
+ * @returns {string}
10
+ */
11
+ export function ensureDotSlashDir(dir) {
12
+ return TemplatePath.addLeadingDotSlash(dir || './');
13
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Normalize the `settings.languages` config into an object map with
3
+ * lowercased, trimmed keys.
4
+ *
5
+ * Accepts an array of language codes or an object keyed by language code;
6
+ * either form ends up with `[normalizedKey]: entry`. Returns undefined if
7
+ * the input is invalid or empty.
8
+ *
9
+ * Lives in `core/utils/` (not `core/locale/`) because the array-vs-object
10
+ * shape coercion is config-shape adapting, not locale handling. The
11
+ * lowercasing of keys is the only locale-shaped part.
12
+ *
13
+ * @param {Object} settings - Options object containing languages.
14
+ * @param {import('../logging/index.js').BaselineLogger} [logger] - Logger for dropped-entry notice.
15
+ * @returns {Record<string, Object>|undefined} Normalized language map, or undefined.
16
+ */
17
+ export function normalizeLanguageMap(settings, logger) {
18
+ const normalizedLanguages = Array.isArray(settings.languages)
19
+ ? Object.fromEntries(
20
+ settings.languages
21
+ .filter((lang) => typeof lang === 'string' && lang.trim())
22
+ .map((lang) => [lang.toLowerCase().trim(), {}])
23
+ )
24
+ : settings.languages && typeof settings.languages === 'object'
25
+ ? Object.fromEntries(
26
+ Object.entries(settings.languages).map(([k, v]) => [k.toLowerCase().trim(), v])
27
+ )
28
+ : undefined;
29
+
30
+ if (logger && Array.isArray(settings.languages)) {
31
+ const normalizedCount = normalizedLanguages ? Object.keys(normalizedLanguages).length : 0;
32
+ if (normalizedCount !== settings.languages.length) {
33
+ logger.info('Some languages entries were invalid and were dropped.');
34
+ }
35
+ }
36
+ return normalizedLanguages;
37
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolve a field with a page → site → fallback precedence chain.
3
+ *
4
+ * @param {{ pageValue?: any, siteValue?: any, fallbackValue?: any, isHome?: boolean }} args
5
+ * @returns {any}
6
+ */
7
+ export function resolveField({ pageValue, siteValue, fallbackValue }) {
8
+ return pageValue ?? siteValue ?? fallbackValue;
9
+ }
@@ -0,0 +1,20 @@
1
+ import { TemplatePath } from '@11ty/eleventy-utils';
2
+ import { addTrailingSlash } from './add-trailing-slash.js';
3
+
4
+ /**
5
+ * Resolve a subdirectory under input and output.
6
+ * Joins inputDir/outputDir with rawDir, normalises, and adds trailing slashes.
7
+ * @param {string} inputDir - The input directory (e.g., "./src/").
8
+ * @param {string} outputDir - The output directory (e.g., "./dist/").
9
+ * @param {string} rawDir - Raw subdirectory value (e.g., "assets", "static").
10
+ * @returns {{input: string, output: string}}
11
+ */
12
+ export function resolveSubdir(inputDir, outputDir, rawDir) {
13
+ const joinedInput = TemplatePath.join(inputDir, rawDir || '');
14
+ const joinedOutput = TemplatePath.join(outputDir, rawDir || '');
15
+
16
+ return {
17
+ input: addTrailingSlash(TemplatePath.standardizeFilePath(joinedInput)),
18
+ output: addTrailingSlash(TemplatePath.standardizeFilePath(joinedOutput))
19
+ };
20
+ }
@@ -0,0 +1,15 @@
1
+ import slugifyLib from 'slugify';
2
+
3
+ /**
4
+ * Slugify a string into a wikilink-friendly key.
5
+ * Lowercases, strips diacritics, replaces non-alphanumerics with hyphens,
6
+ * trims leading/trailing hyphens. Returns undefined for empty input.
7
+ *
8
+ * @param {string|null|undefined} input
9
+ * @returns {string|undefined}
10
+ */
11
+ export function slugify(input) {
12
+ if (input == null) return;
13
+ const slug = slugifyLib(String(input), { lower: true, strict: true, trim: true });
14
+ return slug || undefined;
15
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Title-case a URL slug for display: "core-reference" → "Core Reference".
3
+ * The rough inverse of {@link slugify}: splits on hyphens/underscores and
4
+ * capitalises each word.
5
+ *
6
+ * @param {string} slug
7
+ * @returns {string}
8
+ */
9
+ export function titleCaseSlug(slug) {
10
+ return String(slug)
11
+ .split(/[-_]/)
12
+ .filter(Boolean)
13
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
14
+ .join(' ');
15
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Deduplicate an array by a key (string property name or selector function).
3
+ * Items without a derivable key are kept via their JSON-stringified shape.
4
+ *
5
+ * @template T
6
+ * @param {T[]} arr
7
+ * @param {string | ((item: T) => string | undefined)} keyFn
8
+ * @returns {T[]}
9
+ */
10
+ export const uniqueBy = (arr, keyFn) =>
11
+ Object.values(
12
+ (arr ?? []).reduce((acc, item) => {
13
+ if (!item) return acc;
14
+
15
+ const id = typeof keyFn === 'function' ? keyFn(item) : item?.[keyFn];
16
+
17
+ if (!id) {
18
+ acc[JSON.stringify(item)] = item;
19
+ return acc;
20
+ }
21
+
22
+ acc[id] = item;
23
+ return acc;
24
+ }, {})
25
+ );
@@ -1,8 +1,11 @@
1
- import { TemplatePath } from '@11ty/eleventy-utils';
2
- import { resolveSubdir } from './utils/helpers.js';
3
- import { createLogger } from './logging.js';
1
+ import { ensureDotSlashDir } from './utils/ensure-dot-slash-dir.js';
2
+ import { resolveSubdir } from './utils/resolve-subdir.js';
3
+ import { createLogger } from './logging/index.js';
4
4
  import { getScope, addScopeListener, setEntry } from './registry.js';
5
5
 
6
+ const SCOPE_NAME = 'core:virtual-dir';
7
+ const LOG_NAME = 'virtual-dir';
8
+
6
9
  /**
7
10
  * Virtual directories (runtime substrate)
8
11
  *
@@ -40,8 +43,6 @@ import { getScope, addScopeListener, setEntry } from './registry.js';
40
43
  * live { input, output } cache → consumers
41
44
  */
42
45
 
43
- const SCOPE_NAME = 'core:virtual-dir';
44
-
45
46
  /**
46
47
  * Register a virtual directory on eleventyConfig.directories.
47
48
  *
@@ -56,10 +57,10 @@ const SCOPE_NAME = 'core:virtual-dir';
56
57
  */
57
58
  export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
58
59
  if (!key) {
59
- throw new Error('registerVirtualDir: `name` is required');
60
+ throw new Error('[baseline/virtual-dir] `name` is required');
60
61
  }
61
62
 
62
- const log = createLogger(SCOPE_NAME);
63
+ const log = createLogger(LOG_NAME);
63
64
  const scope = getScope(eleventyConfig, SCOPE_NAME);
64
65
  const rawDir = eleventyConfig.dir?.[key] || key;
65
66
  const rawOutputDir = outputDir ?? rawDir;
@@ -75,7 +76,7 @@ export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
75
76
  // shared listener below refreshes when Eleventy emits its final directories.
76
77
  const existing = Object.getOwnPropertyDescriptor(eleventyConfig.directories, key);
77
78
  if (existing && existing.configurable === false) {
78
- log.info(`directories[${key}] already defined; skipping`);
79
+ log.info(`directories.${key} already defined, skipping`);
79
80
  } else {
80
81
  Object.defineProperty(eleventyConfig.directories, key, {
81
82
  get() {
@@ -101,8 +102,8 @@ export function registerVirtualDir(eleventyConfig, { key, outputDir } = {}) {
101
102
  }
102
103
 
103
104
  function syncCache(cache, dirs, rawDir, rawOutputDir) {
104
- const inputDir = TemplatePath.addLeadingDotSlash(dirs.input || './');
105
- const outputDir = TemplatePath.addLeadingDotSlash(dirs.output || './');
105
+ const inputDir = ensureDotSlashDir(dirs.input);
106
+ const outputDir = ensureDotSlashDir(dirs.output);
106
107
 
107
108
  // resolveSubdir symmetrically resolves against input and output; call twice
108
109
  // so input and output subdirs can differ (e.g. `public` copies to root).
package/index.js CHANGED
@@ -3,36 +3,41 @@ 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 { registerSeoGraph } from './core/seo-graph/index.js';
19
+ import { autoHeadingIds, safeUse, wikilinks } from './core/markdown/index.js';
20
+ import { slugify } from './core/utils/slugify.js';
19
21
  import { assetsCore, headCore, multilangCore, navigatorCore, sitemapCore } from './modules.js';
22
+ import {
23
+ registerGlobals,
24
+ markdownFilter,
25
+ relatedPostsFilter,
26
+ isStringFilter,
27
+ imageShortcode
28
+ } from './core/surface/index.js';
20
29
 
21
30
  const __require = createRequire(import.meta.url);
22
31
  const { name, version } = __require('./package.json');
32
+ const eleventyVersion = process.env.ELEVENTY_VERSION;
33
+ // const absoluteRoot = process.env.ELEVENTY_ROOT; -> Safekeeping.
23
34
 
24
35
  const mode = process.env.ELEVENTY_ENV;
36
+ // eslint-disable-next-line no-unused-vars
25
37
  const isDev = mode === 'development';
38
+ // eslint-disable-next-line no-unused-vars
26
39
  const isProd = mode === 'production';
27
40
 
28
- const LEGACY_OPTION_KEYS = [
29
- 'verbose',
30
- 'enableNavigatorTemplate',
31
- 'enableSitemapTemplate',
32
- 'assetsESBuild',
33
- 'multilingual'
34
- ];
35
-
36
41
  // Whitelist of reserved global data keys used internally across the plugin.
37
42
  // Positive side effect is they all get listed in order and merge data to the same key.
38
43
  // Also prevents name collision with filters.
@@ -44,37 +49,20 @@ const INTERNAL_KEYS = [
44
49
  '_navigator',
45
50
  '_sitemap',
46
51
  '_snapshot',
47
- '_pageContext'
52
+ 'eleventyComputed._pageContext',
53
+ 'eleventyComputed._node',
54
+ 'eleventyComputed._seoGraph',
55
+ 'eleventyComputed._backlinks',
56
+ 'eleventyComputed._outgoing',
57
+ 'eleventyComputed._edges'
48
58
  ];
49
59
 
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
- }
60
+ // Base logger outputs regardless of options.
61
+ const baseLog = createLogger(null, { verbose: true });
64
62
 
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
- }
63
+ printBannerOnce(baseLog, { version, eleventyVersion });
64
+
65
+ let contentGraph = null;
78
66
 
79
67
  /**
80
68
  * Baseline (composition root)
@@ -118,36 +106,29 @@ function splitLegacyOptions(legacy) {
118
106
  */
119
107
  export default function baseline(settings = {}, options = {}) {
120
108
  // --- 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.');
109
+ if (isLegacyShape(settings, arguments.length)) {
110
+ const normalized = normalizeLegacyShape(settings);
111
+ settings = normalized.settings;
112
+ options = normalized.options;
113
+ baseLog.info('Single-object plugin arg is deprecated. Use baseline(settings, options).');
140
114
  }
141
115
 
142
116
  // Validate configuration shape (non-fatal).
143
117
  const parsed = settingsSchema.safeParse(settings);
144
118
  if (!parsed.success) {
145
119
  for (const issue of parsed.error.issues) {
146
- baseLog.info('settings:', `${issue.path.join('.')} ${issue.message}`);
120
+ baseLog.info('settings:', `${issue.path.join('.')}, ${issue.message}`);
147
121
  }
148
122
  }
149
123
 
150
- baseLog.info('Eleventy Baseline', version);
124
+ // Resolve state once, above the closure. Pure; no eleventyConfig.
125
+ const state = deriveBaselineState(settings, options, { mode });
126
+ baseLog.info('Settings and options resolved, modules loaded');
127
+
128
+ // Scoped logging.
129
+ function scopedLog(name) {
130
+ return createLogger(name, { verbose: state.options.verbose });
131
+ }
151
132
 
152
133
  /**
153
134
  * Eleventy plugin initializer.
@@ -160,26 +141,70 @@ export default function baseline(settings = {}, options = {}) {
160
141
  try {
161
142
  eleventyConfig.versionCheck('>=3.0');
162
143
  } catch (e) {
163
- baseLog.error('Eleventy version mismatch:', e.message);
144
+ baseLog.error('Eleventy version mismatch.', e.message);
145
+ }
146
+
147
+ // --- Pre-pass wiring ---
148
+ // One mechanic: the pre-pass runs at the start of every Eleventy
149
+ // build cycle via `eleventy.before`. Initial build, watch rebuild,
150
+ // production build — all the same path. Templates always render
151
+ // against a graph rebuilt from current source. The sentinel keeps
152
+ // the inner Eleventy from re-attaching the hook on re-entry.
153
+ if (process.env[PREPASS_SENTINEL] !== '1') {
154
+ const prepassLog = scopedLog('pre-pass');
155
+
156
+ // Origins HtmlBasePlugin may have rewritten internal hrefs to.
157
+ // Stripped during link extraction so backlinks key on path-only.
158
+ const knownOrigins = new Set(['http://localhost:8080']);
159
+ for (const candidate of [settings.url, process.env.URL]) {
160
+ if (!candidate) continue;
161
+ try {
162
+ knownOrigins.add(new URL(candidate).origin);
163
+ } catch {
164
+ prepassLog.info('No known origins, using localhost only');
165
+ }
166
+ }
167
+
168
+ eleventyConfig.on('eleventy.before', async () => {
169
+ contentGraph = await runPrepass(
170
+ eleventyConfig.directories?.input,
171
+ eleventyConfig.directories?.output,
172
+ scopedLog,
173
+ { quietMode: true, knownOrigins }
174
+ );
175
+ });
164
176
  }
165
177
 
166
178
  INTERNAL_KEYS.forEach((key) => {
179
+ // We leave eleventyComputed callback keys alone, the rest are reserved-empty.
180
+ if (
181
+ key === 'eleventyComputed._pageContext' ||
182
+ key === 'eleventyComputed._node' ||
183
+ key === 'eleventyComputed._seoGraph' ||
184
+ key === 'eleventyComputed._backlinks' ||
185
+ key === 'eleventyComputed._outgoing' ||
186
+ key === 'eleventyComputed._edges'
187
+ )
188
+ return;
167
189
  eleventyConfig.addGlobalData(key, {});
168
190
  });
169
191
 
170
192
  const env = {
171
- name: 'Eleventy Baseline',
172
- package: name,
173
193
  version,
174
- mode
194
+ name: 'Eleventy Baseline',
195
+ env: {
196
+ mode,
197
+ package: name
198
+ }
175
199
  };
176
200
 
177
201
  eleventyConfig.addGlobalData('_baseline', {
178
- env
202
+ ...env,
203
+ options: state.options
179
204
  });
180
205
 
181
206
  if (!settings.url) {
182
- baseLog.warn('settings.url missing canonical URLs will be relative');
207
+ baseLog.warn('settings.url missing, canonical URLs will be relative');
183
208
  }
184
209
 
185
210
  registerGlobals(eleventyConfig);
@@ -188,38 +213,12 @@ export default function baseline(settings = {}, options = {}) {
188
213
  baseHref: process.env.URL || eleventyConfig.pathPrefix
189
214
  });
190
215
 
191
- // --- State layer (authoritative configuration) ---
216
+ // --- Feature exposure to templates ---
192
217
  const hasImageTransformPlugin = eleventyConfig.hasPlugin('eleventyImageTransformPlugin');
193
218
 
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
219
  eleventyConfig.addGlobalData('_baseline', {
221
220
  features: {
222
- ...state.options,
221
+ ...state.features,
223
222
  hasImageTransformPlugin
224
223
  }
225
224
  });
@@ -234,6 +233,9 @@ export default function baseline(settings = {}, options = {}) {
234
233
  outputDir: ''
235
234
  });
236
235
 
236
+ const virtualDirLog = scopedLog('virtual-dir');
237
+ virtualDirLog.info('Virtual directories mounted');
238
+
237
239
  const directories = {
238
240
  input: eleventyConfig.directories?.input,
239
241
  output: eleventyConfig.directories?.output,
@@ -277,6 +279,9 @@ export default function baseline(settings = {}, options = {}) {
277
279
  get contentMap() {
278
280
  return contentMapStore.get();
279
281
  },
282
+ get contentGraph() {
283
+ return contentGraph;
284
+ },
280
285
  translationMap: translationMapStore,
281
286
  slugIndex
282
287
  },
@@ -284,47 +289,85 @@ export default function baseline(settings = {}, options = {}) {
284
289
  helpers
285
290
  };
286
291
 
287
- // Page context registry
292
+ // Page context and SEO graph registries
288
293
  const pageContextRegistry = registerPageContext(eleventyConfig, coreContext);
294
+ const seoGraphRegistry = registerSeoGraph(eleventyConfig, coreContext);
295
+
296
+ // --- Content graph ---
297
+ // Cascade hookup for the content graph. Reads via the runtime getter so
298
+ // serve-mode rebuilds reassigning `contentGraph` are picked up.
299
+ function getNode(pageUrl) {
300
+ return coreContext.runtime.contentGraph?.nodes?.[pageUrl];
301
+ }
302
+
303
+ function getEdges() {
304
+ return coreContext.runtime.contentGraph?.edges ?? [];
305
+ }
306
+
307
+ eleventyConfig.addGlobalData('eleventyComputed._node', () => (data) => {
308
+ const pageUrl = data.page?.url;
309
+ if (!pageUrl) return undefined;
310
+
311
+ return getNode(pageUrl);
312
+ });
313
+
314
+ eleventyConfig.addGlobalData('eleventyComputed._backlinks', () => (data) => {
315
+ const edges = getEdges();
316
+
317
+ const pageUrl = data.page?.url;
318
+ if (!pageUrl) return [];
289
319
 
290
- // Wikilinks: [[slug]] / [[slug | lang]] in body markdown.
320
+ return edges.filter((edge) => edge.to === pageUrl);
321
+ });
322
+
323
+ eleventyConfig.addGlobalData('eleventyComputed._outgoing', () => (data) => {
324
+ const edges = getEdges();
325
+
326
+ const pageUrl = data.page?.url;
327
+ if (!pageUrl) return [];
328
+
329
+ return edges.filter((edge) => edge.from === pageUrl);
330
+ });
331
+
332
+ // --- Markdown engine ---
333
+ // Order matters: attrs first so manual ids are visible to auto-heading-ids'
334
+ // seed pass; wikilinks last since it parses inline tokens independently.
335
+ const mdLog = scopedLog('markdown');
291
336
  eleventyConfig.amendLibrary('md', (md) => {
292
- md.use(wikilinks, { slugIndex, pageContextRegistry, translationMapStore });
337
+ safeUse(md, 'curly_attributes', markdownItAttrs, undefined, mdLog);
338
+ safeUse(md, 'baseline_auto_heading_ids', autoHeadingIds, { slugify }, mdLog);
339
+ safeUse(md, 'baseline_wikilinks', wikilinks, { slugIndex, pageContextRegistry, translationMapStore }, mdLog);
293
340
  });
294
341
 
342
+ // --- Snapshots ---
295
343
  coreContext.snapshots = {
296
344
  contentMap: () => contentMapStore.snapshot(),
297
- pageContext: () => pageContextRegistry.snapshot()
298
- };
299
-
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
345
+ pageContext: () => pageContextRegistry.snapshot(),
346
+ seoGraph: () => seoGraphRegistry.snapshot()
307
347
  };
308
348
 
309
349
  // --- Module registry ---
310
350
  const moduleRegistry = [
311
- { when: features.multilang, name: 'multilang', plugin: multilangCore },
312
- { when: features.sitemap, name: 'sitemap', plugin: sitemapCore },
351
+ { when: state.features.multilang, name: 'multilang', plugin: multilangCore },
352
+ { when: state.features.sitemap, name: 'sitemap', plugin: sitemapCore },
313
353
  { name: 'navigator', plugin: navigatorCore },
314
- { when: features.head, name: 'head', plugin: headCore, consumes: { pageContext: true } },
315
- { when: features.assets, name: 'assets', plugin: assetsCore }
354
+ { when: state.features.head, name: 'head', plugin: headCore, consumes: { pageContext: true, seoGraph: true } },
355
+ { when: state.features.assets, name: 'assets', plugin: assetsCore }
316
356
  ];
317
357
 
358
+ const active = [];
318
359
  for (const entry of moduleRegistry) {
319
360
  const { when = true, name, plugin, consumes = {} } = entry;
320
361
  if (!when) continue;
321
362
  const moduleContext = {
322
363
  ...coreContext,
323
364
  log: scopedLog(name),
324
- resolvePageContext: consumes.pageContext ? pageContextRegistry : null
365
+ resolvePageContext: consumes.pageContext ? pageContextRegistry : null,
366
+ resolveSeoGraph: consumes.seoGraph ? seoGraphRegistry : null
325
367
  };
326
368
 
327
369
  eleventyConfig.addPlugin(plugin, moduleContext);
370
+ active.push(name);
328
371
  }
329
372
 
330
373
  // --- 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
  }