Roadie ist das Frontend-Framework-AddOn für REDAXO. Es liefert ein Komponenten-System (Server-Side-PHP und clientseitiges JS), Asset-Management, Icon-System, Bild- & Video-Komponenten, Media-Pool-Erweiterungen, Backend-Widgets, eine Artikel-Live-Vorschau, SEO/strukturierte Daten (schema.org) inkl. zentraler Unternehmensdaten und Utilities — und baut auf WebAwesome als Web-Component-Bibliothek auf.
Die genutzte WebAwesome-Version ist in src/addons/roadie/package.json deklariert und wird über Yarn Workspaces automatisch bereitgestellt.
- Setup
- Asset Management
- Komponenten-System
- Image-Komponente
- Video-Komponente
- Icon-System
- Section & Layout
- Slice Management
- Navigation
- Media Pool Erweiterungen
- ArticleKey
- ArticleUsage – Löschschutz
- Backend-Widgets
- Live-Vorschau
- Console Commands
- HTML-Helfer
- Filter-System
- Unternehmensdaten
- SEO und schema.org
- Frontend-JS-Komponenten
- Utilities
- REDAXO >= 5.20
- PHP >= 8.4 (die Image-Komponente nutzt Property Hooks)
- Node.js + yarn
- Symfony Webpack Encore (
@symfony/webpack-encore)
WebAwesome (@awesome.me/webawesome) wird nicht separat installiert — es ist als Dependency im Roadie-AddOn deklariert (src/addons/roadie/package.json) und wird über den Yarn-Workspace-Mechanismus automatisch ins Root-node_modules hochgezogen.
Die package.json im Projektstamm definiert:
"workspaces": [
"src/addons/*"
]Das bedeutet: Alle AddOns in src/addons/ werden als Yarn Workspaces behandelt. Ihre dependencies landen gemeinsam im Root-node_modules. So kann das Roadie-AddOn eigene npm-Abhängigkeiten mitbringen, ohne dass diese manuell im Projekt eingetragen werden müssen.
yarn installyarn watch # Entwicklung mit File-Watcher
yarn dev-server # Entwicklung mit Webpack Dev Server (HTTPS)
yarn build # Produktions-Build
yarn generate:icon-manifest # Icon-Manifest neu generieren (nach SVG-Änderungen)
yarn generate:component-imports # Wird automatisch von watch/dev-server aufgerufennpm-Alternativen:
npm run watch
npm run dev-server
npm run buildwatch und dev-server rufen roadie:generate-component-imports automatisch vor dem Build auf.
Der Build nutzt Symfony Webpack Encore mit folgenden Entrypoints:
| Entrypoint | Datei | Zweck |
|---|---|---|
app |
assets/app.js |
Frontend — Styles, WebAwesome, Icon-Libraries, SVG-Sprites |
backend |
assets/backend.js |
REDAXO-Backend — Styles, Backend-spezifische WA-Komponenten |
Wichtige Konfiguration:
assets/icons/→public/build/icons/project/(eigene SVG-Icons)node_modules/@material-symbols/svg-300/sharp/→public/build/icons/material-sharp/(Material Icons)assets/svgs/→ SVG-Sprites viasvg-sprite-loader(inline<use>)- Webpack-Warning-Filter für WebAwesome Dynamic Imports (bekanntes False-Positive beim statischen Analysieren des WA-Autoloaders)
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import '@awesome.me/webawesome/dist/styles/native.css';
import './styles/style.scss';
import './roadie-component-imports'; // Auto-generiert
import '@awesome.me/webawesome/dist/translations/de.js';
import { registerIconLibrary, allDefined } from '@awesome.me/webawesome/dist/webawesome.js';
// Icon-Libraries registrieren
registerIconLibrary('default', {
resolver: (name) => `/build/icons/material-sharp/${name}.svg`,
mutator: (svg) => svg.setAttribute('fill', 'currentColor'),
});
registerIconLibrary('project', {
resolver: (name) => `/build/icons/project/${name}.svg`,
mutator: (svg) => svg.setAttribute('fill', 'currentColor'),
});
// Warten bis alle WA-Komponenten im DOM registriert sind
(async () => await allDefined())();roadie-component-imports.js wird automatisch durch roadie:generate-component-imports generiert und enthält nur die tatsächlich genutzten WA-Komponenten. Nicht manuell bearbeiten.
import '@awesome.me/webawesome/dist/styles/webawesome.css';
import './backend/styles/style.scss';
import '@awesome.me/webawesome/dist/translations/de.js';
import { registerIconLibrary } from '@awesome.me/webawesome/dist/webawesome.js';
// Feste Auswahl an WA-Komponenten für das REDAXO-Backend
import '@awesome.me/webawesome/dist/components/button/button.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
// … weitere Backend-Komponenten
registerIconLibrary('default', {
resolver: name => `/build/icons/material-sharp/${name}.svg`,
});Backend-Komponenten werden nicht auto-generiert, sondern fest eingetragen — nur was das Backend tatsächlich braucht.
Alles, was Roadie zur Laufzeit benötigt, wird im Project-Addon konfiguriert. Typische boot.php:
use Yakamara\Project\Article\ArticleKey;
use Yakamara\Project\Icons\IconLibrary;
use Yakamara\Roadie\Article\ArticleKeyRegistry;
use Yakamara\Roadie\Asset\AssetResolver;
use Yakamara\Roadie\Component\Image\ImageBreakpointValues;
use Yakamara\Roadie\Component\Template;
use Yakamara\Roadie\Icons\IconRegistry;
use Yakamara\Roadie\Section\SectionManager;
use Yakamara\Roadie\Section\SectionVariant;
use Yakamara\Roadie\Widget\ColorPicker;
$addon = rex_addon::get('project');
// 1. Eigene Komponenten-Templates registrieren
Template::addDirectory($addon->getPath('lib/Component/MyComponent/templates'));
// 2. Icon-System konfigurieren
IconRegistry::setDefaultLibrary('material-sharp');
IconRegistry::setIconsDirectory(rex_path::base('assets/backend/icons'));
IconLibrary::register(); // Aliase registrieren
// 3. Bildbreiten & Breakpoints
ImageBreakpointValues::setValues([
'Sm' => 576,
'Md' => 768,
'Lg' => 1280,
'Xl' => 1440,
]);
// 4. Section-Varianten
SectionManager::registerVariants(
SectionVariant::Plain,
SectionVariant::Neutral,
);
// 5. Artikel-Keys
ArticleKeyRegistry::register(ArticleKey::class, 'project');
// 6. Backend-Assets (nur auf content/edit-Seite)
if (rex::isBackend() && is_object(rex::getUser())) {
if ('content/edit' === rex_be_controller::getCurrentPage()) {
rex_view::addJsFile(rex_addon::get('roadie')->getAssetsUrl('iconpicker.js'));
rex_view::addCssFile(rex_addon::get('roadie')->getAssetsUrl('iconpicker.css'));
rex_view::addJsFile(rex_addon::get('roadie')->getAssetsUrl('colorpicker.js'));
rex_view::addCssFile(rex_addon::get('roadie')->getAssetsUrl('colorpicker.css'));
}
$backendAssets = (new AssetResolver())->getEntrypointFiles('backend');
foreach ($backendAssets['js'] as $url) {
rex_view::addJsFile($url, ['defer' => 'defer']);
}
foreach ($backendAssets['css'] as $url) {
rex_view::addCssFile($url);
}
}AssetResolver liest manifest.json und entrypoints.json aus dem Webpack-Build und liefert korrekte URLs — auch für den Dev-Server.
use Yakamara\Roadie\Asset\AssetResolver;
$resolver = new AssetResolver(); // Default: public/build/ (rex_path::frontend('build'))
$url = $resolver->getAssetUrl('app.js');
$files = $resolver->getEntrypointFiles('backend'); // ['js' => [...], 'css' => [...]]Für Templates gibt es die statische Fassade Asset:
use Yakamara\Roadie\Asset\Asset;
echo Asset::url('fonts/my-font.woff2');
echo Asset::preloadFont('fonts/my-font.woff2'); // <link rel="preload">
echo Asset::scriptTags('app'); // <script>-Tags des Entrypoints
echo Asset::linkTags('app'); // <link rel="stylesheet">-Tags
// SVG inline einbetten (mit Barrierefreiheits-Attributen)
echo Asset::svgInline('icons/logo.svg', label: 'Logo');
echo Asset::svgInline('icons/deco.svg'); // aria-hidden="true"
// SVG-Symbol-Referenz (<svg><use>)
echo Asset::svgSymbol('icon-arrow', label: 'Weiter');svgInline() setzt automatisch role="img" + aria-label wenn ein Label angegeben ist, sonst aria-hidden="true".
Asset::scriptTags() und Asset::linkTags() erzeugen die <script>- und <link>-Tags für den jeweiligen Webpack-Entrypoint. Typische Einbindung im REDAXO-HTML-Template:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?= Asset::linkTags('app') ?>
</head>
<body>
<?= $this->getArticle() ?>
<?= Asset::scriptTags('app') ?>
</body>
</html>Der defer-Wert ist bei scriptTags() bereits eingebaut — die Tags werden mit defer-Attribut ausgegeben.
Module können Scripts, Stylesheets und Preconnects zur Laufzeit registrieren; Roadie sammelt sie und injiziert sie einmalig vor </head> (via OUTPUT_FILTER, nur im Frontend). Ideal für Dritt-SDKs (Karten, Player …), die nur auf bestimmten Seiten gebraucht werden.
use Yakamara\Roadie\Asset\HeadAssets;
HeadAssets::addPreconnect('https://maps.example.com');
HeadAssets::addScript('https://maps.example.com/sdk.js', ['defer' => true]);
HeadAssets::addStylesheet(Asset::url('vendor/widget.css'));- Signaturen:
addScript(string $url, array $attributes = []),addStylesheet(string $url, array $attributes = []),addPreconnect(string $origin). $attributes:['defer' => true]rendert als reines Boolean-Attribut;false/nullwerden übersprungen.- Dedup über die URL/Origin (mehrfaches Registrieren derselben URL = ein Tag). Ausgabereihenfolge: Preconnects → Stylesheets → Scripts. Alle URLs werden
rex_escapet.
Registrierung (in roadie boot.php, Frontend): rex_extension::register('OUTPUT_FILTER', HeadAssets::injectIntoHead(...));.
Komponenten-Klassen liegen in lib/Component/{Name}/{Name}.php, Templates in lib/Component/{Name}/templates/{Name}.php. Alle Komponenten erweitern Component und sind direkt per echo oder in Templates nutzbar.
Template-Verzeichnisse registrieren (in boot.php):
use Yakamara\Roadie\Component\Template;
Template::addDirectory($addon->getPath('lib/Component/MyComponent/templates'));use Yakamara\Roadie\Component\Button\Button;
use Yakamara\Roadie\Component\Button\ButtonAppearance;
use Yakamara\Roadie\Component\Icon\Icon;
echo new Button(
label: 'Mehr erfahren',
href: '/ueber-uns',
appearance: ButtonAppearance::Accent,
);
echo new Button(
label: 'Senden',
start: new Icon(name: 'send'),
);Alle Komponenten akzeptieren ein HtmlAttributes-Objekt für zusätzliche Attribute:
use Yakamara\Roadie\Component\HtmlAttributes;
$attrs = new HtmlAttributes([
'data-tracking' => 'cta',
'class' => 'my-button',
]);
echo new Button(
label: 'Klick',
attributes: $attrs,
);Für zusammengesetzte Inhalte steht Component::slot() zur Verfügung:
echo new Dialog(
content: Component::slot('<p>Inhalt</p>'),
footer: Component::slot(
new Button(label: 'Schließen'),
'footer',
),
);Für bedingtes oder dynamisches Rendering innerhalb von Komponenten:
use Yakamara\Roadie\Component\Html;
$content = new Html(function () use ($items) {
foreach ($items as $item) {
echo new Card(title: $item->title);
}
});| Kategorie | Komponenten |
|---|---|
| Aktion | Button, ButtonGroup, CopyButton |
| Formular | Input, Textarea, NumberInput, Select, Combobox, Autocomplete, Listbox, Checkbox, Radio, RadioGroup, Switch, Slider, FileInput |
| Navigation | Breadcrumb, Dropdown, TabGroup, Tree |
| Overlay | Dialog, Drawer, Popup, Popover, Tooltip |
| Anzeige | Card, Callout, Details, Divider, Avatar, Badge, Tag, Scroller |
| Feedback | Spinner, Skeleton, ProgressBar, ProgressRing, Rating |
| Medien | Image, Video, AnimatedImage, Animation, Carousel, Comparison, ZoomableFrame |
| Formatierung | FormatDate, FormatNumber, FormatBytes, RelativeTime |
| Sonstiges | Icon, Link, Page, SplitPanel, QrCode, Sparkline, ColorPicker |
Comboboxvs.Autocomplete:Comboboxbildet WebAwesomes natives<wa-combobox>ab (in der aktuell installierten WA-Version noch nicht enthalten). Für ein durchsuchbares, endpoint-getriebenes Auswahlfeld gibt es die eigenständigeAutocomplete-Komponente (siehe unten).
Durchsuchbares Kombifeld (WAI-ARIA „combobox with list autocomplete"): ein Texteingabefeld mit Listbox-Popover, dessen gruppierte Optionen von einem Endpoint geladen werden. Für Formulare, in denen aus vielen Einträgen einer gewählt wird (tippen → Treffergruppen → auswählen). Vollständig tastaturbedienbar und barrierefrei; die Auswahl landet in einem versteckten Feld.
use Yakamara\Roadie\Component\Autocomplete\Autocomplete;
echo new Autocomplete(
name: 'trip_id', // Hidden-Feld, das den gewählten Wert bekommt
label: 'Reise wählen',
endpoint: '/?rex-api-call=my_endpoint',
placeholder: 'Reisetitel eingeben …',
value: $prefillValue, // optionaler Prefill …
valueLabel: $prefillDisplayLabel, // … + angezeigter Text
minQuery: 2, // Zeichen bis zur Suche (0 = beim Fokus zeigen)
countLabel: '{n} Treffer', // aria-live, "{n}" = Anzahl
emptyLabel: 'Nichts gefunden.',
hint: 'Bitte eine Reise wählen.', // optionaler Hinweis unter dem Feld
);Endpoint-Contract: POST mit JSON {query, …} → Antwort
{"groups": [{"label": "…", "options": [{"value": "…", "label": "…", "image": "…?"}]}]}.
Bei leerer/zu kurzer Eingabe sendet das JS query: "" — der Endpoint entscheidet, was der Ausgangszustand zeigt (z. B. Favoriten). image (Thumbnail-URL) ist optional.
Events (bubblen, ein umschließendes Formular-Component kann auf seinem Root lauschen):
| Event | Zweck |
|---|---|
autocomplete:beforefetch |
event.detail.payload ist mutierbar — hier zusätzliche Request-Parameter beisteuern (z. B. clientseitiger Kontext wie Merkzettel-IDs oder in der Seite aufgelöste Gruppen-Labels). |
autocomplete:select |
event.detail = {value, label} der gewählten Option — z. B. um abhängige Felder nachzuladen. |
Assets (JS/SCSS) werden automatisch gebündelt, sobald use …\Autocomplete\Autocomplete; in einer PHP-Datei steht (roadie:generate-component-imports).
Erzeugt barrierefreie <picture>-Elemente mit responsiven Bildbreiten, Formatkonvertierung, Art Direction und optionaler Bildunterschrift.
Die Komponente ist backend-agnostisch: Sie bekommt als ersten (Pflicht-)Parameter eine ImageSource übergeben, die URL-Auflösung und Metadaten-Lookup kapselt. Zwei Implementierungen liegen bei:
MediaPoolImageSource(string $fileName, string $mediaManagerType)— Bild aus dem REDAXO-Medienpool. URLs laufen über die MediaManager-Konvention/images/{type}/{name}@{w}w.ext, Metadaten kommen aus denmed_*-Feldern des Mediums.ExternalImageSource(string $originalUrl, string $mimeType, ?array $originalDimensions, array $variants, ...)— externe URL mit vorgerenderten Varianten (z. B. aus einer API). Metadaten (Alt, Caption, Credit …) werden direkt im Konstruktor übergeben.
use Yakamara\Roadie\Component\Image\Image;
use Yakamara\Roadie\Component\Image\MediaPoolImageSource;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'),
);MediaPoolImageSource erzeugt URLs der Form images/{type}/datei@200w.webp. Damit diese funktionieren, sind zwei Voraussetzungen nötig:
1. .htaccess — Rewrite-Regel für responsive Images
Im Projektverzeichnis muss folgende Regel eingetragen sein:
# -roadie- = Separator for responsive images
RewriteRule ^images/([^/]*)/(([^/]*)@?([0-9]*)w?\.(jpeg|jpg|avif|webp|png)) %{ENV:BASE}/index.php?rex_media_type=$1&rex_media_file=$2&roadie=true&%{QUERY_STRING} [B]2. MediaManager — roadie_responsive-Effect
In jedem genutzten Medientyp muss der Effect roadie_responsive (rex_effect_roadie_responsive) als erster Effect eingetragen sein. Er liest Breite (@200w) und Format (.webp) aus dem Dateinamen und überschreibt damit die konfigurierten Effekt-Parameter aller nachfolgenden Effekte zur Laufzeit — nur wenn ?roadie=true im Request gesetzt ist.
Wichtig: Steht der Effect nicht an erster Stelle, greifen die nachfolgenden Resize-/Format-Effekte vor der Anpassung und der responsive Mechanismus funktioniert nicht korrekt.
URL-Schema:
datei@200w.jpg→ Originaldatei in 200 px Breite, JPEGdatei.jpg@200w.webp→ Originaldatei in 200 px Breite, konvertiert zu WebP
Erlaubte Breiten werden über ImageResolutionValues validiert — nur registrierte Werte werden akzeptiert, sonst wird keine Größenanpassung vorgenommen.
3. FocusPoint (empfohlen)
Das AddOn focuspoint (FriendsOfREDAXO) ermöglicht es, am Medium im Backend einen Fokuspunkt zu definieren. Statt des Standard-resize-Effects wird dann focuspoint_fit genutzt: Er schneidet das Bild so zu, dass der markierte Fokuspunkt immer im sichtbaren Bereich bleibt.
Typische Effekt-Reihenfolge in einem Medientyp:
roadie_responsive← muss an erster Stelle stehenfocuspoint_fit(Breite + Höhe, Zoom nach Bedarf) — oderresizewenn kein Zuschnitt nötigimage_format(optional, für AVIF/WebP-Konvertierung)
ExternalImageSource nutzt vorgerenderte Varianten statt des MediaManagers — .htaccess-Rewrite und roadie_responsive-Effect sind dafür nicht nötig. Jede Variante ist ein ['width' => int, 'height' => int, 'urls' => [mime => url]]-Eintrag; getUrl() wählt die kleinste Variante, die für die Zielbreite noch breit genug ist. Metadaten werden direkt übergeben.
use Yakamara\Roadie\Component\Image\ExternalImageSource;
use Yakamara\Roadie\Component\Image\ImageFormat;
echo new Image(
source: new ExternalImageSource(
originalUrl: 'https://cdn.example.com/foo.jpg',
mimeType: 'image/jpeg',
originalDimensions: [1600, 900],
variants: [
['width' => 800, 'height' => 450, 'urls' => ['image/jpeg' => '…/foo-800.jpg', 'image/webp' => '…/foo-800.webp']],
['width' => 1600, 'height' => 900, 'urls' => ['image/jpeg' => '…/foo-1600.jpg', 'image/webp' => '…/foo-1600.webp']],
],
alt: 'Beschreibung',
),
formats: [ImageFormat::Webp],
);Im Projekt werden externe Quellen typischerweise nicht direkt instanziiert, sondern über eine Factory gebaut (z. B.
TravelServices::imageSourceFor()für Reisebilder aus der API).
ImageResolution ist ein Enum mit vier Stufen, die jeweils vordefinierte Pixelbreiten abdecken:
| Case | Standardwerte |
|---|---|
ImageResolution::Small |
200, 400, 800 |
ImageResolution::Medium |
1200, 1600 |
ImageResolution::Large |
1920, 2400 |
ImageResolution::All |
alle obigen kombiniert |
use Yakamara\Roadie\Component\Image\ImageResolution;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'),
resolutions: ImageResolution::Medium,
);Mehrere Presets lassen sich kombinieren — Duplikate werden automatisch entfernt und die Werte aufsteigend sortiert:
resolutions: [ImageResolution::Medium, ImageResolution::Large],
// ergibt: [1200, 1600, 1920, 2400]Alternativ können eigene Breiten als Integer-Array übergeben werden:
resolutions: [400, 800, 1200],Die Standardwerte der Enum-Cases können projektspezifisch überschrieben werden (in project/boot.php):
use Yakamara\Roadie\Component\Image\ImageResolutionValues;
ImageResolutionValues::setValues([
'Small' => [200, 400, 800],
'Medium' => [1200, 1600],
'Large' => [1920, 2400],
]);Viewport-Breakpoints für ImageBreakpointValues in project/boot.php setzen:
use Yakamara\Roadie\Component\Image\ImageBreakpointValues;
ImageBreakpointValues::setValues([
'Sm' => 576,
'Md' => 768,
'Lg' => 1280,
'Xl' => 1440,
]);Keys müssen PascalCase sein ('Sm', 'Md', …) — Lowercase wird stillschweigend ignoriert.
Ohne sizes geht der Browser von 100vw aus und lädt unnötig große Bilder. Der ImageSizes-Builder erzeugt den korrekten Wert auf Basis der konfigurierten Breakpoints.
until() — max-width-Bedingungen (von klein nach groß):
use Yakamara\Roadie\Component\Image\ImageSizes;
use Yakamara\Roadie\Component\Image\ImageBreakpoint;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'),
sizes: ImageSizes::create()
->until(ImageBreakpoint::Md, '100vw')
->until(ImageBreakpoint::Lg, '50vw')
->default('453px'),
// → (max-width: 767px) 100vw, (max-width: 1279px) 50vw, 453px
);from() — min-width-Bedingungen (von groß nach klein):
sizes: ImageSizes::create()
->from(ImageBreakpoint::Lg, '453px')
->default('100vw'),
// → (min-width: 1280px) 453px, 100vw
until()undfrom()können nicht gemischt werden — bei Verstoß wird eineInvalidArgumentExceptiongeworfen. Die Reihenfolge der Aufrufe ist irrelevant, die Ausgabe wird automatisch sortiert.
Projektweiter Default-Wert:
Anstatt sizes an jedem Bild zu setzen, kann ein Default registriert werden (in project/boot.php):
ImageSizes::setDefault(
ImageSizes::create()
->until(ImageBreakpoint::Md, '100vw')
->default('50vw'),
);Alle Image-Instanzen ohne explizites sizes greifen automatisch auf diesen Wert zurück.
Aspektverhältnis via CSS-Klasse:
Liefert die ImageSource ein Seitenverhältnis (getAspectRatio()), setzt die Komponente automatisch die CSS-Klasse aspect-ratio--16by9 am <img>-Element, statt width/height-Attribute zu setzen. Das verhindert, dass das Bild in der Downloadgröße (z. B. 200 px) dargestellt wird, und ermöglicht korrektes Layout-Verhalten über CSS.
MediaPoolImageSource leitet das Verhältnis aus einem {w}by{h}-Muster im mediaManagerType ab (z. B. content_16by9 oder 16by9). Liefert die Source kein Verhältnis (z. B. ExternalImageSource), fällt die Komponente auf width/height aus den Referenzdimensionen zurück.
SCSS generiert diese Klassen automatisch:
.aspect-ratio--16by9 { aspect-ratio: 16 / 9; }
.aspect-ratio--4by3 { aspect-ratio: 4 / 3; }
// … sowie Breakpoint-Varianten:
// .aspect-ratio-from-md--16by9, .aspect-ratio-from-lg--4by3, …Bei Motifs mit unterschiedlichen Seitenverhältnissen werden zusätzlich responsive Klassen gesetzt — aspect-ratio-from-{breakpoint}--{w}by{h} — die das Seitenverhältnis ab dem jeweiligen Breakpoint anpassen.
Lazy-Loading-Platzhalter:
Bei loading: ImageLoading::Lazy (Standard) erhält das <img>-Element die Klasse image--placeholder, die einen neutralen Hintergrund zeigt, bis das Bild geladen ist. Kein JavaScript, kein Inline-Style (CSP-konform).
.image--placeholder {
background-color: var(--wa-color-neutral-95);
}ImageMotif definiert ein alternatives Bild ab einem bestimmten Breakpoint. fromBreakpoint erwartet einen ImageBreakpoint-Enum-Case (Xs, Sm, Md, Lg, Xl).
use Yakamara\Roadie\Component\Image\ImageBreakpoint;
use Yakamara\Roadie\Component\Image\ImageMotif;
use Yakamara\Roadie\Component\Image\MediaPoolImageSource;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'content_4by3'),
resolutions: ImageResolution::Medium,
motifs: [
new ImageMotif(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'content_16by9'),
fromBreakpoint: ImageBreakpoint::Md,
),
new ImageMotif(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'content_21by9'),
fromBreakpoint: ImageBreakpoint::Lg,
resolutions: ImageResolution::Large, // überschreibt globale resolutions
sizes: ImageSizes::create()->default('100vw'),
),
],
);Jedes
ImageMotifbekommt eine eigeneImageSource(source). Für Medienpool-Bilder verwendet man typischerweise denselben Dateinamen mit einem anderenmediaManagerType(anderer Zuschnitt/Seitenverhältnis pro Breakpoint).
Art Direction erfordert Breitendeskriptoren im
srcset(200w) — Pixeldichte-Deskriptoren (2x) werden nicht unterstützt.
Das Original-Format wird immer automatisch hinzugefügt. Zusätzliche Formate werden als <source>-Elemente in der angegebenen Reihenfolge ausgegeben — Browser wählen das erste unterstützte Format. Verfügbare Cases im ImageFormat-Enum: Avif, Jpg, Png, Webp.
use Yakamara\Roadie\Component\Image\ImageFormat;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'),
formats: [ImageFormat::Avif, ImageFormat::Webp], // Fallback: Original-Format
);figure, caption, creditText und aiGenerated aktivieren das <figure>-Element und eine <figcaption>. Das figure-Element wird automatisch aktiviert, sobald eine der Properties einen sichtbaren Inhalt liefert — figure: true muss also nicht explizit gesetzt werden.
Jede Property akzeptiert drei Zustände:
| Wert | Verhalten |
|---|---|
true |
Wert wird automatisch aus den Metadaten der ImageSource gelesen (Medienpool: med_*-Felder) |
false / '' |
Property ist deaktiviert, kein Output |
'Eigener Text' |
Eigener Wert wird verwendet |
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'),
caption: true, // liest med_caption
creditText: true, // liest med_credit_text (Standard: true)
copyrightNotice: true, // liest med_copyright_notice, nur JSON-LD (Standard: true)
creator: true, // liest med_creator, nur JSON-LD (Standard: true)
aiGenerated: true, // liest med_is_ai_generated (Standard: true)
);creditText, copyrightNotice, creator und aiGenerated sind standardmäßig true — d. h. ohne explizite Konfiguration werden Metadaten automatisch aus dem Medienpool gelesen.
Properties können nach der Konstruktion überschrieben werden:
$image = new Image(source: new MediaPoolImageSource('REX_MEDIA[1]', 'my_type'));
$image->caption = 'Eigener Text';
$image->creditText = false; // Pflichtnennung in diesem Kontext unterdrückenJSON-LD: copyrightNotice, creator, creditText und aiGenerated werden zusätzlich als <script type="application/ld+json"> mit Schema.org-Markup (ImageObject) ausgegeben.
use Yakamara\Roadie\Component\Image\ImageLoading;
use Yakamara\Roadie\Component\Image\ImageDecoding;
use Yakamara\Roadie\Component\Image\ImageFetchPriority;
echo new Image(
source: new MediaPoolImageSource('REX_MEDIA[1]', 'hero_16by9'),
resolutions: ImageResolution::Large,
loading: ImageLoading::Eager,
fetchPriority: ImageFetchPriority::High, // für Above-the-fold-Bilder (z. B. Hero)
);| Property | Standardwert | Optionen |
|---|---|---|
loading |
ImageLoading::Lazy |
Lazy, Eager |
decoding |
ImageDecoding::Async |
Async, Sync, Auto |
fetchPriority |
null |
High, Low, Auto |
fetchPriority: Highundloading: Lazywidersprechen sich — die Kombination wirft eineInvalidArgumentException.
Rendert selbstgehostete Videos (natives <video>) oder externe Embeds (YouTube) — letztere datenschutzkonform als Zwei-Klick-Wrapper. Wie die Image-Komponente ist sie quellen-agnostisch und bekommt eine VideoSource.
use Yakamara\Roadie\Component\Video\Video;
use Yakamara\Roadie\Component\Video\MediaPoolVideoSource;
use Yakamara\Roadie\Component\Video\YouTubeVideoSource;
// Selbstgehostet (Medienpool)
echo new Video(source: new MediaPoolVideoSource('clip.mp4'));
// YouTube aus URL
$source = YouTubeVideoSource::fromUrl('https://youtu.be/dQw4w9WgXcQ', 'Reise-Teaser');
if (null !== $source) {
echo new Video(source: $source);
}| Parameter | Standard | Bedeutung |
|---|---|---|
source |
– | VideoSource (Pflicht) |
controls |
true |
Native Steuerelemente (nur <video>) — für Barrierefreiheit an lassen |
muted |
false |
Muss true sein, wenn autoplay genutzt wird |
autoplay |
false |
Nur wirksam mit muted: true; bei Embeds ignoriert |
loop |
false |
Nur selbstgehostet |
playsinline |
true |
Inline-Wiedergabe auf iOS (nur selbstgehostet) |
preload |
'metadata' |
<video preload> |
attributes |
– | HtmlAttributes |
Die Bool-Flags greifen nur beim nativen <video>; externe Embeds nutzen die Chrome des Anbieters.
MediaPoolVideoSource(string $fileName, string $posterMediaManagerType = '16by9')— Video aus dem Medienpool. Poster-Bild und WebVTT-Untertitel kommen aus denmed_video_poster/med_video_captions-Feldern des Mediums (siehe Media Pool Erweiterungen); der MIME-Typ wird aus der Extension ermittelt.YouTubeVideoSource(string $videoId, string $title = '', ?ImageSource $poster = null)— YouTube-Embed. Bequem viaYouTubeVideoSource::fromUrl($url, $title, $poster)(erkennt watch-/youtu.be-/embed-/shorts-URLs, gibtnullbei ungültiger URL). Das Poster muss eine lokaleImageSourcesein (keini.ytimg.comvor Einwilligung).
Vor dem Laden eines externen Videos zeigt der Wrapper einen Hinweis mit Link zur Datenschutzseite. Die URL projektweit setzen (in project/boot.php):
use Yakamara\Roadie\Component\Video\VideoConfig;
VideoConfig::setPrivacyUrl(ArticleResolver::getUrl(ArticleKey::Privacy));Ohne gesetzte URL bleibt der Hinweis, der Link wird ausgeblendet.
Das Icon-System besteht aus drei Schichten: SVG-Dateien auf der Festplatte → Manifest als optimierter Cache → Registry als Laufzeit-API. IconPicker und Icon-Komponente greifen beide auf dieselbe Registry zu.
assets/backend/icons/
├── material-sharp/ ← Default-Library
│ ├── arrow_right.svg
│ └── close.svg
└── project/ ← Weitere Libraries (z. B. eigene SVGs)
├── facebook.svg
└── instagram.svg
Jeder Unterordner ist eine Library. Der Name der Default-Library wird via setDefaultLibrary() konfiguriert.
use Yakamara\Roadie\Icons\IconRegistry;
IconRegistry::setDefaultLibrary('material-sharp');
IconRegistry::setIconsDirectory(rex_path::base('assets/backend/icons'));
IconRegistry::setMetaFile(rex_path::base('assets/backend/icons/meta.json')); // OptionalDas Manifest (var/data/addons/roadie/icons.json) wird aus den SVG-Dateien generiert und von der Registry gecacht. Nach jeder Änderung an SVG-Dateien neu ausführen:
php bin/console roadie:generate-icon-manifestSVGs werden automatisch bereinigt: width/height entfernt, fill auf currentColor normalisiert, Whitespace komprimiert. Vollständige Dokumentation unter Console Commands → roadie:generate-icon-manifest.
Optionale Meta-Datei für Labels und Suchbegriffe im IconPicker:
{
"arrow_right": { "label": "Pfeil rechts", "keywords": ["navigation", "weiter"] },
"project/facebook": { "label": "Facebook", "keywords": ["social"] }
}Keys ohne Library-Präfix gelten für die Default-Library, mit library/name-Präfix für weitere Libraries.
Icons aus Nicht-Default-Libraries können mit einem Alias versehen werden, sodass kein library-Parameter im PHP-Code nötig ist:
IconRegistry::alias('social-facebook', 'facebook', 'project');
// Verwendung: new Icon(name: 'social-facebook') — library wird automatisch aufgelöstZentrale Klasse mit allen genutzten Icon-Namen als Konstanten. Verhindert Hardcoding von Strings, macht Umbenennen trivial und registriert Aliase an einer Stelle.
namespace Yakamara\Project\Icons;
use Yakamara\Roadie\Icons\IconRegistry;
final class IconLibrary
{
// Navigation
public const string CLOSE = 'close';
public const string HAMBURGER = 'menu';
public const string NAV_NEXT = 'chevron_right';
public const string NAV_PREV = 'chevron_left';
// Social (library: 'project' — eigene SVGs)
public const string SOCIAL_FACEBOOK = 'social-facebook';
public const string SOCIAL_INSTAGRAM = 'social-instagram';
public static function register(): void
{
IconRegistry::alias(self::SOCIAL_FACEBOOK, 'facebook', 'project');
IconRegistry::alias(self::SOCIAL_INSTAGRAM, 'instagram', 'project');
}
}Registrierung in project/boot.php:
IconLibrary::register();use Yakamara\Roadie\Component\Icon\Icon;
use Yakamara\Project\Icons\IconLibrary;
// Default-Library
echo new Icon(
name: IconLibrary::CLOSE,
);
// Per Alias — kein library-Parameter nötig
echo new Icon(
name: IconLibrary::SOCIAL_FACEBOOK,
);
// Explizite Library
echo new Icon(
name: 'facebook',
library: 'project',
);
// Mit ARIA-Label (aria-label + role="img")
echo new Icon(
name: 'star',
label: 'Favorit',
);Ohne label wird aria-hidden="true" gesetzt.
Der IconPicker zeigt alle Libraries aus dem Manifest in einem modalen Dialog an. Der gespeicherte Wert ist icon-name (Default-Library) oder library:icon-name.
// Im Modul-Input:
echo IconPicker::widget('REX_INPUT_VALUE[1]', 'REX_VALUE[1]');
// Im Modul-Output — Icon-Komponente löst library:name automatisch auf:
if ($value = 'REX_VALUE[1]') {
echo new Icon(name: $value);
}Weitere Details unter Backend-Widgets → IconPicker.
Section ist ein Platzhalter-basiertes Wrapper-System für Sektionen. Frontend: Platzhalter {{{SECTION_...}}} werden nach dem Rendering durch echte Tags ersetzt. Backend: Platzhalter werden mit roadie-section-Klasse umhüllt.
use Yakamara\Roadie\Section\Section;
$section = new Section(
attributes: [
'class' => 'my-section',
'data-variant' => 'brand',
],
tag: 'section',
innerAttributes: ['class' => 'container'],
);
echo $section->getPlaceholder(); // {{{SECTION_xxxxxx}}} — Öffnender Tag
// ... Slice-Inhalt ...
echo '</div></section>'; // Schließender Tag manuell setzenRegistrierung verfügbarer Varianten (in project/boot.php):
use Yakamara\Roadie\Section\SectionManager;
use Yakamara\Roadie\Section\SectionVariant;
SectionManager::registerVariants(
SectionVariant::Plain,
SectionVariant::Neutral,
);Verfügbare Cases: SectionVariant::Plain (Label „Standard"), SectionVariant::Brand („Markenfarbe"), SectionVariant::Neutral („Neutral"). registerVariants() legt die im Backend auswählbare Teilmenge fest — registriert wird nur, was das Projekt tatsächlich nutzt.
Aktuelle Variante im Slice-Output lesen:
$manager = SectionManager::getInstance()->init();
$variant = $manager->getCurrentVariant(); // SectionVariant::Plain
if ($manager->is(SectionVariant::Neutral)) {
// Neutrale Gestaltung
}SliceManager verwaltet die Überschriften-Hierarchie innerhalb von Artikel-Slices und verhindert doppelte <h1>-Tags.
Slice-Typen:
| Typ | Bedeutung |
|---|---|
NEW_SECTION |
Startet einen neuen Abschnitt — Überschrift als h1 (erster Abschnitt) oder h2 (wenn bereits eine h1 existiert) |
SUB_SECTION |
Beginnt einen Unterabschnitt (Tiefe +1) |
CONTINUATION |
Fortsetzung des aktuellen Abschnitts (gleiche Tiefe) |
Im Modul-Input wird der Redakteur den Abschnittstyp wählen. SUB_SECTION und CONTINUATION werden deaktiviert, solange noch kein <h1> im Artikel existiert — das verhindert eine Hierarchie ohne Wurzel.
use Yakamara\Roadie\Slice\SliceManager;
use Yakamara\Roadie\Slice\SliceType;
$sliceManager = SliceManager::getInstance()->init();
?>
<div class="wa-stack">
<label class="wa-form-control-label">
Abschnittstyp
<?php
$s = new rex_select();
$s->setName('REX_INPUT_VALUE[20]');
$s->setSelected('REX_VALUE[20]');
$s->addOption(SliceType::NEW_SECTION->getTranslation(), SliceType::NEW_SECTION->value);
$s->addOption(SliceType::SUB_SECTION->getTranslation(), SliceType::SUB_SECTION->value, 0, 0, !$sliceManager->hasH1() ? ['disabled' => 'disabled'] : []);
$s->addOption(SliceType::CONTINUATION->getTranslation(), SliceType::CONTINUATION->value, 0, 0, !$sliceManager->hasH1() ? ['disabled' => 'disabled'] : []);
echo $s->get();
?>
</label>
<label class="wa-form-control-label">
Überschrift
<input type="text" name="REX_INPUT_VALUE[1]" value="REX_VALUE[1]">
</label>
</div>Im Output wird der gewählte Slice-Typ gesetzt, bevor die Überschrift gerendert wird. SliceManager ermittelt daraus den korrekten Heading-Level (h1, h2, …).
use Yakamara\Roadie\Slice\SliceManager;
use Yakamara\Roadie\Slice\SliceType;
use Yakamara\Roadie\Util\Aria;
$slice = $this->getCurrentSlice();
$sliceType = SliceType::tryFrom($slice->getValue(20));
$sliceManager = SliceManager::getInstance()->init();
$sliceManager->setCurrentSliceType($slice, $sliceType);
// Überschrift mit korrektem h-Level rendern
if ($heading = $slice->getValue(1)) {
$headingId = Aria::id();
echo $sliceManager->renderHeading($heading, ['id' => $headingId]);
}
// Redakteurspflege mit Heading-Levels im HTML anpassen (z. B. aus Redaktor)
echo $sliceManager->processHtml($richText);Weitere Methoden:
// Aktuellen Heading-Tag abfragen
$tag = $sliceManager->getHeadingTag(); // 'h1', 'h2', …
// Prüfen ob bereits ein h1 im Artikel existiert
$sliceManager->hasH1();
// HTML bereinigen (leere Tags entfernen etc.)
$clean = $sliceManager->cleanHtml($html);Roadie stellt zwei Navigationshelfer bereit, die Sprungziel-Daten während des Slice-Renderings einsammeln und an einer definierten Stelle im Template ausgeben. Beide folgen demselben Registry-Pattern: Roadie liefert einen minimalen Default-Renderer, das Project-Addon registriert seinen eigenen.
use Yakamara\Project\Navigation\TocNavigationRenderer;
use Yakamara\Project\Navigation\InPageNavigationRenderer;
use Yakamara\Roadie\Navigation\TocNavigation;
use Yakamara\Roadie\Navigation\InPageNavigation;
TocNavigation::setRenderer(TocNavigationRenderer::class);
InPageNavigation::setRenderer(InPageNavigationRenderer::class);Ohne setRenderer() greift der jeweilige Default-Renderer (DefaultTocNavigationRenderer / DefaultInPageNavigationRenderer), der ein einfaches semantisches <nav> ohne projektspezifisches Styling ausgibt.
Sprungziel-Navigation mit Icon und Label — wird als Ankerliste auf der Seite ausgegeben. Funktioniert über einen Platzhalter: Das Modul setzt den Platzhalter an der gewünschten Position, das Template ersetzt ihn nach dem Rendern aller Slices durch die fertige Navigation.
Modul-Input — Überschrift und Abschnittstyp pflegen:
use Yakamara\Roadie\Slice\SliceManager;
use Yakamara\Roadie\Slice\SliceType;
$sliceManager = SliceManager::getInstance()->init();
?>
<div class="wa-stack">
<label>
Abschnittstyp
<?php
$s = new rex_select();
$s->setName('REX_INPUT_VALUE[20]');
$s->setSelected('REX_VALUE[20]');
$s->addOption(SliceType::NEW_SECTION->getTranslation(), SliceType::NEW_SECTION->value);
echo $s->get();
?>
</label>
<label>
Überschrift
<input type="text" name="REX_INPUT_VALUE[1]" value="REX_VALUE[1]">
</label>
</div>Modul-Output — Überschrift und Platzhalter setzen:
use Yakamara\Roadie\Component\Callout\Callout;
use Yakamara\Roadie\Component\Callout\CalloutVariant;
use Yakamara\Roadie\Navigation\TocNavigation;
use Yakamara\Roadie\Slice\SliceManager;
use Yakamara\Roadie\Slice\SliceType;
$slice = $this->getCurrentSlice();
$sliceType = SliceType::tryFrom($slice->getValue(20));
$sliceManager = SliceManager::getInstance()->init();
$sliceManager->setCurrentSliceType($slice, $sliceType);
if ($heading = $slice->getValue(1)) {
$heading = $sliceManager->renderHeading($heading, ['class' => 'toc-navigation--heading']);
TocNavigation::setHeading($heading);
if (rex::isBackend()) {
echo $heading;
}
}
if (rex::isBackend()) {
echo new Callout(
content: 'Die Sprungziele werden automatisch zusammengestellt und an dieser Position ausgegeben.',
variant: CalloutVariant::Brand,
);
}
echo TocNavigation::placeholder();In anderen Modulen — Sprungziele registrieren (z. B. im Text-/Überschriften-Modul):
use Yakamara\Roadie\Navigation\TocNavigation;
use Yakamara\Roadie\Util\Aria;
if ($heading = $slice->getValue(1)) {
$headingId = Aria::id();
TocNavigation::add(
id: $headingId,
label: $slice->getValue(1),
icon: 'my_icon', // optional, null wenn kein Icon gewünscht
);
}Template — Platzhalter ersetzen (nach $this->getArticle()):
use Yakamara\Roadie\Navigation\TocNavigation;
$content = TocNavigation::replacePlaceholder($content);replacePlaceholder() gibt '' zurück wenn keine Sprungziele registriert sind — der Platzhalter verschwindet lautlos.
Inhaltsverzeichnis-Navigation. Zum Beispiel als seitlicher Drawer — erscheint als fixierter Button am Seitenrand. Wird direkt im Template gerendert, kein Platzhalter nötig.
Modul-Output — Einträge in anderen Modulen registrieren (z. B. im Text-/Überschriften-Modul):
use Yakamara\Roadie\Navigation\InPageNavigation;
use Yakamara\Roadie\Util\Aria;
if ($heading = $slice->getValue(1)) {
$headingId = Aria::id();
$richText .= $sliceManager->renderHeading($heading, ['id' => $headingId]);
InPageNavigation::add(
id: $headingId,
title: $slice->getValue(1),
kicker: $slice->getValue(4) ?: null, // optionaler Obertitel
);
}Template — Navigation ausgeben:
use Yakamara\Roadie\Navigation\InPageNavigation;
if (!InPageNavigation::isEmpty()) {
echo InPageNavigation::render();
}Tipp: Die Ausgabe lässt sich pro Artikel über ein MetaInfo-Feld steuern. Dazu im MetaInfo AddOn ein Checkbox-Feld anlegen (z. B.
art_in_page_navigation) und die Bedingung im Template entsprechend erweitern:if ('1' === $article->getValue('art_in_page_navigation') && !InPageNavigation::isEmpty()) { echo InPageNavigation::render(); }
Der Renderer muss eine statische render(): string-Methode besitzen:
namespace Yakamara\Project\Navigation;
use Yakamara\Roadie\Navigation\TocNavigation;
class TocNavigationRenderer
{
public static function render(): string
{
$items = '';
foreach (TocNavigation::getItems() as $item) {
// projektspezifisches Rendering …
}
return '<nav class="toc-navigation" aria-label="Seitennavigation">'
. TocNavigation::getHeading()
. '<ul class="toc-navigation--list">' . $items . '</ul>'
. '</nav>';
}
}Roadie erweitert den REDAXO Media Pool um Metadaten-Felder für Barrierefreiheit, Urheberrecht und KI-Kennzeichnung.
Im Medien-Formular werden automatisch folgende Felder ergänzt:
Alternativtext & Bildunterschrift
| Feld | DB-Spalte | Sprachspezifisch |
|---|---|---|
| Alt-Text | med_alt, med_alt_2, … |
✓ |
| Bildunterschrift | med_caption, med_caption_2, … |
✓ |
Urheber & Rechte
| Feld | DB-Spalte | Ausgabe |
|---|---|---|
| Urheber | med_creator |
Nur maschinenlesbar (JSON-LD) |
| Urheberrechtshinweis | med_copyright_notice |
Nur maschinenlesbar (JSON-LD) |
| Pflichtnennung | med_credit_text |
Sichtbar am Bild mit © |
Schalter
| Feld | DB-Spalte | Bedeutung |
|---|---|---|
| Schmuckgrafik | med_is_decorative |
Überspringt Alt-Text-Pflicht |
| KI-generiertes Bild | med_is_ai_generated |
Kennzeichnungspflicht gem. EU AI Act |
| Transparenz | med_has_transparency |
Automatisch beim Upload/Replace erkannt (nicht editierbar) — erlaubt passenden Hintergrund für transparente Logos/Icons |
Ist ein Bild als KI-generiert markiert, erscheint im Backend ein Warnhinweis und auf der Website wird das Bild mit einem „KI-generiert"-Label versehen sowie mit digitalSourceType im JSON-LD ausgewiesen.
Fehlt der Alt-Text bei einem nicht-dekorativen Bild, wird in der Medienliste ein Warnbadge am Vorschaubild angezeigt.
Video-Felder
Bei Video-Dateien (mp4, webm, ogg, ogv, mov, m4v) blendet das Medien-Formular zusätzlich eine Video-Sektion ein:
| Feld | DB-Spalte | Bedeutung |
|---|---|---|
| Poster-Bild | med_video_poster |
Medienpool-Datei, die vor dem Start des Videos angezeigt wird |
| Untertitel (WebVTT) | med_video_captions |
.vtt-Datei aus dem Medienpool — aktiviert Untertitel im <video>-Player (a11y) |
In der Medienliste erhalten Videos ein Play-Overlay; fehlt das Poster-Bild, wird ein Warnbadge angezeigt.
Yakamara\Roadie\MediaPool\Media ist ein typsicherer Wrapper um rex_media:
use Yakamara\Roadie\MediaPool\Media;
$media = Media::get('image.jpg');
$media->getAlt(); // Sprachspezifisch
$media->getCaption(); // Sprachspezifisch
$media->getTitle(); // Titel (sprachspezifisch ab Sprache 2, sonst rex_media-Titel)
$media->getCreator(); // Urheber (Fotograf oder KI-Tool)
$media->getCopyrightNotice(); // Rechtlicher Urheberrechtshinweis
$media->getCreditText(); // Pflichtnennung bei Verwendung (sichtbar am Bild)
$media->isDecorative(); // Schmuckgrafik ohne Alt-Text-Pflicht
$media->isAiGenerated(); // KI-generiertes Bild
$media->hasTransparency(); // Nutzt das Bild Transparenz (Alpha-Kanal)?
$media->getVideoPoster(); // Dateiname des Poster-Bilds (Video), '' wenn keins
$media->getVideoCaptions(); // Dateiname der WebVTT-Captions (Video), '' wenn keine
$media->getDimensions(); // [width, height]
$media->getDimensionsByMediaManagerType('my_type'); // [width, height] nach EffektErmöglicht projektweit eindeutige Bezeichner für wichtige Artikel (Impressum, Startseite, Team …), die unabhängig von der Artikel-ID sind. Werte werden in rex_config gespeichert und können per Console-Command oder Backend-UI gepflegt werden.
namespace Yakamara\Project\Article;
enum ArticleKey: string
{
case Imprint = 'imprint';
case Team = 'team';
case Contact = 'contact';
}use Yakamara\Project\Article\ArticleKey;
use Yakamara\Roadie\Article\ArticleKeyRegistry;
ArticleKeyRegistry::register(ArticleKey::class, 'project');Der zweite Parameter ist der rex_config-Namespace (identisch mit dem Addon-Namen).
use Yakamara\Project\Article\ArticleKey;
use Yakamara\Roadie\Article\ArticleResolver;
$article = ArticleResolver::get(ArticleKey::Imprint);
$url = ArticleResolver::getUrl(ArticleKey::Imprint);
$urlDe = ArticleResolver::getUrl(ArticleKey::Imprint, clang: 1);getUrl() gibt '#' zurück, wenn kein Artikel hinterlegt ist.
ArticleResolver::set(ArticleKey::Imprint, articleId: 42);
ArticleResolver::remove(ArticleKey::Imprint);Die Pflegeseite ist im Backend unter System → Roadie → Artikel-Keys erreichbar. Alle registrierten Enums werden dort mit einem Linkmap-Feld angezeigt.
php bin/console roadie:article-key list
php bin/console roadie:article-key set imprint 42
php bin/console roadie:article-key remove imprintSind mehrere Enums mit gleichem Key-Wert registriert, wird ein Fehler mit den betroffenen Namespaces ausgegeben.
Solange ein Artikel einem Key zugewiesen ist, kann er nicht gelöscht werden. Das gilt auch für das Löschen der übergeordneten Kategorie.
Verhindert das Löschen von Artikeln, die noch referenziert werden. Die Prüfung greift auf ART_PRE_DELETED, das sowohl beim direkten Artikel-Löschen als auch beim Löschen einer Kategorie ausgelöst wird.
Roadie registriert automatisch folgende Checker:
| Checker | Prüft |
|---|---|
ArticleSliceUsageChecker |
link1–link10 und linklist1–linklist10 in rex_article_slice |
MetaInfoUsageChecker |
REX_LINK_WIDGET- und REX_LINKLIST_WIDGET-Felder in rex_article |
ArticleKeyUsageChecker |
Alle via ArticleKeyRegistry registrierten Enum-Keys |
use Yakamara\Roadie\ArticleUsage\ArticleUsageChecker;
ArticleUsageChecker::addChecker(static function (int $articleId): array {
$usages = [];
// Prüflogik …
if ($found) {
$usages[] = 'Wird in Komponente X verwendet';
}
return $usages;
});Der Callable erhält die Artikel-ID und gibt eine Liste von menschenlesbaren Verwendungs-Beschreibungen zurück.
Assets werden in project/boot.php auf der content/edit-Seite geladen:
if (rex::isBackend() && 'content/edit' === rex_be_controller::getCurrentPage()) {
rex_view::addJsFile(rex_addon::get('roadie')->getAssetsUrl('iconpicker.js'));
rex_view::addCssFile(rex_addon::get('roadie')->getAssetsUrl('iconpicker.css'));
rex_view::addJsFile(rex_addon::get('roadie')->getAssetsUrl('colorpicker.js'));
rex_view::addCssFile(rex_addon::get('roadie')->getAssetsUrl('colorpicker.css'));
}Ermöglicht die Auswahl eines Icons aus einer oder mehrerer SVG-Libraries.
use Yakamara\Roadie\Widget\IconPicker;
echo IconPicker::widget('REX_INPUT_VALUE[1]', 'REX_VALUE[1]');Gespeicherter Wert: Icon-Name der Default-Library (arrow_right) oder library:name für weitere Libraries.
Moduloutput:
use Yakamara\Roadie\Icons\IconRegistry;
$parsed = IconRegistry::parseValue('REX_VALUE[1]');
$icon = IconRegistry::get($parsed['library'], $parsed['name']);
// $icon['svg'] enthält den bereinigten SVG-MarkupErmöglicht die Auswahl einer vordefinierten Farbe als Key (kein Hex-Wert).
use Yakamara\Roadie\Widget\ColorPicker;
echo ColorPicker::widget('REX_INPUT_VALUE[2]', 'REX_VALUE[2]');Gespeicherter Wert: Farb-Key (z.B. primary, transparent) oder leerer String.
Farben registrieren (in project/boot.php):
ColorPicker::registerGroup(
groupKey: 'brand',
label: 'Markenfarben',
colors: [
'primary' => ['color' => '#003366', 'name' => 'Primärfarbe'],
'secondary' => ['color' => '#668899', 'name' => 'Sekundärfarbe'],
],
);Moduloutput:
$colorKey = ColorPicker::validate('REX_VALUE[2]');Der Farb-Key wird typischerweise als data-*-Attribut ans Element übergeben und per CSS ausgewertet — kein style-Attribut, da CSP keine Inline-Styles erlaubt:
// Ausgabe im Template:
echo '<section data-color="' . rex_escape($colorKey) . '">';
// CSS:
// [data-color="primary"] { --section-color: var(--color-primary); }Ermöglicht die visuelle Auswahl eines Layouts anhand von SVG-Vorschauen. Rendert eine wa-radio-group mit je einem wa-radio appearance="button" pro Option.
use Yakamara\Roadie\Widget\LayoutPicker\LayoutPicker;
use Yakamara\Roadie\Widget\LayoutPicker\LayoutPickerOption;
echo new LayoutPicker(
options: [
new LayoutPickerOption(
value: '1col',
label: '1-spaltig',
svg: LayoutPickerSvg::build([
new LayoutPickerSvgBlock(
col: 1,
span: 12,
),
]),
),
new LayoutPickerOption(
value: '2col',
label: '2-spaltig',
svg: LayoutPickerSvg::build([
new LayoutPickerSvgBlock(
col: 1,
span: 6,
),
new LayoutPickerSvgBlock(
col: 7,
span: 6,
),
]),
),
],
name: 'REX_INPUT_VALUE[3]',
value: 'REX_VALUE[3]',
);Gespeicherter Wert: Der value-String der gewählten Option (z.B. 2col).
SVG-Helfer:
LayoutPickerSvg::build(array $blocks, int $height = 40, int $rows = 1) generiert SVG-Vorschauen auf Basis eines 12-Spalten-Rasters. Das SVG ist immer 100 Einheiten breit, die Höhe ($height) ist variabel. Für mehrzeilige Layouts wird $rows gesetzt — die row/rowSpan-Angaben der Blöcke beziehen sich auf dieses Raster. Spalten und Blöcke verwenden currentColor und passen sich damit automatisch ans CSS-Farbschema an.
use Yakamara\Roadie\Widget\LayoutPicker\LayoutPickerSvgBlock;
use Yakamara\Roadie\Widget\LayoutPicker\LayoutPickerSvg;
// Vollbreite
LayoutPickerSvg::build([
new LayoutPickerSvgBlock(
col: 1,
span: 12,
),
]);
// Zwei gleiche Spalten
LayoutPickerSvg::build([
new LayoutPickerSvgBlock(
col: 1,
span: 6,
),
new LayoutPickerSvgBlock(
col: 7,
span: 6,
),
]);
// Sidebar-Layout mit Header-Leiste (2 Zeilen)
LayoutPickerSvg::build(
blocks: [
new LayoutPickerSvgBlock(
col: 1,
span: 12,
row: 1,
),
new LayoutPickerSvgBlock(
col: 1,
span: 3,
row: 2,
),
new LayoutPickerSvgBlock(
col: 4,
span: 9,
row: 2,
),
],
height: 48,
rows: 2,
);LayoutPickerSvgBlock-Parameter:
| Parameter | Typ | Bedeutung |
|---|---|---|
col |
int |
Startspalte, 1-basiert (1–12) |
span |
int |
Spaltenbreite (1–12) |
row |
int |
Startzeile, 1-basiert, Standard 1 |
rowSpan |
int |
Anzahl der überspannten Zeilen, Standard 1 |
fill |
?string |
Optionale Füllfarbe (z. B. #e63946 oder var(--color-primary)) — überschreibt die Gruppen-Variable --roadie-layout-picker-svg-block |
text |
?string |
Optionales Label, zentriert auf dem Block |
Moduloutput:
$layout = 'REX_VALUE[3]' ?: '1col';Styling: Über .layout-picker, .layout-picker--option und .layout-picker--label. SVG-Größe z.B. via .layout-picker wa-radio svg { width: 4rem; height: auto; }.
Auf der Artikel-Bearbeitungsseite (content/edit) blendet Roadie ein andockbares Vorschau-Panel ein: ein <iframe> der echten Frontend-Seite des Artikels, direkt neben dem Editor. Kein Setup nötig — sobald das AddOn aktiv ist, erscheint unten rechts ein Toggle-Button (Assets und Panel werden in boot.php nur auf content/edit geladen).
- Andockbares Panel rechts; schiebt den Editor-Inhalt per CSS zur Seite (kein Overlay). Der Zustand (offen, Device, Metadaten ausgeblendet) wird client-seitig in
localStoragegehalten und übersteht den Seiten-Reload beim Slice-Speichern — dadurch aktualisiert sich die Vorschau beim Speichern automatisch. - Device-Umschalter: Desktop (1440), Tablet (1024), Tablet Hochkant (768), Mobil (375). Jede Voreinstellung rendert bei fester Viewport-Breite und wird auf die Panelbreite herunterskaliert; die volle Seitenhöhe ist scrollbar. Bewusst nur ein Scroller — der iframe-eigene Scrollbalken wird unterdrückt, die Höhe folgt dem Inhalt (via
ResizeObserver). - Online- und Offline-Artikel: Auch unveröffentlichte Artikel/Entwürfe werden gerendert (über die geteilte Backend-Session), damit der Aufbau vorab prüfbar ist; ein „Offline"-Badge kennzeichnet sie.
- Metadaten ausblenden (opt-in): blendet die Core-Metadaten-Spalte (
#rex-js-main-sidebar) aus und gibt den frei werdenden Platz dem dann breiteren Panel — nur solange das Panel offen ist. Rein über Core-Selektoren, also unabhängig von anderen AddOns (z. B. Sprog-Sprachvergleich). - Scroll-Sync in beide Richtungen:
- Klick auf einen Slice-Block im Editor → die Vorschau scrollt zum Slice und hebt ihn kurz hervor.
- Klick in die Vorschau → der Editor scrollt zum zugehörigen Slice (Header oben ausgerichtet) und flasht den Block. Links in der Vorschau werden dabei nicht verfolgt, damit sie auf dem editierten Artikel bleibt.
- Refresh und „In neuem Tab öffnen" (kanonische URL ohne Vorschau-Parameter).
Roadie hängt sich in den SLICE_SHOW-Extension-Point und setzt vor jeden Slice einen unsichtbaren Anker (<span data-roadie-slice="{id}">). Da SLICE_SHOW-Output in den geteilten Artikel-Cache gebacken wird, wird der Anker als laufzeit-bedingtes echo erzeugt:
if (rex_get('roadie_preview', 'bool', false) && rex_backend_login::hasSession()) {
echo '<span class="roadie-preview-anchor" data-roadie-slice="…" aria-hidden="true"></span>';
}Die Bedingung wird bei jedem Request ausgewertet — der Anker erscheint also nur im Vorschau-iframe (?roadie_preview=1) und nur für eingeloggte Backend-User. Öffentliche Seiten enthalten ihn nie, ganz ohne Output-Nachbearbeitung. Frontend und Backend sind same-origin, daher greift das JS direkt auf das iframe-Dokument zu (kein postMessage).
Deploy-Hinweis: Nach dem Ausrollen einmal den Artikel-Cache leeren (
php bin/console cache:clear), damit die bedingten Slice-Anker in bereits gecachte Seiten gelangen.
Barrierefrei: Toggle mit aria-expanded, Panel mit inert/aria-label und Fokus-Handling, Device-Buttons mit aria-pressed, alle Icon-Buttons mit aria-label + title.
Liest alle SVG-Dateien aus dem konfigurierten Icons-Verzeichnis und schreibt das Manifest nach var/data/addons/roadie/icons.json.
php bin/console roadie:generate-icon-manifestVoraussetzung: IconRegistry::setDefaultLibrary() und IconRegistry::setIconsDirectory() sind in project/boot.php gesetzt.
Verzeichnisstruktur: Jeder Unterordner im Icons-Verzeichnis wird als eigene Library behandelt. Der Ordnername entspricht dem Library-Namen. Der über setDefaultLibrary() konfigurierte Ordner wird als Default markiert — Icons daraus werden ohne Library-Präfix gespeichert.
assets/backend/icons/
└── material-sharp/ ← Default-Library ("material-sharp")
├── arrow-right.svg
└── close.svg
SVG-Bereinigung: Jede Datei wird automatisch normalisiert:
- XML-Deklaration, DOCTYPE und Kommentare werden entfernt
<title>und<desc>werden entferntwidth/height-Attribute am<svg>-Tag werden entfernt (Sizing via CSS)fill="*"wird auffill="currentColor"gesetzt (fill="none"bleibt erhalten)- Whitespace wird normalisiert
Optionale Meta-Datei für Labels und Suchbegriffe (via IconRegistry::setMetaFile()):
{
"arrow-right": { "label": "Pfeil rechts", "keywords": ["navigation", "weiter"] },
"material-sharp/close": { "label": "Schließen", "keywords": ["x", "entfernen"] }
}Ohne Meta-Datei wird der Dateiname als Label verwendet (arrow-right → „Arrow Right").
Scannt das gesamte Projekt nach verwendeten <wa-*>-Tags und Roadie-Komponenten und generiert assets/roadie-component-imports.js für den Webpack-Build.
php bin/console roadie:generate-component-importsWird automatisch aufgerufen vor jedem Build via package.json-Scripts (watch, build, dev-server). Die generierte Datei nicht manuell bearbeiten.
Gescannte Verzeichnisse:
src/templates/— REDAXO-Templatessrc/modules/— REDAXO-Modulesrc/addons/project/— Project-Addon (PHP-Klassen, Templates)assets/scripts/components/— Projektspezifische JS-Komponenten
Was generiert wird:
| Quelle | Ergebnis |
|---|---|
<wa-button> in PHP/HTML |
import '@awesome.me/webawesome/dist/components/button/button.js' |
use Yakamara\Roadie\Component\Button\Button |
SCSS aus lib/Component/Button/styles/ + JS aus lib/Component/Button/scripts/ |
JS-Klasse mit static componentName in assets/scripts/components/ |
register(MyComponent) im ComponentRegistry |
Wenn rex_yform im Code erkannt wird, werden zusätzlich alle src/addons/*/ytemplates/roadie/-Verzeichnisse nach <wa-*>-Tags gescannt.
wa-*-Tags ohne entsprechendes Package (nicht in node_modules/@awesome.me/webawesome/dist/components/) werden als Warnung ausgegeben, aber nicht importiert.
Verwaltet die Zuordnung von Artikel-Keys zu REDAXO-Artikeln auf der Kommandozeile.
# Alle gesetzten Keys auflisten
php bin/console roadie:article-key list
# Einen Key setzen
php bin/console roadie:article-key set imprint 42
# Einen Key entfernen
php bin/console roadie:article-key remove imprintSind mehrere Enums mit gleichem Key-Wert registriert, wird ein Fehler mit den betroffenen Namespaces ausgegeben. Weitere Details unter ArticleKey.
Backfill für die med_has_transparency-Spalte auf Bestands-Medien. Neue Uploads/Updates setzen den Wert automatisch (via MediaTransparencyDetector auf MEDIA_ADDED/MEDIA_UPDATED); dieser Command holt ihn für bereits vorhandene Dateien nach.
php bin/console roadie:media-transparency:scan
php bin/console roadie:media-transparency:scan --limit=1000
php bin/console roadie:media-transparency:scan --all
php bin/console roadie:media-transparency:scan --force| Option | Standard | Bedeutung |
|---|---|---|
--limit |
500 |
Maximale Anzahl der pro Lauf gescannten Dateien |
--all |
– | Alle Dateitypen scannen (Standard: nur Typen mit aktivem Detector — aktuell PNG, WebP, AVIF, GIF, SVG) |
--force |
– | Auch Dateien neu scannen, deren Wert bereits ≠ 0 ist |
Standardmäßig werden nur Dateien mit med_has_transparency = 0 und einem unterstützten Format gescannt. Wird das Limit erreicht, weist der Command darauf hin, erneut zu laufen.
Entfernt verwaiste rex_config-Einträge (company.{domainId}.*) von yrewrite-Domains, die es nicht mehr gibt. yrewrite feuert beim Löschen einer Domain kein Event — dieser Command räumt daher manuell (oder per Cron) auf.
php bin/console roadie:company:cleanup
php bin/console roadie:company:cleanup --dry-run # nur anzeigen, nichts löschenGibt pro betroffener Domain die Anzahl entfernter Keys aus. Betrifft die Unternehmensdaten.
Namespace Yakamara\Roadie\Html — reine PHP-Helfer ohne Template-Dateien.
Erzeugt <ul>, <ol> und <dl> direkt aus PHP-Arrays. Strings werden automatisch escaped, Component-Instanzen werden gerendert, null-Einträge übersprungen.
use Yakamara\Roadie\Html\HtmlList;
use Yakamara\Roadie\Html\HtmlListItem;
use Yakamara\Roadie\Html\HtmlDescriptionListItem;
use Yakamara\Roadie\Component\HtmlAttributes;// Einfache Liste
echo HtmlList::ul(['Punkt 1', 'Punkt 2', new SomeComponent()]);
// Geordnete Liste mit Attributen auf dem Container
echo HtmlList::ol(
['Erster Schritt', 'Zweiter Schritt'],
new HtmlAttributes(['class' => 'steps']),
);
// Einzelne Items mit Attributen via HtmlListItem
echo HtmlList::ul([
new HtmlListItem('Aktiver Punkt', new HtmlAttributes(['class' => 'active', 'aria-current' => 'true'])),
'Normaler Punkt',
null, // wird übersprungen
]);Kurzform — Keys = <dt>, Values = <dd>:
echo HtmlList::dl([
'Reiseziel' => 'Sri Lanka',
'Reiseart' => new Html('<strong>Rundreise</strong>'),
'Preis ab' => null, // wird übersprungen
]);Langform mit HtmlDescriptionListItem für Attribute auf <dt> und <dd>:
echo HtmlList::dl([
new HtmlDescriptionListItem(
term: 'Reiseziel',
definition: 'Sri Lanka',
termAttributes: new HtmlAttributes(['class' => 'label']),
definitionAttributes: new HtmlAttributes(['class' => 'value']),
),
new HtmlDescriptionListItem(
term: 'Preis ab',
definition: null, // wird übersprungen
),
]);Kurzform und Langform sind nicht mischbar innerhalb eines dl()-Aufrufs.
Das Filter-System ermöglicht es, vorselektierte Suchfilter aus einem REDAXO-Modul auf eine Suchseite zu übergeben. Die Filterwerte werden als JSON in einem einzigen REX_VALUE-Feld gespeichert und beim Aufruf der Suchseite als URL-Query-Parameter übergeben.
Namespace Yakamara\Roadie\Filter.
| Klasse | Beschreibung |
|---|---|
FilterDefinition |
Abstrakte Basis für alle Filter |
FilterOption |
Value Object für eine Auswahloption (Label + Value) |
ChoiceFilter |
Auswahl-Filter in vier Varianten (Select, Multi-Select, Radio, Checkboxen) |
RangeFilter |
Wertebereich-Filter (min/max) — rendert keine Moduleingabe, dient nur der URL-Generierung |
TextFilter |
Freitext-Filter (standardmäßig ohne Moduleingabe) |
FilterInput |
Rendert das komplette Filter-Widget für die REDAXO-Moduleingabe |
use Yakamara\Roadie\Filter\FilterOption;
// Aus beliebigen Objekten
$options = FilterOption::fromItems(
items: $myObjects,
label: fn($o) => $o->name,
value: fn($o) => $o->id,
);
// Aus assoziativem Array [value => label]
$options = FilterOption::fromArray(['economy' => 'Economy', 'business' => 'Business']);use Yakamara\Roadie\Filter\ChoiceFilter;
// extended=false + multiple=false → <wa-select>
new ChoiceFilter(key: 'type', label: 'Typ', options: $options);
// extended=false + multiple=true → <wa-select multiple>
new ChoiceFilter(key: 'tags', label: 'Tags', options: $options, multiple: true);
// extended=true + multiple=false → <wa-radio-group>
new ChoiceFilter(key: 'sort', label: 'Sortierung', options: $options, extended: true);
// extended=true + multiple=true → native Checkbox-Gruppe
new ChoiceFilter(key: 'cat', label: 'Kategorien', options: $options, multiple: true, extended: true);Hinweis: Die Checkbox-Variante verwendet native
<input type="checkbox">-Elemente (nichtwa-checkbox), dawa-checkboxein nicht-standardkonformes Verhalten beim Setzen vonvaluehat.
use Yakamara\Roadie\Filter\RangeFilter;
use Yakamara\Roadie\Filter\TextFilter;
// RangeFilter rendert keine Moduleingabe (renderInput() gibt '' zurück); min/max nur für die URL-Generierung
new RangeFilter(key: 'price', label: 'Preis bis (€)', min: 0, max: 15000);
// TextFilter wird standardmäßig NICHT in der Moduleingabe angezeigt (showInModuleInput: false)
new TextFilter(key: 'id', label: 'Reise-ID');MBlock-kompatibel: Alle Filterwerte werden in einem einzigen Hidden Input als JSON gespeichert. JS stellt gespeicherte Werte beim Laden via restore() zurück.
use Yakamara\Roadie\Filter\FilterInput;
// Ohne MBlock
echo FilterInput::render(
filters: MyFilters::getAll(),
inputName: 'REX_INPUT_VALUE[1]',
saved: json_decode('REX_VALUE[1]', true) ?? [],
);
// Mit MBlock — saved weglassen, MBlock füllt den Hidden Input
$form = '...' . FilterInput::render(
filters: MyFilters::getAll(),
inputName: 'REX_INPUT_VALUE[2][0][filter]',
) . '...';
echo MBlock::show(2, $form);// URL zur Suchseite mit vorselektierten Filtern
$filterValues = json_decode($item['filter'] ?? '{}', true) ?? [];
$url = MyFilters::buildUrl(articleId: 42, selected: $filterValues);
// → https://example.com/suche?destination[]=lk&destination[]=mv&price=5000buildUrl() iteriert über alle FilterDefinition-Instanzen und ruft deren buildQueryParams() auf. Leere/nicht gesetzte Filter werden übersprungen.
Zentrale, redaktionell gepflegte Firmen- und Standortdaten pro yrewrite-Domain — Grundlage für Footer, Impressum, Kontaktangaben und schema.org-Auszeichnung. Gespeichert in rex_config (Namespace roadie, Schlüssel company.{domainId}.*).
Unter System → Roadie → Strukturierte Daten (Recht roadie[company], eine Unterseite je Domain) pflegt der Redakteur pro Domain:
- Identität: Name, rechtlicher Name, URL, Logo (Medienpool), Gründungsdatum
- Beschreibung: je Sprache (clang)
- Kontakt: Telefon, E-Mail
- Adresse: Straße, PLZ, Ort, Land, Geo-Koordinaten (lat/lng)
- Rechtliches: USt-IdNr., Registereintrag, Geschäftsführung
- Social Media: ein Feld je registriertem Netzwerk
- schema.org-Zuordnung: welcher Artikel als ContactPage bzw. AboutPage ausgezeichnet wird (Linkmap) — wird von der SEO-Engine gelesen
use Yakamara\Roadie\Company\CompanyProfile;
$company = CompanyProfile::current(); // aktuelle yrewrite-Domain (Fallback: erste konfigurierte)
$company = CompanyProfile::forDomain(1); // bestimmte Domain
$company->name(); // Fallback: rex::getServerName()
$company->url(); // Fallback: Domain-URL
$company->description(); // aktuelle Sprache, Fallback Startsprache
$company->logoUrl(); // absolut (Medienpool-Datei oder http(s)-URL)
$company->get('email'); // beliebiges Feld: company.{domainId}.email
$company->get('city');
$company->socialLinks(); // list<SocialLink> — nur Netzwerke mit hinterlegter URL
$company->socials(); // list<string> der URLs (schema.org sameAs)
CompanyProfile::domains(); // list<rex_yrewrite_domain> (ohne "default")Weitere Felder über get(): legal_name, founding_date, vat_id, register, managing_directors, phone, street, zip, country, lat, lng.
Eingebaut: facebook, instagram, linkedin, pinterest, threads, tiktok, vimeo, whatsapp, x, xing, youtube. Eigene registrieren/entfernen (in project/boot.php):
CompanyProfile::registerSocialNetwork('mastodon', 'Mastodon', icon: 'mastodon');
CompanyProfile::unregisterSocialNetwork('pinterest');icon ist ein reiner Name (Default = Key), aufgelöst über die IconRegistry des Projekts — Roadie ist icon-library-agnostisch. socialLinks() liefert SocialLink-Value-Objects (->network, ->url); SocialNetwork ist ein readonly Value Object (->key, ->label, ->icon), kein Enum.
use Yakamara\Roadie\Company\CompanyJsonLd;
echo CompanyJsonLd::script(CompanyProfile::current(), 'TravelAgency', $domainUrl . '#organization');Baut ein Organization-Node (oder Subtyp, z. B. TravelAgency): name, legalName, url, logo, description, vatID, address (PostalAddress), geo (GeoCoordinates), contactPoint (Telefon/E-Mail), sameAs (Socials). Gibt null zurück, wenn kein Name gesetzt ist.
Verwaiste Daten gelöschter Domains räumt der Command
roadie:company:cleanupauf.
Generische schema.org-JSON-LD-Engine. Roadie emittiert auf YREWRITE_SEO_TAGS (nur Frontend) automatisch die Seiten-Grundtypen; Projekte steuern per Resolver bei, welche Seite als Artikel ausgezeichnet wird.
- WebSite — Anker-Node auf jeder Seite (
@id{domainUrl}#website), Name aus CompanyProfile. - WebPage (Default) oder ContactPage / AboutPage — Subtyp, wenn der aktuelle Artikel der in den Unternehmensdaten hinterlegten Kontakt-/Über-uns-Seite entspricht.
- Organization — aus den Unternehmensdaten.
- Article /
BlogPosting/NewsArticle— nur wenn ein Resolver das anmeldet (die WebPage wird dann permainEntityverknüpft). - FAQPage wird hier nicht automatisch ausgegeben — Consumer emittieren sie inline via
PageSchema::faqPage().
use Yakamara\Roadie\Seo\PageSchemaRegistry;
PageSchemaRegistry::register(static function (rex_article $article): ?array {
if ('' === $article->getValue('art_author')) {
return null; // an nächsten Resolver / Defaults weiterreichen
}
return [
'type' => 'Article', // Article | BlogPosting | NewsArticle
'data' => [
'headline' => $article->getName(),
'authorName' => $article->getValue('art_author'),
],
];
});Rückgabe des Resolvers (callable(rex_article): ?array):
['type' => 'Article', 'data' => [...]]→ Artikel-Node + WebPage-mainEntity.['type' => 'skip']→ unterdrückt jede weitere Ausgabe für diese Seite (z. B. wenn die Seite ihren eigenen Haupt-Node liefert).null→ weiter zum nächsten Resolver. Resolver laufen in Registrierungsreihenfolge, der erste Nicht-Null-Treffer gewinnt.
Für Inline-Ausgabe (z. B. FAQ):
use Yakamara\Roadie\Seo\PageSchema;
echo PageSchema::script(PageSchema::faqPage([
['question' => 'Wann ist die beste Reisezeit?', 'answer' => 'Von Mai bis September.'],
]) ?? []);Weitere Builder: webPage(string $type, array $data), article(array $data), webSite(array $data), script(array $node). @id-Konvention: Organization #organization, WebSite #website, Seite #webpage, Artikel #article.
Optionaler 301-Redirect von Frontend-URLs mit Großbuchstaben auf Kleinschreibung (gegen Duplicate Content), yrewrite-unabhängig. Opt-in in project/boot.php:
use Yakamara\Roadie\Seo\LowercaseUrlRedirect;
LowercaseUrlRedirect::enable();
LowercaseUrlRedirect::addExcludedPrefix('/downloads/'); // zusätzlich zu den DefaultsNur Frontend-GET/HEAD. Vorausgeschlossen (Groß-/Kleinschreibung bleibt erhalten): /media/, /mediatypes/, /images/, /imagetypes/, /assets/, /redaxo/. Der Query-String bleibt erhalten.
Neben den serverseitig gerenderten PHP-Komponenten bringt Roadie ein leichtes clientseitiges Komponenten-Framework mit (src/scripts/), das Verhalten per data-component an Markup bindet.
<div data-component="dropdown" data-dropdown-placement="bottom">
<button data-target="dropdown.trigger" data-action="click:toggle">Menü</button>
<div data-target="dropdown.panel"> … </div>
</div>data-component="name"— Wurzel (mehrere per Leerzeichen getrennt)data-target="name.role"— benannte Kind-Elementedata-action="event:method"— delegiertes Event → Methodenaufrufdata-{name}-{option}="wert"— Optionen (werden zuoptsgemappt, kebab→camel)
import Component from '../core/Component.js';
export default class Dropdown extends Component {
static componentName = 'dropdown';
static defaults = { placement: 'bottom' };
init() {
this.on(this.target('trigger'), 'click', () => this.toggle());
}
toggle() {
this.target('panel').classList.toggle('is-open');
this.emit('toggled');
}
}Basisklasse Component (default export): init()/destroy()-Lifecycle, target(name)/targets(name), data(key), on(el, event, handler) (mit Auto-Cleanup) und emit(event, detail). Der Command roadie:generate-component-imports findet Klassen mit static componentName automatisch und registriert sie — es gibt keine handgepflegte Entry-Datei. Komponenten-lokale Skripte liegen beim jeweiligen PHP-Bauteil (z. B. lib/Component/Video/scripts/Video.js).
-
ComponentRegistry (
src/scripts/core/ComponentRegistry.js) initialisiert alle[data-component](initAll()), verdrahtet delegierte Events (setupDelegation()) und beobachtet DOM-Änderungen für Auto-Init/-Destroy (observeDOM()). Mehrere Komponenten pro Element sind möglich. -
EventBus (Singleton,
src/scripts/core/EventBus.js) — Pub/Sub zwischen Komponenten ohne DOM-Verwandtschaft:const off = EventBus.on('cart:updated', (data) => { /* … */ }); // gibt Unsubscribe zurück EventBus.emit('cart:updated', { count: 3 });
Für Kommunikation innerhalb eines DOM-Teilbaums lieber
Component.emit()verwenden.
Erzeugt eindeutige, ARIA-konforme IDs für id/aria-labelledby-Verknüpfungen:
use Yakamara\Roadie\Util\Aria;
$id = Aria::id(); // z. B. "g4a3f7b2c" (beginnt immer mit einem Buchstaben)
$id = Aria::lastId(); // Letzte generierte ID wiederverwendenSetzt die PHP-Locale anhand der REDAXO-Spracheinstellungen (clang_locale, clang_setlocale):
use Yakamara\Roadie\Util\Locale;
Locale::setDefault();Prüft Dateitypen anhand von Extension und MIME-Typ:
use Yakamara\Roadie\Util\FileTypeDetector;
$detector = new FileTypeDetector('/pfad/zur/datei.jpg');
$detector->exists();
$detector->isRasterImage(); // avif, bmp, gif, jpeg, jpg, png, webp
$detector->isSvg();
$detector->isAudio(); // aac, flac, mp3, ogg, wav, webm
$detector->isVideo(); // avi, mov, mp4, mpeg, ogg, webm
$detector->getMimeType();Hilfstrait für BackedEnum-Klassen:
use Yakamara\Roadie\Trait\EnumToArrayTrait;
enum MyEnum: string {
use EnumToArrayTrait;
case Foo = 'foo';
case Bar = 'bar';
}
MyEnum::values(); // ['foo', 'bar']
MyEnum::names(); // ['Foo', 'Bar']
MyEnum::arrayByValues(); // ['foo' => 'Foo', 'bar' => 'Bar'] (value => name)
MyEnum::arrayByNames(); // ['Foo' => 'foo', 'Bar' => 'bar'] (name => value)