Diil Docs
  1. Doku
  2. Anleitungen

Caching, ETag und 304: schnell und immer aktuell

Aktualisiert:

Content-APIs zwingen dich meistens zur Wahl: schnell oder aktuell. Wir lassen das lieber. Responses werden bei uns gecacht und veralten dort trotzdem nie, jede Response trägt ein ETag, sodass unveränderter Content dich so gut wie nichts kostet, und eine Änderung im CRM ist in dem Moment in der API, in dem die Redaktion auf „Speichern“ klickt. Diese Seite erklärt, wie das funktioniert und wie du bei dir das Beste daraus machst.

Von der Änderung im CRM bis auf deine Seite

Hier die komplette Reise einer Änderung, von der Tastatur der Redaktion bis auf den Bildschirm deiner Besucher:

  1. Die Redaktion speichert

    Jemand korrigiert einen Tippfehler im CRM oder direkt auf der Live-Website. Jede Änderung zählt — ein Text, ein Bild, eine Seiteneinstellung, ein Blog-Post.
  2. Die Content-Version der Website steigt

    Jede Änderung erhöht die Content-Version der Website. Unser serverseitiger Cache (Redis) hängt an dieser Version, also werden alle alten gecachten Antworten auf einen Schlag irrelevant. Niemand muss „den Cache leeren“.
  3. Der nächste API-Request bekommt frische Daten

    Schon der allernächste Request an die API baut seine Response aus dem neuen Content. Auf unserer Seite gibt es null Verzögerung: keine TTL zum Aussitzen, keine Purge-Queue.
  4. Caches zwischen uns und dem Besucher ziehen nach

    Was ein Besucher tatsächlich sieht, kann ein wenig hinterherhinken: Browser oder CDN dürfen die vorherige Response bis zu 60 Sekunden behalten und sie noch einmal ausliefern, während im Hintergrund leise die neue geholt wird (das ist stale-while-revalidate). Dein eigener Server-Cache, falls du einen hast, legt seine Lebensdauer noch obendrauf.

Cache-Header erklärt

Eine typische erfolgreiche Response kommt mit diesen Headern:

200 OK — Response-Headerhttp
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding
Access-Control-Allow-Origin: *
HeaderWas er Caches sagt
Cache-Control: publicJeder darf die Response speichern: Browser, CDN, Proxy. Der Content ist sowieso öffentlich — es ist genau das, was du auf deiner Website zeigst.
max-age=6060 Sekunden lang gilt die Response als frisch und kann wiederverwendet werden, ohne uns überhaupt zu fragen.
stale-while-revalidate=600Die nächsten 10 Minuten darf ein Cache die alte Kopie sofort ausliefern und im Hintergrund eine neue holen. Schnell für diesen Besucher, frisch für den nächsten.
ETag: W/"…"Ein schwaches ETag — ein Hash des Response-Bodys. Gleicher Body, gleiches ETag. Schick es in If-None-Match zurück, und du bekommst 304, wenn sich nichts geändert hat.
Vary: x-crm-key, Accept-EncodingCaches müssen getrennte Kopien pro Site-Key und pro Komprimierung halten. Zwei Websites teilen sich nie eine gecachte Antwort, selbst bei derselben URL.
Cache-Control: no-storeKommt bei jedem Fehler mit. Ein 404 für eine Seite, die die Redaktion gerade anlegt, soll in niemandes Cache hängen bleiben.

CORS ist für jeden Origin offen, die API erlaubt ausdrücklich den Request-Header If-None-Match und gibt ETag für JavaScript frei — alles auf dieser Seite funktioniert also auch im Browser.

ETag und 304 Not Modified

Das ETag ist der günstigste Weg zu fragen: „Hat sich was geändert?“. Merk dir das ETag aus der letzten Response, schick es beim nächsten Mal in If-None-Match mit, und wenn der Content gleich ist, bekommst du 304 Not Modified mit leerem Body. Dein Code nutzt einfach weiter die Kopie, die er schon hat.

Erster Request: die volle Antworthttp
GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40

