@apleasantview/eleventy-plugin-baseline 0.1.0-next.41 → 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 (39) hide show
  1. package/README.md +19 -19
  2. package/core/content-graph/extractors.js +63 -18
  3. package/core/content-graph/graph.js +5 -2
  4. package/core/dates/git-date.js +71 -0
  5. package/core/dates/index.js +55 -0
  6. package/core/locale/derive-lang.js +19 -0
  7. package/core/locale/index.js +6 -0
  8. package/core/locale/normalize-lang.js +13 -0
  9. package/core/locale/normalize-locale.js +20 -0
  10. package/core/locale/open-graph-locale.js +14 -0
  11. package/core/locale/resolve-default.js +27 -0
  12. package/core/locale/resolve-locale.js +16 -0
  13. package/core/markdown/wikilinks.js +1 -1
  14. package/core/page-context/build.js +120 -23
  15. package/core/schema.js +3 -1
  16. package/core/seo-graph/adapter.js +246 -0
  17. package/core/seo-graph/build.js +87 -0
  18. package/core/seo-graph/index.js +1 -0
  19. package/core/seo-graph/open-graph.js +130 -0
  20. package/core/seo-graph/register.js +42 -0
  21. package/core/seo-graph/schema.js +18 -0
  22. package/core/state.js +3 -1
  23. package/core/surface/index.js +1 -1
  24. package/core/types.js +3 -0
  25. package/core/utils/{normalize-languages.js → normalize-language-map.js} +14 -5
  26. package/core/utils/title-case-slug.js +15 -0
  27. package/index.js +15 -9
  28. package/modules/head/drivers/posthtml-head-elements.js +92 -10
  29. package/modules/head/index.js +16 -9
  30. package/modules/head/schema.js +7 -3
  31. package/modules/multilang/filters/i18n-default-translation.js +2 -4
  32. package/modules/multilang/filters/i18n-translation-in.js +2 -2
  33. package/modules/multilang/filters/i18n-translations-for.js +2 -2
  34. package/modules/multilang/index.js +78 -39
  35. package/modules/navigator/index.js +6 -5
  36. package/modules/sitemap/index.js +4 -4
  37. package/modules/sitemap/templates/sitemap-core.html +1 -1
  38. package/package.json +2 -1
  39. /package/core/{surface/global-date-function.js → dates/date-global.js} +0 -0
@@ -1,6 +1,12 @@
1
1
  import { I18nPlugin } from '@11ty/eleventy';
2
2
  import { DeepCopy } from '@11ty/eleventy-utils';
3
- import { normalizeLanguages } from '../../core/utils/normalize-languages.js';
3
+ import {
4
+ normalizeLang,
5
+ normalizeLocale,
6
+ deriveLang,
7
+ resolveDefault
8
+ } from '../../core/locale/index.js';
9
+ import { normalizeLanguageMap } from '../../core/utils/normalize-language-map.js';
4
10
  import i18nTranslationsFor from './filters/i18n-translations-for.js';
5
11
  import i18nTranslationIn from './filters/i18n-translation-in.js';
6
12
  import i18nDefaultTranslation from './filters/i18n-default-translation.js';
@@ -9,10 +15,10 @@ import i18nDefaultTranslation from './filters/i18n-default-translation.js';
9
15
  * Multilang (module)
10
16
  *
11
17
  * Language infrastructure. Normalises language config, builds translation
12
- * relationships, attaches per-page locale data, and exposes cross-language
13
- * lookup filters. Active only when options.multilingual is true and both
14
- * defaultLanguage and at least one languages entry are set; otherwise the
15
- * module exits early.
18
+ * relationships, attaches per-page lang / locale / translationKey /
19
+ * isDefaultLang fields, and exposes cross-language lookup filters. Active
20
+ * only when options.multilingual is true and both defaultLanguage and at
21
+ * least one languages entry are set; otherwise the module exits early.
16
22
  *
17
23
  * Architecture layer:
18
24
  * module
@@ -24,7 +30,8 @@ import i18nDefaultTranslation from './filters/i18n-default-translation.js';
24
30
  *
25
31
  * Lifecycle:
26
32
  * build-time → normalise languages, attach I18nPlugin, register filters
27
- * and computed page.locale
33
+ * and computed page.lang / page.locale / page.translationKey
34
+ * / page.isDefaultLang
28
35
  * cascade-time → translationsMap and translations collections build the
