= 🎙️ ANNU KUTE CED — Hub multimédia du podcast
:toc: left
:toc-title: Sommaire / Contents
:toclevels: 3

[[top]]
🌍 *Hub multimédia du podcast ANNU KUTE CED* +
🌍 *Multimedia hub for the ANNU KUTE CED podcast*

[.text-center]
*Choisissez votre langue / Choose your language :* +
<<fr,🇫🇷 Version française>> · <<en,🇬🇧 English version>>

'''

[[fr]]
== 🇫🇷 Version française

=== 📖 Description

*ANNU KUTE CED* est une interface web responsive qui fait office de *hub multimédia* pour le podcast du même nom. Elle regroupe en un seul lieu :

- 🎥 la chaîne *PeerTube* du podcast (vidéos, shorts, directs) ;
- 🎙️ son compte *Castopod* (derniers épisodes, lecture audio intégrée) ;
- 📡 sa timeline *Mastodon* (actualités et annonces).

La plateforme est légère, déployable sur un serveur mutualisé (PHP + serveur web, sans base de données) et optimisée pour mobile et desktop. C'est aussi une *Progressive Web App* installable, avec mode hors ligne.

🎯 *Mission* : offrir un point d'entrée unique, libre et décentralisé, pour découvrir, écouter et suivre le podcast ANNU KUTE CED sans dépendre des grandes plateformes propriétaires.

=== 🌳 Origine du projet

Cette application est un *fork* de https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI[*FEDIVERSE OKI*^], développée par l'https://o-k-i.net[ORGANISATION KA INTERNATIONALE (OKI)^], elle-même issue du projet https://codeberg.org/Ka-Ubuntu/kaubuntu.re[kaubuntu.re^] du mouvement https://kaubuntu.com[Ka-Ubuntu^]. La licence d'origine (GNU AGPL v3) est conservée et respectée.

[cols="1,3",options="header"]
|===
| Élément | Détail

| Dépôt upstream
| https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI

| Dépôt de ce fork
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`

| Domaine (exemple)
| https://example.com

| Gouvernance
| Application maintenue par OKI ; fork porté par le propriétaire du dépôt *cedric* (Cédric Famibelle-Pronzola)

| Licence
| https://www.gnu.org/licenses/agpl-3.0.html[GNU Affero General Public License v3^] (AGPL-V3) ou ultérieure
|===

=== 🔗 Sources et instances utilisées

Le hub est configuré par défaut pour agréger les sources du podcast ANNU KUTE CED :

[cols="1,2,3",options="header"]
|===
| Service | Instance | Compte / chaîne

| 🎥 PeerTube (vidéos, lives)
| GADE — `https://gade.o-k-i.net`
| https://gade.o-k-i.net/c/annu_kute_ced/videos[`annu_kute_ced`^]

| 🎙️ Castopod (épisodes audio)
| KUTE — `https://kute.o-k-i.net`
| https://kute.o-k-i.net/@annu_kute_cedric[`@annu_kute_cedric`^]

| 📡 Mastodon (timeline)
| BOKANTE — `https://bokante.o-k-i.net`
| https://bokante.o-k-i.net/@cedric[`@cedric`^]

| 🎵 Funkwhale (musique, optionnel, désactivé par défaut)
| MIZIK — `https://mizik.o-k-i.net`
| morceaux aléatoires de l'instance
|===

Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-configuration,Configuration>>).

=== ✨ Fonctionnalités

==== 🎥 Vidéos PeerTube

- Vidéos récentes, tendances et par catégories (configurables)
- *Shorts* : carrousel dédié aux vidéos portrait de moins de 3 minutes (défilement tactile)
- Page vidéo complète : lecteur PeerTube embarqué, description Markdown, badge de licence Creative Commons, commentaires (lecture seule), vidéos suggérées
- *Téléchargement* : fichiers directs et playlists HLS, avec résolution et taille
- *Partage* : copie de lien, code d'intégration, e-mail, Facebook, X, WhatsApp, LinkedIn, Telegram
- Recherche plein texte et recherche par hashtag (préfixe `#`)
- Pagination AJAX « Voir plus » (protégée par jeton CSRF et vérification d'origine)

==== 📺 Directs et annonces

- Page *Direct* (`/direct`) : embed du live PeerTube s'il est en cours, avec autoplay
- *Annonce du prochain live* : date et heure converties automatiquement pour 5 territoires (Ma'ohi Nui, Martinique/Guadeloupe, Guyane, France, Kanaky), image personnalisable (4:5, 1:1, 16:9)
- Section *hero* d'accueil configurable : live, vidéo unique, playlist (PeerTube / Funkwhale / Castopod) ou masquée

==== 🎙️ Podcast et audio

- *Castopod* : derniers épisodes via flux RSS, avec *lecteur audio intégré* (lecture/pause, avance automatique à l'épisode suivant, un seul flux à la fois)
- *Funkwhale* (optionnel) : sélection aléatoire de morceaux de l'instance, avec le même lecteur intégré

==== 📡 Réseaux sociaux et contenus externes

- *Timeline Mastodon* intégrée (bibliothèque vendored https://gitlab.com/idotj/mastodon-embed-timeline[mastodon-embed-timeline^] v4.7.0, AGPLv3)
- *Compatibilité Pleroma* : adaptateur qui convertit les réponses de l'API Pleroma au format Mastodon attendu par la timeline
- *WordPress* (optionnel) : derniers articles via l'API REST, avec image à la une et auteur

==== 📱 PWA et confort d'utilisation

- *Progressive Web App* installable (bouton d'installation automatique)
- Mode hors ligne : pages et ressources statiques en cache via Service Worker
- Indicateur visuel de perte de connexion
- *Mode sombre* (basé sur `prefers-color-scheme`, mémorisé en `localStorage`)
- Interface entièrement responsive (mobile, tablette, desktop)

==== 💝 Dons, compte à rebours et divers

- *Page de dons* : LiberaPay, Ko-fi et Stripe (dons ponctuels et mensuels, montants suggérés)
- *Compte à rebours* : page de lancement/maintenance multi-fuseaux qui verrouille le site jusqu'à la date cible
- Bloc « À propos » configurable (titre, deux paragraphes, image légendée)

==== 🔍 SEO et données structurées

- *JSON-LD* généré automatiquement : `WebSite` (avec `SearchAction`), `PodcastSeries` et `PodcastEpisode` (avec `AudioObject`) pour le podcast Castopod, `VideoObject` (avec `embedUrl`), `CollectionPage`, `BreadcrumbList`, `Organization`
- *Meta descriptions* sur toutes les pages et *URL canoniques* (`link rel="canonical"`) ; pages de résultats de recherche en `noindex, follow`
- Balises *Open Graph* et *Twitter Cards*
- `sitemap.xml` et `robots.txt` fournis en exemples
- Prêt pour *Plausible Analytics* (voir <<fr-analytics,Analytics>>) : désactivé par défaut, sans cookies et conforme RGPD

==== 🛡️ Sécurité

- En-têtes de sécurité complets : CSP dynamique avec *nonce* par requête, HSTS, `X-Frame-Options`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`
- Protection *CSRF* sur les requêtes AJAX, vérification de l'en-tête `Origin`
- Validation anti-*SSRF* des URLs d'instances et liste blanche des endpoints de l'API PeerTube
- Échappement systématique des sorties (anti-XSS)
- Exemples de configuration sécurisée pour Apache et Nginx dans `conf/`

=== 🛠️ Technologies utilisées

- 📄 HTML5, 🎨 CSS3 (Media Queries), ⚡ JavaScript vanilla (sans framework, sans jQuery)
- 🐘 PHP 7.4+ (aucune base de données requise)
- 🔧 Service Worker (cache offline) et 📋 Web App Manifest (installation)
- 📦 Bibliothèques :
  * 🎯 Font Awesome 7 (icônes, via CDN cdnjs)
  * 📡 mastodon-embed-timeline v4.7.0 (vendored dans `js/`, AGPLv3)
  * 📊 Plausible Analytics (pré-configuré, à activer manuellement)

=== 📁 Structure du projet

[source]
----
├── .gitea/
│   └── workflows/                  # CI/CD Gitea Actions (check-pr, deploy-prod)
├── ajax/
│   └── load-more-videos.php        # Endpoint AJAX « Voir plus » (CSRF + Origin)
├── conf/
│   ├── .htaccess.sample            # Configuration Apache sécurisée
│   └── nginx.conf.sample           # Configuration Nginx sécurisée
├── css/                            # Feuilles de style (styles, vidéo, catégories,
│   │                               #   recherche, dons, countdown, mastodon,
│   ├── castopod-podcasts.css       #   castopod, funkwhale, wordpress, grille…)
│   └── …
├── docs/
│   ├── wireframes/                 # Maquettes d'origine (PNG desktop + mobile)
│   ├── docinfo.html                # Styles d'impression (fallback emoji) pour l'export
│   └── generate-readme-pdf.sh      # Export de la doc : README.html / README.pdf
├── img/                            # Logo, favicons, icône de lecture
├── includes/
│   ├── config.php                  # Bootstrap + fonctions API (PeerTube, Castopod,
│   │                               #   Funkwhale) et formatage
│   ├── config.default.php          # Valeurs par défaut de toutes les constantes
│   ├── config.local.php.sample     # Modèle de configuration locale (à copier)
│   ├── security.php                # CSRF, CSP + nonce, en-têtes, validateurs
│   ├── simple-cache.php            # Cache fichier de l'API (cache/api/)
│   ├── wordpress.php               # Intégration API REST WordPress
│   ├── structured-data.php         # Générateurs JSON-LD
│   ├── header.php                  # Barre supérieure (recherche, réseaux, thème…)
│   ├── footer.php                  # Pied de page
│   ├── sidebar.php                 # Navigation latérale
│   ├── mobile-menu.php             # Menu mobile coulissant
│   ├── hero-section.php            # Bannière d'accueil (live/vidéo/playlist)
│   ├── featured-videos.php         # ⚠️ legacy, non utilisé par les pages actuelles
│   ├── recent-videos.php           # ⚠️ legacy, non utilisé par les pages actuelles
│   ├── pwa-init.php                # ⚠️ legacy, PWA désormais inline dans index.php
│   └── lib/
│       └── markdown.php            # Rendu Markdown (sous-ensemble PeerTube)
├── js/
│   ├── main.js                     # UI globale (thème, carrousel, « Voir plus »…)
│   ├── audio-player.js             # Lecteur audio Castopod / Funkwhale
│   ├── categories.js               # « Voir plus » de la page catégorie
│   ├── countdown.js                # Compte à rebours
│   ├── search.js                   # Cartes cliquables de la recherche
│   ├── mastodon-config.php         # Configuration JS de la timeline (servie en JS)
│   ├── mastodon-timeline.umd.js    # mastodon-embed-timeline v4.7.0 (vendored)
│   ├── pleroma-adapter.js          # Adaptateur Pleroma → API Mastodon
│   └── pwa-update.js               # Enregistrement SW + modal de mise à jour
├── scripts/
│   └── check.sh                    # Vérifications du CI en local (avant push)
├── uploads/                        # Images d'annonces (non versionné, hors .gitkeep)
├── index.php                       # Page d'accueil (agrégateur)
├── video.php                       # Page de lecture d'une vidéo
├── categories.php                  # Page d'une catégorie
├── recherche.php                   # Page de recherche
├── direct.php                      # Page du direct / annonce du prochain live
├── countdown.php                   # Page de compte à rebours (maintenance)
├── sw.js                           # Service Worker (PWA)
├── site.webmanifest.sample         # Exemple de manifest PWA
├── browserconfig.xml               # Tuiles Windows
├── sitemap.xml.sample              # Exemple de sitemap
├── robots.txt.sample               # Exemple de robots.txt
├── mentions-legales.php.sample     # Exemple de mentions légales
├── dons.php.sample                 # Exemple de page de dons
├── LICENSE                         # GNU AGPL v3
├── DEPLOY.adoc                     # Guide de déploiement serveur + CI/CD
└── README.adoc
----

NOTE: Les fichiers marqués « legacy » sont conservés pour référence mais ne sont inclus par aucune page ; la page d'accueil rend ces sections en interne.

=== 📋 Prérequis

- 🐘 *PHP 7.4+* (recommandé : PHP 8.0+)
- 📦 *Extensions PHP requises :*
  * `curl` — appels API (PeerTube, Castopod, Funkwhale, WordPress)
  * `json` — traitement des réponses
  * `intl` — dates internationales et fuseaux horaires
  * `mbstring` — chaînes multi-octets
  * `xml` (SimpleXML) — recommandée pour Castopod (un fallback regex existe)
- 🌐 *Serveur web* : Nginx (recommandé) ou Apache
- 🔒 *HTTPS* : requis pour les fonctionnalités PWA

.Installation de l'extension `intl`
[source,bash]
----
# Ubuntu / Debian
sudo apt-get install php-intl

# Fedora / RHEL / CentOS
sudo dnf install php-intl

# Redémarrer le serveur web
sudo systemctl restart apache2        # Apache
sudo systemctl restart php8.3-fpm     # Nginx + php-fpm

# Vérifier
php -m | grep intl
----

=== 🚀 Installation

. 📥 Clonez le dépôt :
+
[source,bash]
----
git clone git@labola.o-k-i.net:cedric/annu-kute-ced.git
cd annu-kute-ced
----
. 📦 Vérifiez les prérequis PHP (voir ci-dessus).
. 🔧 Créez votre configuration locale et les fichiers d'instance :
+
[source,bash]
----
cp includes/config.local.php.sample includes/config.local.php
cp site.webmanifest.sample site.webmanifest
cp robots.txt.sample robots.txt
cp sitemap.xml.sample sitemap.xml
cp mentions-legales.php.sample mentions-legales.php
cp dons.php.sample dons.php
----
. 🛡️ Installez la configuration serveur sécurisée depuis `conf/` (voir <<fr-securite,Sécurité>>).
. ✍️ Rendez le dossier `cache/` accessible en écriture par le serveur web (créé automatiquement au premier appel API).
. 🔒 Assurez-vous que HTTPS est actif (obligatoire pour la PWA).
. 🌐 Faites pointer votre domaine (ex. `example.com`) vers la racine du projet.

[[fr-configuration]]
=== ⚙️ Configuration

==== Le système en trois fichiers

[cols="1,2,2",options="header"]
|===
| Fichier | Rôle | Versionné ?

| `includes/config.php`
| Bootstrap : charge les autres fichiers et contient toutes les fonctions métier
| ✅ Oui

| `includes/config.default.php`
| Valeurs par défaut de chaque constante (définies seulement si absentes)
| ✅ Oui

| `includes/config.local.php`
| Vos surcharges locales — *prioritaires* car chargées en premier
| ❌ Non (`.gitignore`)
|===

Pour personnaliser l'instance, ne modifiez jamais `config.default.php` : définissez la constante dans `config.local.php`. Comme ce dernier est chargé avant, vos valeurs ont toujours la priorité.

[source,bash]
----
cp includes/config.local.php.sample includes/config.local.php
----

==== Référence des constantes

Valeurs par défaut issues de `includes/config.default.php`.

.Application et organisation
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `APP_HOST_NAME`
| `'example.com'`
| Nom de domaine affiché (mentions légales…)

| `ORGANIZATION_SHORT_NAME`
| `'ANNU KUTE CED'`
| Nom court de l'organisation

| `ORGANIZATION_NAME`
| `'ANNU KUTE CED'`
| Nom complet de l'organisation
|===

.PeerTube
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `PEERTUBE_URL`
| `'https://gade.o-k-i.net'`
| URL de l'instance PeerTube (obligatoire)

| `PEERTUBE_DISPLAY_NAME`
| `'gade.o-k-i.net'`
| Nom affiché de l'instance

| `API_KEY`
| `''`
| Clé API optionnelle (`Authorization: ApiKey …`)

| `SHOW_VIDEO_VIEWS`
| `false`
| Afficher le nombre de vues sur les cartes vidéo

| `PRIORITY_CATEGORIES`
| `[10, 15]`
| Catégories mises en avant : `[ID => 'Nom']`, ordre = ordre d'affichage (accueil, sidebar, footer, menu mobile)

| `DEFAULT_TIMEZONE`
| `'America/Guadeloupe'`
| Fuseau horaire d'affichage (https://www.php.net/manual/fr/timezones.php[liste PHP^])
|===

.Section hero (bannière d'accueil)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `HERO_TYPE`
| `'live'`
| `'live'` · `'video'` · `'playlist'` · `'none'`

| `LIVE_ACCOUNT_NAME`
| `'annu_kute_ced'`
| Compte PeerTube surveillé pour les directs (hero + page `/direct`)

| `HERO_VIDEO_ID`
| `'fDE6tvQMXRuBpE4eDDS7kj'`
| UUID de la vidéo (mode `video`), extrait de l'URL `/w/<uuid>`

| `HERO_VIDEO_TITLE`
| `'Vidéo de présentation'`
| Titre (accessibilité)

| `PLAYLIST_PLATFORM`
| `'peertube'`
| `'peertube'` · `'funkwhale'` · `'castopod'`

| `PLAYLIST_INSTANCE_URL`
| `''`
| URL de base de la plateforme de playlist

| `PLAYLIST_ID`
| `''`
| ID de la playlist ; pour Castopod : `'@slug'`

| `PLAYLIST_TITLE` / `PLAYLIST_DESCRIPTION`
| `''` / `''`
| Textes optionnels (sinon récupérés via API)
|===

.Castopod (podcast)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `CASTOPOD_ENABLED`
| `true`
| Activer la section podcast de l'accueil

| `CASTOPOD_URL`
| `'https://kute.o-k-i.net'`
| URL de l'instance Castopod

| `CASTOPOD_PODCAST_SLUGS`
| `['annu_kute_cedric']`
| Slug(s) de podcast ; flux RSS = `{URL}/@{slug}/feed`. Plusieurs podcasts : épisodes fusionnés et triés par date

| `CASTOPOD_EPISODES_COUNT`
| `5`
| Nombre total d'épisodes affichés
|===

WARNING: Castopod applique un *rate limit* strict (HTTP 429) sur les flux RSS. Le code patiente 5 s entre chaque podcast. *Activez le cache* (`CACHE_ENABLED`) pour éviter ces délais à chaque page.

.Funkwhale (musique, optionnel)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `FUNKWHALE_ENABLED`
| `false`
| Activer la section musique

| `FUNKWHALE_URL`
| `'https://mizik.o-k-i.net'`
| URL de l'instance Funkwhale

| `FUNKWHALE_TRACKS_COUNT`
| `10`
| Morceaux aléatoires locaux affichés (50 mis en cache)
|===

.Mastodon (timeline)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `MASTODON_INSTANCE_URL`
| `'https://bokante.o-k-i.net'`
| Instance Mastodon (ou Pleroma, via l'adaptateur)

| `MASTODON_URL`
| `'https://bokante.o-k-i.net/@cedric'`
| URL publique du compte (liens « Voir plus », footer)

| `MASTODON_DATE_FORMAT`
| `'fr-FR'`
| Locale de date des posts

| `MASTODON_BTN_SEE_MORE` / `MASTODON_BTN_RELOAD`
| `'Voir plus de post'` / `'Rafraichir'`
| Libellés des boutons

| `MASTODON_MAX_POST_FETCH` / `MASTODON_MAX_POST_SHOW`
| `'10'` / `'10'`
| Posts récupérés / affichés

| `MASTODON_S3_MEDIA_URL`
| _(non défini)_
| Origine S3 des médias (ajoutée à la CSP `media-src`)
|===

NOTE: La timeline n'a pas de flag d'activation : elle est toujours affichée sur l'accueil.

.WordPress (optionnel)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `WORDPRESS_ENABLED`
| `false`
| Activer la section articles

| `WORDPRESS_URL`
| `''`
| URL du site WordPress (sans `/` final)

| `WORDPRESS_POSTS_COUNT`
| `6`
| Nombre d'articles (API REST `/wp-json/wp/v2/`)
|===

.Dons
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `DONATIONS_ENABLED`
| `false`
| Activer le système (icône cœur, lien « Soutenir », page `/dons.php`)

| `LIBERAPAY_URL` / `KOFI_URL`
| `''` / `''`
| URLs des plateformes externes

| `STRIPE_ENABLED`
| `false`
| Activer l'interface Stripe (onglets ponctuel / mensuel)

| `STRIPE_ONE_TIME_LINKS`
| `[1, 5, 10, 20, 50, 'custom'] => ''`
| Liens de paiement Stripe par montant

| `STRIPE_MONTHLY_LINKS`
| `[1, 5, 10, 20, 50] => ''`
| Liens d'abonnement mensuel Stripe

| `DONATION_AMOUNTS`
| `[1, 5, 10, 20, 50]`
| Montants suggérés

| `DONATION_CURRENCY`
| `'EUR'`
| Devise
|===

NOTE: Au moins une plateforme (LiberaPay, Ko-fi ou Stripe) doit être configurée pour que `dons.php` s'affiche, sinon la page renvoie une erreur 500.

.Compte à rebours (maintenance / lancement)
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `COUNTDOWN_ENABLED`
| `false`
| ⚠️ Si `true`, *toutes* les pages redirigent vers `countdown.php`

| `COUNTDOWN_TARGET_DATE`
| `'2025-10-11 00:00:00'`
| Date cible (`Y-m-d H:i:s`) ; à la fin, redirection vers `/`

| `COUNTDOWN_TIMEZONES`
| 5 territoires
| `[Libellé => fuseau PHP]` affichés sur la page
|===

.Annonce du prochain live
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `NEXT_LIVE_ENABLED`
| `false`
| Afficher l'annonce quand aucun direct n'est en cours

| `NEXT_LIVE_TITLE` / `NEXT_LIVE_DESCRIPTION`
| `'Prochain live'` / `'Rejoignez-nous…'`
| Textes (date et heure ajoutées automatiquement)

| `NEXT_LIVE_DATE`
| `''`
| Date du live (`Y-m-d H:i:s`)

| `NEXT_LIVE_IMAGE`
| `'uploads/next-live.jpg'`
| Image d'annonce (utilisée seulement si le fichier existe)
|===

.Site, réseaux sociaux et contact
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `SITE_NAME`
| `'ANNU KUTE CED'`
| Nom du site (titres, meta)

| `SITE_DESCRIPTION`
| `'Hub multimédia du podcast ANNU KUTE CED'`
| Description SEO

| `SITE_LOGO` / `SITE_FAVICON`
| `'img/logo.png'` / `'img/favicon.png'`
| Chemins des images

| `FACEBOOK_URL`, `X_URL`, `INSTAGRAM_URL`, `YOUTUBE_URL`, `TIKTOK_URL`
| `'#'`
| `'#'` ou vide = icône masquée

| `CONTACT_EMAIL`
| `'kontak@o-k-i.net'`
| E-mail de contact (footer)
|===

.Mentions légales
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `LEGAL_COPYRIGHT`
| `'ANNU KUTE CED'`
| Titulaire du copyright

| `LEGAL_WEBMASTER_NAME` / `LEGAL_WEBMASTER_EMAIL`
| `'Cédric Famibelle-Pronzola'` / `'contact@cedric-pronzola.dev'`
| Webmaster

| `LEGAL_HOST_NAME`, `LEGAL_HOST_COMPANY`, `LEGAL_HOST_RCS`, `LEGAL_HOST_ADDRESS`
| valeurs o2Switch
| Coordonnées de l'hébergeur

| `LEGAL_CONTACT_EMAIL`
| `'kontak@o-k-i.net'`
| Contact juridique

| `LEGAL_LICENSE` / `LEGAL_LICENSE_URL`
| AGPL-V3 + URL GNU
| Licence du code

| `LEGAL_SOURCE_CODE_URL`
| `'https://labola.o-k-i.net/cedric/annu-kute-ced'`
| Dépôt du code source (exigé par l'AGPL pour un service réseau)

| `LEGAL_SERVICE_DESCRIPTION`
| `'est le hub multimédia du podcast ANNU KUTE CED.'`
| Phrase de description du service
|===

.Tags et filtres
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `TAG_INDEPENDENCE`
| `'indépendance'`
| Tag de la fonction `getIndependenceVideos()` (pas de section d'accueil associée actuellement)

| `SHORTS_MAX_DURATION`
| `180`
| Durée max d'un short (s) ; ratio portrait (`aspectRatio ≤ 1`) aussi requis

| `IMPORTANT_TAGS`
| `['ANNUKUTECED']`
| Hashtags de la sidebar, du footer et du menu mobile

| `POPULAR_TAGS`
| `['ANNUKUTECED']`
| Hashtags affichés sur l'accueil
|===

.Pagination et volumes d'affichage
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `VIDEOS_PER_PAGE`
| `12`
| Vidéos par page (recherche)

| `COUNT_VIDEO_SEARCH`
| `20`
| Résultats récupérés par recherche

| `RECENT_VIDEOS_COUNT` / `TRENDING_VIDEOS_COUNT` / `CATEGORY_VIDEOS_COUNT`
| `6`
| Taille des sections d'accueil

| `SHORTS_COUNT` / `SHORTS_COUNT_SEARCH`
| `6` / `100`
| Shorts affichés / pool de recherche

| `LOAD_MORE_COUNT`
| `6`
| Vidéos chargées par clic sur « Voir plus »

| `FEATURED_VIDEOS_COUNT` / `INDEPENDENCE_VIDEOS_COUNT`
| `6`
| Réservés (sections non affichées actuellement)
|===

.Cache
[cols="2,2,3",options="header"]
|===
| Constante | Défaut | Description

| `CACHE_ENABLED`
| `true`
| Active le cache pour *Castopod et Funkwhale*. Indispensable contre le rate limit RSS de Castopod ; les résultats vides (erreur, 429) ne sont jamais mis en cache

| `CACHE_DURATION`
| `3600`
| Durée de vie du cache (s)
|===

NOTE: Le cache de l'API PeerTube et de WordPress (`includes/simple-cache.php`) est *toujours actif*, indépendamment de `CACHE_ENABLED`, avec des TTL par endpoint (catégories 1 h, vidéos/recherche 10 min, articles WP 15 min, comptes 5 min). Les fichiers sont stockés dans `cache/api/`.

.Bloc « À propos » (pas de valeur par défaut — bloc masqué tant que non défini)
[cols="2,3",options="header"]
|===
| Constante | Description

| `MOVEMENT_TITLE`
| Titre du bloc (défaut template : « À propos »)

| `MOVEMENT_DESCRIPTION`
| Premier paragraphe ; *commenter cette ligne masque tout le bloc*

| `MOVEMENT_DESCRIPTION_2`
| Second paragraphe (optionnel)

| `MOVEMENT_IMAGE` / `MOVEMENT_IMAGE_ALT` / `MOVEMENT_CAPTION`
| Image (`img/movement_presentation.png`, non versionnée), texte alternatif et légende (HTML simple accepté)
|===

==== Exemple complet de `config.local.php`

[source,php]
----
<?php
define('APP_HOST_NAME', 'example.com');

// PeerTube (chaîne GADE du podcast)
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
define('PEERTUBE_DISPLAY_NAME', 'gade.o-k-i.net');

// Hero : direct de la chaîne, sinon annonce du prochain live
define('HERO_TYPE', 'live');
define('LIVE_ACCOUNT_NAME', 'annu_kute_ced');
define('NEXT_LIVE_ENABLED', true);
define('NEXT_LIVE_TITLE', 'Prochain live');
define('NEXT_LIVE_DESCRIPTION', 'Enregistrement du prochain épisode !');
define('NEXT_LIVE_DATE', '2025-10-11 10:00:00');

// Castopod (compte KUTE du podcast)
define('CASTOPOD_ENABLED', true);
define('CASTOPOD_URL', 'https://kute.o-k-i.net');
define('CASTOPOD_PODCAST_SLUGS', ['annu_kute_cedric']);
define('CASTOPOD_EPISODES_COUNT', 5);

// Mastodon (compte BOKANTE)
define('MASTODON_INSTANCE_URL', 'https://bokante.o-k-i.net');
define('MASTODON_URL', 'https://bokante.o-k-i.net/@cedric');

// Cache : indispensable pour Castopod (rate limit RSS)
define('CACHE_ENABLED', true);
define('CACHE_DURATION', 3600);
----

=== 🎨 Personnalisation de l'apparence

. 🖼️ Remplacez `img/logo.png` par votre logo (et les favicons du dossier `img/`)
. 🎨 Modifiez les couleurs dans `css/styles.css`
. 📝 Textes : la plupart passent par les constantes de `config.local.php` (bloc « À propos », libellés Mastodon, etc.)

==== Fichiers d'instance à personnaliser

Les fichiers `sitemap.xml`, `robots.txt`, `site.webmanifest`, `mentions-legales.php` et `dons.php` sont ignorés par Git : copiez les `.sample` puis adaptez-les. Les exemples fournis utilisent le domaine générique `example.com` : remplacez-le par votre domaine réel. Dans `mentions-legales.php`, remplacez aussi le placeholder `VOTRE-DATE-MAJ`.

=== 🧭 Ordre des sections sur la page d'accueil

La page d'accueil assemble les sections dans cet ordre, chacune conditionnée par sa configuration :

. *Hero* — si `HERO_TYPE` ≠ `'none'` (live, vidéo ou playlist)
. *Podcasts Castopod* — si `CASTOPOD_ENABLED` et `CASTOPOD_URL` non vide
. *Morceaux Funkwhale* — si `FUNKWHALE_ENABLED` et `FUNKWHALE_URL` non vide
. *Timeline Mastodon* — toujours affichée
. *Articles WordPress* — si `WORDPRESS_ENABLED` et `WORDPRESS_URL` non vide
. *Hashtags populaires* (`POPULAR_TAGS`)
. *Shorts* — carrousel
. *Dernières vidéos* + bouton « Voir plus »
. *Tendances* + « Voir plus »
. *Une section par catégorie prioritaire* (`PRIORITY_CATEGORIES`, seulement si elle contient des vidéos) + « Voir plus »
. *Bloc « À propos »* (si `MOVEMENT_DESCRIPTION` défini) + aside hashtags

Mise en page desktop : hero/Castopod/Funkwhale + Mastodon + WordPress s'organisent en colonnes (1fr 2fr, ou 1fr 2fr 1fr avec WordPress) ; empilés verticalement sur mobile.

[[fr-securite]]
=== 🛡️ Sécurité

==== Ce que l'application applique déjà

- *CSP dynamique* : `default-src 'self'` avec *nonce* unique par requête pour les scripts/styles inline ; origines PeerTube/Mastodon/CDN ajoutées automatiquement ; assouplissements `localhost` pour le développement
- *En-têtes* : `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy` restrictive, HSTS (HTTPS uniquement), CORP/COOP `same-origin`
+
NOTE: L'en-tête COEP a été *volontairement retiré* (commit `62d4d99`) : `require-corp` bloquait les embeds cross-origin de PeerTube, Mastodon, Castopod et Funkwhale.
- *CSRF* : jeton de session (`random_bytes(32)`) injecté en `<meta>`, vérifié par `hash_equals` sur l'endpoint AJAX ; vérification de l'en-tête `Origin`
- *Anti-SSRF* : validation des URLs d'instances (schéma http/https, blocage IP privées et `localhost`), liste blanche des endpoints de l'API PeerTube, cURL sans redirections
- *Anti-XSS* : échappement systématique des sorties ; validation des entrées (UUID vidéo, requête de recherche ≤ 200 car., numéros de page, ID de catégorie 1–20)

==== Configuration serveur (Nginx, recommandé)

Le fichier `conf/nginx.conf.sample` est l'équivalent Nginx complet du `.htaccess` : mêmes protections (fichiers de configuration et `.sample`, dossiers `/includes/`, `/cache/`, `/docs/`, `/conf/`, fichiers cachés, pas de listing), HTTPS forcé, masquage de l'extension `.php` (URLs propres `/video` au lieu de `/video.php`), `no-cache` pour `sw.js` et `site.webmanifest`, plus le cache des assets et gzip.

[source,bash]
----
# Adaptez les chemins dans conf/nginx.conf.sample puis :
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/example.com
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
----

==== Configuration serveur (Apache, alternative)

[source,bash]
----
cp conf/.htaccess.sample .htaccess
----

Protections incluses : blocage des fichiers de configuration et `.sample`, des dossiers `/includes/`, `/cache/`, `/docs/`, `/conf/` ; pas de listing de répertoires ; fichiers cachés bloqués ; HTTPS forcé ; masquage de l'extension `.php`.

WARNING: Cette configuration est *essentielle* : sans elle, vos fichiers de configuration sont exposés aux visiteurs.

=== 📱 Progressive Web App (PWA)

==== Fonctionnalités

- 📲 *Installation native* : bouton « Installer » dans le header à la première visite
- 🌐 *Mode hors ligne* : cache des pages visitées et des ressources statiques
- 📡 *Détection de connexion* : indicateur visuel en cas de perte réseau
- 🔄 *Mise à jour sans friction* : quand une nouvelle version est déployée, un modal stylé la propose ; en acceptant, l'utilisateur la reçoit immédiatement — *aucune purge manuelle du cache navigateur nécessaire*

==== Stratégies de cache du Service Worker (`sw.js`)

[cols="1,2,3",options="header"]
|===
| Type de requête | Stratégie | Détail

| Ressources statiques (`/css/`, `/js/`, `/img/`, CDN cdnjs)
| *Cache First*
| Mise en cache à l'installation et au premier accès ; repli sur `img/logo.png` si une image échoue

| Pages (`/`, `*.php`…)
| *Network First*
| Réseau d'abord, cache en secours hors ligne

| API et AJAX (`/ajax/`, `mastodon-config.php`…)
| *Network Only*
| Jamais de cache ; réponse JSON 503 synthétique hors ligne
|===

==== Cycle de mise à jour automatique

Le mécanisme (`js/pwa-update.js` + `sw.js`) garantit que les visiteurs reçoivent les nouvelles versions *sans purger manuellement le cache de leur navigateur* :

. Au déploiement, *bumpez* le suffixe de version des noms de cache dans `sw.js` (format `JJMMAAAA-HHMM`, ex. `24072026-0612`)
. Au chargement d'une page, le navigateur détecte le nouveau `sw.js` et installe le nouveau Service Worker en arrière-plan ; il reste *en attente* (pas de `skipWaiting()` automatique)
. Un *modal* propose la mise à jour à l'utilisateur :
  * *« Mettre à jour »* → le SW en attente reçoit `SKIP_WAITING`, purge les anciens caches à l'activation, prend le contrôle (`controllerchange`) et la page se recharge sur la nouvelle version
  * *« Plus tard »* → le modal se ferme ; la mise à jour est reproposée au prochain chargement
. Une mise à jour déjà installée lors d'une visite précédente mais jamais activée est également signalée

WARNING: Pour que la détection fonctionne, `sw.js` ne doit *pas* être caché longtemps côté serveur : les configurations fournies (`conf/nginx.conf.sample`, `conf/.htaccess.sample`) le servent en `no-cache` (ainsi que `site.webmanifest`). Si vous utilisez votre propre configuration serveur, reproduisez cette règle.

==== Installation par les visiteurs

- 🌐 *Chrome/Edge* : menu → « Installer example.com »
- 🍎 *Safari iOS* : Partager → « Ajouter à l'écran d'accueil »
- 🦊 *Firefox Android* : menu → « Installer »

[[fr-analytics]]
=== 📊 Analytics (Plausible)

Le site est *pré-configuré* pour https://plausible.io[Plausible Analytics^] (sans cookies, conforme RGPD, données anonymes), mais le script est *désactivé par défaut*.

Pour l'activer :

. Ouvrez `index.php` et *décommentez* le snippet Plausible (balise `<script defer data-domain=… src="https://plausible.io/js/script…js">` et son shim `window.plausible`).
. C'est tout : la CSP autorise déjà `https://plausible.io` en `script-src` et `connect-src`, et le `data-domain` utilise automatiquement le domaine courant.

Données alors collectées (anonymes) : pages visitées, pays d'origine (IP non stockée), type d'appareil, navigateur, temps passé.

=== 🚀 Déploiement sur serveur mutualisé

. 🐘 Vérifiez que l'hébergeur fournit PHP 7.4+ et les extensions requises
. 🔒 Activez HTTPS (obligatoire pour la PWA)
. 📤 Transférez les fichiers via FTP à la racine du site
. 🔧 Permissions : `644` pour les fichiers, `755` pour les dossiers ; `cache/` accessible en écriture
. 📄 Créez les fichiers d'instance depuis les `.sample` (config, manifest, robots, sitemap, mentions légales, dons)
. 🛡️ Copiez `conf/.htaccess.sample` en `.htaccess` (Apache)
. 🌐 Faites pointer votre domaine (ex. `example.com`) vers le dossier d'installation
. 🧪 Testez la PWA via les outils de développement du navigateur (onglet Application)

=== 👨‍💻 Développement et contribution

. 🌿 Créez une branche : `git checkout -b ma-fonctionnalite`
. 💾 Committez avec des *messages atomiques en anglais*, au format conventionnel (< 70 caractères) :
  * `feat: add …`, `fix: prevent …`, `docs: update …`, `refactor: …`, `typo: …`
. 📤 Poussez : `git push origin ma-fonctionnalite`
. 🔀 Ouvrez une pull request

Bonnes pratiques du dépôt :

- Les remontées de bugs et propositions sont les bienvenues sur le dépôt du fork ; les améliorations génériques peuvent être proposées en upstream (FEDIVERSE OKI)
- Ne versionnez jamais `config.local.php` ni les fichiers dérivés des `.sample`
- Quelques constantes sont définies mais non utilisées par le code actuel (`ENABLE_SEARCH`, `ENABLE_COMMENTS`, `ENABLE_USER_ACCOUNTS`, `TAG_SHORT`) : ne vous en servez pas comme points d'extension

==== 📄 Exporter la documentation (HTML / PDF)

Ce README peut être exporté en HTML et en PDF en une seule commande :

[source,bash]
----
docs/generate-readme-pdf.sh           # README.html + README.pdf
docs/generate-readme-pdf.sh --html    # HTML seulement
docs/generate-readme-pdf.sh --pdf     # PDF seulement
----

Les fichiers sont produits à la racine du projet et ignorés par Git (`.gitignore`).

Prérequis : `asciidoctor`, `chromium` (rendu headless) et le paquet `fonts-noto-color-emoji` (émojis couleur dans le PDF). La feuille de style `docs/docinfo.html` (fallback emoji + règles d'impression) est injectée automatiquement via le mécanisme `docinfo` d'Asciidoctor.

NOTE: `asciidoctor-pdf` (utilisé par l'extension AsciiDoc de VSCodium) n'est pas utilisable ici : son moteur Prawn ne sait pas embarquer les polices emoji couleur (CBDT/COLR), les émojis disparaissent du PDF. Chromium les gère nativement.

==== ✅ Vérifications locales et CI/CD

Avant de pousser, exécutez les mêmes vérifications que le CI :

[source,bash]
----
scripts/check.sh
----

Ce script lance : lint PHP (`php -l` sur tous les fichiers, samples inclus), lint JS (`node --check`), rendu AsciiDoc, validation JSON (`site.webmanifest.sample`) et XML (`sitemap.xml.sample`, `browserconfig.xml`), shellcheck. Un outil manquant est signalé et le check correspondant ignoré ; le CI, lui, exécute tout.

Pipelines Gitea Actions (`.gitea/workflows/`) :

- *Vérification PR* (`check-pr.yml`) : sur toute pull request vers `main`
- *Déploiement PROD* (`deploy-prod.yml`) : sur push sur `main` — mêmes vérifications, puis déploiement en SSH sur le serveur (`git pull --ff-only` + bump de version du Service Worker, qui déclenche le modal de mise à jour chez les visiteurs)

La mise en place complète du serveur de production (clone, fichiers d'instance, clés SSH, secrets Gitea) est documentée dans link:DEPLOY.adoc[DEPLOY.adoc].

=== 📜 Licence

Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*

Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la licence publique générale GNU Affero publiée par la Free Software Foundation, soit la version 3 de la licence, soit (à votre choix) toute version ultérieure.

Ce programme est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de COMMERCIALISATION ou d'ADAPTATION À UN USAGE PARTICULIER. Voir la https://www.gnu.org/licenses/agpl-3.0.html[licence publique générale GNU Affero^] pour plus de détails (texte intégral dans `LICENSE`).

=== 📞 Contact

Pour toute question ou suggestion concernant cette plateforme : mailto:kontak@o-k-i.net[kontak@o-k-i.net]

<<top,⬆ Retour au sommaire>>

'''

