diff --git a/.eleventy.js b/.eleventy.js index d100f74..2256368 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,3 +1,4 @@ +const path = require("path"); const markdownIt = require("markdown-it"); const pluginRss = require("@11ty/eleventy-plugin-rss"); const features = require("./features.json"); @@ -19,9 +20,44 @@ function minifyHtmlOutput(content, outputPath) { .trim(); } -module.exports = function(eleventyConfig) { +module.exports = async function(eleventyConfig) { + const Image = (await import("@11ty/eleventy-img")).default; + eleventyConfig.addPlugin(pluginRss); + eleventyConfig.addNunjucksAsyncShortcode("image", async function(src, alt, opts = {}) { + const { + decorative = false, + eager = false, + sizes = "(min-width: 75rem) 800px, (min-width: 48rem) 66vw, 100vw", + widths = [400, 800, 1200] + } = opts; + + if (!decorative && (!alt || !String(alt).trim())) { + throw new Error(`[image shortcode] Missing alt text for image: ${src}. Pass { decorative: true } if this image is purely decorative.`); + } + + const altText = decorative ? "" : alt; + + const relativeSrc = src.replace(/^\/?images\//, ""); + const inputPath = path.join("src/images", relativeSrc); + + const metadata = await Image(inputPath, { + widths, + formats: ["webp", "auto"], + outputDir: "./dist/images/optimized/", + urlPath: "/images/optimized/" + }); + + return Image.generateHTML(metadata, { + alt: altText, + sizes, + loading: eager ? "eager" : "lazy", + decoding: "async", + ...(eager ? { fetchpriority: "high" } : {}) + }); + }); + // Filters eleventyConfig.addFilter("dateFilter", dateFilter); eleventyConfig.addFilter("json", (value) => JSON.stringify(value)); diff --git a/docs/NOTES.md b/docs/NOTES.md index 7343752..579a414 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -12,6 +12,7 @@ Personal reference notes covering how the site is built, the decisions made duri | Date formatting | Luxon v3 | | RSS feed | `@11ty/eleventy-plugin-rss` v1.0.7 | | Search | Pagefind v1.3 (post-build index) | +| Image optimization | `@11ty/eleventy-img` v7 (build-time, generates responsive `` markup) | | CSS minification | LightningCSS v1.32 (post-build) | | Contact form | Netlify Forms (no backend needed) | | Hosting | Netlify, deployed from GitHub | @@ -144,6 +145,23 @@ The `` tag pointing to the feed is added in `head.html` wh Pagefind runs after the build and indexes the HTML in `dist/`. The search box (``) is a web component loaded from `/pagefind/pagefind-component-ui.css` and initialised via `partials/pagefind.html`. The `` landmark in the header wraps it. +### Images + +Images are optimized at build time via `@11ty/eleventy-img`, through a Nunjucks async shortcode registered in `.eleventy.js`: + +```njk +{% image "2021/mona-lisa.jpg", "The Mona Lisa by Leonardo Da Vinci" %} +{% image "2021/mona-lisa.jpg", "Alt text", { eager: true, sizes: "...", widths: [400, 800] } %} +{% image "2026/divider.svg", "", { decorative: true } %} +``` + +- Source files live in `src/images/YYYY/filename.ext` — pass the path relative to `src/images/` (a leading `/images/` or `images/`, as found in frontmatter values, is stripped automatically). +- Generates a `` with a `webp` `` plus an original-format (`"auto"`) `` fallback, at `widths: [400, 800, 1200]` by default. Output is written to `dist/images/optimized/` with content-hashed filenames. +- Defaults to `loading="lazy"`/`decoding="async"`. Pass `{ eager: true }` for above-the-fold images (also adds `fetchpriority="high"`) — used for the About page hero image and `post.html`'s optional feature-image slot. +- **Alt text is mandatory** — the build throws if it's missing or empty. For genuinely decorative images, pass both `alt: ""` and `{ decorative: true }` explicitly; there's no way to ship an empty alt by accident. +- The raw, unprocessed source images are still passthrough-copied to `/images/...` unchanged (`addPassthroughCopy("src/images")`) — required because `meta.html`'s Open Graph/Twitter image tags read the raw `featureImage` frontmatter value directly, not an optimized variant. +- Used in `about.html` (hero image), `post.html` (the `image`/`imageAlt` feature-image frontmatter fields), and inline within post markdown bodies. The latter works because `markdownTemplateEngine: "njk"` means Nunjucks resolves shortcodes in post content before markdown-it renders it — the same mechanism that already let raw HTML `` tags pass through. + ## CSS architecture All CSS lives in two files: @@ -224,6 +242,8 @@ WCAG 2.2 Level AA is a hard requirement throughout. **Contrast** — all text and UI component colours were verified against WCAG 1.4.3 (text, 4.5:1 normal / 3:1 large) and 1.4.11 (non-text, 3:1) during the redesign. +**Images** — alt text is enforced at build time by the `{% image %}` shortcode (see "Images" above); an empty or missing alt fails the build unless `{ decorative: true }` is explicitly passed. + ## Content conventions **British English** throughout — "colour" not "color" in content (CSS uses American English as per CSS spec). diff --git a/package-lock.json b/package-lock.json index d5d01c4..acc4201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@11ty/eleventy": "^3.1.6", + "@11ty/eleventy-img": "^7.0.0", "cross-env": "^7.0.3", "lightningcss": "^1.32.0", "pagefind": "^1.3.0", @@ -129,6 +130,100 @@ "url": "https://opencollective.com/11ty" } }, + "node_modules/@11ty/eleventy-fetch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-fetch/-/eleventy-fetch-5.1.3.tgz", + "integrity": "sha512-4HS6QB/mVWTVlE6kjCKPkjkC1Z0YOqizJaHR05zK+E7a2b4EUC3T+wLktIy7wEl76ZoarkY45EKLmTw1XuR8fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-utils": "^2.0.7", + "@rgrove/parse-xml": "^4.2.1", + "debug": "^4.4.3", + "flatted": "^3.4.2", + "p-queue": "6.6.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-fetch/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@11ty/eleventy-fetch/node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@11ty/eleventy-fetch/node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@11ty/eleventy-img": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-img/-/eleventy-img-7.0.0.tgz", + "integrity": "sha512-4b8BSTd1XZSax3hFg1Iyv3B3Vc2DmuTenbFr7M0+x4H3fNJDYq3Bctc9rDxiD021hzO7v6MPXXCb5HsQT6//Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@11ty/eleventy-fetch": "^5.1.3", + "@11ty/eleventy-utils": "^2.0.7", + "brotli-size": "^4.0.0", + "entities": "^8.0.0", + "obug": "^2.1.4", + "p-queue": "^9.3.3", + "sharp": "^0.35.3" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/11ty" + } + }, + "node_modules/@11ty/eleventy-img/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@11ty/eleventy-plugin-bundle": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", @@ -943,6 +1038,16 @@ "win32" ] }, + "node_modules/@rgrove/parse-xml": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.3.tgz", + "integrity": "sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sindresorhus/slugify": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -1128,6 +1233,19 @@ "node": ">=8" } }, + "node_modules/brotli-size": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", + "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "0.1.1" + }, + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1318,6 +1436,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1423,6 +1547,13 @@ "node": ">= 8" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -1495,6 +1626,13 @@ "dev": true, "license": "MIT" }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -2433,6 +2571,20 @@ } } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2446,6 +2598,46 @@ "node": ">= 0.8" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pagefind": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", @@ -3182,6 +3374,69 @@ "ws": "^8.18.1" } }, + "@11ty/eleventy-fetch": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-fetch/-/eleventy-fetch-5.1.3.tgz", + "integrity": "sha512-4HS6QB/mVWTVlE6kjCKPkjkC1Z0YOqizJaHR05zK+E7a2b4EUC3T+wLktIy7wEl76ZoarkY45EKLmTw1XuR8fQ==", + "dev": true, + "requires": { + "@11ty/eleventy-utils": "^2.0.7", + "@rgrove/parse-xml": "^4.2.1", + "debug": "^4.4.3", + "flatted": "^3.4.2", + "p-queue": "6.6.2" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + } + } + }, + "@11ty/eleventy-img": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@11ty/eleventy-img/-/eleventy-img-7.0.0.tgz", + "integrity": "sha512-4b8BSTd1XZSax3hFg1Iyv3B3Vc2DmuTenbFr7M0+x4H3fNJDYq3Bctc9rDxiD021hzO7v6MPXXCb5HsQT6//Aw==", + "dev": true, + "requires": { + "@11ty/eleventy-fetch": "^5.1.3", + "@11ty/eleventy-utils": "^2.0.7", + "brotli-size": "^4.0.0", + "entities": "^8.0.0", + "obug": "^2.1.4", + "p-queue": "^9.3.3", + "sharp": "^0.35.3" + }, + "dependencies": { + "entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true + } + } + }, "@11ty/eleventy-plugin-bundle": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz", @@ -3533,6 +3788,12 @@ "dev": true, "optional": true }, + "@rgrove/parse-xml": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.3.tgz", + "integrity": "sha512-Jhlb+0zYez1T1yXUQs3F1qAtFuJljBVNdy9TKmLDauAXkxsOXopKYOyQ5Wm6SvP3fycav0GviX4Y15WWhGetMw==", + "dev": true + }, "@sindresorhus/slugify": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -3657,6 +3918,15 @@ "fill-range": "^7.1.1" } }, + "brotli-size": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", + "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, "chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -3769,6 +4039,12 @@ "domhandler": "^5.0.3" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3833,6 +4109,12 @@ "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", "dev": true }, + "eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true + }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -3889,6 +4171,12 @@ } } }, + "flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true + }, "fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -4408,6 +4696,12 @@ "commander": "^5.1.0" } }, + "obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4417,6 +4711,28 @@ "ee-first": "1.1.1" } }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, + "p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "dev": true, + "requires": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + } + }, + "p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "dev": true + }, "pagefind": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", diff --git a/package.json b/package.json index 1f4e74b..5c90a07 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@11ty/eleventy": "^3.1.6", + "@11ty/eleventy-img": "^7.0.0", "cross-env": "^7.0.3", "lightningcss": "^1.32.0", "pagefind": "^1.3.0", diff --git a/src/_includes/layouts/about.html b/src/_includes/layouts/about.html index aa6dc04..2b00840 100644 --- a/src/_includes/layouts/about.html +++ b/src/_includes/layouts/about.html @@ -19,7 +19,7 @@
-{{ featureImageAlt or featureImageCaption }} +{% image featureImage, featureImageAlt or featureImageCaption, { eager: true, sizes: "(min-width: 75rem) 400px, (min-width: 48rem) 33vw, 100vw", widths: [400, 800] } %} {% if featureImageCaption %}
{{ featureImageCaption }}
{% endif %} diff --git a/src/_includes/layouts/post.html b/src/_includes/layouts/post.html index 4c745d2..4e9842e 100644 --- a/src/_includes/layouts/post.html +++ b/src/_includes/layouts/post.html @@ -15,7 +15,7 @@

{{ title }}

{% if image %}
-{{ imageAlt or '' }} +{% image image, imageAlt, { eager: true } %}
{% endif %} diff --git a/src/posts/2020-11-05-envision-glasses.md b/src/posts/2020-11-05-envision-glasses.md index 114406e..27579e1 100644 --- a/src/posts/2020-11-05-envision-glasses.md +++ b/src/posts/2020-11-05-envision-glasses.md @@ -35,7 +35,7 @@ Envision Glasses are available in two styles: the standard Titanium frames with The frames consist of the arm that goes over your left ear and the bits that frame your eyes. The rest, known as the body, attaches to the frames just over your right eye to form the arm that hooks over your right ear. -Photo of Envision Glasses with the standard titanium frames; the frames are very lightweight metal just a few millimetres wide +{% image "2020/google-glass-with-titanium-frames.png", "Photo of Envision Glasses with the standard titanium frames; the frames are very lightweight metal just a few millimetres wide" %} The body is made of black plastic and it's a lot more substantial than the frames because it's where all the hardware (including the camera and speaker) is found. When attached to the frames the body curves smoothly around the top of the frame over your right eye by about a centimetre. This is where the camera is found so it points forward in the direction you're facing. Continuing about two thirds of the way along the frame over your right eye is a clear perspex block that creates a Heads Up Display (HUD). @@ -45,7 +45,7 @@ If you have usable sight, the HUD is in the top right corner of your field of vi I chose the standard titanium frames because the little jewelry I wear is platinum or silver and very plain, so they felt more my style than the darker and heavier Smith Optics frames (think NHS specs from the 1970s or classic Ray Ban frames). -Photo of Envision Glasses with the Smith Optics frames; the frames are black plastic, 2 or 3 times thicker than the titanium frames +{% image "2020/google-glass-with-smith-optics-frames.jpg", "Photo of Envision Glasses with the Smith Optics frames; the frames are black plastic, 2 or 3 times thicker than the titanium frames" %} The asthetic imbalance between the light metal frames and the heavier black plastic body, particularly where it curves around the top of one eye, makes me feel a bit like I've been assimilated by the [Borg](https://en.wikipedia.org/wiki/Borg), but I'm actually OK with that. The hardware has to go somewhere and I think the glasses manage it in a reasonably stylish way. diff --git a/src/posts/2021-01-02-thoughts-on-screen-readers-and-image-recognition.md b/src/posts/2021-01-02-thoughts-on-screen-readers-and-image-recognition.md index a8c8281..cbffc2c 100644 --- a/src/posts/2021-01-02-thoughts-on-screen-readers-and-image-recognition.md +++ b/src/posts/2021-01-02-thoughts-on-screen-readers-and-image-recognition.md @@ -30,7 +30,7 @@ image recognition has been around since the 1960s but it is only relatively rece To help mitigate this, the Picture Smart feature in Jaws uses certainty qualifiers to describe the image recognition results. For example here are the results for this image of the Mona Lisa by Leonardo Da Vinci: -The Mona Lisa by Leonardo Da Vinci +{% image "2021/mona-lisa.jpg", "The Mona Lisa by Leonardo Da Vinci" %} * Caption is Mona Lisa posing for a picture * These tags describe the photo: Drawing, human face, painting, person, sketch, woman @@ -44,7 +44,7 @@ If you're thinking to yourself that providing text descriptions is no longer som Here are the Picture Smart image recognition results for this image of The Metamorphosis of Narcissus by Salvador Dali: -The Metamorphosis of Narcissus by Salvador Dali +{% image "2021/metamorphosis-of-narcissus.jpg", "The Metamorphosis of Narcissus by Salvador Dali" %} * Caption is a painting of a person * These tags describe the photo: Art, cartoon, drawing, text diff --git a/src/posts/2021-07-27-accessibe-and-data-protection.md b/src/posts/2021-07-27-accessibe-and-data-protection.md index 040967c..8d170f5 100644 --- a/src/posts/2021-07-27-accessibe-and-data-protection.md +++ b/src/posts/2021-07-27-accessibe-and-data-protection.md @@ -97,7 +97,7 @@ AccessiBe's privacy policy makes me think there is a high risk to individuals, a On 17 March 2021, I sent AccessiBe a Direct Message (DM) on Twitter, asking them for a copy of their DPIA: -Screenshot of a DM sent to AccessiBe, asking for a copy of its DPIA +{% image "2021/dm-to-accessibe_2021-03-17.png", "Screenshot of a DM sent to AccessiBe, asking for a copy of its DPIA" %} When AccessiBe did not respond, I completed the contact form on their website on 23 March. I was promptly called by one of their sales team, who could not tell me anything about AccessiBe's compliance with the General Data Protection Regulations (GDPR), but assured me someone would be in touch who could help. diff --git a/src/posts/2021-12-03-patrick-h-laukes-goulash.md b/src/posts/2021-12-03-patrick-h-laukes-goulash.md index f3db8ce..7273c3e 100644 --- a/src/posts/2021-12-03-patrick-h-laukes-goulash.md +++ b/src/posts/2021-12-03-patrick-h-laukes-goulash.md @@ -31,7 +31,7 @@ The 15th in a series of posts that bring together the two sides of my blog: food ## Method -Goulash in a big red dutch oven with some gnocchi cooking in a pot next to it +{% image "2021/goulash-in-big-red-dutch-oven-and-gnocchi-cooking-in-a-pot-next-to-it.jpg", "Goulash in a big red dutch oven with some gnocchi cooking in a pot next to it" %} 1. Peel and slice the onions (no need to get too fancy with how fine you cut them, as they'll mostly dissolve), peel and crush garlic, slice peppers into relatively thin strips. 2. Put the butter and/or oil in a big pot - I use a dutch oven/Le Creuset job for this. Set the heat to high. Onions in the pot with a pinch of salt, and fry them up for a few minutes. @@ -46,7 +46,7 @@ At the end, the consistency should be fairly thick (somewhere between a soup and If the sauce is too runny for your liking, you can add some corn starch slurry (one teaspoon of corn starch, mixed with a little bit of boiling water in a bowl first before pouring into the pot, to avoid lumps) and briefly bring it to the boil again until it thickens. -A plate with the finished goulash and some gnocchi +{% image "2021/finished-plate-of-goulash-with-gnocchi.jpg", "A plate with the finished goulash and some gnocchi" %} Serve with whatever starchy accompaniment you want: spaetzle (as most common in Austria), large potato dumplings (the more common German version), gnocchi, rigatoni, potatoes, rice, or just with bread. diff --git a/src/posts/2023-08-17-adventures-with-bemyai.md b/src/posts/2023-08-17-adventures-with-bemyai.md index 17b7452..10f7ca2 100644 --- a/src/posts/2023-08-17-adventures-with-bemyai.md +++ b/src/posts/2023-08-17-adventures-with-bemyai.md @@ -30,7 +30,7 @@ When I posted on the socials that I was experimenting with BeMyAI, people were c So I took a photo of a bit of my office that I know very well. I've owned most of the things in the picture since before I [lost my sight](/losing-sight), and I'm very familiar with the rest because it's my house after all. -Photo of a shelf in my office +{% image "2023/my-office-shelf.jpg", "Photo of a shelf in my office" %} Here's the initial description: diff --git a/src/posts/2025-06-01-using-ray-ban-meta-glasses.md b/src/posts/2025-06-01-using-ray-ban-meta-glasses.md index 803b069..8117261 100644 --- a/src/posts/2025-06-01-using-ray-ban-meta-glasses.md +++ b/src/posts/2025-06-01-using-ray-ban-meta-glasses.md @@ -14,7 +14,7 @@ The first thing that appealed to me about the Meta Glasses is that they look and I chose the Skyler frames in shiny black, with lenses that transition from clear to amethyst (when out in sunlight). -Close-up of the right temple area of the Ray-Ban Meta Glasses, cut-away to reveal the integration of technology including the capture camera. The frames are black and the Ray-Ban logo is visible on the frames and at the top of the lens. +{% image "2025/ray-ban-meta-glasses.jpg", "Close-up of the right temple area of the Ray-Ban Meta Glasses, cut-away to reveal the integration of technology including the capture camera. The frames are black and the Ray-Ban logo is visible on the frames and at the top of the lens." %} Ray-Bans have a very robust look and feel, which makes them a strong presence on your face. Of the three basic frame styles (Wayfarer, Skyler, and Headliner), the Skyler frames appeared to be the least heavy looking - though there really is not much in it. diff --git a/src/posts/2026-07-13-accessibility-is-resistance.md b/src/posts/2026-07-13-accessibility-is-resistance.md index f09f30c..1cf5d92 100644 --- a/src/posts/2026-07-13-accessibility-is-resistance.md +++ b/src/posts/2026-07-13-accessibility-is-resistance.md @@ -45,7 +45,7 @@ Like many people, I shop online. I might even do it more regularly than most, be If I asked you to think of a clothing brand likely to have an accessible website, you might think of retailers with plenty of money - Amazon Essentials, Calvin Klein, or Marks and Spencer perhaps. Let's use Calvin Klein as the working example. Here's a product page from their UK website: -Screenshot of a product page on the Calvin Klein website showing a large image and some associated product information +{% image "2026/CK-product-page.png", "Screenshot of a product page on the Calvin Klein website showing a large image and some associated product information" %} Notice how much information about the product is held in image format? Here's the product information available as text on the page: @@ -65,7 +65,7 @@ Is the jumper going to make me appear sophisticated and elegant, or look like a Now, if you're even a little bit familiar with accessibility, you'll be thinking "Aha! But, what about [text descriptions](https://tetralogical.com/blog/2022/01/24/text-descriptions/)?". Here's an image from the product page: -Product image from the Calvin Klein website showing a model wearing a black jumper +{% image "2026/CK-product-image.png", "Product image from the Calvin Klein website showing a model wearing a black jumper" %} And the text description? Here you go: @@ -99,7 +99,7 @@ I have a pair of [Meta Ray-Bans](/using-ray-ban-meta-glasses) and whereas there Had you been standing next to me at this tea plantation, not far from Hangzhou China, earlier this year, this would have been your view: -A view looking out over a tea plantation in China +{% image "2026/Hangzhou-tea-plantation.jpg", "A view looking out over a tea plantation in China" %} But since you weren't there, an the aforementioned person I share my life with had gone off to order some lunch, it was just me and the view.