Einführung
Es gab eine Zeit, vor Google Analytics und vor Produkt-Dashboards, in der fast jede Website stolz einen kleinen Zähler am Seitenende zeigte: „Du bist Besucher Nummer 12.345“. Diese Old-School-Zähler verfolgten keine Personen, erstellten keine Zielgruppenprofile und sendeten keine Daten an Dritte. Sie zählten nur Impressionen und zeigten eine ehrliche Zahl.
Diese Anleitung beschreibt, wie du diese Idee für einen modernen Blog auf Cloudflare nachbauen kannst: einen Impressions- oder Aufrufzähler pro Artikel, persistent in einer Datenbank gespeichert, ohne separaten Server, praktisch ohne Kosten und ohne die Privatsphäre der Leser zu opfern.
Als Speicher dient Cloudflare D1, eine verwaltete SQLite-Datenbank, die am Edge läuft und über ein Binding mit deinem Worker verbunden wird. D1 wurde statt KV gewählt, weil KV keinen atomaren Inkrementbefehl hat und bei gleichzeitigem Traffic Zählungen verlieren würde. D1 führt count = count + 1 atomar in einer einzigen SQL-Anweisung aus.
Die Anleitung behandelt zwei Implementierungen desselben Musters:
- Astro (konkret ein Blog im Stil von AstroPaper) mit dem Adapter
@astrojs/cloudflareundoutput: 'server'. - SvelteKit mit dem Adapter
@sveltejs/adapter-cloudflare, ebenfalls in SSR.
In beiden Fällen hat das Ergebnis diese Eigenschaften:
- Der Zähler lebt in einem eigenen Endpoint desselben Projekts; es gibt keinen separaten Server und kein separates Deployment.
- Die Website braucht kein zusätzliches SSR: Wenn du bereits mit einem Cloudflare-Adapter arbeitest, verwendet der Endpoint denselben Worker.
- Der Nutzer wird nicht getrackt, es gibt keine Third-Party-Cookies und keine persistenten IDs; es wird nur eine Impression gezählt.
- Das Inkrement ist atomar, sodass der Zähler bei Nebenläufigkeit nicht beschädigt wird.
- Für einen persönlichen Blog sind die Kosten effektiv null.
Inhaltsverzeichnis
Inhaltsverzeichnis
- Allgemeine Architektur
- Voraussetzungen
- Kostet das etwas?
- D1-Datenbank erstellen
- Teil 1: Astro (AstroPaper) in SSR mit dem Cloudflare-Adapter
- Teil 2: SvelteKit mit dem Cloudflare-Adapter in SSR
- Optionale visuelle Anpassung
- Optionale Variante: Reloads desselben Besuchers nicht zählen
- Grundlegende Härtung
- Troubleshooting
- Fazit
- Anhang: Berechtigungen des Cloudflare-API-Tokens
- Referenzen
Allgemeine Architektur
Das Muster ist in beiden Frameworks identisch. Es ändert sich nur, wie das Framework das D1-Binding dem Endpoint-Code bereitstellt.
flowchart LR
READER["Browser des Lesers"] -->|"POST /api/views/:slug"| WORKER["Worker am Edge (Astro oder SvelteKit mit Cloudflare-Adapter)"]
WORKER -->|"DB-Binding"| D1[("Cloudflare D1 SQLite")]
D1 -->|"aktualisierter count"| WORKER
WORKER -->|"JSON { count }"| READER
READER -->|"rendern"| DOM["12.345 Aufrufe"]
Der Ablauf bei jedem Laden eines Artikels:
- Die Seite wird normal gerendert (statisch oder SSR, das spielt keine Rolle).
- Ein kleines clientseitiges Script sendet beim Laden
POST /api/views/<slug>. - Der Endpoint, innerhalb desselben Workers, führt ein atomares
UPSERTin D1 aus und gibt die neue Summe zurück. - Das Script schreibt die Zahl in den DOM.
Voraussetzungen
Verwende diese Checkliste vor der Implementierung:
- Du hast einen Blog auf Cloudflare Workers (oder Pages) mit Astro +
@astrojs/cloudflareoder mit SvelteKit +@sveltejs/adapter-cloudflarebereitgestellt. - [ ] Du hast Wrangler installiert und authentifiziert (npx wrangler login). - [ ] Dein Projekt deployt bereits korrekt, bevor du den Zähler hinzufügst. - [ ] Du kennst den slug oder eindeutigen Bezeichner jedes Artikels, der zur Renderzeit verfügbar ist (Frontmatter, Routenparameter usw.). - [ ] Du akzeptierst, dass der Anfangszähler bei null beginnt; es gibt keine historischen Daten zu importieren (falls doch, kannst du sie mit einemINSERTvorladen). - [ ] Du bist damit einverstanden, Impressionen zu zählen, keine eindeutigen Nutzer, es sei denn, du verwendest die unten beschriebene optionale Deduplizierung.
Kostet das etwas?
Für einen persönlichen Blog: nein. Die Limits des kostenlosen Cloudflare-Plans liegen weit über dem, was ein Besucherzähler verbraucht:
| Ressource | Limit des kostenlosen Plans | Verbrauch des Zählers |
|---|---|---|
| Workers / Aufrufe | 100.000 Requests/Tag | 1 POST pro Artikelladung |
| D1-Zeilenlesungen | 5.000.000 gelesene Zeilen/Tag | 1 Zeile pro Impression |
| D1-Zeilenschreibungen | 100.000 geschriebene Zeilen/Tag | 1 Zeile pro Impression |
| D1-Speicher | 5 GB | eine winzige Zeile pro Artikel |
Eine einzige Zeile pro Artikel (Slug + Integer) wiegt nur wenige Bytes. Selbst ein Blog mit Zehntausenden täglichen Besuchen bleibt komfortabel im kostenlosen Plan. Wenn du ihn irgendwann überschreitest, berechnet der D1-Bezahlplan gelesene/geschriebene Zeilen und GB-Monate; für diesen Anwendungsfall sind das Cent-Beträge.
D1-Datenbank erstellen
Dieser Schritt ist für Astro und SvelteKit identisch. Die Tabelle ist dieselbe.
Erstelle die Datenbank:
npx wrangler d1 create viewsWrangler gibt einen Block mit der database_id aus. Bewahre sie auf; du brauchst sie in wrangler.jsonc.
Erstelle die Tabelle. Es ist sinnvoll, sie in einer versionierten Schema-Datei zu speichern:
CREATE TABLE IF NOT EXISTS views ( slug TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0);Wende sie zuerst remote an (die echte Datenbank, die dein deployed Worker nutzt):
npx wrangler d1 execute views --remote --file=./schema.sqlUnd, falls du lokal mit wrangler dev entwickelst, auch auf die lokale Kopie:
npx wrangler d1 execute views --local --file=./schema.sqlTeil 1: Astro (AstroPaper) in SSR mit dem Cloudflare-Adapter
Dieser Teil setzt einen Blog im Stil von AstroPaper mit output: 'server' und adapter: cloudflare() voraus.
D1-Binding in wrangler deklarieren
Füge das Binding DB zu deinem wrangler.jsonc hinzu (neben assets, compatibility_date usw.):
{ "name": "mein-blog", "compatibility_date": "2026-03-06", "compatibility_flags": ["nodejs_compat"], "assets": { "binding": "ASSETS", "directory": "./dist", }, // [!code ++] "d1_databases": [ // [!code ++] { // [!code ++] "binding": "DB", // [!code ++] "database_name": "views", // [!code ++] "database_id": "HIER-DIE-DATABASE-ID-EINFUEGEN", // [!code ++] }, // [!code ++] ],}Binding für TypeScript typisieren
In Astro 6 (Adapter @astrojs/cloudflare 13.x) generierst du die Typen aus den in wrangler.jsonc deklarierten Bindings:
npx wrangler typesDas erzeugt eine Datei worker-configuration.d.ts mit dem globalen Interface Env (das dein DB bereits enthält) sowie den Definitionen des Moduls cloudflare:workers. Deine tsconfig.json nimmt die Datei über das übliche include-Muster auf.
Der Zähler-Endpoint
Erstelle src/pages/api/views/[slug].ts. Ein einziger POST inkrementiert und gibt die Summe zurück:
import type { APIRoute } from "astro";import { env } from "cloudflare:workers";
// Die Website kann statisch sein; dieser Endpoint muss dynamisch sein.export const prerender = false;
export const POST: APIRoute = async ({ params }) => { const slug = params.slug; if (!slug) { return new Response("Slug fehlt", { status: 400 }); }
const db = env.DB;
// Atomarer UPSERT: mit 1 einfügen oder 1 addieren, wenn die Zeile schon existiert. await db .prepare( "INSERT INTO views (slug, count) VALUES (?, 1) " + "ON CONFLICT(slug) DO UPDATE SET count = count + 1" ) .bind(slug) .run();
const row = await db .prepare("SELECT count FROM views WHERE slug = ?") .bind(slug) .first<{ count: number }>();
return Response.json({ count: row?.count ?? 0 });};
// Optional: lesen ohne zu inkrementieren (z. B. für eine Statistikseite).export const GET: APIRoute = async ({ params }) => { const slug = params.slug; if (!slug) return new Response("Slug fehlt", { status: 400 });
const row = await env.DB.prepare("SELECT count FROM views WHERE slug = ?") .bind(slug) .first<{ count: number }>();
return Response.json({ count: row?.count ?? 0 });};Die visuelle Komponente
Erstelle eine wiederverwendbare Komponente src/components/ViewCounter.astro, die den Slug entgegennimmt und die Zahl rendert:
---interface Props { slug: string;}const { slug } = Astro.props;---
<span class="view-counter" aria-live="polite"> <span id="view-count" data-slug={slug}>…</span> Aufrufe</span>
<script> function loadViewCount() { const el = document.getElementById("view-count"); if (!el) return; const slug = el.dataset.slug; fetch(`/api/views/${slug}`, { method: "POST" }) .then(r => r.json() as Promise<{ count: number }>) .then(d => { el.textContent = new Intl.NumberFormat().format(d.count); }) .catch(() => { el.textContent = "—"; }); } // astro:page-load läuft beim ersten Laden und nach jeder ClientRouter-Navigation. document.addEventListener("astro:page-load", loadViewCount);</script>Verwende sie im Artikel-Layout (in AstroPaper typischerweise src/layouts/PostDetails.astro oder ähnlich) und übergib den Slug des Posts:
---import ViewCounter from "@/components/ViewCounter.astro";// `post` ist der Artikel-Eintrag in deinem Layout.---
<ViewCounter slug={post.id} />Teil 2: SvelteKit mit dem Cloudflare-Adapter in SSR
Das Muster ist dasselbe; die Framework-Mechanik ändert sich. SvelteKit stellt Cloudflare-Bindings über event.platform.env bereit.
Adapter und Binding
Stelle sicher, dass du @sveltejs/adapter-cloudflare in svelte.config.js verwendest:
import adapter from "@sveltejs/adapter-cloudflare";import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
export default { preprocess: vitePreprocess(), kit: { adapter: adapter(), },};Deklariere das Binding DB wie zuvor in wrangler.jsonc:
{ "name": "mein-sveltekit-blog", "compatibility_date": "2026-03-06", "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", "database_name": "views", "database_id": "HIER-DIE-DATABASE-ID-EINFUEGEN", }, ],}Plattform typisieren
In SvelteKit leben Bindings in App.Platform. Deklariere sie in src/app.d.ts:
declare global { namespace App { interface Platform { env: { DB: D1Database; }; } }}
export {};Der Zähler-Endpoint
Erstelle src/routes/api/views/[slug]/+server.ts:
import { json, error } from "@sveltejs/kit";import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ params, platform }) => { const db = platform?.env.DB; if (!db) throw error(500, "D1-Binding nicht verfügbar");
const slug = params.slug;
await db .prepare( "INSERT INTO views (slug, count) VALUES (?, 1) " + "ON CONFLICT(slug) DO UPDATE SET count = count + 1" ) .bind(slug) .run();
const row = await db .prepare("SELECT count FROM views WHERE slug = ?") .bind(slug) .first<{ count: number }>();
return json({ count: row?.count ?? 0 });};
export const GET: RequestHandler = async ({ params, platform }) => { const db = platform?.env.DB; if (!db) throw error(500, "D1-Binding nicht verfügbar");
const row = await db .prepare("SELECT count FROM views WHERE slug = ?") .bind(params.slug) .first<{ count: number }>();
return json({ count: row?.count ?? 0 });};Die visuelle Komponente
Erstelle src/lib/components/ViewCounter.svelte:
<script lang="ts"> import { onMount } from "svelte";
export let slug: string;
let count: number | null = null;
onMount(async () => { try { const r = await fetch(`/api/views/${slug}`, { method: "POST" }); const d = (await r.json()) as { count: number }; count = d.count; } catch { count = null; } });</script>
<span aria-live="polite"> {count === null ? "—" : new Intl.NumberFormat().format(count)} Aufrufe</span>Verwende sie auf der Artikelseite (src/routes/blog/[slug]/+page.svelte oder dort, wo du den Post renderst):
<script lang="ts"> import ViewCounter from "$lib/components/ViewCounter.svelte"; export let data; // kommt aus deinem +page.ts / load</script>
<ViewCounter slug={data.post.slug} />Optionale visuelle Anpassung
Die ehrliche Zahl reicht bereits aus, aber ein Teil des Charmes alter Besucherzähler war ihr Aussehen: weiße Ziffern auf schwarzem Grund, LED-Display-Stil oder mechanische Zählwerk-Optik. Es gibt zwei Wege dahin.
Option A (empfohlen): LED-Stil mit CSS
Gib dem Zähler mit einer Monospace-Schrift, dunklem Hintergrund und leichtem Leuchten ein digitales Display-Aussehen. Die Komponente ändert sich nicht: Du fügst nur Styles hinzu. Für das <span class="view-counter"> der Astro-Komponente (oder das <span> der Svelte-Komponente) ergänzt du:
.view-counter { display: inline-flex; align-items: center; gap: 0.35em; font-family: "Courier New", ui-monospace, monospace; font-weight: 700; letter-spacing: 0.08em; padding: 0.15em 0.5em; border-radius: 0.25em; background: #000; color: #00ff6a; /* klassisches Phosphorgrün */ text-shadow: 0 0 6px #00ff6a; /* LED-Leuchten */}Für das klassische Weiß auf Schwarz aus dem Anhang änderst du color: #fff und entfernst oder passt text-shadow an.
Option A+: jede Ziffer in einem eigenen „Feld“
Die mechanische Zählwerk-Optik (jede Ziffer in einem eigenen schwarzen Feld) erfordert, jede Ziffer in ein eigenes Element einzuwickeln. Ändere das Rendering so, dass pro Ziffer ein <span> erzeugt wird, statt die rohe Zahl zu schreiben. In der Astro-Komponente:
fetch(`/api/views/${slug}`, { method: "POST" }) .then(r => r.json() as Promise<{ count: number }>) .then(d => { const digits = new Intl.NumberFormat().format(d.count); el.textContent = ""; for (const ch of digits) { const span = document.createElement("span"); span.className = /\d/.test(ch) ? "digit" : "sep"; span.textContent = ch; el.appendChild(span); } });Mit dem passenden CSS:
.view-counter :global(.digit) { display: inline-block; min-width: 1ch; padding: 0.1em 0.25em; margin: 0 1px; text-align: center; background: #111; color: #fff; border-radius: 3px; box-shadow: inset 0 0 0 1px #333;}.view-counter :global(.sep) { /* Tausendertrennzeichen */ padding: 0 0.1em; color: #888;}Option B: Bilder für die Ziffern (die Retro-Methode)
Das ist die ursprüngliche Methode der Neunziger: ein Bild pro Ziffer (0.gif … 9.gif) oder ein einzelnes Sprite mit allen zehn Ziffern, und die Zahl wird zusammengesetzt, indem für jede Ziffer das passende Bild platziert wird. Das reproduziert exakte Pixel-Art, die CSS nicht immer nachahmen kann, hat aber Nachteile:
- Jede Ziffer ist ein Bild: Auf hochauflösenden Displays skaliert es nicht scharf, sofern du keine @2x-Versionen vorbereitest.
- Die Zahl ist kein auswählbarer Text mehr; du musst einen
alt/aria-label-Wert setzen, damit die Zugänglichkeit erhalten bleibt. - Es fügt HTTP-Requests und Sprites hinzu, die gepflegt und vorgeladen werden müssen.
Wenn du es aus ästhetischen Gründen trotzdem möchtest, platziert das Rendering ein <img> pro Ziffer:
fetch(`/api/views/${slug}`, { method: "POST" }) .then(r => r.json() as Promise<{ count: number }>) .then(d => { const n = String(d.count); // ohne Trennzeichen, einfacher el.textContent = ""; el.setAttribute("aria-label", `${n} Aufrufe`); for (const ch of n) { const img = document.createElement("img"); img.src = `/counter/${ch}.gif`; // 0.gif … 9.gif in /public/counter/ img.alt = ""; // dekorativ; aria-label trägt den Wert img.className = "digit-img"; el.appendChild(img); } });.view-counter :global(.digit-img) { height: 1.2em; width: auto; image-rendering: pixelated; /* erhält scharfe Pixel-Art-Kanten beim Skalieren */ vertical-align: middle;}Lege deine Dateien 0.gif…9.gif in public/counter/ (Astro) oder static/counter/ (SvelteKit), damit sie als statische Assets ausgeliefert werden.
Optionale Variante: Reloads desselben Besuchers nicht zählen
Das Basisdesign zählt jede Impression, einschließlich Reloads. Um näher an „Aufrufe“ heranzukommen, ohne den Nutzer zu tracken, markierst du in localStorage, dass dieser Browser diesen Artikel bereits gezählt hat. Das ist rein lokale Deduplizierung: keine Cookies, keine IDs, keine Daten an Dritte.
Ersetze den fetch-Aufruf durch:
const key = `viewed:${slug}`;if (!localStorage.getItem(key)) { localStorage.setItem(key, "1"); fetch(`/api/views/${slug}`, { method: "POST" }) .then(r => r.json()) .then(d => { /* d.count rendern */ });} else { // Wurde schon gezählt: nur die Summe lesen, ohne zu inkrementieren. fetch(`/api/views/${slug}`) .then(r => r.json()) .then(d => { /* d.count rendern */ });}Grundlegende Härtung
Einige optionale Maßnahmen, geordnet nach tatsächlichem Nutzen für einen Blog:
- Offensichtliche Bots filtern. Da die Zählung durch clientseitiges JavaScript ausgelöst wird, aktivieren die meisten einfachen Crawler sie bereits nicht (sie führen das Script nicht aus). Für solche, die es doch tun, kannst du im Endpoint den
User-Agentprüfen und das Inkrement für bekannte Bot-Muster auslassen. Perfektion ist nicht zu erwarten. - Slug validieren. Akzeptiere nur Slugs, die wirklich existieren, damit niemand mit
POST /api/views/irgendwasMüllzeilen aufbläht. Wenn du eine Liste veröffentlichter Slugs hast, lehne alles ab, was nicht darin steht; oder beschränke das Format mit einem einfachen regulären Ausdruck. - Rate Limiting. Für absichtlichen Missbrauch bietet Cloudflare Rate-Limiting-Regeln am Edge für die Route
/api/views/*, ohne deinen Code anzufassen. - Schreibvorgänge bei Skalierung reduzieren. Falls das Volumen es irgendwann rechtfertigt, dedupliziere pro Client (vorheriger Abschnitt) oder sammle Inkremente in Speicher/KV und schreibe sie in Batches nach D1. Für fast jeden Blog ist das voreilige Optimierung; tue es erst, wenn Messungen zeigen, dass es nötig ist.
Troubleshooting
Die Zahl erscheint als „—“ oder ändert sich nicht.
Öffne die Browser-Konsole und den Netzwerk-Tab. Prüfe, ob POST /api/views/<slug> mit 200 und { "count": N } antwortet. Ein 500 bedeutet meistens, dass das Binding DB den Endpoint nicht erreicht hat.
env.DB is undefined (Astro) oder platform is undefined (SvelteKit).
Das Binding ist nicht verbunden. Prüfe, ob d1_databases mit der richtigen database_id in wrangler.jsonc steht, ob der Name des binding mit dem im Code verwendeten Namen (DB) übereinstimmt, ob du neu deployed hast und —in der Entwicklung— ob du mit dem Cloudflare-Adapter läufst (nicht mit einem dev ohne Plattform-Emulation).
Property 'runtime' does not exist on type 'Locals' (Astro).
Du verwendest das alte Muster Astro.locals.runtime.env, das in Astro 6 entfernt wurde. Importiere env aus cloudflare:workers (siehe Endpoint) und generiere die Typen mit npx wrangler types.
no such table: views.
Du hast das Schema nicht in der Umgebung angewendet, die du gerade triffst. Denke daran, dass --local und --remote verschiedene Datenbanken sind: Führe schema.sql je nach Bedarf in beiden aus.
Es funktioniert lokal, aber nicht in Produktion (oder umgekehrt).
Fast immer ist es dasselbe --local-vs.---remote-Problem. Die lokale Datenbank von wrangler dev ist nicht die Datenbank, die dein deployed Worker verwendet.
Der Zähler steigt in Zweierschritten.
In der Entwicklung können React Strict Mode (falls du ihn in Astro-Islands nutzt) oder ein doppeltes Mounting zwei fetch-Aufrufe auslösen. In Produktion passiert das nicht; wenn es dich in dev stört, nutze die Variante mit localStorage.
Fazit
Den Old-School-Besucherzähler auf Cloudflare nachzubauen ist überraschend direkt: eine D1-Tabelle mit einer Zeile pro Artikel, ein Endpoint mit atomarem UPSERT und ein clientseitiges Script von etwa zehn Zeilen. Es braucht keinen separaten Server, SSR muss nicht nur dafür aktiviert werden, und die Kosten sind in der Praxis null.
Dasselbe Muster funktioniert für Astro und SvelteKit; nur die Art ändert sich, wie das Framework dir das D1-Binding bereitstellt: import { env } from "cloudflare:workers" in Astro 6, platform.env in SvelteKit. Die Datenbanklogik —SQL-Anweisung, atomares Inkrement, Lesen der Summe— ist in beiden Fällen exakt gleich.
Und dem ursprünglichen Geist treu zählt der Zähler Impressionen ehrlich und ohne irgendwen zu tracken: eine einfache Zahl am Ende des Artikels, wie früher.
Anhang: Berechtigungen des Cloudflare-API-Tokens
Die Operationen in dieser Anleitung —D1-Datenbank erstellen und das Schema mit Wrangler anwenden— berühren nur D1. Sie benötigen keine Berechtigungen für Workers, DNS oder andere Teile des Kontos.
Wenn du wrangler login verwendest (interaktiver OAuth-Flow), holt Wrangler die Berechtigungen für dich ein, und du musst kein Token erstellen. Dieser Anhang gilt, wenn du lieber ein explizites API-Token verwendest, zum Beispiel um Wrangler in einer nicht interaktiven Umgebung oder mit eingeschränkten Zugangsdaten auszuführen.
Minimal notwendige Berechtigung
Die Operation ähnelt der in diesem Beitrag über das Deployment von SvelteKit auf Cloudflare beschriebenen.
Für D1 ist die minimale Berechtigung:
| Operation der Anleitung | Befehl | Erforderliche Berechtigung |
|---|---|---|
| Datenbank erstellen | wrangler d1 create views | Account → D1 → Edit |
| Schema anwenden (remote) | wrangler d1 execute views --remote --file=./schema.sql | Account → D1 → Edit |
| Zeilen vorladen/abfragen (remote) | wrangler d1 execute ... --command "..." | Account → D1 → Edit |
Das reicht. Edit umfasst Lesen und Schreiben in D1; du brauchst keine separate Leseberechtigung. --local-Operationen berühren die Cloudflare-API nicht (sie laufen gegen eine lokale Kopie auf der Festplatte) und verbrauchen daher keine Token-Berechtigungen.

Token erstellen
- Gehe zu My Profile → API Tokens und wähle Create Token → Create Custom Token.
- Füge unter Permissions eine Zeile hinzu:
Account·D1·Edit. - Wähle unter Account Resources Include → dein Konto (nicht „All accounts“).
- Optional, aber empfohlen: Setze eine TTL (Ablaufdatum) und beschränke das Token, falls dein Netzwerk fest ist, über Client IP Address Filtering.
- Erstelle das Token und kopiere es genau einmal; Cloudflare zeigt es nicht erneut an.
Token mit Wrangler verwenden
Wrangler liest das Token aus der Umgebungsvariable CLOUDFLARE_API_TOKEN. Für D1-Befehle ist es außerdem sinnvoll, CLOUDFLARE_ACCOUNT_ID zu setzen (sichtbar im Dashboard deines Kontos):
export CLOUDFLARE_API_TOKEN="dein-auf-D1-Edit-beschraenktes-token"export CLOUDFLARE_ACCOUNT_ID="deine-account-id"
npx wrangler d1 create viewsnpx wrangler d1 execute views --remote --file=./schema.sqlReferenzen
- Cloudflare. (2026). D1: Cloudflare’s native serverless SQL database. Cloudflare Docs. https://developers.cloudflare.com/d1/
- Cloudflare. (2026). D1 Workers Binding API (prepare, bind, run, first). Cloudflare Docs. https://developers.cloudflare.com/d1/worker-api/
- Cloudflare. (2026). Wrangler commands: d1 create / d1 execute. Cloudflare Docs. https://developers.cloudflare.com/workers/wrangler/commands/#d1
- Cloudflare. (2026). Create API token and token permissions. Cloudflare Docs. https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
- Cloudflare. (2026). Pricing: Workers & D1 free tier limits. Cloudflare Docs. https://developers.cloudflare.com/d1/platform/pricing/
- Astro. (2026). Cloudflare adapter (@astrojs/cloudflare): accessing the runtime. Astro Docs. https://docs.astro.build/en/guides/integrations-guide/cloudflare/
- Astro. (2026). Endpoints and on-demand rendering (prerender). Astro Docs. https://docs.astro.build/en/guides/endpoints/
- SvelteKit. (2026). adapter-cloudflare and platform.env bindings. Svelte Docs. https://svelte.dev/docs/kit/adapter-cloudflare
- SvelteKit. (2026). Server routes (+server.js). Svelte Docs. https://svelte.dev/docs/kit/routing#server
- SQLite. (2026). UPSERT (ON CONFLICT … DO UPDATE). SQLite Documentation. https://www.sqlite.org/lang_upsert.html
