> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tape.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosting a Static Site

A tape can host a whole website. Upload your build with `tape deploy` and share it
at `https://<tape-address-base32>.tape.site/` on Tapenet. There is no server to run
or DNS to configure. You can add your own domain later.

## What you need

* The `tape` CLI ([install](/tools/cli)).
* A configured local deployment, or a funded wallet from the
  [Tapenet setup guide](/tapenet/access).
* A built site: a directory with an `index.html` at its root. Any static site generator's output works.

The commands use the signing key at `~/.config/solana/id.json`, the CLI's default. Pass
`-k <path>` for another keypair, and use `-u <rpc url>` or `TAPE_RPC_URL` to choose the
Solana RPC ([CLI context](/tools/cli/commands/context-management)).

## 1. Reserve a tape

A tape is prepaid storage: a capacity, for a number of epochs. Size it to your build
with room for redeploys, and pick how long the site should stay up. The example below
reserves 100 MiB for at least a week. `--duration` buys enough epochs to cover that time
at the network's current epoch duration, which is one hour on Tapenet
([epochs](/protocol/architecture/epochs)). Pass `--epochs <n>` instead to buy an exact
number of epochs.

Check the price and your balance first. `--dry-run` spends nothing:

```bash theme={null}
tape create --capacity 100m --duration 7d --dry-run
```

```text Output theme={null}
capacity:         104857600 bytes
epochs:           active=241 expiry=410
epoch duration:   1h
expires:          ~2026-09-23 22:00 UTC (in 7d)
cost:             0.0161226 TAPE + ~0.00564372 SOL
wallet balance:   10.0 TAPE, 1.5 SOL
dry run:          nothing was reserved
```

