# Static sites and front-end builds

> Publish plain HTML or build React, Vue, Vite, Angular, Astro, Gatsby or Docusaurus sites from Git, with precompressed assets, caching, rollback and SPA routing.

Source: https://zopanel.net/docs/static-sites  
Updated: 2026-10-09

A static site is a folder of HTML, CSS, JavaScript and media files that nginx serves directly, with no application process. Use it for plain HTML pages and for front-end frameworks that compile to files (React, Vue, Svelte, Angular, static Astro or Next.js exports, documentation generators). If your project needs a server at runtime, see [Node.js apps](/docs/nodejs) instead.

## Choose how to publish

| Method | When to use | How it works |
| --- | --- | --- |
| **HTML** website | Hand-written or pre-built files, no build step | Upload the files to `domains/<domain>/public_html`. |
| **Git deploy** | A repository, with or without a build step | ZoPanel clones, builds and publishes each release, with rollback. |
| **Deploy** tab with **Uploaded files (File Manager)** | Source code without Git | Upload the project to `domains/<domain>/source`, then deploy. It is built like a Git release. |

## Publish plain HTML files

1. Open **Websites → New website**, enter the **Domain** and choose **HTML**.
2. Keep **Free SSL (Let's Encrypt)** on and click **Create website**.
3. Upload your files to `domains/<domain>/public_html` with the [File Manager](/docs/file-manager) or [SFTP](/docs/ftp-sftp). The home page must be `index.html` (or `index.htm`).

Changes are live as soon as the files are saved. PHP is not run on these websites: requests for `.php` files are refused with 403.

## Deploy a front-end build from Git

1. Open **Websites → New website**, choose **Git deploy**, enter the **Repository URL**, **Branch** and, for monorepos, the **Root directory**.
2. Click **Create website**. ZoPanel installs dependencies, runs the build and serves the output folder.
3. Open the **Deploy** tab to see the detected settings in **Build & run**: **Type** is **Static site** and **Output directory** is the folder that is published.

You can also connect an existing **HTML** website to Git from its **Deploy** tab. After the first deployment, the website serves the build output instead of `public_html`.

### What is detected

With **Auto-detect** on, ZoPanel reads `package.json` and picks the output folder:

| Dependency in package.json | Detected as | Output directory |
| --- | --- | --- |
| `vite` (and no `start` script) | Vite (React/Vue/Svelte) | `dist` |
| `react-scripts` | Create React App | `build` |
| `@angular/core` | Angular | read from `angular.json` (`outputPath` + `/browser`), else `dist` |
| `gatsby` | Gatsby | `public` |
| `@docusaurus/core` | Docusaurus | `build` |
| `vitepress` | VitePress | `.vitepress/dist` |
| `astro` without `@astrojs/node` | Astro | `dist` |
| `@sveltejs/kit` with `@sveltejs/adapter-static` | SvelteKit | `build` |
| `next` with `output: 'export'` in `next.config.js`, `.mjs` or `.ts` | Next.js | `out` |

The build command is `npm run build` (or `pnpm`/`yarn run build` when their lockfile is present) and the install command follows the lockfile, as for [Node.js apps](/docs/nodejs). If `package.json` has no `build` script, the note "No build script found in package.json" appears.

Without `package.json` (and no PHP, Python, Ruby, Go, Java or .NET project files), a repository with `index.html` at its root is served as **Static HTML** from the root directory. Anything else is served as static files with the note "No known framework detected; serving files as a static site."

### Other generators

Hugo, Jekyll and other generators that are not npm packages are not installed on the server. Build the site in CI (or locally) and push the generated folder, for example to a `deploy` branch, then deploy that branch. For npm-based generators that are not in the table (Eleventy, Nuxt with `nuxi generate`…), set the settings yourself:

1. On the **Deploy** tab, turn **Auto-detect** off in **Build & run**.
2. Set **Type** to **Static site**.
3. Set **Build command**, for example `npm run generate`, and **Output directory**, for example `.output/public` for Nuxt or `_site` for Eleventy.
4. Click **Save & deploy**.

Build tools run with the server's newest Node.js, or the version from `.nvmrc`, `.node-version` or `engines.node` when the runtime is `node`.

### Environment variables in the build

Front-end frameworks embed some variables at build time, for example `VITE_API_URL` for Vite or `NEXT_PUBLIC_*` for Next.js. Add them in **Environment variables** and click **Save & redeploy**: they are available to the build command. Everything embedded in the output is public, so never put secrets there.

## How files are served

- The website's document root points to `current/<root directory>/<output directory>`, where `current` is the live release.
- Directory requests serve `index.html`. A missing file returns 404.
- Images, CSS, JavaScript, fonts and videos (`css`, `js`, `mjs`, `jpg`, `jpeg`, `png`, `gif`, `webp`, `avif`, `svg`, `ico`, `woff`, `woff2`, `ttf`, `eot`, `mp4`, `webm`) are sent with a 30-day browser cache (`Cache-Control: public`). HTML has no such header, so browsers check for a new version.
- After each build, ZoPanel stores compressed copies (`.gz`, and `.br` when the server has Brotli) of text files between 1 KB and 5 MB: CSS, JavaScript, SVG, JSON, HTML, XML, TXT and WebAssembly. nginx sends them without compressing on every request.

**Tip:** because assets are cached for 30 days, use file names that change with their content (Vite, Create React App, Angular and Next.js do this by default). An unchanged name such as `style.css` may stay cached in visitors' browsers after a deploy.

## Single-page apps (client-side routing)

ZoPanel has no automatic SPA fallback: a direct visit to `/dashboard/settings` returns 404 if that file does not exist. Choose one of these:

- **Hash routing** (`/#/dashboard`), which needs nothing on the server.
- **Prerendering** each route to its own `index.html` (Astro, Next.js export, Gatsby, Docusaurus and VitePress do this).
- **Custom error page:** on the website's **Tools** tab, set the **404** page in **Custom error pages** to `/index.html`. The app loads on every route, but the response status stays 404, which search engines treat as a missing page.
- **Administrators** can return the app with status 200 by adding this line on the website's **Advanced** tab under **Custom nginx directives**, then **Test & save**:

```nginx
error_page 404 =200 /index.html;
```

Leave the 404 **Custom error page** empty when you use this directive.

## Updates and rollback

Each deploy builds a new release in `domains/<domain>/releases/<timestamp>/`, then switches `current` to it in one step, so visitors never see a half-uploaded site. If the build fails, the live release is untouched.

The **Releases** card keeps the last 5 builds; **Rollback** switches back to one instantly, without rebuilding. To deploy automatically on every push, see [Git deploy](/docs/git-deploy#automatic-deploys-on-push).

**Note:** files you upload into a release folder disappear with the next deploy. Put user uploads in a **Persistent path**, or in [S3 storage](/docs/apps).

## Troubleshooting

| Message or symptom | Cause and fix |
| --- | --- |
| 404 on every page after the deploy | **Output directory** is wrong: the build wrote elsewhere. Check the folder in the build log in **Tasks** and correct it with **Auto-detect** off. |
| 404 when reloading a page of a React/Vue app | Client-side routing: see [Single-page apps](#single-page-apps-client-side-routing). |
| `build failed: …` | Read the build log. With `npm ci`, the lockfile must match `package.json`. |
| `a start command is required for server apps` | The project was detected as a Node.js server (for example a Vite project with a `start` script). Turn **Auto-detect** off and set **Type** to **Static site**. |
| `Node.js is not installed — install it under Runtimes` | The build needs Node.js. An administrator installs it under **Runtimes**. |
| Old CSS or JavaScript after a deploy | The browser cache (30 days) keeps files whose name did not change. Use hashed file names or add a version to the URL. |
| 403 on a `.php` file | Static websites never run PHP. Use a PHP website instead. |
| Placeholder page on an **HTML** website | Upload into `public_html`, with an `index.html` at its top level. |

## Related

- [Your first website](/docs/first-website)
- [Git deploy](/docs/git-deploy)
- [Node.js apps](/docs/nodejs)
- [Website tools](/docs/website-tools)
- [Vite: deploying a static site](https://vite.dev/guide/static-deploy)
- [Next.js static exports](https://nextjs.org/docs/app/guides/static-exports)
