Pages — sites statiques
Un site recopié sur tous les nodes et servi directement par nginx. Aucun processus, aucune RAM réservée.
Pages héberge un site déjà construit : tu envoies un dossier de fichiers, il est recopié sur chacun de nos nodes, et nginx le sert depuis le disque. Il n'y a aucun programme à faire tourner, donc rien à redémarrer, rien à surveiller, et pas de quota mémoire.
Pages ou conteneur ?#
C'est la première décision, et elle se tranche en une question : ton site a-t-il besoin d'exécuter du code à chaque visite ?
| Pages | Conteneur | |
|---|---|---|
| Pour | Site vitrine, documentation, application front (React, Vue…), portfolio | API, rendu serveur, base de données, bot |
| Ce qui tourne | Rien — nginx sert des fichiers | Ton programme, en permanence |
| Redondance | Une copie sur chaque node, d'office | Une instance |
| Démarrage à froid | Aucun | Le temps de démarrer ton programme |
| Sauvegardes | Inutiles — les mises en ligne font l'historique | À planifier |
| Coût mémoire | Nul | Ton enveloppe RAM |
À noter
Mettre un site en ligne#
Fais pointer ton domaine
Un enregistrement A vers l'adresse d'un de nos nodes, affichée à la création du site. Si tu as aussi un AAAA, il doit pointer vers un node lui aussi — sinon le certificat échouera sans que rien ne l'explique (voir plus bas).
Crée le site
Tableau de bord → Pages → Nouveau site. Choisis un gabarit : il règle le repli SPA correctement et met en ligne une page d'accueil tout de suite, pour que le domaine affiche quelque chose au lieu d'une erreur.
Envoie ton contenu
Trois façons, détaillées ci-dessous : une archive ZIP, un dépôt Git, ou la commande npx ecloudserv-pages.
Attends le certificat
Il est demandé automatiquement à Let's Encrypt dès que le domaine résout. Compte quelques minutes.
Les trois façons de publier#
1. Une archive ZIP
Le plus direct. Compresse le contenu du dossier de sortie, pas le dossier lui-même : il doit y avoir un index.html à la racine de l'archive. C'est l'erreur la plus fréquente, et elle est refusée avec un message explicite plutôt que de produire un site vide.
cd dist
zip -r ../site.zip .Limite : 30 Mo d'archive, 60 Mo décompressés, 5 000 fichiers.
2. Un dépôt Git
Onglet Source du site : colle l'adresse d'un dépôt public (GitHub, GitLab ou Codeberg), la branche, et éventuellement le sous-dossier qui contient le site compilé (dist, public…).
Le tableau de bord te donne alors une URL de crochet à coller dans les réglages Webhooks de ta forge : chaque git push redéploie le site.
Rien n'est compilé chez nous
npm run build d'un dépôt quelconque sur notre infrastructure reviendrait à y exécuter du code arbitraire fourni par un client. Donc : soit ton dépôt contient déjà le site construit, soit tu utilises la commande ci-dessous.Un déploiement Git qui échoue est enregistré dans l'historique avec son message d'erreur. C'est indispensable : quand le déclencheur est un crochet appelé par la forge, personne ne lit la réponse HTTP — sans cette trace, une branche renommée donnerait un site qui cesse silencieusement de se mettre à jour.
3. La commande en ligne
C'est la voie à prendre si ton site demande une compilation : elle compile chez toi et n'envoie que le dossier de sortie.
npm run build
npx ecloudserv-pages deploy --dir dist --site <id-du-site>Poser le jeton sur ta machine
C'est l'étape qu'on saute et qui produit le message ✗ Aucun jeton. La commande ne lit jamais tes cookies de navigateur : elle s'authentifie avec une clé d'API, qu'il faut lui donner.
Créer la clé
list et whoami, mais deploy sera refusé en 403. La valeur en clair n'est affichée qu'une fois : copie-la tout de suite.La donner à la commande
Vérifier avant de publier
npx ecloudserv-pages whoami répond par ton nom d'utilisateur si le jeton est accepté. C'est la façon la plus rapide de séparer « mauvaise clé » de « mauvais site ».:: Pour la fenêtre en cours seulement
set ECLOUDSERV_TOKEN=ecl_xxxxxxxx
npx ecloudserv-pages whoami
:: Ou définitivement, pour toutes les fenêtres À VENIR.
:: (setx n'affecte pas la fenêtre courante : rouvre-la ensuite.)
setx ECLOUDSERV_TOKEN ecl_xxxxxxxxLe jeton ne va JAMAIS dans un fichier versionné
ecloudserv-pages.json, ni dans un .env poussé sur une forge. Une clé d'API donne les mêmes droits que ton compte ; une clé publiée par mégarde se révoque dans le tableau de bord, mais tout ce qu'elle a permis entre-temps est déjà fait. Dans un CI, passe par les secrets de la forge.L'identifiant du site s'obtient avec npx ecloudserv-pages list, ou se lit dans l'adresse de sa page dans le tableau de bord.
// ecloudserv-pages.json, à la racine du projet — versionnable.
// Le jeton, lui, reste dans ECLOUDSERV_TOKEN.
{ "site": "abc123", "dir": "dist" }.git, node_modules, .env, .DS_Store et Thumbs.db sont écartés d'office — ils n'ont rien à faire sur un site public. Pour le reste, un .ecloudservignore dans le dossier publié, un motif par ligne (*.map, rapport/) ; ou --ignore directement sur la ligne de commande. Un nom sans / s'applique à tous les niveaux, et * ne traverse pas les dossiers — il n'y a volontairement ni ! ni **.
Les autres commandes
| Commande | Ce qu'elle fait |
|---|---|
| deploy | Compresse le dossier et le met en ligne. |
| pack --out site.zip | Construit l'archive sans l'envoyer, pour l'ouvrir et vérifier. |
| list | Tes sites et leur identifiant. |
| status --site <id> | Certificat, contenu servi, règles retenues. |
| deployments --site <id> | Historique des mises en ligne. |
| rollback <id> --site <id> | Remet en ligne une version précédente. |
| whoami | Vérifie que le jeton est accepté, sans rien publier. |
| init | Écrit ecloudserv-pages.json. |
--dry-run prépare tout et n'envoie rien : c'est la façon la plus rapide de vérifier qu'on pointe le bon dossier. Après un déploiement réussi, les lignes de _redirects et _headers refusées sont affichées — sans ça, une redirection mal écrite est ignorée en silence et on cherche longtemps pourquoi elle ne s'applique pas.
Dans un script d'intégration continue
Les codes de sortie sont distincts pour qu'un script sache quoi faire : 0 publié, 1 erreur d'usage (dossier absent, site non indiqué), 2 refus de l'API (jeton, droits, site inconnu, archive trop grosse), 3 API injoignable. Seul le 3 mérite d'être réessayé — et les coupures réseau, les 429 et les 5xx le sont déjà trois fois, avec une attente croissante : un redémarrage d'API pendant un déploiement ne casse pas la chaîne. Un 4xx n'est jamais réessayé, insister ne changerait rien.
L'archive est déterministe : deux exécutions sur le même dossier produisent le même fichier octet pour octet. Les liens symboliques sont suivis — les dossiers de sortie des monorepos en contiennent, et les ignorer publiait des sites auxquels il manquait des fichiers sans le moindre avertissement.
Gabarits#
Un gabarit ne compile rien : il règle ton site correctement et te donne la commande et le dossier de sortie exacts de ton framework, y compris les options qui ne s'inventent pas.
| Gabarit | Compilation | Dossier à envoyer | Repli SPA |
|---|---|---|---|
| HTML statique | — | . | non |
| Next.js | next build | out | non |
| Nuxt | nuxi generate | .output/public | non |
| Astro | astro build | dist | non |
| Vite (React, Vue, Svelte) | vite build | dist | oui |
| SvelteKit | vite build | build | non |
| Documentation | vitepress build | .vitepress/dist | non |
Les réglages qui piègent
- Next.js : il faut
output: "export"dansnext.config.js, sinonnext buildne produit pas de dossierout. Etimages.unoptimized: true, l'optimiseur d'images exigeant un serveur. - Nuxt :
nuxi generate, pasnuxi build— le second produit un serveur Node, que nginx seul ne peut pas servir. - SvelteKit : installe
@sveltejs/adapter-static, l'adaptateur par défaut produit un serveur. - Astro : laisse
output: "static"(le défaut) — un adaptateur SSR produirait un serveur.
Le repli SPA, en une phrase
Activé, toute URL inconnue renvoie index.html — ce qu'exige un routeur côté client. À ne surtout pas activer sur un site à plusieurs fichiers HTML : il transformerait chaque vraie 404 en page d'accueil, rendant les liens morts invisibles et faisant indexer des doublons aux moteurs de recherche.
Redirections et en-têtes#
Deux fichiers texte à la racine de ton site, lus à chaque mise en ligne et traduits en configuration serveur. C'est la convention de Netlify et de Cloudflare Pages : si tu l'as déjà utilisée ailleurs, tes fichiers marchent tels quels.
_redirects
# source destination code (301 par défaut)
/ancien-article /blog/nouveau 301
/blog/* /articles/* 301
/promo https://boutique.fr/x 302
/app/* /index.html 200- un
*final capture la suite du chemin et la recolle à la destination :/blog/mon-articledevient/articles/mon-article; - codes acceptés :
301,302,303,307,308, et200; 200est une réécriture interne : le visiteur garde son URL et on lui sert un autre fichier. C'est le repli d'une application monopage, en plus précis que l'option « Monopage » qui l'applique partout.
_headers
/*
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
/assets/*
Cache-Control: public, max-age=31536000, immutableUn chemin en début de ligne, ses en-têtes indentés en dessous. Les en-têtes que le serveur calcule lui-même (Content-Length, Transfer-Encoding, Date, Server…) sont refusés : les imposer donnerait des réponses incohérentes selon le navigateur.
Une ligne refusée est signalée, pas devinée
Ces fichiers vivent dans ton dépôt, à côté de ton code. Ils ne sont pas modifiables depuis le tableau de bord : il y aurait alors deux sources de vérité, et la prochaine mise en ligne écraserait silencieusement ce que tu aurais réglé à l'écran.
Mises en ligne et retour arrière#
Chaque publication crée une mise en ligne immuable, avec sa date, sa source (envoi / Git / gabarit) et sa référence. L'onglet Mises en ligne permet de rebasculer sur une précédente : c'est instantané, rien n'est reconstruit — l'archive existe déjà.
Attention
Répartition et cache#
Le tableau de bord ne pousse pas de fichiers vers les nodes : il publie un état(« ce node doit avoir ces sites, dans ces versions ») et chaque node va chercher ce qui lui manque. C'est ce qui rend l'ensemble réparable tout seul — un node qui rejoint la plateforme, ou qui a perdu son disque, se resynchronise au cycle suivant sans qu'on rejoue le moindre déploiement.
Le basculement d'une version à l'autre est atomique : aucun visiteur ne peut tomber sur un site à moitié extrait.
Cache navigateur
| Fichiers | Cache-Control | Pourquoi |
|---|---|---|
Empreintés (app.a1b2c3d4.js, _next/static/) | max-age=31536000, immutable | Leur nom change quand leur contenu change : le navigateur ne redemande jamais. |
| Autres ressources (images, CSS, polices) | max-age=3600 | Une heure, puis revalidation. |
| HTML | max-age=0, must-revalidate | Il change à chaque mise en ligne à la même URL : le mettre en cache long servirait l'ancienne page pendant des heures après un déploiement. |
Sécurité#
Un site statique n'exécute rien, il n'a donc pas de faille applicative — mais il peut toujours être noyé sous le trafic. L'onglet Sécurité expose le même bouclier L7 que les domaines proxifiés :
- Limites strictes — bride plus tôt les visiteurs anormaux ;
- Mode attaque — vérifie chaque nouveau visiteur avant de servir la page ;
- Protection anti-bots et blocage des robots d'IA ;
- Vérification en cas de dépassement — au lieu d'un 429 sec, le visiteur passe une vérification et repart. Activé par défaut.
La limitation de débit s'applique aux pages uniquement, jamais aux ressources : un seul chargement en tire trente d'un coup, et les compter reviendrait à bloquer quelqu'un pour avoir simplement ouvert le site.
La liste noire d'adresses de ton compte s'applique aussi à tes Pages, en HTTP comme en HTTPS. Voir Sécurité et Pare-feu applicatif.
Référencement#
L'onglet Référencement génère et sert robots.txt et sitemap.xml. Ils sont posés par-dessus le contenu déployé, à chaque cycle : corriger une ligne de robots.txt ne demande donc pas de redéployer tout le site.
À noter
robots.txt vide dit « aucune règle », son absence dit « je n'en ai pas ». Le bouton Retirer l'efface vraiment des nodes — sans quoi un fichier supprimé resterait servi indéfiniment.Le certificat n'arrive pas#
L'onglet Aperçu pose un diagnostic complet plutôt qu'un message générique. Dans l'ordre des causes réelles :
Le DNS ne pointe pas (encore) vers un node
Let's Encrypt valide par le domaine : tant qu'il ne résout pas vers une de nos adresses, rien ne peut aboutir. Le diagnostic affiche ce que le domaine résout et les adresses acceptables.
Un enregistrement AAAA égaré
Attention
AAAA mène ailleurs (ou nulle part), la validation échoue alors que « le DNS est correct ». Un contrôle qui ne regarde que l'IPv4 déclare tout bon. Le nôtre le signale.Le site n'a pas encore de contenu
Un site sans mise en ligne n'a pas de dossier sur les nodes : il n'est pas ajouté à la configuration nginx, donc le domaine tombe sur la page d'accueil d'nginx. Le diagnostic sait le distinguer d'une vraie panne — il vérifie que ton domaine figure dans la configuration réellement envoyée aux nodes, et sonde le chemin de validation de bout en bout.
Limites#
| Limite | |
|---|---|
| Archive envoyée | 30 Mo |
| Contenu décompressé | 60 Mo |
| Un seul fichier | 25 Mo |
| Nombre de fichiers | 5 000 |
| Dépôt Git téléchargé | 80 Mo |
| Mises en ligne restaurables | 5 |
| robots.txt / sitemap.xml | 16 Ko / 512 Ko |
Les fichiers volumineux (vidéos, jeux de données, téléchargements) ont leur place dans le stockage objet, pas dans l'archive du site : celle-ci est recopiée sur chaque node.
Ce que Pages ne fait pas#
- Aucune compilation côté serveur. Voir plus haut : c'est un choix de sécurité, pas un manque.
- Aucun dépôt privé. Il n'y a ni application OAuth, ni jeton d'accès à protéger. Pour un dépôt privé, utilise
npx ecloudserv-pagesdepuis ton intégration continue. - Pas de code côté serveur : ni redirections dynamiques, ni fonctions, ni en-têtes par route. Il te faut un conteneur, ou un domaine en reverse proxy.
- Pas de certificat générique (
*.exemple.fr) : il demande une validation DNS que nous ne pilotons pas. - Un domaine ne peut pas être à la fois une Page et un proxy — les deux produiraient le même
server_name, et nginx servirait celui que le hasard de génération place en premier. Retire-le des Domaines d'abord.
Référence#
/pagesportée read/pagesportée write{ name, domain, spa?, ssl?, template? }/pages/templatesportée read/pages/:id/dnsportée read/pages/:id/deployportée writemultipart/form-data./pages/:id/gitportée write{ repo, branch?, subdir?, autoDeploy? }. repo: null débranche tout et retire le crochet./pages/:id/deploy-gitportée write/pages/:id/rollback/:deployIdportée write/pages/:id/shieldportée write/pages/hook/:token