Files
lewisdale.dev/config/plugins/markdown-it-image.js
T
Lewis Dale f45b6308f9
Build and copy to prod / build-and-copy (push) Successful in 4m15s
Fix npm audit vulnerabilities and broken image build
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
2026-11-03 13:39:36 +00:00

83 lines
2.8 KiB
JavaScript

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 };
}