Eines der leistungsstärksten Features in Next.js ist Incremental Static Regeneration (ISR) — damit lassen sich statische Seiten nach dem Deployment aktualisieren, ohne die gesamte Site neu zu bauen. Astro bietet zwar keine eingebaute ISR-Unterstützung, aber ein ähnliches Muster lässt sich mit serverseitigem Rendering und Cloudflare KV als verteilter Cache-Schicht umsetzen.
Hochwertige Starter-Kits mit integriertem Authentifizierungsfluss (Auth.js), Objekt-Uploads (AWS, Clouflare R2, Firebase Storage, Supabase Storage), integrierten Zahlungen (Stripe, LemonSqueezy), E-Mail-Verifizierungsablauf (Resend, Postmark, Sendgrid) und viel mehr . Kompatibel mit jeder Datenbank (Redis, Postgres, MongoDB, SQLite, Firestore).
Get all 3 kits Bundle ↗- Next.js Starter Kit
- SvelteKit Starter Kit
- Astro Starter Kit
One-time license · Lifetime updates
In diesem umfassenden Leitfaden lernen Sie, wie Sie ein ISR-ähnliches System in Astro aufbauen, das das Beste aus beiden Welten verbindet: die Performance statischer Generierung mit der Flexibilität dynamischer Updates. Wir behandeln zeitbasierte Revalidierung, On-Demand-Revalidierung über Webhooks und Strategien zur Cache-Invalidierung.
Voraussetzungen
Sie benötigen Folgendes:
- Node.js 20 oder höher
- Ein Cloudflare-Konto
ISR verstehen und warum es wichtig ist
Klassische Static Site Generation (SSG) hat eine wesentliche Einschränkung: Bei jeder Inhaltsänderung müssen Sie die gesamte Site neu bauen und erneut deployen. Bei Sites mit Tausenden von Seiten kann das Minuten oder sogar Stunden dauern.
Incremental Static Regeneration löst dieses Problem mit einem hybriden Ansatz:
- Veralteten Inhalt sofort ausliefern: Wenn ein Nutzer eine Seite anfordert, liefern Sie die gecachte Version sofort aus
- Im Hintergrund revalidieren: Ist der Cache veraltet (basierend auf einem Zeit-Schwellenwert), starten Sie eine Regenerierung im Hintergrund
- Cache aktualisieren: Sobald die Regenerierung abgeschlossen ist, aktualisieren Sie den Cache für künftige Anfragen
Das bringt Ihnen:
- ⚡ Blitzschnelle Antwortzeiten (Auslieferung aus dem Cache)
- 🔄 Immer aktuelle Inhalte (automatische Revalidierung)
- 📉 Kürzere Build-Zeiten (kein vollständiger Rebuild nötig)
- 💰 Geringere Kosten (weniger Rechenzeit für Builds)
Wie wir ISR in Astro umsetzen
Unsere Implementierung nutzt:
- Astro mit SSR: Serverseitiges Rendering für dynamische Seitengenerierung
- Cloudflare KV: Verteilter Key-Value-Store zum Cachen gerenderter HTML-Seiten
- Middleware: Eigene Logik zur Prüfung der Cache-Gültigkeit und für Revalidierung
- Webhooks: API-Endpunkte für On-Demand-Cache-Invalidierung
Der Ablauf sieht so aus:
User Request → Middleware → Check KV Cache ↓ Cache exists & fresh? ↓ Yes ─────────────→ Return cached HTML ↓ No Generate page → Store in KV → Return HTMLNeue Astro-Anwendung erstellen
Legen wir los und erstellen ein neues Astro-Projekt. Führen Sie den folgenden Befehl aus:
npm create astro@latest my-isr-appWählen Sie bei den Abfragen:
Use minimal (empty) template, wenn Sie gefragt werden, wie Sie das neue Projekt starten möchten.Yes, wenn Sie gefragt werden, ob Abhängigkeiten installiert werden sollen.Yes, wenn Sie gefragt werden, ob ein Git-Repository initialisiert werden soll.
Anschließend wechseln Sie in das Projektverzeichnis und starten die App:
cd my-isr-appnpm install wranglernpm run devDie App sollte unter localhost:4321 laufen.
Cloudflare-Adapter in Ihr Astro-Projekt integrieren
Um Ihr Astro-Projekt auf Cloudflare Workers zu deployen und Cloudflare KV zu nutzen, installieren Sie den Cloudflare-Adapter. Führen Sie den folgenden Befehl aus:
npx astro add cloudflare --yesAktualisieren Sie astro.config.mjs, sodass output auf server steht:
import { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ output: 'server', adapter: cloudflare()});Cloudflare KV für Caching einrichten
Cloudflare KV (Key-Value) ist ein global verteilter Key-Value-Store, der sich hervorragend zum Cachen gerenderter Seiten am Edge eignet.
KV-Namespace erstellen
Erstellen Sie zuerst einen KV-Namespace für Ihren Cache:
# Create a production KV namespacenpx wrangler kv namespace create ISR_CACHEWählen Sie bei den Abfragen Folgendes:
- Would you like Wrangler to add it on your behalf? … yes
- What binding name would you like to use? … ISR_CREATE
- For local dev, do you want to connect to the remote resource instead of a local resource? … yes
Wrangler erstellt automatisch eine wrangler.jsonc-Datei.
Umgebungstypen konfigurieren
Erstellen Sie eine neue Datei src/env.d.ts (oder aktualisieren Sie die bestehende), um TypeScript-Definitionen für das KV-Binding hinzuzufügen:
/// <reference types="astro/client" />
type KVNamespace = import('@cloudflare/workers-types').KVNamespacetype ENV = { ISR_CACHE: KVNamespace}
type Runtime = import('@astrojs/cloudflare').Runtime<ENV>
declare namespace App { interface Locals extends Runtime { // Add custom locals here }}Damit kennt TypeScript Ihren KV-Namespace und bietet passende Autovervollständigung.
ISR-Cache-Utility erstellen
Erstellen wir als Nächstes ein wiederverwendbares Utility zur Verwaltung des ISR-Caches. Legen Sie eine neue Datei src/lib/isr-cache.ts an:
export interface ISRCacheOptions { /** * Time in seconds before cache is considered stale * @default 60 */ revalidate?: number
/** * Custom cache key prefix * @default 'isr' */ prefix?: string
/** * Whether to enable cache * @default true */ enabled?: boolean}
export interface CachedData { /** The cached HTML content */ html: string
/** Timestamp when the cache was created */ timestamp: number
/** Revalidation time in seconds */ revalidate: number
/** URL that was cached */ url: string}
export class ISRCache { private kv: KVNamespace private options: Required<ISRCacheOptions>
constructor(kv: KVNamespace, options: ISRCacheOptions = {}) { this.kv = kv this.options = { revalidate: options.revalidate ?? 60, prefix: options.prefix ?? 'isr', enabled: options.enabled ?? true, } }
/** * Generate a cache key for a given URL */ private getCacheKey(url: string): string { // Remove query params and hash for consistent caching const cleanUrl = url.split('?')[0].split('#')[0] return `${this.options.prefix}:${cleanUrl}` }
/** * Get cached data if it exists */ async get(url: string): Promise<CachedData | null> { if (!this.options.enabled) return null
try { const key = this.getCacheKey(url) const cached = await this.kv.get(key, 'json') as CachedData | null
return cached } catch (error) { console.error('[ISR Cache] Error getting cache:', error) return null } }
/** * Check if cached data is still fresh */ isFresh(cachedData: CachedData): boolean { const now = Date.now() const age = (now - cachedData.timestamp) / 1000 // Convert to seconds return age < cachedData.revalidate }
/** * Store HTML in cache */ async set(url: string, html: string, revalidate?: number): Promise<void> { if (!this.options.enabled) return
try { const key = this.getCacheKey(url) const data: CachedData = { html, timestamp: Date.now(), revalidate: revalidate ?? this.options.revalidate, url, }
// Store in KV with metadata await this.kv.put(key, JSON.stringify(data), { // Optional: Set expiration time to automatically clean up old cache // expirationTtl: (revalidate ?? this.options.revalidate) * 2, metadata: { url, cachedAt: new Date().toISOString(), }, })
console.log(`[ISR Cache] Cached: ${url} (revalidate: ${data.revalidate}s)`) } catch (error) { console.error('[ISR Cache] Error setting cache:', error) } }
/** * Invalidate (delete) cache for a specific URL */ async invalidate(url: string): Promise<boolean> { try { const key = this.getCacheKey(url) await this.kv.delete(key) console.log(`[ISR Cache] Invalidated: ${url}`) return true } catch (error) { console.error('[ISR Cache] Error invalidating cache:', error) return false } }
/** * Invalidate multiple URLs at once */ async invalidateMany(urls: string[]): Promise<number> { let invalidated = 0
await Promise.all( urls.map(async (url) => { const success = await this.invalidate(url) if (success) invalidated++ }) )
return invalidated }
/** * List all cached URLs with a given prefix */ async list(prefix?: string): Promise<string[]> { try { const listPrefix = prefix ? `${this.options.prefix}:${prefix}` : `${this.options.prefix}:` const list = await this.kv.list({ prefix: listPrefix })
return list.keys.map((key) => key.name.replace(`${this.options.prefix}:`, '')) } catch (error) { console.error('[ISR Cache] Error listing cache:', error) return [] } }
/** * Get cache statistics */ async getStats(): Promise<{ totalKeys: number keys: string[] }> { const keys = await this.list() return { totalKeys: keys.length, keys, } }}
/** * Helper function to create an ISR cache instance */export function createISRCache(locals: App.Locals, options?: ISRCacheOptions): ISRCache { const kv = locals.runtime.env.ISR_CREATE if (!kv) { throw new Error('ISR_CREATE KV namespace not found. Make sure it is configured in wrangler.toml') } return new ISRCache(kv, options)}Dieses Utility bietet eine saubere API für:
- Gecachte Seiten abrufen:
cache.get(url) - Frische prüfen:
cache.isFresh(data) - Cache setzen:
cache.set(url, html, revalidate) - Cache invalidieren:
cache.invalidate(url)odercache.invalidateMany(urls) - Gecachte Seiten auflisten:
cache.list()
Zeitbasierte Revalidierung implementieren
Erstellen wir als Nächstes Middleware, die das ISR-Muster umsetzt. Legen Sie eine neue Datei src/middleware.ts an:
import { defineMiddleware } from 'astro:middleware'import { createISRCache } from './lib/isr-cache'
const revalidate = 2
export const onRequest = defineMiddleware(async (context, next) => { const { request, locals, url } = context
// Only apply ISR to GET requests for HTML pages if (request.method !== 'GET') { return next() }
// Skip ISR for API routes and static assets if (url.pathname.startsWith('/api/') || url.pathname.match(/\.(js|css|png|jpg|svg|ico|woff|woff2)$/)) { return next() }
// Skip ISR if KV is not available (local development) if (!locals.runtime?.env?.ISR_CREATE) { console.log('[ISR] KV not available, skipping cache') return next() }
try { // Create ISR cache instance with 60 second default revalidation const cache = createISRCache(locals, { revalidate, enabled: true, })
// Try to get cached version const cached = await cache.get(url.pathname)
if (cached) { // Check if cache is still fresh if (cache.isFresh(cached)) { console.log(`[ISR] Cache HIT (fresh): ${url.pathname}`)
// Return cached HTML with cache headers return new Response(cached.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, s-maxage=31536000, stale-while-revalidate', 'X-Cache-Status': 'HIT', 'X-Cache-Age': String(Math.floor((Date.now() - cached.timestamp) / 1000)), }, }) } else { console.log(`[ISR] Cache HIT (stale): ${url.pathname}`)
// Cache is stale, but serve it anyway while we regenerate // This is the "stale-while-revalidate" pattern
// Return stale content immediately const response = new Response(cached.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, s-maxage=31536000, stale-while-revalidate', 'X-Cache-Status': 'STALE', 'X-Cache-Age': String(Math.floor((Date.now() - cached.timestamp) / 1000)), }, })
// Trigger background regeneration (non-blocking) context.locals.runtime.ctx.waitUntil( (async () => { try { console.log(`[ISR] Regenerating in background: ${url.pathname}`)
// Generate fresh page const freshResponse = await next() const freshHtml = await freshResponse.text()
// Update cache await cache.set(url.pathname, freshHtml, 60)
console.log(`[ISR] Background regeneration complete: ${url.pathname}`) } catch (error) { console.error(`[ISR] Background regeneration failed: ${url.pathname}`, error) } })() )
return response } } else { console.log(`[ISR] Cache MISS: ${url.pathname}`)
// No cache exists, generate page const response = await next()
// Only cache successful HTML responses if (response.status === 200 && response.headers.get('content-type')?.includes('text/html')) { const html = await response.text()
// Store in cache (non-blocking) context.locals.runtime.ctx.waitUntil(cache.set(url.pathname, html, 60))
// Return response with cache miss header return new Response(html, { status: response.status, headers: { ...Object.fromEntries(response.headers), 'X-Cache-Status': 'MISS', }, }) }
return response } } catch (error) { console.error('[ISR] Error in middleware:', error) // On error, fall back to normal SSR return next() }})Diese Middleware setzt das vollständige ISR-Muster um:
- Cache HIT (fresh): Gecachtes HTML sofort zurückgeben
- Cache HIT (stale): Veraltetes HTML sofort zurückgeben, im Hintergrund regenerieren
- Cache MISS: Seite generieren, cachen und HTML zurückgeben
Der Clou liegt im stale-while-revalidate-Muster — Nutzer erhalten immer sofortige Antworten, auch während der Cache aktualisiert wird.
Cache Hits ausliefern
Cache im Hintergrund automatisch regenerieren
Revalidierungs-API-Endpunkt erstellen
Erstellen wir als Nächstes API-Endpunkte für On-Demand-Revalidierung. Das ist nützlich, wenn Sie den Cache sofort invalidieren möchten — zum Beispiel, wenn sich Inhalte in Ihrem CMS ändern.
Legen Sie eine neue Datei src/pages/api/revalidate.ts an:
import type { APIRoute } from 'astro'import { createISRCache } from '../../lib/isr-cache'
/** * POST /api/revalidate * * Invalidate cache for specific paths on-demand * * Body: * { * "secret": "your-secret-key", * "paths": ["/", "/blog/post-2"] * } */
export const POST: APIRoute = async ({ request, locals }) => { try { // Parse request body const body = await request.json() const { secret, paths } = body
// Validate secret (you should set this as an environment variable) const REVALIDATION_SECRET = locals.runtime.env.REVALIDATION_SECRET || 'your-secret-key'
if (secret !== REVALIDATION_SECRET) { return new Response( JSON.stringify({ success: false, error: 'Invalid secret', }), { status: 401, headers: { 'Content-Type': 'application/json' }, } ) }
// Validate paths if (!Array.isArray(paths) || paths.length === 0) { return new Response( JSON.stringify({ success: false, error: 'Invalid paths array', }), { status: 400, headers: { 'Content-Type': 'application/json' }, } ) }
// Create cache instance const cache = createISRCache(locals)
// Invalidate all specified paths const invalidated = await cache.invalidateMany(paths)
return new Response( JSON.stringify({ success: true, invalidated, paths, message: `Successfully invalidated ${invalidated} path(s)`, }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ) } catch (error) { console.error('[Revalidate API] Error:', error)
return new Response( JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error', }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ) }}Jetzt können Sie den Cache on-demand invalidieren mit POST /api/revalidate, indem Sie die zu invalidierenden Pfade übergeben.
Ein Beispiel-cURL:
curl -X POST https://my-isr-app.launchfast.workers.dev/api/revalidate \-H "Content-Type: application/json" \-d '{"secret":"your-secret-key","paths":["/"]}'
Auf Cloudflare Workers deployen
Deployen Sie Ihre Anwendung auf Cloudflare Workers:
# Build the projectnpm run build
# Deploy to Cloudflare Workersnpx wrangler deployNach dem Deployment:
- Öffnen Sie Ihr Cloudflare-Dashboard
- Navigieren Sie zu Workers
- Fügen Sie eine Umgebungsvariable hinzu:
- Name:
REVALIDATION_SECRET - Wert: Eine sichere Zufallszeichenkette
- Name:
Ihr ISR-System ist jetzt live in Produktion!
Vergleich mit Next.js ISR
So vergleicht sich unsere Astro-ISR-Implementierung mit Next.js ISR:
| Feature | Next.js ISR | Astro + Cloudflare KV ISR |
|---|---|---|
| Zeitbasierte Revalidierung | ✅ Eingebaut | ✅ Eigene Implementierung |
| On-Demand-Revalidierung | ✅ revalidatePath() | ✅ Eigener API-Endpunkt |
| Stale-while-revalidate | ✅ Automatisch | ✅ In Middleware implementiert |
| Edge-Caching | ⚠️ Abhängig vom Hosting | ✅ Global über Cloudflare |
| Cache-Persistenz | ⚠️ Abhängig vom Hosting | ✅ Persistent in KV |
| Setup-Aufwand | ✅ Zero Config | ⚠️ Manuelle Einrichtung nötig |
| Flexibilität | ⚠️ Begrenzt | ✅ Volle Kontrolle |
Fazit
Sie haben Incremental Static Regeneration in Astro mit Cloudflare KV erfolgreich implementiert!
Astro bietet zwar keine eingebaute ISR wie Next.js, aber diese eigene Implementierung gibt Ihnen noch mehr Flexibilität und Kontrolle. Sie können die Cache-Logik an Ihre Anforderungen anpassen, verschiedene Storage-Backends nutzen und für Ihren Use Case optimieren.
Bei Fragen oder Anmerkungen erreichen Sie mich gerne auf Twitter.