feat(web): route R2 assets through Pages Function instead of r2.aicodebattle.com

The aicodebattle.com domain was never registered. Replace all hardcoded
https://r2.aicodebattle.com references with the /r2/ relative path, served
by a Cloudflare Pages Function that reads from the acb-data R2 bucket via
binding. Adds web/functions/r2/[[path]].ts and web/wrangler.toml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-30 07:55:44 -04:00
parent 8652e77655
commit ae0f072f80
8 changed files with 49 additions and 9 deletions

View file

@ -0,0 +1,34 @@
interface Env {
ACB_BUCKET: R2Bucket;
}
export const onRequest: PagesFunction<Env> = async (context) => {
try {
const url = new URL(context.request.url);
// Strip the leading /r2/ prefix to get the R2 object key
const key = url.pathname.replace(/^\/r2\//, '');
if (!key) {
return new Response('Not Found', { status: 404 });
}
if (!context.env.ACB_BUCKET) {
return new Response('R2 binding not configured', { status: 503 });
}
const object = await context.env.ACB_BUCKET.get(key);
if (!object) {
return new Response('Not Found', { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('Cache-Control', 'public, max-age=60');
headers.set('Access-Control-Allow-Origin', '*');
return new Response(object.body, { headers });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return new Response(`Error: ${msg}`, { status: 500 });
}
};

View file

@ -243,7 +243,7 @@ export async function registerBot(request: RegisterRequest): Promise<RegisterRes
// R2_BASE_URL is the Cloudflare R2 bucket custom domain for live data.
// The evolver writes live.json here every cycle with Cache-Control: max-age=10.
const R2_BASE_URL = 'https://r2.aicodebattle.com';
const R2_BASE_URL = '/r2';
export async function fetchEvolutionData(): Promise<EvolutionLiveData> {
// Evolution data changes every ~10s — bypass SWR, always fetch fresh
@ -500,7 +500,7 @@ export interface EnrichedIndex {
entries: EnrichedMatchEntry[];
}
const R2_COMMENTARY_BASE = 'https://r2.aicodebattle.com';
const R2_COMMENTARY_BASE = '/r2';
export async function fetchCommentary(matchId: string): Promise<EnrichedCommentary | null> {
try {

View file

@ -43,7 +43,7 @@ export interface CarouselOptions {
const DEFAULT_AUTO_ADVANCE_DELAY = 3000;
const METADATA_PANEL_WIDTH = 280;
const TRANSITION_MS = 300;
const R2_BASE = 'https://r2.aicodebattle.com';
const R2_BASE = '/r2';
const B2_FALLBACK = 'https://b2.aicodebattle.com';
const SWIPE_THRESHOLD = 50; // min px to trigger advance
const VELOCITY_THRESHOLD = 0.3; // px/ms — fast flick triggers even below threshold

View file

@ -14,7 +14,7 @@ const PLAYER_COLORS = [
];
// Configuration
const R2_BASE = 'https://r2.aicodebattle.com';
const R2_BASE = '/r2';
const B2_BASE = 'https://b2.aicodebattle.com';
const PAGES_BASE = 'https://ai-code-battle.pages.dev';

View file

@ -223,7 +223,7 @@ export function startAmbientPolling(intervalMs = 30_000): void {
try {
// Fetch evolution live data for generation changes
const evoResp = await fetch(
'https://r2.aicodebattle.com/evolution/live.json',
'/r2/evolution/live.json',
);
if (evoResp.ok) {
const evoData: LiveJSON = await evoResp.json();

View file

@ -68,7 +68,7 @@ export function getBotProfileOGTags(bot: {
win_rate: number;
evolved?: boolean;
}): OGTags {
const cardUrl = `https://r2.aicodebattle.com/cards/${bot.id}.png`;
const cardUrl = `/r2/cards/${bot.id}.png`;
return {
title: `${bot.name} - Bot Profile`,
@ -89,7 +89,7 @@ export function getReplayOGTags(match: {
}): OGTags {
const winner = match.participants.find(p => p.won);
const winnerName = winner ? winner.name : 'Draw';
const thumbnailUrl = `https://r2.aicodebattle.com/thumbnails/${match.id}.png`;
const thumbnailUrl = `/r2/thumbnails/${match.id}.png`;
return {
title: `Match: ${match.participants.map(p => p.name).join(' vs ')}`,

View file

@ -17,7 +17,7 @@ interface Section {
}
const PAGES_BASE = 'https://ai-code-battle.pages.dev';
const R2_BASE = 'https://r2.aicodebattle.com';
const R2_BASE = '/r2';
const B2_BASE = 'https://b2.aicodebattle.com';
const sections: Section[] = [
@ -497,7 +497,7 @@ export function renderDocsApiPage(): void {
<p>For replays and match metadata, always try R2 first and fall back to B2:</p>
<pre><code>async function fetchReplay(matchId: string): Promise<Replay> {
// Try R2 warm cache first
const r2Url = \`https://r2.aicodebattle.com/replays/\${matchId}.json.gz\`;
const r2Url = \`/r2/replays/\${matchId}.json.gz\`;
const r2Resp = await fetch(r2Url);
if (r2Resp.ok) {
return decompress(await r2Resp.arrayBuffer());

6
web/wrangler.toml Normal file
View file

@ -0,0 +1,6 @@
name = "ai-code-battle"
pages_build_output_dir = "dist"
[[r2_buckets]]
binding = "ACB_BUCKET"
bucket_name = "acb-data"