@apleasantview/eleventy-plugin-baseline 0.1.0-next.29 → 0.1.0-next.33
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 +33 -4
- package/core/debug.js +18 -3
- package/core/filters/isString.js +5 -0
- package/core/filters/markdown.js +6 -0
- package/core/filters/related-posts.js +6 -0
- package/core/helpers.js +6 -97
- package/core/logging.js +2 -2
- package/core/modules.js +0 -4
- package/core/shortcodes/image.js +162 -128
- package/eleventy.config.js +34 -22
- package/modules/assets-core/plugins/assets-core.js +126 -13
- package/modules/assets-esbuild/{filters/inline-esbuild.js → process.js} +10 -1
- package/modules/assets-postcss/fallback/postcss.config.js +1 -1
- package/modules/assets-postcss/process.js +49 -0
- package/modules/head-core/drivers/posthtml-head-elements.js +13 -18
- package/modules/head-core/plugins/head-core.js +19 -1
- package/modules/head-core/utils/head-utils.js +76 -10
- package/modules/multilang-core/plugins/multilang-core.js +26 -9
- package/modules/navigator-core/plugins/navigator-core.js +19 -1
- package/modules/navigator-core/templates/navigator-core.html +4 -4
- package/modules/sitemap-core/plugins/sitemap-core.js +23 -0
- package/modules/sitemap-core/templates/sitemap-core.html +27 -18
- package/modules/sitemap-core/templates/sitemap-index.html +1 -1
- package/package.json +1 -1
- package/modules/assets-esbuild/plugins/assets-esbuild.js +0 -71
- package/modules/assets-postcss/filters/inline-postcss.js +0 -38
- package/modules/assets-postcss/plugins/assets-postcss.js +0 -75
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
# Eleventy
|
|
1
|
+
# Eleventy Baseline
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
_An experimental Swiss army knife for Eleventy._
|
|
4
|
+
|
|
5
|
+
Eleventy Baseline is a lightweight toolkit built around a simple question:
|
|
6
|
+
|
|
7
|
+
> What if Eleventy had a minimal, optional layer of conventions — just enough to eliminate repetition, but not enough to feel restrictive?
|
|
8
|
+
|
|
9
|
+
It explores what a "core" for Eleventy could look like without becoming a framework or theme — small, practical tools rather than sweeping abstractions. If you've ever started a new Eleventy project and found yourself copy-pasting the same asset pipeline, the same head template, the same image shortcode for the third time, this is for you.
|
|
10
|
+
|
|
11
|
+
This is a practical, evolving baseline. Things might shift, break, or get renamed as the project evolves.
|
|
4
12
|
|
|
5
13
|
## Install
|
|
6
14
|
|
|
@@ -16,6 +24,8 @@ For a fresh project (install Eleventy and eleventy-img too):
|
|
|
16
24
|
npm install @11ty/eleventy @11ty/eleventy-img @apleasantview/eleventy-plugin-baseline
|
|
17
25
|
```
|
|
18
26
|
|
|
27
|
+
Requires Eleventy 3.x and Node >=20.
|
|
28
|
+
|
|
19
29
|
## Usage
|
|
20
30
|
|
|
21
31
|
In your Eleventy config (ESM):
|
|
@@ -34,13 +44,32 @@ export default function (eleventyConfig) {
|
|
|
34
44
|
export const config = baselineConfig;
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
## What's included
|
|
48
|
+
|
|
49
|
+
When the plugin loads, you get core filters, Nunjucks globals, debugging utilities, and an image shortcode (via eleventy-img) out of the box. On top of that, the plugin is organized into opt-in modules — take what you need:
|
|
50
|
+
|
|
51
|
+
| Module | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `assets-core` | Shared foundation for the asset pipeline |
|
|
54
|
+
| `assets-esbuild` | JS bundling via esbuild, with an inline injection filter for critical scripts |
|
|
55
|
+
| `assets-postcss` | CSS processing via PostCSS + cssnano, with an inline injection filter for critical styles |
|
|
56
|
+
| `head-core` | Drop `<baseline-head>` into your template and get sensible meta, canonical, og:image, and basic SEO defaults — processed by PostHTML at build time |
|
|
57
|
+
| `multilang-core` | Directory-based multilingual support: per-language collections, hreflang, sitemaps, and language normalization |
|
|
58
|
+
| `navigator-core` | Navigation tree helpers and a `_navigator` Nunjucks global |
|
|
59
|
+
| `sitemap-core` | XML sitemap generation with draft-page support |
|
|
38
60
|
|
|
39
61
|
## Docs
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
Full documentation — tutorials, how-to guides, and reference — lives at:
|
|
42
64
|
[https://eleventy-plugin-baseline.netlify.app/](https://eleventy-plugin-baseline.netlify.app/)
|
|
43
65
|
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Opinions, issues, and pull requests are welcome. If something doesn't work as documented, or
|
|
69
|
+
you've found a pattern that fits the spirit of the project,
|
|
70
|
+
[open an issue](https://github.com/apleasantview/eleventy-plugin-baseline/issues) and let's talk.
|
|
71
|
+
You can also find me on [Mastodon](https://mastodon.social/@crisverstraeten).
|
|
72
|
+
|
|
44
73
|
## License
|
|
45
74
|
|
|
46
75
|
MIT. See `LICENSE`.
|
package/core/debug.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { inspect as utilInspect } from 'node:util';
|
|
2
2
|
|
|
3
3
|
// Adapted from pdehaan - https://github.com/pdehaan/eleventy-plugin-debug
|
|
4
|
-
const debugOptions =
|
|
5
|
-
space: 0
|
|
6
|
-
});
|
|
4
|
+
const debugOptions = { space: 0 };
|
|
7
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Pretty-print an object using Node's util.inspect.
|
|
8
|
+
* @param {*} obj - Value to inspect.
|
|
9
|
+
* @param {Object} [options={}] - Options forwarded to util.inspect.
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
8
12
|
function inspect(obj, options = {}) {
|
|
9
13
|
return utilInspect(obj, options);
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Serialize an object to JSON.
|
|
18
|
+
* @param {*} obj - Value to serialize.
|
|
19
|
+
* @param {number} [space] - Indentation level (default 0, compact).
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
12
22
|
function json(obj, space = debugOptions.space) {
|
|
13
23
|
return JSON.stringify(obj, null, space);
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Return an object's own keys, sorted alphabetically.
|
|
28
|
+
* @param {Object} obj
|
|
29
|
+
* @returns {string[]}
|
|
30
|
+
*/
|
|
16
31
|
function keys(obj) {
|
|
17
32
|
return Object.keys(obj).sort();
|
|
18
33
|
}
|
package/core/filters/isString.js
CHANGED
package/core/filters/markdown.js
CHANGED
|
@@ -3,6 +3,12 @@ import markdownit from 'markdown-it';
|
|
|
3
3
|
|
|
4
4
|
const md = markdownit();
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Render a string as inline Markdown (no wrapping <p> tag).
|
|
8
|
+
* @param {string} string - Markdown source.
|
|
9
|
+
* @returns {string} HTML output.
|
|
10
|
+
*/
|
|
6
11
|
export const markdownFilter = (string) => {
|
|
12
|
+
if (!string) return '';
|
|
7
13
|
return md.renderInline(string);
|
|
8
14
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter the current page out of a collection.
|
|
3
|
+
* Uses `this.ctx.page` from the Nunjucks runtime to identify the current page.
|
|
4
|
+
* @param {Array<Object>} [collection=[]] - Collection to filter.
|
|
5
|
+
* @returns {Array<Object>} Collection without the current page.
|
|
6
|
+
*/
|
|
1
7
|
export default function relatedPostsFilter(collection = []) {
|
|
2
8
|
const page = this?.ctx?.page;
|
|
3
9
|
if (!page?.url) return collection;
|
package/core/helpers.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { TemplatePath } from '@11ty/eleventy-utils';
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -14,11 +13,12 @@ export function addTrailingSlash(path) {
|
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
* Resolves the assets directory
|
|
18
|
-
*
|
|
19
|
-
* @param {string} inputDir - The input directory (e.g., "./src/")
|
|
20
|
-
* @param {string}
|
|
21
|
-
* @
|
|
16
|
+
* Resolves the assets directory paths from config.
|
|
17
|
+
* Joins inputDir/outputDir with rawDir, normalizes, and adds trailing slashes.
|
|
18
|
+
* @param {string} inputDir - The input directory (e.g., "./src/").
|
|
19
|
+
* @param {string} outputDir - The output directory (e.g., "./dist/").
|
|
20
|
+
* @param {string} rawDir - Raw directory value from config (e.g., "assets").
|
|
21
|
+
* @returns {{assetsDir: string, assetsOutputDir: string}}
|
|
22
22
|
*/
|
|
23
23
|
export function resolveAssetsDir(inputDir, outputDir, rawDir) {
|
|
24
24
|
// Join input/output with assets subdir and normalize
|
|
@@ -34,94 +34,3 @@ export function resolveAssetsDir(inputDir, outputDir, rawDir) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Builds glob patterns for fast-glob (absolute paths)
|
|
39
|
-
* @param {string[]} patterns - User-provided patterns
|
|
40
|
-
* @param {string} assetsDir - Assets directory (relative, e.g., "./src/assets/")
|
|
41
|
-
* @returns {string[]} Absolute glob patterns
|
|
42
|
-
*/
|
|
43
|
-
export function buildGlobPatterns(patterns, assetsDir) {
|
|
44
|
-
const assetsDirAbsolute = TemplatePath.absolutePath(TemplatePath.stripLeadingDotSlash(assetsDir));
|
|
45
|
-
|
|
46
|
-
return patterns.map((pattern) => {
|
|
47
|
-
const normalized = TemplatePath.standardizeFilePath(pattern);
|
|
48
|
-
return normalized.startsWith('/') || path.isAbsolute(normalized)
|
|
49
|
-
? normalized
|
|
50
|
-
: TemplatePath.join(assetsDirAbsolute, normalized);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Extracts file metadata from a file path
|
|
56
|
-
* @param {string} filePath - Normalized file path
|
|
57
|
-
* @returns {{basename: string, fileSlug: string, inputFileExtension: string}}
|
|
58
|
-
*/
|
|
59
|
-
export function extractFileMetadata(filePath) {
|
|
60
|
-
const ext = path.extname(filePath); // Returns extension with dot (e.g., ".css") or ""
|
|
61
|
-
const inputFileExtension = ext && ext.length > 0 ? ext.slice(1) : '';
|
|
62
|
-
const basename = TemplatePath.getLastPathSegment(filePath, false);
|
|
63
|
-
const fileSlug = ext ? basename.slice(0, -ext.length) : basename;
|
|
64
|
-
|
|
65
|
-
return { basename, fileSlug, inputFileExtension };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Creates a collection item from a relative file path
|
|
70
|
-
* @param {string} inputPath - Relative path from project root (e.g., "./src/assets/css/index.css")
|
|
71
|
-
* @param {string} inputDir - Input directory (e.g., "./src/")
|
|
72
|
-
* @param {string} outputDir - Output directory (e.g., "./dist/")
|
|
73
|
-
* @param {string} assetsDirRelative - Assets directory relative to input (e.g., "assets")
|
|
74
|
-
* @param {string} passthroughOutput - Output path for passthrough
|
|
75
|
-
* @param {boolean} passthrough - Whether passthrough is enabled
|
|
76
|
-
* @returns {object} Collection item
|
|
77
|
-
*/
|
|
78
|
-
export function createCollectionItem(
|
|
79
|
-
inputPath,
|
|
80
|
-
inputDir,
|
|
81
|
-
outputDir,
|
|
82
|
-
assetsDirRelative,
|
|
83
|
-
passthroughOutput,
|
|
84
|
-
passthrough
|
|
85
|
-
) {
|
|
86
|
-
const { basename, fileSlug, inputFileExtension } = extractFileMetadata(inputPath);
|
|
87
|
-
|
|
88
|
-
// Get path relative to input directory
|
|
89
|
-
// e.g., inputPath = "./src/assets/css/index.css", inputDir = "./src/"
|
|
90
|
-
// relToInput = "assets/css/index.css"
|
|
91
|
-
const relToInput = TemplatePath.stripLeadingSubPath(inputPath, TemplatePath.addLeadingDotSlash(inputDir));
|
|
92
|
-
|
|
93
|
-
// outputPath: prepend output directory (with leading ./)
|
|
94
|
-
// e.g., relToInput = "assets/css/index.css", outputDir = "./dist/"
|
|
95
|
-
// outputPath = "./dist/assets/css/index.css"
|
|
96
|
-
const outputPath = TemplatePath.addLeadingDotSlash(
|
|
97
|
-
TemplatePath.normalize(TemplatePath.join(TemplatePath.addLeadingDotSlash(outputDir), relToInput))
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// relToAssets: path relative to assets directory for URL generation
|
|
101
|
-
// e.g., inputPath = "./src/assets/css/index.css", assetsDirRelative = "assets"
|
|
102
|
-
// relToAssets = "css/index.css"
|
|
103
|
-
const assetsDirPath = TemplatePath.addLeadingDotSlash(TemplatePath.join(inputDir, assetsDirRelative));
|
|
104
|
-
const relToAssets = TemplatePath.stripLeadingSubPath(inputPath, assetsDirPath);
|
|
105
|
-
|
|
106
|
-
const url = passthrough ? TemplatePath.join(passthroughOutput, relToAssets).replace(/\/$/, '') : undefined;
|
|
107
|
-
|
|
108
|
-
// filePathStem: path relative to input without extension, with leading slash
|
|
109
|
-
// e.g., relToInput = "assets/css/index.css"
|
|
110
|
-
// filePathStem = "/assets/css/index"
|
|
111
|
-
const filePathStem =
|
|
112
|
-
'/' +
|
|
113
|
-
(inputFileExtension
|
|
114
|
-
? relToInput.slice(0, -inputFileExtension.length - 1) // Remove extension and dot
|
|
115
|
-
: relToInput);
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
inputPath,
|
|
119
|
-
outputPath,
|
|
120
|
-
basename,
|
|
121
|
-
fileSlug,
|
|
122
|
-
inputFileExtension,
|
|
123
|
-
filePathStem,
|
|
124
|
-
dir: TemplatePath.getDirFromFilePath(inputPath),
|
|
125
|
-
url
|
|
126
|
-
};
|
|
127
|
-
}
|
package/core/logging.js
CHANGED
|
@@ -25,8 +25,8 @@ export function logIfVerbose(verbose, message, ...args) {
|
|
|
25
25
|
* @param {boolean} verbose - Whether verbose logging is enabled
|
|
26
26
|
* @param {string} message - Warning message
|
|
27
27
|
*/
|
|
28
|
-
export function warnIfVerbose(verbose, message) {
|
|
28
|
+
export function warnIfVerbose(verbose, message, ...args) {
|
|
29
29
|
if (verbose) {
|
|
30
|
-
console.warn(`[eleventy-plugin-baseline] WARN ${message}
|
|
30
|
+
console.warn(`[eleventy-plugin-baseline] WARN ${message}`, ...args);
|
|
31
31
|
}
|
|
32
32
|
}
|
package/core/modules.js
CHANGED
|
@@ -5,8 +5,6 @@ import { EleventyHtmlBasePlugin } from '@11ty/eleventy';
|
|
|
5
5
|
import multilangCore from '../modules/multilang-core/plugins/multilang-core.js';
|
|
6
6
|
import navigatorCore from '../modules/navigator-core/plugins/navigator-core.js';
|
|
7
7
|
import assetsCore from '../modules/assets-core/plugins/assets-core.js';
|
|
8
|
-
import assetsPostCSS from '../modules/assets-postcss/plugins/assets-postcss.js';
|
|
9
|
-
import assetsESBuild from '../modules/assets-esbuild/plugins/assets-esbuild.js';
|
|
10
8
|
import headCore from '../modules/head-core/plugins/head-core.js';
|
|
11
9
|
import sitemapCore from '../modules/sitemap-core/plugins/sitemap-core.js';
|
|
12
10
|
|
|
@@ -15,8 +13,6 @@ export default {
|
|
|
15
13
|
multilangCore,
|
|
16
14
|
navigatorCore,
|
|
17
15
|
assetsCore,
|
|
18
|
-
assetsPostCSS,
|
|
19
|
-
assetsESBuild,
|
|
20
16
|
headCore,
|
|
21
17
|
sitemapCore
|
|
22
18
|
};
|
package/core/shortcodes/image.js
CHANGED
|
@@ -1,128 +1,162 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import Image from '@11ty/eleventy-img';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_WIDTHS = [320, 640, 960, 1280];
|
|
5
|
-
const DEFAULT_FORMATS = ['avif', 'webp'
|
|
6
|
-
const DEFAULT_SIZES = '(max-width: 768px) 100vw, 768px';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {string}
|
|
26
|
-
* @param {
|
|
27
|
-
* @param {string} [options.
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {string} [options.
|
|
30
|
-
* @param {string} [options.
|
|
31
|
-
* @param {
|
|
32
|
-
* @param {string} [options.
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import Image from '@11ty/eleventy-img';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_WIDTHS = [320, 640, 960, 1280, 1920, 'auto'];
|
|
5
|
+
const DEFAULT_FORMATS = ['avif', 'webp'];
|
|
6
|
+
const DEFAULT_SIZES = '(max-width: 768px) 100vw, 768px';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pick the smallest and largest rendition from eleventy-img metadata.
|
|
10
|
+
* Uses the first available format; entries are ordered smallest → largest.
|
|
11
|
+
* @param {Object} metadata - eleventy-img metadata keyed by format.
|
|
12
|
+
* @returns {{lowsrc: Object, highsrc: Object}} Smallest and largest rendition.
|
|
13
|
+
*/
|
|
14
|
+
function pickRenditions(metadata) {
|
|
15
|
+
const firstFormat = Object.values(metadata)[0];
|
|
16
|
+
const lowsrc = firstFormat?.[0];
|
|
17
|
+
const highsrc = firstFormat?.[firstFormat.length - 1];
|
|
18
|
+
return { lowsrc, highsrc };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Responsive image shortcode using @11ty/eleventy-img.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {string} options.src Required image source (local or remote).
|
|
26
|
+
* @param {string} options.alt Required alt text (empty string allowed for decorative).
|
|
27
|
+
* @param {string} [options.caption=""] Optional caption; enables figure wrapper when non-empty.
|
|
28
|
+
* @param {("lazy"|"eager")} [options.loading="lazy"] Loading behavior.
|
|
29
|
+
* @param {string} [options.containerClass=""] Class applied to <picture>.
|
|
30
|
+
* @param {string} [options.imageClass=""] Class applied to <img>.
|
|
31
|
+
* @param {Array<number|string>} [options.widths=DEFAULT_WIDTHS] Widths passed to eleventy-img.
|
|
32
|
+
* @param {string} [options.sizes=DEFAULT_SIZES] Sizes attribute used on sources.
|
|
33
|
+
* @param {string[]} [options.formats=DEFAULT_FORMATS] Output formats (order matters).
|
|
34
|
+
* @param {string} [options.outputDir] Output directory for generated assets.
|
|
35
|
+
* @param {string} [options.urlPath="/media/"] Public URL base for generated assets.
|
|
36
|
+
* @param {Object} [options.attrs={}] Extra attributes applied to <img>; `class` merges with imageClass.
|
|
37
|
+
* @param {string} [options.style] Inline style applied to <img>. Separate from attrs.style — if both are passed, attrs.style takes precedence via restAttrs spread.
|
|
38
|
+
* @param {boolean} [options.figure=true] Wrap in <figure> when caption is provided.
|
|
39
|
+
* @param {boolean} [options.setDimensions=true] When false, omit width/height on <img>.
|
|
40
|
+
*/
|
|
41
|
+
export async function imageShortcode(options = {}) {
|
|
42
|
+
const outputBase = this?.eleventy?.directories?.output || 'dist';
|
|
43
|
+
const {
|
|
44
|
+
src,
|
|
45
|
+
alt,
|
|
46
|
+
caption = '',
|
|
47
|
+
loading = 'lazy',
|
|
48
|
+
containerClass = '',
|
|
49
|
+
imageClass = '',
|
|
50
|
+
style,
|
|
51
|
+
widths = DEFAULT_WIDTHS,
|
|
52
|
+
sizes = DEFAULT_SIZES,
|
|
53
|
+
formats = DEFAULT_FORMATS,
|
|
54
|
+
outputDir = path.join('.', outputBase, 'media'),
|
|
55
|
+
urlPath = '/media/',
|
|
56
|
+
attrs = {},
|
|
57
|
+
figure = true,
|
|
58
|
+
setDimensions = true
|
|
59
|
+
} = options;
|
|
60
|
+
// Read from global data set during plugin init. When true, `eleventy:ignore`
|
|
61
|
+
// is added to the <img> (line 140) to prevent double-processing.
|
|
62
|
+
const hasImageTransformPlugin = this.ctx._baseline.hasImageTransformPlugin;
|
|
63
|
+
|
|
64
|
+
// --- Validation and normalization ---
|
|
65
|
+
|
|
66
|
+
if (!src) throw new Error(`imageShortcode: src is required (received ${JSON.stringify(src)})`);
|
|
67
|
+
if (alt == null) {
|
|
68
|
+
console.warn('imageShortcode: alt is required (use empty string for decorative images)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normalizedCaption = String(caption);
|
|
72
|
+
const normalizedAlt = alt == null ? '' : String(alt);
|
|
73
|
+
|
|
74
|
+
const inputDir = this?.eleventy?.directories?.input;
|
|
75
|
+
const isRemote = /^https?:\/\//i.test(src);
|
|
76
|
+
// Note: remote URLs rely on eleventy-img's built-in fetch — no timeout/retry control at shortcode level.
|
|
77
|
+
const resolvedSrc = !isRemote && inputDir ? path.join(inputDir, src.replace(/^\//, '')) : src;
|
|
78
|
+
|
|
79
|
+
const imageOptions = {
|
|
80
|
+
widths: [...widths],
|
|
81
|
+
formats: [...formats],
|
|
82
|
+
outputDir,
|
|
83
|
+
urlPath,
|
|
84
|
+
filenameFormat(id, srcPath, width, format) {
|
|
85
|
+
const extension = path.extname(srcPath);
|
|
86
|
+
const name = path.basename(srcPath, extension);
|
|
87
|
+
return `${name}-${id.slice(0, 6)}-${width}w.${format}`;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// --- Image processing ---
|
|
92
|
+
// In serve mode, `transformOnRequest` defers processing to first browser request
|
|
93
|
+
// for faster dev startup. If it fails, retry without it — this is an edge case
|
|
94
|
+
// but one that has bitten in practice. In build mode, errors surface immediately.
|
|
95
|
+
|
|
96
|
+
let metadata;
|
|
97
|
+
try {
|
|
98
|
+
metadata = await Image(resolvedSrc, {
|
|
99
|
+
transformOnRequest: process.env.ELEVENTY_RUN_MODE === 'serve',
|
|
100
|
+
...imageOptions
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (process.env.ELEVENTY_RUN_MODE === 'serve') {
|
|
104
|
+
console.warn(`imageShortcode: transformOnRequest failed for ${src}, retrying.\n > ${error?.message || error}`);
|
|
105
|
+
metadata = await Image(resolvedSrc, imageOptions);
|
|
106
|
+
} else {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { lowsrc, highsrc } = pickRenditions(metadata);
|
|
112
|
+
if (!lowsrc || !highsrc) {
|
|
113
|
+
throw new Error(`imageShortcode: no renditions produced for ${src}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- HTML assembly ---
|
|
117
|
+
// One <source> per format, each carrying the full srcset for that format.
|
|
118
|
+
const sourceTags = Object.values(metadata)
|
|
119
|
+
.map((formatEntries) => {
|
|
120
|
+
const type = formatEntries[0].sourceType;
|
|
121
|
+
const srcset = formatEntries.map((entry) => entry.srcset).join(', ');
|
|
122
|
+
return `<source type="${type}" srcset="${srcset}" sizes="${sizes}">`;
|
|
123
|
+
})
|
|
124
|
+
.join('\n');
|
|
125
|
+
|
|
126
|
+
// Pull `class` out of attrs so it can merge with imageClass. Remaining attrs
|
|
127
|
+
// and `eleventy:ignore` (if needed) are spread onto imageAttributes below.
|
|
128
|
+
const { class: attrClass, ...restAttrs } = attrs;
|
|
129
|
+
const combinedClass = [imageClass, attrClass].filter(Boolean).join(' ').trim() || undefined;
|
|
130
|
+
|
|
131
|
+
const imageAttributes = {
|
|
132
|
+
src: lowsrc.url,
|
|
133
|
+
alt: normalizedAlt,
|
|
134
|
+
loading,
|
|
135
|
+
decoding: loading === 'eager' ? 'sync' : 'async',
|
|
136
|
+
class: combinedClass,
|
|
137
|
+
style,
|
|
138
|
+
...(setDimensions ? { width: highsrc.width, height: highsrc.height } : {}),
|
|
139
|
+
...restAttrs,
|
|
140
|
+
...(hasImageTransformPlugin ? { 'eleventy:ignore': true } : {})
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Build the attribute string, dropping any empty/null values to keep output clean.
|
|
144
|
+
const imgAttrString = Object.entries(imageAttributes)
|
|
145
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
146
|
+
.map(([key, value]) => (value === true ? key : `${key}="${value}"`))
|
|
147
|
+
.join(' ');
|
|
148
|
+
|
|
149
|
+
const pictureClass = containerClass && containerClass.trim() ? ` class="${containerClass.trim()}"` : '';
|
|
150
|
+
|
|
151
|
+
const picture = `<picture${pictureClass}>
|
|
152
|
+
${sourceTags}
|
|
153
|
+
<img ${imgAttrString}>
|
|
154
|
+
</picture>`;
|
|
155
|
+
|
|
156
|
+
if (!figure || !normalizedCaption) return picture;
|
|
157
|
+
|
|
158
|
+
return `<figure>
|
|
159
|
+
${picture}
|
|
160
|
+
<figcaption>${normalizedCaption}</figcaption>
|
|
161
|
+
</figure>`;
|
|
162
|
+
}
|