[[en]]
== 🇬🇧 English version

=== 📖 Description

*ANNU KUTE CED* is a responsive web interface acting as a *multimedia hub* for the podcast of the same name. It brings together in a single place:

- 🎥 the podcast's *PeerTube* channel (videos, shorts, live streams);
- 🎙️ its *Castopod* account (latest episodes, built-in audio playback);
- 📡 its *Mastodon* timeline (news and announcements).

The platform is lightweight, deployable on shared hosting (PHP + web server, no database), and optimized for both mobile and desktop. It is also an installable *Progressive Web App* with offline support.

🎯 *Mission*: provide a single, free and decentralized entry point to discover, listen to and follow the ANNU KUTE CED podcast without relying on Big Tech platforms.

=== 🌳 Project origin

This application is a *fork* of https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI[*FEDIVERSE OKI*^], developed by the https://o-k-i.net[ORGANISATION KA INTERNATIONALE (OKI)^], itself derived from the https://codeberg.org/Ka-Ubuntu/kaubuntu.re[kaubuntu.re^] project by the https://kaubuntu.com[Ka-Ubuntu^] movement. The original license (GNU AGPL v3) is preserved and respected.

[cols="1,3",options="header"]
|===
| Item | Detail

| Upstream repository
| https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/FEDIVERSE-OKI

