Theme-Entwicklung (Referenz)
Auf dieser Seite
English (summary)
A theme is a directory CMS/themes/<slug>/ with at least style.css (header comment) and index.php or functions.php; theme.json declares templates, partials, menu locations and customizer sections, update.json the version metadata. ThemeManager::render($template, $data) includes header.php, <template>.php (fallback index.php) and footer.php; templates receive their data as local variables. Themes must be CSP-compatible (nonce on every inline <script>/<style>, no inline handlers), fire head and body_end, escape all output and respect the privacy settings (local fonts, external consent banner).
Deutsch
1. Mindestaufbau
CMS/themes/mein-theme/
├── style.css # Pflicht: Header-Kommentar (Theme Name, Version, Author, Description)
├── functions.php # Theme-Bootstrap (Hooks, Helfer, Assets) – oder index.php
├── theme.json # Metadaten, Templates, Partials, Menüpositionen, Customizer-Bereiche
├── update.json # Version, min_cms_version, Changelog (für Marketplace/Updates)
├── header.php footer.php
├── index.php # Fallback für alle Templates
├── home.php page.php blog.php blog-single.php category.php tag.php
├── author.php archive.php search.php contact.php 404.php error.php
├── login.php register.php forgot-password.php # nur für Auth-Modus „legacy“
├── partials/ # wiederverwendbare Bausteine
├── includes/ # PHP-Helfer
├── js/ … # Theme-Skripte (extern, CSP)
├── admin/customizer.php (+ admin/customizer/) # optional: eigener Customizer
└── member/<seite>.php # optional: Overrides des Mitgliederbereichs
Health-Check beim Aktivieren (ThemeManager::healthCheckTheme()): style.css vorhanden, index.php oder functions.php vorhanden, alle PHP-Dateien syntaktisch gültig.
2. theme.json
{
"name": "Mein Theme",
"slug": "mein-theme",
"version": "1.0.0",
"author": "Firma",
"description": "…",
"tags": ["blog", "responsive"],
"supports": ["custom-logo", "sticky-header", "custom-css", "categories"],
"menus": { "primary": "Hauptmenü", "footer": "Footer" },
"templates": { "home": "home.php", "page": "page.php", "blog-single": "blog-single.php", "404": "404.php" },
"partials": { "header": "header.php", "footer": "footer.php", "sidebar": "partials/sidebar.php" },
"customization": { "colors": { … } },
"post_templates": [
{ "id": "default", "label": "Standard", "description": "", "file": "blog-single.php", "meta_fields": [] }
]
}
menusregistriert Menüpositionen (zusätzlich Filterregister_menu_locations).post_templates(optional) bietet im Beitragseditor eine Template-Auswahl mit eigenen Metafeldern an; ohne Angabe gibt es nur „Standard“.cms-defaultdefiniert keine Beitrags-Templates.
3. Rendering-Vertrag
- Rahmen:
ThemeManager::render()bindetheader.php, das Template undfooter.phpein und zählt den Seitenaufruf (TrackingService). Templates rufen den Rahmen nicht selbst auf; nurerror.php(vom Fatal-Handler direkt eingebunden) rendert ihn selbst. - Template-Hierarchie:
<template>.php→index.php. Der Filtertemplate_namekann umleiten. Fürauthorsundsitemap(Routen/autoren,/sitemap) greift ohne eigenes Template der Fallback. - Daten je Template (als Variablen verfügbar):
| Template | Variablen |
|---|---|
home |
– (Startseite liest selbst: Customizer-Modus posts oder landing) |
page |
$page (Array, content = gerendertes, bereinigtes HTML; auch Hub-Sites), $contentLocale |
blog |
$posts (Objekte), $total, $currentPage, $totalPages, $perPage (Paginierung ?p=N) |
blog-single |
$post (Objekt), $contentLocale |
category |
$category (Array), $posts, $query, $total, $currentPage, $totalPages, $perPage; Übersicht /kategorie mit $overviewItems |
tag |
$tag (name, slug, slug_de, slug_en), $posts, $query, … ; Übersicht /tag mit $overviewItems |
author |
$author, $posts, $total, $currentPage, $totalPages, $perPage |
search |
$results (Arrays mit _type, _type_label, slug, title, meta_description; Plugin-Treffer zusätzlich url, optional date), $query, $type, $sort (relevance/date), $total, $location, $filter |
contact, 404 |
– |
login, register, forgot-password |
nur im Auth-Modus legacy (../admin/themes-design/CMS-LOGINPAGE.md) |
- Suche: Treffer sind bereits gefiltert (jedes Suchwort im sichtbaren Text) und sortiert; ohne Suchbegriff keine Treffer. Formulare senden
q,type,sort. - Links: Beitrags-URLs mit
PermalinkService::buildPostUrl(), Archive mitcms_get_archive_url(), lokalisierte Pfade mitContentLocalizationService::buildLocalizedPath(). - Es gibt keine Klassen
\CMS\CMS,PageService,PostService,ContentHelperund keine MethodeThemeManager::registerMenuLocation().
4. Hooks im Theme
| Pflicht/Empfehlung | Hook | Wo |
|---|---|---|
| Pflicht | Hooks::doAction('head') |
im <head> von header.php (SEO-Meta, Consent, Analytics, Styles) |
| Pflicht | Hooks::doAction('body_end') |
vor </body> in footer.php (Consent-Skript, Web Vitals, PhotoSwipe) |
| Empfohlen | Hooks::applyFilters('page_title', $titel) |
<title> |
| Optional | before_header, after_header, before_footer, after_footer |
löst der Core aus |
before_footer und body_end laufen je Request nur einmal. Eigene Theme-Aktionen meldet man in functions.php an, z. B. Hooks::addAction('head', [$this, 'enqueueStyles'], 10).
5. Sicherheit & CSP
header.phpgibtcms_csp_runtime_tags()als erstes Skript aus (der Core ergänzt es zur Sicherheit als erstehead-Aktion).- Jedes Inline-
<style>/<script>trägtSecurity::instance()->nonceAttr(). Inline-Eventhandler (onclick=),javascript:-URLs,eval()und Fremdskripte ohneSecurity::allowCspSources()werden blockiert;style-Attribute schreibt der Core in nonce-geschützte Klassen um. - Alle Ausgaben escapen (
htmlspecialchars(..., ENT_QUOTES, 'UTF-8'),esc_html(),esc_url(),esc_attr());$page['content']/$post-Inhalt ist bereits bereinigtes HTML. - Formulare (Kontakt, Newsletter, Favoriten) brauchen ein CSRF-Token: öffentliche POSTs prüft der Router global mit der Aktion
form_guard(Security::instance()->generateToken('form_guard')), Logout mitlogout. - Keine Zugangsdaten oder Deployment-Daten im Theme-Verzeichnis.
6. Datenschutz
- Bei
privacy_use_local_fonts = 1keine Google-Fonts laden; lokale Schriften kommen aus der Schriftverwaltung (../admin/themes-design/FONTS.md). - Eigene Cookie-Banner nur ausgeben, wenn
CookieConsentService::getInstance()->isManagedExternally()false ist. - Externe Einbettungen (YouTube, Karten) nur nach
hasConsentForService().
7. Customizer, Menüs, Mitgliederbereich
- Customizer:
admin/customizer.phpwird unter/admin/theme-editoreingebunden (../admin/themes-design/CUSTOMIZER.md). Werte überThemeCustomizer::instance()->get($bereich, $schluessel, $standard)lesen, als CSS-Variablen ausgeben. - Menüs:
ThemeManager::instance()->getMenu('primary')liefert die Einträge (title,url,target,icon,parent_id). - Mitgliederbereich:
member/<seite>.phpim Theme überschreibt die Core-Seite;member/dashboard.phpaktiviert zusätzlich die Route/dashboard.
8. Versionierung & Auslieferung
theme.json,update.json(min_cms_version≥3.4.00),style.cssund die Versionskonstante infunctions.php(beicms-default:MERIDIAN_THEME_VERSION) tragen dieselbe Version.- Auslieferung als ZIP mit dem Theme-Ordner auf oberster Ebene; für den Marketplace mit SHA-256-Prüfsumme (../admin/themes-design/MARKETPLACE.md).
- Ein statischer Vertragsprüfer (
TESTS/theme-contract/run.php) wird in früheren Release-Notizen erwähnt, ist in diesem Repository aber nicht enthalten.
Verwandte Dokumente
README.md · DEVELOPMENT.md · COMPONENTS.md · DESIGN-SYSTEM.md · JAVASCRIPT.md · ../core/HOOKS-REFERENCE.md