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

+3 -6
View File
@@ -9,15 +9,14 @@ const defaultSizes = `
(min-width: 2500px) 3840px (min-width: 2500px) 3840px
`; `;
const imageShortcode = (src, alt, cls, sizes = defaultSizes, widths = [300, 600, 1000, 1440, 2500, 3840]) => { const imageShortcode = async (src, alt, cls, sizes = defaultSizes, widths = [300, 600, 1000, 1440, 2500, 3840]) => {
let options = { let options = {
widths: widths, widths: widths,
formats: ['webp', 'jpeg'], formats: ['webp', 'jpeg'],
outputDir: "./_site/img", outputDir: "./_site/img",
}; };
// generate images, while this is async we don’t wait let metadata = await Image(src, options);
Image(src, options);
let imageAttributes = { let imageAttributes = {
class: cls, class: cls,
@@ -25,8 +24,6 @@ const imageShortcode = (src, alt, cls, sizes = defaultSizes, widths = [300, 600,
sizes, sizes,
decoding: "async", decoding: "async",
}; };
// get metadata even the images are not fully generated
let metadata = Image.statsSync(src, options);
return Image.generateHTML(metadata, imageAttributes); return Image.generateHTML(metadata, imageAttributes);
} }
@@ -49,6 +46,6 @@ const remoteImageShortcode = async (src, alt, cls, sizes = defaultSizes, widths
return Image.generateHTML(metadata, imageAttributes); return Image.generateHTML(metadata, imageAttributes);
} }
export default function(eleventyConfig) { export default function(eleventyConfig) {
eleventyConfig.addShortcode("image", imageShortcode); eleventyConfig.addAsyncShortcode("image", imageShortcode);
eleventyConfig.addAsyncShortcode("remoteImage", remoteImageShortcode); eleventyConfig.addAsyncShortcode("remoteImage", remoteImageShortcode);
} }
+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 };
}
+20 -16
View File
@@ -9,12 +9,29 @@ import { full as markdownItEmoji } from 'markdown-it-emoji';
import markdownItFootnote from 'markdown-it-footnote'; import markdownItFootnote from 'markdown-it-footnote';
import markdownitMark from 'markdown-it-mark'; import markdownitMark from 'markdown-it-mark';
import markdownitAbbr from 'markdown-it-abbr'; import markdownitAbbr from 'markdown-it-abbr';
import markdownItEleventyImg from 'markdown-it-eleventy-img';
import markdownItCallouts from 'markdown-it-obsidian-callouts'; import markdownItCallouts from 'markdown-it-obsidian-callouts';
import { slugifyString } from '../utils.js'; import { slugifyString } from '../utils.js';
import { createMarkdownImages } from './markdown-it-image.js';
const icons = JSON.parse(fs.readFileSync('./config/plugins/icons.json'), 'utf-8'); const icons = JSON.parse(fs.readFileSync('./config/plugins/icons.json'), 'utf-8');
const markdownImages = createMarkdownImages({
imageDir: path.join("src", "images"),
imgOptions: {
widths: [300, 600, 1000, 1440],
urlPath: "/img/",
outputDir: path.join("_site", "img"),
formats: ["avif", "webp", "jpeg"]
},
globalAttributes: {
sizes: `(max-width: 300px) 300px,
(max-width: 600px) 600px,
(max-width: 1000px) 1000px,
(max-width: 1440px) 1440px,
100%`
}
});
const markdownLib = markdownIt({ const markdownLib = markdownIt({
html: true, html: true,
breaks: true, breaks: true,
@@ -53,27 +70,14 @@ const markdownLib = markdownIt({
.use(markdownitMark) .use(markdownitMark)
.use(markdownitAbbr) .use(markdownitAbbr)
.use(markdownItCallouts) .use(markdownItCallouts)
.use(markdownItEleventyImg, { .use(markdownImages.markdownItPlugin);
imgOptions: {
widths: [300, 600, 1000, 1440],
urlPath: "/img/",
outputDir: path.join("_site", "img"),
formats: ["avif", "webp", "jpeg"]
},
globalAttributes: {
sizes: `(max-width: 300px) 300px,
(max-width: 600px) 600px,
(max-width: 1000px) 1000px,
(max-width: 1440px) 1440px,
100%`
}
});
markdownLib.renderer.rules.emoji = function(token, idx) { markdownLib.renderer.rules.emoji = function(token, idx) {
return `<span class="fa-li"><i class="fa-solid fa-${token[idx].markup}" aria-hidden="true"></i></span>`; return `<span class="fa-li"><i class="fa-solid fa-${token[idx].markup}" aria-hidden="true"></i></span>`;
}; };
export default function(eleventyConfig) { export default function(eleventyConfig) {
eleventyConfig.addPlugin(markdownImages.eleventyPlugin);
eleventyConfig.addFilter('md', content => content ? markdownLib.render(content) : ""); eleventyConfig.addFilter('md', content => content ? markdownLib.render(content) : "");
eleventyConfig.setLibrary('md', markdownLib); eleventyConfig.setLibrary('md', markdownLib);
} }
+166 -2398
View File
File diff suppressed because it is too large. Load diff
+10 -4
View File
@@ -21,7 +21,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@11ty/eleventy": "^3.0.0", "@11ty/eleventy": "^3.0.0",
"@11ty/eleventy-fetch": "^3.0.0", "@11ty/eleventy-fetch": "^6.0.1",
"@11ty/eleventy-img": "^7.1.1", "@11ty/eleventy-img": "^7.1.1",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
"@lewisdale/blog-post-cli": "^1.0.3", "@lewisdale/blog-post-cli": "^1.0.3",
@@ -33,14 +33,13 @@
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-abbr": "^2.0.0", "markdown-it-abbr": "^2.0.0",
"markdown-it-anchor": "^9.2.0", "markdown-it-anchor": "^9.2.0",
"markdown-it-eleventy-img": "^0.10.2",
"markdown-it-emoji": "^3.0.0", "markdown-it-emoji": "^3.0.0",
"markdown-it-footnote": "^4.0.0", "markdown-it-footnote": "^4.0.0",
"markdown-it-link-attributes": "^4.0.1", "markdown-it-link-attributes": "^4.0.1",
"markdown-it-mark": "^4.0.0", "markdown-it-mark": "^4.0.0",
"markdown-it-obsidian-callouts": "^0.2.6", "markdown-it-obsidian-callouts": "^0.2.6",
"markdown-it-prism": "^2.3.0", "markdown-it-prism": "^2.3.0",
"npm-run-all": "^4.1.5", "npm-run-all2": "^9.0.4",
"p-map": "^7.0.2", "p-map": "^7.0.2",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-cli": "^10.1.0", "postcss-cli": "^10.1.0",
@@ -57,7 +56,14 @@
}, },
"allowScripts": { "allowScripts": {
"sharp@0.35.5": false, "sharp@0.35.5": false,
"sharp@0.33.5": false,
"fsevents@2.3.2": false "fsevents@2.3.2": false
},
"overrides": {
"eleventy-plugin-og-image": {
"satori": {
".": "^0.63.2",
"fflate": "^0.7.5"
}
}
} }
} }
+3 -3
View File
@@ -1,11 +1,11 @@
<div style="height: 100%; width: 100%; display: flex; align-items: stretch; background-color: #262626;"> <div style="height: 100%; width: 100%; display: flex; align-items: stretch; background-color: #262626;">
<div style="display: flex; flex-direction: column; flex-grow: 1; align-items: center; margin: 80px; background-color: #262626;"> <div style="display: flex; flex-direction: column; flex-grow: 1; align-items: center; margin: 80px; background-color: #262626;">
<div style="display: flex; flex-grow: 1; flex-direction: column; align-items: center; justify-content: center; font-family: 'Space Grotesk'; font-weight: 500; color: #e5e5e5; text-align: center;"> <div style="display: flex; flex-grow: 1; flex-direction: column; align-items: center; justify-content: center; font-family: 'Space Grotesk'; font-weight: 500; color: #e5e5e5; text-align: center;">
<h1 style="font-size: 80px;">{{ title | safe }}</h1> <h1 style="font-size: 80px; font-weight: 500;">{{ title | safe }}</h1>
<h2 style="font-size: 44px;">{{ subTitle | safe }}</h2> <h2 style="font-size: 44px; font-weight: 500;">{{ subTitle | safe }}</h2>
</div> </div>
<div style="display: flex; justify-self: flex-end; flex-grow: 0; font-family: 'Space Grotesk'; font-weight: 500; color: #e5e5e5; text-align: center;"> <div style="display: flex; justify-self: flex-end; flex-grow: 0; font-family: 'Space Grotesk'; font-weight: 500; color: #e5e5e5; text-align: center;">
<h3 style="font-size: 20px;">LewisDale.dev</h3> <h3 style="font-size: 20px; font-weight: 500;">LewisDale.dev</h3>
</div> </div>
</div> </div>
</div> </div>