Diese Seite ist die vollständige Referenz für Phoca CMS. Sie beginnt mit den einfachen, alltäglichen Aufgaben - eine Seite, einen Menüpunkt, eine Sprache hinzufügen - und wird weiter unten zunehmend technischer. Wenn Sie nur Inhalte hinzufügen möchten, brauchen Sie nur den Abschnitt „Schnelleinstieg" unten. Wenn Sie das System selbst erweitern (oder eine KI sind, die gebeten wird, ein neues Projekt darauf aufzubauen), lesen Sie die ganze Seite - der letzte Abschnitt ist genau dafür geschrieben.
Installation
- Entpacken Sie das Archiv und kopieren Sie alle Dateien in das Zielverzeichnis auf Ihrem Localhost oder Webserver.
- Passen Sie die Konfiguration in
config/config.phpan. - Ändern Sie optional den Ordnernamen (phocacms) auf Ihrem Server in der .htaccess-Datei (ajax/.htaccess):
RewriteRule ^ /phocacms/index.php
Das System ist nun vollständig einsatzbereit. Eine ausführliche Dokumentation zur Verwaltung von Inhalten und zur Erweiterung des Systems finden Sie unter content/en/documentation.md.
Schnelleinstieg
Eine Seite hinzufügen
Legen Sie eine Markdown-Datei unter content/en/ihre-seite.md an:
---
title: Preise
description: SEO-Beschreibung für diese Seite.
---
Beliebiger Inhalt, in Markdown.
Das ist der gesamte Vorgang. Die Seite ist sofort unter /ihre-seite live und erscheint automatisch im Hauptmenü - es gibt nirgendwo eine separate Seitenliste zu pflegen.
Einen Menüpunkt hinzufügen
Menüpunkte fügen Sie nicht direkt hinzu - das Menü ist immer einfach die Liste der vorhandenen Seiten. Also:
- Menüpunkt hinzufügen: eine Seite anlegen (siehe oben). Sie erscheint automatisch, beschriftet mit ihrem
title. - Anderes Menü-Label als der Seitentitel:
nav_label: Shopim Front Matter dieser Seite ergänzen. - Seite aus dem Menü heraushalten (z. B. eine nur von woanders verlinkte Seite):
nav: hideim Front Matter ergänzen. Die Seite funktioniert weiterhin unter ihrer URL, wird nur nicht gelistet.
Eine Sprache hinzufügen
config/config.phpöffnen und den Sprachcode zuSITE_SUPPORTED_LANGShinzufügen (sowie ein Anzeige-Label zuSITE_LANG_LABELS).lang/en.phpnachlang/{code}.phpkopieren und die Werte darin übersetzen.- Das reicht, um die Sprache zu aktivieren. Seiten greifen automatisch auf die Standardsprache zurück, bis Sie sie nach und nach übersetzen (siehe nächster Punkt).
Eine Seite übersetzen
Legen Sie denselben Dateinamen im Content-Ordner der neuen Sprache an, z. B. content/de/preise.md, mit eigenem title und Text. Zwei zusätzliche Front-Matter-Felder sind hier wichtig:
slug- das eigene URL-Segment dieser Sprache. Englischabout.mdkann/about-usnutzen; Deutschabout.mdkannslug: ueber-unssetzen und wird zu/de/ueber-uns. Jede Sprache steuert ihre URL unabhängig.- Legen Sie die Datei gar nicht erst an, sehen Besucher dieser Sprache einfach die Version der Standardsprache statt eines toten Links - übersetzen Sie Seiten in beliebiger Reihenfolge, in der Zwischenzeit geht nichts kaputt.
Seitenweite Texte hinzufügen (Tagline, Footer-Text)
Das ist keine Seite - es ist content/{lang}/site.md, ein reservierter Dateiname, der von Header/Footer gelesen wird:
---
tagline: Eine kurze Zeile im Header-Bereich.
footer_about: Ein bis zwei Sätze im Footer.
---
Unter der Haube
Der Rest dieser Seite richtet sich an alle, die das System selbst verändern, nicht nur Inhalte hinzufügen.
Philosophie
Phoca CMS ist kein Framework und kein CMS mit Admin-Oberfläche. Es ist ein kleiner, flacher Satz von PHP-Dateien, den Sie in ein neues Projekt kopieren und formen. Drei Regeln halten das Ganze zusammen:
- Seiten sind Dateien, keine Konfiguration. Wie oben gezeigt - es gibt nirgendwo eine zentrale Routenliste für statische Seiten.
- Systemtexte und Inhalte sind zwei verschiedene Dinge.
lang/{lang}.phpenthält wiederverwendbares UI-Chrome. Alles, was ein Besucher als „Inhalt der Seite" liest, liegt incontent/. - Alles liegt eine Ebene tief.
/system,/layouts,/templates,/lang,/ajax,/content,/configliegen alle direkt im Projekt-Root.
Verzeichnisstruktur
index.php Front Controller - der einzige Einstiegspunkt
.htaccess Rewrite auf index.php + Sicherheit/Caching
assets/ css, js, images - einzig direkt erreichbarer Ordner
system/ Engine-Klassen (selten angefasst, sobald ein Projekt läuft)
layouts/ wiederverwendbares Chrome: head, header, menu, footer, sidebars
templates/ Ganzseiten-Templates, die Layouts zusammensetzen
ajax/ Handler für /ajax/{name}
lang/ NUR SYSTEM/UI-Texte (en.php, de.php, fr.php...)
content/ die eigentlichen Seiten, pro Sprache, als Markdown
config/ config.php + dynamic-routes.php
Ablauf eines Requests
Jeder Request (außer statischen Assets) läuft durch index.php. Der Reihe nach:
config/config.phpwird geladen - definiert Konstanten, registriert den Autoloader für/systemund lädtsystem/Helpers.php.Pages::load()durchsuchtcontent/{default_lang}/*.mdund lädtconfig/dynamic-routes.phpfür musterbasierte Routen.currentPath()berechnet den Request-Pfad ohne den Base-Path des Deployments.Pages::detectLanguage($path)teilt das in[$lang, $contentPath]auf.- AJAX-Requests werden separat behandelt, bevor irgendein Seiten-Rendering passiert.
Pages::match($contentPath, $lang)findet eine statische Seite oder eine dynamische Route.Content::get($source)lädt den Inhalt (Markdown oder Datenbank).- Ein
SEO-Objekt wird aus Config-Defaults + Routen-Overrides + Front-Matter zusammengesetzt. tpl($route['template'], [...])rendert die passende Datei in/templates.
Front-Matter-Referenz
| Feld | Wirkung |
|---|---|
title |
Seiten-<h1> und Standard-SEO-Titel |
slug |
Eigenes URL-Segment für diese Sprache |
nav_label |
Menü-Beschriftung, falls abweichend von title |
nav: hide |
Seite bleibt erreichbar, aber nicht im Menü |
nav_order |
Sortierreihenfolge im Menü (Zahl, niedrigere zuerst, z.B. 2) |
template |
home oder page |
description, schema_type, og_image |
SEO-Overrides |
Jedes andere Front-Matter-Feld steht dem Template als $content['meta']['ihr_feld'] zur Verfügung.
Der Markdown-Text unterstützt volles GitHub Flavored Markdown (GFM) via Parsedown Extra. Das beinhaltet Überschriften (# bis ######), fett, kursiv, [Links](url), Aufzählungen, nummerierte Listen, Inline-`Code`, Code-Blöcke mit Sprachklassen, Blockzitate und Tabellen:
```php
echo "so";
```
Inhalte aus der Datenbank
[
'key' => 'product',
'pattern' => '#^/product/([a-z0-9\-]+)/?$#',
'template' => 'page',
'source' => ['type' => 'db', 'table' => 'products', 'param' => 0],
],
Content::fromDatabase() erwartet mindestens slug, title, body (oder body_html), published. Zugriff über system/Database.php (fetchOne(), fetchAll(), execute()).
Layouts und Templates
<?= layout('header', ['currentKey' => $currentKey]) ?>
echo tpl('home', ['seo' => $seo, 'content' => $content]);
Menü & Footer intern
layouts/menu.php ruft Pages::navItems() auf. Der Footer nutzt dieselbe Liste und Content::site() für content/{lang}/site.md.
Mehrsprachigkeit intern
system/Pages.php erkennt die Sprache, system/Lang.php löst t()-Schlüssel auf mit Fallback auf die Standardsprache. Verwenden Sie th('key') anstelle von t('key'), wenn der String sicheres HTML (<br>, <b>, <a>...) enthalten darf.
Für Front-Matter-Felder, die Inline-HTML enthalten können, verwenden Sie den Helper safe_html() anstelle von htmlspecialchars().
Der Sprachumschalter verlinkt über Pages::alternateUrl() immer dieselbe Seite.
AJAX
<?php
if (!defined('ROUTED_THROUGH_FRONT_CONTROLLER')) { http_response_code(404); exit; }
$input = json_decode(file_get_contents('php://input'), true) ?? [];
echo json_encode(['ok' => true]);
Wichtig zu dieser Guard-Zeile: /ajax/*-Dateien dürfen NIE über Require/Deny in ajax/.htaccess geschützt werden. Apache wertet die verzeichnisbezogene Konfiguration anhand der Verzeichnisse der ANGEFRAGTEN URL aus - da ajax/ physisch existiert, wird ajax/.htaccess für /ajax/example mitgemerged, obwohl das Root-.htaccess-Rewrite die Anfrage eigentlich an index.php weiterleitet. Ein „Require all denied" dort würde auch die legitime, umgeschriebene Anfrage blockieren (auf echtem Apache bestätigt - keine Theorie). index.php definiert ROUTED_THROUGH_FRONT_CONTROLLER direkt vor dem Laden eines Handlers, jeder Handler prüft das - blockiert direkten Zugriff auf PHP-Ebene, funktioniert serverunabhängig, ohne fest codierten Pfad.
CMS.ajax.get('example').then(data => console.log(data));
Konfigurationsreferenz (config/config.php)
| Konstante | Zweck |
|---|---|
APP_ENV |
'local' aktiviert ausführliche PHP-Fehler und die php -S-Testhilfe unten |
DB_HOST, DB_NAME, DB_USER, DB_PASS |
MariaDB-Verbindung, aus Umgebungsvariablen |
SITE_NAME, SITE_DEFAULT_DESCRIPTION, SITE_TWITTER_HANDLE |
SEO-/Branding-Defaults |
SITE_DEFAULT_LANG, SITE_SUPPORTED_LANGS, SITE_LANG_LABELS |
Mehrsprachigkeit |
ADSENSE_CLIENT_ID |
siehe „Werbung" - standardmäßig leer, rendert nichts |
Lokal testen: APP_ENV=local php -S localhost:8000 index.php im Projekt-Root ausführen.
SEO
system/SEO.php baut Title, Meta-Description, Canonical, hreflang, Open Graph, Twitter Card und JSON-LD aus Config-Defaults + Route + Front-Matter.
Theme
Design-Tokens in assets/css/theme.css, angewendet über assets/js/theme.js (früh, ohne Flackern, mit localStorage).
Werbung (Google AdSense)
Die Umgebungsvariable ADSENSE_CLIENT_ID setzen (Format ca-pub-XXXXXXXXXXXXXXXX), um Werbung seitenweit zu aktivieren - layouts/head.php bindet dann automatisch Googles Auto-Ads-Skript ein. Ist sie nicht gesetzt, wird nichts Werbebezogenes gerendert, nicht einmal ein leeres Script-Tag. Für eine konkrete Platzierung: layout('ad-slot', ['slot' => 'ihre-anzeigen-id']) - rendert ebenfalls nichts, solange ADSENSE_CLIENT_ID nicht gesetzt ist.
Deployment
BASE_PATH wird aus ROOT_PATH relativ zu DOCUMENT_ROOT berechnet, nicht aus der Request-URL - das funktioniert zuverlässig sowohl unter Apache als auch mit php -S.
Für ein neues Projekt erweitern
- Grundgerüst kopieren,
SITE_NAMEumbenennen. - Platzhalter-Inhalte durch echte Seiten ersetzen.
- Projektspezifische Klassen zu
/systemhinzufügen. - AJAX-Handler zu
/ajaxhinzufügen. - Datenbank-Routen nur für Inhalte, die wirklich keine Datei sein können, zu
config/dynamic-routes.phphinzufügen. theme.css/style.cssfür die visuelle Identität erweitern.
Praxisbeispiel: Numerly
Numerly zeigt, wie ein echtes Projekt auf Phoca CMS aussieht: eigene Klassen in /system (NumerologyEngine, ResultStore, RateLimiter), ein neuer Content::get()-Typ ('result'), eigene AJAX-Handler, dynamische Routen für /share/{key} und /result/{key} - während Layouts und das page-Template fast unverändert blieben.
Hinweise für KI-Assistenten
- Diese ganze Seite lesen, bevor Sie irgendeine Datei anfassen.
- Inhalt und Übersetzung gehören nach
/contentund/lang- niemals Text in einem Template oder Layout hardcoden. - Sicheres HTML und Escaping:
- Maskieren Sie einfachen Text immer mit
htmlspecialchars(). - Für System-Texte (in
/lang) verwenden Siet('key')für einfachen Text (denken Sie an das Escaping:htmlspecialchars(t('key'))). Verwenden Sieth('key'), wenn die Übersetzung sichere HTML-Tags enthält (hierbei wirdstrip_tagsmit einer Allowlist genutzt, also nicht escapen). - Wenn ein Front-Matter-Feld Inline-HTML (wie
<br>oder<strong>) enthält, nutzen Siesafe_html($content['meta']['feld'])anstelle vonhtmlspecialchars().
- Maskieren Sie einfachen Text immer mit
- Wirklich neue Projektlogik gehört in eine neue, kleine Klasse in
/system, angebunden über/ajaxoderconfig/dynamic-routes.php. - Routing-, Template- und Sprachsystem nicht umschreiben - bei Bedarf gezielt erweitern, wie bei Numerly geschehen.
- Statische Seiten nie im Code auflisten - stattdessen eine Markdown-Datei anlegen.
- Immer
url()/asset_url()nutzen, nie einen fest codierten Pfad.