@apleasantview/eleventy-plugin-baseline 0.1.0-next.33 → 0.1.0-next.39
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.
- package/README.md +48 -23
- package/core/content-map-store.js +51 -0
- package/core/filters/index.js +4 -0
- package/core/filters/isString.js +1 -1
- package/core/filters/related-posts.js +1 -1
- package/core/global-functions/index.js +6 -0
- package/core/logging.js +25 -25
- package/core/page-context.js +310 -0
- package/core/registry.js +110 -0
- package/core/schema.js +37 -0
- package/core/shortcodes/image.js +8 -3
- package/core/shortcodes/index.js +2 -0
- package/core/slug-index.js +61 -0
- package/core/translation-map-store.js +46 -0
- package/core/types.js +73 -0
- package/core/utils/helpers.js +75 -0
- package/core/utils/pick.js +7 -0
- package/core/virtual-dir.js +111 -0
- package/core/wikilinks.js +152 -0
- package/index.js +364 -0
- package/modules/assets/index.js +162 -0
- package/modules/{assets-esbuild/process.js → assets/processors/esbuild-process.js} +3 -1
- package/modules/{assets-postcss/process.js → assets/processors/postcss-process.js} +5 -2
- package/modules/assets/schema.js +14 -0
- package/modules/head/drivers/capo-adapter.js +72 -0
- package/modules/head/drivers/posthtml-head-elements.js +140 -0
- package/modules/head/index.js +106 -0
- package/modules/head/schema.js +42 -0
- package/modules/head/utils/alternates.js +11 -0
- package/modules/head/utils/dedupe.js +47 -0
- package/modules/multilang/index.js +149 -0
- package/modules/navigator/index.js +140 -0
- package/modules/navigator/schema.js +13 -0
- package/modules/{navigator-core → navigator}/templates/navigator-core.html +10 -4
- package/{core → modules/navigator/utils}/debug.js +7 -1
- package/modules/sitemap/index.js +121 -0
- package/modules/{sitemap-core → sitemap}/templates/sitemap-core.html +2 -2
- package/modules/{sitemap-core → sitemap}/templates/sitemap-index.html +2 -2
- package/modules.js +6 -0
- package/package.json +15 -6
- package/core/filters.js +0 -9
- package/core/globals.js +0 -6
- package/core/helpers.js +0 -36
- package/core/modules.js +0 -18
- package/core/shortcodes.js +0 -3
- package/eleventy.config.js +0 -169
- package/modules/assets-core/plugins/assets-core.js +0 -197
- package/modules/head-core/drivers/posthtml-head-elements.js +0 -127
- package/modules/head-core/plugins/head-core.js +0 -75
- package/modules/head-core/utils/head-utils.js +0 -249
- package/modules/multilang-core/plugins/multilang-core.js +0 -118
- package/modules/navigator-core/plugins/navigator-core.js +0 -57
- package/modules/sitemap-core/plugins/sitemap-core.js +0 -88
- /package/core/{globals → global-functions}/date.js +0 -0
- /package/modules/{assets-postcss/fallback → assets/configs}/postcss.config.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-default-translation.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-translation-in.js +0 -0
- /package/modules/{multilang-core → multilang}/filters/i18n-translations-for.js +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# Eleventy Baseline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Baseline makes the structural decisions that Eleventy leaves open: directory layout, asset pipeline, image handling, SEO, sitemaps.
|
|
4
4
|
|
|
5
|
-
Eleventy
|
|
5
|
+
If you've started a new Eleventy project and found yourself wiring up the same things for the third time, this is for you. Directory structure, template engine, image formats, meta tags, asset bundling, sitemap — decisions that are individually small but collectively slow you down. Baseline makes them together, so they fit together. You get to skip the setup and start building.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You still own your site. Baseline handles the infrastructure — the parts that have well-tested answers. Your design, your content, the things that make your site yours — those stay yours.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
This is a practical, evolving baseline. Things might shift, break, or get renamed as the project evolves.
|
|
9
|
+
This is a working plugin, not a finished product. Things might shift, break, or get renamed.
|
|
12
10
|
|
|
13
11
|
## Install
|
|
14
12
|
|
|
@@ -28,35 +26,62 @@ Requires Eleventy 3.x and Node >=20.
|
|
|
28
26
|
|
|
29
27
|
## Usage
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Add the plugin and re-export the config. The config export sets the directory structure (`src/`, `dist/`, `_includes/`, `_data/`) so Eleventy and Baseline agree on where things live.
|
|
32
30
|
|
|
33
31
|
```js
|
|
34
32
|
import baseline, { config as baselineConfig } from '@apleasantview/eleventy-plugin-baseline';
|
|
35
33
|
|
|
36
34
|
export default function (eleventyConfig) {
|
|
37
|
-
eleventyConfig.addPlugin(baseline
|
|
38
|
-
// verbose: false,
|
|
39
|
-
// enableNavigatorTemplate: false,
|
|
40
|
-
// enableSitemapTemplate: true,
|
|
41
|
-
});
|
|
35
|
+
eleventyConfig.addPlugin(baseline());
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
export const config = baselineConfig;
|
|
45
39
|
```
|
|
46
40
|
|
|
41
|
+
The plugin takes two arguments: `settings` (site identity — title, url, languages, head extras) and `options` (runtime behavior — verbose, sitemap, navigator).
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const settings = {
|
|
45
|
+
title: 'My Site',
|
|
46
|
+
tagline: 'Built with Baseline',
|
|
47
|
+
url: 'https://www.example.com/',
|
|
48
|
+
defaultLanguage: 'en',
|
|
49
|
+
languages: {
|
|
50
|
+
en: { title: 'My Site' },
|
|
51
|
+
nl: { title: 'Mijn Site' }
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
eleventyConfig.addPlugin(
|
|
56
|
+
baseline(settings, {
|
|
57
|
+
verbose: false, // extra logging during builds
|
|
58
|
+
sitemap: true, // XML sitemap generation
|
|
59
|
+
navigator: false // debug page for inspecting template data
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
```
|
|
63
|
+
|
|
47
64
|
## What's included
|
|
48
65
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
The plugin registers everything on load. No setup beyond the config above.
|
|
67
|
+
|
|
68
|
+
**Core** — always active:
|
|
69
|
+
|
|
70
|
+
- An image shortcode (via eleventy-img) — AVIF and WebP, responsive widths, lazy loading. Alt text is required — the build warns if you skip it.
|
|
71
|
+
- Filters: `markdownify`, `relatedPosts`, `isString`
|
|
72
|
+
- A date-formatting global
|
|
73
|
+
- Drafts preprocessor — drafts stay out of production builds automatically
|
|
74
|
+
- Static passthrough (`src/static/` → site root)
|
|
75
|
+
|
|
76
|
+
**Modules** — opt-in, loaded individually:
|
|
77
|
+
|
|
78
|
+
| Module | What it does |
|
|
79
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `assets` | The asset pipeline. One entry point per directory (`index.css`, `index.js`). Bundles JS via esbuild and processes CSS via PostCSS. Inline filters (`inlinePostCSS`, `inlineESbuild`) for critical-path assets |
|
|
81
|
+
| `head` | `<head>` tags (charset, viewport, title, description, robots, canonical, hreflang) handled for you by dropping `<baseline-head>` in your layout |
|
|
82
|
+
| `multilang` | Directory-based multilingual support. Per-language collections, translation mapping, i18n filters. Wraps Eleventy's I18n plugin |
|
|
83
|
+
| `navigator` | Debug tooling. Globals for inspecting template data, plus debug filters (`_inspect`, `_json`, `_keys`). Optional virtual debug page |
|
|
84
|
+
| `sitemap` | XML sitemap. Every page is included unless you exclude it. Multilingual sites get per-language sitemaps plus an index |
|
|
60
85
|
|
|
61
86
|
## Docs
|
|
62
87
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getScope, addScopeListener, setEntry, getEntry } from './registry.js';
|
|
2
|
+
|
|
3
|
+
const SCOPE_NAME = 'core:content-map-store';
|
|
4
|
+
const KEY = 'contentMap';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Content map store (runtime substrate)
|
|
8
|
+
*
|
|
9
|
+
* Captures Eleventy's content map (emitted once per build via
|
|
10
|
+
* `eleventy.contentMap`) so late-lifecycle consumers can read it back. The
|
|
11
|
+
* store self-attaches its listener; callers create it once during plugin init.
|
|
12
|
+
*
|
|
13
|
+
* Architecture layer:
|
|
14
|
+
* runtime substrate
|
|
15
|
+
*
|
|
16
|
+
* System role:
|
|
17
|
+
* Capture point for the content map; read by page-context for canonical
|
|
18
|
+
* URL resolution.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle:
|
|
21
|
+
* cascade-time → listener writes the map when Eleventy emits it
|
|
22
|
+
* transform-time → consumers read via get()
|
|
23
|
+
*
|
|
24
|
+
* Why this exists:
|
|
25
|
+
* The content map is event-only; without a store, late-lifecycle consumers
|
|
26
|
+
* have no way to read it back.
|
|
27
|
+
*
|
|
28
|
+
* Scope:
|
|
29
|
+
* Owns capture and read of the content map.
|
|
30
|
+
* Does not own the map's shape (Eleventy's) or how consumers use it.
|
|
31
|
+
*
|
|
32
|
+
* Data flow:
|
|
33
|
+
* eleventy.contentMap event → registry scope → get()
|
|
34
|
+
*
|
|
35
|
+
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
|
|
36
|
+
* @returns {{get: () => object | null, snapshot: () => object | null}}
|
|
37
|
+
*/
|
|
38
|
+
export function createContentMapStore(eleventyConfig) {
|
|
39
|
+
const scope = getScope(eleventyConfig, SCOPE_NAME);
|
|
40
|
+
|
|
41
|
+
addScopeListener(eleventyConfig, SCOPE_NAME, 'eleventy.contentMap', 'write', (scope, data) => {
|
|
42
|
+
setEntry(scope, KEY, data);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const read = () => getEntry(scope, KEY) ?? null;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
get: read,
|
|
49
|
+
snapshot: read
|
|
50
|
+
};
|
|
51
|
+
}
|
package/core/filters/isString.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @param {Array<Object>} [collection=[]] - Collection to filter.
|
|
5
5
|
* @returns {Array<Object>} Collection without the current page.
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
7
|
+
export function relatedPostsFilter(collection = []) {
|
|
8
8
|
const page = this?.ctx?.page;
|
|
9
9
|
if (!page?.url) return collection;
|
|
10
10
|
return collection.filter((post) => post.url !== page.url);
|
package/core/logging.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
* Gets verbose flag from Eleventy global data
|
|
3
|
-
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
|
|
4
|
-
* @returns {boolean}
|
|
5
|
-
*/
|
|
6
|
-
export function getVerbose(eleventyConfig) {
|
|
7
|
-
const baselineData = eleventyConfig.globalData?._baseline;
|
|
8
|
-
return !!baselineData?.verbose;
|
|
9
|
-
}
|
|
1
|
+
import chalk from 'kleur';
|
|
10
2
|
|
|
11
3
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
15
|
-
* @
|
|
4
|
+
* @typedef {Object} BaselineLogger
|
|
5
|
+
* @property {(...args: unknown[]) => void} info Verbose-only.
|
|
6
|
+
* @property {(...args: unknown[]) => void} warn Always visible.
|
|
7
|
+
* @property {(...args: unknown[]) => void} error Always visible.
|
|
16
8
|
*/
|
|
17
|
-
export function logIfVerbose(verbose, message, ...args) {
|
|
18
|
-
if (verbose) {
|
|
19
|
-
console.log(`[eleventy-plugin-baseline] INFO ${message}`, ...args);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
9
|
|
|
23
10
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
11
|
+
* Create a namespaced logger. Prefix is `[baseline]` at plugin root and
|
|
12
|
+
* `[baseline:<namespace>]` inside modules. `info` is gated behind `verbose`;
|
|
13
|
+
* `warn` and `error` always emit.
|
|
14
|
+
*
|
|
15
|
+
* @param {string | null | undefined} namespace
|
|
16
|
+
* @param {{ verbose?: boolean }} [options]
|
|
17
|
+
* @returns {BaselineLogger}
|
|
27
18
|
*/
|
|
28
|
-
export function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
export function createLogger(namespace, { verbose = false } = {}) {
|
|
20
|
+
const label = namespace ? `[baseline/${namespace}]` : '[baseline]';
|
|
21
|
+
return {
|
|
22
|
+
info: (...args) => {
|
|
23
|
+
if (verbose) console.log(chalk.gray(label), ...args);
|
|
24
|
+
},
|
|
25
|
+
warn: (...args) => {
|
|
26
|
+
console.warn(chalk.yellow().bold(label), ...args);
|
|
27
|
+
},
|
|
28
|
+
error: (...args) => {
|
|
29
|
+
console.error(chalk.red().bold(label), ...args);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
32
|
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import pick from './utils/pick.js';
|
|
2
|
+
import { slugify } from './utils/helpers.js';
|
|
3
|
+
import { createLogger } from './logging.js';
|
|
4
|
+
import { getScope, memoize, setEntry } from './registry.js';
|
|
5
|
+
|
|
6
|
+
const SCOPE_NAME = 'core:page-context';
|
|
7
|
+
const COMPUTED_KEY = 'eleventyComputed._pageContext';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Page context (runtime substrate)
|
|
11
|
+
*
|
|
12
|
+
* A normalised per-page object built once at cascade-time and cached for
|
|
13
|
+
* transform-time consumers. The shape downstream modules read instead of
|
|
14
|
+
* re-deriving from raw cascade data.
|
|
15
|
+
*
|
|
16
|
+
* Architecture layer:
|
|
17
|
+
* runtime substrate
|
|
18
|
+
*
|
|
19
|
+
* System role:
|
|
20
|
+
* Lifecycle bridge between Eleventy's data cascade and the htmlTransformer.
|
|
21
|
+
* Head reads it via `getByKey`; navigator snapshots it for inspection.
|
|
22
|
+
*
|
|
23
|
+
* Lifecycle:
|
|
24
|
+
* cascade-time → eleventyComputed._pageContext builds and caches the context
|
|
25
|
+
* transform-time → consumers retrieve the cached context by page.url
|
|
26
|
+
*
|
|
27
|
+
* Why this exists:
|
|
28
|
+
* Eleventy's htmlTransformer context exposes only page metadata, not the
|
|
29
|
+
* data cascade. The cache lets transform-time consumers read the same
|
|
30
|
+
* normalised view that cascade-time produced.
|
|
31
|
+
*
|
|
32
|
+
* Scope:
|
|
33
|
+
* Owns the page-context shape, memoisation, key-based lookup, and snapshot.
|
|
34
|
+
* Does not own the meaning of any field; modules consume them as they see fit.
|
|
35
|
+
* Templates with `_internal: true` are skipped (synthetic sitemap pages, etc.).
|
|
36
|
+
*
|
|
37
|
+
* Data flow:
|
|
38
|
+
* data cascade → buildPageContext → registry scope → head, navigator
|
|
39
|
+
*
|
|
40
|
+
* @param {import("@11ty/eleventy").UserConfig} eleventyConfig
|
|
41
|
+
* @param {Object} coreContext - Resolved baseline core context (state, runtime, helpers).
|
|
42
|
+
*/
|
|
43
|
+
export function registerPageContext(eleventyConfig, coreContext) {
|
|
44
|
+
const { state, runtime, site } = coreContext;
|
|
45
|
+
const { slugIndex } = runtime;
|
|
46
|
+
const { settings, options } = state;
|
|
47
|
+
|
|
48
|
+
const log = createLogger(SCOPE_NAME, { verbose: options.verbose });
|
|
49
|
+
const scope = getScope(eleventyConfig, SCOPE_NAME);
|
|
50
|
+
|
|
51
|
+
// Head options.
|
|
52
|
+
const separator = options.head?.titleSeparator ?? ' – ';
|
|
53
|
+
const generator = options.head?.showGenerator ?? false;
|
|
54
|
+
|
|
55
|
+
function shouldSkip(data) {
|
|
56
|
+
if (data._internal) return true;
|
|
57
|
+
if (data.page?.outputFileExtension !== 'html') return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Helpers ---
|
|
62
|
+
const uniqueBy = (arr, keyFn) =>
|
|
63
|
+
Object.values(
|
|
64
|
+
(arr ?? []).reduce((acc, item) => {
|
|
65
|
+
if (!item) return acc;
|
|
66
|
+
|
|
67
|
+
const id = typeof keyFn === 'function' ? keyFn(item) : item?.[keyFn];
|
|
68
|
+
|
|
69
|
+
if (!id) {
|
|
70
|
+
acc[JSON.stringify(item)] = item;
|
|
71
|
+
return acc;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
acc[id] = item;
|
|
75
|
+
return acc;
|
|
76
|
+
}, {})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// --- SEO helpers ---
|
|
80
|
+
function stripTrackingParams(urlObj) {
|
|
81
|
+
['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid'].forEach((p) =>
|
|
82
|
+
urlObj.searchParams.delete(p)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
urlObj.hash = '';
|
|
86
|
+
return urlObj;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractFirstParagraph(data) {
|
|
90
|
+
const html = data?.content;
|
|
91
|
+
if (!html) return null;
|
|
92
|
+
const match = html.match(/<p>(.*?)<\/p>/i);
|
|
93
|
+
return match?.[1] ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeCanonical(path, siteUrl) {
|
|
97
|
+
if (!path || !siteUrl) return null;
|
|
98
|
+
|
|
99
|
+
const url = new URL(path, siteUrl);
|
|
100
|
+
|
|
101
|
+
url.hash = '';
|
|
102
|
+
|
|
103
|
+
return stripTrackingParams(url).href;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Field resolver ---
|
|
107
|
+
function resolveField({ pageValue, siteValue, fallbackValue, isHome }) {
|
|
108
|
+
let value = pageValue ?? siteValue ?? fallbackValue ?? null;
|
|
109
|
+
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Builders ---
|
|
114
|
+
function buildSite(lang, userSettings) {
|
|
115
|
+
const langEntry = lang ? userSettings.languages?.[lang] : undefined;
|
|
116
|
+
return {
|
|
117
|
+
title: langEntry?.title ?? userSettings.title ?? '',
|
|
118
|
+
tagline: langEntry?.tagline ?? userSettings.tagline ?? '',
|
|
119
|
+
description: langEntry?.description ?? userSettings.description ?? '',
|
|
120
|
+
url: userSettings.url ?? '',
|
|
121
|
+
noindex: userSettings.noindex === true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildPage(pageInput) {
|
|
126
|
+
return {
|
|
127
|
+
inputPath: pageInput?.inputPath ?? null,
|
|
128
|
+
fileSlug: pageInput?.fileSlug ?? null,
|
|
129
|
+
filePathStem: pageInput?.filePathStem ?? null,
|
|
130
|
+
outputFileExtension: pageInput?.outputFileExtension ?? null,
|
|
131
|
+
templateSyntax: pageInput?.templateSyntax ?? null,
|
|
132
|
+
date: pageInput?.date ?? null,
|
|
133
|
+
url: pageInput?.url ?? null,
|
|
134
|
+
outputPath: pageInput?.outputPath ?? null,
|
|
135
|
+
lang: pageInput?.lang ?? null,
|
|
136
|
+
locale: pageInput?.locale ?? null,
|
|
137
|
+
sitemap: pageInput?.sitemap ?? null
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildEntry(data) {
|
|
142
|
+
const rawSlug = data?.slug ?? data?.page?.fileSlug;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
title: data?.seo?.title ?? data?.title ?? null,
|
|
146
|
+
description: data?.seo?.description ?? data?.description ?? null,
|
|
147
|
+
excerpt: data?.excerpt ?? null,
|
|
148
|
+
slug: slugify(rawSlug),
|
|
149
|
+
head: data?.head ?? null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildQuery({ entry, page }) {
|
|
154
|
+
return {
|
|
155
|
+
isHome: page.url === '/'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildMeta({ data, site, page, query }) {
|
|
160
|
+
const noindex = site.noindex || data?.noindex === true;
|
|
161
|
+
|
|
162
|
+
const robots = noindex
|
|
163
|
+
? 'noindex, nofollow'
|
|
164
|
+
: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1';
|
|
165
|
+
|
|
166
|
+
const contentMap = runtime.contentMap;
|
|
167
|
+
|
|
168
|
+
const siteTitle = site.title;
|
|
169
|
+
const siteDescription = site.description;
|
|
170
|
+
const tagline = site.tagline;
|
|
171
|
+
|
|
172
|
+
const pageTitle = data?.seo?.title ?? data?.title ?? siteTitle;
|
|
173
|
+
const pageDescription = data?.seo?.description ?? data?.description ?? data?.excerpt ?? extractFirstParagraph(data);
|
|
174
|
+
|
|
175
|
+
function enhance(value) {
|
|
176
|
+
if (query.isHome && !data?.seo?.title && tagline) {
|
|
177
|
+
return `${siteTitle}${separator}${tagline}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!query.isHome && pageTitle && siteTitle && pageTitle !== siteTitle) {
|
|
181
|
+
return `${pageTitle}${separator}${siteTitle}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- DESCRIPTION ----
|
|
188
|
+
const description = resolveField({
|
|
189
|
+
pageValue: pageDescription,
|
|
190
|
+
siteValue: siteDescription,
|
|
191
|
+
isHome: query.isHome
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---- TITLE ----
|
|
195
|
+
const base = resolveField({
|
|
196
|
+
pageValue: pageTitle,
|
|
197
|
+
siteValue: siteTitle
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const title = enhance(base);
|
|
201
|
+
|
|
202
|
+
// ---- CANONICAL ----
|
|
203
|
+
let canonical = null;
|
|
204
|
+
|
|
205
|
+
if (!noindex) {
|
|
206
|
+
const rawCanonical =
|
|
207
|
+
data?.canonical ?? page.url ?? (page.inputPath && contentMap?.inputPathToUrl?.[page.inputPath]?.[0]);
|
|
208
|
+
|
|
209
|
+
canonical = normalizeCanonical(rawCanonical, site.url);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
title,
|
|
214
|
+
description,
|
|
215
|
+
canonical,
|
|
216
|
+
robots,
|
|
217
|
+
noindex
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildRender(data) {
|
|
222
|
+
return {
|
|
223
|
+
generator: data?.eleventy?.generator ?? null
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// HEAD (global + page-level merge + dedupe)
|
|
228
|
+
function buildHead({ userSettings, data }) {
|
|
229
|
+
const userHead = userSettings.head ?? {};
|
|
230
|
+
const pageHead = data?.head ?? {};
|
|
231
|
+
|
|
232
|
+
const link = uniqueBy([...(userHead.link ?? []), ...(pageHead.link ?? [])], (item) => {
|
|
233
|
+
if (item?.rel === 'canonical') {
|
|
234
|
+
try {
|
|
235
|
+
return normalizeCanonical(item.href, site.url);
|
|
236
|
+
} catch {
|
|
237
|
+
return item?.href;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return item?.href;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const script = uniqueBy([...(userHead.script ?? []), ...(pageHead.script ?? [])], 'src');
|
|
244
|
+
|
|
245
|
+
const style = uniqueBy([...(userHead.style ?? []), ...(pageHead.style ?? [])], 'href');
|
|
246
|
+
|
|
247
|
+
const meta = uniqueBy([...(userHead.meta ?? []), ...(pageHead.meta ?? [])], 'name');
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
link,
|
|
251
|
+
script,
|
|
252
|
+
style,
|
|
253
|
+
meta
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Main context builder.
|
|
259
|
+
* Pure transformation: Eleventy data → normalised page context.
|
|
260
|
+
*/
|
|
261
|
+
function buildPageContext(data) {
|
|
262
|
+
const pageInput = data.page ?? {};
|
|
263
|
+
const userSettings = data.settings ?? settings;
|
|
264
|
+
|
|
265
|
+
const page = buildPage(pageInput);
|
|
266
|
+
const site = buildSite(page.lang, userSettings);
|
|
267
|
+
const entry = buildEntry(data);
|
|
268
|
+
const query = buildQuery({ entry, page });
|
|
269
|
+
const meta = buildMeta({ data, site, page, query });
|
|
270
|
+
const render = buildRender(data);
|
|
271
|
+
const head = buildHead({ userSettings, data });
|
|
272
|
+
|
|
273
|
+
const context = {
|
|
274
|
+
site,
|
|
275
|
+
page,
|
|
276
|
+
entry,
|
|
277
|
+
query,
|
|
278
|
+
meta,
|
|
279
|
+
render,
|
|
280
|
+
head
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const inspectionKey = context.page.url ?? context.page.inputPath;
|
|
284
|
+
if (inspectionKey) setEntry(scope, inspectionKey, context);
|
|
285
|
+
|
|
286
|
+
if (slugIndex && entry.slug && page.url) {
|
|
287
|
+
const eligible = page.locale?.isDefaultLang === true;
|
|
288
|
+
if (eligible) {
|
|
289
|
+
slugIndex.set(entry.slug, page.url, page.inputPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return context;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
eleventyConfig.addGlobalData(COMPUTED_KEY, () => {
|
|
297
|
+
return (data) => {
|
|
298
|
+
if (shouldSkip(data)) return null;
|
|
299
|
+
return memoize(scope, data, buildPageContext);
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
log.info('Page context added to the data cascade and registry exposed');
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
get: (data) => scope.cache.get(data),
|
|
307
|
+
getByKey: (key) => scope.values.get(key),
|
|
308
|
+
snapshot: () => Object.fromEntries(scope.values)
|
|
309
|
+
};
|
|
310
|
+
}
|
package/core/registry.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State isolation kernel (registry)
|
|
3
|
+
*
|
|
4
|
+
* Per-config scopes that hold caches, named values, and listener dedup sets.
|
|
5
|
+
* Every store and sub-registry in the substrate borrows a scope here; no
|
|
6
|
+
* feature data lives directly inside.
|
|
7
|
+
*
|
|
8
|
+
* Architecture layer:
|
|
9
|
+
* registry
|
|
10
|
+
*
|
|
11
|
+
* System role:
|
|
12
|
+
* Underpins virtual-dir, content-map store, translation-map store, and the
|
|
13
|
+
* page-context registry. The seam between eleventyConfig and any long-lived
|
|
14
|
+
* runtime state Baseline carries.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle:
|
|
17
|
+
* build-time → scopes created on demand
|
|
18
|
+
* cascade-time → values populated by store writers
|
|
19
|
+
* transform-time → values read by transform-time consumers
|
|
20
|
+
*
|
|
21
|
+
* Why this exists:
|
|
22
|
+
* Eleventy has no first-class place to hang per-config singletons. A
|
|
23
|
+
* WeakMap keyed by eleventyConfig keeps state isolated across reloads and
|
|
24
|
+
* parallel builds without leaks, and gives every consumer a stable slot.
|
|
25
|
+
*
|
|
26
|
+
* Scope:
|
|
27
|
+
* Owns scope creation, listener dedup, and identity-keyed memoisation.
|
|
28
|
+
* Does not own any feature data; only the containers feature code lives in.
|
|
29
|
+
*
|
|
30
|
+
* Data flow:
|
|
31
|
+
* eleventyConfig → scope → { cache, values, listeners } → consumer
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const roots = new WeakMap();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get or create the per-eleventyConfig root that holds all named scopes.
|
|
38
|
+
*/
|
|
39
|
+
function getRoot(eleventyConfig) {
|
|
40
|
+
let root = roots.get(eleventyConfig);
|
|
41
|
+
if (!root) {
|
|
42
|
+
root = new Map();
|
|
43
|
+
roots.set(eleventyConfig, root);
|
|
44
|
+
}
|
|
45
|
+
return root;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get or create a named scope bound to an Eleventy config instance.
|
|
50
|
+
*
|
|
51
|
+
* A scope is a per-config bag of state: a cache (for identity-keyed
|
|
52
|
+
* memoisation), a values map (for named entries), and a listeners set
|
|
53
|
+
* (for deduping event hookups).
|
|
54
|
+
*
|
|
55
|
+
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
|
|
56
|
+
* @param {string} name - Scope identifier (e.g. 'page-context', 'content-store').
|
|
57
|
+
* @returns {{cache: WeakMap, values: Map, listeners: Set<string>}}
|
|
58
|
+
*/
|
|
59
|
+
export function getScope(eleventyConfig, name) {
|
|
60
|
+
const root = getRoot(eleventyConfig);
|
|
61
|
+
|
|
62
|
+
if (!root.has(name)) {
|
|
63
|
+
root.set(name, {
|
|
64
|
+
cache: new WeakMap(),
|
|
65
|
+
values: new Map(),
|
|
66
|
+
listeners: new Set()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return root.get(name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Attach an Eleventy event listener once per (event, key) pair within a scope.
|
|
75
|
+
*
|
|
76
|
+
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
|
|
77
|
+
* @param {string} scopeName
|
|
78
|
+
* @param {string} eventName - Eleventy event name (e.g. 'eleventy.contentMap').
|
|
79
|
+
* @param {string} listenerKey - Stable identifier for dedup.
|
|
80
|
+
* @param {(scope: object, payload: any) => void} handler
|
|
81
|
+
*/
|
|
82
|
+
export function addScopeListener(eleventyConfig, scopeName, eventName, listenerKey, handler) {
|
|
83
|
+
const scope = getScope(eleventyConfig, scopeName);
|
|
84
|
+
const dedupKey = `${eventName}::${listenerKey}`;
|
|
85
|
+
|
|
86
|
+
if (scope.listeners.has(dedupKey)) return;
|
|
87
|
+
scope.listeners.add(dedupKey);
|
|
88
|
+
|
|
89
|
+
eleventyConfig.on(eventName, (payload) => handler(scope, payload));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Memoise a value in the scope's cache by object identity.
|
|
94
|
+
*/
|
|
95
|
+
export function memoize(scope, key, factory) {
|
|
96
|
+
if (scope.cache.has(key)) {
|
|
97
|
+
return scope.cache.get(key);
|
|
98
|
+
}
|
|
99
|
+
const value = factory(key);
|
|
100
|
+
scope.cache.set(key, value);
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setEntry(scope, name, value) {
|
|
105
|
+
scope.values.set(name, value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getEntry(scope, name) {
|
|
109
|
+
return scope.values.get(name);
|
|
110
|
+
}
|