From acae034fd80fe63a7bcd7568f9b3a886b22d40af Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Mar 2026 02:00:00 -0800 Subject: [PATCH] feat: enable Cloudflare edge caching with public Cache-Control headers Change all route Cache-Control headers from `private` to `public` with `s-maxage` and `stale-while-revalidate` to allow Cloudflare to cache responses at the edge. All routes serve public Electron release data with no user-specific content, so this is safe. Also fix timezone detection for Cloudflare proxying: - Prefer CF-Connecting-IP (real client IP) over x-forwarded-for - Use first IP in x-forwarded-for (originating client) not last (proxy) Co-Authored-By: Claude Opus 4.6 --- app/helpers/timezone.ts | 11 +++++++++-- app/routes/build/release-job.tsx | 2 +- app/routes/history.tsx | 2 +- app/routes/history/date.tsx | 2 +- app/routes/home.tsx | 2 +- app/routes/pr/details.tsx | 2 +- app/routes/pr/lookup.tsx | 2 +- app/routes/release/all.tsx | 2 +- app/routes/release/compare.tsx | 2 +- app/routes/release/single.tsx | 2 +- app/routes/schedule.tsx | 2 +- 11 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/helpers/timezone.ts b/app/helpers/timezone.ts index 2bbe321..cdd4342 100644 --- a/app/helpers/timezone.ts +++ b/app/helpers/timezone.ts @@ -19,13 +19,20 @@ function isValidTimezone(tz: string): boolean { export const guessTimeZoneFromRequest = (request: Request): string => { const cookies = parse(request.headers.get('Cookie') || ''); if (cookies.tz && isValidTimezone(cookies.tz)) { - // TODO: Check it's a valid timezone return cookies.tz; } + // CF-Connecting-IP is the real client IP when behind Cloudflare, + // x-forwarded-for may contain Cloudflare edge IPs which would + // resolve to the data center's location instead of the user's. + const clientIp = request.headers.get('cf-connecting-ip'); + if (clientIp) { + const geo = lookup(clientIp.trim()); + return geo?.timezone ?? DEFAULT_TIMEZONE; + } const forwardedIps = request.headers.get('x-forwarded-for'); if (forwardedIps) { const allIps = forwardedIps.split(','); - const ip = allIps[allIps.length - 1].trim(); + const ip = allIps[0].trim(); const geo = lookup(ip); return geo?.timezone ?? DEFAULT_TIMEZONE; } diff --git a/app/routes/build/release-job.tsx b/app/routes/build/release-job.tsx index 80540da..52f39ba 100644 --- a/app/routes/build/release-job.tsx +++ b/app/routes/build/release-job.tsx @@ -30,7 +30,7 @@ export const loader = async (args: LoaderFunctionArgs) => { // Guess at three hours for the build time const estimatedCompletion = new Date(started.getTime() + 1_000 * 60 * 60 * 3); const timeZone = guessTimeZoneFromRequest(args.request); - args.context.cacheControl = 'private, max-age=30'; + args.context.cacheControl = 'public, max-age=30, s-maxage=30, stale-while-revalidate=60'; return { ...build, started: prettyDateString(build.started, timeZone), diff --git a/app/routes/history.tsx b/app/routes/history.tsx index 76f661c..43ff9b7 100644 --- a/app/routes/history.tsx +++ b/app/routes/history.tsx @@ -86,7 +86,7 @@ export const loader = async (args: LoaderFunctionArgs) => { calendarData[month][day].stable.push(release.version); } } - args.context.cacheControl = 'private, max-age=120'; + args.context.cacheControl = 'public, max-age=120, s-maxage=300, stale-while-revalidate=120'; const currentMonth = currentDate.getMonth(); const currentDayOfMonth = currentDate.getDate(); diff --git a/app/routes/history/date.tsx b/app/routes/history/date.tsx index 38a2a38..537e139 100644 --- a/app/routes/history/date.tsx +++ b/app/routes/history/date.tsx @@ -34,7 +34,7 @@ export const loader = async (args: LoaderFunctionArgs) => { isSameDay(new Date(r.fullDate), new Date(year, month - 1, day)), ); - args.context.cacheControl = 'private, max-age=120'; + args.context.cacheControl = 'public, max-age=120, s-maxage=300, stale-while-revalidate=120'; const timeZone = guessTimeZoneFromRequest(args.request); diff --git a/app/routes/home.tsx b/app/routes/home.tsx index ff8e911..b58f1c1 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -18,7 +18,7 @@ export const meta: MetaFunction = () => { export const loader = async (args: LoaderFunctionArgs) => { const [releases, active] = await Promise.all([getLatestReleases(), getActiveReleasesOrUpdate()]); const timeZone = guessTimeZoneFromRequest(args.request); - args.context.cacheControl = 'private, max-age=30'; + args.context.cacheControl = 'public, max-age=30, s-maxage=30, stale-while-revalidate=60'; return { releases, active, timeZone }; }; diff --git a/app/routes/pr/details.tsx b/app/routes/pr/details.tsx index a9e538e..bca31cb 100644 --- a/app/routes/pr/details.tsx +++ b/app/routes/pr/details.tsx @@ -67,7 +67,7 @@ export const loader = async (args: LoaderFunctionArgs) => { createdAt: prettyDateString(pr.createdAt, timeZone), mergedAt: pr.mergedAt ? prettyDateString(pr.mergedAt, timeZone) : null, }; - args.context.cacheControl = 'private, max-age=60'; + args.context.cacheControl = 'public, max-age=60, s-maxage=120, stale-while-revalidate=60'; } return pr; }; diff --git a/app/routes/pr/lookup.tsx b/app/routes/pr/lookup.tsx index 18d669e..8dfdc7f 100644 --- a/app/routes/pr/lookup.tsx +++ b/app/routes/pr/lookup.tsx @@ -24,7 +24,7 @@ export const loader = async (args: LoaderFunctionArgs) => { ...pr, createdAt: prettyDateString(pr.createdAt, guessTimeZoneFromRequest(args.request)), })); - args.context.cacheControl = 'private, max-age=60'; + args.context.cacheControl = 'public, max-age=60, s-maxage=120, stale-while-revalidate=60'; } return recentPRs ?? []; }; diff --git a/app/routes/release/all.tsx b/app/routes/release/all.tsx index 67c2ef9..0eedb38 100644 --- a/app/routes/release/all.tsx +++ b/app/routes/release/all.tsx @@ -77,7 +77,7 @@ export const loader = async (args: LoaderFunctionArgs) => { const timeZone = guessTimeZoneFromRequest(args.request); - args.context.cacheControl = 'private, max-age=30'; + args.context.cacheControl = 'public, max-age=30, s-maxage=30, stale-while-revalidate=60'; return { releases: page > maxPage ? [undefined] : inChannel.slice(start, end), maxPage, diff --git a/app/routes/release/compare.tsx b/app/routes/release/compare.tsx index 45eec97..f6f92f5 100644 --- a/app/routes/release/compare.tsx +++ b/app/routes/release/compare.tsx @@ -96,7 +96,7 @@ export const loader = async (args: LoaderFunctionArgs) => { }), ); - args.context.cacheControl = 'private, max-age=300'; + args.context.cacheControl = 'public, max-age=300, s-maxage=600, stale-while-revalidate=300'; return { fromElectronRelease, diff --git a/app/routes/release/single.tsx b/app/routes/release/single.tsx index af6f733..1604a59 100644 --- a/app/routes/release/single.tsx +++ b/app/routes/release/single.tsx @@ -66,7 +66,7 @@ export const loader = async (args: LoaderFunctionArgs) => { const isLatestStable = latestReleases.latestSupported[0]?.version === version.substr(1); const isLatestPreRelease = latestReleases.lastPreRelease?.version === version.substr(1); - args.context.cacheControl = 'private, max-age=300'; + args.context.cacheControl = 'public, max-age=300, s-maxage=600, stale-while-revalidate=300'; return { allVersionsInMajor, electronRelease, diff --git a/app/routes/schedule.tsx b/app/routes/schedule.tsx index 8165e0b..ba27e26 100644 --- a/app/routes/schedule.tsx +++ b/app/routes/schedule.tsx @@ -59,7 +59,7 @@ function DependencyRelease({ export const loader = async (args: LoaderFunctionArgs) => { const timeZone = guessTimeZoneFromRequest(args.request); const releases = await getRelativeSchedule(); - args.context.cacheControl = 'private, max-age=120'; + args.context.cacheControl = 'public, max-age=120, s-maxage=300, stale-while-revalidate=120'; return { releases, timeZone }; };