diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index dfdd752..f4b79ad 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -23,12 +23,15 @@ + {% set hasGeneratedOg = page.fileSlug | hasOgImage %} {% if ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10 %} {% elif image and image != "" and (image | length) > 10 %} {% elif contentImage and contentImage != "" and (contentImage | length) > 10 %} + {% elif hasGeneratedOg %} + {% else %} {% endif %} @@ -37,7 +40,7 @@ {# Twitter Card meta tags #} - {% set hasImage = (ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10) or (image and image != "" and (image | length) > 10) or (contentImage and contentImage != "" and (contentImage | length) > 10) %} + {% set hasImage = hasGeneratedOg or (ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10) or (image and image != "" and (image | length) > 10) or (contentImage and contentImage != "" and (contentImage | length) > 10) %} @@ -47,6 +50,8 @@ {% elif contentImage and contentImage != "" and (contentImage | length) > 10 %} + {% elif hasGeneratedOg %} + {% endif %} {# Favicon #} diff --git a/eleventy.config.js b/eleventy.config.js index d3919b1..547fdad 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -9,7 +9,8 @@ import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; import { minify } from "html-minifier-terser"; import { createHash } from "crypto"; import { execFileSync } from "child_process"; -import { readFileSync } from "fs"; +import { readFileSync, existsSync } from "fs"; +import { generateOgImages } from "./lib/og.js"; import { resolve, dirname } from "path"; import { fileURLToPath } from "url"; @@ -165,6 +166,7 @@ export default function (eleventyConfig) { eleventyConfig.addPassthroughCopy("images"); eleventyConfig.addPassthroughCopy("js"); eleventyConfig.addPassthroughCopy("favicon.ico"); + eleventyConfig.addPassthroughCopy({ ".cache/og": "og" }); // Watch for content changes eleventyConfig.addWatchTarget("./content/"); @@ -266,6 +268,12 @@ export default function (eleventyConfig) { } }); + // Check if a generated OG image exists for this page slug + eleventyConfig.addFilter("hasOgImage", (fileSlug) => { + if (!fileSlug) return false; + return existsSync(resolve(__dirname, ".cache", "og", `${fileSlug}.png`)); + }); + // Current timestamp filter (for client-side JS buildtime) eleventyConfig.addFilter("timestamp", () => Date.now()); @@ -445,6 +453,18 @@ export default function (eleventyConfig) { .slice(0, 5); }); + // Generate OpenGraph images for posts without photos + eleventyConfig.on("eleventy.before", async () => { + const contentDir = resolve(__dirname, "content"); + const cacheDir = resolve(__dirname, ".cache"); + const siteName = process.env.SITE_NAME || "My IndieWeb Blog"; + try { + await generateOgImages(contentDir, cacheDir, siteName); + } catch (err) { + console.error("[og] Image generation failed:", err.message); + } + }); + // Pagefind indexing + WebSub hub notification after each build eleventyConfig.on("eleventy.after", async ({ dir, runMode }) => { // Pagefind indexing diff --git a/lib/og.js b/lib/og.js new file mode 100644 index 0000000..fdffe94 --- /dev/null +++ b/lib/og.js @@ -0,0 +1,329 @@ +/** + * OpenGraph image generation for posts without photos. + * Uses Satori (layout → SVG) + @resvg/resvg-js (SVG → PNG). + * Generated images are cached in .cache/og/ and passthrough-copied to output. + */ + +import satori from "satori"; +import { Resvg } from "@resvg/resvg-js"; +import { + readFileSync, + writeFileSync, + mkdirSync, + existsSync, + readdirSync, +} from "node:fs"; +import { resolve, join, basename, dirname } from "node:path"; +import { createHash } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import matter from "gray-matter"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const WIDTH = 1200; +const HEIGHT = 630; + +const COLORS = { + bg: "#09090b", + title: "#f4f4f5", + date: "#a1a1aa", + siteName: "#71717a", + accent: "#3b82f6", + badge: "#2563eb", + badgeText: "#ffffff", +}; + +const POST_TYPE_MAP = { + articles: "Article", + notes: "Note", + bookmarks: "Bookmark", + photos: "Photo", + likes: "Like", + replies: "Reply", + reposts: "Repost", + pages: "Page", + videos: "Video", + audio: "Audio", + jams: "Jam", + rsvps: "RSVP", + events: "Event", +}; + +function loadFonts() { + const fontsDir = resolve( + __dirname, + "..", + "node_modules", + "@fontsource", + "inter", + "files", + ); + return [ + { + name: "Inter", + data: readFileSync(join(fontsDir, "inter-latin-400-normal.woff")), + weight: 400, + style: "normal", + }, + { + name: "Inter", + data: readFileSync(join(fontsDir, "inter-latin-700-normal.woff")), + weight: 700, + style: "normal", + }, + ]; +} + +function computeHash(title, date, postType, siteName) { + return createHash("md5") + .update(`${title}|${date}|${postType}|${siteName}`) + .digest("hex") + .slice(0, 12); +} + +function detectPostType(filePath) { + const parts = filePath.split("/"); + const contentIdx = parts.indexOf("content"); + if (contentIdx >= 0 && contentIdx + 1 < parts.length) { + const typeDir = parts[contentIdx + 1]; + if (POST_TYPE_MAP[typeDir]) return POST_TYPE_MAP[typeDir]; + } + return "Post"; +} + +function formatDate(dateStr) { + if (!dateStr) return ""; + try { + const d = new Date(dateStr); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + } catch { + return ""; + } +} + +function truncateTitle(title, max = 120) { + if (!title || title.length <= max) return title || "Untitled"; + return title.slice(0, max).trim() + "\u2026"; +} + +function extractBodyText(raw) { + const body = raw + .replace(/^---[\s\S]*?---\s*/, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/[#*_~`>]/g, "") + .replace(/!\[[^\]]*\]\([^)]+\)/g, "") + .replace(/\n+/g, " ") + .trim(); + if (!body) return "Untitled"; + return body.length > 120 ? body.slice(0, 120).trim() + "\u2026" : body; +} + +function buildCard(title, dateStr, postType, siteName) { + return { + type: "div", + props: { + style: { + display: "flex", + width: `${WIDTH}px`, + height: `${HEIGHT}px`, + backgroundColor: COLORS.bg, + }, + children: [ + { + type: "div", + props: { + style: { + width: "6px", + height: "100%", + backgroundColor: COLORS.accent, + flexShrink: 0, + }, + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + padding: "60px", + flex: 1, + overflow: "hidden", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + gap: "24px", + }, + children: [ + { + type: "div", + props: { + style: { display: "flex" }, + children: [ + { + type: "span", + props: { + style: { + backgroundColor: COLORS.badge, + color: COLORS.badgeText, + fontSize: "16px", + fontWeight: 700, + fontFamily: "Inter", + padding: "6px 16px", + borderRadius: "999px", + textTransform: "uppercase", + letterSpacing: "0.05em", + }, + children: postType, + }, + }, + ], + }, + }, + { + type: "div", + props: { + style: { + color: COLORS.title, + fontSize: "48px", + fontWeight: 700, + fontFamily: "Inter", + lineHeight: 1.2, + overflow: "hidden", + }, + children: truncateTitle(title), + }, + }, + dateStr + ? { + type: "div", + props: { + style: { + color: COLORS.date, + fontSize: "24px", + fontWeight: 400, + fontFamily: "Inter", + }, + children: formatDate(dateStr), + }, + } + : null, + ].filter(Boolean), + }, + }, + { + type: "div", + props: { + style: { + color: COLORS.siteName, + fontSize: "20px", + fontWeight: 400, + fontFamily: "Inter", + }, + children: siteName, + }, + }, + ], + }, + }, + ], + }, + }; +} + +function scanContentFiles(contentDir) { + const files = []; + function walk(dir) { + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (entry.name === ".indiekit") continue; + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith(".md")) { + files.push(fullPath); + } + } + } + walk(contentDir); + return files; +} + +/** + * Generate OG images for all content posts without photos. + * @param {string} contentDir - Path to content/ directory + * @param {string} cacheDir - Path to .cache/ directory + * @param {string} siteName - Site name for the card + */ +export async function generateOgImages(contentDir, cacheDir, siteName) { + const ogDir = join(cacheDir, "og"); + mkdirSync(ogDir, { recursive: true }); + + const manifestPath = join(ogDir, "manifest.json"); + let manifest = {}; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch { + // First run + } + + const fonts = loadFonts(); + const mdFiles = scanContentFiles(contentDir); + + let generated = 0; + let skipped = 0; + const newManifest = {}; + + for (const filePath of mdFiles) { + const raw = readFileSync(filePath, "utf8"); + const { data: fm } = matter(raw); + + if (fm.photo || fm.image) { + skipped++; + continue; + } + + const slug = basename(filePath, ".md"); + const title = fm.title || fm.name || extractBodyText(raw); + const date = fm.published || fm.date || ""; + const postType = detectPostType(filePath); + const hash = computeHash(title, date, postType, siteName); + + if (manifest[slug]?.hash === hash && existsSync(join(ogDir, `${slug}.png`))) { + newManifest[slug] = manifest[slug]; + skipped++; + continue; + } + + const card = buildCard(title, date, postType, siteName); + const svg = await satori(card, { width: WIDTH, height: HEIGHT, fonts }); + const resvg = new Resvg(svg, { + fitTo: { mode: "width", value: WIDTH }, + }); + const pngBuffer = resvg.render().asPng(); + + writeFileSync(join(ogDir, `${slug}.png`), pngBuffer); + newManifest[slug] = { title: slug, hash }; + generated++; + } + + writeFileSync(manifestPath, JSON.stringify(newManifest, null, 2)); + console.log( + `[og] Generated ${generated} images, skipped ${skipped} (cached or have photos)`, + ); +} diff --git a/package-lock.json b/package-lock.json index f41c894..3594c53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,17 @@ "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "@atproto/api": "^0.12.0", "@chrisburnell/eleventy-cache-webmentions": "^2.2.7", + "@fontsource/inter": "^5.2.8", "@quasibit/eleventy-plugin-sitemap": "^2.2.0", + "@resvg/resvg-js": "^2.6.2", "eleventy-plugin-embed-everything": "^1.21.0", + "gray-matter": "^4.0.3", "html-minifier-terser": "^7.0.0", "markdown-it": "^14.0.0", "markdown-it-anchor": "^9.2.0", "pagefind": "^1.3.0", - "rss-parser": "^3.13.0" + "rss-parser": "^3.13.0", + "satori": "^0.19.2" }, "devDependencies": { "@tailwindcss/typography": "^0.5.0", @@ -466,6 +470,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -1001,6 +1014,221 @@ "node": ">=10.0.0" } }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rgrove/parse-xml": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.2.0.tgz", @@ -1010,6 +1238,22 @@ "node": ">=14.0.0" } }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sindresorhus/slugify": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", @@ -1297,6 +1541,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.17", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", @@ -1452,6 +1705,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", @@ -1580,6 +1842,47 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", + "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1982,6 +2285,15 @@ "dev": true, "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2145,6 +2457,12 @@ } } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/filesize": { "version": "10.1.6", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", @@ -2393,6 +2711,18 @@ "node": ">= 0.4" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-minifier-terser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", @@ -2755,6 +3085,16 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3225,6 +3565,12 @@ "@pagefind/windows-x64": "1.4.0" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -3235,6 +3581,16 @@ "tslib": "^2.0.3" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -3562,7 +3918,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/posthtml": { @@ -3946,6 +4301,28 @@ "entities": "^4.4.0" } }, + "node_modules/satori": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.19.2.tgz", + "integrity": "sha512-71plFHWcq6WJBM5sf/n0eHOmTBiKLUB/G8du7SmLTTLHKEKrV3TPHGKcEVIoyjnbhnjvu9HhLyF9MATB/zzL7g==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.17", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -4182,6 +4559,12 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4369,6 +4752,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4455,6 +4844,16 @@ "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", "license": "MIT" }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -4656,6 +5055,12 @@ "node": ">=12" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 1380505..1bfe437 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,17 @@ "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "@atproto/api": "^0.12.0", "@chrisburnell/eleventy-cache-webmentions": "^2.2.7", + "@fontsource/inter": "^5.2.8", "@quasibit/eleventy-plugin-sitemap": "^2.2.0", + "@resvg/resvg-js": "^2.6.2", "eleventy-plugin-embed-everything": "^1.21.0", + "gray-matter": "^4.0.3", "html-minifier-terser": "^7.0.0", "markdown-it": "^14.0.0", "markdown-it-anchor": "^9.2.0", "pagefind": "^1.3.0", - "rss-parser": "^3.13.0" + "rss-parser": "^3.13.0", + "satori": "^0.19.2" }, "devDependencies": { "@tailwindcss/typography": "^0.5.0",