| This fork's repository
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`

| Domain (example)
| https://example.com

| Governance
| Application maintained by OKI; fork carried by the repository owner *cedric* (Cédric Famibelle-Pronzola)

| License
| https://www.gnu.org/licenses/agpl-3.0.html[GNU Affero General Public License v3^] (AGPL-V3) or later
|===

=== 🔗 Aggregated sources and instances

The hub is configured by default to aggregate the ANNU KUTE CED podcast's sources:

[cols="1,2,3",options="header"]
|===
| Service | Instance | Account / channel

| 🎥 PeerTube (videos, lives)
| GADE — `https://gade.o-k-i.net`
| https://gade.o-k-i.net/c/annu_kute_ced/videos[`annu_kute_ced`^]

| 🎙️ Castopod (audio episodes)
| KUTE — `https://kute.o-k-i.net`
| https://kute.o-k-i.net/@annu_kute_cedric[`@annu_kute_cedric`^]

| 📡 Mastodon (timeline)
| BOKANTE — `https://bokante.o-k-i.net`
| https://bokante.o-k-i.net/@cedric[`@cedric`^]

| 🎵 Funkwhale (music, optional, disabled by default)
| MIZIK — `https://mizik.o-k-i.net`
| random tracks from the instance
|===

All of these sources can be changed in `includes/config.local.php` (see <<en-configuration,Configuration>>).

