diff --git a/src/app/social/posts/SocialPostBase.js b/src/app/social/posts/SocialPostBase.js index 48538e9..e629598 100644 --- a/src/app/social/posts/SocialPostBase.js +++ b/src/app/social/posts/SocialPostBase.js @@ -87,7 +87,10 @@ export default class SocialPostBase { if (filename) { // Keep the established public URLs; removing X does not require breaking image links. - await this.publicStorage.writeBytes(`twitter-images/${filename}`, image); + // These URLs are overwritten, so caches must revalidate before reusing them. + await this.publicStorage.writeBytes(`twitter-images/${filename}`, image, { + cacheControl: 'no-cache', + }); this.info(`Saved public image as ${filename}`); } } diff --git a/test/social/SocialPostBase.test.js b/test/social/SocialPostBase.test.js index e65a97f..0f2e894 100644 --- a/test/social/SocialPostBase.test.js +++ b/test/social/SocialPostBase.test.js @@ -55,8 +55,12 @@ test('posts to every client, saves the public image, and records the time per cl assert.deepEqual(bluesky.sent, [{ status: 'Post 1', media: [{ file: IMAGE, type: 'image/png' }] }]); assert.deepEqual(other.sent, bluesky.sent); assert.equal(post.images, 1); + + const publicImage = await s.publicBucket.get('twitter-images/hourly.png'); + + assert.equal(publicImage.httpMetadata.cacheControl, 'no-cache'); assert.deepEqual( - new Uint8Array(await (await s.publicBucket.get('twitter-images/hourly.png')).arrayBuffer()), + new Uint8Array(await publicImage.arrayBuffer()), IMAGE, ); assert.deepEqual(await json(s.privateBucket, 'bluesky-lastPostTimes.json'), { hourly: 3600 }); diff --git a/workers/updater/README.md b/workers/updater/README.md index bcca201..4c832ff 100644 --- a/workers/updater/README.md +++ b/workers/updater/README.md @@ -41,6 +41,30 @@ ambiguous outcome; storage cannot make those two external operations atomic. ## Social posts and screenshots +### Public routing and caching + +Cloudflare Pages serves the frontend. Cloud Connector serves individual +`/data/`, `/assets/splatnet/`, and `/twitter-images/` files directly from R2 +at their original site URLs; do not redirect file requests to the asset host. +Production `/data`, slash-terminated `/data/` directories, and slash-terminated +`/assets/splatnet/` directories redirect to the browser at `assets.splatoon2.ink`. +The asset-browser Worker handles directory listings only. + +Cloudflare's `Long-lived immutable media` cache rule uses a one-year edge TTL +for production `/assets/` files (excluding directories and HTML), and image +files under `assets.splatoon2.ink/assets/splatnet/`. The zone's Browser Cache TTL +is **Respect Existing Headers**; the old `/assets/*` Page Rule that forced a +one-month browser TTL is disabled. These settings are managed in Cloudflare, +not Wrangler. Do not add a browser TTL override to obtain longer edge caching. + +Rotating public images under `/twitter-images/` reuse their filenames. The +updater writes them with `Cache-Control: no-cache` so caches revalidate before +reuse. Keep them outside the long-lived asset rule. On September 15, 2026, +production routing and cache settings were verified, the existing schedule and +gear PNGs received this metadata, and their old edge-cache entries were purged. + +### Posting pipeline + `src/app/social` posts to Bluesky. Twitter/X support and its dependency have been removed. Local commands are `npm run social` and `npm run social:test`.