zerolith

← Tous les articles

FrontendTutoriel

Héberger un site statique avec son API sur la même origine

8 min de lecture

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 :

adresse d'un site
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 :

déployer par l'API
# 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>/publish

Le 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épond 304) ; les assets à empreinte (index-B3x9Kq1z.js) partent en Cache-Control: immutable, le HTML en no-cache, pour qu'une nouvelle version soit visible au rechargement suivant ;
  • /docs/ sert docs/index.html, et /docs redirige vers /docs/ ;
  • un chemin sans extension inconnu (/tableau-de-bord/reglages) renvoie index.html en 200 : 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 votre 404.html si 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 :

depuis une page du site
// 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 :

main.py
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 401 sous /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 fonction contact du 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é sous api/ 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 »