Ionosphere.tv
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: ISR with on-demand revalidation

- Pages cache API responses for 1 hour (revalidate: 3600)
- POST /api/revalidate triggers Next.js ISR for all data pages
- Appview invalidate endpoint triggers frontend revalidation
- After publishing new data: POST /xrpc/tv.ionosphere.invalidate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+38 -3
+11 -2
apps/ionosphere-appview/src/routes.ts
··· 453 453 return c.json({ entries }); 454 454 }); 455 455 456 - // Invalidate cache (call after data changes) 457 - app.post("/xrpc/tv.ionosphere.invalidateConcordance", (c) => { 456 + // Invalidate all caches (call after data changes) 457 + app.post("/xrpc/tv.ionosphere.invalidate", (c) => { 458 458 indexCache = null; 459 + 460 + // Trigger frontend ISR revalidation 461 + const frontendUrl = process.env.FRONTEND_URL; 462 + const revalidateSecret = process.env.REVALIDATE_SECRET; 463 + if (frontendUrl) { 464 + const qs = revalidateSecret ? `?secret=${encodeURIComponent(revalidateSecret)}` : ""; 465 + fetch(`${frontendUrl}/api/revalidate${qs}`, { method: "POST" }).catch(() => {}); 466 + } 467 + 459 468 return c.json({ ok: true }); 460 469 }); 461 470
+25
apps/ionosphere/src/app/api/revalidate/route.ts
··· 1 + import { revalidatePath } from "next/cache"; 2 + import { NextRequest, NextResponse } from "next/server"; 3 + 4 + const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET; 5 + 6 + export async function POST(request: NextRequest) { 7 + const { searchParams } = new URL(request.url); 8 + const secret = searchParams.get("secret"); 9 + 10 + if (REVALIDATE_SECRET && secret !== REVALIDATE_SECRET) { 11 + return NextResponse.json({ error: "invalid secret" }, { status: 401 }); 12 + } 13 + 14 + // Revalidate all data-driven pages 15 + revalidatePath("/talks", "page"); 16 + revalidatePath("/speakers", "page"); 17 + revalidatePath("/concepts", "page"); 18 + revalidatePath("/concordance", "page"); 19 + // Dynamic routes 20 + revalidatePath("/talks/[rkey]", "page"); 21 + revalidatePath("/speakers/[rkey]", "page"); 22 + revalidatePath("/concepts/[rkey]", "page"); 23 + 24 + return NextResponse.json({ revalidated: true, timestamp: Date.now() }); 25 + }
+1 -1
apps/ionosphere/src/lib/api.ts
··· 1 1 const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 2 2 3 3 async function fetchApi<T>(path: string): Promise<T> { 4 - const res = await fetch(`${API_BASE}${path}`, { cache: "no-store" }); 4 + const res = await fetch(`${API_BASE}${path}`, { next: { revalidate: 3600 } }); 5 5 if (!res.ok) throw new Error(`API error: ${res.status} ${path}`); 6 6 return res.json(); 7 7 }
+1
fly.appview.toml
··· 14 14 PUBLIC_JETSTREAM_URL = "wss://jetstream1.us-west.bsky.network" 15 15 BOT_HANDLE = "ionosphere.tv" 16 16 BOT_DID = "did:plc:lkeq4oghyhnztbu4dxr3joff" 17 + FRONTEND_URL = "https://ionosphere.tv" 17 18 18 19 [http_service] 19 20 internal_port = 8080