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
- Décompressez et copiez tous les fichiers dans le répertoire cible sur votre serveur local (localhost) ou web.
- Ajustez les paramètres dans
config/config.php. - 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 :
- Pour ajouter un lien de menu : ajoutez une page (voir ci-dessus). Elle apparaît automatiquement, avec son
titlecomme libellé. - Pour utiliser un libellé différent du titre : ajoutez
nav_label: Boutiqueau front matter de cette page. - Pour garder une page hors du menu (par exemple une page seulement liée depuis ailleurs) : ajoutez
nav: hide. La page fonctionne toujours à son URL, elle n'est simplement pas listée.
Ajouter une langue
- Ouvrez
config/config.phpet ajoutez le code de langue àSITE_SUPPORTED_LANGS(et un libellé àSITE_LANG_LABELS). - Copiez
lang/en.phpverslang/{code}.phpet traduisez les valeurs. - 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 :
slug- le segment d'URL propre à cette langue.about.mden anglais peut utiliser/about-us; en français,about.mdpeut définirslug: a-propospour devenir/fr/a-propos. Chaque langue contrôle son URL indépendamment.- Si vous ne créez pas le fichier, les visiteurs de cette langue voient simplement la version de la langue par défaut au lieu d'un lien mort - traduisez les pages dans l'ordre que vous voulez, rien ne casse entre-temps.
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 :
- Les pages sont des fichiers, pas de la config. Comme montré ci-dessus.
- Chaînes système et contenu sont deux choses différentes.
lang/{lang}.phpcontient le chrome UI réutilisable ; tout ce qu'un visiteur lit comme « contenu du site » vit danscontent/. - Tout est à un seul niveau de profondeur.
/system,/layouts,/templates,/lang,/ajax,/content,/configsont tous directement à la racine du projet.
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
config/config.phpest chargé - définit les constantes, enregistre l'autoloader, chargesystem/Helpers.php.Pages::load()parcourtcontent/{default_lang}/*.mdet chargeconfig/dynamic-routes.php.currentPath()calcule le chemin sans le chemin de base du déploiement.Pages::detectLanguage($path)sépare en[$lang, $contentPath].- Les requêtes AJAX sont traitées à part, avant tout rendu de page.
Pages::match($contentPath, $lang)trouve une page statique ou une route dynamique.Content::get($source)charge le contenu.- Un objet
SEOest construit à partir des defaults + overrides. 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
- Copier le socle, renommer
SITE_NAME. - Remplacer les pages d'exemple par de vraies pages.
- Ajouter des classes propres au projet dans
/system. - Ajouter des handlers AJAX dans
/ajax. - Ajouter des routes DB dans
config/dynamic-routes.phpuniquement si nécessaire. - Étendre
theme.css/style.csspour 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
- Lisez toute cette page avant de toucher au moindre fichier.
- Le contenu et la traduction vont dans
/contentet/lang- ne jamais coder en dur du texte dans un gabarit ou un layout. - HTML sécurisé et échappement :
- Échappez toujours le texte brut avec
htmlspecialchars(). - Pour les chaînes système (dans
/lang), utilisezt('key')pour le texte brut (n'oubliez pas de l'échapper :htmlspecialchars(t('key'))). Utilisezth('key')si la traduction contient des balises HTML sûres (il utilise une liste blanche, ne l'échappez donc pas). - Si un champ du front matter contient du HTML en ligne (comme
<br>ou<strong>), utilisezsafe_html($content['meta']['champ'])au lieu dehtmlspecialchars().
- Échappez toujours le texte brut avec
- Toute logique vraiment nouvelle va dans une nouvelle classe, petite et ciblée, dans
/system, connectée via/ajaxouconfig/dynamic-routes.php. - Ne réécrivez pas le routage, les gabarits ou le système de langues - étendez-les de façon ciblée si nécessaire, comme Numerly l'a fait.
- Ne listez jamais de pages statiques dans le code - créez un fichier Markdown à la place.
- Utilisez toujours
url()/asset_url(), jamais un chemin codé en dur.