1 Grundlagen der Template-Programmierung
Was ein Template ist
Ein Template ist ein Textdokument – meist HTML –, in dem CMS-Befehle stehen. Die Befehle fragen Datensätze ab, setzen Inhalte ein, binden andere Templates ein, werten Formulare aus und steuern den Ablauf. Dazwischen darf PHP-Code stehen.
(CMS:INCLUDE header)
<h1>(CMS:TEXT title)</h1>
<div>(CMS:TEXT text)</div>
<div>(CMS:MEDIA field=bild_klein attr[id]=bild1)</div>
<div>erstellt: (CMS:DATE created_at format="dd.MM.y")</div>
(CMS:INCLUDE footer)
Beim Speichern übersetzt camindo CMS das Template in PHP. Zur Laufzeit wird nur noch dieses PHP ausgeführt – es braucht keine Template-Engine, die den Text bei jedem Aufruf neu auswertet.
Wie eine Seite entsteht
Ruft ein Besucher eine Adresse auf, ermittelt camindo CMS:
- die Instanz – aus der Domain,
- die Sprache – aus dem Sprachkürzel der Adresse, sonst aus der Spracheinstellung des Browsers, sonst die Hauptsprache,
- den Datensatz – aus dem Slug oder der Nummer (
record_id) in der Adresse; ohne Angabe den Start-Datensatz der Instanz, - das Template – aus der Adresse, aus dem Datensatz oder das Standard-Template seines Inhaltsbereichs.
Die Inhalte eines aufgerufenen Datensatzes stehen im Template zur Verfügung. Standardmäßig
beziehen sich alle Ausgabe-Befehle auf diesen Datensatz – vgl. oben (CMS:TEXT title). Führt
das Template eine Abfrage mittels CMS:SELECT durch, stehen die Inhalte der ermittelten
Datensätze in gleicher Weise im Ergebnis-Template zur Verfügung; siehe unten Listen, Select, Ergebnisse.
Adressen
[/{sprache}]/{slug oder nummer}[,{template}]
| Adresse | Bedeutung |
|---|---|
/ |
Startseite der Instanz |
/betriebsfeier-2026 |
Datensatz mit diesem Slug, Standard-Template |
/termine/betriebsfeier-2026 |
Datensatz mit dem Slug betriebsfeier-2026 im Bereich mit dem Slug-Prefix termine |
/termine |
Übersicht des Bereichs mit dem Slug-Prefix termine – sein Übersichts-Template, ohne Datensatz |
/1758 |
Datensatz Nummer 1758 |
/en/summer-party-2026 |
englische Sprachversion |
/betriebsfeier-2026,druck |
Datensatz mit dem Template druck – nur wenn es für den Bereich als URL-Override freigegeben ist |
/0,suche |
Template suche ohne Datensatz – nur wenn es als „Standalone“ markiert ist |
Reine Ziffernfolgen sind immer Datensatznummern, alles andere ist ein Slug. Ist der erste Teil der Adresse der Slug-Prefix eines Inhaltsbereichs, wird der Rest nur in diesem Bereich gesucht; sonst der ganze Pfad in den Bereichen ohne Präfix. Das Sprachkürzel steht in der Adresse, sobald die Instanz mehr als eine Sprache führt. Adressen erzeugen Sie mit CMS:URL – nie von Hand, dann stimmen Sprache, Slug und Domain immer.
Besteht die Adresse nur aus dem Sprachkürzel (/en, ohne Schrägstrich am Ende),
leitet camindo CMS auf die Startseite der Sprache um (/en/ oder /en_US/ – je
nachdem, wie diese eingerichtet ist). Führt der Auftritt nur eine Sprache und hat ein
Datensatz genau diesen Slug, wird stattdessen der Datensatz gezeigt. Adressen, unter
denen es nichts gibt – unbekannter Datensatz, unbekannte Sprache, nicht freigegebenes
Template –, antworten mit dem Status 404.
Syntax der CMS-Befehle
(CMS:BEFEHL wert parameter=wert parameter[schlüssel]="wert" …)
- Befehlsnamen und Parameternamen sind unabhängig von Groß- und Kleinschreibung.
- Parameter sind durch Leerzeichen getrennt; ein Befehl darf über mehrere Zeilen gehen.
- Die Reihenfolge, in der Parameter verarbeitet werden, bestimmt jeder Befehl selbst, nicht die Reihenfolge, in der die Parameter geschrieben werden.
- Der erste Wert ohne
=gilt als Hauptparameter des Befehls:(CMS:TEXT titel)ist dasselbe wie(CMS:TEXT field=titel). - Werte, die Leerzeichen enthalten, stehen in Anführungszeichen, einfache oder doppelte:
(CMS:TEXT field="title")entspricht(CMS:TEXT field='title'). - Unterschlüssel in eckigen Klammern:
filter[area]=artikel. Leere Klammern hängen an eine Liste an:sort[]=datum sort[]=titel. Wird derselbe Unterschlüssel mehrfach angegeben, entsteht ebenfalls eine Liste ([]ist im Gegensatz zur PHP-Syntax für Arrays optional). - Statt
=darf ein Vergleichsoperator zwischen Parameter und Wert stehen –==,!=,!,!!,<,>,<=,>=,<<,>>:filter[preis] >= 100ist dasselbe wiefilter[preis]=">=100". Der Wert gilt dann unverändert, ein Operator an seinem Anfang wird nicht mehr ausgewertet:filter[code] < ">2"vergleicht mit dem Text „>2“. Bei=bleibt es wie gehabt –filter[code] = ">2"bedeutet „größer als 2“ (siehe Filter-Syntax). - Der Zeilenumbruch hinter einem CMS-Befehl wird geschluckt, analog zu einem
<?php … ?>-Block. - Befehlsparameter in doppelten Anführungszeichen dürfen PHP-Variablen enthalten:
attr[alt]="{$myvar}"(Parameter ohne Anführungszeichen werden intern wie Parameter mit doppelten Anführungszeichen behandelt).
Fehler – ein unbekannter Befehl, ein unbekannter Parameter, eine fehlende Klammer – verhindern das Speichern nicht. Die Detailansicht des Templates zeigt sie mit Zeilennummer an. Sie erscheinen auch auf der Startseite im Widget „Template-Meldungen“ – für den Fall, dass Templates über das Dateisystem bearbeitet werden.
CMS-Befehl und PHP-Code kombinieren
$set <?php … ?>-Blöcke dürfen frei im Template stehen, CMS-Befehle innerhalb von PHP-Blöcken
führen allerdings zu Fehlern! Um Inhalte aus CMS-Befehlen in PHP-Blöcken zu verwenden,
nutzen Sie den Parameter set (etwa set = myvar) und greifen den Inhalt im PHP-Block aus $set
($set['myvar']) auf.
$cms Inhalte aus dem aufgerufenen Datensatz stehen in $cms; ebenso Inhalte aus Datensätzen, die mittels
CMS:SELECT ermittelt werden.
Für die Ausgabe sollten immer die CMS-Ausgabebefehle (etwa
CMS:TEXT) verwendet werden. Diese Befehle kümmern sich um korrekte Formatierung, ein<?php echo $cms[…] ?>um nichts davon. Verwenden Sie$cmsnur dort, wo ein Befehl einen Wert als Parameter braucht, etwafilter[kategorie]="{$cms['record_id']}".
Gemeinsame Parameter der Ausgabebefehle
| Parameter | Wirkung |
|---|---|
ifempty |
Ersatztext, wenn der Wert leer ist |
instead |
Ersatztext, wenn der Wert nicht leer ist |
prefix, suffix |
Text davor bzw. dahinter – nur wenn der Wert nicht leer ist |
set |
den Wert nicht ausgeben, sondern in $set[…] ablegen |
id |
aus einem anderen Datensatz lesen (Nummer oder Slug), z. B. (CMS:TEXT titel id=impressum) – nur veröffentlichte Datensätze, wie in der URL |
replace |
Teiltexte ersetzen (replace="alt?neu") – bei TEXT, NUMBER, DATE, DATETIME, TIME |
(CMS:TEXT untertitel prefix="<h2>" suffix="</h2>")
(CMS:NUMBER preis format="%.2f" decimal="," suffix=" €" ifempty="auf Anfrage")
Werte in Attributen und Javascript
camindo CMS entfernt aus allen Datensatz-Inhalten Script, Event-Handler (onclick …),
iframe, object, embed und javascript:-Links – beim Speichern und noch einmal bei
der Ausgabe. HTML-Formatierung bleibt, deshalb gibt CMS:TEXT Inhalte unmaskiert aus.
Setzen Sie einen Wert in ein HTML-Attribut ein, maskieren Sie ihn mit specialchars=true
– ein Anführungszeichen im Inhalt beendete sonst das Attribut:
<img src="…" alt="(CMS:TEXT title specialchars=true)">
<a href="(CMS:URL)" title="(CMS:TEXT teaser specialchars=true striptags=true lbrconvert=false)">…</a>
Dasselbe gilt für Formulareingaben, die Sie nach dem Absenden wieder ins Feld setzen:
<input name="data[nickname]" value="(CMS:TEXT useval="{$_POST['data']['nickname']}" specialchars=true)">
Befehle, die selbst maskieren: Einige Befehle maskieren ihre Ausgabe bereits mit
htmlspecialchars. Dort entfällt specialchars – ein bereits maskierter Wert würde
doppelt maskiert, aus & würde sichtbar &:
| Befehl | maskiert |
|---|---|
| CMS:MEDIA | alle Attribute des <img>-Tags: src, alt, width, height und attr[…] – nicht die Einzelwerte über subfield |
| CMS:USER_TOKEN | den Wert des versteckten Feldes |
| CMS:USER_PASSWORD_RULES | die gesamte Ausgabe (JSON für ein Attribut) |
| CMS:XML | version und encoding |
| CMS:JUMP_LIST, CMS:JUMP_PREV, CMS:JUMP_NEXT | die URL in {href} |
Was Sie einem dieser Befehle übergeben, muss deshalb unmaskierter Text sein – ohne HTML-Tags und Entities:
(CMS:TEXT title striptags=true ampconvert=false lbrconvert=false set=title)
(CMS:MEDIA bild attr[alt]="{$set['title']}")
Nie maskiert werden prefix, suffix, ifempty und instead – sie stammen aus
dem Template und dürfen HTML enthalten. specialchars kennen nur
CMS:TEXT und CMS:SNIPPET. Werte anderer Befehle, etwa
CMS:MEDIA … subfield=filename, CMS:RELATION oder CMS:RAW,
legen Sie für ein Attribut mit set ab und geben sie über CMS:TEXT maskiert aus:
(CMS:MEDIA dokument subfield=filename set=dateiname)
<a href="(CMS:MEDIA dokument subfield=path)" title="(CMS:TEXT useval="{$set['dateiname']}" specialchars=true lbrconvert=false)">…</a>
CMS:URL und CMS:MEDIA … subfield=path erzeugen nur URL-taugliche Zeichen, ebenso
CMS:TEXT … urlencode=true – sie können ohne Maskierung in href und src stehen.
Braucht Javascript einen Wert, geben Sie ihn als data--Attribut aus und lesen ihn
dort – so steht nie Inhalt im Script-Code selbst:
<div id="produkt" data-title="(CMS:TEXT title specialchars=true)"></div>
<script>var title = document.getElementById('produkt').dataset.title;</script>
Listen: SELECT und die Ergebnisschleife
Listen entstehen immer aus zwei Templates: Das aufrufende Template fragt mit CMS:SELECT ab und nennt ein Listentemplate. Das Listentemplate durchläuft die Treffer zwischen CMS:RESULT_START und CMS:RESULT_END.
Aufrufendes Template:
<h2>Aktuelle Meldungen</h2>
(CMS:SELECT meldungen_liste
filter[area]=meldungen
sort=datum
order=desc
limit=10
noresult="<p>Keine Meldungen.</p>"
)
Listentemplate meldungen_liste:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT titel)</a> – (CMS:DATE datum)</li>
(CMS:RESULT_END)
</ul>
Innerhalb der Schleife ist die jeweilige Zeile der laufende Datensatz. Ein
Listentemplate darf selbst wieder CMS:SELECT enthalten – etwa die Schlagwörter
jedes Artikels.
Blätterfunktion: count=true zählt alle Treffer; CMS:JUMP_LIST,
CMS:JUMP_PREV und CMS:JUMP_NEXT erzeugen daraus die
Navigation.
Veröffentlichung: Templates finden nur veröffentlichte Datensätze –
freigeschaltet, Veröffentlichungsdatum erreicht, Ablaufdatum nicht erreicht. Die
Parameter future, archive lockern das je Abfrage, etwa für ein Archiv
oder eine Terminvorschau.
Der Parameter
stateist ausschließlich für Backend-interne Ansichten vorgesehen.
Bereiche ansprechen
Inhaltsbereiche werden über ihren Namen oder über ihre Nummer angesprochen (area_id):
filter[area]=meldungen, filter[area]=12 oder filter[area_id]=12. Mehrere Bereiche
trennen Sie durch Komma. Weil Feldnamen systemweit einheitlich sind, lassen sich mehrere
Bereiche gemeinsam filtern und sortieren:
(CMS:SELECT zeitleiste
filter[area]=meldungen,termine,blog
filter[datum] >= 2026-01-01
sort=datum
order=desc
)
sort=area sortiert nach Bereich; _default steht in Filtern und Sortierung für das
Hauptfeld jedes Bereichs, egal wie es dort heißt.
Übersichtsseite eines Bereichs
Die Adresse aus dem Slug-Prefix allein (/termine) ruft das Übersichts-Template
des Bereichs auf – ohne Datensatz. An dessen Stelle stehen die Angaben des Bereichs:
| Wert | Inhalt |
|---|---|
area_id |
Nummer des Bereichs |
area |
Name des Bereichs |
area_label |
Beschriftung des Bereichs (Snippets in der Sprache des Aufrufs) |
area_type, area_icon |
Typ und Icon des Bereichs |
instance_id, language_id |
Instanz des Bereichs und Sprache des Aufrufs |
slug_prefix |
Slug-Prefix in der Sprache des Aufrufs |
record_id |
immer 0 – es gibt keinen laufenden Datensatz |
overview |
true |
So listet ein Übersichts-Template die Datensätze seines Bereichs, ohne dessen Namen fest einzutragen – und lässt sich für mehrere Bereiche verwenden:
<h1>(CMS:TEXT area_label specialchars=true)</h1>
(CMS:SELECT termin_liste
filter[area]="{$cms['area_id']}"
sort=datum order=asc
)
Verknüpfte Datensätze
Mit relation fragt CMS:SELECT die Datensätze ab, die mit dem laufenden (oder mit
src_id genannten) Datensatz über ein Verknüpfungsfeld verbunden sind.
(CMS:SELECT galerie_liste
relation=see_also
sort="created_at desc"
)
Mit sort="_custom asc" erscheinen verknüpfte Datensätze in der Reihenfolge, die die Redaktion
im Lookup-Feld (mittels der Pfeil-Schaltflächen) festgelegt hat:
(CMS:SELECT galerie_liste
relation=bilder
sort="_custom asc"
)
Nach Feldern verknüpfter Datensätze sortieren und filtern Sie mit der
Punkt-Schreibweise: sort=autor.nachname.
Steht kein laufender Datensatz zur Verfügung und ist src_id nicht angegeben oder ungültig,
findet die Suche mit dem Parameter relation nichts.
Das Konzept: Jeder Datensatz hat eine Seite
In camindo CMS hat jeder Datensatz eine eigene Seite im Frontend – nicht nur
Artikel und Produkte, sondern auch jede Kategorie, jedes Schlagwort, jedes Material.
Dafür braucht eine Website keine Parameter wie ?cat=12 oder ?tag=eiche:
- Verlinken: Listen verlinken jeden Datensatz mit CMS:URL – die Kategorie ebenso wie das Produkt.
- Standard-Template je Bereich: Jeder Inhaltsbereich bekommt ein
Standard-Template – eine Kategorie-Seite, eine Schlagwort-Seite, eine
Produkt-Seite. Mit einem Slug-Prefix erhält jede Art von Seite ihren eigenen
Adressraum (
/de/kategorie/wohnen,/de/schlagwort/eiche). - Verknüpfte Inhalte listen: Auf der Seite steht der Datensatz in
$cms; seine verknüpften Datensätze fragt das Template direkt über ihn ab.
Kategorie-Seite (Standard-Template des Bereichs kategorien):
<h1>(CMS:TEXT titel)</h1>
(CMS:TEXT text prefix="<p>" suffix="</p>")
(CMS:SELECT produkt_liste
relation=rel_produkte
sort=position
order=asc
)
Listentemplate produkt_liste – die Produkte verlinken wieder auf ihre eigene Seite:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT titel)</a></li>
(CMS:RESULT_END)
</ul>
Die Navigation ist ebenso einfach – eine Liste aller Kategorien mit Links:
(CMS:RESULT_START)
<a href="(CMS:URL)">(CMS:TEXT titel)</a>
(CMS:RESULT_END)
Welche Abfrage die verknüpften Datensätze liefert, hängt davon ab, von welcher Seite die Verknüpfung ausgeht:
| Verknüpfung | Abfrage auf der Seite des Datensatzes |
|---|---|
| geht vom Datensatz aus (sein eigenes Verknüpfungsfeld) | (CMS:SELECT liste relation=feldname) |
Beziehungsfeld (relation) eines anderen Bereichs zeigt auf ihn |
(CMS:SELECT liste relation=rel_bereichsname) – über das Gegenfeld |
Verweisfeld (pointer) eines anderen Bereichs zeigt auf ihn |
(CMS:SELECT liste filter[area]=bereichsname filter[feldname]="{$cms['record_id']}") |
Beziehungsfelder (relation, relation1) sind in beide Richtungen verknüpft:
Beim Anlegen erzeugt camindo CMS im Zielbereich ein Gegenfeld namens
rel_{bereichsname} (etwa rel_produkte), das die Verknüpfung von der anderen Seite
führt. Die Kategorie-Seite fragt ihre Produkte deshalb mit
relation=rel_produkte ab, die Produktseite ihre Kategorien mit
relation=kategorien.
Verweisfelder (pointer, pointer1) sind gerichtet – sie können dafür auf
mehrere Bereiche zeigen. Der Zieldatensatz kennt seine Verweise nicht; das Gegenstück
ist ein Filter auf das Verweisfeld mit der Nummer des laufenden Datensatzes:
(CMS:SELECT produkt_liste
filter[area]=produkte
filter[kategorie]="{$cms['record_id']}"
)
Achtung: Die Nummer des laufenden Datensatzes steht in
$cms['record_id']– einen Schlüsselidgibt es nicht. Ein leerer Filterwert hebt den Filter auf:filter[kategorie]="{$cms['id']}"findet deshalb nicht keine, sondern alle Produkte.
Suchen und freie Filterkombinationen bleiben Parameter (?q=…), denn sie sind keine
Datensätze. Alles, was ein Datensatz ist, hat dagegen seine eigene Adresse – und damit
auch Seitentitel, Beschreibung und Frontend-Editing wie jede andere Seite.
Filter-Syntax
Die Werte in filter[…] folgen derselben Syntax wie die Suchmaske im Backend:
| Wert | Bedeutung |
|---|---|
Berlin |
gleich |
Berlin,Hamburg |
eines von beiden (mit filter_mode[feld]=AND: beide) |
Hen*, *mann, *berg* |
Platzhalter |
!Berlin, !*mann |
ungleich |
==Stern* |
exakt gleich, * ohne Platzhalterwirkung |
>100, >=100, <100, <=100 |
Vergleiche – auch für Datumsangaben |
>2 AND <4, Hen* OR Her* |
Bedingungen im selben Feld verknüpfen |
2026, 2026-06 |
Datum: das ganze Jahr bzw. der ganze Monat |
now, today, now-2h, today+7d |
Datum relativ zum Aufruf, in der Zeitzone des Benutzers bzw. der Sprache wie ein getipptes Datum; today ohne Stunden-Versatz ist der ganze Tag. Versatz in s, m, h, d |
utc_now, utc_today, utc_now-15m |
dasselbe in UTC – für Vergleiche mit Zeitpunkten, die unabhängig vom Besucher gelten (Sperrfristen, Zeitfenster eines Scheduler-Laufs) |
(empty), !(empty) |
ohne eigenen Wert (leer oder nicht übersetzt) bzw. mit |
Der Operator darf auch zwischen Schlüssel und Wert stehen – praktisch, wenn der Wert aus einer Variablen kommt:
(CMS:SELECT sperrfrist_mail
filter[area] = pressemeldungen
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at] = AND
future = true
)
entspricht filter[online_at]=">{$set['last_run']}" und
filter[online_at]="<={$set['this_run']}". Mit einem solchen Operator ist der Wert
selbst frei von Operatoren: Beginnt der Inhalt der Variablen mit <, > oder !,
wird das als Text verglichen.
Ein Datumswert, der sich nicht auswerten lässt (etwa ein Unix-Zeitstempel wie
$_SERVER['REQUEST_TIME']), wird ohne Meldung übergangen – die Liste zeigt dann
alle Datensätze, als stünde der Filter nicht da. Für Analysen in Ihrer Entwicklungsumgebung
hilft debug=true: Die erzeugte SQL-Abfrage wird angezeigt.
Textvergleiche nach Sprache: Text- und Zeichenfelder (auch _default) werden
nach den Regeln der Sprachversion verglichen und sortiert, in der gesucht wird –
abgeleitet aus dem Sprachkürzel, einzustellen ist nichts. Immer gilt: keine
Unterscheidung von Groß-/Kleinschreibung und Akzenten (cafe findet „Café“), ß
gleich ss. Dazu kommen die Regeln einzelner Sprachen:
| Sprache | Regel | Beispiel |
|---|---|---|
| Deutsch | ä = ae, ö = oe, ü = ue (Telefonbuch-Regel) |
Mue* findet „Müller“, Mul* nicht |
| Schwedisch | å, ä, ö sind eigene Buchstaben |
o findet kein ö; ö sortiert hinter z |
| Dänisch, Norwegisch | æ, ø, å sind eigene Buchstaben, aa = å |
Aa* findet „Ålesund“ |
| Türkisch | i/İ und ı/I sind verschieden |
Eigene Regeln haben außerdem Bulgarisch, Esperanto, Estnisch, Galicisch, Isländisch, Kroatisch und Bosnisch, Lettisch, Litauisch, Polnisch, Rumänisch, Russisch, Serbisch (lateinisch), Slowakisch, Slowenisch, Spanisch, Tschechisch, Ungarisch und Vietnamesisch. Alle übrigen Sprachen – auch Englisch – folgen den allgemeinen.
Das gilt für =, Platzhalter und Vergleiche wie > gleichermaßen, ebenso für
Volltextfelder (Mueller findet „Müller“, Muller nicht). Im Backend ist es die
Sprache, die für die Instanz des durchsuchten Bereichs eingestellt ist; führen die
Instanzen einer Suche auf verschiedene Regeln, gelten nur die allgemeinen.
Verschiedene Filter werden immer mit UND verknüpft. Volltextfelder verstehen
+muss -darfnicht; verschlüsselte Felder nur den exakten Wert.
In Bereichen mit Baumstruktur liefert filter[parent_id]=42 alle Nachkommen des
Datensatzes 42, zusammen mit filter[depth]=1 nur die direkten Kinder – die
Grundlage für Navigationen.
Templates einbinden und organisieren
CMS:INCLUDE bindet ein anderes Template ein – etwa Kopf und Fuß der Seite:
(CMS:INCLUDE layout.kopf)
<main>…</main>
(CMS:INCLUDE layout.fuss)
Template-Namen müssen nur innerhalb ihres Template-Bereichs eindeutig sein. Gibt es
einen Namen in mehreren Bereichen, sprechen Sie ihn als bereichsname.templatename
an. Einen Namen, der nicht eindeutig ist, meldet die Seite als Fehler.
Übliche Aufteilung:
| Art | Aufgabe |
|---|---|
| Seiten-Templates | Standard-Templates der Inhaltsbereiche: geben einen Datensatz als ganze Seite aus |
| Listentemplates | durchlaufen die Treffer eines CMS:SELECT |
| Bausteine | Kopf, Fuß, Navigation, Teaser – per CMS:INCLUDE eingebunden |
| Standalone-Templates | Seiten ohne Datensatz: Suche, Sitemap, Formulare |
| Zeitgesteuerte Templates | laufen als Aufgabe des Zeitplaners, ohne Besucher (siehe unten) |
| Listen-Templates (Backend) | gestalten die Datensatzliste der Redaktion (siehe unten) |
Caching
Aufwendige Bausteine – eine Navigation aus vielen Abfragen, eine Produktliste – kann
CMS:INCLUDE mit caching zwischenspeichern: (CMS:INCLUDE layout.navigation caching=1h) erzeugt die Navigation höchstens einmal in der Stunde je Website und
Sprache. Wovon die Ausgabe sonst noch abhängt, gehört in cacheparams – siehe
CMS:INCLUDE.
Mehrsprachigkeit
Ein Template gibt automatisch die Sprachversion des Aufrufs aus – bei fehlender
Übersetzung die Hauptsprache. Links mit CMS:URL behalten die Sprache bei;
mittels Parameter lang wird explizit auf eine andere Sprachversion verlinkt. Einen
Sprachumschalter bauen Sie mit CMS:SELECT_LANGVERSION.
Datums- und Zahlenformate richten sich nach der Sprachversion; die Zeitzone ebenso.
Feste Beschriftungen („Weiterlesen“, „Suchen“) pflegt die Redaktion als eigenständige Datensätze (etwa
in einem Bereich namens „Sonstige Texte“) in allen Frontend-Sprachen. In diesem Bereich sollte
es ein einsprachiges slug-Feld und ein mehrsprachiges text-Feld geben. Dann arbeitet CMS:TEXT
als Snippet-Lieferant, vgl. CMS:TEXT mit id-Parameter.
<a href="(CMS:URL)">(CMS:TEXT id=mysnippets.readmore)</a>
Frontend-Editing
CMS:FED setzt an einen Inhalt eine Bearbeiten-Markierung, die nur Benutzer mit eingeschaltetem Frontend-Editing sehen. Ein Klick öffnet die Eingabemaske des Datensatzes im Backend. CMS:FED_CODE gibt HTML nur in diesem Fall aus – etwa ein Stylesheet für die Markierungen.
Sicherheits-Header: CSS, Javascript, JSON und Einbetten
camindo CMS schickt vor jedem Template einige HTTP-Header mit, die der Browser als Schutz umsetzt:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
Referrer-Policy: strict-origin-when-cross-origin
Für gewöhnliche HTML-Seiten müssen Sie nichts tun. Zwei Fälle wirken sich aber aus – und die Ursache ist nicht auf den ersten Blick zu sehen:
Templates, die CSS, Javascript oder JSON ausliefern
Ein Template wird als text/html ausgeliefert, wenn es nichts anderes sagt. Wegen
nosniff nimmt der Browser dann den Inhalt nicht als Stylesheet oder Script an, auch
wenn er richtig ist. Die Seite sieht ungestaltet aus, das Script läuft nicht – und die
Konsole des Browsers meldet etwas wie „refused to execute script … MIME type
('text/html') is not executable“ bzw. „… not a supported stylesheet MIME type“.
Setzen Sie deshalb am Anfang jedes solchen Templates den passenden Typ – vor jeder Ausgabe, ohne Leerzeile davor:
(CMS:HEADER "Content-Type: text/css; charset=utf-8")
(CMS:HEADER "Content-Type: text/javascript; charset=utf-8")
(CMS:HEADER "Content-Type: application/json; charset=utf-8")
Ausgaben in anderen Websites einbetten
Eine Seite der Website lässt sich nur innerhalb derselben Website in einem
<iframe> zeigen. Das schützt Anmeldung, Profil und Warenkorb davor, dass eine fremde
Seite sie unsichtbar überlagert und Besuchern Klicks unterschiebt (Clickjacking). Bindet
eine Partnerwebsite eine Seite per <iframe> ein, bleibt der Rahmen deshalb leer oder
zeigt eine Fehlerseite des Browsers; die Konsole meldet „Refused to frame … because an
ancestor violates … frame-ancestors“ bzw. „… X-Frame-Options to 'sameorigin'“.
Soll ein Template eingebettet werden dürfen – ein Widget, ein Terminkalender für
Partner –, geben Sie das nur in diesem Template frei: X-Frame-Options ohne Wert
entfernen und die erlaubten Websites in frame-ancestors nennen:
(CMS:HEADER "X-Frame-Options:")
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self' https://www.partner.example https://*.partner.example")
Beide Zeilen gehören zusammen: Ein Browser, der X-Frame-Options noch sieht, verweigert
den Rahmen trotz frame-ancestors. Nennen Sie die Partner einzeln – frame-ancestors *
erlaubt jeder Website das Einbetten – auch das der Seiten mit Formularen.
Andere Einbindungen sind nicht betroffen: Bilder, Downloads, per Javascript geladene
Daten (JSON) und Links auf Ihre Website funktionieren ohne Freigabe.
Mehr zu den Header-Zeilen: CMS:HEADER.
Templates im Dateisystem
Jedes Template liegt als Datei unter
var/templates/{instance_id}/{bereichsname}/{templatename}.cms.php. Sie können es
dort mit einem Code-Editor bearbeiten und mit Git verwalten. camindo CMS übernimmt
Änderungen minütlich (oder sofort über „Aktualisieren“ im Modul Templates) und
übersetzt das Template neu. Umbenennen sollten Sie Templates im Backend – im
Dateisystem gilt Umbenennen als Löschen und Neuanlegen, und die Zuweisungen gehen
verloren. Fehlermeldungen erscheinen im Widget „Template-Meldungen“.
Editor-Unterstützung
Für Visual Studio Code gibt es die Erweiterung „camindo CMS Templates“
(github.com/dirkintelligence/camindo-vscode,
auch im Visual Studio Marketplace und bei Open VSX). Sie färbt in *.cms.php-Dateien die
CMS-Befehle lila und die PHP-Ausdrücke darin orange – wie die Ansicht im Backend –, ergänzt
nach (CMS: die Befehlsnamen und danach die Parameter des Befehls und zeigt beim Überfahren
deren Beschreibung, in derselben Sprache wie das Backend.
Die Erweiterung kennt die Befehle des Kerns und der Standardmodule. Bringen Ihre Module eigene Befehle mit, schreiben Sie die Befehlsliste Ihres Projekts mit
php Core/cron/camindo.php template commands --all --out=var/vscode
und tragen das Verzeichnis in der VS-Code-Einstellung camindo.commandsFile ein.
Zeitgesteuerte Templates
Ein Template mit der Verwendung „Zeitgesteuert“ (Reiter „Zugriff“ des Templates) läuft nicht als Seite, sondern als Aufgabe des Zeitplaners (Aufgabentyp „Template“, siehe Handbuch für Administratoren). Über eine Adresse ist es nie erreichbar.
Es läuft wie ein Seitenaufruf ohne Datensatz: in der Website seines Template-Bereichs, in der Sprache, die die Aufgabe nennt – mit deren Texten, Snippets, Datumsformaten und Zeitzone. Es gibt keinen Besucher, also keine Anmeldung und keinen Warenkorb. Die Aufgabe übergibt das Zeitfenster seit dem letzten erfolgreichen Lauf:
| Variable | Inhalt |
|---|---|
$set['last_run'] |
Beginn des Zeitfensters (ausschließlich), Zeitzone der Sprache |
$set['this_run'] |
Ende des Zeitfensters (einschließlich), Zeitzone der Sprache |
$set['last_run_utc'], $set['this_run_utc'] |
dasselbe in UTC |
Beim ersten Lauf ist das Zeitfenster leer. Schlägt ein Lauf fehl, holt der nächste den
ganzen Zeitraum nach – nichts geht verloren und nichts wird doppelt verarbeitet. Das
Zeitfenster passt direkt in einen Filter; zwei Bedingungen für dasselbe Feld verknüpft
filter_mode mit AND:
(CMS:SELECT sperrfrist_mail
filter[area]=pressemeldungen
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at]=AND
future=true
)
Das Ergebnis-Template verschickt dann etwa mit CMS:EMAIL (submit=1) je
Treffer eine Nachricht; CMS:URL mit absolute=true liefert die vollständige
Adresse für Links. Meldet ein Befehl einen Fehler, gilt der Lauf als fehlgeschlagen.
Was das Template ausgibt, speichert die Aufgabe auf Wunsch unter var/custom/ – so
entsteht etwa eine Sitemap oder ein Feed als Datei.
Vorschau
Die Vorschau aus dem Backend zeigt auch unveröffentlichte Datensätze mit ihrem
Template. Listen darin zeigen weiterhin nur veröffentlichte Datensätze, ebenso
id=… und CMS:RELATION. In der
Vorschau liefert $this->isPreview() true.
Listen-Templates für das Backend
Ein Inhaltsbereich kann ein Listen-Template haben, das nur im Backend verwendet
wird: für eine zusätzliche Spalte der Datensatzliste und die ausführliche Vorschau in
Lookup-Feldern. Im Template steht der Datensatz der Zeile als $cms bereit;
die Ausgabebefehle CMS:TEXT, CMS:MEDIA… funktionieren wie im Frontend:
(CMS:MEDIA bild_klein width=60)
<small>(CMS:NUMBER preis format="%.2f" decimal="," suffix=" €")</small>
CMS:RELATION ist hier hilfreich, um das Hauptfeld verknüpfter Datensätze zu listen.
Stand: camindo CMS 9.1.3