AssetQueue - Tutorial
AssetQueue — Tutorial
Dieses Tutorial zeigt, wie CSS- und JavaScript-Dateien in WBCE über das Queue-System eingebunden werden, und was jeder Aufruf konkret in den HTML-Output schreibt.
Warum ein Queue-System?
Das naive Vorgehen in Templates und Modulen:
<link rel="stylesheet" href="<?=TEMPLATE_DIR; ?>/styles.css">
<script src="<?=TEMPLATE_DIR; ?>/mymod/frontend.js"></script>
Das funktioniert — hat aber mehrere Probleme:
1. Doppelte Einbindungen. Wenn zwei Module dasselbe jQuery einbinden, landet es zweimal im HTML. Das Queue-System erkennt gleiche Dateien und überspringt den zweiten Eintrag automatisch.
2. Unkontrollierte Reihenfolge. Ein echo landet genau dort im Output, wo es im PHP-Code steht — oft mitten im Body, manchmal zu früh, manchmal zu spät für Dependencies.
3. Kein Bundling. Zehn <link>-Tags bedeuten zehn HTTP-Requests. Besonders auf HTTP/1.1 macht das einen messbaren Unterschied. Auch auf HTTP/2 lohnt sich Bundling für oft gecachte Bundles (ein Cache-Miss statt zehn).
4. Kein automatisches Cache-Busting. Nach einem Update läuft der Browser-Cache weiter mit der alten Datei, bis der Benutzer manuell löscht — oder bis ?v=2 händisch ergänzt wird.
Das Queue-System löst alle vier Probleme: Aufrufe werden gesammelt, dedupliziert, an die richtige Position im Dokument injiziert, optional gebündelt und minifiziert, und automatisch mit Cache-Busting-Parametern versehen.
insertCssFile() / insertJsFile()
Die Grundfunktionen. In PHP oder im Bootstrap eines Templates:
insertCssFile('{TEMPLATE}/styles.css');
insertJsFile('{MODULES}/mymod/frontend.js');
Was im <head> landet:
<link rel="stylesheet" href="/wbce/templates/mytheme/styles.css?1720000000">
Was vor </body> landet:
<script src="/wbce/modules/mymod/frontend.js?1720000000"></script>
CSS landet standardmäßig in head_late, JS in body_late.
Mit Position
insertCssFile('{TEMPLATE}/critical.css', 'head_early');
insertJsFile('{TEMPLATE}/init.js', 'head_late');
Mit HTML-Attributen
// Nur für Druck
insertCssFile('{TEMPLATE}/print.css', 'head_late', ['media' => 'print']);
// Defer — lädt parallel, führt nach dem Parsen aus
insertJsFile('{MODULES}/mymod/heavy.js', 'body_late', ['defer' => true]);
// Async — lädt und führt sofort aus, sobald verfügbar
insertJsFile('{MODULES}/mymod/tracker.js', 'body_late', ['async' => true]);
// ES-Modul
insertJsFile('{TEMPLATE}/app.js', 'body_late', ['type' => 'module']);
HTML-Output für die Beispiele:
<link rel="stylesheet" href="…/print.css?…" media="print">
<script src="…/heavy.js?…" defer></script>
<script src="…/tracker.js?…" async></script>
<script src="…/app.js?…" type="module"></script>
Mit ID (Dedup-Schlüssel)
insertJsFile(WB_URL . '/include/jquery/jquery-min.js', 'head_early', [], 'jquery');
Eine ID verhindert Doppeleinbindungen auch wenn dieselbe Datei unter verschiedenen URLs angefordert wird. Wer jQuery später nochmals einbindet (gleiche ID), wird ignoriert.
Token-System
Statt absoluter URLs werden Token-Platzhalter verwendet, die zur Laufzeit aufgelöst werden:
Token | Löst auf |
|---|---|
| URL des aktiven Templates |
|
|
| wie |
|
|
| Root-URL der WBCE-Installation |
| URL des Admin-Bereichs |
| URL des Media-Verzeichnisses |
Vorteil: Kein hartcodiertes WB_URL . '/modules/...', keine TEMPLATE_DIR-Verkettungen. Installationsunabhängig und testbar.
Eigene Tokens registrieren
I::addUrlToken('{MYMOD}', WB_URL . '/modules/my_module');
insertCssFile('{MYMOD}/assets/backend.css');
insertJsFile('{MYMOD}/assets/backend.js');
Sinnvoll in index.php oder initialize_fe.php eines Moduls — einmal registrieren, überall verwenden.
insertCssCode() / insertJsCode()
Inline-Code direkt in den Head oder Body schreiben — ohne <style>- oder <script>-Wrapper, den fügt die Queue selbst hinzu.
insertCssCode(':root { --primary: #3a7bd5; --gap: 1.5rem; }');
insertJsCode('window.SITE_LANG = "' . LANGUAGE . '";');
HTML-Output:
<style>
:root { --primary: #3a7bd5; --gap: 1.5rem; }
</style>
<script>
window.SITE_LANG = "DE";
</script>
Mit Position — CSS-Variablen möglichst früh, damit sie beim Parsen schon da sind:
insertCssCode(':root { --primary: #3a7bd5 }', 'head_early');
Mit ID — verhindert Doppelausgabe wenn dasselbe Code-Snippet von mehreren Stellen eingefügt werden könnte:
insertCssCode('.sr-only { position: absolute; … }', 'head_late', 'sr-only-helper');
insertHtmlCode()
Beliebige HTML-Blöcke an einer Queue-Position einschleusen.
// Preconnect-Hint
insertHtmlCode('<link rel="preconnect" href="https://api.example.com">', 'head_early');
// Structured Data (JSON-LD)
insertHtmlCode('<script type="application/ld+json">' . json_encode($schema) . '</script>', 'head_late');
// Kein-JS-Fallback
insertHtmlCode('<noscript><style>.js-only { display:none }</style></noscript>', 'head_late');
HTML-Output (Beispiel Structured Data):
<script type="application/ld+json">{"@context":"https://schema.org","@type":"Article",...}</script>
loadPlugin()
Ein Plugin-Verzeichnis mit einer einzigen Zeile einbinden. Die plugin.json im Verzeichnis deklariert, welche Dateien geladen werden sollen und ob es Dependencies gibt.
loadPlugin('include/wbeSelect');
include/wbeSelect/plugin.json:
{
"css": ["wbeSelect.css"],
"js": ["wbeSelect.js"]
}
HTML-Output:
<link rel="stylesheet" href="/wbce/include/wbeSelect/wbeSelect.css?1720000000">
<script src="/wbce/include/wbeSelect/wbeSelect.js?1720000000"></script>
Mit Dependencies
{
"css": ["datepicker.css"],
"js": ["datepicker.js"],
"require": ["include/jquery-slim"]
}
require-Einträge werden zuerst geladen (depth-first), Deduplication greift auch hier — wird include/jquery-slim von zwei Plugins angefordert, landet es nur einmal im HTML.
JS-Dateien mit expliziter Position
Wenn ein Plugin sowohl ein Polyfill (muss in den Head) als auch den Hauptcode (Body) mitbringt:
{
"css": ["wbeSelect.css"],
"js": {
"head_early": ["wbeSelect-polyfill.js"],
"body_late": ["wbeSelect.js"]
}
}
Position überschreiben
// CSS in head_early statt head_late
loadPlugin('include/wbeSelect', 'head_early');
// CSS head_late, JS in head_early
loadPlugin('include/wbeSelect', 'head_late', 'head_early');
insertCssBundle() / insertJsBundle()
Mehrere Dateien zu einer einzigen gecachten Datei zusammenfassen — ein HTTP-Request statt vieler.
I::insertCssBundle([
'{TEMPLATE}/styles.css',
'{TEMPLATE}/cookie-consent.css',
'{MODULES}/ckeditor/frontend.css',
'{MODULES}/mod_multilingual/frontend.css',
], 'dolce-piano-main');
Was im <head> landet:
<link rel="stylesheet" href="/wbce/cache/assets/combined_dolce-piano-main.css?1720000000">
Statt vier Requests: einer. Die kombinierte Datei wird beim ersten Aufruf gebaut und danach aus dem Cache serviert. Ändert sich eine Quelldatei, wird der Cache automatisch invalidiert (via mtime-Vergleich).
Warum auch Modul-CSS bundeln?
Module wie ckeditor oder mod_multilingual legen ihre frontend.css im Modulordner ab und registrieren sie über register_frontend_modfiles('css'). Das läuft durch die Queue, aber als einzelne Datei. Wer diese Dateien ins Bundle aufnimmt, hat sie unter Kontrolle — Minifizierung, Reihenfolge und Bundling inklusive.
Wichtig: Reihenfolge
Das Bundle muss vor register_frontend_modfiles('css') registriert werden. Die Queue erkennt bereits gebündelte Dateien und überspringt sie bei der Einzel-Registrierung — aber nur, wenn das Bundle zuerst da ist.
// Richtig ?
I::insertCssBundle([
'{TEMPLATE}/styles.css',
'{MODULES}/ckeditor/frontend.css',
], 'mein-bundle');
register_frontend_modfiles('css'); // ckeditor wird übersprungen — schon im Bundle
// Falsch ?
register_frontend_modfiles('css'); // ckeditor landet einzeln in der Queue
I::insertCssBundle([
'{TEMPLATE}/styles.css',
'{MODULES}/ckeditor/frontend.css', // zu spät — ckeditor ist schon drin
], 'mein-bundle');
// Ergebnis: ckeditor erscheint doppelt
JS-Bundle
I::insertJsBundle([
'{TEMPLATE}/vendor/alpine.js',
'{TEMPLATE}/js/main.js',
'{TEMPLATE}/js/cookieconsent.js',
], 'dolce-piano-scripts');
Was vor </body> landet:
<script src="/wbce/cache/assets/combined_dolce-piano-scripts.js?1720000000"></script>
Mit Position
// Bundle explizit in den Head (z.B. für kritisches JS)
I::insertJsBundle(['{TEMPLATE}/js/critical.js'], 'critical', 'head_early');
Remote-Dateien im Bundle
CDN-URLs werden automatisch herausgefiltert und einzeln nach dem Bundle-Tag geladen:
I::insertCssBundle([
'{TEMPLATE}/styles.css',
'https://cdn.example.com/lib.css', // wird einzeln geladen, nicht gebündelt
], 'mein-bundle');
HTML-Output:
<link rel="stylesheet" href="/wbce/cache/assets/combined_mein-bundle.css?…">
<link rel="stylesheet" href="https://cdn.example.com/lib.css">
insertWebFont() / insertFont()
Web-Fonts vom CDN werden lokal gecacht und DSGVO-konform eingebunden.
// Google Fonts, Bunny Fonts, Fontshare, … — ein Aufruf
insertWebFont('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
// Lokale .woff2-Datei
insertFont('{TEMPLATE}/fonts/MyFont.woff2', ['family' => 'MyFont', 'weight' => '400']);
? Ausführliches Tutorial: FontCache_TUTORIAL.md
Twig-Funktionen
Im Twig-Template stehen folgende Funktionen zur Verfügung:
Twig-Aufruf | Entspricht PHP |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Nicht verfügbar in Twig:
Funktion | Grund |
|---|---|
| Muss vor dem Template-Render registriert sein (Reihenfolge-Garantie) |
| Wie |
| Löst I/O aus — gehört ins Bootstrap, nicht in die View-Schicht |
| Wie |
| Infrastruktur-Setup, gehört ins Bootstrap |
Bundles und Fonts immer in initialize_fe.php oder der Template-index.php registrieren, bevor Twig rendert.
Referenz
Positions-System
Jeder Aufruf akzeptiert eine optionale Position. Die Canonical-Namen:
Position | Anchor im Dokument | Reihenfolge | Typischer Inhalt |
|---|---|---|---|
| Direkt nach | 1 | Charset, Viewport — absolut erstes im Head |
| Direkt nach | 2 | Kritisches CSS, CSS-Variablen, Preloads |
| Kurz vor | 3 | Meta-Tags, OG-Tags (vor head_late) |
| Kurz vor | 4 | Modul-CSS, Plugin-CSS (Standard für CSS) |
| Kurz vor | 5 | Override-CSS das immer nach allem kommen soll |
| Direkt nach | 6 | Absolut erstes im Body (vor body_early) |
| Direkt nach | 7 | Feature-Detection, Early-Init-JS |
| Kurz vor | 8 | Modul-JS, Plugin-JS (Standard für JS) |
| Kurz vor | 9 | Analytics, Tracking — immer letztes |
head_middle, head_late und head_last teilen denselben Anchor (</head>). body_top und body_early teilen denselben Anchor (nach <body>). body_late und body_last teilen denselben Anchor (</body>). Die Reihenfolge innerhalb eines gemeinsamen Anchors wird durch Reverse-Insertion garantiert.
Shorthands (werden ebenfalls akzeptiert):
Shorthand | Löst auf |
|---|---|
|
|
|
|
|
|
|
|
|
|
Legacy-Aliases (aus WBCE 1.x) werden weiterhin akzeptiert:
Legacy | Canonical |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Das +/--Suffix aus WBCE 1.x war ein Hinweis auf "früher" vs. "später" innerhalb einer Position — diese Feinsteuerung übernehmen jetzt die getrennten Canonical-Namen (head_early / head_late / head_last usw.).
Theorie: Was passiert beim Bundling?
insertCssBundle(['a.css', 'b.css'], 'mein-bundle')wird aufgerufenDie Queue löst Token auf und bestimmt die absoluten Pfade der Quelldateien
Sie prüft, ob
cache/assets/combined_mein-bundle.cssexistiert und ob alle Quelldateien unverändert sind (mtime-Vergleich)Falls veraltet oder fehlend: Dateien werden geladen, optional minifiziert (via
MatthiasMullie\Minifywenn vorhanden), zusammengefasst, und atomar incache/assets/geschriebenDie Quelldateien werden in
$seenPathseingetragen — spätereinsertCssFile()- Aufrufe für dieselben Dateien werden lautlos ignoriertDie Bundle-URL wird in die Queue eingetragen und beim Render als einzelner
<link>-Tag ausgegeben
Minifizierung ohne Bundle: Auch einzelne insertCssFile()-Aufrufe profitieren von Minifizierung, wenn MINIFY_ASSETS aktiv ist. Die minifizierte Version landet in cache/assets/ mit einem lesbaren Namen (z.B. wbcetik-css-main.min.css).
Theorie: Cache Busting
Wenn ASSET_CACHE_BUSTING aktiv ist, hängt die Queue automatisch ?{mtime} an jede lokale Datei-URL:
<link rel="stylesheet" href="/wbce/templates/mytheme/styles.css?1720000000">
Der Timestamp ist die filemtime() der Datei auf dem Server. Nach einem Deployment ändert sich der Timestamp — Browser und CDN verwerfen den Cache automatisch. Kein händisches ?v=2 mehr nötig.
Externe URLs (CDN) erhalten kein ?mtime — dort hat der Server keinen Dateizugriff.
Theorie: Deduplication
Die Queue führt zwei Lookup-Tabellen:
$seen— Schnell-Check auf exakt denselben URL-String (Token-Schreibweise)$seenPaths— Vergleich des aufgelösten absoluten Dateipfads
Der Path-Check fängt Fälle wie diese ab:
insertCssFile('{MODULES}/ckeditor/frontend.css');
// … woanders im Code:
insertCssFile(WB_URL . '/modules/ckeditor/frontend.css'); // ? selbe Datei, andere URL
Beide zeigen auf denselben absoluten Pfad — der zweite Eintrag wird übersprungen. Das gilt auch für Bundle-Quellen: Wer eine Datei ins Bundle aufnimmt, kann sie danach bedenkenlos nochmals als Einzeldatei registrieren — sie erscheint trotzdem nur einmal im HTML.