diff --git a/public/404.html b/public/404.html index 5614acd..ae2646e 100644 --- a/public/404.html +++ b/public/404.html @@ -17,9 +17,9 @@ - - - + + + @@ -31,6 +31,15 @@ + @@ -51,9 +60,12 @@
Homo Reseticus surrounded by an unreachable node pattern
diff --git a/public/assets/homo-reseticus-404-276.0fde8f3e.webp b/public/assets/homo-reseticus-404-276.0fde8f3e.webp new file mode 100644 index 0000000..078fbc1 Binary files /dev/null and b/public/assets/homo-reseticus-404-276.0fde8f3e.webp differ diff --git a/public/assets/homo-reseticus-404-552.f829379a.webp b/public/assets/homo-reseticus-404-552.f829379a.webp new file mode 100644 index 0000000..2012e5b Binary files /dev/null and b/public/assets/homo-reseticus-404-552.f829379a.webp differ diff --git a/public/assets/homo-reseticus-404-828.68be2119.webp b/public/assets/homo-reseticus-404-828.68be2119.webp new file mode 100644 index 0000000..ab95dec Binary files /dev/null and b/public/assets/homo-reseticus-404-828.68be2119.webp differ diff --git a/public/assets/homo-reseticus-404.png b/public/assets/homo-reseticus-404.png deleted file mode 100644 index feced4d..0000000 Binary files a/public/assets/homo-reseticus-404.png and /dev/null differ diff --git a/public/assets/styles.css b/public/assets/styles.css index e17fa74..275035f 100644 --- a/public/assets/styles.css +++ b/public/assets/styles.css @@ -957,7 +957,7 @@ dialog::backdrop { .not-found-visual { position: relative; isolation: isolate; - width: clamp(190px, 39vmin, 414px); + width: clamp(127px, 26vmin, 276px); max-width: 100%; } @@ -974,14 +974,14 @@ dialog::backdrop { background: radial-gradient( ellipse at 50% 47%, - rgb(255 255 255 / 58%) 0%, - rgb(244 248 244 / 36%) 36%, - rgb(211 224 213 / 16%) 58%, + rgb(0 255 65 / 48%) 0%, + rgb(0 230 57 / 30%) 36%, + rgb(0 128 33 / 14%) 58%, transparent 76% ), radial-gradient( ellipse at 50% 48%, - rgb(192 216 198 / 12%) 0%, + rgb(0 255 65 / 14%) 0%, transparent 72% ); content: ""; @@ -993,7 +993,7 @@ dialog::backdrop { display: block; width: 100%; height: auto; - filter: contrast(1.06) drop-shadow(0 0 18px rgb(229 226 225 / 14%)); + filter: contrast(1.06) drop-shadow(0 0 16px rgb(0 255 65 / 16%)); } .not-found-action { diff --git a/server.js b/server.js index 0982421..4638649 100644 --- a/server.js +++ b/server.js @@ -24,6 +24,9 @@ const REQUEST_MAX_BYTES = 6 * 1024 * 1024; const API_READ_LIMIT_PER_MINUTE = 120; const MEDIA_READ_LIMIT_PER_MINUTE = 600; const SSE_HEARTBEAT_MS = 25_000; +const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable'; +const REVALIDATING_CACHE_CONTROL = 'public, max-age=300, must-revalidate'; +const FINGERPRINTED_ASSET = /\.[a-f0-9]{8,}\.[a-z0-9]+$/i; const events = new Set(); const DISCOVERY_ROUTES = new Set([ '/openapi.json', @@ -152,6 +155,8 @@ const server = http.createServer(async (req, res) => { } if (req.method === 'GET' && url.pathname.startsWith('/assets/')) { + const isVersioned = url.searchParams.has('v') || FINGERPRINTED_ASSET.test(url.pathname); + res.setHeader('Cache-Control', isVersioned ? IMMUTABLE_CACHE_CONTROL : REVALIDATING_CACHE_CONTROL); return sendFile(res, `./public${url.pathname}`); } diff --git a/src/seo.js b/src/seo.js index c91aca2..1aaaf69 100644 --- a/src/seo.js +++ b/src/seo.js @@ -2,7 +2,7 @@ const SITE_NAME = 'The Meme Protocol'; const SITE_DESCRIPTION = 'A live, moderated meme stream for The Meme Protocol: WebP memes, MEME_CONSENSUS_SCORE ranking, and community review.'; const REPO_URL = 'https://git.yoonect.com/Nautilus/bitsforfree'; const SOCIAL_IMAGE_PATH = '/assets/social-preview-v1.png'; -const NOT_FOUND_IMAGE_PATH = '/assets/homo-reseticus-404.png'; +const NOT_FOUND_IMAGE_PATH = '/assets/homo-reseticus-404-828.68be2119.webp'; const FAVICON_PATH = '/assets/favicon'; const FAVICON_VERSION = '20260814'; diff --git a/tests/notFound.test.js b/tests/notFound.test.js index 8558e92..545b720 100644 --- a/tests/notFound.test.js +++ b/tests/notFound.test.js @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import sharp from 'sharp'; import { renderNotFound } from '../src/seo.js'; const template = fs.readFileSync(new URL('../public/404.html', import.meta.url), 'utf8'); @@ -10,7 +11,11 @@ const page = renderNotFound({ pathname: '/missing-node' }); const styles = fs.readFileSync(new URL('../public/assets/styles.css', import.meta.url), 'utf8'); -const artwork = fs.readFileSync(new URL('../public/assets/homo-reseticus-404.png', import.meta.url)); +const artworkPaths = [ + new URL('../public/assets/homo-reseticus-404-276.0fde8f3e.webp', import.meta.url), + new URL('../public/assets/homo-reseticus-404-552.f829379a.webp', import.meta.url), + new URL('../public/assets/homo-reseticus-404-828.68be2119.webp', import.meta.url) +]; test('404 page is noindexed and links visitors back to the live feed', () => { assert.match(page, /404 \| The Meme Protocol<\/title>/); @@ -26,7 +31,8 @@ test('404 page carries adjusted site discovery and social metadata', () => { assert.match(page, /<link rel="service-desc"[^>]+href="\/openapi\.json">/); assert.match(page, /<link rel="help"[^>]+href="\/meme-api\.skill\.md">/); assert.match(page, /<meta property="og:url" content="https:\/\/memes\.example\/missing-node">/); - assert.match(page, /<meta property="og:image" content="https:\/\/memes\.example\/assets\/homo-reseticus-404\.png">/); + assert.match(page, /<meta property="og:image" content="https:\/\/memes\.example\/assets\/homo-reseticus-404-828\.68be2119\.webp">/); + assert.match(page, /<meta property="og:image:type" content="image\/webp">/); assert.match(page, /<meta name="twitter:card" content="summary_large_image">/); assert.doesNotMatch(page, /<link rel="canonical"/); }); @@ -38,13 +44,30 @@ test('404 page keeps the homepage protocol menu and replaces its live status', ( assert.match(page, /ERROR_404 \/\/ NODE_UNREACHABLE/); }); -test('404 page uses the supplied artwork with a diffused backdrop', () => { - assert.match(page, /src="\/assets\/homo-reseticus-404\.png"/); +test('404 page uses responsive artwork with a diffused green backdrop', () => { + assert.match(page, /src="\/assets\/homo-reseticus-404-552\.f829379a\.webp"/); + assert.match(page, /srcset="[^"]+276\.0fde8f3e\.webp 276w,[^"]+552\.f829379a\.webp 552w,[^"]+828\.68be2119\.webp 828w"/); + assert.match(page, /fetchpriority="high"/); assert.match(page, /alt="Homo Reseticus surrounded by an unreachable node pattern"/); assert.match(styles, /\.not-found-visual::before/); - assert.match(styles, /radial-gradient\(/); + assert.match(styles, /rgb\(0 255 65 \/ 48%\)/); assert.match(styles, /filter: blur\(36px\)/); assert.match(styles, /transform: scale\(1\.24\)/); + assert.match(styles, /width: clamp\(127px, 26vmin, 276px\)/); assert.match(styles, /\.not-found-page \.pulse/); - assert.equal(artwork.subarray(1, 4).toString('ascii'), 'PNG'); +}); + +test('404 artwork variants retain lossless WebP transparency', async () => { + const expectedSizes = [[276, 271], [552, 542], [828, 813]]; + + for (const [index, artworkPath] of artworkPaths.entries()) { + const artwork = fs.readFileSync(artworkPath); + const metadata = await sharp(artwork).metadata(); + assert.equal(artwork.subarray(0, 4).toString('ascii'), 'RIFF'); + assert.equal(artwork.subarray(8, 12).toString('ascii'), 'WEBP'); + assert.equal(artwork.subarray(12, 16).toString('ascii'), 'VP8L'); + assert.equal(metadata.format, 'webp'); + assert.equal(metadata.hasAlpha, true); + assert.deepEqual([metadata.width, metadata.height], expectedSizes[index]); + } });