29
36
  * per-translationKey map and write it to the store
30
37
  *
@@ -35,15 +42,16 @@ import i18nDefaultTranslation from './filters/i18n-default-translation.js';
35
42
  * lifecycle boundary.
36
43
  *
37
44
  * Scope:
38
- * Owns language normalisation, page.locale computation, the translations
39
- * and translationsMap collections, and the i18n filters
40
- * (i18nTranslationsFor, i18nTranslationIn, i18nDefaultTranslation).
41
- * Does not own URL routing (I18nPlugin) or hreflang rendering (head).
45
+ * Owns language normalisation, per-page flat locale fields (lang, locale,
46
+ * translationKey, isDefaultLang), the translations and translationsMap
47
+ * collections, and the i18n filters (i18nTranslationsFor,
48
+ * i18nTranslationIn, i18nDefaultTranslation). Does not own URL routing
49
+ * (I18nPlugin) or hreflang rendering (head).
42
50
  *
43
51
  * Data flow:
44
- * settings.languages + page.lang/translationKey → normalisation +
45
- * I18nPlugin → collections + computed page.locale + translation-map
46
- * store → head, sitemap
52
+ * settings.languages + page.lang/locale/translationKey → normalisation
53
+ * + I18nPlugin → collections + flat computed page fields +
54
+ * translation-map store → head, sitemap
47
55
  *
48
56
  * @param {import("@11ty/eleventy/src/UserConfig.js").default} eleventyConfig
49
57
  * @param {Object} moduleContext
@@ -52,18 +60,18 @@ export function multilangCore(eleventyConfig, moduleContext) {
52
60
  const { state, runtime, log } = moduleContext;
53
61
  const { settings, options } = state;
54
62
 
55
- // --- Language normalization ---
56
- // Accept languages as array or object; normalize to object map.
57
- // Drives collection building, locale data, and sitemap-core language config.
58
- const normalizeLanguageCode = (lang) => (lang || '').toLowerCase().trim();
59
- const defaultLanguage = normalizeLanguageCode(settings.defaultLanguage);
60
- const languages = normalizeLanguages(settings, log);
63
+ // --- Default resolution ---
64
+ // resolveDefault returns { lang, locale } from settings.defaultLocale (preferred)
65
+ // or settings.defaultLanguage (cosmetic alias; locale derived via Intl.Locale,
66
+ // returning the bare language subtag when no region is given).
67
+ const { lang: defaultLanguage, locale: defaultLocale } = resolveDefault(settings);
68
+ const languages = normalizeLanguageMap(settings, log);
61
69
  const hasLanguages = languages && Object.keys(languages).length > 0;
62
70
 
63
71
  const isMultilingual = options.multilang === true && defaultLanguage && hasLanguages;
64
72
 
65
73
  if (!isMultilingual) {
66
- log.info('Multilang inactive, needs options.multilang, settings.defaultLanguage, and languages');
74
+ log.info('Multilang inactive, needs options.multilang, settings.defaultLanguage or defaultLocale, and languages');
67
75
  return;
68
76
  }
69
77
 
@@ -75,24 +83,50 @@ export function multilangCore(eleventyConfig, moduleContext) {
75
83
  errorMode: 'allow-fallback'
76
84
  });
77
85
 
78
- // Computed locale data: every page gets a page.locale object with its
79
- // resolved lang, translationKey, and whether it's the default language.
80
- eleventyConfig.addGlobalData('eleventyComputed.page.locale', () => {
81
- return (data) => {
82
- const translationKey = data.translationKey;
83
- const lang = normalizeLanguageCode(data.lang || data.language || defaultLanguage);
84
- const isDefaultLang = lang === defaultLanguage;
86
+ // --- Per-page resolvers ---
87
+ // Shared between the four flat eleventyComputed registrations below and
88
+ // the buildTranslations collection iterator. Closes over defaults and
89
+ // the languages map.
90
+ //
91
+ // Accept `language` as a writer-side alias for `lang`. Cheap, forgiving,
92
+ // and means existing front matter using either spelling keeps working.
93
+ // Also derives lang from data.locale when neither is set.
94
+ function resolvePageLang(data) {
95
+ return (
96
+ normalizeLang(data.lang || data.language || deriveLang(data.locale)) || defaultLanguage
97
+ );
98
+ }
85
99
 
86
- return {
87
- translationKey,
88
- lang,
89
- isDefaultLang
90
- };
91
- };
92
- });
100
+ function resolvePageLocale(data) {
101
+ if (data.locale) return normalizeLocale(data.locale);
102
+ const lang = resolvePageLang(data);
103
+ return normalizeLocale(languages?.[lang]?.locale) ?? defaultLocale;
104
+ }
105
+
106
+ // --- Computed per-page fields ---
107
+ // Four independent registrations merge cleanly at the leaves (validated
108
+ // 2026-05-25 via temp/workbench/multilang-glow-up/eleventy-probe/).
109
+ // Replaces the historical single-bag page.locale object with flat
110
+ // siblings on page.
111
+ eleventyConfig.addGlobalData(
112
+ 'eleventyComputed.page.lang',
113
+ () => (data) => resolvePageLang(data)
114
+ );
115
+ eleventyConfig.addGlobalData(
116
+ 'eleventyComputed.page.locale',
117
+ () => (data) => resolvePageLocale(data)
118
+ );
119
+ eleventyConfig.addGlobalData(
120
+ 'eleventyComputed.page.translationKey',
121
+ () => (data) => data.translationKey
122
+ );
123
+ eleventyConfig.addGlobalData(
124
+ 'eleventyComputed.page.isDefaultLang',
125
+ () => (data) => resolvePageLang(data) === defaultLanguage
126
+ );
93
127
 
94
128
  // Build a set of allowed language codes for validation during collection building.
95
- const allowedLanguages = new Set(Object.keys(languages).map(normalizeLanguageCode));
129
+ const allowedLanguages = new Set(Object.keys(languages).map(normalizeLang));
96
130
 
97
131
  // Build both the map (keyed by translationKey → lang) and the flat list.
98
132
  // Shared logic for both collections — called once per collection registration.
