Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4b56296e
|
||
|
|
84013376f2
|
||
|
|
dafb5e5753
|
||
|
|
4114b9e8e3
|
||
|
|
521ad8c7f1
|
||
|
|
576a72a0e1
|
||
|
|
9cf7487f24
|
||
|
|
681683c059
|
||
|
|
7e8f078b60
|
||
|
|
7f1bdaa2ab
|
||
|
|
f760dfb72c
|
@@ -0,0 +1,55 @@
|
|||||||
|
name: Vérification PR
|
||||||
|
run-name: Vérification PR de ${{ gitea.actor }}
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: curl, intl, mbstring, xml
|
||||||
|
|
||||||
|
- name: Lint PHP (tous les fichiers, samples inclus)
|
||||||
|
run: |
|
||||||
|
fail=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
php -l "$f" > /dev/null || { echo "::error file=$f::Erreur de syntaxe PHP"; fail=1; }
|
||||||
|
done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
|
||||||
|
[ "$fail" -eq 0 ] && echo "PHP lint OK" || exit 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Lint JS (sw.js et js/*.js)
|
||||||
|
run: |
|
||||||
|
for f in sw.js js/*.js; do
|
||||||
|
node --check "$f" || exit 1
|
||||||
|
done
|
||||||
|
echo "JS lint OK"
|
||||||
|
|
||||||
|
- name: Installer Asciidoctor
|
||||||
|
run: sudo gem install asciidoctor
|
||||||
|
|
||||||
|
- name: Valider README.adoc
|
||||||
|
run: asciidoctor -o /tmp/readme.html README.adoc
|
||||||
|
|
||||||
|
- name: Valider JSON et XML
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y libxml2-utils
|
||||||
|
python3 -m json.tool site.webmanifest.sample > /dev/null
|
||||||
|
xmllint --noout sitemap.xml.sample browserconfig.xml
|
||||||
|
|
||||||
|
- name: Shellcheck (scripts shell)
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y shellcheck
|
||||||
|
shellcheck docs/generate-readme-pdf.sh scripts/check.sh
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
name: Déploiement PROD
|
||||||
|
run-name: ${{ gitea.actor }} déploie en PROD
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: curl, intl, mbstring, xml
|
||||||
|
|
||||||
|
- name: Lint PHP (tous les fichiers, samples inclus)
|
||||||
|
run: |
|
||||||
|
fail=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
php -l "$f" > /dev/null || { echo "::error file=$f::Erreur de syntaxe PHP"; fail=1; }
|
||||||
|
done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
|
||||||
|
[ "$fail" -eq 0 ] && echo "PHP lint OK" || exit 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Lint JS (sw.js et js/*.js)
|
||||||
|
run: |
|
||||||
|
for f in sw.js js/*.js; do
|
||||||
|
node --check "$f" || exit 1
|
||||||
|
done
|
||||||
|
echo "JS lint OK"
|
||||||
|
|
||||||
|
- name: Installer Asciidoctor
|
||||||
|
run: sudo gem install asciidoctor
|
||||||
|
|
||||||
|
- name: Valider README.adoc
|
||||||
|
run: asciidoctor -o /tmp/readme.html README.adoc
|
||||||
|
|
||||||
|
- name: Valider JSON et XML
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y libxml2-utils
|
||||||
|
python3 -m json.tool site.webmanifest.sample > /dev/null
|
||||||
|
xmllint --noout sitemap.xml.sample browserconfig.xml
|
||||||
|
|
||||||
|
- name: Shellcheck (scripts shell)
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y shellcheck
|
||||||
|
shellcheck docs/generate-readme-pdf.sh scripts/check.sh
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Déployer sur le serveur
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd ${{ secrets.PROD_DEPLOY_PATH }}
|
||||||
|
|
||||||
|
# Annuler le bump de version du déploiement précédent pour
|
||||||
|
# garantir le fast-forward, puis récupérer la dernière version.
|
||||||
|
git checkout -- sw.js
|
||||||
|
git pull --ff-only origin main
|
||||||
|
|
||||||
|
# Bumper la version des caches du Service Worker : chaque
|
||||||
|
# déploiement déclenche le modal de mise à jour PWA chez les
|
||||||
|
# visiteurs (voir js/pwa-update.js). Ce bump n'est pas commité.
|
||||||
|
VERSION=$(date +%d%m%Y-%H%M)
|
||||||
|
sed -i "s/annu-kute-ced-static-[0-9]\{8\}-[0-9]\{4\}/annu-kute-ced-static-$VERSION/" sw.js
|
||||||
|
sed -i "s/annu-kute-ced-dynamic-[0-9]\{8\}-[0-9]\{4\}/annu-kute-ced-dynamic-$VERSION/" sw.js
|
||||||
|
echo "Version des caches bumpée : $VERSION"
|
||||||
@@ -26,8 +26,8 @@ temp/
|
|||||||
cache/
|
cache/
|
||||||
|
|
||||||
# Artefacts de documentation générés (docs/generate-readme-pdf.sh)
|
# Artefacts de documentation générés (docs/generate-readme-pdf.sh)
|
||||||
README.html
|
/*.html
|
||||||
README.pdf
|
/*.pdf
|
||||||
|
|
||||||
# Dossier pour les images d'annonces (tout ignorer sauf .gitkeep)
|
# Dossier pour les images d'annonces (tout ignorer sauf .gitkeep)
|
||||||
uploads/*
|
uploads/*
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
= 🚀 DEPLOY — Mise en production et CI/CD
|
||||||
|
:toc: left
|
||||||
|
:toc-title: Sommaire
|
||||||
|
:toclevels: 3
|
||||||
|
|
||||||
|
Ce document décrit la mise en production complète d'*ANNU KUTE CED* sur un serveur fraîchement installé, puis l'activation du déploiement continu via *Gitea Actions*.
|
||||||
|
|
||||||
|
Il est complémentaire au link:README.adoc[README] (installation manuelle, configuration de l'application).
|
||||||
|
|
||||||
|
== 🧭 Vue d'ensemble
|
||||||
|
|
||||||
|
L'architecture retenue (identique à celle de pawol.nu) :
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
┌─────────────┐ push main ┌──────────────────┐ SSH (git pull) ┌─────────────┐
|
||||||
|
│ Dépôt git │ ─────────────▶│ Gitea Actions │ ─────────────────▶│ Serveur │
|
||||||
|
│ (LaBola) │ │ check → deploy │ │ production │
|
||||||
|
└─────────────┘ └──────────────────┘ └─────────────┘
|
||||||
|
----
|
||||||
|
|
||||||
|
. *Vérification* (`check-pr.yml` + job `check` de `deploy-prod.yml`) : lint PHP/JS, validation AsciiDoc, JSON, XML, shellcheck. Les mêmes vérifications sont exécutables en local avec `scripts/check.sh`.
|
||||||
|
. *Déploiement* (`deploy-prod.yml`) : le runner se connecte en SSH au serveur, qui tient *un clone du dépôt*, fait `git pull --ff-only`, puis *bumpe la version des caches* du Service Worker (`sw.js`) pour déclencher le modal de mise à jour PWA chez les visiteurs.
|
||||||
|
|
||||||
|
Pourquoi un `git pull` sur le serveur plutôt qu'un rsync : tous les fichiers propres à l'instance (`config.local.php`, `.htaccess`, `sitemap.xml`, `robots.txt`, `site.webmanifest`, `mentions-legales.php`, `dons.php`, `uploads/`, `cache/`) sont ignorés par Git — un pull ne les écrase jamais. Le déploiement est ainsi sans risque pour la configuration de production.
|
||||||
|
|
||||||
|
== 📋 Prérequis
|
||||||
|
|
||||||
|
- Un serveur *Debian 12* ou *Ubuntu 24.04* fraîchement installé, avec accès `root` (ou `sudo`)
|
||||||
|
- Un nom de domaine dont le *DNS pointe vers le serveur* (enregistrement A/AAAA)
|
||||||
|
- Le dépôt Gitea : `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
|
||||||
|
- Un *runner Gitea Actions* opérationnel sur l'instance LaBola (déjà le cas pour pawol.nu)
|
||||||
|
|
||||||
|
NOTE: Sur un hébergement mutualisé (pas d'accès root), adaptez : les fichiers sont déployés dans le docroot fourni par l'hébergeur, et la clé SSH s'ajoute via le panneau de contrôle (o2switch : *Clés SSH* dans cPanel).
|
||||||
|
|
||||||
|
== 1️⃣ Installation des paquets
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# Nginx + PHP-FPM (choix recommandé ; Apache possible, voir §5 option B)
|
||||||
|
apt update && apt upgrade -y
|
||||||
|
apt install -y nginx php-fpm \
|
||||||
|
php-curl php-intl php-mbstring php-xml \
|
||||||
|
git curl unzip
|
||||||
|
|
||||||
|
# Vérifier les extensions requises
|
||||||
|
php -m | grep -E 'curl|intl|mbstring|SimpleXML|json'
|
||||||
|
----
|
||||||
|
|
||||||
|
== 2️⃣ Utilisateur de déploiement
|
||||||
|
|
||||||
|
Le runner CI se connectera en SSH avec cet utilisateur. Ne *pas* utiliser root.
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
adduser --disabled-password --gecos "Deploy annu-kute-ced" deploy
|
||||||
|
usermod -aG www-data deploy
|
||||||
|
|
||||||
|
# Répertoire de l'application
|
||||||
|
mkdir -p /var/www/annu-kute-ced
|
||||||
|
chown -R deploy:www-data /var/www/annu-kute-ced
|
||||||
|
----
|
||||||
|
|
||||||
|
== 3️⃣ Clone du dépôt
|
||||||
|
|
||||||
|
Le serveur de production tient un clone du dépôt. Le runner y exécutera `git pull` à chaque déploiement.
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# Clé SSH du serveur pour lire le dépôt (deploy key Gitea, lecture seule)
|
||||||
|
sudo -u deploy ssh-keygen -t ed25519 -f /home/deploy/.ssh/id_ed25519 -N ""
|
||||||
|
cat /home/deploy/.ssh/id_ed25519.pub
|
||||||
|
----
|
||||||
|
|
||||||
|
. Sur LaBola : *Settings du dépôt → Deploy Keys → Add deploy key* (coller la clé publique, lecture seule suffit).
|
||||||
|
. Puis :
|
||||||
|
+
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
sudo -u deploy git clone git@labola.o-k-i.net:cedric/annu-kute-ced.git /var/www/annu-kute-ced
|
||||||
|
cd /var/www/annu-kute-ced
|
||||||
|
sudo -u deploy git config pull.ff only # sécurité : refuser tout pull non fast-forward
|
||||||
|
----
|
||||||
|
|
||||||
|
== 4️⃣ Fichiers d'instance
|
||||||
|
|
||||||
|
Ces fichiers sont ignorés par Git : ils vivent *uniquement sur le serveur* et ne seront jamais écrasés par les déploiements.
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
cd /var/www/annu-kute-ced
|
||||||
|
sudo -u deploy cp includes/config.local.php.sample includes/config.local.php
|
||||||
|
sudo -u deploy cp site.webmanifest.sample site.webmanifest
|
||||||
|
sudo -u deploy cp robots.txt.sample robots.txt
|
||||||
|
sudo -u deploy cp sitemap.xml.sample sitemap.xml
|
||||||
|
sudo -u deploy cp mentions-legales.php.sample mentions-legales.php
|
||||||
|
# Uniquement si vous utilisez Apache (option B de l'étape 5) :
|
||||||
|
# sudo -u deploy cp conf/.htaccess.sample .htaccess
|
||||||
|
# Facultatif (page de dons) :
|
||||||
|
# sudo -u deploy cp dons.php.sample dons.php
|
||||||
|
----
|
||||||
|
|
||||||
|
Éditer ensuite les fichiers copiés :
|
||||||
|
|
||||||
|
- `includes/config.local.php` : `APP_HOST_NAME`, sources (PeerTube/Castopod/Mastodon), `CACHE_ENABLED=true` (recommandé pour Castopod), etc. — voir la link:README.adoc#fr-configuration[référence de configuration]
|
||||||
|
- `sitemap.xml`, `robots.txt`, `site.webmanifest` : remplacer `example.com` par le domaine réel
|
||||||
|
- `mentions-legales.php` : remplacer le placeholder `VOTRE-DATE-MAJ`
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# Le cache de l'API doit être accessible en écriture par PHP
|
||||||
|
mkdir -p /var/www/annu-kute-ced/cache
|
||||||
|
chown -R www-data:www-data /var/www/annu-kute-ced/cache
|
||||||
|
----
|
||||||
|
|
||||||
|
== 5️⃣ VirtualHost
|
||||||
|
|
||||||
|
=== Option A — Nginx + PHP-FPM (recommandée, configuration fournie)
|
||||||
|
|
||||||
|
Le fichier `conf/nginx.conf.sample` est l'équivalent Nginx complet du `.htaccess` : mêmes protections (fichiers de configuration, répertoires sensibles, dotfiles, pas de listing), masquage de l'extension `.php`, HTTPS forcé, `no-cache` pour `sw.js` et `site.webmanifest`, plus le cache des assets et gzip.
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# Adapter conf/nginx.conf.sample :
|
||||||
|
# - server_name : votre domaine
|
||||||
|
# - root : /var/www/annu-kute-ced
|
||||||
|
# - fastcgi_pass : socket de votre version PHP (ex. /var/run/php/php8.3-fpm.sock)
|
||||||
|
# - ssl_certificate(_key) : chemins de vos certificats (étape 6)
|
||||||
|
cp conf/nginx.conf.sample /etc/nginx/sites-available/annu-kute-ced
|
||||||
|
ln -s /etc/nginx/sites-available/annu-kute-ced /etc/nginx/sites-enabled/
|
||||||
|
nginx -t && systemctl reload nginx
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Option B — Apache
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
apt install -y apache2 php libapache2-mod-php
|
||||||
|
a2enmod rewrite headers
|
||||||
|
cat > /etc/apache2/sites-available/annu-kute-ced.conf <<'EOF'
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName example.com
|
||||||
|
ServerAlias www.example.com
|
||||||
|
DocumentRoot /var/www/annu-kute-ced
|
||||||
|
|
||||||
|
<Directory /var/www/annu-kute-ced>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
</VirtualHost>
|
||||||
|
EOF
|
||||||
|
a2ensite annu-kute-ced
|
||||||
|
systemctl reload apache2
|
||||||
|
----
|
||||||
|
|
||||||
|
Le `.htaccess` copié à l'étape 4 applique les règles de sécurité, le HTTPS forcé (actif après l'étape 6) et le `no-cache` de `sw.js`.
|
||||||
|
|
||||||
|
== 6️⃣ HTTPS (obligatoire pour la PWA)
|
||||||
|
|
||||||
|
Méthode recommandée par l'EFF : *Certbot via snap* (https://certbot.eff.org/instructions?ws=nginx&os=snap[instructions officielles^]). Prérequis : le domaine pointe vers le serveur et le site répond déjà en HTTP sur le port 80 (étape 5 terminée).
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# 1. Installer snapd (Ubuntu : déjà présent. Debian : apt + support classic)
|
||||||
|
apt install -y snapd
|
||||||
|
snap install core && snap refresh core
|
||||||
|
|
||||||
|
# 2. Retirer tout certbot installé via apt (évite les conflits de commande)
|
||||||
|
apt-get remove -y certbot || true
|
||||||
|
|
||||||
|
# 3. Installer Certbot et préparer la commande
|
||||||
|
snap install --classic certbot
|
||||||
|
ln -s /snap/bin/certbot /usr/local/bin/certbot
|
||||||
|
|
||||||
|
# 4. Obtenir le certificat ET laisser Certbot configurer Nginx automatiquement
|
||||||
|
certbot --nginx -d example.com -d www.example.com
|
||||||
|
# Variante prudente (ne fait qu'émettre le certificat, vhost édité à la main) :
|
||||||
|
# certbot certonly --nginx -d example.com -d www.example.com
|
||||||
|
|
||||||
|
# 5. Vérifier le renouvellement automatique (timer systemd/cron inclus avec le snap)
|
||||||
|
certbot renew --dry-run
|
||||||
|
----
|
||||||
|
|
||||||
|
NOTE: Pour Apache (option B), la méthode snap est identique, avec `certbot --apache` à l'étape 4.
|
||||||
|
|
||||||
|
Ouvrez ensuite `https://example.com` dans un navigateur : le cadenas doit apparaître dans la barre d'URL.
|
||||||
|
|
||||||
|
== 7️⃣ Vérification du site
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
curl -I https://example.com
|
||||||
|
# Attendu : HTTP/2 200, en-têtes de sécurité (CSP, X-Frame-Options…)
|
||||||
|
curl -I https://example.com/sw.js
|
||||||
|
# Attendu : Cache-Control: no-cache
|
||||||
|
----
|
||||||
|
|
||||||
|
Ouvrir le site dans un navigateur : vidéos, podcasts, timeline et le bouton d'installation PWA doivent fonctionner.
|
||||||
|
|
||||||
|
== 8️⃣ Clé SSH du CI
|
||||||
|
|
||||||
|
C'est la clé utilisée par le runner Gitea Actions pour déployer. Elle est *différente* de la deploy key de lecture (étape 3) : celle-ci doit pouvoir écrire dans le clone.
|
||||||
|
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
# Générer la paire SUR VOTRE POSTE (pas sur le serveur)
|
||||||
|
ssh-keygen -t ed25519 -f annu-kute-ced-deploy -C "ci-gitea-annu-kute-ced"
|
||||||
|
|
||||||
|
# Autoriser la clé publique pour l'utilisateur deploy
|
||||||
|
sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys < annu-kute-ced-deploy.pub
|
||||||
|
----
|
||||||
|
|
||||||
|
La clé privée (`annu-kute-ced-deploy`, sans passphrase) ira dans les secrets Gitea à l'étape suivante. *Conservez-la en lieu sûr et ne la commitez jamais.*
|
||||||
|
|
||||||
|
== 9️⃣ Secrets Gitea
|
||||||
|
|
||||||
|
Dans LaBola : *Settings du dépôt → Actions → Secrets*, créer :
|
||||||
|
|
||||||
|
[cols="1,3",options="header"]
|
||||||
|
|===
|
||||||
|
| Secret | Valeur
|
||||||
|
|
||||||
|
| `SSH_HOST`
|
||||||
|
| Adresse du serveur (IP ou FQDN), port 22 par défaut (sinon `host:port`)
|
||||||
|
|
||||||
|
| `SSH_USER`
|
||||||
|
| `deploy`
|
||||||
|
|
||||||
|
| `SSH_KEY`
|
||||||
|
| Contenu *intégral* de la clé privée générée à l'étape 8
|
||||||
|
|
||||||
|
| `PROD_DEPLOY_PATH`
|
||||||
|
| `/var/www/annu-kute-ced`
|
||||||
|
|===
|
||||||
|
|
||||||
|
WARNING: `SSH_KEY` donne un accès shell au serveur avec les droits de `deploy`. Ne jamais la coller ailleurs que dans les secrets Gitea, et révoquer la clé publique (`authorized_keys`) en cas de doute.
|
||||||
|
|
||||||
|
== 🔟 Test du pipeline
|
||||||
|
|
||||||
|
. Pousser un commit sur `main` (par exemple une correction de coquille dans le README).
|
||||||
|
. Dans LaBola, onglet *Actions* : le workflow *Déploiement PROD* doit exécuter `check` puis `deploy` au vert.
|
||||||
|
. Côté serveur :
|
||||||
|
+
|
||||||
|
[source,bash]
|
||||||
|
----
|
||||||
|
cd /var/www/annu-kute-ced
|
||||||
|
git log -1 --oneline # doit correspondre au commit poussé
|
||||||
|
grep STATIC_CACHE_NAME sw.js # le suffixe de version a été bumpé à l'heure du déploiement
|
||||||
|
----
|
||||||
|
. Côté visiteur : au prochain chargement de page, le modal « Nouvelle version disponible » apparaît (Service Worker déjà installé lors d'une visite précédente).
|
||||||
|
|
||||||
|
== 🔄 Fonctionnement courant
|
||||||
|
|
||||||
|
[cols="1,3",options="header"]
|
||||||
|
|===
|
||||||
|
| Événement | Résultat
|
||||||
|
|
||||||
|
| Pull request vers `main`
|
||||||
|
| Workflow *Vérification PR* : lints et validations bloquants en cas d'erreur
|
||||||
|
|
||||||
|
| Push sur `main`
|
||||||
|
| Workflow *Déploiement PROD* : mêmes vérifications, puis SSH → `git pull --ff-only` → bump de la version des caches `sw.js` → modal de mise à jour chez les visiteurs
|
||||||
|
|
||||||
|
| En local, avant de pousser
|
||||||
|
| `scripts/check.sh` exécute les mêmes vérifications que le CI
|
||||||
|
|===
|
||||||
|
|
||||||
|
Le bump de version dans `sw.js` est fait *sur le serveur uniquement* : le dépôt garde sa valeur de référence, le working tree du serveur est nettoyé (`git checkout -- sw.js`) avant chaque pull pour garantir le fast-forward.
|
||||||
|
|
||||||
|
== 🧰 Dépannage
|
||||||
|
|
||||||
|
[cols="1,3",options="header"]
|
||||||
|
|===
|
||||||
|
| Symptôme | Piste
|
||||||
|
|
||||||
|
| `git pull --ff-only` échoue sur le serveur
|
||||||
|
| Une modification locale existe : `git status` dans le docroot, puis `git checkout -- <fichier>` (le workflow le fait déjà pour `sw.js`)
|
||||||
|
|
||||||
|
| L'action ne se déclenche pas
|
||||||
|
| Vérifier que le runner act est en ligne (LaBola → *Site Administration → Actions → Runners*) et que Actions est activé pour le dépôt (*Settings → Units*)
|
||||||
|
|
||||||
|
| `Permission denied (publickey)` dans le job deploy
|
||||||
|
| Clé publique absente de `/home/deploy/.ssh/authorized_keys`, ou mauvais `SSH_USER`/`SSH_HOST`
|
||||||
|
|
||||||
|
| Les visiteurs ne reçoivent pas la mise à jour
|
||||||
|
| Vérifier `curl -I https://example.com/sw.js` → `Cache-Control: no-cache` requis (règles fournies dans `conf/`)
|
||||||
|
|
||||||
|
| Erreurs 500 sur les vidéos/podcasts
|
||||||
|
| `cache/` non accessible en écriture : `chown -R www-data:www-data cache/`
|
||||||
|
|
||||||
|
| `php-intl` manquant
|
||||||
|
| `apt install php-intl` puis `systemctl restart php*-fpm` (Nginx) ou `systemctl restart apache2` (Apache)
|
||||||
|
|===
|
||||||
|
|
||||||
|
== 🔒 Notes de sécurité
|
||||||
|
|
||||||
|
- La clé privée du CI est *dédiée* à ce dépôt : une clé compromise ne donne accès qu'au compte `deploy`, sans sudo.
|
||||||
|
- La deploy key Gitea (étape 3) est en *lecture seule*.
|
||||||
|
- Pour restreindre davantage la clé du CI, on peut limiter les commandes dans `authorized_keys` (`command="..."`, `no-pty`) — facultatif, hors scope de ce guide.
|
||||||
|
- Les fichiers sensibles (`includes/`, `.htaccess`, `config.local.php`) sont bloqués en accès web par les configurations fournies dans `conf/`.
|
||||||
|
|
||||||
|
== 📞 Support
|
||||||
|
|
||||||
|
En cas de blocage : mailto:kontak@o-k-i.net[kontak@o-k-i.net]
|
||||||
@@ -42,8 +42,8 @@ Cette application est un *fork* de https://labola.o-k-i.net/ORGANISATION-KA-INTE
|
|||||||
| Dépôt de ce fork
|
| Dépôt de ce fork
|
||||||
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
|
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
|
||||||
|
|
||||||
| Domaine de production
|
| Domaine (exemple)
|
||||||
| https://annukuteced.buzz
|
| https://example.com
|
||||||
|
|
||||||
| Gouvernance
|
| Gouvernance
|
||||||
| Application maintenue par OKI ; fork porté par le propriétaire du dépôt *cedric* (Cédric Famibelle-Pronzola)
|
| Application maintenue par OKI ; fork porté par le propriétaire du dépôt *cedric* (Cédric Famibelle-Pronzola)
|
||||||
@@ -124,7 +124,8 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
|
|||||||
|
|
||||||
==== 🔍 SEO et données structurées
|
==== 🔍 SEO et données structurées
|
||||||
|
|
||||||
- *JSON-LD* généré automatiquement : `WebSite` (avec `SearchAction`), `VideoObject`, `CollectionPage`, `BreadcrumbList`
|
- *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*
|
- Balises *Open Graph* et *Twitter Cards*
|
||||||
- `sitemap.xml` et `robots.txt` fournis en exemples
|
- `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
|
- Prêt pour *Plausible Analytics* (voir <<fr-analytics,Analytics>>) : désactivé par défaut, sans cookies et conforme RGPD
|
||||||
@@ -151,6 +152,8 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
|
|||||||
|
|
||||||
[source]
|
[source]
|
||||||
----
|
----
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/ # CI/CD Gitea Actions (check-pr, deploy-prod)
|
||||||
├── ajax/
|
├── ajax/
|
||||||
│ └── load-more-videos.php # Endpoint AJAX « Voir plus » (CSRF + Origin)
|
│ └── load-more-videos.php # Endpoint AJAX « Voir plus » (CSRF + Origin)
|
||||||
├── conf/
|
├── conf/
|
||||||
@@ -192,7 +195,10 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
|
|||||||
│ ├── search.js # Cartes cliquables de la recherche
|
│ ├── search.js # Cartes cliquables de la recherche
|
||||||
│ ├── mastodon-config.php # Configuration JS de la timeline (servie en JS)
|
│ ├── mastodon-config.php # Configuration JS de la timeline (servie en JS)
|
||||||
│ ├── mastodon-timeline.umd.js # mastodon-embed-timeline v4.7.0 (vendored)
|
│ ├── mastodon-timeline.umd.js # mastodon-embed-timeline v4.7.0 (vendored)
|
||||||
│ └── pleroma-adapter.js # Adaptateur Pleroma → API Mastodon
|
│ ├── 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)
|
├── uploads/ # Images d'annonces (non versionné, hors .gitkeep)
|
||||||
├── index.php # Page d'accueil (agrégateur)
|
├── index.php # Page d'accueil (agrégateur)
|
||||||
├── video.php # Page de lecture d'une vidéo
|
├── video.php # Page de lecture d'une vidéo
|
||||||
@@ -208,6 +214,7 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
|
|||||||
├── mentions-legales.php.sample # Exemple de mentions légales
|
├── mentions-legales.php.sample # Exemple de mentions légales
|
||||||
├── dons.php.sample # Exemple de page de dons
|
├── dons.php.sample # Exemple de page de dons
|
||||||
├── LICENSE # GNU AGPL v3
|
├── LICENSE # GNU AGPL v3
|
||||||
|
├── DEPLOY.adoc # Guide de déploiement serveur + CI/CD
|
||||||
└── README.adoc
|
└── README.adoc
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -222,7 +229,7 @@ NOTE: Les fichiers marqués « legacy » sont conservés pour référence mais n
|
|||||||
* `intl` — dates internationales et fuseaux horaires
|
* `intl` — dates internationales et fuseaux horaires
|
||||||
* `mbstring` — chaînes multi-octets
|
* `mbstring` — chaînes multi-octets
|
||||||
* `xml` (SimpleXML) — recommandée pour Castopod (un fallback regex existe)
|
* `xml` (SimpleXML) — recommandée pour Castopod (un fallback regex existe)
|
||||||
- 🌐 *Serveur web* : Apache ou Nginx
|
- 🌐 *Serveur web* : Nginx (recommandé) ou Apache
|
||||||
- 🔒 *HTTPS* : requis pour les fonctionnalités PWA
|
- 🔒 *HTTPS* : requis pour les fonctionnalités PWA
|
||||||
|
|
||||||
.Installation de l'extension `intl`
|
.Installation de l'extension `intl`
|
||||||
@@ -266,7 +273,7 @@ cp dons.php.sample dons.php
|
|||||||
. 🛡️ Installez la configuration serveur sécurisée depuis `conf/` (voir <<fr-securite,Sécurité>>).
|
. 🛡️ 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).
|
. ✍️ 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).
|
. 🔒 Assurez-vous que HTTPS est actif (obligatoire pour la PWA).
|
||||||
. 🌐 Faites pointer votre domaine (`annukuteced.buzz`) vers la racine du projet.
|
. 🌐 Faites pointer votre domaine (ex. `example.com`) vers la racine du projet.
|
||||||
|
|
||||||
[[fr-configuration]]
|
[[fr-configuration]]
|
||||||
=== ⚙️ Configuration
|
=== ⚙️ Configuration
|
||||||
@@ -307,7 +314,7 @@ Valeurs par défaut issues de `includes/config.default.php`.
|
|||||||
| Constante | Défaut | Description
|
| Constante | Défaut | Description
|
||||||
|
|
||||||
| `APP_HOST_NAME`
|
| `APP_HOST_NAME`
|
||||||
| `'annukuteced.buzz'`
|
| `'example.com'`
|
||||||
| Nom de domaine affiché (mentions légales…)
|
| Nom de domaine affiché (mentions légales…)
|
||||||
|
|
||||||
| `ORGANIZATION_SHORT_NAME`
|
| `ORGANIZATION_SHORT_NAME`
|
||||||
@@ -341,7 +348,7 @@ Valeurs par défaut issues de `includes/config.default.php`.
|
|||||||
| Afficher le nombre de vues sur les cartes vidéo
|
| Afficher le nombre de vues sur les cartes vidéo
|
||||||
|
|
||||||
| `PRIORITY_CATEGORIES`
|
| `PRIORITY_CATEGORIES`
|
||||||
| `[11, 15, 4, 9, 10]`
|
| `[10, 15]`
|
||||||
| Catégories mises en avant : `[ID => 'Nom']`, ordre = ordre d'affichage (accueil, sidebar, footer, menu mobile)
|
| Catégories mises en avant : `[ID => 'Nom']`, ordre = ordre d'affichage (accueil, sidebar, footer, menu mobile)
|
||||||
|
|
||||||
| `DEFAULT_TIMEZONE`
|
| `DEFAULT_TIMEZONE`
|
||||||
@@ -629,11 +636,11 @@ NOTE: Au moins une plateforme (LiberaPay, Ko-fi ou Stripe) doit être configuré
|
|||||||
| Durée max d'un short (s) ; ratio portrait (`aspectRatio ≤ 1`) aussi requis
|
| Durée max d'un short (s) ; ratio portrait (`aspectRatio ≤ 1`) aussi requis
|
||||||
|
|
||||||
| `IMPORTANT_TAGS`
|
| `IMPORTANT_TAGS`
|
||||||
| `['ANNUKUTECED', 'podcast', …]`
|
| `['ANNUKUTECED']`
|
||||||
| Hashtags de la sidebar, du footer et du menu mobile
|
| Hashtags de la sidebar, du footer et du menu mobile
|
||||||
|
|
||||||
| `POPULAR_TAGS`
|
| `POPULAR_TAGS`
|
||||||
| `['podcast', 'annukuteced', …]`
|
| `['ANNUKUTECED']`
|
||||||
| Hashtags affichés sur l'accueil
|
| Hashtags affichés sur l'accueil
|
||||||
|===
|
|===
|
||||||
|
|
||||||
@@ -673,8 +680,8 @@ NOTE: Au moins une plateforme (LiberaPay, Ko-fi ou Stripe) doit être configuré
|
|||||||
| Constante | Défaut | Description
|
| Constante | Défaut | Description
|
||||||
|
|
||||||
| `CACHE_ENABLED`
|
| `CACHE_ENABLED`
|
||||||
| `false`
|
| `true`
|
||||||
| Active le cache pour *Castopod et Funkwhale* (fortement recommandé)
|
| 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`
|
| `CACHE_DURATION`
|
||||||
| `3600`
|
| `3600`
|
||||||
@@ -706,7 +713,7 @@ NOTE: Le cache de l'API PeerTube et de WordPress (`includes/simple-cache.php`) e
|
|||||||
[source,php]
|
[source,php]
|
||||||
----
|
----
|
||||||
<?php
|
<?php
|
||||||
define('APP_HOST_NAME', 'annukuteced.buzz');
|
define('APP_HOST_NAME', 'example.com');
|
||||||
|
|
||||||
// PeerTube (chaîne GADE du podcast)
|
// PeerTube (chaîne GADE du podcast)
|
||||||
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
||||||
@@ -743,7 +750,7 @@ define('CACHE_DURATION', 3600);
|
|||||||
|
|
||||||
==== Fichiers d'instance à personnaliser
|
==== 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 sont *pré-remplis pour `annukuteced.buzz`* ; pour un autre domaine, remplacez simplement les occurrences du domaine. Dans `mentions-legales.php`, remplacez aussi le placeholder `VOTRE-DATE-MAJ`.
|
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
|
=== 🧭 Ordre des sections sur la page d'accueil
|
||||||
|
|
||||||
@@ -776,29 +783,29 @@ NOTE: L'en-tête COEP a été *volontairement retiré* (commit `62d4d99`) : `req
|
|||||||
- *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-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)
|
- *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 (Apache)
|
==== 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]
|
[source,bash]
|
||||||
----
|
----
|
||||||
cp conf/.htaccess.sample .htaccess
|
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` (URLs propres `/video` au lieu de `/video.php`).
|
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.
|
WARNING: Cette configuration est *essentielle* : sans elle, vos fichiers de configuration sont exposés aux visiteurs.
|
||||||
|
|
||||||
==== Configuration serveur (Nginx)
|
|
||||||
|
|
||||||
[source,bash]
|
|
||||||
----
|
|
||||||
# Adaptez les chemins dans conf/nginx.conf.sample puis :
|
|
||||||
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/annukuteced.buzz
|
|
||||||
sudo ln -s /etc/nginx/sites-available/annukuteced.buzz /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
----
|
|
||||||
|
|
||||||
WARNING: Le bloc `location ~* \.(php|inc|conf|config|local)$ { deny all; }` de l'exemple correspond tel quel à *tous* les fichiers `.php`. Ajustez-le pour ne cibler que les chemins sensibles (par exemple `^/includes/`), sous peine de rendre l'application inaccessible.
|
|
||||||
|
|
||||||
=== 📱 Progressive Web App (PWA)
|
=== 📱 Progressive Web App (PWA)
|
||||||
|
|
||||||
==== Fonctionnalités
|
==== Fonctionnalités
|
||||||
@@ -806,7 +813,7 @@ WARNING: Le bloc `location ~* \.(php|inc|conf|config|local)$ { deny all; }` de l
|
|||||||
- 📲 *Installation native* : bouton « Installer » dans le header à la première visite
|
- 📲 *Installation native* : bouton « Installer » dans le header à la première visite
|
||||||
- 🌐 *Mode hors ligne* : cache des pages visitées et des ressources statiques
|
- 🌐 *Mode hors ligne* : cache des pages visitées et des ressources statiques
|
||||||
- 📡 *Détection de connexion* : indicateur visuel en cas de perte réseau
|
- 📡 *Détection de connexion* : indicateur visuel en cas de perte réseau
|
||||||
- 🔄 *Mise à jour* : proposition de rechargement quand une nouvelle version est détectée
|
- 🔄 *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`)
|
==== Stratégies de cache du Service Worker (`sw.js`)
|
||||||
|
|
||||||
@@ -827,11 +834,22 @@ WARNING: Le bloc `location ~* \.(php|inc|conf|config|local)$ { deny all; }` de l
|
|||||||
| Jamais de cache ; réponse JSON 503 synthétique hors ligne
|
| Jamais de cache ; réponse JSON 503 synthétique hors ligne
|
||||||
|===
|
|===
|
||||||
|
|
||||||
TIP: Pour déployer une nouvelle version des assets, changez le suffixe de version des noms de cache dans `sw.js` (format date, ex. `08072026-0720`) : les anciens caches sont supprimés à l'activation.
|
==== 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
|
==== Installation par les visiteurs
|
||||||
|
|
||||||
- 🌐 *Chrome/Edge* : menu → « Installer annukuteced.buzz »
|
- 🌐 *Chrome/Edge* : menu → « Installer example.com »
|
||||||
- 🍎 *Safari iOS* : Partager → « Ajouter à l'écran d'accueil »
|
- 🍎 *Safari iOS* : Partager → « Ajouter à l'écran d'accueil »
|
||||||
- 🦊 *Firefox Android* : menu → « Installer »
|
- 🦊 *Firefox Android* : menu → « Installer »
|
||||||
|
|
||||||
@@ -855,7 +873,7 @@ Données alors collectées (anonymes) : pages visitées, pays d'origine (IP non
|
|||||||
. 🔧 Permissions : `644` pour les fichiers, `755` pour les dossiers ; `cache/` accessible en écriture
|
. 🔧 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)
|
. 📄 Créez les fichiers d'instance depuis les `.sample` (config, manifest, robots, sitemap, mentions légales, dons)
|
||||||
. 🛡️ Copiez `conf/.htaccess.sample` en `.htaccess` (Apache)
|
. 🛡️ Copiez `conf/.htaccess.sample` en `.htaccess` (Apache)
|
||||||
. 🌐 Faites pointer le domaine `annukuteced.buzz` vers le dossier d'installation
|
. 🌐 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)
|
. 🧪 Testez la PWA via les outils de développement du navigateur (onglet Application)
|
||||||
|
|
||||||
=== 👨💻 Développement et contribution
|
=== 👨💻 Développement et contribution
|
||||||
@@ -874,21 +892,41 @@ Bonnes pratiques du dépôt :
|
|||||||
|
|
||||||
==== 📄 Exporter la documentation (HTML / PDF)
|
==== 📄 Exporter la documentation (HTML / PDF)
|
||||||
|
|
||||||
Ce README peut être exporté en HTML et en PDF en une seule commande :
|
Ce README peut être exporté en HTML et en PDF en une seule commande (tout fichier `.adoc` peut être passé en argument) :
|
||||||
|
|
||||||
[source,bash]
|
[source,bash]
|
||||||
----
|
----
|
||||||
docs/generate-readme-pdf.sh # README.html + README.pdf
|
docs/generate-readme-pdf.sh # README.html + README.pdf
|
||||||
docs/generate-readme-pdf.sh --html # HTML seulement
|
docs/generate-readme-pdf.sh --html # HTML seulement
|
||||||
docs/generate-readme-pdf.sh --pdf # PDF seulement
|
docs/generate-readme-pdf.sh --pdf # PDF seulement
|
||||||
|
docs/generate-readme-pdf.sh DEPLOY.adoc # DEPLOY.html + DEPLOY.pdf
|
||||||
|
docs/generate-readme-pdf.sh DEPLOY.adoc --pdf # DEPLOY.pdf seulement
|
||||||
----
|
----
|
||||||
|
|
||||||
Les fichiers sont produits à la racine du projet et ignorés par Git (`.gitignore`).
|
Les fichiers sont produits à la racine du projet (nommés d'après la source) 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.
|
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.
|
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
|
=== 📜 Licence
|
||||||
|
|
||||||
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
|
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
|
||||||
@@ -934,8 +972,8 @@ This application is a *fork* of https://labola.o-k-i.net/ORGANISATION-KA-INTERNA
|
|||||||
| This fork's repository
|
| This fork's repository
|
||||||
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
|
| `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
|
||||||
|
|
||||||
| Production domain
|
| Domain (example)
|
||||||
| https://annukuteced.buzz
|
| https://example.com
|
||||||
|
|
||||||
| Governance
|
| Governance
|
||||||
| Application maintained by OKI; fork carried by the repository owner *cedric* (Cédric Famibelle-Pronzola)
|
| Application maintained by OKI; fork carried by the repository owner *cedric* (Cédric Famibelle-Pronzola)
|
||||||
@@ -1016,7 +1054,8 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
|
|||||||
|
|
||||||
==== 🔍 SEO and structured data
|
==== 🔍 SEO and structured data
|
||||||
|
|
||||||
- Automatically generated *JSON-LD*: `WebSite` (with `SearchAction`), `VideoObject`, `CollectionPage`, `BreadcrumbList`
|
- 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
|
- *Open Graph* and *Twitter Cards* meta tags
|
||||||
- `sitemap.xml` and `robots.txt` provided as samples
|
- `sitemap.xml` and `robots.txt` provided as samples
|
||||||
- Ready for *Plausible Analytics* (see <<en-analytics,Analytics>>): disabled by default, cookie-free and GDPR-compliant
|
- Ready for *Plausible Analytics* (see <<en-analytics,Analytics>>): disabled by default, cookie-free and GDPR-compliant
|
||||||
@@ -1043,6 +1082,8 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
|
|||||||
|
|
||||||
[source]
|
[source]
|
||||||
----
|
----
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/ # Gitea Actions CI/CD (check-pr, deploy-prod)
|
||||||
├── ajax/
|
├── ajax/
|
||||||
│ └── load-more-videos.php # "Load more" AJAX endpoint (CSRF + Origin)
|
│ └── load-more-videos.php # "Load more" AJAX endpoint (CSRF + Origin)
|
||||||
├── conf/
|
├── conf/
|
||||||
@@ -1084,7 +1125,10 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
|
|||||||
│ ├── search.js # Clickable search result cards
|
│ ├── search.js # Clickable search result cards
|
||||||
│ ├── mastodon-config.php # Timeline JS config (served as JavaScript)
|
│ ├── mastodon-config.php # Timeline JS config (served as JavaScript)
|
||||||
│ ├── mastodon-timeline.umd.js # mastodon-embed-timeline v4.7.0 (vendored)
|
│ ├── mastodon-timeline.umd.js # mastodon-embed-timeline v4.7.0 (vendored)
|
||||||
│ └── pleroma-adapter.js # Pleroma → Mastodon API adapter
|
│ ├── 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)
|
├── uploads/ # Announcement images (not versioned, except .gitkeep)
|
||||||
├── index.php # Homepage (aggregator)
|
├── index.php # Homepage (aggregator)
|
||||||
├── video.php # Video playback page
|
├── video.php # Video playback page
|
||||||
@@ -1100,6 +1144,7 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
|
|||||||
├── mentions-legales.php.sample # Legal notice sample
|
├── mentions-legales.php.sample # Legal notice sample
|
||||||
├── dons.php.sample # Donation page sample
|
├── dons.php.sample # Donation page sample
|
||||||
├── LICENSE # GNU AGPL v3
|
├── LICENSE # GNU AGPL v3
|
||||||
|
├── DEPLOY.adoc # Server deployment + CI/CD guide
|
||||||
└── README.adoc
|
└── README.adoc
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -1114,7 +1159,7 @@ NOTE: Files marked "legacy" are kept for reference but are not included by any p
|
|||||||
* `intl` — international dates and timezones
|
* `intl` — international dates and timezones
|
||||||
* `mbstring` — multi-byte strings
|
* `mbstring` — multi-byte strings
|
||||||
* `xml` (SimpleXML) — recommended for Castopod (a regex fallback exists)
|
* `xml` (SimpleXML) — recommended for Castopod (a regex fallback exists)
|
||||||
- 🌐 *Web server*: Apache or Nginx
|
- 🌐 *Web server*: Nginx (recommended) or Apache
|
||||||
- 🔒 *HTTPS*: required for PWA features
|
- 🔒 *HTTPS*: required for PWA features
|
||||||
|
|
||||||
.Installing the `intl` extension
|
.Installing the `intl` extension
|
||||||
@@ -1158,7 +1203,7 @@ cp dons.php.sample dons.php
|
|||||||
. 🛡️ Install the hardened server configuration from `conf/` (see <<en-securite,Security>>).
|
. 🛡️ 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 the `cache/` directory writable by the web server (created automatically on the first API call).
|
||||||
. 🔒 Make sure HTTPS is enabled (mandatory for the PWA).
|
. 🔒 Make sure HTTPS is enabled (mandatory for the PWA).
|
||||||
. 🌐 Point your domain (`annukuteced.buzz`) to the project root.
|
. 🌐 Point your domain (e.g. `example.com`) to the project root.
|
||||||
|
|
||||||
[[en-configuration]]
|
[[en-configuration]]
|
||||||
=== ⚙️ Configuration
|
=== ⚙️ Configuration
|
||||||
@@ -1199,7 +1244,7 @@ Default values from `includes/config.default.php`.
|
|||||||
| Constant | Default | Description
|
| Constant | Default | Description
|
||||||
|
|
||||||
| `APP_HOST_NAME`
|
| `APP_HOST_NAME`
|
||||||
| `'annukuteced.buzz'`
|
| `'example.com'`
|
||||||
| Displayed domain name (legal notice…)
|
| Displayed domain name (legal notice…)
|
||||||
|
|
||||||
| `ORGANIZATION_SHORT_NAME`
|
| `ORGANIZATION_SHORT_NAME`
|
||||||
@@ -1233,7 +1278,7 @@ Default values from `includes/config.default.php`.
|
|||||||
| Show view counts on video cards
|
| Show view counts on video cards
|
||||||
|
|
||||||
| `PRIORITY_CATEGORIES`
|
| `PRIORITY_CATEGORIES`
|
||||||
| `[11, 15, 4, 9, 10]`
|
| `[10, 15]`
|
||||||
| Featured categories: `[ID => 'Name']`, order = display order (homepage, sidebar, footer, mobile menu)
|
| Featured categories: `[ID => 'Name']`, order = display order (homepage, sidebar, footer, mobile menu)
|
||||||
|
|
||||||
| `DEFAULT_TIMEZONE`
|
| `DEFAULT_TIMEZONE`
|
||||||
@@ -1521,11 +1566,11 @@ NOTE: At least one platform (LiberaPay, Ko-fi or Stripe) must be configured for
|
|||||||
| Max short duration (s); portrait ratio (`aspectRatio ≤ 1`) also required
|
| Max short duration (s); portrait ratio (`aspectRatio ≤ 1`) also required
|
||||||
|
|
||||||
| `IMPORTANT_TAGS`
|
| `IMPORTANT_TAGS`
|
||||||
| `['ANNUKUTECED', 'podcast', …]`
|
| `['ANNUKUTECED']`
|
||||||
| Hashtags in the sidebar, footer and mobile menu
|
| Hashtags in the sidebar, footer and mobile menu
|
||||||
|
|
||||||
| `POPULAR_TAGS`
|
| `POPULAR_TAGS`
|
||||||
| `['podcast', 'annukuteced', …]`
|
| `['ANNUKUTECED']`
|
||||||
| Hashtags displayed on the homepage
|
| Hashtags displayed on the homepage
|
||||||
|===
|
|===
|
||||||
|
|
||||||
@@ -1565,8 +1610,8 @@ NOTE: At least one platform (LiberaPay, Ko-fi or Stripe) must be configured for
|
|||||||
| Constant | Default | Description
|
| Constant | Default | Description
|
||||||
|
|
||||||
| `CACHE_ENABLED`
|
| `CACHE_ENABLED`
|
||||||
| `false`
|
| `true`
|
||||||
| Enable caching for *Castopod and Funkwhale* (strongly recommended)
|
| Enable caching for *Castopod and Funkwhale*. Essential against the Castopod RSS rate limit; empty results (error, 429) are never cached
|
||||||
|
|
||||||
| `CACHE_DURATION`
|
| `CACHE_DURATION`
|
||||||
| `3600`
|
| `3600`
|
||||||
@@ -1598,7 +1643,7 @@ NOTE: The PeerTube API and WordPress cache (`includes/simple-cache.php`) is *alw
|
|||||||
[source,php]
|
[source,php]
|
||||||
----
|
----
|
||||||
<?php
|
<?php
|
||||||
define('APP_HOST_NAME', 'annukuteced.buzz');
|
define('APP_HOST_NAME', 'example.com');
|
||||||
|
|
||||||
// PeerTube (the podcast's GADE channel)
|
// PeerTube (the podcast's GADE channel)
|
||||||
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
||||||
@@ -1635,7 +1680,7 @@ define('CACHE_DURATION', 3600);
|
|||||||
|
|
||||||
==== Instance files to customize
|
==== 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 are *pre-filled for `annukuteced.buzz`*; for another domain, simply replace the domain occurrences. In `mentions-legales.php`, also replace the `VOTRE-DATE-MAJ` placeholder.
|
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
|
=== 🧭 Homepage section order
|
||||||
|
|
||||||
@@ -1668,29 +1713,29 @@ NOTE: The COEP header was *deliberately removed* (commit `62d4d99`): `require-co
|
|||||||
- *Anti-SSRF*: instance URL validation (http/https scheme, blocking private IPs and `localhost`), allowlist of PeerTube API endpoints, cURL without redirects
|
- *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)
|
- *Anti-XSS*: systematic output escaping; input validation (video UUID, search query ≤ 200 chars, page numbers, category ID 1–20)
|
||||||
|
|
||||||
==== Server configuration (Apache)
|
==== 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]
|
[source,bash]
|
||||||
----
|
----
|
||||||
cp conf/.htaccess.sample .htaccess
|
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 (clean URLs `/video` instead of `/video.php`).
|
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.
|
WARNING: This configuration is *essential*: without it, your configuration files are exposed to visitors.
|
||||||
|
|
||||||
==== Server configuration (Nginx)
|
|
||||||
|
|
||||||
[source,bash]
|
|
||||||
----
|
|
||||||
# Adjust the paths in conf/nginx.conf.sample, then:
|
|
||||||
sudo cp conf/nginx.conf.sample /etc/nginx/sites-available/annukuteced.buzz
|
|
||||||
sudo ln -s /etc/nginx/sites-available/annukuteced.buzz /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
----
|
|
||||||
|
|
||||||
WARNING: The sample's `location ~* \.(php|inc|conf|config|local)$ { deny all; }` block as written matches *all* `.php` files. Adjust it to target only sensitive paths (e.g. `^/includes/`), or the whole application will become unreachable.
|
|
||||||
|
|
||||||
=== 📱 Progressive Web App (PWA)
|
=== 📱 Progressive Web App (PWA)
|
||||||
|
|
||||||
==== Features
|
==== Features
|
||||||
@@ -1698,7 +1743,7 @@ WARNING: The sample's `location ~* \.(php|inc|conf|config|local)$ { deny all; }`
|
|||||||
- 📲 *Native installation*: an "Install" button in the header on first visit
|
- 📲 *Native installation*: an "Install" button in the header on first visit
|
||||||
- 🌐 *Offline mode*: visited pages and static assets cached
|
- 🌐 *Offline mode*: visited pages and static assets cached
|
||||||
- 📡 *Connection detection*: visual indicator when the network is lost
|
- 📡 *Connection detection*: visual indicator when the network is lost
|
||||||
- 🔄 *Updates*: reload prompt when a new version is detected
|
- 🔄 *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`)
|
==== Service Worker caching strategies (`sw.js`)
|
||||||
|
|
||||||
@@ -1719,11 +1764,22 @@ WARNING: The sample's `location ~* \.(php|inc|conf|config|local)$ { deny all; }`
|
|||||||
| Never cached; synthetic 503 JSON response when offline
|
| Never cached; synthetic 503 JSON response when offline
|
||||||
|===
|
|===
|
||||||
|
|
||||||
TIP: To deploy a new version of the assets, change the version suffix of the cache names in `sw.js` (date format, e.g. `08072026-0720`): old caches are deleted on activation.
|
==== 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
|
==== Installation by visitors
|
||||||
|
|
||||||
- 🌐 *Chrome/Edge*: menu → "Install annukuteced.buzz"
|
- 🌐 *Chrome/Edge*: menu → "Install example.com"
|
||||||
- 🍎 *Safari iOS*: Share → "Add to Home Screen"
|
- 🍎 *Safari iOS*: Share → "Add to Home Screen"
|
||||||
- 🦊 *Firefox Android*: menu → "Install"
|
- 🦊 *Firefox Android*: menu → "Install"
|
||||||
|
|
||||||
@@ -1747,7 +1803,7 @@ Data then collected (anonymous): visited pages, country of origin (IP not stored
|
|||||||
. 🔧 Permissions: `644` for files, `755` for directories; `cache/` must be writable
|
. 🔧 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)
|
. 📄 Create the instance files from the `.sample` files (config, manifest, robots, sitemap, legal notice, donations)
|
||||||
. 🛡️ Copy `conf/.htaccess.sample` to `.htaccess` (Apache)
|
. 🛡️ Copy `conf/.htaccess.sample` to `.htaccess` (Apache)
|
||||||
. 🌐 Point the `annukuteced.buzz` domain to the installation directory
|
. 🌐 Point your domain (e.g. `example.com`) to the installation directory
|
||||||
. 🧪 Test the PWA via the browser developer tools (Application tab)
|
. 🧪 Test the PWA via the browser developer tools (Application tab)
|
||||||
|
|
||||||
=== 👨💻 Development and contributing
|
=== 👨💻 Development and contributing
|
||||||
@@ -1766,21 +1822,41 @@ Repository best practices:
|
|||||||
|
|
||||||
==== 📄 Exporting the documentation (HTML / PDF)
|
==== 📄 Exporting the documentation (HTML / PDF)
|
||||||
|
|
||||||
This README can be exported to HTML and PDF with a single command:
|
This README can be exported to HTML and PDF with a single command (any `.adoc` file can be passed as argument):
|
||||||
|
|
||||||
[source,bash]
|
[source,bash]
|
||||||
----
|
----
|
||||||
docs/generate-readme-pdf.sh # README.html + README.pdf
|
docs/generate-readme-pdf.sh # README.html + README.pdf
|
||||||
docs/generate-readme-pdf.sh --html # HTML only
|
docs/generate-readme-pdf.sh --html # HTML only
|
||||||
docs/generate-readme-pdf.sh --pdf # PDF only
|
docs/generate-readme-pdf.sh --pdf # PDF only
|
||||||
|
docs/generate-readme-pdf.sh DEPLOY.adoc # DEPLOY.html + DEPLOY.pdf
|
||||||
|
docs/generate-readme-pdf.sh DEPLOY.adoc --pdf # DEPLOY.pdf only
|
||||||
----
|
----
|
||||||
|
|
||||||
Files are produced in the project root and are Git-ignored (`.gitignore`).
|
Files are produced in the project root (named after the source) 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.
|
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.
|
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
|
=== 📜 License
|
||||||
|
|
||||||
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
|
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ if ($categoryId && isset($allCategories[$categoryId])) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
||||||
<title><?php echo htmlspecialchars($categoryName); ?> - <?php echo SITE_NAME; ?></title>
|
<title><?php echo htmlspecialchars($categoryName); ?> - <?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="Découvrez toutes les vidéos de la catégorie <?php echo htmlspecialchars($categoryName); ?> sur <?php echo SITE_NAME; ?>. Contenu multimédia de qualité et exclusif.">
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/categories.php?id=' . $categoryId; ?>">
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ if ($categoryId && isset($allCategories[$categoryId])) {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#FF0000">
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="Catégorie : <?php echo htmlspecialchars($categoryName); ?> - <?php echo SITE_NAME; ?>">
|
<meta property="og:title" content="Catégorie : <?php echo htmlspecialchars($categoryName); ?> - <?php echo SITE_NAME; ?>">
|
||||||
@@ -112,7 +114,7 @@ if ($categoryId && isset($allCategories[$categoryId])) {
|
|||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="<?php echo SITE_NAME; ?>">
|
<img src="img/logo.png" alt="<?php echo SITE_NAME; ?>">
|
||||||
</div>
|
</div>
|
||||||
<h2 class="section-title">Catégorie : <?php echo htmlspecialchars($categoryName); ?></h2>
|
<h1 class="section-title">Catégorie : <?php echo htmlspecialchars($categoryName); ?></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (empty($videos)): ?>
|
<?php if (empty($videos)): ?>
|
||||||
@@ -168,5 +170,6 @@ if ($categoryId && isset($allCategories[$categoryId])) {
|
|||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/categories.js"></script>
|
<script src="js/categories.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -55,3 +55,15 @@ RewriteRule ^([^/]+)$ $1.php [L]
|
|||||||
RewriteCond %{HTTP:X-Forwarded-Proto} !https
|
RewriteCond %{HTTP:X-Forwarded-Proto} !https
|
||||||
RewriteCond %{HTTPS} !on
|
RewriteCond %{HTTPS} !on
|
||||||
RewriteRule ^(.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
RewriteRule ^(.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# CACHE
|
||||||
|
# ======================
|
||||||
|
|
||||||
|
# Le Service Worker et le manifest ne doivent pas être cachés longtemps :
|
||||||
|
# le navigateur doit pouvoir détecter les nouvelles versions du site.
|
||||||
|
<FilesMatch "^(sw\.js|site\.webmanifest)$">
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set Cache-Control "no-cache, must-revalidate"
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ server {
|
|||||||
# ======================
|
# ======================
|
||||||
|
|
||||||
# Bloquer l'accès aux fichiers de configuration
|
# Bloquer l'accès aux fichiers de configuration
|
||||||
location ~* \.(php|inc|conf|config|local)$ {
|
# (les .php de includes/ sont déjà bloqués par le bloc "répertoires
|
||||||
|
# sensibles" ci-dessous ; ne PAS ajouter php ici, sinon toute
|
||||||
|
# l'application serait inaccessible)
|
||||||
|
location ~* \.(inc|conf|config|local)$ {
|
||||||
deny all;
|
deny all;
|
||||||
return 404;
|
return 404;
|
||||||
}
|
}
|
||||||
@@ -75,6 +78,18 @@ server {
|
|||||||
# OPTIMISATIONS
|
# OPTIMISATIONS
|
||||||
# ======================
|
# ======================
|
||||||
|
|
||||||
|
# Le Service Worker et le manifest ne doivent pas être cachés longtemps :
|
||||||
|
# le navigateur doit pouvoir détecter les nouvelles versions du site.
|
||||||
|
# (location exacte = prioritaire sur les regex ci-dessous ; "expires -1"
|
||||||
|
# émet Cache-Control: no-cache sans annuler les add_header de sécurité)
|
||||||
|
location = /sw.js {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /site.webmanifest {
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
# Cache des fichiers statiques
|
# Cache des fichiers statiques
|
||||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ setSecurityHeaders();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
||||||
<title><?php echo SITE_NAME; ?> - Ouverture prochaine</title>
|
<title><?php echo SITE_NAME; ?> - Ouverture prochaine</title>
|
||||||
|
<meta name="description" content="<?php echo htmlspecialchars(SITE_DESCRIPTION); ?> Ouverture prochaine.">
|
||||||
|
<link rel="canonical" href="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/countdown.php'; ?>">
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="css/countdown.css?v=<?php echo filemtime('css/countdown.css'); ?>">
|
<link rel="stylesheet" href="css/countdown.css?v=<?php echo filemtime('css/countdown.css'); ?>">
|
||||||
@@ -183,5 +185,6 @@ setSecurityHeaders();
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="js/countdown.js"></script>
|
<script src="js/countdown.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3411,4 +3411,118 @@ i.icon-mastodon,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
Modal de mise à jour PWA (js/pwa-update.js)
|
||||||
|
========================================== */
|
||||||
|
|
||||||
|
.pwa-update-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.55);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-overlay.pwa-update-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-modal {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 28px 24px 24px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateY(12px) scale(0.97);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-visible .pwa-update-modal {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-logo {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-text {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn-primary {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn-secondary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: var(--hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-red);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.pwa-update-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-update-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ $liveStream = getLiveStream();
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Direct - <?php echo SITE_NAME; ?></title>
|
<title>Direct - <?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="Suivez nos diffusions en direct sur <?php echo SITE_NAME; ?>. Contenu en temps réel, discussions et événements exclusifs.">
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl(); ?>/direct.php">
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ $liveStream = getLiveStream();
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#FF0000">
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="Direct - <?php echo SITE_NAME; ?>">
|
<meta property="og:title" content="Direct - <?php echo SITE_NAME; ?>">
|
||||||
@@ -191,7 +193,7 @@ $liveStream = getLiveStream();
|
|||||||
$dynamicTitle = NEXT_LIVE_TITLE;
|
$dynamicTitle = NEXT_LIVE_TITLE;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<h2><?php echo htmlspecialchars($dynamicTitle); ?></h2>
|
<h1><?php echo htmlspecialchars($dynamicTitle); ?></h1>
|
||||||
<?php
|
<?php
|
||||||
if (!empty(NEXT_LIVE_DATE)) {
|
if (!empty(NEXT_LIVE_DATE)) {
|
||||||
$liveHour = $liveDate->format('H\hi');
|
$liveHour = $liveDate->format('H\hi');
|
||||||
@@ -260,7 +262,7 @@ $liveStream = getLiveStream();
|
|||||||
?>
|
?>
|
||||||
<div class="no-live-message">
|
<div class="no-live-message">
|
||||||
<i class="fas fa-tv"></i>
|
<i class="fas fa-tv"></i>
|
||||||
<h2>Aucun direct en cours</h2>
|
<h1>Aucun direct en cours</h1>
|
||||||
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
|
<p>Revenez plus tard pour découvrir nos prochaines diffusions en direct.</p>
|
||||||
<a href="index.php" class="btn-primary">Retour à l'accueil</a>
|
<a href="index.php" class="btn-primary">Retour à l'accueil</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,5 +276,6 @@ $liveStream = getLiveStream();
|
|||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
<?php include 'includes/mobile-menu.php'; ?>
|
<?php include 'includes/mobile-menu.php'; ?>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Generate the documentation export(s) from README.adoc.
|
# Generate the HTML/PDF export(s) from any .adoc document (README.adoc by default).
|
||||||
#
|
#
|
||||||
# docs/generate-readme-pdf.sh # README.html + README.pdf
|
# docs/generate-readme-pdf.sh # README.html + README.pdf
|
||||||
# docs/generate-readme-pdf.sh --html # README.html only
|
# docs/generate-readme-pdf.sh --html # README.html only
|
||||||
# docs/generate-readme-pdf.sh --pdf # README.pdf only
|
# docs/generate-readme-pdf.sh --pdf # README.pdf only
|
||||||
|
# docs/generate-readme-pdf.sh DEPLOY.adoc # DEPLOY.html + DEPLOY.pdf
|
||||||
|
# docs/generate-readme-pdf.sh DEPLOY.adoc --pdf # DEPLOY.pdf only
|
||||||
#
|
#
|
||||||
# Both outputs land in the project root and are Git-ignored.
|
# Outputs land in the project root (named after the source) and are Git-ignored.
|
||||||
#
|
#
|
||||||
# Requirements:
|
# Requirements:
|
||||||
# - asciidoctor (e.g. gem install asciidoctor)
|
# - asciidoctor (e.g. gem install asciidoctor)
|
||||||
@@ -22,14 +24,41 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
MODE="${1:---all}"
|
SRC="README.adoc"
|
||||||
HTML_OUT="README.html"
|
MODE="--all"
|
||||||
PDF_OUT="README.pdf"
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--html|--pdf|--all)
|
||||||
|
MODE="$arg"
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,12p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*.adoc)
|
||||||
|
SRC="$arg"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [fichier.adoc] [--html|--pdf|--all]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -f "$SRC" ]]; then
|
||||||
|
echo "Erreur : fichier introuvable : $SRC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE="$(basename "$SRC" .adoc)"
|
||||||
|
HTML_OUT="$BASE.html"
|
||||||
|
PDF_OUT="$BASE.pdf"
|
||||||
|
|
||||||
build_html() {
|
build_html() {
|
||||||
asciidoctor -b html5 -a docinfodir=docs -a docinfo=shared README.adoc -o "$1"
|
asciidoctor -b html5 -a docinfodir=docs -a docinfo=shared "$SRC" -o "$1"
|
||||||
echo "HTML generated: $1"
|
echo "HTML generated: $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +87,4 @@ case "$MODE" in
|
|||||||
build_html "$HTML_OUT"
|
build_html "$HTML_OUT"
|
||||||
build_pdf "$HTML_OUT"
|
build_pdf "$HTML_OUT"
|
||||||
;;
|
;;
|
||||||
-h|--help)
|
|
||||||
sed -n '2,10p' "$0"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 [--html|--pdf|--all]" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
require_once 'includes/config.php';
|
require_once 'includes/config.php';
|
||||||
require_once 'includes/security.php';
|
require_once 'includes/security.php';
|
||||||
|
require_once 'includes/structured-data.php';
|
||||||
|
|
||||||
// Vérifier si les dons sont activés
|
// Vérifier si les dons sont activés
|
||||||
if (!defined('DONATIONS_ENABLED') || !DONATIONS_ENABLED) {
|
if (!defined('DONATIONS_ENABLED') || !DONATIONS_ENABLED) {
|
||||||
@@ -59,6 +60,7 @@ $stripeMonthlyLinks = defined('STRIPE_MONTHLY_LINKS') ? STRIPE_MONTHLY_LINKS : [
|
|||||||
<!-- PERSONNALISEZ: Titre et description de votre page de dons -->
|
<!-- PERSONNALISEZ: Titre et description de votre page de dons -->
|
||||||
<title>Soutenir <?php echo ORGANIZATION_NAME; ?> - Dons</title>
|
<title>Soutenir <?php echo ORGANIZATION_NAME; ?> - Dons</title>
|
||||||
<meta name="description" content="Soutenez <?php echo ORGANIZATION_NAME; ?> par un don. Chaque contribution compte pour maintenir le hub multimédia du podcast.">
|
<meta name="description" content="Soutenez <?php echo ORGANIZATION_NAME; ?> par un don. Chaque contribution compte pour maintenir le hub multimédia du podcast.">
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/dons.php'; ?>">
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
@@ -76,24 +78,31 @@ $stripeMonthlyLinks = defined('STRIPE_MONTHLY_LINKS') ? STRIPE_MONTHLY_LINKS : [
|
|||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="Soutenir <?php echo ORGANIZATION_NAME; ?>">
|
<meta property="og:title" content="Soutenir <?php echo ORGANIZATION_NAME; ?>">
|
||||||
<meta property="og:description" content="Soutenez <?php echo ORGANIZATION_NAME; ?> par un don et aidez-nous à maintenir le hub multimédia du podcast.">
|
<meta property="og:description" content="Soutenez <?php echo ORGANIZATION_NAME; ?> par un don et aidez-nous à maintenir le hub multimédia du podcast.">
|
||||||
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta property="og:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/dons.php'; ?>">
|
<meta property="og:url" content="<?php echo getBaseUrl() . '/dons.php'; ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
||||||
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
|
||||||
|
<!-- Twitter Card Meta Tags -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="Soutenir <?php echo ORGANIZATION_NAME; ?>">
|
||||||
|
<meta name="twitter:description" content="Soutenez <?php echo ORGANIZATION_NAME; ?> par un don et aidez-nous à maintenir le hub multimédia du podcast.">
|
||||||
|
<meta name="twitter:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
|
|
||||||
<!-- Schema.org pour les dons -->
|
<!-- Schema.org pour les dons -->
|
||||||
<script type="application/ld+json">
|
<?php
|
||||||
{
|
outputJsonLd(json_encode([
|
||||||
"@context": "https://schema.org",
|
"@context" => "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type" => "Organization",
|
||||||
"name": "<?php echo SITE_NAME; ?>",
|
"name" => SITE_NAME,
|
||||||
"description": "<?php echo SITE_DESCRIPTION; ?>",
|
"description" => SITE_DESCRIPTION,
|
||||||
"url": "<?php echo 'https://' . $_SERVER['HTTP_HOST']; ?>",
|
"url" => getBaseUrl(),
|
||||||
"potentialAction": {
|
"potentialAction" => [
|
||||||
"@type": "DonateAction"
|
"@type" => "DonateAction"
|
||||||
}
|
]
|
||||||
}
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
</script>
|
?>
|
||||||
|
|
||||||
<!-- Script pour éviter le flash en mode sombre -->
|
<!-- Script pour éviter le flash en mode sombre -->
|
||||||
<script>
|
<script>
|
||||||
@@ -291,5 +300,6 @@ $stripeMonthlyLinks = defined('STRIPE_MONTHLY_LINKS') ? STRIPE_MONTHLY_LINKS : [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 767 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 377 KiB |
@@ -7,7 +7,7 @@
|
|||||||
* Il est utilisé pour initialiser les variables non définies dans config.local.php.
|
* Il est utilisé pour initialiser les variables non définies dans config.local.php.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (!defined('APP_HOST_NAME')) define('APP_HOST_NAME', 'annukuteced.buzz');
|
if (!defined('APP_HOST_NAME')) define('APP_HOST_NAME', 'example.com');
|
||||||
|
|
||||||
if (!defined('ORGANIZATION_SHORT_NAME')) define('ORGANIZATION_SHORT_NAME', 'ANNU KUTE CED');
|
if (!defined('ORGANIZATION_SHORT_NAME')) define('ORGANIZATION_SHORT_NAME', 'ANNU KUTE CED');
|
||||||
if (!defined('ORGANIZATION_NAME')) define('ORGANIZATION_NAME', 'ANNU KUTE CED');
|
if (!defined('ORGANIZATION_NAME')) define('ORGANIZATION_NAME', 'ANNU KUTE CED');
|
||||||
@@ -38,11 +38,8 @@ if (!defined('SHOW_VIDEO_VIEWS')) define('SHOW_VIDEO_VIEWS', false); // Masquer
|
|||||||
// format: [ID catégorie => Nom personnalisé]
|
// format: [ID catégorie => Nom personnalisé]
|
||||||
if (!defined('PRIORITY_CATEGORIES')) {
|
if (!defined('PRIORITY_CATEGORIES')) {
|
||||||
define('PRIORITY_CATEGORIES', [
|
define('PRIORITY_CATEGORIES', [
|
||||||
11 => 'Actualités & Politique',
|
10 => 'Divertissement',
|
||||||
15 => 'Science et Technologie',
|
15 => 'Science et Technologie'
|
||||||
4 => 'Art',
|
|
||||||
9 => 'Humour',
|
|
||||||
10 => 'Divertissement'
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +97,9 @@ define('ENABLE_SEARCH', true);
|
|||||||
if (!defined('ENABLE_USER_ACCOUNTS')) define('ENABLE_USER_ACCOUNTS', false);
|
if (!defined('ENABLE_USER_ACCOUNTS')) define('ENABLE_USER_ACCOUNTS', false);
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', false);
|
// Activé par défaut : indispensable pour Castopod (rate limit strict sur les
|
||||||
|
// flux RSS) et Funkwhale. Sans cache, une requête part à chaque chargement.
|
||||||
|
if (!defined('CACHE_ENABLED')) define('CACHE_ENABLED', true);
|
||||||
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure)
|
if (!defined('CACHE_DURATION')) define('CACHE_DURATION', 3600); // En secondes (1 heure)
|
||||||
|
|
||||||
// =========================================
|
// =========================================
|
||||||
@@ -173,21 +172,14 @@ if (!defined('TAG_SHORT')) define('TAG_SHORT', 'short');
|
|||||||
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
|
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
|
||||||
if (!defined('IMPORTANT_TAGS')) {
|
if (!defined('IMPORTANT_TAGS')) {
|
||||||
define('IMPORTANT_TAGS', [
|
define('IMPORTANT_TAGS', [
|
||||||
'ANNUKUTECED',
|
'ANNUKUTECED'
|
||||||
'podcast',
|
|
||||||
'fediverse',
|
|
||||||
'audio',
|
|
||||||
'vidéo'
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hashtags populaires à afficher sur la page d'accueil
|
// Hashtags populaires à afficher sur la page d'accueil
|
||||||
if (!defined('POPULAR_TAGS')) {
|
if (!defined('POPULAR_TAGS')) {
|
||||||
define('POPULAR_TAGS', [
|
define('POPULAR_TAGS', [
|
||||||
'podcast',
|
'ANNUKUTECED'
|
||||||
'annukuteced',
|
|
||||||
'fediverse',
|
|
||||||
'audio'
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Note: config.local.php ne doit pas être versionné dans git
|
* Note: config.local.php ne doit pas être versionné dans git
|
||||||
*/
|
*/
|
||||||
|
|
||||||
define('APP_HOST_NAME', 'annukuteced.buzz');
|
define('APP_HOST_NAME', 'example.com');
|
||||||
|
|
||||||
// define('ORGANIZATION_SHORT_NAME', 'ANNU KUTE CED');
|
// define('ORGANIZATION_SHORT_NAME', 'ANNU KUTE CED');
|
||||||
// define('ORGANIZATION_NAME', 'ANNU KUTE CED');
|
// define('ORGANIZATION_NAME', 'ANNU KUTE CED');
|
||||||
@@ -79,19 +79,12 @@ define('APP_HOST_NAME', 'annukuteced.buzz');
|
|||||||
|
|
||||||
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
|
// Hashtags importants à afficher dans la sidebar, footer et menu mobile
|
||||||
define('IMPORTANT_TAGS', [
|
define('IMPORTANT_TAGS', [
|
||||||
'ANNUKUTECED',
|
'ANNUKUTECED'
|
||||||
'podcast',
|
|
||||||
'fediverse',
|
|
||||||
'audio',
|
|
||||||
'vidéo'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Hashtags populaires à afficher sur la page d'accueil
|
// Hashtags populaires à afficher sur la page d'accueil
|
||||||
define('POPULAR_TAGS', [
|
define('POPULAR_TAGS', [
|
||||||
'podcast',
|
'ANNUKUTECED'
|
||||||
'annukuteced',
|
|
||||||
'fediverse',
|
|
||||||
'audio'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Durée maximale des shorts en secondes
|
// Durée maximale des shorts en secondes
|
||||||
@@ -154,11 +147,8 @@ define('SHORTS_MAX_DURATION', 180); // 3 minutes
|
|||||||
// 17 : Kids
|
// 17 : Kids
|
||||||
// 18 : Food
|
// 18 : Food
|
||||||
define('PRIORITY_CATEGORIES', [
|
define('PRIORITY_CATEGORIES', [
|
||||||
11 => 'Actualités & Politique',
|
10 => 'Divertissement',
|
||||||
15 => 'Science et Technologie',
|
15 => 'Science et Technologie'
|
||||||
4 => 'Art',
|
|
||||||
9 => 'Humour',
|
|
||||||
10 => 'Divertissement'
|
|
||||||
// Ajoutez d'autres catégories selon vos besoins
|
// Ajoutez d'autres catégories selon vos besoins
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -866,8 +866,9 @@ function getCastopodEpisodes($castopodUrl = null, $podcastSlugs = null, $count =
|
|||||||
// Limiter au nombre demandé
|
// Limiter au nombre demandé
|
||||||
$allEpisodes = array_slice($allEpisodes, 0, $count);
|
$allEpisodes = array_slice($allEpisodes, 0, $count);
|
||||||
|
|
||||||
// Mettre en cache
|
// Mettre en cache (uniquement si non vide : une erreur temporaire
|
||||||
if (defined('CACHE_ENABLED') && CACHE_ENABLED) {
|
// — rate limit HTTP 429, timeout… — ne doit pas être figée en cache)
|
||||||
|
if (!empty($allEpisodes) && defined('CACHE_ENABLED') && CACHE_ENABLED) {
|
||||||
saveToCache($cacheKey, $allEpisodes);
|
saveToCache($cacheKey, $allEpisodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-copyright">
|
<div class="footer-copyright">
|
||||||
<?php echo LEGAL_COPYRIGHT; ?> <?php echo date('Y'); ?> - Licence libre <a href="<?php echo LEGAL_LICENSE_URL; ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a>
|
<?php echo LEGAL_COPYRIGHT; ?> <?php echo date('Y'); ?> - Licence libre <a href="<?php echo LEGAL_LICENSE_URL; ?>" target="_blank" rel="noopener noreferrer"><?php echo LEGAL_LICENSE; ?></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function generateWebSiteJsonLd() {
|
|||||||
],
|
],
|
||||||
"publisher" => [
|
"publisher" => [
|
||||||
"@type" => "Organization",
|
"@type" => "Organization",
|
||||||
"name" => "OKI",
|
"name" => ORGANIZATION_NAME,
|
||||||
"url" => $baseUrl,
|
"url" => $baseUrl,
|
||||||
"logo" => [
|
"logo" => [
|
||||||
"@type" => "ImageObject",
|
"@type" => "ImageObject",
|
||||||
@@ -69,6 +69,8 @@ function generateVideoObjectJsonLd($videoData, $video) {
|
|||||||
? truncateText(strip_tags($video['description']), 300)
|
? truncateText(strip_tags($video['description']), 300)
|
||||||
: "Regardez cette vidéo sur " . SITE_NAME,
|
: "Regardez cette vidéo sur " . SITE_NAME,
|
||||||
"url" => $videoUrl,
|
"url" => $videoUrl,
|
||||||
|
"embedUrl" => PEERTUBE_URL . "/videos/embed/" . $video['id'],
|
||||||
|
"inLanguage" => "fr-FR",
|
||||||
"thumbnailUrl" => $thumbnailUrl,
|
"thumbnailUrl" => $thumbnailUrl,
|
||||||
"uploadDate" => formatDateISO8601($video['date']),
|
"uploadDate" => formatDateISO8601($video['date']),
|
||||||
"duration" => $duration,
|
"duration" => $duration,
|
||||||
@@ -159,6 +161,147 @@ function generateVideoObjectJsonLd($videoData, $video) {
|
|||||||
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le JSON-LD pour un podcast (PodcastSeries + PodcastEpisode + AudioObject)
|
||||||
|
*
|
||||||
|
* @param array $episodes Épisodes formatés par getCastopodEpisodes()
|
||||||
|
* @return string JSON-LD pour le podcast, chaîne vide si aucun épisode
|
||||||
|
*/
|
||||||
|
function generatePodcastJsonLd($episodes) {
|
||||||
|
if (empty($episodes)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = getBaseUrl();
|
||||||
|
$first = $episodes[0];
|
||||||
|
|
||||||
|
// Informations de la série, déduites du flux RSS
|
||||||
|
$seriesName = !empty($first['podcastTitle']) ? $first['podcastTitle'] : SITE_NAME;
|
||||||
|
$seriesUrl = !empty($first['podcastLink']) ? $first['podcastLink'] : $baseUrl;
|
||||||
|
$seriesImage = !empty($first['image']) ? $first['image'] : $baseUrl . '/img/logo.png';
|
||||||
|
$webFeed = !empty($first['podcastSlug'])
|
||||||
|
? rtrim(CASTOPOD_URL, '/') . '/@' . $first['podcastSlug'] . '/feed'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$series = [
|
||||||
|
"@type" => "PodcastSeries",
|
||||||
|
"@id" => $seriesUrl . '#podcast',
|
||||||
|
"name" => $seriesName,
|
||||||
|
"url" => $seriesUrl,
|
||||||
|
"image" => $seriesImage,
|
||||||
|
"inLanguage" => "fr-FR",
|
||||||
|
"author" => [
|
||||||
|
"@type" => "Organization",
|
||||||
|
"name" => ORGANIZATION_NAME,
|
||||||
|
"url" => $baseUrl
|
||||||
|
],
|
||||||
|
"publisher" => [
|
||||||
|
"@type" => "Organization",
|
||||||
|
"name" => SITE_NAME,
|
||||||
|
"url" => $baseUrl,
|
||||||
|
"logo" => [
|
||||||
|
"@type" => "ImageObject",
|
||||||
|
"url" => $baseUrl . "/img/logo.png"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($webFeed) {
|
||||||
|
$series["webFeed"] = $webFeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodeItems = [];
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$item = [
|
||||||
|
"@type" => "PodcastEpisode",
|
||||||
|
"name" => $episode['title'],
|
||||||
|
"url" => $episode['link'],
|
||||||
|
"partOfSeries" => ["@id" => $seriesUrl . '#podcast']
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($episode['pubDate'])) {
|
||||||
|
$item["datePublished"] = formatDateISO8601($episode['pubDate']);
|
||||||
|
}
|
||||||
|
if (!empty($episode['image'])) {
|
||||||
|
$item["image"] = $episode['image'];
|
||||||
|
}
|
||||||
|
if (!empty($episode['description'])) {
|
||||||
|
$item["description"] = truncateText($episode['description'], 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
$seconds = parseDurationToSeconds($episode['duration'] ?? '');
|
||||||
|
if ($seconds > 0) {
|
||||||
|
$item["timeRequired"] = formatDurationISO8601($seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($episode['audioUrl'])) {
|
||||||
|
$audio = [
|
||||||
|
"@type" => "AudioObject",
|
||||||
|
"contentUrl" => $episode['audioUrl']
|
||||||
|
];
|
||||||
|
$format = guessAudioEncodingFormat($episode['audioUrl']);
|
||||||
|
if ($format !== null) {
|
||||||
|
$audio["encodingFormat"] = $format;
|
||||||
|
}
|
||||||
|
$item["associatedMedia"] = $audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
$episodeItems[] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
"@context" => "https://schema.org",
|
||||||
|
"@graph" => array_merge([$series], $episodeItems)
|
||||||
|
];
|
||||||
|
|
||||||
|
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une durée iTunes en secondes
|
||||||
|
* Formats acceptés : "3600", "90", "MM:SS", "HH:MM:SS"
|
||||||
|
*
|
||||||
|
* @param string $duration Durée brute issue du flux RSS
|
||||||
|
* @return int Durée en secondes (0 si non parsable)
|
||||||
|
*/
|
||||||
|
function parseDurationToSeconds($duration) {
|
||||||
|
$duration = trim((string) $duration);
|
||||||
|
if ($duration === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($duration, ':') !== false) {
|
||||||
|
$parts = array_map('intval', explode(':', $duration));
|
||||||
|
$seconds = 0;
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$seconds = $seconds * 60 + $part;
|
||||||
|
}
|
||||||
|
return $seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, (int) $duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déduit le type MIME d'un fichier audio depuis son extension
|
||||||
|
*
|
||||||
|
* @param string $url URL du fichier audio
|
||||||
|
* @return string|null Type MIME (audio/mpeg, audio/mp4, audio/ogg) ou null
|
||||||
|
*/
|
||||||
|
function guessAudioEncodingFormat($url) {
|
||||||
|
$extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));
|
||||||
|
$map = [
|
||||||
|
'mp3' => 'audio/mpeg',
|
||||||
|
'm4a' => 'audio/mp4',
|
||||||
|
'mp4' => 'audio/mp4',
|
||||||
|
'ogg' => 'audio/ogg',
|
||||||
|
'opus' => 'audio/ogg',
|
||||||
|
'wav' => 'audio/wav'
|
||||||
|
];
|
||||||
|
|
||||||
|
return $map[$extension] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Génère le JSON-LD pour les fils d'Ariane (BreadcrumbList)
|
* Génère le JSON-LD pour les fils d'Ariane (BreadcrumbList)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ if (defined('COUNTDOWN_ENABLED') && COUNTDOWN_ENABLED === true) {
|
|||||||
require_once 'includes/structured-data.php';
|
require_once 'includes/structured-data.php';
|
||||||
// Appliquer les en-têtes de sécurité
|
// Appliquer les en-têtes de sécurité
|
||||||
setSecurityHeaders();
|
setSecurityHeaders();
|
||||||
|
|
||||||
|
// Récupérer les épisodes Castopod (réutilisés pour le JSON-LD et l'affichage)
|
||||||
|
$castopodEpisodes = [];
|
||||||
|
if (defined('CASTOPOD_ENABLED') && CASTOPOD_ENABLED && defined('CASTOPOD_URL') && !empty(CASTOPOD_URL)) {
|
||||||
|
$castopodEpisodes = getCastopodEpisodes();
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
@@ -21,6 +27,8 @@ setSecurityHeaders();
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
<meta name="csrf-token" content="<?php echo generateCSRFToken(); ?>">
|
||||||
<title><?php echo SITE_NAME; ?></title>
|
<title><?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="<?php echo htmlspecialchars(SITE_DESCRIPTION); ?>">
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/'; ?>">
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
<link rel="stylesheet" href="css/mastodon-timeline.min.css?v=<?php echo filemtime('css/mastodon-timeline.min.css'); ?>">
|
<link rel="stylesheet" href="css/mastodon-timeline.min.css?v=<?php echo filemtime('css/mastodon-timeline.min.css'); ?>">
|
||||||
@@ -54,9 +62,9 @@ setSecurityHeaders();
|
|||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="<?php echo SITE_NAME; ?>">
|
<meta property="og:title" content="<?php echo SITE_NAME; ?>">
|
||||||
<meta property="og:description" content="Découvrez notre plateforme multimédia avec des vidéos, des shorts et des directs. Tendances, catégories et contenus exclusifs vous attendent.">
|
<meta property="og:description" content="<?php echo htmlspecialchars(SITE_DESCRIPTION); ?>">
|
||||||
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta property="og:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>">
|
<meta property="og:url" content="<?php echo getBaseUrl() . '/'; ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
||||||
<meta property="og:locale" content="fr_FR">
|
<meta property="og:locale" content="fr_FR">
|
||||||
@@ -64,8 +72,8 @@ setSecurityHeaders();
|
|||||||
<!-- Twitter Card Meta Tags -->
|
<!-- Twitter Card Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="<?php echo SITE_NAME; ?>">
|
<meta name="twitter:title" content="<?php echo SITE_NAME; ?>">
|
||||||
<meta name="twitter:description" content="Découvrez notre plateforme multimédia avec des vidéos, des shorts et des directs. Tendances, catégories et contenus exclusifs vous attendent.">
|
<meta name="twitter:description" content="<?php echo htmlspecialchars(SITE_DESCRIPTION); ?>">
|
||||||
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta name="twitter:image" content="<?php echo getBaseUrl() . '/img/logo.png'; ?>">
|
||||||
|
|
||||||
<!-- Données structurées JSON-LD pour le site web -->
|
<!-- Données structurées JSON-LD pour le site web -->
|
||||||
<?php
|
<?php
|
||||||
@@ -78,6 +86,11 @@ setSecurityHeaders();
|
|||||||
];
|
];
|
||||||
$breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs);
|
$breadcrumbJsonLd = generateBreadcrumbJsonLd($breadcrumbs);
|
||||||
outputJsonLd($breadcrumbJsonLd);
|
outputJsonLd($breadcrumbJsonLd);
|
||||||
|
|
||||||
|
// Données structurées du podcast (PodcastSeries + PodcastEpisode)
|
||||||
|
if (!empty($castopodEpisodes)) {
|
||||||
|
outputJsonLd(generatePodcastJsonLd($castopodEpisodes));
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Script pour éviter le flash en mode sombre -->
|
<!-- Script pour éviter le flash en mode sombre -->
|
||||||
@@ -105,6 +118,7 @@ setSecurityHeaders();
|
|||||||
<?php include 'includes/sidebar.php'; ?>
|
<?php include 'includes/sidebar.php'; ?>
|
||||||
<!-- Contenu principal -->
|
<!-- Contenu principal -->
|
||||||
<main class="main-content" id="main-content" role="main">
|
<main class="main-content" id="main-content" role="main">
|
||||||
|
<h1 class="sr-only"><?php echo SITE_NAME . ' — ' . SITE_DESCRIPTION; ?></h1>
|
||||||
<?php include 'includes/header.php'; ?>
|
<?php include 'includes/header.php'; ?>
|
||||||
<!-- Hero and Mastodon container -->
|
<!-- Hero and Mastodon container -->
|
||||||
<div class="hero-mastodon-wrapper">
|
<div class="hero-mastodon-wrapper">
|
||||||
@@ -130,8 +144,7 @@ setSecurityHeaders();
|
|||||||
</div>
|
</div>
|
||||||
<div class="castopod-episodes-list">
|
<div class="castopod-episodes-list">
|
||||||
<?php
|
<?php
|
||||||
$castopodEpisodes = getCastopodEpisodes();
|
// $castopodEpisodes a été récupéré en début de page (JSON-LD + affichage)
|
||||||
|
|
||||||
if (empty($castopodEpisodes)) {
|
if (empty($castopodEpisodes)) {
|
||||||
echo '<div class="castopod-no-episodes">Aucun épisode disponible</div>';
|
echo '<div class="castopod-no-episodes">Aucun épisode disponible</div>';
|
||||||
} else {
|
} else {
|
||||||
@@ -390,7 +403,7 @@ setSecurityHeaders();
|
|||||||
<section class="video-section" aria-labelledby="shorts-heading">
|
<section class="video-section" aria-labelledby="shorts-heading">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="Logo <?php echo SITE_NAME; ?>" aria-hidden="true">
|
<img src="img/logo.png" alt="" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
<h2 id="shorts-heading" class="section-title">Shorts</h2>
|
<h2 id="shorts-heading" class="section-title">Shorts</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,7 +459,7 @@ setSecurityHeaders();
|
|||||||
<section class="video-section" aria-labelledby="recent-videos-heading">
|
<section class="video-section" aria-labelledby="recent-videos-heading">
|
||||||
<header class="section-header">
|
<header class="section-header">
|
||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="Logo <?php echo SITE_NAME; ?>" aria-hidden="true">
|
<img src="img/logo.png" alt="" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
<h2 id="recent-videos-heading" class="section-title">Dernières vidéos</h2>
|
<h2 id="recent-videos-heading" class="section-title">Dernières vidéos</h2>
|
||||||
</header>
|
</header>
|
||||||
@@ -508,7 +521,7 @@ setSecurityHeaders();
|
|||||||
<section class="video-section" aria-labelledby="trending-videos-heading">
|
<section class="video-section" aria-labelledby="trending-videos-heading">
|
||||||
<header class="section-header">
|
<header class="section-header">
|
||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="Logo <?php echo SITE_NAME; ?>" aria-hidden="true">
|
<img src="img/logo.png" alt="" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
<h2 id="trending-videos-heading" class="section-title">Tendances</h2>
|
<h2 id="trending-videos-heading" class="section-title">Tendances</h2>
|
||||||
</header>
|
</header>
|
||||||
@@ -526,7 +539,7 @@ setSecurityHeaders();
|
|||||||
?>
|
?>
|
||||||
<article class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
<article class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
||||||
<div class="video-thumbnail">
|
<div class="video-thumbnail">
|
||||||
<img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>">
|
<img src="<?php echo htmlspecialchars($video['thumbnail']); ?>" alt="<?php echo htmlspecialchars($video['title']); ?>">
|
||||||
<div class="video-play-icon">
|
<div class="video-play-icon">
|
||||||
<i class="fas fa-play-circle"></i>
|
<i class="fas fa-play-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,7 +590,7 @@ setSecurityHeaders();
|
|||||||
<section class="video-section" data-category-id="<?php echo $category['id']; ?>" aria-labelledby="category-heading-<?php echo $category['id']; ?>">
|
<section class="video-section" data-category-id="<?php echo $category['id']; ?>" aria-labelledby="category-heading-<?php echo $category['id']; ?>">
|
||||||
<header class="section-header">
|
<header class="section-header">
|
||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="Logo <?php echo SITE_NAME; ?>" aria-hidden="true">
|
<img src="img/logo.png" alt="" aria-hidden="true">
|
||||||
</div>
|
</div>
|
||||||
<h2 id="category-heading-<?php echo $category['id']; ?>" class="section-title"><?php echo htmlspecialchars($category['name']); ?></h2>
|
<h2 id="category-heading-<?php echo $category['id']; ?>" class="section-title"><?php echo htmlspecialchars($category['name']); ?></h2>
|
||||||
</header>
|
</header>
|
||||||
@@ -586,7 +599,7 @@ setSecurityHeaders();
|
|||||||
<?php foreach ($category['videos'] as $video): ?>
|
<?php foreach ($category['videos'] as $video): ?>
|
||||||
<article class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
<article class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
||||||
<div class="video-thumbnail">
|
<div class="video-thumbnail">
|
||||||
<img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>">
|
<img src="<?php echo htmlspecialchars($video['thumbnail']); ?>" alt="<?php echo htmlspecialchars($video['title']); ?>">
|
||||||
<div class="video-play-icon">
|
<div class="video-play-icon">
|
||||||
<i class="fas fa-play-circle"></i>
|
<i class="fas fa-play-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -678,34 +691,11 @@ setSecurityHeaders();
|
|||||||
<script src="js/mastodon-timeline.umd.js"></script>
|
<script src="js/mastodon-timeline.umd.js"></script>
|
||||||
<script src="js/mastodon-config.php?v=<?php echo md5(MASTODON_INSTANCE_URL . MASTODON_DATE_FORMAT . MASTODON_BTN_SEE_MORE . MASTODON_BTN_RELOAD . MASTODON_MAX_POST_FETCH . MASTODON_MAX_POST_SHOW); ?>"></script>
|
<script src="js/mastodon-config.php?v=<?php echo md5(MASTODON_INSTANCE_URL . MASTODON_DATE_FORMAT . MASTODON_BTN_SEE_MORE . MASTODON_BTN_RELOAD . MASTODON_MAX_POST_FETCH . MASTODON_MAX_POST_SHOW); ?>"></script>
|
||||||
|
|
||||||
<!-- PWA Service Worker -->
|
<!-- PWA : enregistrement du Service Worker + modal de mise à jour -->
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
|
|
||||||
|
<!-- PWA : bouton d'installation -->
|
||||||
<script nonce="<?php echo getCspNonce(); ?>">
|
<script nonce="<?php echo getCspNonce(); ?>">
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(function(registration) {
|
|
||||||
console.log('Service Worker enregistré avec succès:', registration.scope);
|
|
||||||
|
|
||||||
// Écouter les mises à jour
|
|
||||||
registration.addEventListener('updatefound', function() {
|
|
||||||
const newWorker = registration.installing;
|
|
||||||
newWorker.addEventListener('statechange', function() {
|
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
||||||
// Nouvelle version disponible
|
|
||||||
console.log('Nouvelle version disponible');
|
|
||||||
if (confirm('Une nouvelle version est disponible. Voulez-vous recharger la page ?')) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.log('Échec de l\'enregistrement du Service Worker:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gestion de l'installation PWA
|
// Gestion de l'installation PWA
|
||||||
let deferredPrompt;
|
let deferredPrompt;
|
||||||
const installButton = document.getElementById('install-pwa');
|
const installButton = document.getElementById('install-pwa');
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Enregistrement du Service Worker et gestion des mises à jour.
|
||||||
|
*
|
||||||
|
* Quand une nouvelle version du site est déployée (sw.js modifié, suffixe de
|
||||||
|
* version des caches bumpé), le nouveau Service Worker s'installe en arrière-
|
||||||
|
* plan et reste en attente. Un modal propose alors la mise à jour :
|
||||||
|
* - « Mettre à jour » : le SW en attente prend le contrôle (SKIP_WAITING),
|
||||||
|
* les anciens caches sont purgés à l'activation, puis la page se recharge
|
||||||
|
* sur la nouvelle version (controllerchange).
|
||||||
|
* - « Plus tard » : le modal se ferme ; la mise à jour sera reproposée au
|
||||||
|
* prochain chargement de page.
|
||||||
|
*
|
||||||
|
* Aucune purge manuelle du cache navigateur n'est nécessaire.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modalShown = false;
|
||||||
|
let refreshing = false;
|
||||||
|
|
||||||
|
// Recharge la page quand le nouveau Service Worker prend le contrôle
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
||||||
|
if (refreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshing = true;
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit et affiche le modal de mise à jour (une seule fois).
|
||||||
|
* @param {ServiceWorker} waitingWorker Le SW en attente d'activation
|
||||||
|
*/
|
||||||
|
function showUpdateModal(waitingWorker) {
|
||||||
|
if (modalShown || !waitingWorker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalShown = true;
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'pwa-update-overlay';
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.setAttribute('aria-labelledby', 'pwa-update-title');
|
||||||
|
overlay.innerHTML =
|
||||||
|
'<div class="pwa-update-modal">' +
|
||||||
|
'<img class="pwa-update-logo" src="img/logo.png" alt="" aria-hidden="true">' +
|
||||||
|
'<h2 class="pwa-update-title" id="pwa-update-title">Nouvelle version disponible</h2>' +
|
||||||
|
'<p class="pwa-update-text">' +
|
||||||
|
'Une mise à jour du site est prête à être installée. ' +
|
||||||
|
'Elle est rapide et ne supprime aucune de vos données.' +
|
||||||
|
'</p>' +
|
||||||
|
'<div class="pwa-update-actions">' +
|
||||||
|
'<button type="button" class="pwa-update-btn pwa-update-btn-primary">' +
|
||||||
|
'Mettre à jour' +
|
||||||
|
'</button>' +
|
||||||
|
'<button type="button" class="pwa-update-btn pwa-update-btn-secondary">' +
|
||||||
|
'Plus tard' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Force le reflow pour que la transition CSS d'entrée joue
|
||||||
|
void overlay.offsetWidth;
|
||||||
|
overlay.classList.add('pwa-update-visible');
|
||||||
|
|
||||||
|
const primaryBtn = overlay.querySelector('.pwa-update-btn-primary');
|
||||||
|
const secondaryBtn = overlay.querySelector('.pwa-update-btn-secondary');
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
overlay.classList.remove('pwa-update-visible');
|
||||||
|
overlay.addEventListener('transitionend', function () {
|
||||||
|
overlay.remove();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryBtn.addEventListener('click', function () {
|
||||||
|
primaryBtn.disabled = true;
|
||||||
|
secondaryBtn.disabled = true;
|
||||||
|
primaryBtn.textContent = 'Mise à jour…';
|
||||||
|
// Le rechargement est déclenché par l'event controllerchange
|
||||||
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
});
|
||||||
|
|
||||||
|
secondaryBtn.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function onEscape(event) {
|
||||||
|
if (event.key === 'Escape' && document.body.contains(overlay)) {
|
||||||
|
closeModal();
|
||||||
|
document.removeEventListener('keydown', onEscape);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accessibilité : focus sur l'action principale
|
||||||
|
primaryBtn.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Surveille l'installation d'un nouveau Service Worker.
|
||||||
|
* @param {ServiceWorkerRegistration} registration
|
||||||
|
*/
|
||||||
|
function trackInstalling(registration) {
|
||||||
|
registration.addEventListener('updatefound', function () {
|
||||||
|
const newWorker = registration.installing;
|
||||||
|
if (!newWorker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newWorker.addEventListener('statechange', function () {
|
||||||
|
// installed + un contrôleur actif = mise à jour en attente
|
||||||
|
// (sans contrôleur, c'est la toute première installation)
|
||||||
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
|
showUpdateModal(registration.waiting);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(function (registration) {
|
||||||
|
// Cas où une mise à jour est déjà en attente (installée lors
|
||||||
|
// d'une visite précédente, jamais activée)
|
||||||
|
if (registration.waiting && navigator.serviceWorker.controller) {
|
||||||
|
showUpdateModal(registration.waiting);
|
||||||
|
}
|
||||||
|
trackInstalling(registration);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.log('Échec de l\'enregistrement du Service Worker:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
// Inclure la configuration
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
// Appliquer les en-têtes de sécurité
|
||||||
|
setSecurityHeaders();
|
||||||
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Mentions Légales - ANNU KUTE CED</title>
|
<title>Mentions Légales - <?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="Consultez les mentions légales de <?php echo SITE_NAME; ?>. Informations légales, conditions d'utilisation et politique de confidentialité.">
|
||||||
|
<link rel="canonical" href="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/mentions-legales.php'; ?>">
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
|
|
||||||
@@ -13,25 +21,25 @@
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#FF0000">
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="Mentions Légales - ANNU KUTE CED">
|
<meta property="og:title" content="Mentions Légales - <?php echo SITE_NAME; ?>">
|
||||||
<meta property="og:description" content="Consultez les mentions légales d'ANNU KUTE CED. Informations légales, conditions d'utilisation et politique de confidentialité du hub multimédia du podcast.">
|
<meta property="og:description" content="Consultez les mentions légales de <?php echo SITE_NAME; ?>. Informations légales, conditions d'utilisation et politique de confidentialité.">
|
||||||
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
||||||
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>">
|
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:site_name" content="ANNU KUTE CED">
|
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
||||||
<meta property="og:locale" content="fr_FR">
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
|
||||||
<!-- Twitter Card Meta Tags -->
|
<!-- Twitter Card Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="Mentions Légales - ANNU KUTE CED">
|
<meta name="twitter:title" content="Mentions Légales - <?php echo SITE_NAME; ?>">
|
||||||
<meta name="twitter:description" content="Consultez les mentions légales d'ANNU KUTE CED. Informations légales, conditions d'utilisation et politique de confidentialité du hub multimédia du podcast.">
|
<meta name="twitter:description" content="Consultez les mentions légales de <?php echo SITE_NAME; ?>. Informations légales, conditions d'utilisation et politique de confidentialité.">
|
||||||
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
||||||
|
|
||||||
<!-- Script pour éviter le flash en mode sombre -->
|
<!-- Script pour éviter le flash en mode sombre -->
|
||||||
<script>
|
<script nonce="<?php echo getCspNonce(); ?>">
|
||||||
(function() {
|
(function() {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
@@ -44,12 +52,6 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<?php
|
|
||||||
// Inclure la configuration
|
|
||||||
require_once 'includes/config.php';
|
|
||||||
// Appliquer les en-têtes de sécurité
|
|
||||||
setSecurityHeaders();
|
|
||||||
?>
|
|
||||||
<?php include 'includes/sidebar.php'; ?>
|
<?php include 'includes/sidebar.php'; ?>
|
||||||
<!-- Contenu principal -->
|
<!-- Contenu principal -->
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
@@ -58,9 +60,9 @@
|
|||||||
<!-- Section Mentions Légales -->
|
<!-- Section Mentions Légales -->
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-logo">
|
<div class="section-logo">
|
||||||
<img src="img/logo.png" alt="ANNU KUTE CED">
|
<img src="img/logo.png" alt="<?php echo SITE_NAME; ?>">
|
||||||
</div>
|
</div>
|
||||||
<h2 class="section-title">Mentions Légales</h2>
|
<h1 class="section-title">Mentions Légales</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
@@ -243,5 +245,6 @@
|
|||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
<?php include 'includes/mobile-menu.php'; ?>
|
<?php include 'includes/mobile-menu.php'; ?>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ if ($resultsCount > 0) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?></title>
|
<title><?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="<?php echo !empty($query) ? 'Résultats de recherche pour « ' . htmlspecialchars($query) . ' » sur ' . SITE_NAME . '. Découvrez des vidéos correspondantes à votre recherche.' : 'Recherchez des vidéos sur ' . SITE_NAME . '. ' . SITE_DESCRIPTION; ?>">
|
||||||
|
<?php if (!empty($query)): ?>
|
||||||
|
<meta name="robots" content="noindex, follow">
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/recherche.php?q=' . urlencode($query); ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/recherche.php'; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
|
|
||||||
@@ -62,11 +69,11 @@ if ($resultsCount > 0) {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#FF0000">
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:title" content="<?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?>">
|
<meta property="og:title" content="<?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?>">
|
||||||
<meta property="og:description" content="<?php echo !empty($query) ? 'Résultats de recherche pour \"' . htmlspecialchars($query) . '\" sur ' . SITE_NAME . '. Découvrez des vidéos correspondantes à votre recherche.' : 'Recherchez des vidéos sur ' . SITE_NAME . '. Plateforme multimédia avec un contenu de qualité et exclusif.'; ?>">
|
<meta property="og:description" content="<?php echo !empty($query) ? 'Résultats de recherche pour « ' . htmlspecialchars($query) . ' » sur ' . SITE_NAME . '. Découvrez des vidéos correspondantes à votre recherche.' : 'Recherchez des vidéos sur ' . SITE_NAME . '. ' . SITE_DESCRIPTION; ?>">
|
||||||
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta property="og:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
||||||
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>">
|
<meta property="og:url" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
@@ -76,7 +83,7 @@ if ($resultsCount > 0) {
|
|||||||
<!-- Twitter Card Meta Tags -->
|
<!-- Twitter Card Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:title" content="<?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?>">
|
<meta name="twitter:title" content="<?php echo !empty($query) ? 'Recherche: ' . htmlspecialchars($query) . ' - ' : 'Recherche - '; ?><?php echo SITE_NAME; ?>">
|
||||||
<meta name="twitter:description" content="<?php echo !empty($query) ? 'Résultats de recherche pour \"' . htmlspecialchars($query) . '\" sur ' . SITE_NAME . '. Découvrez des vidéos correspondantes à votre recherche.' : 'Recherchez des vidéos sur ' . SITE_NAME . '. Plateforme multimédia avec un contenu de qualité et exclusif.'; ?>">
|
<meta name="twitter:description" content="<?php echo !empty($query) ? 'Résultats de recherche pour « ' . htmlspecialchars($query) . ' » sur ' . SITE_NAME . '. Découvrez des vidéos correspondantes à votre recherche.' : 'Recherchez des vidéos sur ' . SITE_NAME . '. ' . SITE_DESCRIPTION; ?>">
|
||||||
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
<meta name="twitter:image" content="<?php echo 'https://' . $_SERVER['HTTP_HOST'] . '/img/logo.png'; ?>">
|
||||||
|
|
||||||
<?php if (!empty($query) && !empty($currentPageVideos)): ?>
|
<?php if (!empty($query) && !empty($currentPageVideos)): ?>
|
||||||
@@ -132,12 +139,12 @@ if ($resultsCount > 0) {
|
|||||||
</div>
|
</div>
|
||||||
<?php if (!empty($query)): ?>
|
<?php if (!empty($query)): ?>
|
||||||
<?php if ($isTagSearch): ?>
|
<?php if ($isTagSearch): ?>
|
||||||
<h2 class="section-title">Vidéos avec le hashtag : "<?php echo htmlspecialchars($searchTag); ?>"</h2>
|
<h1 class="section-title">Vidéos avec le hashtag : "<?php echo htmlspecialchars($searchTag); ?>"</h1>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h2 class="section-title">Résultats pour : "<?php echo htmlspecialchars($query); ?>"</h2>
|
<h1 class="section-title">Résultats pour : "<?php echo htmlspecialchars($query); ?>"</h1>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h2 class="section-title">Rechercher des vidéos</h2>
|
<h1 class="section-title">Rechercher des vidéos</h1>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -261,5 +268,6 @@ if ($resultsCount > 0) {
|
|||||||
|
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/search.js"></script>
|
<script src="js/search.js"></script>
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ Disallow: /conf/
|
|||||||
Disallow: /cache/
|
Disallow: /cache/
|
||||||
|
|
||||||
# Sitemap
|
# Sitemap
|
||||||
Sitemap: https://annukuteced.buzz/sitemap.xml
|
Sitemap: https://example.com/sitemap.xml
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lance en local les mêmes vérifications que le CI (.gitea/workflows/).
|
||||||
|
# À exécuter avant de pousser : scripts/check.sh
|
||||||
|
#
|
||||||
|
# Un outil manquant n'est pas bloquant : le check correspondant est ignoré
|
||||||
|
# avec un avertissement (le CI, lui, exécute tout). Le script retourne un
|
||||||
|
# code non nul si une vérification échoue.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
warn=0
|
||||||
|
|
||||||
|
step() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
|
||||||
|
|
||||||
|
# need <commande> <paquet> : vérifie la présence d'un outil
|
||||||
|
need() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "⚠️ '$1' non installé — check ignoré (paquet : $2)"
|
||||||
|
warn=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step "PHP lint (*.php, *.php.sample)"
|
||||||
|
if need php php-cli; then
|
||||||
|
php_fail=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
if ! php -l "$f" > /dev/null; then
|
||||||
|
echo "❌ $f"
|
||||||
|
php_fail=1
|
||||||
|
fi
|
||||||
|
done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
|
||||||
|
if [ "$php_fail" -eq 0 ]; then
|
||||||
|
echo "✅ PHP OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "JS lint (sw.js, js/*.js)"
|
||||||
|
if need node nodejs; then
|
||||||
|
js_fail=0
|
||||||
|
for f in sw.js js/*.js; do
|
||||||
|
node --check "$f" || js_fail=1
|
||||||
|
done
|
||||||
|
if [ "$js_fail" -eq 0 ]; then
|
||||||
|
echo "✅ JS OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "AsciiDoc (README.adoc, DEPLOY.adoc)"
|
||||||
|
if need asciidoctor asciidoctor; then
|
||||||
|
adoc_fail=0
|
||||||
|
for f in README.adoc DEPLOY.adoc; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
asciidoctor -o "/tmp/check-$$.html" "$f" || adoc_fail=1
|
||||||
|
done
|
||||||
|
rm -f "/tmp/check-$$.html"
|
||||||
|
if [ "$adoc_fail" -eq 0 ]; then
|
||||||
|
echo "✅ AsciiDoc OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "JSON (site.webmanifest.sample)"
|
||||||
|
if need python3 python3; then
|
||||||
|
if python3 -m json.tool site.webmanifest.sample > /dev/null; then
|
||||||
|
echo "✅ JSON OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "XML (sitemap.xml.sample, browserconfig.xml)"
|
||||||
|
if need xmllint libxml2-utils; then
|
||||||
|
if xmllint --noout sitemap.xml.sample browserconfig.xml; then
|
||||||
|
echo "✅ XML OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Shellcheck (scripts shell)"
|
||||||
|
if need shellcheck shellcheck; then
|
||||||
|
if shellcheck docs/generate-readme-pdf.sh scripts/check.sh; then
|
||||||
|
echo "✅ Shellcheck OK"
|
||||||
|
else
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo "❌ Des vérifications ont échoué — corrigez avant de pousser."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$warn" -ne 0 ]; then
|
||||||
|
echo "⚠️ Checks disponibles OK, mais certains outils manquent (le CI exécutera tout)."
|
||||||
|
else
|
||||||
|
echo "✅ Tous les checks passent."
|
||||||
|
fi
|
||||||
@@ -1,67 +1,67 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/</loc>
|
<loc>https://example.com/</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/index</loc>
|
<loc>https://example.com/index</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/index.php</loc>
|
<loc>https://example.com/index.php</loc>
|
||||||
<changefreq>daily</changefreq>
|
<changefreq>daily</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/direct</loc>
|
<loc>https://example.com/direct</loc>
|
||||||
<changefreq>hourly</changefreq>
|
<changefreq>hourly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/direct.php</loc>
|
<loc>https://example.com/direct.php</loc>
|
||||||
<changefreq>hourly</changefreq>
|
<changefreq>hourly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/mentions-legales</loc>
|
<loc>https://example.com/mentions-legales</loc>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.3</priority>
|
<priority>0.3</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/mentions-legales.php</loc>
|
<loc>https://example.com/mentions-legales.php</loc>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.3</priority>
|
<priority>0.3</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/recherche</loc>
|
<loc>https://example.com/recherche</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/recherche.php</loc>
|
<loc>https://example.com/recherche.php</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/categories</loc>
|
<loc>https://example.com/categories</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/categories.php</loc>
|
<loc>https://example.com/categories.php</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.8</priority>
|
<priority>0.8</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/video</loc>
|
<loc>https://example.com/video</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://annukuteced.buzz/video.php</loc>
|
<loc>https://example.com/video.php</loc>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const CACHE_NAME = 'fediverse-oki-08072026-0720';
|
// Version du cache : à bumper à chaque déploiement (format JJMMAAAA-HHMM).
|
||||||
const STATIC_CACHE_NAME = 'fediverse-oki-static-08072026-0720';
|
// Tout changement de ce fichier déclenche l'installation d'un nouveau
|
||||||
const DYNAMIC_CACHE_NAME = 'fediverse-oki-dynamic-08072026-0720';
|
// Service Worker chez les visiteurs ; les anciens caches sont purgés à
|
||||||
|
// l'activation (voir l'event 'activate' plus bas).
|
||||||
|
const STATIC_CACHE_NAME = 'annu-kute-ced-static-24072026-0612';
|
||||||
|
const DYNAMIC_CACHE_NAME = 'annu-kute-ced-dynamic-24072026-0612';
|
||||||
|
|
||||||
// Ressources à mettre en cache immédiatement
|
// Ressources à mettre en cache immédiatement
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
@@ -46,13 +49,15 @@ self.addEventListener('install', event => {
|
|||||||
console.log('Service Worker: Mise en cache des assets statiques');
|
console.log('Service Worker: Mise en cache des assets statiques');
|
||||||
return cache.addAll(STATIC_ASSETS);
|
return cache.addAll(STATIC_ASSETS);
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
return self.skipWaiting();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Service Worker: Erreur lors de la mise en cache:', err);
|
console.error('Service Worker: Erreur lors de la mise en cache:', err);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
// Note : pas de skipWaiting() ici. Lors d'une mise à jour, le nouveau SW
|
||||||
|
// reste en attente jusqu'à ce que l'utilisateur accepte la mise à jour
|
||||||
|
// via le modal (message SKIP_WAITING envoyé par js/pwa-update.js).
|
||||||
|
// Lors de la toute première visite (aucun SW actif), l'activation est
|
||||||
|
// immédiate.
|
||||||
});
|
});
|
||||||
|
|
||||||
// Activation du Service Worker
|
// Activation du Service Worker
|
||||||
@@ -202,20 +207,10 @@ function isApiRequest(url) {
|
|||||||
url.includes('mastodon-config.php');
|
url.includes('mastodon-config.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des messages du client
|
// Gestion des messages du client : l'utilisateur a accepté la mise à jour,
|
||||||
|
// le SW en attente prend le contrôle (purge des anciens caches à l'activation).
|
||||||
self.addEventListener('message', event => {
|
self.addEventListener('message', event => {
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notification de mise à jour
|
|
||||||
self.addEventListener('message', event => {
|
|
||||||
if (event.data && event.data.type === 'CHECK_UPDATE') {
|
|
||||||
// Vérifier s'il y a une mise à jour
|
|
||||||
event.ports[0].postMessage({
|
|
||||||
type: 'UPDATE_AVAILABLE',
|
|
||||||
version: CACHE_NAME
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ if (empty($videoData) || isset($videoData['error'])) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php echo !empty($video['title']) ? htmlspecialchars($video['title']) . ' - ' : ''; ?><?php echo SITE_NAME; ?></title>
|
<title><?php echo !empty($video['title']) ? htmlspecialchars($video['title']) . ' - ' : ''; ?><?php echo SITE_NAME; ?></title>
|
||||||
|
<meta name="description" content="<?php echo !empty($video['description']) ? htmlspecialchars(substr(strip_tags($video['description']), 0, 200)) . '...' : 'Regardez cette vidéo sur ' . SITE_NAME; ?>">
|
||||||
|
<?php if (!isset($videoNotFound) && !empty($video)): ?>
|
||||||
|
<link rel="canonical" href="<?php echo getBaseUrl() . '/video.php?id=' . $video['id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime('css/styles.css'); ?>">
|
||||||
<link rel="stylesheet" href="css/video-page.css?v=<?php echo filemtime('css/video-page.css'); ?>">
|
<link rel="stylesheet" href="css/video-page.css?v=<?php echo filemtime('css/video-page.css'); ?>">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">
|
||||||
@@ -130,7 +134,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
|
||||||
<link rel="manifest" href="site.webmanifest">
|
<link rel="manifest" href="site.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#FF0000">
|
||||||
|
|
||||||
<!-- Meta tags pour le partage sur les réseaux sociaux -->
|
<!-- Meta tags pour le partage sur les réseaux sociaux -->
|
||||||
<meta property="og:title" content="<?php echo !empty($video['title']) ? htmlspecialchars($video['title']) : 'Vidéo'; ?> - <?php echo SITE_NAME; ?>">
|
<meta property="og:title" content="<?php echo !empty($video['title']) ? htmlspecialchars($video['title']) : 'Vidéo'; ?> - <?php echo SITE_NAME; ?>">
|
||||||
@@ -141,6 +145,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
|||||||
<meta property="og:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; ?>">
|
<meta property="og:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; ?>">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
||||||
|
<meta property="og:locale" content="fr_FR">
|
||||||
|
|
||||||
<!-- Meta tags pour Twitter -->
|
<!-- Meta tags pour Twitter -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
@@ -668,5 +673,6 @@ if (empty($videoData) || isset($videoData['error'])) {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||