Website mit SSR
Deine Website ist eine Svelte-5-App mit Server-Side-Rendering.
Der tibi-server rendert sie pro URL vor und legt das HTML in der Sammlung ssr ab.
Container: Seiten-Routen, Daten-Lookups, Blöcke, SSR-Bundle, Cache.
Ordnerfrontend/src/
- App.svelte
- ssr.ts
Ordnerblocks/
- …
- frontend/spa.html
- api/templates/spa.html
- api/collections/ssr.yml
Ordnerapi/hooks/
- lib/app.server.js
- lib/utils.js
- ssr/get_read.js
- ssr/post_bind.js
- clear_cache.js
| URL | Was passiert |
|---|---|
/ |
Leitet auf die Browser-Sprache um (/de/ oder /en/) |
/:lang/* |
z. B. /de/, /de/ueber-uns — Sprache plus Seitenpfad |
| Unbekannter Pfad | NotFound.svelte (404) |
content.pathsteht ohne Sprachpräfix in der Datenbank (/, nicht/de/).- Pfad-Regel:
^/[a-z0-9-/]*$— klein, keine Umlaute. translationKey+langundpath+langsind je eindeutig. Setze immertranslationKey, sonst kollidiert die zweite Seite aufnull/de.- Sprache steckt im Pfad (
extractLanguageFromPath), Titel und Meta kommen auscontentEntry.meta.
Daten laden und Lookups
Abschnitt betitelt „Daten laden und Lookups“App.svelte lädt pro Seite drei Dinge über getCachedEntries:
const CONTENT_MEDIA_LOOKUP = ["blocks.heroImage.image:medialib", "blocks.image:medialib"].join(",")const NAVIGATION_CONTENT_LOOKUP = "elements.page:content"
// Inhalt: genau ein Treffer (limit 1), nur aktiv, mit Medien-Lookup und Kommentar-ZählergetCachedEntries("content", { filter: { lang, path: routePath, active: true }, limit: 1, sort: "sort", aggregate: { collection: "comment", foreignField: "contentId", op: "count", filter: { active: true }, as: "commentCount" }, lookup: CONTENT_MEDIA_LOOKUP,})// Navigation: je ein Eintrag header/footer mit aufgelösten SeitenpfadengetCachedEntries("navigation", { filter: { type: "header", language: lang }, lookup: NAVIGATION_CONTENT_LOOKUP, ...})| Lookup | Wirkung |
|---|---|
blocks.heroImage.image:medialib, blocks.image:medialib |
Bild-IDs werden zu _lookup-Objekten mit file.src |
elements.page:content |
Navigations-Elemente bekommen den Zielpfad (_lookup.page.path) |
| Kommentar-Aggregat | commentCount ohne zweite Abfrage |
sort: "sort" sortiert nach dem Feld sort (manuelle Reihenfolge, aufsteigend) — der Parametername und der Feldname heißen hier zufällig gleich.
BlockRenderer.svelte mappt content.blocks auf Komponenten:
type |
Komponente | Hinweis |
|---|---|---|
hero |
HeroBlock.svelte |
Ohne Bild: Verlauf (Gradient-Fallback) |
features |
FeaturesBlock.svelte |
Icons über FeatureIcon.svelte |
richtext |
RichtextBlock.svelte |
Bilder nur bei aufgelöstem _lookup.image.file.src |
accordion |
AccordionBlock.svelte |
Aufklapp-Sektionen |
contact-form |
ContactFormBlock.svelte |
Formular aus widgets/ (Form, Input, Select, Button) |
hide: trueblendet einen Block aus.- Unbekannte Typen werden still übersprungen — nur auf
localhosterscheint ein gelber Hinweis. - Bilder laufen ausschließlich über
MedialibImage.svelte(file.srcoder Data-URI, kein?filterauf Data-URIs).
Eigener Block
Abschnitt betitelt „Eigener Block“- Feld in
api/collections/content.ymlunterblocksergänzen:typeplus Felder mitdependsOn: $parent.type— so zeigt der Editor nur passende Felder. - Komponente in
frontend/src/blocks/anlegen, z. B.QuoteBlock.svelte:- Props per Runen:
let { block } = $props()—blocknie destrukturieren. - Abgeleitete Werte mit
$derived, Zustand mit$state. - SSR-sicher schreiben: jeder
window-Zugriff braucht einen Guard (typeof window !== "undefined"). - Texte über
svelte-i18n($_), Styling Tailwind v4 (bg-linear-to-*,aspect-video, wichtiges!hinten:bg-brand-600!).
- Props per Runen:
- Fall in
BlockRenderer.svelteergänzen:BlockRenderer.svelte — neuen Fall einhängen {:else if block.type === "quote"}<QuoteBlock {block} /> - Seite mit dem neuen Block anlegen und anonym prüfen (nächster Abschnitt).
SSR-Bundle
Abschnitt betitelt „SSR-Bundle“Der Server ruft app.default.render({ url }) auf. Den Wrapper liefert frontend/src/ssr.ts (Svelte-5-render aus svelte/server, i18n-Nachrichten synchron geladen, Sprache aus der URL).
frontend/src/ssr.ts ── esbuild (esbuild.config.server.js) ──▶ api/hooks/lib/app.server.js Svelte 5, generate: "server" gebaut, nie von Hand anfassen| Befehl | Wirkung |
|---|---|
yarn build |
Frontend-Bundle (frontend/dist) |
yarn build:server |
SSR-Bundle nach api/hooks/lib/app.server.js (Ziel esnext, goja-kompatibel) |
Beide Builds schreiben buildInfo — der Server-Build überschreibt sie nie, sonst löst checkBuildVersion einen Schein-Reload aus.
Nach jedem Frontend-Build: yarn build:server erneut laufen lassen und deployen (siehe Veröffentlichen).
Vorlage und Cache-Sammlung
Abschnitt betitelt „Vorlage und Cache-Sammlung“Quelldatei der Vorlage ist frontend/spa.html. ci-modify-config.sh ersetzt __TIMESTAMP__ durch einen Build-Stempel und kopiert die Datei nach api/templates/spa.html.
get_read.js liest sie pro Anfrage und füllt die Marker.
| Marker | Inhalt |
|---|---|
<!--HEAD--> |
Head aus dem SSR-Render |
<!--HTML--> |
Gerendertes App-HTML |
<!--PRELOAD--> |
Preload-Hinweise (scripts/preload-meta.sh) |
__TIMESTAMP__ |
Build-Stempel für /dist-Assets (Cache-Buster, schon beim Kopieren ersetzt) |
<!--SSR.ERROR--> |
Render-Fehler als Kommentar (leer im Normalfall) |
<!--SSR.COMMENT--> |
Diagnose (url, deps) als Kommentar |
<html lang> |
Sprache aus dem URL-Präfix (de/en, sonst de) |
Die Sammlung ssr (api/collections/ssr.yml) hält einen Eintrag je URL:
| Feld | Rolle |
|---|---|
path |
URL ohne Query, eindeutig (Unique-Index — parallele Writes gewinnt genau einer) |
content |
Fertiges HTML aus der Vorlage |
validUntil |
Ablaufdatum — der Hook setzt es derzeit auf null, Einträge leben bis zur Invalidierung |
dependencies |
col:<id> (Detail) oder col:* (Liste), indexiert |
_testdata |
Test-Marker, im Editor verborgen |
Schreiben und Löschen steuert clear_cache.js als return-Hook (post, put, delete u. a. in content, navigation, medialib, comment, tag) über utils.clearSSRCache(); POST /api/ssr?clear=1 wischt alles (post_bind.js).
Cache und Invalidierung
Abschnitt betitelt „Cache und Invalidierung“api/hooks/ssr/get_read.js rendert jede anonyme Anfrage — außer Tracking (x-ssr-skip) und Admins:
- URL normieren (Query ab, Schrägstrich ab,
/index→/), perssrValidatePathprüfen (404 oder kein Rendering). - Cache in der Sammlung
ssrsuchen (pathist eindeutig). Treffer mitX-SSR-Cache: trueausliefern; abgelaufene (validUntil) löschen. - Neu rendern,
window.__SSR_CACHE__mit den gelesenen Einträgen einbetten, Abhängigkeiten alscol:id(Detail) odercol:*(Liste) speichern. Gibt escol:*, fallen diecol:idderselben Sammlung weg. - Parallel geschriebene Duplikate (
duplicate keyaufpath) werden geschluckt — der Eintrag existiert dann schon.
Invalidierung übernimmt api/hooks/lib/utils.js (clearSSRCache), verdrahtet über clear_cache.js als return-Hook (u. a. content, navigation, medialib, comment, tag):
| Methode | Was gelöscht wird |
|---|---|
POST (neuer Eintrag) |
Nur Seiten mit col:* (Listen) |
PUT / DELETE |
Seiten mit col:<id> oder col:* |
POST /api/ssr?clear=1 |
Alles (manueller Komplett-Wisch) |
Jede andere POST-Anfrage an ssr antwortet 500 (ssr is only a dummy collection).
Antwort-Köpfe X-SSR-Cleared und X-SSR-Cleared-Collection sagen, was flog.
Abnahme
Abschnitt betitelt „Abnahme“curl -s http://localhost:8080/de/ | grep -o 'window.__SSR_CACHE__ = .*' | head -c 600# Erwartung: navigation count > 0, heroImage/image mit _lookupcurl -sI http://localhost:8080/de/ | grep -i x-ssr-cache# Zweiter Aufruf: X-SSR-Cache: true# PUT/DELETE auf einen Eintrag → betroffene Seiten neu gerendert (kein X-SSR-Cache: true beim nächsten Aufruf)# POST (neu) → nur Listen-Seiten neu# Zweifel? POST /api/ssr?clear=1 wischt allescurl -s "http://localhost:8080/de/?noCache=1" | head -c 300| Fehlerbild | Ursache | Lösung |
|---|---|---|
| Öffentlich leer, mit Token voll | active:true fehlt (auch auf Navigation/Medien) |
Flag setzen, anonym erneut prüfen |
X-SSR-Cache: true trotz neuer Daten |
Abhängigkeit fehlt (z. B. Liste ohne col:*) |
?clear=1 oder ?noCache=1, Hook-Verdrahtung prüfen |
500 ssr is only a dummy collection |
POST an ssr ohne ?clear=1 |
Query-Parameter ergänzen |
| PAT reicht, anonym fehlt Lookup | lookup permission denied — Ziel-Sammlung ohne Leserecht |
Rechte und Tokens |
Verifiziert am e3f92eb 2026-10-17 (tibi-svelte-starter)