---
title: Publish a static site
description: Deploy HTML, CSS and an image with the small Node.js server that answers the application's health check.
slug: /guides/publish-a-static-site
order: 42
visibility: public
redirects_from: []
---

# Publish a static site

Prompt:

> Publish this HTML page, stylesheet and image as a public site.

Put the public files under `client/` and include a small Node.js server for the health check. The platform serves the files before requests reach that server. The same server below also serves them when you test on your own computer.

## What your AI tool does

- Keeps the page, stylesheet and image together under `client/`, linked by root paths such as `/styles.css` and `/logo.png`.
- Adds `package.json`, a server and the manifest, then checks the site locally where a shell is available.
- Chooses an available deploy route, waits for the outcome, and checks the deployed page and image.
- Asks which files may be public before placing them under `client/`.

## What you need

- The HTML page, stylesheet and image you want to publish, with no secret or private file among them.
- A connected, signed-in AI tool for the deploy ([Connect your AI coding tool](/cloud/getting-started/connect-your-tool/)).
- Node.js 24 or later for a run on your computer. A tool with no shell can use the file-list deploy route and skip that local run.

## Before your AI starts

These steps are for your AI tool.

- Read the `deploy` skill with `read_context`, id `skill:deploy`.
- Ask the account's owner which plan to use when creating the application. Follow [Deploy from your computer](/cloud/getting-started/deploy-an-application/) for application creation and manifest submission.
- Keep every file under `client/` public. This example needs no database, sign-in for visitors, library package or runtime dependency.

## Steps

### 1. Put the page's files in the deploy folder

Use this layout, with your own HTML, CSS and PNG image:

```text
package.json
server.mjs
manifest.json
client/index.html
client/styles.css
client/logo.png
```

In `index.html`, link the stylesheet as `/styles.css` and the image as `/logo.png`. If your image has another name or type, update the HTML and the local server's file table together. Keep files the page does not need outside `client/`.

Write `package.json` at the folder's root:

```json
{
  "name": "static-site",
  "private": true,
  "type": "module",
  "scripts": { "start": "node server.mjs" }
}
```

With no dependencies, this application needs no package lockfile or installation step of its own. The deploy still needs the root package file and its `start` script.

### 2. Add the health server and manifest

Write `server.mjs` beside `package.json`:

```js
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';

const files = new Map([
  ['/', ['index.html', 'text/html; charset=utf-8']],
  ['/index.html', ['index.html', 'text/html; charset=utf-8']],
  ['/styles.css', ['styles.css', 'text/css; charset=utf-8']],
  ['/logo.png', ['logo.png', 'image/png']],
]);

createServer(async (request, response) => {
  request.signal?.addEventListener('abort', () => response.destroy(), { once: true });
  const path = new URL(request.url, 'http://localhost').pathname;
  if (request.method !== 'GET') {
    response.writeHead(404).end();
    return;
  }
  if (path === '/health') {
    response.writeHead(200, { 'content-type': 'text/plain' }).end('ok');
    return;
  }
  const file = files.get(path);
  if (!file) {
    response.writeHead(404).end();
    return;
  }
  try {
    const bytes = await readFile(new URL(`./client/${file[0]}`, import.meta.url));
    if (!response.destroyed) response.writeHead(200, { 'content-type': file[1] }).end(bytes);
  } catch (error) {
    if (!response.destroyed) response.writeHead(error.code === 'ENOENT' ? 404 : 500).end();
  }
}).listen(Number(process.env.PORT || 8080), '0.0.0.0');
```

The file table lets a local run serve only the named files. In a deployed copy, the platform normally serves `client/` first. While its file store fails, a request for a file beyond the shell can reach your server as an ordinary request ([Serve a web client](/cloud/guides/serve-a-web-client/#5-know-what-a-client-file-counts-and-who-may-read-it)); this server can serve it too. `/health` reaches this server. The deploy gate needs three consecutive 200 answers within 180 seconds, so keep the server running and answer that path as soon as it listens ([The deploy health check](/cloud/concepts/the-deploy-health-check/)).

Write and submit this `manifest.json` before the first deploy:

```json
{
  "manifest_version": 1,
  "services": [],
  "health": "/health",
  "region": "usa",
  "egress": [],
  "audience": { "kind": "public" },
  "packages": []
}
```

`packages: []` declares that the application uses no library entry. The server uses Node.js's built-in modules alone. [Author the manifest](/cloud/guides/author-the-manifest/) explains the other settings.

### 3. Check it on your computer

Where a shell is available, start the application from this folder:

```sh
npm start
```

Keep it running while you request `http://localhost:8080/health`, `/`, `/styles.css` and `/logo.png`; each should return 200. Open the page in a browser when available and check that the image and styles load. If `PORT` is set, use that port instead. Stop the process you started when the check is finished.

This server reads no platform credentials or service settings, so this local check needs no provision command or Turn Zero sign-in. A tool that cannot run a shell skips the check and reports it as unrun.

### 4. Deploy the folder and check the result

Follow [Ways to deploy](/cloud/getting-started/ways-to-deploy/) to choose the route. Submit the manifest and call `check_deploy` as that page directs before deploying.

- Prefer the `turnzero` command where it works. It sends the folder and binary image together.
- For a first small test or a tool with no shell, `deploy_files` can send the whole file list. Send the PNG's bytes as base64 in `content` with `"encoding": "base64"`. Its limits are 512 KiB per file, 200 entries and 4 MiB in total; the request's JSON also has a 4 MiB bound. [Deploy from a chat](/cloud/getting-started/build-an-app-from-a-chat/) explains splitting calls within those limits.
- Past those limits, use the command or the upload route from Ways to deploy, as the tool permits. Preserve any actual permission refusal.

Read `read_status` with `wait_seconds` until the deploy ends. Then request `/health`, `/`, `/styles.css` and `/logo.png` on the hostname the platform returned. With no shell, use `request_route`. Read a failed outcome before retrying; a lost response does not show whether the deploy started.

## Expected result

The deploy reaches `deployed`; `/health` returns 200 and the page, stylesheet and image load on the application's public hostname. A request the platform serves as a client file wakes no application and costs no backend action. A request falling back to this server uses a backend action. The page, `client/index.html`, and stylesheet, `client/styles.css`, are shell files and count nothing. The image, `client/logo.png`, is beyond the shell and counts its bytes toward `data_transfer_bytes` ([Serve a web client](/cloud/guides/serve-a-web-client/)).

## Refusals

| What you see | Cause | Remedy |
| --- | --- | --- |
| The deploy's health check fails | The server did not listen on `PORT` or `/health` did not answer 200. | Read the failed outcome and logs; check the start script, port and health route before another deploy. |
| The image or stylesheet returns 404 | A file is missing or the HTML uses another name or case. | Match the deployed path and filename, including case; keep each file under `client/`. |
| `deploy_files_too_large` | The file list is past that route's limits. | Use the command or upload route where available; do not split calls to bypass the total limit. |

## Related

- [Serve a web client](/cloud/guides/serve-a-web-client/) explains public files, routing and metering.
- [Ways to deploy](/cloud/getting-started/ways-to-deploy/) chooses the route your tool can use.
- [The deploy health check](/cloud/concepts/the-deploy-health-check/) explains readiness and failed outcomes.