HTTP/1.1 200 OK
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"
Vary: x-crm-key, Accept-Encoding

{ "id": 26, "marker": "home", "name": "Home", "content": { … } }
Nächster Request: nichts hat sich geänderthttp
GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
If-None-Match: W/"a41f9c0e7b2d58f3"

HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, stale-while-revalidate=600
ETag: W/"a41f9c0e7b2d58f3"

Sobald die Redaktion auf dieser Seite etwas ändert, ändert sich der Body, der Hash ändert sich, und derselbe Request liefert ein frisches 200 mit neuem ETag.

Caching-Rezepte

Im Browser: schon erledigt

Wenn du Content direkt im Browser holst, brauchst du keine einzige Zeile Caching-Code. Ein schlichtes fetch nutzt den HTTP-Cache des Browsers: 60 Sekunden lang verwendet es die Response wieder, danach revalidiert es ganz von selbst mit If-None-Match.

Browser — nichts zu konfigurierenjs
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
  headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
  // cache: 'default' ist der Standard — max-age und ETag erledigt der Browser für dich
});
const page = await res.json();

Wundere dich nicht, dass dein Code nie ein 304 zu sehen bekommt: Der Browser tauscht es gegen das gecachte 200 aus, bevor es bei dir ankommt. Öffne den Network-Tab in den DevTools, um zu sehen, was wirklich passiert ist — dort findest du das 304 oder „disk cache“.

Auf einem Node-Server: ein winziger ETag-Cache

Serverseitiges fetch in Node hat keinen eigenen HTTP-Cache, jeder Aufruf geht also den ganzen Weg bis zur API. Eine kleine Map löst das: Kopie 60 Sekunden lang wiederverwenden (genau wie max-age), dann mit If-None-Match nachfragen und den Body nur laden, wenn er sich geändert hat.

lib/content.ts — Express, Fastify, Nuxt, Remix, alles auf Nodets
const API = 'https://back.sitecog.com/content';
const TTL = 60_000; // wie max-age=60

type Entry = { etag: string | null; data: unknown; at: number };
// Ein Prozess, ein Site-Key. Mehrere Keys? Dann gehört der Key mit in den Cache-Key.
const cache = new Map<string, Entry>();

export async function getContent<T>(path: string): Promise<T> {
  const url = API + path;
  const cached = cache.get(url);

  // 1. Frisch genug — die API gar nicht erst aufrufen
  if (cached && Date.now() - cached.at < TTL) return cached.data as T;

  // 2. Mit dem ETag, das wir schon haben, fragen: "Hat sich was geändert?"
  const headers: Record<string, string> = { 'x-crm-key': process.env.CRM_KEY! };
  if (cached?.etag) headers['if-none-match'] = cached.etag;

  const res = await fetch(url, { headers });

  if (res.status === 304 && cached) {
    cached.at = Date.now(); // gleicher Content, noch mal 60 Sekunden Ruhe
    return cached.data as T;
  }
  if (!res.ok) throw new Error(`Content API ${res.status} for ${path}`);

  const data = (await res.json()) as T;
  cache.set(url, { etag: res.headers.get('etag'), data, at: Date.now() });
  return data;
}

// Verwendung
const home = await getContent('/v1/pages/home?lang=en');

Next.js: revalidate und Refresh auf Abruf

Im App Router erledigt der fetch-Cache die Arbeit für dich. revalidate: 60 passt zu unserem max-age: Next.js behält die Response eine Minute lang und aktualisiert sie dann im Hintergrund.

app/page.tsxtsx
export default async function Home() {
  const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
    headers: { 'x-crm-key': process.env.CRM_KEY! },
    next: { revalidate: 60, tags: ['crm-content'] },
  });
  const page = await res.json();

  return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}

Weniger Verzögerung gewünscht? Senk revalidate — aber behalt die Limits im Auge. Du willst einen „Jetzt veröffentlichen“-Button für einen großen Launch? Versieh deine Fetches mit einem Tag und stell eine kleine Route bereit, die den Tag verwirft:

