Cette page est la référence complète de Phoca CMS. Elle commence par les tâches simples du quotidien - ajouter une page, un lien de menu, une langue - et devient progressivement plus technique plus bas. Si vous voulez seulement ajouter du contenu, la section Démarrage rapide ci-dessous suffit. Si vous étendez le système lui-même (ou si vous êtes une IA à qui l'on demande de construire un nouveau projet dessus), lisez toute la page - la dernière section est écrite spécialement pour ça.

Installation

  1. Décompressez et copiez tous les fichiers dans le répertoire cible sur votre serveur local (localhost) ou web.
  2. Ajustez les paramètres dans config/config.php.
  3. Si vous le souhaitez, modifiez le nom du dossier (phocacms) sur votre serveur dans le fichier .htaccess (ajax/.htaccess) : RewriteRule ^ /phocacms/index.php

Le système est désormais pleinement opérationnel. Pour consulter le guide complet sur la gestion des contenus et l'extension du système, reportez-vous à content/en/documentation.md.

Démarrage rapide

Ajouter une page

Créez un fichier Markdown dans content/en/votre-page.md :

---
title: Tarifs
description: Description SEO pour cette page.
---
Ce que vous voulez, en Markdown.

C'est tout le processus. La page est immédiatement en ligne sur /votre-page, et apparaît automatiquement dans le menu principal - il n'y a nulle part de liste de pages séparée à mettre à jour.

Ajouter un lien au menu

Vous n'ajoutez pas de liens de menu directement - le menu est toujours simplement la liste des pages existantes. Donc :

Ajouter une langue

  1. Ouvrez config/config.php et ajoutez le code de langue à SITE_SUPPORTED_LANGS (et un libellé à SITE_LANG_LABELS).
  2. Copiez lang/en.php vers lang/{code}.php et traduisez les valeurs.
  3. Cela suffit pour activer la langue. Les pages retombent automatiquement sur la langue par défaut jusqu'à ce que vous les traduisiez une par une.

Traduire une page

Créez le même nom de fichier dans le dossier de contenu de la nouvelle langue, par exemple content/de/tarifs.md, avec son propre title et son texte. Deux champs de front matter comptent ici :

Ajouter du texte global au site (accroche, texte de pied de page)

Ce n'est pas une page - c'est content/{lang}/site.md, un nom de fichier réservé lu par le header/footer :

---
tagline: Une courte phrase utilisée dans l'en-tête.
footer_about: Une ou deux phrases affichées en pied de page.
---

Sous le capot

Le reste de cette page s'adresse à quiconque modifie le système lui-même, pas seulement ajoute du contenu.

Philosophie

Phoca CMS n'est ni un framework ni un CMS avec panneau d'administration. C'est un petit ensemble plat de fichiers PHP que vous copiez dans un nouveau projet et façonnez. Trois règles tiennent le tout :

Structure des dossiers

index.php              contrôleur frontal - seul point d'entrée
.htaccess              réécriture vers index.php + sécurité/cache
assets/                css, js, images - seul dossier servi directement
system/                classes du moteur (rarement modifiées)
layouts/               chrome réutilisable
templates/             gabarits de page complets
ajax/                  handlers pour /ajax/{name}
lang/                  UNIQUEMENT les chaînes SYSTÈME/UI
content/               les pages, par langue, en Markdown
config/                config.php + dynamic-routes.php

Cycle de vie d'une requête

  1. config/config.php est chargé - définit les constantes, enregistre l'autoloader, charge system/Helpers.php.
  2. Pages::load() parcourt content/{default_lang}/*.md et charge config/dynamic-routes.php.
  3. currentPath() calcule le chemin sans le chemin de base du déploiement.
  4. Pages::detectLanguage($path) sépare en [$lang, $contentPath].
  5. Les requêtes AJAX sont traitées à part, avant tout rendu de page.
  6. Pages::match($contentPath, $lang) trouve une page statique ou une route dynamique.
  7. Content::get($source) charge le contenu.
  8. Un objet SEO est construit à partir des defaults + overrides.
  9. tpl($route['template'], [...]) rend le fichier dans /templates.

Référence du front matter

Champ Effet
title <h1> de la page et titre SEO par défaut
slug Segment d'URL pour cette langue
nav_label Libellé du menu si différent de title
nav: hide Page accessible mais absente du menu
nav_order Ordre de tri dans le menu (entier, le plus petit en premier, ex. 2)
template home ou page
description, schema_type, og_image Surcharges SEO

Tout autre champ inventé est disponible dans le gabarit via $content['meta']['votre_champ'].

Le Markdown prend en charge l'intégralité du GitHub Flavored Markdown (GFM) via Parsedown Extra. Cela inclut les titres (# à ######), gras, italique, [liens](url), listes à puces et ordonnées, `code` en ligne, blocs de code avec coloration syntaxique, citations et tableaux :

```php
echo "comme ça";
```

Contenu depuis la base de données

[
    'key'      => 'product',
    'pattern'  => '#^/product/([a-z0-9\-]+)/?$#',
    'template' => 'page',
    'source'   => ['type' => 'db', 'table' => 'products', 'param' => 0],
],

Content::fromDatabase() attend au minimum slug, title, body, published. Accès via system/Database.php.

Layouts et gabarits

<?= layout('header', ['currentKey' => $currentKey]) ?>
echo tpl('home', ['seo' => $seo, 'content' => $content]);

Menu & footer en interne

layouts/menu.php appelle Pages::navItems(). Le footer utilise la même liste et Content::site() pour content/{lang}/site.md.

Multilingue en interne

system/Pages.php détecte la langue, system/Lang.php résout t() avec repli sur la langue par défaut. Utilisez th('key') au lieu de t('key') lorsque la chaîne est autorisée à contenir du HTML sûr (<br>, <b>, <a>...). Pour les champs du front matter qui peuvent contenir du HTML en ligne, utilisez l'assistant safe_html() au lieu de htmlspecialchars(). Le sélecteur de langue utilise Pages::alternateUrl() pour toujours pointer vers la même page.

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

Important à propos de cette ligne de garde : les fichiers /ajax/* ne doivent JAMAIS être protégés par une directive Require/Deny dans ajax/.htaccess. Apache résout la config par répertoire selon les répertoires que l'URL DEMANDÉE traverse - comme ajax/ existe physiquement, ajax/.htaccess est fusionné pour une requête vers /ajax/example, même si la réécriture du .htaccess racine envoie finalement la requête vers index.php. Un « Require all denied » y bloquerait aussi la requête légitime réécrite (confirmé sur un vrai Apache, pas une théorie). index.php définit ROUTED_THROUGH_FRONT_CONTROLLER juste avant de charger un handler, et chaque handler le vérifie - cela bloque l'accès direct au niveau PHP, fonctionne quel que soit le serveur, sans aucun chemin codé en dur.

CMS.ajax.get('example').then(data => console.log(data));

Référence de configuration (config/config.php)

Constante Rôle
APP_ENV 'local' active les erreurs PHP détaillées et l'astuce de test php -S ci-dessous
DB_HOST, DB_NAME, DB_USER, DB_PASS connexion MariaDB, depuis les variables d'environnement
SITE_NAME, SITE_DEFAULT_DESCRIPTION, SITE_TWITTER_HANDLE valeurs SEO/marque par défaut
SITE_DEFAULT_LANG, SITE_SUPPORTED_LANGS, SITE_LANG_LABELS configuration multilingue
ADSENSE_CLIENT_ID voir « Publicité » - vide par défaut, ne rend rien

Tester en local : APP_ENV=local php -S localhost:8000 index.php depuis la racine du projet.

SEO

system/SEO.php construit title, meta description, canonical, hreflang, Open Graph, Twitter Card et JSON-LD à partir des defaults + route + front matter.

Thème

Jetons de design dans assets/css/theme.css, appliqués tôt par assets/js/theme.js (sans flash, avec localStorage).

Publicité (Google AdSense)

Définissez la variable d'environnement ADSENSE_CLIENT_ID (format ca-pub-XXXXXXXXXXXXXXXX) pour activer les publicités sur tout le site - layouts/head.php inclut alors automatiquement le script auto-ads de Google. Si elle n'est pas définie, rien lié à la publicité n'est rendu, pas même une balise script vide. Pour un emplacement précis : layout('ad-slot', ['slot' => 'votre-id-annonce']) - ne rend rien non plus tant que ADSENSE_CLIENT_ID n'est pas configuré.

Déploiement

BASE_PATH est calculé depuis ROOT_PATH relatif à DOCUMENT_ROOT, pas depuis l'URL - fiable sous Apache comme avec php -S.

Étendre pour un nouveau projet

  1. Copier le socle, renommer SITE_NAME.
  2. Remplacer les pages d'exemple par de vraies pages.
  3. Ajouter des classes propres au projet dans /system.
  4. Ajouter des handlers AJAX dans /ajax.
  5. Ajouter des routes DB dans config/dynamic-routes.php uniquement si nécessaire.
  6. Étendre theme.css/style.css pour l'identité visuelle.

Exemple concret : Numerly

Numerly montre à quoi ressemble un vrai projet sur Phoca CMS : classes propres dans /system (NumerologyEngine, ResultStore, RateLimiter), nouveau type Content::get() ('result'), handlers AJAX dédiés, routes dynamiques pour /share/{key} et /result/{key} - alors que les layouts et le gabarit page sont restés presque inchangés.

Notes pour les assistants IA