Fix npm audit vulnerabilities and broken image build
Build and copy to prod / build-and-copy (push) Successful in 4m15s

Eleventy Image v7 removed its synchronous stats API, which broke the
build. Rather than downgrade (older versions bundle a vulnerable sharp),
adapt to v7 and clear out the remaining vulnerable dependencies:

- Make the `image` shortcode async
- Replace the unmaintained markdown-it-eleventy-img with a local plugin
  that pre-loads image metadata in eleventy.before, so markdown (and
  feed) images keep rendering as <picture> elements
- Upgrade @11ty/eleventy-fetch to v6
- Swap npm-run-all for the maintained npm-run-all2 fork
- Override eleventy-plugin-og-image's satori (and its pinned fflate) to
  patched versions, and set explicit heading weights in the OG template
  so newer satori doesn't synthesise bold text
This commit is contained in:
Lewis Dale committed 2026-11-03 13:39:36 +00:00
1 parent 13d0b244e5
commit f45b6308f9
6 files changed
+284 -2427

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
import path from "node:path";
import fs from "node:fs/promises";
import Image from "@11ty/eleventy-img";
// Some files in src/images have a literal query string in their filename (e.g. `foo.jpeg?width=300`)
const IMAGE_EXTENSIONS = /\.(jpe?g|png|webp|gif|avif|svg|tiff?)(\?.*)?$/i;
/**
* Renders local markdown images as responsive <picture> elements via Eleventy Image.
*
* Replaces `markdown-it-eleventy-img`, which is unmaintained and pinned to an old
* (vulnerable) Eleventy Image + sharp. Eleventy Image v7 dropped its synchronous
* stats API, but markdown-it renders synchronously, so image metadata is read
* up-front in `eleventy.before` and looked up while rendering. Images are only
* generated once a page actually references them, and `eleventy.after` waits for
* those to finish.
*
* Remote images are passed through to markdown-it's default renderer untouched.
*/
export function createMarkdownImages({ imageDir, imgOptions = {}, globalAttributes = {} }) {
const metadataByPath = new Map();
let pendingGenerations = [];
async function loadMetadata() {
const entries = await fs.readdir(imageDir, { recursive: true, withFileTypes: true });
const files = entries
.filter(entry => entry.isFile() && IMAGE_EXTENSIONS.test(entry.name))
.map(entry => path.join(entry.parentPath, entry.name));
await Promise.all(files.map(async file => {
const metadata = await Image(file, { ...imgOptions, statsOnly: true });
metadataByPath.set(path.normalize(file), metadata);
}));
}
async function waitForGenerations() {
const generations = pendingGenerations;
pendingGenerations = [];
await Promise.all(generations);
}
function markdownItPlugin(md) {
md.renderer.rules.image = (tokens, index, rendererOptions, env, renderer) => {
const token = tokens[index];
const src = token.attrGet("src");
if (Image.Util.isRemoteUrl(src)) {
token.attrSet("alt", token.content);
return renderer.renderToken(tokens, index, rendererOptions);
}
const filePath = path.normalize(decodeURIComponent(src));
const metadata = metadataByPath.get(filePath);
if (!metadata) {
throw new Error(`Markdown image "${src}" was not found in ${imageDir}. Local markdown images must live in that directory.`);
}
pendingGenerations.push(Image(filePath, imgOptions));
const tokenAttributes = Object.fromEntries(
token.attrs
.filter(([name]) => name !== "src")
.map(([name, value]) => [name.toLowerCase().trim(), value])
);
return Image.generateHTML(metadata, {
...globalAttributes,
...tokenAttributes,
alt: token.content,
}, {
whitespaceMode: "inline"
});
};
}
function eleventyPlugin(eleventyConfig) {
eleventyConfig.on("eleventy.before", loadMetadata);
eleventyConfig.on("eleventy.after", waitForGenerations);
}
return { markdownItPlugin, eleventyPlugin };
}