app/api/revalidate/route.tsts
import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  if (req.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
    return new Response('Nope', { status: 401 });
  }
  // Next.js 16 nimmt ein zweites Argument; in Next.js 15 ist es einfach revalidateTag('crm-content')
  revalidateTag('crm-content', { expire: 0 });
  return Response.json({ revalidated: true });
}

Ruf sie auf, wo es zu deinem Team passt: aus einem Deploy-Skript, einem Bookmarklet, einem Chatbot, einem Button in deinem eigenen Admin-Panel. Das Secret gehört nur dir — mit dem Site-Key hat es nichts zu tun.

Live-Modus: jeden Cache umgehen

Wenn die Redaktion deine Website im Live-Modus des CRM öffnet, will sie ihre Änderung direkt nach dem Speichern sehen — nicht eine Minute später. Im CRM-Frame bekommt die URL ?crm_live=1. Bewährte Praxis (und genau das, was unser Referenz-Client macht): Läuft die Seite in einem iframe oder hat sie crm_live, holst du mit cache: 'no-store'. Bei uns ist ohnehin alles aktuell, mehr braucht es also nicht.

const isLive =
  window.self !== window.top ||
  new URLSearchParams(location.search).has('crm_live');

const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
  headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
  cache: isLive ? 'no-store' : 'default',
});

Mehr zum Live-Modus und dem Markup dahinter findest du auf der Seite Widget & Markup.

„Warum sehe ich meine Änderung nicht?“

Die beliebteste Frage zu jedem Cache, ever. Geh diese Liste von oben nach unten durch — sie reicht von „in zehn Sekunden geprüft“ bis „mach dir erst mal einen Tee“.

  1. Frag die API direkt. Ein schlichtes curl hat keinen Cache, und bei uns ist immer alles aktuell. Zeigt curl den neuen Text, ist die API in Ordnung und die alte Kopie steckt irgendwo unterwegs in einem Cache. Zeigt curl den alten Text, arbeite die Liste weiter ab.
  2. Wurde wirklich gespeichert? Prüf es im CRM. Bei Blog-Posts gilt: Entwürfe werden nie ausgeliefert, und ein für die Zukunft geplanter Post bleibt bis zu seinem Datum unsichtbar (und kann ein paar Minuten später auftauchen).
  3. Richtige Website? Ein Key gehört zu genau einer Website. Staging und Produktion mit unterschiedlichen Keys lesen unterschiedlichen Content.
  4. Richtige Sprache? Einen Fallback auf dem Server gibt es nicht. Hat die Redaktion den deutschen Text geändert und du renderst Englisch, passiert sichtbar nichts. Eine leere Übersetzung kommt als "" zurück, und dein Fallback-Code zeigt dann womöglich still eine andere Sprache an. Siehe Sprachen & Fallbacks.
  5. Richtiger Block? Marker unterscheiden Groß- und Kleinschreibung, und Block-Marker sind nur innerhalb einer Section eindeutig — /v1/blocks/title ohne ?section= liefert den ältesten Block mit diesem Marker, und das ist vielleicht nicht der, der geändert wurde.
  6. Versteckte Section? Sections mit show: false werden trotzdem ausgeliefert. Hat die Redaktion eine Section versteckt und sie ist immer noch auf der Website, prüft dein Template show nicht.
  7. Dein eigener Cache. revalidate, ISR, eine In-Memory-Map, ein CDN vor deiner Website, eine Seite, die einmal zur Build-Zeit gebaut wurde. Das ist der übliche Verdächtige.
  8. Der Browser. Bis zu 60 Sekunden plus ein Refresh im Hintergrund. Ein Hard Reload (Ctrl+Shift+R oder Cmd+Shift+R) klärt das.
  9. Bearbeitung im Live-Modus? Stell sicher, dass du im CRM-Frame mit cache: 'no-store' holst (siehe oben).
Sieh nach, was die API gerade liefertbash
curl -i "https://back.sitecog.com/content/v1/pages/home?lang=en" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"