Fix npm audit vulnerabilities and broken image build
Build and copy to prod / build-and-copy (push) Successful in 4m15s
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:
1 parent
13d0b244e5
commit
f45b6308f9
6 files changed
+284
-2427
No files matched your search
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
Generated
+166
-2398
File diff suppressed because it is too large.
Load diff
+10
-4
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
Reference in new issue
Block a user