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

{TEMPLATE}

URL des aktiven Templates

{MODULES}

WB_URL/modules

{MODULES_URL}

wie {MODULES}, alternativ

{TEMPLATES_URL}

WB_URL/templates

{WB_URL}

Root-URL der WBCE-Installation

{ADMIN_URL}

URL des Admin-Bereichs

{MEDIA_URL}

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

{{ loadPlugin('include/wbeSelect') }}

loadPlugin(...)

{{ insertCssFile(TEMPLATE_DIR ~ '/x.css') }}

insertCssFile(...)

{{ insertJsFile(INCLUDE_URL ~ '/x.js') }}

insertJsFile(...)

{{ insertCssCode('.foo { color:red }') }}

insertCssCode(...)

{{ insertJsCode('window.x = 1') }}

insertJsCode(...)

{{ insertHtmlCode('<noscript>…</noscript>') }}

insertHtmlCode(...)

{{ insertFile(url, pos, type) }}

insertCssFile oder insertJsFile

{{ insertTitle('Meine Seite') }}

I::insertTitle(...)

{{ insertMeta('description', 'Mein Text') }}

I::insertMeta(...)

Nicht verfügbar in Twig:

Funktion

Grund

insertCssBundle

Muss vor dem Template-Render registriert sein (Reihenfolge-Garantie)

insertJsBundle

Wie insertCssBundle

insertWebFont

Löst I/O aus — gehört ins Bootstrap, nicht in die View-Schicht

insertFont

Wie insertWebFont

I::addUrlToken

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

head_top

Direkt nach <head>

1

Charset, Viewport — absolut erstes im Head

head_early

Direkt nach </title>

2

Kritisches CSS, CSS-Variablen, Preloads

head_middle

Kurz vor </head>

3

Meta-Tags, OG-Tags (vor head_late)

head_late

Kurz vor </head>

4

Modul-CSS, Plugin-CSS (Standard für CSS)

head_last

Kurz vor </head>

5

Override-CSS das immer nach allem kommen soll

body_top

Direkt nach <body>

6

Absolut erstes im Body (vor body_early)

body_early

Direkt nach <body>

7

Feature-Detection, Early-Init-JS

body_late

Kurz vor </body>

8

Modul-JS, Plugin-JS (Standard für JS)

body_last

Kurz vor </body>

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

head

head_late

body

body_late

early

head_early

middle

head_middle

late

head_late (CSS) / body_late (JS)

Legacy-Aliases (aus WBCE 1.x) werden weiterhin akzeptiert:

Legacy

Canonical

HEAD TOP, HEAD TOP+, HEAD TOP-

head_early

HEAD+

head_middle

KEY+, DESC+

head_middle

HEAD BTM, HEAD BTM+, HEAD BTM-

head_late

HEAD-, HEAD

head_late

HEAD MODFILES, CSS HEAD MODFILES

head_late

BODY TOP, BODY TOP+, BODY TOP-

body_early

BODY+

body_early

BODY BTM, BODY BTM+, BODY BTM-

body_late

BODY-, BODY

body_late

BODY MODFILES, JS BODY MODFILES

body_late

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?

  1. insertCssBundle(['a.css', 'b.css'], 'mein-bundle') wird aufgerufen

  2. Die Queue löst Token auf und bestimmt die absoluten Pfade der Quelldateien

  3. Sie prüft, ob cache/assets/combined_mein-bundle.css existiert und ob alle Quelldateien unverändert sind (mtime-Vergleich)

  4. Falls veraltet oder fehlend: Dateien werden geladen, optional minifiziert (via MatthiasMullie\Minify wenn vorhanden), zusammengefasst, und atomar in cache/assets/ geschrieben

  5. Die Quelldateien werden in $seenPaths eingetragen — spätere insertCssFile()- Aufrufe für dieselben Dateien werden lautlos ignoriert

  6. Die 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.