The first number is exact: capacity in MiB × epochs × the network's current
[storage price](/protocol/architecture/token-economics#prices-are-votes). The SOL figure is
rent for the tape's accounts plus transaction fees, so it is an estimate. Uploading the
files afterwards costs only small SOL fees. Then reserve it:

```bash theme={null}
tape create --capacity 100m --duration 7d
```

```text Output theme={null}
tape address:     E79y5bh5zTLq9h7SgPGRsDpcmTB1oa7YGscFFFiojy2D
label:            yk5y3rg6cwclvyz7sv763kttt2d2sinpzdm3chuzw72th45s2c3a
capacity:         104857600 bytes
epochs:           active=241 expiry=410
epoch duration:   1h
expires:          ~2026-09-23 22:00 UTC (in 7d)
cost:             0.0161226 TAPE + ~0.00564372 SOL
```

`tape create` checks that the wallet can pay before it signs anything. The expiry time is
an estimate based on the network's current epoch duration.

The tape's keypair is saved under `~/.tape/cassettes/` and the tape becomes the active one, so the next commands need no `--tape` flag. Keep the keypair: it is the only thing that can write to this tape.

## 2. Upload the site

```bash theme={null}
tape deploy ./dist/index.html
```

```text Output theme={null}
Deployed 4 files (4 uploaded, 0 unchanged)
Tape: E79y5bh5zTLq9h7SgPGRsDpcmTB1oa7YGscFFFiojy2D
URL:  https://yk5y3rg6cwclvyz7sv763kttt2d2sinpzdm3chuzw72th45s2c3a.tape.site/
```

Each file becomes a named object on the tape, named by its path relative to the directory. Content types are set from the file extension. The command returns when every file is written on chain.

Hidden files and directories are skipped, and visible symlinks are rejected.
The `.well-known/` namespace is never published and cannot be enabled with an
override, so tape content cannot answer domain-ownership or ACME challenges.

Check what landed:

```bash theme={null}
tape object ls
```

```text Output theme={null}
TYPE            SIZE  CONTENT-TYPE  NAME
object           130  text/html     404.html
object            59  text/css      app.css
object           120  text/html     docs/index.html
object           253  text/html     index.html
```

If the last file you uploaded is missing from the listing, run it again a few seconds later. The listing can lag the write by a moment.

## 3. Open it

Open the `URL` that `tape deploy` printed and share it. Visitors do not need a wallet or
the CLI. The subdomain is the tape address encoded as lowercase base32, because hostnames
are case-insensitive while tape addresses are not; the CLI does the encoding for you.

The reservation controls how long the site is stored. `tape info` shows the estimated
expiry time, and `tape extend --duration 7d` adds another week.

Paths map to object names. `/` serves `index.html`, `/docs/` serves `docs/index.html`, `/app.css` serves `app.css`, and a path with no object serves your `404.html` with status 404. A request for `/docs` without the trailing slash redirects to `/docs/`, so relative links inside the page resolve.

The URL uses `tape.site`, Tapenet's site domain. For a local deployment or your own
gateway, set `TAPE_SITE_DOMAIN` (or `site_domain` in the
[CLI config](/tools/cli/commands/context-management)) to its configured site domain, or use
the [`/site/{tape}/` route](#site-conventions).

## 4. Use your own domain (optional)

The `tape.site` URL is ready to share without this step. To use a domain you own,
choose a gateway with self-serve domains enabled (`txt_domains: true`).

Add two DNS records at your domain:

```text theme={null}
example.com          A      <gateway address>     (or a CNAME to the gateway)
_tape.example.com    TXT    "E79y5bh5zTLq9h7SgPGRsDpcmTB1oa7YGscFFFiojy2D"
```

The gateway looks up the TXT record on the first request, checks it with DNSSEC, and serves the tape at `https://example.com/` from the domain root. Nothing else is needed. Confirm with:

```bash theme={null}
curl -s https://example.com/ | diff - ./dist/index.html
```

An empty diff means the page served is the page you built.

For a gateway you operate, see [custom domains and subdomains](#custom-domains-and-subdomains)
below for configuration.

## Redeploy

Run the same command again after a rebuild:

```bash theme={null}
tape deploy ./dist/index.html
```

```text Output theme={null}
Deployed 4 files (0 uploaded, 4 unchanged)
Tape: E79y5bh5zTLq9h7SgPGRsDpcmTB1oa7YGscFFFiojy2D
URL:  https://yk5y3rg6cwclvyz7sv763kttt2d2sinpzdm3chuzw72th45s2c3a.tape.site/
```

Only changed files are uploaded, so a redeploy costs what changed. Nothing changed here, so nothing was written. The gateway caches site responses for 60 seconds and revalidates, so a new version is live everywhere within a minute.

Files removed from the build stay on the tape by default. Pass `--prune` to
remove stale names after the new entry point is safely published, or delete a
single name with `tape object rm <name>`.

## Frameworks

**[Astro](https://astro.build).** `astro build` output deploys as it is. Nested routes, hashed assets and images all work. Astro's default output puts each page at `about/index.html`, and the gateway's redirect from `/about` to `/about/` handles the links.

**[Vite](https://vite.dev), React Router, or any single-page app.** On `tape.site`,
the site is served from the domain root. Add a settings file to the build output
so unknown routes serve the index page instead of the 404 page:

```json dist/_site.json theme={null}
{ "spa_fallback": true }
```

The fallback applies to paths that look like routes. A missing file such as `/missing.png` still returns 404. The gateway honours `_site.json` unless the operator has turned per-site settings off ([site settings](#site-settings-on-the-tape)).

If you serve through a gateway's `/site/{tape}/` path instead, build with relative
asset URLs (`vite build --base=./`) and configure your router's base path to match
that prefix.

**[Next.js](https://nextjs.org).** Use `output: 'export'` and deploy the `out` directory. A dapp that calls an RPC from the browser lists its endpoints in `_site.json` under `connect_origins` ([site settings](#site-settings-on-the-tape)).

## Put Cloudflare in front

If your domain is on [Cloudflare](https://www.cloudflare.com), proxy the record from step 4 (the orange cloud). Hashed assets are cached at the edge on the second request. HTML pages are not cached by default and need a cache rule on your zone if you want them cached. The operator side is under [fronting with a CDN](#fronting-with-a-cdn) below.

## Deploy from CI with an S3 tool

If your pipeline already speaks S3, the gateway's S3 endpoint accepts the same site as a bucket sync:

```bash theme={null}
rclone sync ./dist tapedrive:<bucket-label>
```

This path needs a credential from the gateway operator and a one-time delegation of the tape to the gateway's key. Both are on the [S3 gateway](/tools/s3-gateway) page, and the rclone remote is on the [Rclone](/tools/quickstarts/rclone) page. Do not pass `--checksum` to rclone: ETags here are not MD5 hashes, so rclone would compare sizes instead and miss a changed file of the same length.

## Troubleshooting

| Symptom                                   | Cause and fix                                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| The root URL returns 404                  | There is no `index.html` at the top of the directory you synced. Sync the build output, not the project.                 |
| A page renders as plain text or downloads | The file has an unusual extension. Content types come from the extension; rename the file.                               |
| The site does not update after a redeploy | Responses are cached for 60 seconds. Wait a minute, then reload.                                                         |
| `insufficient capacity` on sync           | The tape is full. Grow it with `tape resize --add 100m`.                                                                 |
| The site disappears                       | The reservation expired. `tape info` shows the estimated expiry time. Extend before it with `tape extend --duration 7d`. |
| `/about` works but `/about/` returns 404  | The build wrote `about.html`, not `about/index.html`. Set your generator to directory output, or link to `about.html`.   |

## For operators

The rest of this page is the gateway side: what the site route does, and the settings that give sites domains, caching and a CDN.

## Site conventions

| Request                                   | Behavior                                            |
| ----------------------------------------- | --------------------------------------------------- |
| `/site/{tape}`                            | `308` to `/site/{tape}/`, so relative links resolve |
| `/site/{tape}/` or any path ending in `/` | serves that directory's `index.html`                |
| a path with no matching object            | serves the site's `404.html` with status `404`      |
| any path with `?download=1`               | forces a download instead of rendering              |

Single-page apps can opt into serving `index.html` for unknown paths instead of the 404 page, per site through `_site.json` below, or gateway-wide with the operator's `site.spa_fallback` setting (off by default).

## Custom domains and subdomains

A gateway can serve sites by hostname, from the domain root, with no path prefix. Three forms:

```yaml theme={null}
gateway:
  site:
    # exact hostnames -> tape address, operator managed
    domains:
      example.com: "7f3kD9mQxYz2pW8vN4cRtE5uH6bJamd9GvKq2rTeUWXs"
    # every <subdomain-label>.sites.example.net serves that tape
    subdomain_suffix: "sites.example.net"
    # or fully self-serve: serve any domain whose DNS proves a binding
    txt_domains: true
```

With `txt_domains` on, any domain owner can attach their domain with two DNS records and no operator involvement:

```text theme={null}
example.com          A      <gateway address>     (or a proxied CNAME)
_tape.example.com    TXT    "<tape address>"
```

The gateway resolves the TXT record with DNSSEC validation, honors its TTL (clamped between a minute and an hour), and caches misses briefly. Removing the record detaches the domain on the same schedule. For `subdomain_suffix`, wildcard DNS plus a wildcard certificate gives every tape a hostname with zero per-site setup; the subdomain label is the lowercase encoding printed by `tape info` and included in the URL `tape deploy` prints, because hostnames are case-insensitive while tape addresses are not.

Host-served sites each get their own browser origin, which is the real tenant isolation story: cookies and storage on one site are invisible to every other. The path-based `/site/` route shares the gateway's origin across sites, fine for previews and internal use.

## Site settings on the tape

A site can carry its own serving settings as a reserved object named `_site.json` at the tape root, synced like any other file:

```json theme={null}
{
  "spa_fallback": true,
  "max_age_secs": 300,
  "cors_origins": ["https://app.example.com"],
  "connect_origins": ["https://api.devnet.solana.com", "wss://api.devnet.solana.com"]
}
```

Every field is optional and falls back to the gateway-wide setting. Unknown fields are ignored, so older gateways serve newer sites. The settings travel with the site: deploy to any gateway and your dapp's RPC allowlist comes along. Operators keep the last word with `site.tenant_overrides: false`, which ignores per-site settings entirely.

## Caching

Names are pointers that can be re-aimed at new tracks, so site responses are cached briefly and revalidated: `Cache-Control: public, max-age=60, must-revalidate` with the content commitment as ETag, answering `304 Not Modified` to `If-None-Match`. A redeploy propagates within a minute, and unchanged files revalidate without re-downloading.

## Fronting with a CDN

Any pull-through CDN works, and the general guidance in [serving through a CDN](/tools/cdn) applies. For Cloudflare in front of a custom-domain site, the operator side:

1. Add the site hostname to `gateway.site.domains` and CNAME it to the gateway, proxied.
2. Trust Cloudflare's edges in gateway config so per-visitor metering follows the real client IP. `trusted_proxies` accepts CIDR ranges:

```yaml theme={null}
gateway:
  metering:
    trusted_proxies: ["173.245.48.0/20", "2400:cb00::/32"]
```

3. Nothing else. The gateway's revalidation headers mean the edge re-checks every minute with a cheap `304` instead of re-downloading, and `Range` requests pass through.

Without host mapping, a single Cloudflare Transform Rule rewriting `/*` to `/site/<tape>/$1` serves a path-based site the same way.

## Cross-origin reads

When another origin needs to fetch site content (an app embedding hosted assets), allow it explicitly:

```yaml theme={null}
gateway:
  site:
    cors_origins: ["https://app.example.com"]   # or ["*"]
```

Matching responses carry `Access-Control-Allow-Origin`. With no entries, site content is same-origin only.

## Calling APIs from hosted pages

A dapp needs its RPC endpoint, and by default the content security policy keeps hosted pages same-origin, so browser calls to external APIs are blocked. Allow endpoints explicitly:

```yaml theme={null}
gateway:
  site:
    connect_origins:
      - "https://api.devnet.solana.com"
      - "wss://api.devnet.solana.com"   # list the websocket form too
```

Entries are added to the content security policy's `connect-src`, so hosted pages can `fetch` and open websockets to them; a single `"*"` entry allows any endpoint. Wallet extensions inject their own providers and are unaffected either way. With this set, a statically exported dapp (for example a Next.js `output: 'export'` build published with `tape deploy`) runs entirely from the tape.

## Boundaries

Site responses carry `X-Content-Type-Options: nosniff` and a same-origin content security policy, and the gateway sets no cookies. Site reads are metered separately from object reads, with headroom sized for multi-asset page loads ([rate limits](/apis/rate-limits)). Route details live in the [gateway API reference](/apis/gateway/site/serve-site).
