diff --git a/astro.config.mjs b/astro.config.mjs index d96c899..56c2b91 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -40,16 +40,11 @@ const postLastmodLookup = new Map( export default defineConfig({ site: 'https://schmelczer.dev', trailingSlash: 'ignore', - build: { inlineStylesheets: 'always' }, - redirects: { - '/writing/': '/articles/', - '/writing/[slug]': '/articles/[slug]', - }, integrations: [ sitemap({ filter: (page) => { const path = new URL(page).pathname; - return !path.startsWith('/writing/') && path !== '/404/'; + return !/^\/tags\/[^/]+\/?$/.test(path) && path !== '/404/'; }, serialize(item) { const url = new URL(item.url); @@ -93,7 +88,6 @@ export default defineConfig({ behavior: 'append', properties: { className: ['heading-anchor'], - ariaLabel: 'Permalink', }, // Glyph rendered via CSS ::before so it doesn't leak into the TOC // when astro:content extracts heading.text from the rendered HTML. @@ -110,11 +104,33 @@ export default defineConfig({ return (tree) => { visit(tree, 'element', (node) => { if (!SCROLLABLE.has(node.tagName)) return; - node.properties = node.properties ?? {}; node.properties.tabindex = '0'; }); }; }, + function rehypeLabelHeadingPermalinks() { + function textOf(node) { + if (!node) return ''; + if (node.type === 'text') return node.value ?? ''; + return (node.children ?? []).map(textOf).join(''); + } + + return (tree) => { + visit(tree, 'element', (node) => { + if (!/^h[2-6]$/.test(node.tagName)) return; + const headingText = textOf(node).trim(); + if (!headingText) return; + + for (const child of node.children ?? []) { + const className = child.properties?.className; + const classes = Array.isArray(className) ? className : [className]; + if (child.tagName === 'a' && classes.includes('heading-anchor')) { + child.properties.ariaLabel = `Permalink to ${headingText}`; + } + } + }); + }; + }, ], }, }); diff --git a/package-lock.json b/package-lock.json index fc57806..0ea3e53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,6 @@ "": { "name": "schmelczer-dev", "license": "GPL-3.0-or-later", - "dependencies": { - "sharp": "^0.34.5" - }, "devDependencies": { "@astrojs/check": "^0.9.9", "@astrojs/rss": "^4.0.18", @@ -19,6 +16,7 @@ "prettier-plugin-astro": "^0.14.1", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", + "sharp": "^0.34.5", "typescript": "^5.9.3", "unist-util-visit": "^5.1.0" }, @@ -478,6 +476,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -930,6 +929,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -942,6 +942,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -964,6 +965,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -986,6 +988,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1002,6 +1005,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1018,6 +1022,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1034,6 +1039,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1050,6 +1056,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1066,6 +1073,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1082,6 +1090,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1098,6 +1107,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1114,6 +1124,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1130,6 +1141,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1146,6 +1158,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1168,6 +1181,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1190,6 +1204,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1212,6 +1227,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1234,6 +1250,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1256,6 +1273,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1278,6 +1296,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1300,6 +1319,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1322,6 +1342,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1341,6 +1362,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1360,6 +1382,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1379,6 +1402,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2892,6 +2916,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5240,6 +5265,7 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5252,6 +5278,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -5524,6 +5551,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, "optional": true }, "node_modules/typesafe-path": { @@ -6268,9 +6296,9 @@ } }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -6352,19 +6380,6 @@ "dev": true, "license": "MIT" }, - "node_modules/yaml-language-server/node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6630,7 +6645,7 @@ "integrity": "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg==", "dev": true, "requires": { - "yaml": "^2.8.2" + "yaml": "^2.9.0" } }, "@babel/helper-string-parser": { @@ -6756,6 +6771,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "optional": true, "requires": { "tslib": "^2.4.0" @@ -6946,12 +6962,14 @@ "@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==" + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true }, "@img/sharp-darwin-arm64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-darwin-arm64": "1.2.4" @@ -6961,6 +6979,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-darwin-x64": "1.2.4" @@ -6970,66 +6989,77 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "dev": true, "optional": true }, "@img/sharp-libvips-darwin-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-arm": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-ppc64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-riscv64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-s390x": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "dev": true, "optional": true }, "@img/sharp-libvips-linux-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "dev": true, "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "dev": true, "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "dev": true, "optional": true }, "@img/sharp-linux-arm": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-arm": "1.2.4" @@ -7039,6 +7069,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-arm64": "1.2.4" @@ -7048,6 +7079,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-ppc64": "1.2.4" @@ -7057,6 +7089,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-riscv64": "1.2.4" @@ -7066,6 +7099,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-s390x": "1.2.4" @@ -7075,6 +7109,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linux-x64": "1.2.4" @@ -7084,6 +7119,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" @@ -7093,6 +7129,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "dev": true, "optional": true, "requires": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" @@ -7102,6 +7139,7 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "dev": true, "optional": true, "requires": { "@emnapi/runtime": "^1.7.0" @@ -7111,18 +7149,21 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "dev": true, "optional": true }, "@img/sharp-win32-ia32": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "dev": true, "optional": true }, "@img/sharp-win32-x64": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "dev": true, "optional": true }, "@jridgewell/sourcemap-codec": { @@ -8066,7 +8107,8 @@ "detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true }, "devalue": { "version": "5.8.1", @@ -9614,12 +9656,14 @@ "semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==" + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true }, "sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, "requires": { "@img/colour": "^1.0.0", "@img/sharp-darwin-arm64": "0.34.5", @@ -9800,6 +9844,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, "optional": true }, "typesafe-path": { @@ -10248,9 +10293,9 @@ "dev": true }, "yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true }, "yaml-language-server": { @@ -10269,7 +10314,7 @@ "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", - "yaml": "2.7.1" + "yaml": "^2.9.0" }, "dependencies": { "ajv": { @@ -10302,12 +10347,6 @@ "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", "dev": true - }, - "yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true } } }, diff --git a/package.json b/package.json index b7d350e..66171b0 100644 --- a/package.json +++ b/package.json @@ -49,5 +49,8 @@ "typescript": "^5.9.3", "unist-util-visit": "^5.1.0", "sharp": "^0.34.5" + }, + "overrides": { + "yaml": "^2.9.0" } } diff --git a/public/_headers b/public/_headers deleted file mode 100644 index 76e5aa6..0000000 --- a/public/_headers +++ /dev/null @@ -1,21 +0,0 @@ -/* - X-Content-Type-Options: nosniff - Referrer-Policy: strict-origin-when-cross-origin - -/_astro/* - Cache-Control: public, max-age=31536000, immutable - -/fonts/* - Cache-Control: public, max-age=31536000, immutable - -/media/* - Cache-Control: public, max-age=86400, stale-while-revalidate=604800 - -/favicon.ico - Cache-Control: public, max-age=604800 - -/*.xml - Cache-Control: public, max-age=300 - -/*.webmanifest - Cache-Control: public, max-age=300 diff --git a/public/media/video/ad_astra.mp4 b/public/media/video/ad_astra.mp4 index 50e7274..9a094c9 100644 Binary files a/public/media/video/ad_astra.mp4 and b/public/media/video/ad_astra.mp4 differ diff --git a/public/media/video/ad_astra.vtt b/public/media/video/ad_astra.vtt new file mode 100644 index 0000000..578c7b6 --- /dev/null +++ b/public/media/video/ad_astra.vtt @@ -0,0 +1,13 @@ +WEBVTT + +00:00.000 --> 00:04.000 +No spoken dialogue. Game audio only. + +00:04.000 --> 00:35.000 +The Ad Astra handheld board runs the game on a small OLED display. + +00:35.000 --> 01:05.000 +The player controls the game through the IR input while the engine updates the display in real time. + +01:05.000 --> 01:34.600 +The clip continues showing gameplay on the custom ATtiny85-based board. diff --git a/public/media/video/ad_astra.webm b/public/media/video/ad_astra.webm index 444dba7..2ad0d44 100644 Binary files a/public/media/video/ad_astra.webm and b/public/media/video/ad_astra.webm differ diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs index ded49c4..b6f4301 100644 --- a/scripts/check-links.mjs +++ b/scripts/check-links.mjs @@ -56,7 +56,7 @@ try { } const files = await walk(dist); -const htmlAndXml = files.filter((file) => /\.(html|xml)$/.test(file)); +const checkedFiles = files.filter((file) => /\.(html|xml|css|webmanifest)$/.test(file)); function pagePathname(file) { const rel = path.relative(dist, file).replaceAll(path.sep, '/'); @@ -65,14 +65,52 @@ function pagePathname(file) { return `/${rel}`; } -for (const file of htmlAndXml) { +function collectUrlReferences(body, rel) { + const urls = []; + + for (const match of body.matchAll(/\b(?:href|src|poster)=["']([^"']+)["']/g)) { + urls.push(match[1]); + } + + for (const match of body.matchAll(/\bsrcset=["']([^"']+)["']/g)) { + for (const candidate of match[1].split(',')) { + const url = candidate.trim().split(/\s+/)[0]; + if (url) urls.push(url); + } + } + + if (rel.endsWith('.css')) { + for (const match of body.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g)) { + urls.push(match[1]); + } + } + + if (rel.endsWith('.webmanifest')) { + try { + const manifest = JSON.parse(body); + for (const key of ['start_url', 'scope']) { + if (typeof manifest[key] === 'string') urls.push(manifest[key]); + } + for (const icon of manifest.icons ?? []) { + if (typeof icon?.src === 'string') urls.push(icon.src); + } + for (const screenshot of manifest.screenshots ?? []) { + if (typeof screenshot?.src === 'string') urls.push(screenshot.src); + } + } catch { + failures.push(`${rel}: invalid web manifest JSON`); + } + } + + return urls; +} + +for (const file of checkedFiles) { const body = await readFile(file, 'utf8'); const rel = path.relative(dist, file); const baseUrl = new URL(pagePathname(file), 'https://schmelczer.dev'); - const matches = body.matchAll(/\b(?:href|src)=["']([^"'#?]+)(?:[?#][^"']*)?["']/g); - for (const match of matches) { - const raw = match[1]; + for (const raw of collectUrlReferences(body, rel)) { if (/^(mailto:|tel:|data:)/i.test(raw)) continue; let parsed; diff --git a/scripts/check-overflow.mjs b/scripts/check-overflow.mjs index aae620e..8b2e75a 100644 --- a/scripts/check-overflow.mjs +++ b/scripts/check-overflow.mjs @@ -1,9 +1,10 @@ import { createServer } from 'node:http'; -import { readdir, readFile, stat } from 'node:fs/promises'; +import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises'; import path from 'node:path'; import { chromium } from 'playwright'; const dist = path.resolve('dist'); +const browserTmp = path.resolve('.astro', 'playwright-overflow-tmp'); const INDEX_FILE = 'index.html'; const MAX_NAV_RETRIES = 4; // Common device widths: iPhone SE / Galaxy S / iPhone 14 / iPad portrait / @@ -30,6 +31,7 @@ const MIME = { '.woff2': 'font/woff2', '.mp4': 'video/mp4', '.webm': 'video/webm', + '.vtt': 'text/vtt; charset=utf-8', '.pdf': 'application/pdf', }; @@ -59,9 +61,6 @@ async function discoverRoutes() { if (!file.endsWith('.html')) continue; const rel = path.relative(dist, file).replaceAll(path.sep, '/'); if (rel === '404.html') continue; - // /writing/* are meta-refresh redirect stubs to /articles/*, not real - // pages; measuring them would just remeasure /articles/. - if (rel.startsWith('writing/')) continue; if (rel === INDEX_FILE) { routes.add('/'); } else if (rel.endsWith(`/${INDEX_FILE}`)) { @@ -104,6 +103,16 @@ try { throw new Error('dist/ does not exist. Run npm run build first.'); } +// Some CI/dev containers mount /tmp as a very small tmpfs. Chromium uses the +// process temp directory for profiles and internal files; putting it under the +// already-ignored .astro/ directory keeps the overflow check reproducible even +// when the system temp mount is full. +await rm(browserTmp, { recursive: true, force: true }); +await mkdir(browserTmp, { recursive: true }); +process.env.TMPDIR = browserTmp; +process.env.TMP = browserTmp; +process.env.TEMP = browserTmp; + const routes = await discoverRoutes(); const server = createServer(async (req, res) => { @@ -125,6 +134,12 @@ const failures = []; function launchBrowser() { return chromium.launch({ headless: true, + env: { + ...process.env, + TMPDIR: browserTmp, + TMP: browserTmp, + TEMP: browserTmp, + }, args: ['--disable-dev-shm-usage', '--disable-gpu', '--no-sandbox'], }); } @@ -183,11 +198,11 @@ async function openBrowser() { async function newMeasurementContext(browser, width) { const context = await browser.newContext({ viewport: { width, height: 900 }, - javaScriptEnabled: false, + javaScriptEnabled: true, }); await context.route('**/*', (route) => { const type = route.request().resourceType(); - if (['font', 'image', 'media'].includes(type)) { + if (type === 'media') { route.abort('blockedbyclient'); } else { route.continue(); @@ -281,6 +296,7 @@ try { } } finally { server.close(); + await rm(browserTmp, { recursive: true, force: true }).catch(() => {}); } if (failures.length > 0) { diff --git a/src/components/ArticleList.astro b/src/components/ArticleList.astro index 0c0a8f3..791686e 100644 --- a/src/components/ArticleList.astro +++ b/src/components/ArticleList.astro @@ -7,7 +7,6 @@ import { ARTICLE_THUMBNAIL, articlePath, formatDate, formatDateShort } from '../ interface Props { posts: CollectionEntry<'posts'>[]; showYear?: boolean; - currentTag?: string; tagLimit?: number; // Opt-in: eagerly load the first thumbnail. Only set when the list is // reliably above the fold (home, tag pages). Lists below substantial @@ -15,13 +14,7 @@ interface Props { eagerFirstThumbnail?: boolean; } -const { - posts, - showYear = true, - currentTag, - tagLimit = 3, - eagerFirstThumbnail = false, -} = Astro.props; +const { posts, showYear = true, tagLimit = 3, eagerFirstThumbnail = false } = Astro.props; ---
    @@ -31,11 +24,13 @@ const { return (
  1. - + {tag} {counts && counts[tag] !== undefined && ( {counts[tag]} diff --git a/src/content.config.ts b/src/content.config.ts index 1ddedb6..46c0793 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -3,22 +3,38 @@ import type { SchemaContext } from 'astro:content'; import { glob } from 'astro/loaders'; import { z } from 'astro/zod'; -const safeUrl = z.string().refine( +function isRootRelativeUrl(url: string) { + return url.startsWith('/') && !url.startsWith('//'); +} + +const linkUrl = z.string().refine( (url) => { - if (url.startsWith('/')) return !url.startsWith('//'); + if (isRootRelativeUrl(url)) return true; try { const parsed = new URL(url); - return ['http:', 'https:', 'mailto:'].includes(parsed.protocol); + return ['https:', 'mailto:'].includes(parsed.protocol); } catch { return false; } }, - { message: 'URL must be an absolute http(s)/mailto URL or a root-relative path.' } + { message: 'URL must be an absolute https/mailto URL or a root-relative path.' } +); + +const mediaUrl = z.string().refine( + (url) => { + if (isRootRelativeUrl(url)) return true; + try { + return new URL(url).protocol === 'https:'; + } catch { + return false; + } + }, + { message: 'Media URL must be an absolute https URL or a root-relative path.' } ); const linkSchema = z.object({ label: z.string(), - url: safeUrl, + url: linkUrl, download: z.boolean().optional(), }); @@ -43,8 +59,10 @@ const mediaSchema = ({ image }: SchemaContext) => .object({ type: z.literal('video'), poster: image().optional(), - mp4: safeUrl.optional(), - webm: safeUrl.optional(), + mp4: mediaUrl.optional(), + webm: mediaUrl.optional(), + captions: mediaUrl.optional(), + captionsLabel: z.string().default('English captions'), alt: z.string().optional(), decorative: z.boolean().optional(), caption: z.string().optional(), @@ -56,7 +74,19 @@ const mediaSchema = ({ image }: SchemaContext) => ]) .refine((item) => item.decorative || (Boolean(item.alt) && Boolean(item.caption)), { message: 'Meaningful media needs both alt text and a caption.', - }); + }) + .refine( + (item) => item.type !== 'video' || item.decorative || Boolean(item.captions), + { + message: 'Meaningful video needs captions.', + } + ) + .refine( + (item) => item.type !== 'video' || item.decorative || Boolean(item.transcript), + { + message: 'Meaningful video needs a transcript.', + } + ); const posts = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/posts' }), diff --git a/src/content/posts/ad-astra-attiny85-game-engine.md b/src/content/posts/ad-astra-attiny85-game-engine.md index b2c5c51..0570b2d 100644 --- a/src/content/posts/ad-astra-attiny85-game-engine.md +++ b/src/content/posts/ad-astra-attiny85-game-engine.md @@ -20,8 +20,10 @@ media: poster: ./_assets/ad-astra.jpg webm: /media/video/ad_astra.webm mp4: /media/video/ad_astra.mp4 + captions: /media/video/ad_astra.vtt alt: Video demonstration of the embedded game running on a small OLED display. caption: The game engine ran on an ATtiny85V with an OLED display and IR input. + transcript: No spoken dialogue. The demonstration shows the Ad Astra handheld board running its OLED game, with the player moving through the small display while the IR input controls gameplay. --- Ad Astra came from wanting to combine graphics and microcontrollers without hiding behind a large development board. The result was a small embedded game engine and game built around an ATtiny85V, an OLED display, IR input, EEPROM persistence, and a custom PCB. diff --git a/src/content/posts/declared-shared-simulation-code.md b/src/content/posts/declared-shared-simulation-code.md index 9ad1591..f26512c 100644 --- a/src/content/posts/declared-shared-simulation-code.md +++ b/src/content/posts/declared-shared-simulation-code.md @@ -15,8 +15,6 @@ audience: technical links: - label: Source url: https://github.com/schmelczer/decla.red - - label: Demo - url: https://decla.red - label: BSc thesis url: /media/downloads/sdf2d-andras-schmelczer.pdf download: true diff --git a/src/content/posts/life-towers-immutable-tries.md b/src/content/posts/life-towers-immutable-tries.md index d9478f3..9a90cf6 100644 --- a/src/content/posts/life-towers-immutable-tries.md +++ b/src/content/posts/life-towers-immutable-tries.md @@ -16,8 +16,6 @@ audience: recruiter-relevant links: - label: Source url: https://github.com/schmelczer/life-towers/ - - label: Demo - url: https://towers.schmelczer.dev media: - type: image src: ./_assets/towers.jpg diff --git a/src/content/posts/photo-site-generator.md b/src/content/posts/photo-site-generator.md index 0dca526..0fb796f 100644 --- a/src/content/posts/photo-site-generator.md +++ b/src/content/posts/photo-site-generator.md @@ -11,9 +11,7 @@ role: Site generator author stack: ['Webpack', 'Image processing', 'Static site generation'] outcome: A generated static photo site for publishing photography with responsive image output audience: general -links: - - label: Site - url: https://photo.schmelczer.dev +links: [] --- Photos was a small webpage where you could view my photos. diff --git a/src/content/posts/sdf-2d-ray-tracing.md b/src/content/posts/sdf-2d-ray-tracing.md index 0263659..0486298 100644 --- a/src/content/posts/sdf-2d-ray-tracing.md +++ b/src/content/posts/sdf-2d-ray-tracing.md @@ -16,8 +16,6 @@ audience: recruiter-relevant links: - label: NPM package url: https://www.npmjs.com/package/sdf-2d - - label: Demo - url: https://sdf2d.schmelczer.dev - label: Video url: https://www.youtube.com/watch?v=K3cEtnZUNR0 - label: BSc thesis diff --git a/src/content/projects/declared.md b/src/content/projects/declared.md index afa9297..8c24a1f 100644 --- a/src/content/projects/declared.md +++ b/src/content/projects/declared.md @@ -12,8 +12,6 @@ essay: declared-shared-simulation-code links: - label: Source url: https://github.com/schmelczer/decla.red - - label: Demo - url: https://decla.red - label: BSc thesis url: /media/downloads/sdf2d-andras-schmelczer.pdf download: true diff --git a/src/content/projects/photos.md b/src/content/projects/photos.md index feb917f..a29a6fe 100644 --- a/src/content/projects/photos.md +++ b/src/content/projects/photos.md @@ -9,7 +9,5 @@ sortDate: 2016-07-01 technologies: ['Webpack', 'Image processing', 'Static site generation'] selected: false essay: photo-site-generator -links: - - label: Site - url: https://photo.schmelczer.dev +links: [] --- diff --git a/src/content/projects/sdf-2d.md b/src/content/projects/sdf-2d.md index 59890e6..cb3a24d 100644 --- a/src/content/projects/sdf-2d.md +++ b/src/content/projects/sdf-2d.md @@ -12,8 +12,6 @@ essay: sdf-2d-ray-tracing links: - label: NPM package url: https://www.npmjs.com/package/sdf-2d - - label: Demo - url: https://sdf2d.schmelczer.dev - label: Video url: https://www.youtube.com/watch?v=K3cEtnZUNR0 - label: BSc thesis diff --git a/src/content/projects/towers.md b/src/content/projects/towers.md index 57038d0..051a1c7 100644 --- a/src/content/projects/towers.md +++ b/src/content/projects/towers.md @@ -12,6 +12,4 @@ essay: life-towers-immutable-tries links: - label: Source url: https://github.com/schmelczer/life-towers/ - - label: Demo - url: https://towers.schmelczer.dev --- diff --git a/src/layouts/Post.astro b/src/layouts/Post.astro index d3245e4..236d9d9 100644 --- a/src/layouts/Post.astro +++ b/src/layouts/Post.astro @@ -12,6 +12,7 @@ import { adjacentPosts, articlePath, buildBreadcrumbJsonLd, + buildPersonJsonLd, buildBreadcrumbTrail, formatDate, getPublishedPosts, @@ -72,6 +73,7 @@ const blogPosting = { }; const breadcrumbJsonLd = buildBreadcrumbJsonLd(trail); +const personJsonLd = buildPersonJsonLd(); ---
    @@ -126,7 +128,7 @@ const breadcrumbJsonLd = buildBreadcrumbJsonLd(trail); alt={post.data.thumbnail.alt} formats={['avif', 'webp']} fallbackFormat="jpg" - widths={[640, 960, 1280, 1600, 1920, 2400]} + widths={[640, 960, 1280, 1600, 1920]} sizes="(max-width: 700px) calc(100vw - 3rem), (max-width: 1100px) calc(100vw - 4rem), 56rem" loading="eager" fetchpriority="high" diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro index 70f8580..48c0df7 100644 --- a/src/pages/articles/index.astro +++ b/src/pages/articles/index.astro @@ -62,7 +62,11 @@ const jsonLd = [blogJsonLd, breadcrumbJsonLd]; return (

    {year}

    - +
    ); }) diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro index d3eec13..4fb93eb 100644 --- a/src/pages/projects/index.astro +++ b/src/pages/projects/index.astro @@ -32,7 +32,7 @@ const jsonLd = [collectionJsonLd, breadcrumbJsonLd];

    Selected Projects

    - +
    diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index a2954e5..d87cbd2 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -44,7 +44,7 @@ function absolutizeUrls(html: string, baseUrl: string) { }); } -export const GET: APIRoute = async (context) => { +export const GET: APIRoute = async () => { const posts = await getPublishedPosts(); const feedUrl = absoluteUrl('/rss.xml'); const channelImage = await optimizeOgImage(ogDefault); @@ -82,7 +82,7 @@ export const GET: APIRoute = async (context) => { return rss({ title: site.name, description: site.description, - site: context.site ?? site.url, + site: site.url, xmlns: { atom: 'http://www.w3.org/2005/Atom', content: 'http://purl.org/rss/1.0/modules/content/', diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro index afee5dd..44dc0f6 100644 --- a/src/pages/tags/[tag].astro +++ b/src/pages/tags/[tag].astro @@ -36,6 +36,7 @@ const breadcrumbJsonLd = buildBreadcrumbJsonLd(trail); title={title} description={`Project articles and technical notes filed under #${tag}.`} jsonLd={breadcrumbJsonLd} + noindex >

    Articles

    - + diff --git a/src/scripts/theme-init.js b/src/scripts/theme-init.js index b79a163..c2c3606 100644 --- a/src/scripts/theme-init.js +++ b/src/scripts/theme-init.js @@ -10,17 +10,11 @@ document.documentElement.classList.add('js'); var STORAGE_KEY = 'theme'; - var LEGACY_KEY = 'dark-mode'; var THEME_BG = { light: '#fbfaf7', dark: '#151514' }; var saved = null; try { var value = localStorage.getItem(STORAGE_KEY); - if (value === 'light' || value === 'dark') { - saved = value; - } else { - var legacy = localStorage.getItem(LEGACY_KEY); - if (legacy !== null) saved = JSON.parse(legacy) ? 'dark' : 'light'; - } + if (value === 'light' || value === 'dark') saved = value; } catch (e) {} var theme = saved || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; diff --git a/src/styles/global.css b/src/styles/global.css index 3b3dcaa..290bbdc 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -358,6 +358,13 @@ margin-inline: calc(-1 * var(--space-1)); } + .footer-contact { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-2) var(--space-5); + } + /* Page header (shared by .home-intro, .page-header, .post-header) */ .home-intro { max-width: var(--measure-wide); @@ -470,10 +477,17 @@ font-size: var(--fs-caption); } - .breadcrumbs li + li::before { + .breadcrumbs li { + display: inline-flex; + align-items: baseline; + min-width: 0; + } + + .breadcrumbs li:not(:last-child)::after { content: '›'; - margin-right: var(--space-2); + margin-left: var(--space-2); color: var(--color-rule-medium); + flex: none; } .breadcrumbs a { @@ -538,7 +552,7 @@ } .tag-list a:hover, - .tag-list a[aria-current='true'] { + .tag-list a[aria-current='page'] { color: var(--color-fg); } @@ -601,6 +615,12 @@ padding-right: var(--space-3); } + .article-list h3, + .project-list h3 { + font-size: var(--fs-base); + line-height: var(--leading-snug); + } + .article-list .entry-title, .project-list h3 a { display: inline-flex; @@ -750,11 +770,6 @@ vertical-align: 0.15em; } - .project-list h3 { - font-size: var(--fs-base); - line-height: var(--leading-snug); - } - /* -- Project links ---------------------------------------------------- */ .project-links { @@ -768,6 +783,7 @@ .project-links a { min-height: 44px; + min-width: 44px; display: inline-flex; align-items: center; color: var(--color-link); @@ -791,7 +807,7 @@ } .project-card .project-links a { - min-height: 2.25rem; + min-height: 44px; } /* -- Post layout ------------------------------------------------------ */ @@ -914,7 +930,7 @@ font-weight: var(--weight-regular); font-size: 0.85em; text-decoration: none; - opacity: 0.4; + opacity: 0; transition: opacity 150ms ease; } @@ -936,7 +952,7 @@ @media (hover: none) { .prose .heading-anchor { - opacity: 0.6; + opacity: 1; } } @@ -1151,6 +1167,11 @@ line-height: 1.45; } + .media-transcript strong { + color: var(--color-fg); + font-weight: var(--weight-semibold); + } + /* -- Post nav --------------------------------------------------------- */ .post-nav { @@ -1223,6 +1244,11 @@ overflow-wrap: anywhere; } + .project-card h3 a { + min-height: 44px; + min-width: 44px; + } + .post-toc ol { list-style: none; padding: 0; @@ -1309,10 +1335,8 @@ display: inline-block; width: var(--switcher-w); height: var(--switcher-h); - /* Vertical margin enlarges the comfortable click target to 44px while - keeping the visual track at 24px. Hit area is the button's box; - margin is not clickable, but combined with header gap it ensures - adequate spacing between adjacent targets. */ + /* Adjacent header targets remain at least 44px apart while the visual + track stays compact. */ margin: max(var(--space-2), calc((44px - var(--switcher-h)) / 2)) 0; overflow: hidden; border: 1px solid var(--color-rule-medium); @@ -1384,24 +1408,36 @@ .theme-switcher { width: auto; height: auto; + min-block-size: 44px; + min-inline-size: 44px; + display: inline-flex; + align-items: center; + justify-content: center; padding: var(--space-1) var(--space-2); + overflow: visible; background: ButtonFace; color: ButtonText; border: 1px solid ButtonBorder; box-shadow: none; } - .theme-switcher::before, .theme-switcher::after { content: none; } .theme-switcher::before { content: 'Light'; + position: static; + width: auto; + height: auto; + transform: none; + background: transparent; + border-radius: 0; } .theme-switcher[aria-pressed='true']::before { content: 'Dark'; + transform: none; } } } @@ -1449,12 +1485,29 @@ padding-right: 0; } + .article-list time { + text-align: start; + white-space: nowrap; + } + .article-list .entry-thumbnail { aspect-ratio: 1; } .project-card { --project-thumb-size: 7rem; + + grid-template-columns: 1fr; + grid-template-areas: + 'thumb' + 'summary'; + } + + .project-card .project-thumbnail { + height: auto; + border-right: 0; + border-bottom: 1px solid var(--color-rule); + aspect-ratio: 16 / 9; } .project-card .project-meta {