Skip to main content

Host a Webpage

There are two primary methods most developers should use for uploading a website to Swarm - swarm-cli and bee-js. Depending on the specific use case, it may make more sense to pick one or the other.

For a simple website such as a personal blog or company page, using swarm-cli is simplest and fastest way to get your site uploaded and accessible on Swarm.

However for developers who need finer grained control over the process or who wish to build a more complex application which require programmatically spinning up new pages, bee-js is required.

tip

The guides below assume you already have a registered ENS domain name. By using an ENS domain name, you can make your Swarm hosted website accessible through an easy to remember human-readable name rather than a Swarm hash. If you don't have an ENS domain name registered, you can get one using the official ENS application at app.ens.domains. Refer to their support section for a step-by-step guide to register an ENS domain.

FIX FOR ENS NOT WORKING ON LOCALHOST

If the site doesn’t load from localhost, it’s probably an with the resolver RPC (the RPC endpoint for the Ethereum node used to resolve your ENS domain name).

Some endpoints, such as:

https://cloudflare-eth.com

may not resolve properly on localhost.

As of the writing of this guide, both of these free and public endpoints work reliably for localhost resolution:

https://mainnet.infura.io/v3/<infura-api-key>
https://eth-mainnet.public.blastapi.io

Alternatively, you can run your own Ethereum node and use that as the RPC.

Host a Site With swarm-cli

This guide shows you how to get your website hosted on Swarm with just a few simple commands by using swarm-cli from your terminal.

Prerequisites

Upload & Access by Hash

You can download the example website files from the ethersphere/examples repository.

Uploading the Website

  1. Go to the folder containing your website files.

The example website files look like this:

my-website/
├── index.html # main landing page
├── 404.html # custom error page
├── styles.css # basic styling
├── script.js # optional script
├── favicon.svg # site icon
└── robots.txt # default robots config
  • index.html will be served by default when users visit the root URL.
  • 404.html will be served for non-existent paths.
  • The other files are optional and can be customized.
  1. Run:
swarm-cli upload . `
--stamp <BATCH_ID> `
--index-document index.html `
--error-document 404.html
  • Replace <BATCH_ID> with your postage batch ID.
  • --index-document tells Bee which file to serve at the root.
  • --error-document defines the fallback file for missing paths.
  1. The upload will return a Swarm reference hash, for example:
cf50756e6115445fd283691673fa4ad2204849558a6f3b3f4e632440f1c3ab7c

Copy this and save it. You’ll need it for both direct access and ENS integration.

Accessing the Website

Anyone with a Bee node can now access the site using the Swarm hash you just saved:

http://localhost:1633/bzz/<SWARM_HASH>/

If you have not already connected your site to your ENS domain, do that now before returning here.

If you have an ENS domain and Swarm hosted website, you can make the site available through the domain by registering website's Swarm hash as a content hash through the ENS domain management app. However, if you ever edit and reupload your site to Swarm, you will need to re-register your new website hash to make it available at your ENS domain.

Therefore, instead of directly using your website hash as the content hash for your ENS domain, upload your site as a feed update and use the feed manifest hash as the content hash. Then every time you update your site as a new feed update, the ENS domain will always resolve to the newest version of your site without the need to register a new hash each time.

tip

The examples below refer to core feed concepts such as "publisher identity", and "topic". To learn more about these concepts refer to the bee-js documentation.

In this section, you will:

  1. Create a publisher identity
  2. Upload your site to a feed (this automatically creates the feed manifest)
  3. Copy the feed manifest reference
  4. Use that manifest reference as your ENS contenthash

Step 1: Create a dedicated publisher identity

This key will sign feed updates.

swarm-cli identity create website-publisher

Terminal output:

Name: website-publisher
Type: V3 Wallet
Private key: 0x22e918ef68c9bc975112ceaaee0ee0f147baa79da257873659bddbfd84a646fe
Public key: 0x218c79f8dfb26d077b6379eb56aa9c6e71edf74dde8ecd27dac5016528aea80ee121b9e5050adf3948c8b0d8cffda763d7fb1f5608250b5009c5d50e158ab4a5
Address: 0x2fb11d37a9913bd3258b9918c399f35fd842a232

Record the output in a secure location as a backup — you will need this identity for future updates.

If you need to view/export it later:

swarm-cli identity export website-publisher

Step 2: Upload your website to a feed (creates the manifest automatically)

swarm-cli feed upload ./website \
--identity website-publisher \
--topic-string website \
--stamp <BATCH_ID> \
--index-document index.html \
--error-document 404.html

You will see output that includes your feed manifest reference, for example:

Swarm hash: 387dc3cf98419dcb20c68b284373bf7d9e8dcb27daadb67e1e6b6e0f17017f1f
URL: http://localhost:1633/bzz/387dc3cf98419dcb20c68b284373bf7d9e8dcb27daadb67e1e6b6e0f17017f1f/
Feed Manifest URL: http://localhost:1633/bzz/6c30ef2254ac15658959cb18dd123bcce7c16d06fa7d0d4550a1ee87b0a846a2/
Stamp ID: 3d98a22f
Usage: 50%
Capacity (mutable): 20.445 KB remaining out of 40.890 KB

You can find the manifest hash at Feed Manifest URL in the URL right after /bzz/: 6c30ef2254ac15658959cb18dd123bcce7c16d06fa7d0d4550a1ee87b0a846a2

Save this hash, you will use it for the next step.

This is your permanent website reference. It is a reference to a feed manifest which points to the latest feed entry so that you can use it as a static, unchanging reference for your website even as you make multiple updates to the site. Every time you update the website through the feed, this manifest will point to the hash for the newest version of the website.

Step 3: Use the feed reference as the ENS contenthash

Follow the official ENS guide for registering a content hash adding your content hash in the ENS UI (see guide). However, rather than registering your website's hash directly, register the feed manifest hash we saved from the previous step from our example above.

Example:

bzz://6c30ef2254ac15658959cb18dd123bcce7c16d06fa7d0d4550a1ee87b0a846a2

Now your ENS name will always point to a static reference which will always resolve to the latest version of your website.

Updating your site in the future

When you have a new version of your site, just run feed upload again using the same topic and identity:

swarm-cli feed upload ./website \
--identity website-publisher \
--topic-string website \
--stamp <BATCH_ID> \
--index-document index.html \
--error-document 404.html
  • The feed manifest reference stays the same.
  • The feed now points to the newly uploaded site version.
  • No ENS changes needed.

Host a Website with bee-js

This guide explains how to host a website on Swarm using the bee-js JavaScript SDK instead of the CLI.

For developers building apps, tools, or automated deployments, bee-js offers programmatic control over uploading and updating content on Swarm.

Prerequisites

Upload and Access by Hash

Install bee-js:

npm install @ethersphere/bee-js

Website upload script:

import { Bee } from "@ethersphere/bee-js";

const bee = new Bee("http://localhost:1633");

const batchId = "<BATCH_ID>"; // Replace with your actual postage batch ID

const result = await bee.uploadFilesFromDirectory(batchId, "./website", {
indexDocument: "index.html",
errorDocument: "404.html"
});

console.log("Swarm hash:", result.reference.toHex());
Swarm hash: 6c45eae389b3bffce21443316d0bd47c4101545092b7c72c313a33ee7d003475

After running the script, copy the Swarm hash output to the console and then use it to open your Swarm hosted website in the browser:

http://localhost:1633/bzz/<SWARM_HASH>/

If you have not already connected your site to your ENS domain, do that now before returning here.

If you have an ENS domain and Swarm hosted website, you can make the site available through the domain by registering website's Swarm hash as a content hash through the ENS domain management app. However, if you ever edit and reupload your site to Swarm, you will need to re-register your new website hash to make it available at your ENS domain.

Therefore, instead of directly using your website hash as the content hash for your ENS domain, upload your site as a feed update and use the feed manifest hash as the content hash. Then every time you update your site as a new feed update, the ENS domain will always resolve to the newest version of your site without the need to register a new hash each time.

tip

You will need a publisher key to use for setting up your website feed.

You can use the PrivateKey class to generate a dedicated publisher key:

const crypto = require('crypto');
const { PrivateKey } = require('@ethersphere/bee-js');

// Generate 32 random bytes and construct a private key
const hexKey = '0x' + crypto.randomBytes(32).toString('hex');
const privateKey = new PrivateKey(hexKey);

console.log('Private key:', privateKey.toHex());
console.log('Public address:', privateKey.publicKey().address().toHex());

Example output:

Private key: 634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd
Public address: 8d3766440f0d7b949a5e32995d09619a7f86e632

Store this key securely.

Anyone with access to it can publish to your feed.

It is recommended to use a separate publishing key for each feed.

Example Script

tip

The script below refers to some core feed concepts such as the feed "topic" and "writer". To learn more about these concepts and feeds in general, refer to the bee-js documentation.

The script performs these steps:

  1. Connects to your Bee node and loads your postage batch + publisher private key.
  2. Creates a feed topic and writer for publishing website updates.
  3. Uploads the ./website directory to Swarm and logs the resulting content hash.
  4. Publishes that hash to the feed so it becomes the latest feed entry.
  5. Creates a feed manifest and logs its reference — this is the permanent hash you use for ENS or stable URLs.
import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js";
const bee = new Bee("http://localhost:1633");
const batchId = "<BATCH_ID>" // Replace with your batch id
const privateKey = new PrivateKey("<PUBLISHER_KEY>"); // Replace with your publisher private key
const owner = privateKey.publicKey().address();

// Upload and Create Feed Manifest

const topic = Topic.fromString("website");
const writer = bee.makeFeedWriter(topic, privateKey);

const upload = await bee.uploadFilesFromDirectory(batchId, "./website", {
indexDocument: "index.html",
errorDocument: "404.html"
});

console.log("Website Swarm Hash:", upload.reference.toHex())

await writer.uploadReference(batchId, upload.reference);

const manifestRef = await bee.createFeedManifest(batchId, topic, owner);
console.log("Feed Manifest:", manifestRef.toHex());

Upon the successful execution of the script, the hash of the uploaded website will be logged along feed manifest hash. Copy the "Feed Manifest" hash to be used in the next step:

Website Swarm Hash: 6c45eae389b3bffce21443316d0bd47c4101545092b7c72c313a33ee7d003475
Feed Manifest: caa414d70028d14b0bdd9cbab18d1c1a0a3bab1b20a56cf06937a6b20c7e7377

Follow the official ENS guide for registering a content hash adding your content hash in the ENS UI (see guide). However, rather than registering your website's hash directly, register the feed manifest hash we saved from the previous step from our example above.

bzz://<manifestRef>

Future updates just re-run:

await writer.upload(batchId, newUpload.reference);

Your ENS domain will always point to the latest upload via the feed manifest.

You’ve now got a programmatic way to deploy and update your Swarm-hosted site with ENS support using bee-js!

Connect Site to ENS Domain

Once your site is uploaded to Swarm, you can make it accessible via an easy to remember ENS domain name rather than its Swarm hash:

https://yourname.eth.limo/
https://yourname.bzz.link/

or through your own node:

http://localhost:1633/bzz/yourname.eth/

Using the Official ENS Guide

ENS provides a clear walkthrough with screenshots showing how to add a content hash to your domain with their easy to use app:

How to add a Decentralized website to an ENS name

The guide covers:

  • Opening your ENS domain in the ENS Manager
  • Navigating to the Records tab
  • Adding a Content Hash
  • Confirming the transaction

Swarm-Specific Step

When you reach Step 2 in the ENS guide (“Add content hash record”), enter your Swarm reference in the following format:

tip

For the content hash, you can use a Swarm hosted website's hash directly, or as is recommended in the swarm-cli and bee-js guides above, publish your site to a feed and use the feed manifest hash instead. By using a feed manifest as the content hash, you can avoid repeated ENS registry updates.

bzz://<SWARM_HASH>

Example:

bzz://cf50756e6115445fd283691673fa4ad2204849558a6f3b3f4e632440f1c3ab7c

This works across:

  • eth.limo and bzz.link
  • localhost (with a compatible RPC)
  • any ENS-compatible Swarm resolver

You do not need to encode the hash or use any additional tools. bzz://<hash> is sufficient.

Client-Side Routing

This section explains how to add hash based client side routing to your Swarm hosted site so that you can have clean URLs for each page of your website.

Why Hash Based Client Side Routing?

Swarm does not behave like a traditional web server — there is no server-side routing, and every route must correspond to a real file inside the site manifest. If you try to use typical "clean URLs" like:

/about
/contact
/dashboard/settings

Swarm will look for literal files such as:

about
contact
dashboard/settings

...which obviously don’t exist unless you manually manipulate the manifest. This is theoretically possible, but is tricky and complex to do manually, and there is currently not (yet) any tooling to make it easier.

How to Add Routing

If you want multiple pages on a Swarm-hosted website, you should use a client-side router. Swarm has no server backend running code and so can’t rewrite paths, so we use React Router’s HashRouter, which keeps all routing inside the browser.

Below is the simplest way to set this up using create-swarm-app and then adding your own pages.

1. Create a New Vite + React Project (with create-swarm-app)

Run:

npm init swarm-app@latest my-dapp-new vite-tsx

This generates a clean project containing:

src/
App.tsx
index.tsx
config.ts
public/
index.html
package.json

You now have a fully working Vite/React app ready for Swarm uploads.

2. Install React Router

Inside the project:

npm install react-router-dom

This gives you client-side navigation capability.

3. Switch the App to Use Hash-Based Routing

Swarm only serves literal files, so /#/about is the only reliable way to have “pages.”

Replace your App.tsx with:

import { HashRouter, Routes, Route, Link } from 'react-router-dom'
import { Home } from './Home'
import { About } from './About'
import { NotFound } from './NotFound'

export function App() {
return (
<HashRouter>
<nav style={{ display: 'flex', gap: '12px', padding: '12px' }}>
<Link to="/">Home NEW</Link>
<Link to="/about">About</Link>
</nav>

<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</HashRouter>
)
}

This gives you usable routes:

/#/         → Home
/#/about → About
/#/anything → React 404 page

4. Add Your Page Components

Example Home.tsx:

export function Home() {
return (
<div style={{ padding: '20px' }}>
<h1>Home</h1>
<p>Welcome to your Swarm-powered app.</p>
</div>
)
}

Example About.tsx:

export function About() {
return (
<div style={{ padding: '20px' }}>
<h1>About</h1>
<p>This demo shows how to upload files or directories to Swarm using Bee-JS.</p>
</div>
)
}

Example NotFound.tsx:

export function NotFound() {
return (
<div style={{ padding: '20px' }}>
<h1>Page Not Found</h1>
<a href="./#/">Return to Home</a>
</div>
)
}

5. Add a Static 404.html for Non-Hash URLs

Swarm still needs a fallback for URLs like:

/non-existent-file

Create public/404.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>404 – Not Found</title>
<style>
body { font-family: sans-serif; padding: 40px; }
a { color: #007bff; }
</style>
</head>

<nav style="display: flex; gap: 12px; padding: 12px">
<a href="./#/">Home</a>
<a href="./#/about">About</a>
</nav>

<body>
<h1>404</h1>
<p>This page doesn't exist.</p>
<p><a href="./#/">Return to Home</a></p>
</body>
</html>

Vite will automatically include this in dist/.

This file handles non-hash missing paths. React handles hash missing paths.

6. Build the Project

Before uploading, compile the Vite app into a static bundle:

npm run build

This produces a dist/ folder containing:

dist/
index.html
404.html
assets/

Everything inside dist/ will be uploaded to your Swarm feed.

7. Create a Publisher Identity and Deploy Using a Feed Manifest

For stable URLs, use a feed manifest reference. This gives you a permanent Swarm URL that always resolves to the latest version of your content.

Create an identity (if you don’t have one yet):

swarm-cli identity create web-publisher

Upload your built site to the feed:

swarm-cli feed upload ./dist \
--identity web-publisher \
--topic-string website \
--stamp <BATCH_ID> \
--index-document index.html \
--error-document 404.html

The output includes:

  • the content hash
  • the feed manifest URL → this is your permanent website URL
  • stamp usage details

Example:

Feed Manifest URL:
http://localhost:1633/bzz/<feed-manifest-hash>/

This URL never changes, even when you update your site.

8. Visit Your Site

  • Home: /#/

  • About: /#/about

  • Invalid hash route: handled by NotFound.tsx

  • Invalid non-hash route: handled by 404.html

Summary

You now have:

  • A Vite + React app
  • Hash-based routing fully compatible with Swarm
  • A static 404 for non-hash paths
  • A React 404 for invalid hash paths
  • Stable, versioned deployments using feed manifests