Skip to content

Repository files navigation

Roadie Addon

Version REDAXO PHP WebAwesome Yarn

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.


Inhaltsverzeichnis


Setup

Voraussetzungen

  • 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.

Yarn Workspaces

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.

Installation

yarn install

Scripts

yarn 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 aufgerufen

npm-Alternativen:

npm run watch
npm run dev-server
npm run build

watch und dev-server rufen roadie:generate-component-imports automatisch vor dem Build auf.


webpack.config.js

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 via svg-sprite-loader (inline <use>)
  • Webpack-Warning-Filter für WebAwesome Dynamic Imports (bekanntes False-Positive beim statischen Analysieren des WA-Autoloaders)

assets/app.js (Frontend-Entrypoint)

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.


assets/backend.js (Backend-Entrypoint)

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.


project/boot.php — Checkliste

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);
    }
}

Asset Management

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".

Template-Integration

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.

HeadAssets — deferred <head>-Assets

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/null werden ü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-System

Aufbau

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'));

Komponenten verwenden

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'),
);

HTML-Attribute

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,
);

Slots

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',
    ),
);

Html-Closure

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);
    }
});

Verfügbare Komponenten

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

Combobox vs. Autocomplete: Combobox bildet 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ändige Autocomplete-Komponente (siehe unten).

Autocomplete-Komponente

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).


Image-Komponente

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 den med_*-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'),
);

Setup: MediaManager-Effect und .htaccess

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, JPEG
  • datei.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:

  1. roadie_responsivemuss an erster Stelle stehen
  2. focuspoint_fit (Breite + Höhe, Zoom nach Bedarf) — oder resize wenn kein Zuschnitt nötig
  3. image_format (optional, für AVIF/WebP-Konvertierung)

Externe Bildquellen

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).


Responsive Breiten

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],
]);

Breakpoint-Konfiguration

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.


sizes-Attribut

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() und from() können nicht gemischt werden — bei Verstoß wird eine InvalidArgumentException geworfen. 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 und Platzhalter

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);
}

Art Direction (Motifs)

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 ImageMotif bekommt eine eigene ImageSource (source). Für Medienpool-Bilder verwendet man typischerweise denselben Dateinamen mit einem anderen mediaManagerType (anderer Zuschnitt/Seitenverhältnis pro Breakpoint).

Art Direction erfordert Breitendeskriptoren im srcset (200w) — Pixeldichte-Deskriptoren (2x) werden nicht unterstützt.


Bildformate

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 und Copyright

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ücken

JSON-LD: copyrightNotice, creator, creditText und aiGenerated werden zusätzlich als <script type="application/ld+json"> mit Schema.org-Markup (ImageObject) ausgegeben.


Loading, Decoding und Fetch Priority

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: High und loading: Lazy widersprechen sich — die Kombination wirft eine InvalidArgumentException.


Video-Komponente

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

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.

Quellen

  • MediaPoolVideoSource(string $fileName, string $posterMediaManagerType = '16by9') — Video aus dem Medienpool. Poster-Bild und WebVTT-Untertitel kommen aus den med_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 via YouTubeVideoSource::fromUrl($url, $title, $poster) (erkennt watch-/youtu.be-/embed-/shorts-URLs, gibt null bei ungültiger URL). Das Poster muss eine lokale ImageSource sein (kein i.ytimg.com vor Einwilligung).

Datenschutz-Link (Zwei-Klick)

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.


Icon-System

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.


Konfiguration (in project/boot.php)

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'));  // Optional

Manifest erstellen

Das 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-manifest

SVGs 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.


Aliase

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öst

IconLibrary (Empfehlung für Project-Addons)

Zentrale 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();

Icon-Komponente

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.


IconPicker (Redakteursauswahl im Backend)

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 & Layout

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 setzen

Section-Varianten

Registrierung 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
}

Slice Management

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)

Modul-Input

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>

Modul-Output

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);

Navigation

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.

Renderer registrieren (in project/boot.php)

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.


TocNavigation

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.


InPageNavigation

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();
}

Eigenen Renderer implementieren

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>';
    }
}

Media Pool Erweiterungen

Roadie erweitert den REDAXO Media Pool um Metadaten-Felder für Barrierefreiheit, Urheberrecht und KI-Kennzeichnung.

Metadaten-Felder (Backend)

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.

Media-Klasse

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 Effekt

ArticleKey

Ermö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.

Enum definieren (im Project-Addon)

namespace Yakamara\Project\Article;

enum ArticleKey: string
{
    case Imprint  = 'imprint';
    case Team     = 'team';
    case Contact  = 'contact';
}

Registrieren (in project/boot.php)

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).

Artikel auflösen

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.

Artikel-Keys setzen / entfernen

ArticleResolver::set(ArticleKey::Imprint, articleId: 42);
ArticleResolver::remove(ArticleKey::Imprint);

Backend-UI

Die Pflegeseite ist im Backend unter System → Roadie → Artikel-Keys erreichbar. Alle registrierten Enums werden dort mit einem Linkmap-Feld angezeigt.

