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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -23
  2. package/core/back-compat/options.js +69 -0
  3. package/core/content-graph/backlinks.js +65 -0
  4. package/core/content-graph/extractors.js +140 -0
  5. package/core/content-graph/graph.js +118 -0
  6. package/core/content-graph/index.js +2 -0
  7. package/core/content-graph/prepass.js +121 -0
  8. package/core/logging/banner.js +49 -0
  9. package/core/{logging.js → logging/index.js} +19 -2
  10. package/core/logging/quips.js +30 -0
  11. package/core/markdown/auto-heading-ids.js +86 -0
  12. package/core/markdown/index.js +5 -0
  13. package/core/markdown/safe-use.js +42 -0
  14. package/core/{wikilinks.js → markdown/wikilinks.js} +3 -3
  15. package/core/page-context/build.js +239 -0
  16. package/core/page-context/index.js +1 -0
  17. package/core/page-context/register.js +73 -0
  18. package/core/page-context/seo-helpers.js +56 -0
  19. package/core/schema.js +19 -1
  20. package/core/slug-index.js +2 -2
  21. package/core/state.js +73 -0
  22. package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
  23. package/core/surface/index.js +22 -0
  24. package/core/utils/add-trailing-slash.js +11 -0
  25. package/core/utils/ensure-dot-slash-dir.js +13 -0
  26. package/core/utils/normalize-languages.js +28 -0
  27. package/core/utils/resolve-field.js +9 -0
  28. package/core/utils/resolve-subdir.js +20 -0
  29. package/core/utils/slugify.js +15 -0
  30. package/core/utils/unique-by.js +25 -0
  31. package/core/virtual-dir.js +11 -10
  32. package/index.js +152 -115
  33. package/modules/assets/index.js +4 -2
  34. package/modules/assets/processors/esbuild-process.js +2 -2
  35. package/modules/assets/processors/postcss-process.js +2 -2
  36. package/modules/head/drivers/posthtml-head-elements.js +1 -3
  37. package/modules/head/index.js +7 -10
  38. package/modules/multilang/index.js +4 -2
  39. package/modules/navigator/index.js +33 -20
  40. package/modules/navigator/templates/navigator-core.html +1 -1
  41. package/modules/sitemap/index.js +7 -3
  42. package/package.json +4 -2
  43. package/core/filters/index.js +0 -4
  44. package/core/global-functions/index.js +0 -6
  45. package/core/page-context.js +0 -310
  46. package/core/shortcodes/index.js +0 -2
  47. package/core/utils/helpers.js +0 -75
  48. /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
  49. /package/core/{filters → surface/filters}/isString.js +0 -0
  50. /package/core/{filters → surface/filters}/related-posts.js +0 -0
  51. /package/core/{global-functions/date.js → surface/global-date-function.js} +0 -0
@@ -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/helpers.js';
4
+ import { normalizeLanguages } from '../../core/utils/normalize-languages.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/helpers.js) and emits virtual templates that Eleventy
22
+ * core/utils/normalize-languages.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/helpers.js) or noindex
40
+ * Does not own language normalisation (core/utils/normalize-languages.js) or noindex
41
41
  * propagation through the cascade.
42
42
  *
43
43
  * Data flow:
@@ -108,6 +108,8 @@ export function sitemapCore(eleventyConfig, moduleContext) {
108
108
  languages: languages,
109
109
  _internal: true
110
110
  });
111
+
112
+ log.info('Sitemaps written per-language');
111
113
  } else {
112
114
  eleventyConfig.addTemplate('_baseline/sitemap-core.html', baseContent, {
113
115
  permalink: '/sitemap.xml',
@@ -117,5 +119,7 @@ export function sitemapCore(eleventyConfig, moduleContext) {
117
119
  eleventyExcludeFromCollections: true,
118
120
  _internal: true
119
121
  });
122
+
123
+ log.info('Sitemap written');
120
124
  }
121
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apleasantview/eleventy-plugin-baseline",
3
- "version": "0.1.0-next.40",
3
+ "version": "0.1.0-next.41",
4
4
  "description": "An experimental Swiss army knife toolkit for Eleventy",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,7 +34,7 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/apleasantview/eleventy-plugin-baseline/issues"
36
36
  },
37
- "homepage": "https://eleventy-plugin-baseline.netlify.app/",
37
+ "homepage": "https://www.eleventy-baseline.dev/",
38
38
  "peerDependencies": {
39
39
  "@11ty/eleventy": ">=3.1.0",
40
40
  "@11ty/eleventy-img": "^6.0.4"
@@ -46,8 +46,10 @@
46
46
  "dotenv": "^17.2.3",
47
47
  "esbuild": "0.27.0",
48
48
  "kleur": "^4.1.5",
49
+ "linkedom": "^0.18.12",
49
50
  "luxon": "^3.7.2",
50
51
  "markdown-it": "^14.1.1",
52
+ "markdown-it-attrs": "^4.3.1",
51
53
  "postcss": "^8.5.6",
52
54
  "postcss-import": "^16.1.1",
53
55
  "postcss-import-ext-glob": "^2.1.1",
@@ -1,4 +0,0 @@
1
- // Filters barrel.
2
- export { markdownFilter } from './markdown.js';
3
- export { relatedPostsFilter } from './related-posts.js';
4
- export { isStringFilter } from './isString.js';
@@ -1,6 +0,0 @@
1
- import { registerDateGlobal } from './date.js';
2
-
3
- /** @param {import("@11ty/eleventy").UserConfig} eleventyConfig */
4
- export function registerGlobals(eleventyConfig) {
5
- registerDateGlobal(eleventyConfig);
6
- }
@@ -1,310 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- // Shortcodes barrel.
2
- export { imageShortcode } from './image.js';
@@ -1,75 +0,0 @@
1
- import { TemplatePath } from '@11ty/eleventy-utils';
2
- import slugifyLib from 'slugify';
3
-
4
- /**
5
- * Helper function to add trailing slash to a path
6
- * @param {string} path
7
- * @returns {string}
8
- */
9
- export function addTrailingSlash(path) {
10
- if (path.slice(-1) === '/') {
11
- return path;
12
- }
13
- return path + '/';
14
- }
15
-
16
- /**
17
- * Resolve a subdirectory under input and output.
18
- * Joins inputDir/outputDir with rawDir, normalises, and adds trailing slashes.
19
- * @param {string} inputDir - The input directory (e.g., "./src/").
20
- * @param {string} outputDir - The output directory (e.g., "./dist/").
21
- * @param {string} rawDir - Raw subdirectory value (e.g., "assets", "static").
22
- * @returns {{input: string, output: string}}
23
- */
24
- export function resolveSubdir(inputDir, outputDir, rawDir) {
25
- const joinedInput = TemplatePath.join(inputDir, rawDir || '');
26
- const joinedOutput = TemplatePath.join(outputDir, rawDir || '');
27
-
28
- return {
29
- input: addTrailingSlash(TemplatePath.standardizeFilePath(joinedInput)),
30
- output: addTrailingSlash(TemplatePath.standardizeFilePath(joinedOutput))
31
- };
32
- }
33
-
34
- /**
35
- * Slugify a string into a wikilink-friendly key.
36
- * Lowercases, strips diacritics, replaces non-alphanumerics with hyphens,
37
- * trims leading/trailing hyphens. Returns null for empty input.
38
- *
39
- * @param {string|null|undefined} input
40
- * @returns {string|null}
41
- */
42
- export function slugify(input) {
43
- if (input == null) return null;
44
- const slug = slugifyLib(String(input), { lower: true, strict: true, trim: true });
45
- return slug || null;
46
- }
47
-
48
- /**
49
- * Normalize language input to an object map.
50
- * Accepts an array of language codes or an object keyed by language code.
51
- * Returns null if input is invalid or empty.
52
- *
53
- * @param {Object} settings - Options object containing languages.
54
- * @param {import('../logging.js').BaselineLogger} [logger] - Logger for dropped-entry notice.
55
- * @returns {Record<string, Object>|null} Normalized language map, or null.
56
- */
57
- export function normalizeLanguages(settings, logger) {
58
- const normalizedLanguages = Array.isArray(settings.languages)
59
- ? Object.fromEntries(
60
- settings.languages
61
- .filter((lang) => typeof lang === 'string' && lang.trim())
62
- .map((lang) => [lang.toLowerCase().trim(), {}])
63
- )
64
- : settings.languages && typeof settings.languages === 'object'
65
- ? settings.languages
66
- : null;
67
-
68
- if (logger && Array.isArray(settings.languages)) {
69
- const normalizedCount = normalizedLanguages ? Object.keys(normalizedLanguages).length : 0;
70
- if (normalizedCount !== settings.languages.length) {
71
- logger.info('Some languages entries were invalid and were dropped.');
72
- }
73
- }
74
- return normalizedLanguages;
75
- }
File without changes