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

  1. Entpacken Sie das Archiv und kopieren Sie alle Dateien in das Zielverzeichnis auf Ihrem Localhost oder Webserver.
  2. Passen Sie die Konfiguration in config/config.php an.
  3. Ä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:

Eine Sprache hinzufügen

  1. config/config.php öffnen und den Sprachcode zu SITE_SUPPORTED_LANGS hinzufügen (sowie ein Anzeige-Label zu SITE_LANG_LABELS).
  2. lang/en.php nach lang/{code}.php kopieren und die Werte darin übersetzen.
  3. 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:

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:

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:

  1. config/config.php wird geladen - definiert Konstanten, registriert den Autoloader für /system und lädt system/Helpers.php.
  2. Pages::load() durchsucht content/{default_lang}/*.md und lädt config/dynamic-routes.php für musterbasierte Routen.
  3. currentPath() berechnet den Request-Pfad ohne den Base-Path des Deployments.
  4. Pages::detectLanguage($path) teilt das in [$lang, $contentPath] auf.
  5. AJAX-Requests werden separat behandelt, bevor irgendein Seiten-Rendering passiert.
  6. Pages::match($contentPath, $lang) findet eine statische Seite oder eine dynamische Route.
  7. Content::get($source) lädt den Inhalt (Markdown oder Datenbank).
  8. Ein SEO-Objekt wird aus Config-Defaults + Routen-Overrides + Front-Matter zusammengesetzt.
  9. 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

  1. Grundgerüst kopieren, SITE_NAME umbenennen.
  2. Platzhalter-Inhalte durch echte Seiten ersetzen.
  3. Projektspezifische Klassen zu /system hinzufügen.
  4. AJAX-Handler zu /ajax hinzufügen.
  5. Datenbank-Routen nur für Inhalte, die wirklich keine Datei sein können, zu config/dynamic-routes.php hinzufügen.
  6. theme.css/style.css fü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