@@ -104,7 +138,7 @@ export function multilangCore(eleventyConfig, moduleContext) {
104
138
  const translationKey = page.data.translationKey;
105
139
  if (!translationKey) continue;
106
140
 
107
- const lang = page.data.lang || page.data.language || defaultLanguage;
141
+ const lang = resolvePageLang(page.data);
108
142
  if (!lang) continue;
109
143
 
110
144
  if (allowedLanguages.size && !allowedLanguages.has(lang)) {
@@ -112,8 +146,13 @@ export function multilangCore(eleventyConfig, moduleContext) {
112
146
  continue;
113
147
  }
114
148
 
115
- const locale = { locale: { translationKey, lang, isDefaultLang: lang === defaultLanguage } };
116
- const safeCopy = DeepCopy(page, locale);
149
+ const isDefaultLang = lang === defaultLanguage;
150
+ const locale = resolvePageLocale(page.data);
151
+
152
+ // Attach flat per-page fields. Mirrors the eleventyComputed shape
153
+ // so collection consumers read item.lang / item.locale /
154
+ // item.translationKey / item.isDefaultLang directly.
155
+ const safeCopy = DeepCopy(page, { lang, locale, translationKey, isDefaultLang });
117
156
  list.push(safeCopy);
118
157
 
119
158
  if (!map[translationKey]) map[translationKey] = {};
@@ -121,7 +160,7 @@ export function multilangCore(eleventyConfig, moduleContext) {
121
160
  title: page.data.title,
122
161
  url: page.url,
123
162
  lang,
124
- isDefaultLang: lang === defaultLanguage,
163
+ isDefaultLang,
125
164
  data: page.data
126
165
  };
127
166
  }
@@ -26,8 +26,8 @@ const __dirname = path.dirname(__filename);
26
26
  * Lifecycle:
27
27
  * build-time → register `_navigator` ({ nodes, edges, backlinks }),
28
28
  * debug globals, filters, and the optional virtual debug page
29
- * cascade-time → eleventyComputed `_snapshot` resolves contentMap and
30
- * pageContext on each page
29
+ * cascade-time → eleventyComputed `_snapshot` resolves contentMap,
30
+ * pageContext, and seoGraph on each page
31
31
  *
32
32
  * Why this exists:
33
33
  * Templates need an addressable cross-page surface for graph reads, and
@@ -55,11 +55,11 @@ const __dirname = path.dirname(__filename);
55
55
  * @param {Object} moduleContext
56
56
  * @param {Object} moduleContext.state - Resolved plugin state.
57
57
  * @param {Object} moduleContext.runtime - Lazy access layer; reads contentGraph.
58
- * @param {Object} moduleContext.snapshots - Thunks: { contentMap, pageContext }.
58
+ * @param {Object} moduleContext.snapshots - Thunks: { contentMap, pageContext, seoGraph }.
59
59
  */
60
60
  export function navigatorCore(eleventyConfig, moduleContext) {
61
61
  const { state, runtime, snapshots, log, env } = moduleContext;
62
- const { settings, options } = state;
62
+ const { options } = state;
63
63
 
64
64
  // Structural-only options check: log on mismatch, do not throw.
65
65
  const parsed = optionsSchema.safeParse(options.navigator);
@@ -78,7 +78,8 @@ export function navigatorCore(eleventyConfig, moduleContext) {
78
78
  eleventyConfig.addGlobalData('eleventyComputed._snapshot', () => {
79
79
  return () => ({
80
80
  contentMap: snapshots.contentMap(),
81
- pageContext: snapshots.pageContext()
81
+ pageContext: snapshots.pageContext(),
82
+ seoGraph: snapshots.seoGraph()
82
83
  });
83
84
  });
84
85
 
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { normalizeLanguages } from '../../core/utils/normalize-languages.js';
4
+ import { normalizeLanguageMap } from '../../core/utils/normalize-language-map.js';
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
@@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename);
19
19
  *
20
20
  * System role:
21
21
  * Reads the same normalised language map as multilang (via
22
- * core/utils/normalize-languages.js) and emits virtual templates that Eleventy
22
+ * core/utils/normalize-language-map.js) and emits virtual templates that Eleventy
23
23
  * renders to XML. Pages opt out via `noindex` in the cascade.
24
24
  *
25
25
  * Lifecycle:
@@ -37,7 +37,7 @@ const __dirname = path.dirname(__filename);
37
37
  * Owns computed page.sitemap and the virtual sitemap templates
38
38
  * (single-language /sitemap.xml, or per-lang /{lang}/sitemap.xml plus a
39
39
  * /sitemap.xml index).
40
- * Does not own language normalisation (core/utils/normalize-languages.js) or noindex
40
+ * Does not own language normalisation (core/utils/normalize-language-map.js) or noindex
41
41
  * propagation through the cascade.
42
42
  *
43
43
  * Data flow:
@@ -56,7 +56,7 @@ export function sitemapCore(eleventyConfig, moduleContext) {
56
56
  // Drives collection building, locale data, and sitemap-core language config.
57
57
  const normalizeLanguageCode = (lang) => (lang || '').toLowerCase().trim();
58
58
  const defaultLanguage = normalizeLanguageCode(settings.defaultLanguage);
59
- const languages = normalizeLanguages(settings, log);
59
+ const languages = normalizeLanguageMap(settings, log);
60
60
  const hasLanguages = languages && Object.keys(languages).length > 0;
61
61
  const isMultilingual = options.multilang === true && defaultLanguage && hasLanguages;
62
62
 
@@ -2,7 +2,7 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
3
3
  {%- if not settings.noindex %}
4
4
  {%- for item in collections.all %}
5
- {%- if not item.data.eleventyExcludeFromCollections and (not item.data.sitemap or item.data.sitemap.ignore != true) and item.data.noindex != true %}
5
+ {%- if item.url and not item.data.eleventyExcludeFromCollections and (not item.data.sitemap or item.data.sitemap.ignore != true) and item.data.noindex != true %}
6
6
  {%- set pageLang = item.data.lang or settings.defaultLanguage %}
7
7
  {%- if (not isMultilingual) or (not sitemapLang) or (pageLang == sitemapLang) %}
8
8
  {%- set absoluteUrl = item.url | htmlBaseUrl %}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apleasantview/eleventy-plugin-baseline",
3
- "version": "0.1.0-next.41",
3
+ "version": "0.1.0-next.42",
4
4
  "description": "An experimental Swiss army knife toolkit for Eleventy",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@11ty/eleventy-utils": "^2.0.7",
44
+ "@jdevalk/seo-graph-core": "^0.6.2",
44
45
  "@rviscomi/capo.js": "^2.1.0",
45
46
  "cssnano": "^7.1.2",
46
47
  "dotenv": "^17.2.3",