Héberger un site statique avec son API sur la même origine
Un build, un dossier dist/, et la question qui suit : où le mettre, et comment ses pages parlent au backend ? Sur Zerolith, le site et ses fonctions partagent la même origine. Déploiement, repli SPA, vrais 404, versions, et le prix au centime près.
Un outil de génération d'app (Lovable, Bolt, v0, Claude, Cursor) vous laisse en général avec deux
choses : un dossier dist/ après npm run build, et une question. Où le mettre, et comment ses
pages parlent-elles au backend ? La réponse habituelle tient en trois services : un hébergeur
statique pour le frontend, un autre pour l'API, et une configuration CORS entre les deux qui finit
toujours par casser au mauvais moment.
Zerolith sert maintenant les sites statiques. Ce qui change, c'est que vos fonctions répondent
sur la même origine que le site, sous /api/. Voici comment ça marche, ce que ça coûte, et ce
que le service ne fait pas.
Ce qu'on publie
Un site, c'est le résultat du build, pas les sources : le dossier que Vite, Astro, Next en
export statique ou un simple éditeur de texte produisent, avec un index.html à la racine.
Zerolith ne lance pas npm install et n'exécute aucun script de build chez lui. C'est un choix, pas
un oubli : un postinstall est du code arbitraire, et l'exécuter pour chaque déploiement demande
un bac à sable qui n'existe pas encore.
Chaque site reçoit un nom, qui devient son adresse :
https://<nom>.sites.zerolith.fr
Le nom fait de 3 à 40 caractères (minuscules, chiffres, tirets). Les sites ne sont pas servis
sous zerolith.io : un site est du HTML arbitraire écrit par quelqu'un d'autre que nous, et le
mettre sous le domaine du tableau de bord donnerait à chaque page la réputation (et les cookies) de
celui-ci.
Trois façons de déployer
Le tableau de bord. Page Sites, on crée le site, puis on glisse le dossier dist/ dans la zone
prévue. Les fichiers partent directement vers le stockage objet, sans transiter par l'API, et le
site est en ligne dès la publication.
Un agent IA. Avec le serveur MCP branché, l'outil deploy_site reçoit le
contenu des fichiers dans la conversation et publie en un appel, jusqu'à environ 1,4 Mo. Au-delà,
create_site_deploy renvoie des URL d'envoi présignées que l'agent remplit avec curl. En pratique
: « construis le projet et publie-le sur mon site demo » suffit.
L'API. Trois appels, et c'est là qu'on voit comment ça fonctionne :
# 1. Le site
curl -X POST -H "Authorization: Bearer $JWT" \
-d '{"name": "demo"}' https://zerolith.io/api/sites
# 2. Un déploiement : la LISTE des fichiers, avec leur empreinte et leur taille
curl -X POST -H "Authorization: Bearer $JWT" \
-d '{"files": [{"path": "index.html", "sha256": "9f2c…", "size": 812},
{"path": "assets/index-B3x9Kq1z.js", "sha256": "41ab…", "size": 70211}]}' \
https://zerolith.io/api/sites/<site_id>/deploys
# → { "deploy": {…}, "uploads": [ {"sha256": "…", "url": "https://…"} ] }
# 3. Envoyer chaque fichier demandé (PUT sur l'URL présignée), puis publier
curl -X POST -H "Authorization: Bearer $JWT" \
https://zerolith.io/api/sites/<site_id>/deploys/<deploy_id>/publishLe stockage est adressé par contenu : un fichier est rangé sous son empreinte SHA-256. La réponse de l'étape 2 ne réclame donc que les fichiers que le site ne possède pas déjà. Redéployer après avoir corrigé une faute dans une page n'envoie qu'une page, et un asset inchangé d'une version à l'autre n'est stocké qu'une fois. L'URL présignée est signée pour une taille précise : un fichier plus gros que celui annoncé est refusé par le stockage lui-même, pas par un contrôle de notre côté qu'on pourrait contourner.
Deux contrôles avant même d'envoyer quoi que ce soit : un fichier .env (ou .env.production,
etc.) est refusé, parce qu'une fois publié il serait lisible par n'importe qui, et un build qui
en contient un contient en général les clés avec lesquelles il a été construit. Un dossier .git
est refusé pour la même raison.
Ce que le site sert, exactement
Le comportement est pensé pour une application à routage côté client, et pour ne jamais mentir sur un fichier absent :
- un fichier présent est servi avec son type MIME et un
ETag(un rechargement répond304) ; les assets à empreinte (index-B3x9Kq1z.js) partent enCache-Control: immutable, le HTML enno-cache, pour qu'une nouvelle version soit visible au rechargement suivant ; /docs/sertdocs/index.html, et/docsredirige vers/docs/;- un chemin sans extension inconnu (
/tableau-de-bord/reglages) renvoieindex.htmlen200: c'est le routeur de React ou de Vue qui prend la main ; - un fichier absent AVEC extension (
/assets/ancien.js,/robots.txt) renvoie un vrai 404, avec votre404.htmlsi vous en avez un.
Ce dernier point mérite qu'on s'y arrête. Beaucoup d'hébergeurs répondent index.html à tout ce
qu'ils ne trouvent pas. Le navigateur reçoit alors du HTML là où il attendait du JavaScript, la page
plante sur une erreur de syntaxe incompréhensible, et rien dans les journaux n'indique qu'un fichier
manquait. Un 404 se voit tout de suite. Le repli SPA se désactive par site si votre site n'a pas de
routeur.
Tout cela se vérifie sur un site de démonstration,
zerolith-test.sites.zerolith.fr. La page teste
elle-même, depuis le navigateur, le chargement des assets, une route SPA (/route/test), un fichier
absent qui doit répondre 404 et un appel /api/myip/ vers une fonction du même compte. Ouvrez
l'onglet réseau pendant le chargement : chaque requête et son code de retour y apparaissent.
Le backend sur la même origine
C'est la raison pour laquelle on a construit ce service plutôt que de renvoyer vers un hébergeur
existant. Toute requête sous /api/<fonction>/ est transmise à votre fonction du même nom :
// Aucune URL absolue, aucun CORS : c'est la même origine.
const res = await fetch("/api/contact/", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, message }),
});Côté fonction, c'est un handler ordinaire, qui reçoit le reste du chemin après /api/contact :
def handler(request):
data = request.json()
# … enregistrer, envoyer un e-mail, appeler une API tierce …
return {"ok": True, "received": data.get("email")}Quelques règles, qui découlent toutes du fait qu'un navigateur est un client public :
- Seules les fonctions publiques répondent. Une fonction privée renvoie
401sous/api/, et c'est voulu : la page n'a aucun moyen de garder une clé d'API secrète, n'importe qui la lirait dans l'onglet réseau. Une fonction publique est limitée en débit par fonction, pas sur votre budget global. - Le routage est fixe.
/api/contact/…va à la fonctioncontactdu propriétaire du site, et à aucune autre. Personne ne peut faire servir la fonction d'un autre compte par votre site. - Pas de
/api/dans vos fichiers. Un fichier publié sousapi/serait masqué par le routage, il est donc refusé au déploiement plutôt que d'être ignoré en silence.
Pour un état persistant (des inscriptions, un panier, des scores), la fonction s'appuie sur la base de données privée : le site, l'API et la base vivent sur le même compte, facturés sur le même relevé.
Versions et retour arrière
Chaque publication est un déploiement conservé. Le compte d'essai en garde 3, un compte payant 10. Revenir en arrière, c'est republier un déploiement précédent : pas de nouvel envoi, les fichiers sont déjà là, et la bascule est immédiate. Au-delà de la limite, le plus ancien déploiement qui n'est pas en ligne est supprimé, avec les fichiers que plus aucune version n'utilise. La version en ligne, elle, n'est jamais supprimée.
Ce que ça coûte
| Essai | Compte payant | |
|---|---|---|
| Sites | 1 | 10 |
| Stockage par site | 10 Mo | 500 Mo |
| Fichiers par déploiement | 1 000 | 4 000 |
| Taille max d'un fichier | 25 Mo | 25 Mo |
| Déploiements conservés | 3 | 10 |
| Indexation par les moteurs | non (noindex) |
oui |
Le stockage est gratuit jusqu'à 10 Mo par compte, tous sites confondus. Au-delà, un compte payant paie 0,40 € par Go et par mois, au prorata et réglé chaque jour. Un compte d'essai n'est jamais facturé pour ses sites : son unique site est plafonné à la franchise. Le trafic n'est pas facturé, mais il est limité en débit par site, pour qu'un site très sollicité ne prive pas les autres.
Faisons le calcul pour une app Vite typique. Un build de 2 Mo, dix versions conservées dont la
moitié des assets change à chaque fois : environ 11 Mo stockés, donc 1 Mo au-dessus de la
franchise, soit 0,0004 € par mois. Ce qui coûtera vraiment, ce sont les fonctions derrière
/api/, au prix habituel, à la seconde de pod en vie (le détail est sur
la page tarifs).
Le noindex du compte d'essai est un garde-fou contre l'abus, pas une punition : un hébergement
gratuit et indexé est exactement ce que recherchent les auteurs de pages de phishing. Il disparaît
au premier rechargement de crédit.
Ce que le service ne fait pas (encore)
Autant le dire clairement, plutôt que de vous laisser le découvrir :
- Pas de build. Il faut publier le dossier construit. Un agent IA connecté au MCP peut lancer le build sur votre machine puis publier, ce qui couvre l'essentiel des cas.
- Pas de rendu côté serveur. Un Next.js en SSR ne passe pas ; en
output: "export", si. - Pas de redirections ni d'en-têtes configurables à la
_redirects/_headers. - Pas de CDN mondial. Les sites sont servis depuis nos serveurs en France, avec un cache en mémoire devant le stockage. Pour un public européen, c'est rapide. Pour un public mondial, un CDN devant le site reste une bonne idée.
Et ensuite
Un site peut aussi être servi sous votre propre domaine : on le rattache depuis la page Sites,
on prouve qu'on possède la zone par un enregistrement TXT, on fait pointer un CNAME vers
<nom>.sites.zerolith.fr, et le certificat TLS est émis automatiquement. Même prix qu'un domaine
sur une fonction, 1 € par mois. Le détail est dans la documentation.
Le plus court chemin pour essayer reste le tableau de bord : créer un compte, créer un site, glisser
le dossier dist/. Le crédit offert à l'inscription couvre largement les fonctions qu'une petite
app appellera derrière.
Essayez sur votre propre compte
L'inscription vient avec du crédit, de quoi déployer, planifier et mesurer tout ce qui précède sans sortir de carte bancaire.
Démarrer gratuitement »