=== ✨ Features

==== 🎥 PeerTube videos

- Recent, trending and per-category videos (configurable)
- *Shorts*: dedicated carousel for portrait videos under 3 minutes (touch scrolling)
- Full video page: embedded PeerTube player, Markdown description, Creative Commons licence badge, comments (read-only), suggested videos
- *Downloads*: direct files and HLS playlists, with resolution and size
- *Sharing*: copy link, embed code, e-mail, Facebook, X, WhatsApp, LinkedIn, Telegram
- Full-text search and hashtag search (`#` prefix)
- AJAX "Load more" pagination (protected by CSRF token and Origin check)

==== 📺 Live streams and announcements

- *Live page* (`/direct`): embeds the PeerTube live stream when one is running, with autoplay
- *Next live announcement*: date and time automatically converted for 5 territories (Ma'ohi Nui, Martinique/Guadeloupe, French Guiana, France, Kanaky), custom image (4:5, 1:1, 16:9)
- Configurable homepage *hero section*: live, single video, playlist (PeerTube / Funkwhale / Castopod) or hidden

==== 🎙️ Podcast and audio

- *Castopod*: latest episodes via RSS feed, with a *built-in audio player* (play/pause, auto-advance to the next episode, one stream at a time)
- *Funkwhale* (optional): random track selection from the instance, using the same built-in player

==== 📡 Social networks and external content

- Embedded *Mastodon timeline* (vendored https://gitlab.com/idotj/mastodon-embed-timeline[mastodon-embed-timeline^] v4.7.0 library, AGPLv3)
- *Pleroma compatibility*: adapter converting Pleroma API responses to the Mastodon format expected by the timeline
- *WordPress* (optional): latest posts via the REST API, with featured image and author

==== 📱 PWA and user experience

- Installable *Progressive Web App* (automatic install button)
- Offline mode: visited pages and static assets cached via Service Worker
- Visual indicator when the connection is lost
- *Dark mode* (based on `prefers-color-scheme`, persisted in `localStorage`)
- Fully responsive interface (mobile, tablet, desktop)

==== 💝 Donations, countdown and more

- *Donation page*: LiberaPay, Ko-fi and Stripe (one-time and monthly donations, suggested amounts)
- *Countdown*: multi-timezone launch/maintenance page that locks the whole site until the target date
- Configurable "About" block (title, two paragraphs, captioned image)

==== 🔍 SEO and structured data

- Automatically generated *JSON-LD*: `WebSite` (with `SearchAction`), `PodcastSeries` and `PodcastEpisode` (with `AudioObject`) for the Castopod podcast, `VideoObject` (with `embedUrl`), `CollectionPage`, `BreadcrumbList`, `Organization`
- *Meta descriptions* on every page and *canonical URLs* (`link rel="canonical"`); search result pages set to `noindex, follow`
- *Open Graph* and *Twitter Cards* meta tags
- `sitemap.xml` and `robots.txt` provided as samples
- Ready for *Plausible Analytics* (see <<en-analytics,Analytics>>): disabled by default, cookie-free and GDPR-compliant

==== 🛡️ Security

- Full security headers: dynamic CSP with a per-request *nonce*, HSTS, `X-Frame-Options`, `nosniff`, `Referrer-Policy`, `Permissions-Policy`
- *CSRF* protection on AJAX requests, `Origin` header verification
- Anti-*SSRF* validation of instance URLs and an allowlist of PeerTube API endpoints
- Systematic output escaping (anti-XSS)
- Hardened Apache and Nginx configuration samples in `conf/`

=== 🛠️ Tech stack

- 📄 HTML5, 🎨 CSS3 (Media Queries), ⚡ vanilla JavaScript (no framework, no jQuery)
- 🐘 PHP 7.4+ (no database required)
- 🔧 Service Worker (offline cache) and 📋 Web App Manifest (installation)
- 📦 Libraries:
  * 🎯 Font Awesome 7 (icons, via cdnjs CDN)
  * 📡 mastodon-embed-timeline v4.7.0 (vendored in `js/`, AGPLv3)
  * 📊 Plausible Analytics (pre-configured, to be enabled manually)

=== 📁 Project structure

[source]
----
├── .gitea/
│   └── workflows/                  # Gitea Actions CI/CD (check-pr, deploy-prod)
├── ajax/
│   └── load-more-videos.php        # "Load more" AJAX endpoint (CSRF + Origin)
├── conf/
│   ├── .htaccess.sample            # Hardened Apache configuration
│   └── nginx.conf.sample           # Hardened Nginx configuration
├── css/                            # Stylesheets (styles, video, categories,
│   │                               #   search, donations, countdown, mastodon,
│   ├── castopod-podcasts.css       #   castopod, funkwhale, wordpress, grid…)
│   └── …
├── docs/
│   ├── wireframes/                 # Original mockups (desktop + mobile PNGs)
│   ├── docinfo.html                # Print styles (emoji fallback) for the export
│   └── generate-readme-pdf.sh      # Docs export: README.html / README.pdf
├── img/                            # Logo, favicons, play icon
├── includes/
│   ├── config.php                  # Bootstrap + API functions (PeerTube, Castopod,
│   │                               #   Funkwhale) and formatting helpers
│   ├── config.default.php          # Default values for every constant
│   ├── config.local.php.sample     # Local configuration template (to copy)
│   ├── security.php                # CSRF, CSP + nonce, headers, validators
│   ├── simple-cache.php            # File-based API cache (cache/api/)
│   ├── wordpress.php               # WordPress REST API integration
│   ├── structured-data.php         # JSON-LD generators
│   ├── header.php                  # Top bar (search, socials, theme…)
│   ├── footer.php                  # Footer
│   ├── sidebar.php                 # Side navigation
│   ├── mobile-menu.php             # Slide-in mobile menu
│   ├── hero-section.php            # Homepage banner (live/video/playlist)
│   ├── featured-videos.php         # ⚠️ legacy, unused by current pages
│   ├── recent-videos.php           # ⚠️ legacy, unused by current pages
│   ├── pwa-init.php                # ⚠️ legacy, PWA now inlined in index.php
│   └── lib/
│       └── markdown.php            # Markdown renderer (PeerTube-flavored subset)
├── js/
│   ├── main.js                     # Global UI (theme, carousel, "Load more"…)
│   ├── audio-player.js             # Castopod / Funkwhale audio player
│   ├── categories.js               # Category page "Load more"
│   ├── countdown.js                # Countdown timer
│   ├── search.js                   # Clickable search result cards
│   ├── mastodon-config.php         # Timeline JS config (served as JavaScript)
│   ├── mastodon-timeline.umd.js    # mastodon-embed-timeline v4.7.0 (vendored)
│   ├── pleroma-adapter.js          # Pleroma → Mastodon API adapter
│   └── pwa-update.js               # SW registration + update modal
├── scripts/
│   └── check.sh                    # CI checks run locally (before pushing)
├── uploads/                        # Announcement images (not versioned, except .gitkeep)
├── index.php                       # Homepage (aggregator)
├── video.php                       # Video playback page
├── categories.php                  # Category page
├── recherche.php                   # Search page
├── direct.php                      # Live stream / next-live announcement page
├── countdown.php                   # Countdown page (maintenance)
├── sw.js                           # Service Worker (PWA)
├── site.webmanifest.sample         # PWA manifest sample
├── browserconfig.xml               # Windows tiles
├── sitemap.xml.sample              # Sitemap sample
├── robots.txt.sample               # robots.txt sample
├── mentions-legales.php.sample     # Legal notice sample
├── dons.php.sample                 # Donation page sample
├── LICENSE                         # GNU AGPL v3
├── DEPLOY.adoc                     # Server deployment + CI/CD guide
└── README.adoc
----

NOTE: Files marked "legacy" are kept for reference but are not included by any page; the homepage renders those sections internally.

=== 📋 Requirements

- 🐘 *PHP 7.4+* (recommended: PHP 8.0+)
- 📦 *Required PHP extensions:*
  * `curl` — API calls (PeerTube, Castopod, Funkwhale, WordPress)
  * `json` — response handling
  * `intl` — international dates and timezones
  * `mbstring` — multi-byte strings
  * `xml` (SimpleXML) — recommended for Castopod (a regex fallback exists)
- 🌐 *Web server*: Nginx (recommended) or Apache
- 🔒 *HTTPS*: required for PWA features

.Installing the `intl` extension
[source,bash]
----
# Ubuntu / Debian
sudo apt-get install php-intl

# Fedora / RHEL / CentOS
sudo dnf install php-intl

# Restart the web server
sudo systemctl restart apache2        # Apache
sudo systemctl restart php8.3-fpm     # Nginx + php-fpm

# Check
php -m | grep intl
----

=== 🚀 Installation

. 📥 Clone the repository:
+
[source,bash]
----
git clone git@labola.o-k-i.net:cedric/annu-kute-ced.git
cd annu-kute-ced
----
. 📦 Check the PHP requirements (see above).
. 🔧 Create your local configuration and instance files:
+
[source,bash]
----
cp includes/config.local.php.sample includes/config.local.php
cp site.webmanifest.sample site.webmanifest
cp robots.txt.sample robots.txt
cp sitemap.xml.sample sitemap.xml
cp mentions-legales.php.sample mentions-legales.php
cp dons.php.sample dons.php
----
. 🛡️ Install the hardened server configuration from `conf/` (see <<en-securite,Security>>).
. ✍️ Make the `cache/` directory writable by the web server (created automatically on the first API call).
. 🔒 Make sure HTTPS is enabled (mandatory for the PWA).
. 🌐 Point your domain (e.g. `example.com`) to the project root.

[[en-configuration]]
=== ⚙️ Configuration

==== The three-file system

[cols="1,2,2",options="header"]
|===
| File | Role | Versioned?

| `includes/config.php`
| Bootstrap: loads the other files and holds all business functions
| ✅ Yes

| `includes/config.default.php`
| Default values for every constant (defined only if missing)
| ✅ Yes

| `includes/config.local.php`
| Your local overrides — *take precedence* as they load first
| ❌ No (`.gitignore`)
|===

To customize the instance, never edit `config.default.php`: define the constant in `config.local.php`. Since it loads first, your values always win.

[source,bash]
----
cp includes/config.local.php.sample includes/config.local.php
----

==== Constants reference

Default values from `includes/config.default.php`.

.Application and organization
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `APP_HOST_NAME`
| `'example.com'`
| Displayed domain name (legal notice…)

| `ORGANIZATION_SHORT_NAME`
| `'ANNU KUTE CED'`
| Short organization name

| `ORGANIZATION_NAME`
| `'ANNU KUTE CED'`
| Full organization name
|===

.PeerTube
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `PEERTUBE_URL`
| `'https://gade.o-k-i.net'`
| PeerTube instance URL (required)

| `PEERTUBE_DISPLAY_NAME`
| `'gade.o-k-i.net'`
| Displayed instance name

| `API_KEY`
| `''`
| Optional API key (`Authorization: ApiKey …`)

| `SHOW_VIDEO_VIEWS`
| `false`
| Show view counts on video cards

| `PRIORITY_CATEGORIES`
| `[10, 15]`
| Featured categories: `[ID => 'Name']`, order = display order (homepage, sidebar, footer, mobile menu)

| `DEFAULT_TIMEZONE`
| `'America/Guadeloupe'`
| Display timezone (https://www.php.net/manual/en/timezones.php[PHP list^])
|===

.Hero section (homepage banner)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `HERO_TYPE`
| `'live'`
| `'live'` · `'video'` · `'playlist'` · `'none'`

| `LIVE_ACCOUNT_NAME`
| `'annu_kute_ced'`
| PeerTube account watched for live streams (hero + `/direct` page)

| `HERO_VIDEO_ID`
| `'fDE6tvQMXRuBpE4eDDS7kj'`
| Video UUID (`video` mode), extracted from the `/w/<uuid>` URL

| `HERO_VIDEO_TITLE`
| `'Vidéo de présentation'`
| Title (accessibility)

| `PLAYLIST_PLATFORM`
| `'peertube'`
| `'peertube'` · `'funkwhale'` · `'castopod'`

| `PLAYLIST_INSTANCE_URL`
| `''`
| Base URL of the playlist platform

| `PLAYLIST_ID`
| `''`
| Playlist ID; for Castopod: `'@slug'`

| `PLAYLIST_TITLE` / `PLAYLIST_DESCRIPTION`
| `''` / `''`
| Optional texts (otherwise fetched via API)
|===

.Castopod (podcast)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `CASTOPOD_ENABLED`
| `true`
| Enable the homepage podcast section

| `CASTOPOD_URL`
| `'https://kute.o-k-i.net'`
| Castopod instance URL

| `CASTOPOD_PODCAST_SLUGS`
| `['annu_kute_cedric']`
| Podcast slug(s); RSS feed = `{URL}/@{slug}/feed`. Multiple podcasts: episodes merged and date-sorted

| `CASTOPOD_EPISODES_COUNT`
| `5`
| Total number of episodes displayed
|===

WARNING: Castopod enforces a strict *rate limit* (HTTP 429) on RSS feeds. The code waits 5 s between podcasts. *Enable the cache* (`CACHE_ENABLED`) to avoid these delays on every page.

.Funkwhale (music, optional)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `FUNKWHALE_ENABLED`
| `false`
| Enable the music section

| `FUNKWHALE_URL`
| `'https://mizik.o-k-i.net'`
| Funkwhale instance URL

| `FUNKWHALE_TRACKS_COUNT`
| `10`
| Random local tracks displayed (50 cached)
|===

.Mastodon (timeline)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `MASTODON_INSTANCE_URL`
| `'https://bokante.o-k-i.net'`
| Mastodon instance (or Pleroma, via the adapter)

| `MASTODON_URL`
| `'https://bokante.o-k-i.net/@cedric'`
| Public account URL ("See more" links, footer)

| `MASTODON_DATE_FORMAT`
| `'fr-FR'`
| Post date locale

| `MASTODON_BTN_SEE_MORE` / `MASTODON_BTN_RELOAD`
| `'Voir plus de post'` / `'Rafraichir'`
| Button labels

| `MASTODON_MAX_POST_FETCH` / `MASTODON_MAX_POST_SHOW`
| `'10'` / `'10'`
| Posts fetched / displayed

| `MASTODON_S3_MEDIA_URL`
| _(undefined)_
| S3 media origin (added to the CSP `media-src`)
|===

NOTE: The timeline has no enable flag: it is always displayed on the homepage.

.WordPress (optional)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `WORDPRESS_ENABLED`
| `false`
| Enable the posts section

| `WORDPRESS_URL`
| `''`
| WordPress site URL (no trailing `/`)

| `WORDPRESS_POSTS_COUNT`
| `6`
| Number of posts (REST API `/wp-json/wp/v2/`)
|===

.Donations
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `DONATIONS_ENABLED`
| `false`
| Enable the system (heart icon, "Support" link, `/dons.php` page)

| `LIBERAPAY_URL` / `KOFI_URL`
| `''` / `''`
| External platform URLs

| `STRIPE_ENABLED`
| `false`
| Enable the Stripe interface (one-time / monthly tabs)

| `STRIPE_ONE_TIME_LINKS`
| `[1, 5, 10, 20, 50, 'custom'] => ''`
| Stripe payment links per amount

| `STRIPE_MONTHLY_LINKS`
| `[1, 5, 10, 20, 50] => ''`
| Stripe monthly subscription links

| `DONATION_AMOUNTS`
| `[1, 5, 10, 20, 50]`
| Suggested amounts

| `DONATION_CURRENCY`
| `'EUR'`
| Currency
|===

NOTE: At least one platform (LiberaPay, Ko-fi or Stripe) must be configured for `dons.php` to render, otherwise the page returns a 500 error.

.Countdown (maintenance / launch)
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `COUNTDOWN_ENABLED`
| `false`
| ⚠️ When `true`, *every* page redirects to `countdown.php`

| `COUNTDOWN_TARGET_DATE`
| `'2025-10-11 00:00:00'`
| Target date (`Y-m-d H:i:s`); on completion, redirects to `/`

| `COUNTDOWN_TIMEZONES`
| 5 territories
| `[Label => PHP timezone]` shown on the page
|===

.Next-live announcement
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `NEXT_LIVE_ENABLED`
| `false`
| Show the announcement when no live stream is running

| `NEXT_LIVE_TITLE` / `NEXT_LIVE_DESCRIPTION`
| `'Prochain live'` / `'Rejoignez-nous…'`
| Texts (date and time appended automatically)

| `NEXT_LIVE_DATE`
| `''`
| Live date (`Y-m-d H:i:s`)

| `NEXT_LIVE_IMAGE`
| `'uploads/next-live.jpg'`
| Announcement image (used only if the file exists)
|===

.Site, social networks and contact
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `SITE_NAME`
| `'ANNU KUTE CED'`
| Site name (titles, meta)

| `SITE_DESCRIPTION`
| `'Hub multimédia du podcast ANNU KUTE CED'`
| SEO description

| `SITE_LOGO` / `SITE_FAVICON`
| `'img/logo.png'` / `'img/favicon.png'`
| Image paths

| `FACEBOOK_URL`, `X_URL`, `INSTAGRAM_URL`, `YOUTUBE_URL`, `TIKTOK_URL`
| `'#'`
| `'#'` or empty = icon hidden

| `CONTACT_EMAIL`
| `'kontak@o-k-i.net'`
| Contact e-mail (footer)
|===

.Legal notice
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `LEGAL_COPYRIGHT`
| `'ANNU KUTE CED'`
| Copyright holder

| `LEGAL_WEBMASTER_NAME` / `LEGAL_WEBMASTER_EMAIL`
| `'Cédric Famibelle-Pronzola'` / `'contact@cedric-pronzola.dev'`
| Webmaster

| `LEGAL_HOST_NAME`, `LEGAL_HOST_COMPANY`, `LEGAL_HOST_RCS`, `LEGAL_HOST_ADDRESS`
| o2Switch values
| Hosting provider details

| `LEGAL_CONTACT_EMAIL`
| `'kontak@o-k-i.net'`
| Legal contact

| `LEGAL_LICENSE` / `LEGAL_LICENSE_URL`
| AGPL-V3 + GNU URL
| Code license

| `LEGAL_SOURCE_CODE_URL`
| `'https://labola.o-k-i.net/cedric/annu-kute-ced'`
| Source code repository (required by the AGPL for a network service)

| `LEGAL_SERVICE_DESCRIPTION`
| `'est le hub multimédia du podcast ANNU KUTE CED.'`
| Service description sentence
|===

.Tags and filters
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `TAG_INDEPENDENCE`
| `'indépendance'`
| Tag used by `getIndependenceVideos()` (no associated homepage section currently)

| `SHORTS_MAX_DURATION`
| `180`
| Max short duration (s); portrait ratio (`aspectRatio ≤ 1`) also required

| `IMPORTANT_TAGS`
| `['ANNUKUTECED']`
| Hashtags in the sidebar, footer and mobile menu

| `POPULAR_TAGS`
| `['ANNUKUTECED']`
| Hashtags displayed on the homepage
|===

.Pagination and display volumes
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `VIDEOS_PER_PAGE`
| `12`
| Videos per page (search)

| `COUNT_VIDEO_SEARCH`
| `20`
| Results fetched per search

| `RECENT_VIDEOS_COUNT` / `TRENDING_VIDEOS_COUNT` / `CATEGORY_VIDEOS_COUNT`
| `6`
| Homepage section sizes

| `SHORTS_COUNT` / `SHORTS_COUNT_SEARCH`
| `6` / `100`
| Shorts displayed / search pool

| `LOAD_MORE_COUNT`
| `6`
| Videos loaded per "Load more" click

| `FEATURED_VIDEOS_COUNT` / `INDEPENDENCE_VIDEOS_COUNT`
| `6`
| Reserved (sections not currently displayed)
|===

.Cache
[cols="2,2,3",options="header"]
|===
| Constant | Default | Description

| `CACHE_ENABLED`
| `true`
| Enable caching for *Castopod and Funkwhale*. Essential against the Castopod RSS rate limit; empty results (error, 429) are never cached

| `CACHE_DURATION`
| `3600`
| Cache lifetime (s)
|===

NOTE: The PeerTube API and WordPress cache (`includes/simple-cache.php`) is *always on*, regardless of `CACHE_ENABLED`, with per-endpoint TTLs (categories 1 h, videos/search 10 min, WP posts 15 min, accounts 5 min). Files are stored in `cache/api/`.

."About" block (no default values — block hidden until defined)
[cols="2,3",options="header"]
|===
| Constant | Description

| `MOVEMENT_TITLE`
| Block title (template fallback: "À propos")

| `MOVEMENT_DESCRIPTION`
| First paragraph; *commenting this line hides the whole block*

| `MOVEMENT_DESCRIPTION_2`
| Second paragraph (optional)

| `MOVEMENT_IMAGE` / `MOVEMENT_IMAGE_ALT` / `MOVEMENT_CAPTION`
| Image (`img/movement_presentation.png`, not versioned), alt text and caption (simple HTML allowed)
|===

==== Complete `config.local.php` example

[source,php]
----
<?php
define('APP_HOST_NAME', 'example.com');

// PeerTube (the podcast's GADE channel)
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
define('PEERTUBE_DISPLAY_NAME', 'gade.o-k-i.net');

// Hero: channel live stream, otherwise next-live announcement
define('HERO_TYPE', 'live');
define('LIVE_ACCOUNT_NAME', 'annu_kute_ced');
define('NEXT_LIVE_ENABLED', true);
define('NEXT_LIVE_TITLE', 'Prochain live');
define('NEXT_LIVE_DESCRIPTION', 'Recording the next episode!');
define('NEXT_LIVE_DATE', '2025-10-11 10:00:00');

// Castopod (the podcast's KUTE account)
define('CASTOPOD_ENABLED', true);
define('CASTOPOD_URL', 'https://kute.o-k-i.net');
define('CASTOPOD_PODCAST_SLUGS', ['annu_kute_cedric']);
define('CASTOPOD_EPISODES_COUNT', 5);

// Mastodon (BOKANTE account)
define('MASTODON_INSTANCE_URL', 'https://bokante.o-k-i.net');
define('MASTODON_URL', 'https://bokante.o-k-i.net/@cedric');

// Cache: essential for Castopod (RSS rate limit)
define('CACHE_ENABLED', true);
define('CACHE_DURATION', 3600);
----

=== 🎨 Appearance customization

. 🖼️ Replace `img/logo.png` with your logo (and the favicons in `img/`)
. 🎨 Edit the colors in `css/styles.css`
. 📝 Most texts are set through `config.local.php` constants ("About" block, Mastodon labels, etc.)

==== Instance files to customize

The `sitemap.xml`, `robots.txt`, `site.webmanifest`, `mentions-legales.php` and `dons.php` files are Git-ignored: copy the `.sample` files then adapt them. The provided samples use the generic `example.com` domain: replace it with your real domain. In `mentions-legales.php`, also replace the `VOTRE-DATE-MAJ` placeholder.

=== 🧭 Homepage section order

The homepage assembles sections in this order, each gated by its configuration:

. *Hero* — if `HERO_TYPE` ≠ `'none'` (live, video or playlist)
. *Castopod podcasts* — if `CASTOPOD_ENABLED` and `CASTOPOD_URL` is not empty
. *Funkwhale tracks* — if `FUNKWHALE_ENABLED` and `FUNKWHALE_URL` is not empty
. *Mastodon timeline* — always displayed
. *WordPress posts* — if `WORDPRESS_ENABLED` and `WORDPRESS_URL` is not empty
. *Popular hashtags* (`POPULAR_TAGS`)
. *Shorts* — carousel
. *Latest videos* + "Load more" button
. *Trending* + "Load more"
. *One section per priority category* (`PRIORITY_CATEGORIES`, only when it has videos) + "Load more"
. *"About" block* (if `MOVEMENT_DESCRIPTION` is defined) + hashtags aside

Desktop layout: hero/Castopod/Funkwhale + Mastodon + WordPress are arranged in columns (1fr 2fr, or 1fr 2fr 1fr with WordPress); stacked vertically on mobile.

[[en-securite]]
=== 🛡️ Security

==== What the application already enforces

- *Dynamic CSP*: `default-src 'self'` with a unique per-request *nonce* for inline scripts/styles; PeerTube/Mastodon/CDN origins added automatically; `localhost` relaxations for development
- *Headers*: `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `X-XSS-Protection`, `Referrer-Policy: strict-origin-when-cross-origin`, restrictive `Permissions-Policy`, HSTS (HTTPS only), CORP/COOP `same-origin`
+
NOTE: The COEP header was *deliberately removed* (commit `62d4d99`): `require-corp` broke cross-origin embeds from PeerTube, Mastodon, Castopod and Funkwhale.
- *CSRF*: session token (`random_bytes(32)`) injected as a `<meta>` tag, verified with `hash_equals` on the AJAX endpoint; `Origin` header verification
- *Anti-SSRF*: instance URL validation (http/https scheme, blocking private IPs and `localhost`), allowlist of PeerTube API endpoints, cURL without redirects
- *Anti-XSS*: systematic output escaping; input validation (video UUID, search query ≤ 200 chars, page numbers, category ID 1–20)

==== Server configuration (Nginx, recommended)

The `conf/nginx.conf.sample` file is the complete Nginx equivalent of the `.htaccess`: same protections (configuration and `.sample` files, `/includes/`, `/cache/`, `/docs/`, `/conf/` directories, hidden files, no directory listing), HTTPS enforced, `.php` extension masking (clean URLs `/video` instead of `/video.php`), `no-cache` for `sw.js` and `site.webmanifest`, plus asset caching and gzip.

[source,bash]
----
# Adjust the paths in conf/nginx.conf.sample, then:
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/example.com
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
----

==== Server configuration (Apache, alternative)

[source,bash]
----
cp conf/.htaccess.sample .htaccess
----

Included protections: blocking of configuration and `.sample` files, of the `/includes/`, `/cache/`, `/docs/`, `/conf/` directories; no directory listing; hidden files blocked; HTTPS enforced; `.php` extension masking.

WARNING: This configuration is *essential*: without it, your configuration files are exposed to visitors.

=== 📱 Progressive Web App (PWA)

==== Features

- 📲 *Native installation*: an "Install" button in the header on first visit
- 🌐 *Offline mode*: visited pages and static assets cached
- 📡 *Connection detection*: visual indicator when the network is lost
- 🔄 *Frictionless updates*: when a new version is deployed, a styled modal offers it; on accept, the user gets it immediately — *no manual browser-cache purge needed*

==== Service Worker caching strategies (`sw.js`)

[cols="1,2,3",options="header"]
|===
| Request type | Strategy | Detail

| Static assets (`/css/`, `/js/`, `/img/`, cdnjs CDN)
| *Cache First*
| Cached at install time and on first access; falls back to `img/logo.png` if an image fails

| Pages (`/`, `*.php`…)
| *Network First*
| Network first, cache fallback when offline

| API and AJAX (`/ajax/`, `mastodon-config.php`…)
| *Network Only*
| Never cached; synthetic 503 JSON response when offline
|===

==== Automatic update cycle

This mechanism (`js/pwa-update.js` + `sw.js`) ensures visitors receive new versions *without manually purging their browser cache*:

. When deploying, *bump* the version suffix of the cache names in `sw.js` (format `DDMMYYYY-HHMM`, e.g. `24072026-0612`)
. On page load, the browser detects the new `sw.js` and installs the new Service Worker in the background; it stays *waiting* (no automatic `skipWaiting()`)
. A *modal* offers the update to the user:
  * *"Mettre à jour"* → the waiting SW receives `SKIP_WAITING`, purges old caches on activation, takes control (`controllerchange`) and the page reloads onto the new version
  * *"Plus tard"* → the modal closes; the update is offered again on the next page load
. An update already installed during a previous visit but never activated is also reported

WARNING: For detection to work, `sw.js` must *not* be cached for long by the server: the provided configurations (`conf/nginx.conf.sample`, `conf/.htaccess.sample`) serve it with `no-cache` (as well as `site.webmanifest`). If you use your own server configuration, reproduce this rule.

==== Installation by visitors

- 🌐 *Chrome/Edge*: menu → "Install example.com"
- 🍎 *Safari iOS*: Share → "Add to Home Screen"
- 🦊 *Firefox Android*: menu → "Install"

[[en-analytics]]
=== 📊 Analytics (Plausible)

The site is *pre-configured* for https://plausible.io[Plausible Analytics^] (cookie-free, GDPR-compliant, anonymous data), but the script is *disabled by default*.

To enable it:

. Open `index.php` and *uncomment* the Plausible snippet (the `<script defer data-domain=… src="https://plausible.io/js/script…js">` tag and its `window.plausible` shim).
. That's it: the CSP already allows `https://plausible.io` in `script-src` and `connect-src`, and the `data-domain` automatically uses the current domain.

Data then collected (anonymous): visited pages, country of origin (IP not stored), device type, browser, time spent.

=== 🚀 Deploying to shared hosting

. 🐘 Check that the host provides PHP 7.4+ and the required extensions
. 🔒 Enable HTTPS (mandatory for the PWA)
. 📤 Upload the files via FTP to the site root
. 🔧 Permissions: `644` for files, `755` for directories; `cache/` must be writable
. 📄 Create the instance files from the `.sample` files (config, manifest, robots, sitemap, legal notice, donations)
. 🛡️ Copy `conf/.htaccess.sample` to `.htaccess` (Apache)
. 🌐 Point your domain (e.g. `example.com`) to the installation directory
. 🧪 Test the PWA via the browser developer tools (Application tab)

=== 👨‍💻 Development and contributing

. 🌿 Create a branch: `git checkout -b my-feature`
. 💾 Commit with *atomic, English messages*, in conventional format (< 70 characters):
  * `feat: add …`, `fix: prevent …`, `docs: update …`, `refactor: …`, `typo: …`
. 📤 Push: `git push origin my-feature`
. 🔀 Open a pull request

Repository best practices:

- Bug reports and suggestions are welcome on the fork's repository; generic improvements can be proposed upstream (FEDIVERSE OKI)
- Never version `config.local.php` or files derived from the `.sample` files
- A few constants are defined but unused by the current code (`ENABLE_SEARCH`, `ENABLE_COMMENTS`, `ENABLE_USER_ACCOUNTS`, `TAG_SHORT`): do not rely on them as extension points

==== 📄 Exporting the documentation (HTML / PDF)

This README can be exported to HTML and PDF with a single command:

[source,bash]
----
docs/generate-readme-pdf.sh           # README.html + README.pdf
docs/generate-readme-pdf.sh --html    # HTML only
docs/generate-readme-pdf.sh --pdf     # PDF only
----

Files are produced in the project root and are Git-ignored (`.gitignore`).

Requirements: `asciidoctor`, `chromium` (headless rendering) and the `fonts-noto-color-emoji` package (color emoji in the PDF). The `docs/docinfo.html` stylesheet (emoji fallback + print rules) is automatically injected through Asciidoctor's `docinfo` mechanism.

NOTE: `asciidoctor-pdf` (used by VSCodium's AsciiDoc extension) cannot be used here: its Prawn engine cannot embed color-emoji fonts (CBDT/COLR), so emoji disappear from the PDF. Chromium handles them natively.

==== ✅ Local checks and CI/CD

Before pushing, run the same checks as the CI:

[source,bash]
----
scripts/check.sh
----

This script runs: PHP lint (`php -l` on every file, samples included), JS lint (`node --check`), AsciiDoc rendering, JSON (`site.webmanifest.sample`) and XML (`sitemap.xml.sample`, `browserconfig.xml`) validation, shellcheck. A missing tool is reported and its check skipped; the CI runs everything.

Gitea Actions pipelines (`.gitea/workflows/`):

- *PR check* (`check-pr.yml`): on every pull request to `main`
- *PROD deployment* (`deploy-prod.yml`): on push to `main` — same checks, then SSH deployment to the server (`git pull --ff-only` + Service Worker version bump, which triggers the update modal for visitors)

The full production server setup (clone, instance files, SSH keys, Gitea secrets) is documented in link:DEPLOY.adoc[DEPLOY.adoc].

=== 📜 License

Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the https://www.gnu.org/licenses/agpl-3.0.html[GNU Affero General Public License^] for more details (full text in `LICENSE`).

=== 📞 Contact

For any question or suggestion about this platform: mailto:kontak@o-k-i.net[kontak@o-k-i.net]

<<top,⬆ Back to top>>