Console Command

php bin/console roadie:article-key list
php bin/console roadie:article-key set imprint 42
php bin/console roadie:article-key remove imprint

Sind mehrere Enums mit gleichem Key-Wert registriert, wird ein Fehler mit den betroffenen Namespaces ausgegeben.

Löschschutz

Solange ein Artikel einem Key zugewiesen ist, kann er nicht gelöscht werden. Das gilt auch für das Löschen der übergeordneten Kategorie.


ArticleUsage – Löschschutz

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 link1link10 und linklist1linklist10 in rex_article_slice
MetaInfoUsageChecker REX_LINK_WIDGET- und REX_LINKLIST_WIDGET-Felder in rex_article
ArticleKeyUsageChecker Alle via ArticleKeyRegistry registrierten Enum-Keys

Eigene Checker registrieren

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.


Backend-Widgets

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'));
}

IconPicker

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-Markup

ColorPicker

Ermö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); }

LayoutPicker

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; }.


Live-Vorschau

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).

Funktionen

  • Andockbares Panel rechts; schiebt den Editor-Inhalt per CSS zur Seite (kein Overlay). Der Zustand (offen, Device, Metadaten ausgeblendet) wird client-seitig in localStorage gehalten 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).

Slice-Zuordnung (Scroll-Sync)

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.


Console Commands

roadie:generate-icon-manifest

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-manifest

Voraussetzung: 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 entfernt
  • width/height-Attribute am <svg>-Tag werden entfernt (Sizing via CSS)
  • fill="*" wird auf fill="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").


roadie:generate-component-imports

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-imports

Wird automatisch aufgerufen vor jedem Build via package.json-Scripts (watch, build, dev-server). Die generierte Datei nicht manuell bearbeiten.

Gescannte Verzeichnisse:

  • src/templates/ — REDAXO-Templates
  • src/modules/ — REDAXO-Module
  • src/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.


roadie:article-key

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 imprint

Sind mehrere Enums mit gleichem Key-Wert registriert, wird ein Fehler mit den betroffenen Namespaces ausgegeben. Weitere Details unter ArticleKey.


roadie:media-transparency:scan

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.


roadie:company:cleanup

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öschen

Gibt pro betroffener Domain die Anzahl entfernter Keys aus. Betrifft die Unternehmensdaten.


HTML-Helfer

Namespace Yakamara\Roadie\Html — reine PHP-Helfer ohne Template-Dateien.

HtmlList

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;

<ul> und <ol>

// 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
]);

<dl>

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.


Filter-System

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.

Klassen-Übersicht

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

FilterOption

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']);

ChoiceFilter

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 (nicht wa-checkbox), da wa-checkbox ein nicht-standardkonformes Verhalten beim Setzen von value hat.

RangeFilter / TextFilter

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');

FilterInput — Moduleingabe

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-Generierung — Modulausgabe

// 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=5000

buildUrl() iteriert über alle FilterDefinition-Instanzen und ruft deren buildQueryParams() auf. Leere/nicht gesetzte Filter werden übersprungen.


Unternehmensdaten

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}.*).

Backend-Seite „Strukturierte Daten"

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

CompanyProfile — Zugriff im Code

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.

Social-Netzwerke

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.

Organization-JSON-LD

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:cleanup auf.


SEO und schema.org

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.

Was automatisch ausgegeben 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 per mainEntity verknüpft).
  • FAQPage wird hier nicht automatisch ausgegeben — Consumer emittieren sie inline via PageSchema::faqPage().

Resolver registrieren (in project/boot.php)

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.

PageSchema — Node-Builder

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.

LowercaseUrlRedirect

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 Defaults

Nur Frontend-GET/HEAD. Vorausgeschlossen (Groß-/Kleinschreibung bleibt erhalten): /media/, /mediatypes/, /images/, /imagetypes/, /assets/, /redaxo/. Der Query-String bleibt erhalten.


Frontend-JS-Komponenten

Neben den serverseitig gerenderten PHP-Komponenten bringt Roadie ein leichtes clientseitiges Komponenten-Framework mit (src/scripts/), das Verhalten per data-component an Markup bindet.

HTML-Kontrakt

<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-Elemente
  • data-action="event:method" — delegiertes Event → Methodenaufruf
  • data-{name}-{option}="wert" — Optionen (werden zu opts gemappt, kebab→camel)

Eigene Komponente

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).

Registry & EventBus

  • 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.


Utilities

Aria

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 wiederverwenden

Locale

Setzt die PHP-Locale anhand der REDAXO-Spracheinstellungen (clang_locale, clang_setlocale):

use Yakamara\Roadie\Util\Locale;

Locale::setDefault();

FileTypeDetector

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();

EnumToArrayTrait

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)

About

Roadie ist das Frontend-Framework-AddOn für REDAXO. Es liefert ein Komponenten-System, Asset-Management, Icon-System, Backend-Widgets und Utilities — und baut auf WebAwesome als Web-Component-Bibliothek auf.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages