From 341d10a4c8329f133b3daf7ce78b8f54ec1b2ea4 Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Mon, 17 Nov 2025 13:49:37 +0700 Subject: [PATCH 1/8] article stub --- docs/develop/mutable-content.md | 271 ++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 docs/develop/mutable-content.md diff --git a/docs/develop/mutable-content.md b/docs/develop/mutable-content.md new file mode 100644 index 00000000..99802fe1 --- /dev/null +++ b/docs/develop/mutable-content.md @@ -0,0 +1,271 @@ +--- +title: "Mutable" Content +id: mutable-content +sidebar_label: "Mutable" Content +--- + + +:::information +While *all data uploaded to Swarm is technically immutable*, mutable functionality can be simulated using feeds. A feed is essentially an ordered series of Swarm uploads (feed updates) which always resolve to the latest update. We can then use a feed manifest to create a static reference to the feed, so that the reference will always resolve to the latest feed update. In this way mutable functionality is emulated on top of Swarm's immutable [DISC](/docs/concepts/DISC/). +::: + +# Granular Asset Feeds (Advanced Website Architecture) + +This guide extends the core website-hosting workflow by showing how to create **per-asset feeds** so you can update individual files (header images, CSS, JS bundles, logos, site config, etc.) without re-uploading the entire website. + +This technique effectively turns Swarm into a **decentralized CDN**: +each asset gets its own permanent reference, and updates flow atomically through feeds. + +## 🧠 Why Use Per-Asset Feeds? + +Traditional upload-and-replace workflows require re-deploying everything, even if only one asset changes. With Swarm feeds: + +- Every file can have its own independent "channel" (feed topic). +- Each feed has a **stable manifest hash** that never changes. +- When you update an asset, only that feed is updated. +- Your website fetches each asset by its feed manifest, not by static hash. + +This gives you: + +- Faster updates +- Zero ENS updates +- Atomic, no-downtime asset changes +- Modular deployments +- A CDN-like architecture, but decentralized + +--- + +## 🧩 Architecture Overview + +For a typical site: + +| Asset | Feed Topic | Purpose | +|----------------|-------------------|---------| +| Main website | `website` | Full site HTML/JS/CSS bundle | +| Header image | `header-image` | Frequently changed graphic | +| CSS theme | `main-css` | Style updates | +| JS bundle | `main-js` | Client logic | +| Config JSON | `site-config` | Dynamic data | + +Each of these gets: + +- its own private key +- its own topic +- its own feed manifest +- its own permanent URL + +--- + +## πŸ” Generate Publisher Keys + +Every feed should have its own publishing key. + +```js +import crypto from "crypto"; +import { PrivateKey } from "@ethersphere/bee-js"; + +const hexKey = '0x' + crypto.randomBytes(32).toString('hex'); +const pk = new PrivateKey(hexKey); + +console.log("Private key:", pk.toHex()); +console.log("Address:", pk.publicKey().address().toHex()); +``` + +--- + +## πŸ› οΈ Create a Feed Per Asset + +Example: header image. + +```js +import { Bee, PrivateKey, Topic } from "@ethersphere/bee-js"; + +const bee = new Bee("http://localhost:1633"); +const batchId = ""; + +const pk = new PrivateKey(""); +const topic = Topic.fromString("header-image"); +const owner = pk.publicKey().address(); + +const writer = bee.makeFeedWriter(topic, pk); +``` + +--- + +## πŸ“€ Upload Asset + Publish Feed Update + +```js +const upload = await bee.uploadFile(batchId, "./assets/header.jpg"); +await writer.uploadReference(batchId, upload.reference); + +const manifest = await bee.createFeedManifest(batchId, topic, owner); +console.log("Header Manifest:", manifest.toHex()); +``` + +Stable URL: + +``` +bzz:/// +``` + +--- + +## πŸ” Updating the Asset + +```js +const newUpload = await bee.uploadFile(batchId, "./assets/header-new.jpg"); +await writer.uploadReference(batchId, newUpload.reference); +``` + +Manifest stays the same. + +--- + +## πŸ”— Reference Asset Feeds in HTML + +```html +Header + + +``` + +--- + +## βš™οΈ ENS Integration + +Optional: map each feed to ENS. + +``` +header.mysite.eth β†’ bzz:// +``` + +--- + +# Visual Diagram + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Website β”‚ + β”‚ (HTML references feeds) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + Uses feed manifests instead of static hashes + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Main Site β”‚ β”‚ Header Image β”‚ β”‚ CSS Stylesheet β”‚ +β”‚ Feed β”‚ β”‚ Feed β”‚ β”‚ Feed β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ +Updates only when Updates only when Updates only when +full site changes header changes CSS changes + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Feed Entry β”‚ β”‚ Feed Entry β”‚ β”‚ Feed Entry β”‚ +β”‚ Latest Hash β”‚ β”‚ Latest Hash β”‚ β”‚ Latest Hash β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Feed Manifestβ”‚ β”‚ Feed Manifest β”‚ β”‚ Feed Manifest β”‚ +β”‚ (Stable URL) β”‚ β”‚ (Stable URL) β”‚ β”‚ (Stable URL) β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό + ENS Contenthash ENS Subdomain ENS Subdomain + e.g. mysite.eth header.mysite.eth css.mysite.eth +``` + +--- + +# Template Website Referencing Feeds + +`index.html`: + +```html + + + + + + + + +
+ Header +
+ +
+

Welcome to My Swarm-Powered Site

+
+ +
+ Loaded config: + +
+ + +``` + +`app.js`: + +```js +async function loadConfig() { + const url = "bzz:///"; + const res = await fetch(url); + const json = await res.json(); + + document.getElementById("config-output").innerText = JSON.stringify(json); +} +loadConfig(); +``` + +`styles.css`: + +```css +body { + font-family: sans-serif; + padding: 2rem; +} +header img { + width: 100%; + max-height: 300px; + object-fit: cover; +} +``` + +--- + +# Decentralized CDN Architecture for Bee-JS + +Swarm enables a CDN-like architecture where each asset is versioned and independently updatable via feeds. + +### Advantages + +- Atomic updates +- Zero downtime +- Scalable +- ENS-friendly +- Secure per-asset keys + +### Pattern + +```js +const upload = await bee.uploadFile(batchId, "./file"); +await writer.uploadReference(batchId, upload.reference); +``` + +Updates later: + +```js +await writer.uploadReference(batchId, newUpload.reference); +``` + +Manifest stays constant. + +--- + +You're ready to build modular, feed-powered sites on Swarm! + From 3f532a1a565292a4284aecc896aedb105c47f7b2 Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Mon, 17 Nov 2025 16:56:24 +0700 Subject: [PATCH 2/8] add section about routing to website hosting page and draft of dynamic content page --- ...{mutable-content.md => dynamic-content.md} | 126 ++----- docs/develop/host-your-website.md | 314 +++++++++++++++++- docs/develop/introduction.md | 9 +- docs/develop/tools-and-features/feeds.md | 10 +- docusaurus.config.js | 4 - sidebars.js | 1 + 6 files changed, 347 insertions(+), 117 deletions(-) rename docs/develop/{mutable-content.md => dynamic-content.md} (70%) diff --git a/docs/develop/mutable-content.md b/docs/develop/dynamic-content.md similarity index 70% rename from docs/develop/mutable-content.md rename to docs/develop/dynamic-content.md index 99802fe1..fd874c86 100644 --- a/docs/develop/mutable-content.md +++ b/docs/develop/dynamic-content.md @@ -1,41 +1,42 @@ --- -title: "Mutable" Content -id: mutable-content -sidebar_label: "Mutable" Content +title: Dynamic Content +id: dynamic-content +sidebar_label: Dynamic Content --- -:::information -While *all data uploaded to Swarm is technically immutable*, mutable functionality can be simulated using feeds. A feed is essentially an ordered series of Swarm uploads (feed updates) which always resolve to the latest update. We can then use a feed manifest to create a static reference to the feed, so that the reference will always resolve to the latest feed update. In this way mutable functionality is emulated on top of Swarm's immutable [DISC](/docs/concepts/DISC/). +## Using Feeds for Dynamic Content + +:::info +Although all data on Swarm is immutable, feeds provide an updatable reference that enables dynamic content, simulating a mutable resource which always resolves to its latest update through a static feed manifest reference. +::: + +:::tip +This guide relies heavily on the use of Swarm feeds. If you are not already familiar with feeds, it's recommended to familiarize yourself with them. See the [bee-docs feeds section](/docs/develop/tools-and-features/feeds/) for a high level overview and then check the [bee-js-docs feeds section](https://bee-js.ethswarm.org/docs/soc-and-feeds/#feeds) for a more detailed explanation and example implementation. ::: -# Granular Asset Feeds (Advanced Website Architecture) -This guide extends the core website-hosting workflow by showing how to create **per-asset feeds** so you can update individual files (header images, CSS, JS bundles, logos, site config, etc.) without re-uploading the entire website. +This guide shows how to create **per-asset feeds** so you can update individual files (header images, CSS, JS bundles, logos, site config, etc.) without re-uploading the entire website. This technique effectively turns Swarm into a **decentralized CDN**: each asset gets its own permanent reference, and updates flow atomically through feeds. -## 🧠 Why Use Per-Asset Feeds? +### Why Use Per-Asset Feeds? Traditional upload-and-replace workflows require re-deploying everything, even if only one asset changes. With Swarm feeds: - Every file can have its own independent "channel" (feed topic). - Each feed has a **stable manifest hash** that never changes. -- When you update an asset, only that feed is updated. -- Your website fetches each asset by its feed manifest, not by static hash. +- When you update an asset, only that feed is updated. This gives you: -- Faster updates +- Faster and smaller updates - Zero ENS updates -- Atomic, no-downtime asset changes -- Modular deployments -- A CDN-like architecture, but decentralized - ---- +- No-downtime asset changes +- A decentralized CDN-like architecture -## 🧩 Architecture Overview +### Architecture Overview For a typical site: @@ -54,9 +55,8 @@ Each of these gets: - its own feed manifest - its own permanent URL ---- -## πŸ” Generate Publisher Keys +### Generate Publisher Keys Every feed should have its own publishing key. @@ -71,9 +71,7 @@ console.log("Private key:", pk.toHex()); console.log("Address:", pk.publicKey().address().toHex()); ``` ---- - -## πŸ› οΈ Create a Feed Per Asset +### Create a Feed Per Asset Example: header image. @@ -90,9 +88,7 @@ const owner = pk.publicKey().address(); const writer = bee.makeFeedWriter(topic, pk); ``` ---- - -## πŸ“€ Upload Asset + Publish Feed Update +### Upload Asset + Publish Feed Update ```js const upload = await bee.uploadFile(batchId, "./assets/header.jpg"); @@ -108,9 +104,8 @@ Stable URL: bzz:/// ``` ---- -## πŸ” Updating the Asset +### Updating the Asset ```js const newUpload = await bee.uploadFile(batchId, "./assets/header-new.jpg"); @@ -119,9 +114,8 @@ await writer.uploadReference(batchId, newUpload.reference); Manifest stays the same. ---- -## πŸ”— Reference Asset Feeds in HTML +### Reference Asset Feeds in HTML ```html Header @@ -129,9 +123,8 @@ Manifest stays the same. ``` ---- -## βš™οΈ ENS Integration +## ENS Integration Optional: map each feed to ENS. @@ -139,9 +132,7 @@ Optional: map each feed to ENS. header.mysite.eth β†’ bzz:// ``` ---- - -# Visual Diagram +### Visual Diagram ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” @@ -178,67 +169,8 @@ full site changes header changes CSS changes e.g. mysite.eth header.mysite.eth css.mysite.eth ``` ---- - -# Template Website Referencing Feeds - -`index.html`: - -```html - - - - - - - - -
- Header -
- -
-

Welcome to My Swarm-Powered Site

-
- -
- Loaded config: - -
- - -``` - -`app.js`: - -```js -async function loadConfig() { - const url = "bzz:///"; - const res = await fetch(url); - const json = await res.json(); - - document.getElementById("config-output").innerText = JSON.stringify(json); -} -loadConfig(); -``` - -`styles.css`: - -```css -body { - font-family: sans-serif; - padding: 2rem; -} -header img { - width: 100%; - max-height: 300px; - object-fit: cover; -} -``` - ---- -# Decentralized CDN Architecture for Bee-JS +## Decentralized CDN Architecture for Bee-JS Swarm enables a CDN-like architecture where each asset is versioned and independently updatable via feeds. @@ -263,9 +195,5 @@ Updates later: await writer.uploadReference(batchId, newUpload.reference); ``` -Manifest stays constant. - ---- - -You're ready to build modular, feed-powered sites on Swarm! +While the manifest stays constant. diff --git a/docs/develop/host-your-website.md b/docs/develop/host-your-website.md index a530b4b8..4a479915 100644 --- a/docs/develop/host-your-website.md +++ b/docs/develop/host-your-website.md @@ -478,4 +478,316 @@ This works across: * localhost (with a compatible RPC) * any ENS-compatible Swarm resolver -You do not need to encode the hash or use any additional tools. `bzz://` is sufficient. \ No newline at end of file +You do not need to encode the hash or use any additional tools. `bzz://` 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 error prone to do manually, and there is currently not (yet) any tooling to make it easier. + +### How to Add Routing + +The solution is to use client side hash based routing. The hash fragment (`/#/about`) is handled entirely on the client and never reaches Swarm, which solves the routing problem cleanly. + +We can do this easily using the convenient [`create-swarm-app` tool](https://www.npmjs.com/package/create-swarm-app) to generate an SPA website with a vite powered front end, and then add hash based client side routing. You can read more about using [`create-swarm-app` in the bee-js documentation](https://bee-js.ethswarm.org/docs/getting-started/#quickstart-with-create-swarm-app). + + +Below is the recommended setup: + + +#### 1. Start From the Vite Template (swarm-create-app) + +When you scaffold a new app using: + +``` +npm init swarm-app@latest my-dapp-new vite-tsx +``` + +...you get a minimal Vite + React + TypeScript project, like this: + +``` +src/ + index.tsx + App.tsx + config.ts +index.html +package.json +tsconfig.json +``` + +This runs smoothly on Swarm as a static site. + +Now we’ll extend it with routing. + +#### 2. Install React Router + +Inside your Vite project: + +```bash +npm install react-router-dom +``` + +That’s it β€” React Router works great in any SPA. + + +#### 3. Use a Hash Router (Required for Swarm) + +Since Swarm can only serve files that actually exist in the manifest, and we want to avoid any manipulation of the manifest, we we must use hash based routing which works entirely on the client side. + +By using `HashRouter` we get this functionality: + +* `/#/about` stays inside the browser +* Swarm only receives the file `index.html` +* React Router handles the rest + + +#### 4. Update `App.tsx` to Use Hash-Based Routing + +Replace the default `App.tsx` with this template for a basic routing setup: + +```tsx +import { HashRouter, Routes, Route, Link } from 'react-router-dom' +import { Home } from './Home' +import { About } from './About' + +export function App() { + return ( + + + + + } /> + } /> + + + ) +} +``` + +This turns your app into a proper SPA that is completely compatible with Swarm. + + +#### 5. Create Component Files for Each Route + +**`Home.tsx`:** + +```tsx +export function Home() { + return ( +
+

Home

+

Welcome to your Swarm-powered app.

+

You can generate postage batches, upload content, and get permanent Swarm references.

+

Routing is handled entirely client-side using HashRouter.

+
+ ) +} +``` + +**`About.tsx`:** + +```tsx +export function About() { + return ( +
+

About This App

+

This demo shows how to upload files or directories to Swarm using Bee-JS.

+

You can generate a postage batch, upload content, and host it as a decentralized website.

+

Hash-based routing ensures all routes resolve correctly on Swarm.

+
+ ) +} +``` + +These files can live inside `src/` or `src/pages/` β€” up to you. + +#### 6. Handling 404 Responses (Hash and Non-Hash Routes) + +Swarm will return your `404.html` file when a user requests a **file that doesn’t exist in the manifest**, such as: + +``` +/this-path-does-not-exist +``` + +But because hash-based routing happens fully in the browser, invalid hash routes like: + +``` +#/does/not/exist +``` + +never hit Swarm β€” they must be handled inside your React app. + +The following setup covers both cases: + + + +**1. Add a Static `404.html` for Non-Hash Requests** + +Create a `404.html` inside `public/` so Vite copies it into `dist/`: + +```html + + + + + 404 – Not Found + + + + + + +

404

+

This page doesn't exist.

+

Return to Home

+ + +``` + +Swarm will show this page automatically for missing non-hash paths. + + +**2. Add a Catch-All Route for Invalid Hash URLs** + +React Router handles broken hash routes internally. +Include a simple `NotFound` component: + +**`NotFound.tsx`:** + +```tsx +export function NotFound() { + return ( +
+

Page Not Found

+ Return to Home +
+ ) +} +``` + +Then add the catch-all route: + +**`App.tsx`:** + +```tsx +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 ( + + + + + } /> + } /> + } /> + + + ) +} +``` + + +**Summary:** + +* **`404.html`** handles missing non-hash routes requested directly from Swarm. +* **`NotFound.tsx`** handles missing hash routes inside your SPA. + +With this setup, users always see a consistent 404 response no matter how they land on an invalid path. + + +#### 7. Build & Upload Like Normal + +You don’t need any special build steps. Just run: + +```bash +npm run build +``` + +This generates a static bundle inside `dist/`, containing: + +* `index.html` +* `assets/` (your JS/CSS chunks) +* No extra files per route (because it’s an SPA) + +Then upload it using Bee-JS or Swarm-CLI: + + + + +```bash +swarm-cli feed upload ./dist \ + --identity website-publisher \ + --topic-string website \ + --stamp \ + --index-document index.html \ + --error-document 404.html +``` + + + + +```powershell +swarm-cli feed upload .\dist ` + --identity website-publisher ` + --topic-string website ` + --stamp 3d98a22f522377ae9cc2aa3bca7f352fb0ed6b16bad73f0246b0a5c155f367bc ` + --index-document index.html ` + --error-document 404.html +``` + + + + +Or from Bee-JS: + +```ts +bee.uploadFilesFromDirectory(batchId, './dist', { + indexDocument: 'index.html' +}) +``` + +Once deployed, your routes work like this: + +``` +/#/ β†’ Home +/#/about β†’ About +``` + +With this setup, no manifest changes are needed. Everything lives in a single HTML file, and routing is handled entirely client side. diff --git a/docs/develop/introduction.md b/docs/develop/introduction.md index ad9dce1f..e4f6778b 100644 --- a/docs/develop/introduction.md +++ b/docs/develop/introduction.md @@ -61,16 +61,17 @@ pagination_next: null Open guide - B(Blog Index Feed Entry
JSON List of Posts) + + B --> C1[Post #1 Feed Manifest
Stable URL] + B --> C2[Post #2 Feed Manifest
Stable URL] + B --> C3[Post #3 Feed Manifest
Stable URL] + + C1 --> D1A[Post #1 HTML Feed] + C1 --> D1B[Post #1 CSS Feed] + C1 --> D1C[Post #1 JS Feed] + C1 --> D1D[Post #1 Image Feeds] + + C2 --> D2A[Post #2 HTML Feed] + C2 --> D2B[Post #2 CSS Feed] + C2 --> D2C[Post #2 JS Feed] + C2 --> D2D[Post #2 Image Feeds] + + C3 --> D3A[Post #3 HTML Feed] + C3 --> D3B[Post #3 CSS Feed] + C3 --> D3C[Post #3 JS Feed] + C3 --> D3D[Post #3 Image Feeds] ``` - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Website β”‚ - β”‚ (HTML references feeds) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - Uses feed manifests instead of static hashes - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ β”‚ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Main Site β”‚ β”‚ Header Image β”‚ β”‚ CSS Stylesheet β”‚ -β”‚ Feed β”‚ β”‚ Feed β”‚ β”‚ Feed β”‚ -β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ -Updates only when Updates only when Updates only when -full site changes header changes CSS changes - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Feed Entry β”‚ β”‚ Feed Entry β”‚ β”‚ Feed Entry β”‚ -β”‚ Latest Hash β”‚ β”‚ Latest Hash β”‚ β”‚ Latest Hash β”‚ -β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Feed Manifestβ”‚ β”‚ Feed Manifest β”‚ β”‚ Feed Manifest β”‚ -β”‚ (Stable URL) β”‚ β”‚ (Stable URL) β”‚ β”‚ (Stable URL) β”‚ -β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό - ENS Contenthash ENS Subdomain ENS Subdomain - e.g. mysite.eth header.mysite.eth css.mysite.eth + +This is a **literal feed of feed manifests**, but described in a way that’s directly tied to the practical outcome: a fully dynamic, updatable blog where *every moving part can change independently*. + + +### Dynamic Blog Structure (Feed-Indexed Manifests) + +A dynamic blog is built by combining multiple levels of Swarm feeds: + +#### **1. Top-level: Blog Feed Manifest (your main website URL)** + +This is the stable URL you give to users. It loads your SPA (React + hash routing). +Your frontend then fetches the blog index. + +#### **2. Blog Index Feed (list of posts)** + +This feed contains a JSON array of **feed manifest addresses**, one per post. + +Example `posts.json` stored inside the index feed: + +```json +{ + "posts": [ + "0xPOST1_MANIFEST", + "0xPOST2_MANIFEST", + "0xPOST3_MANIFEST" + ] +} ``` +Update this feed whenever you add or remove posts. -## Decentralized CDN Architecture for Bee-JS +#### **3. Blog Post Manifests (one per post)** -Swarm enables a CDN-like architecture where each asset is versioned and independently updatable via feeds. +Each post gets its own feed manifest which always points to its latest version. -### Advantages +Each post contains: -- Atomic updates -- Zero downtime -- Scalable -- ENS-friendly -- Secure per-asset keys +* the post HTML (also a feed) +* post-specific CSS (feed) +* post-specific JS (feed) +* post images (feeds) +* metadata JSON (feed) -### Pattern +Your structure might look like: -```js -const upload = await bee.uploadFile(batchId, "./file"); -await writer.uploadReference(batchId, upload.reference); +``` +post-1/ + post.html β†’ feed + style.css β†’ feed + main.js β†’ feed + cover.jpg β†’ feed + metadata.json β†’ feed ``` -Updates later: +All bundled into **one feed manifest for the post**, so the post has a single stable URL. + +#### **4. Optional: ENS Integration** + +Each post can also have an ENS subdomain if desired: -```js -await writer.uploadReference(batchId, newUpload.reference); ``` +post1.blog.eth β†’ post 1 feed manifest +post2.blog.eth β†’ post 2 feed manifest +``` + + +### How the Frontend Uses These Feeds + +Inside your React app (using [`HashRouter`](/docs/develop/host-your-website#client-side-routing)), you would: + +1. Load the **post index feed** +2. Display the list of posts +3. When the user navigates to `/#/post/`, load that post’s feed manifest +4. Render its HTML + assets + +This gives you: + +* **fully dynamic content** +* posts you can update individually +* a blog you can expand infinitely +* stable URLs for *every* level of content +* no need to reupload the main site for a new post + + +### In Summary + +> A dynamic blog is built using a *feed-indexed set of page manifests*: +> one feed acts as the post index, and each post has its own feed manifest that points to its latest version. Inside each post, the HTML, CSS, JS, images, and metadata are each stored in their own feeds so everything can update independently. -While the manifest stays constant. From 01d564f93ba15c72f07fc5253d9f543cd48ca0be Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Wed, 19 Nov 2025 18:10:17 +0700 Subject: [PATCH 5/8] edits --- docs/develop/dynamic-content.md | 70 +++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/docs/develop/dynamic-content.md b/docs/develop/dynamic-content.md index fc64846c..d83989ea 100644 --- a/docs/develop/dynamic-content.md +++ b/docs/develop/dynamic-content.md @@ -5,7 +5,7 @@ sidebar_label: Dynamic Content --- -## Using Feeds for Dynamic Content (Single Page Site) +## Feeds for Dynamic Content :::info Although all data on Swarm is immutable, feeds provide an updatable reference that enables dynamic content, simulating a mutable resource which always resolves to its latest update through a static feed manifest reference. @@ -32,7 +32,6 @@ Traditional upload-and-replace workflows require re-deploying everything, even i This gives you: - Faster and smaller updates -- Zero ENS updates - No-downtime asset changes - A decentralized CDN-like architecture @@ -53,12 +52,12 @@ Each of these gets: - its own private key - its own topic - its own feed manifest -- its own permanent URL +- its own permanent URL (using the static reference from the feed manifest) ### Generate Publisher Keys -Every feed should have its own publishing key. +Every feed should have its own publishing key. These must be saved and stored securely as they grant the ability to make updates to their respective asset feeds. ```js import crypto from "crypto"; @@ -123,47 +122,60 @@ Manifest stays the same. ``` - - -## Hierarchical Feed-Indexed Page Manifests (Multi-page Site) +## Using Nested Feeds for Multi-Page Sites In this pattern, you build a **fully dynamic blog** where: * The blog homepage is served from **one feed manifest** -* The list of posts comes from **another feed** (the β€œpost index”) +* The list of posts comes from **another feed** (the "post index") * Each blog post lives at its own **independent feed manifest** * And each post’s internal assets (HTML, CSS, JS, images, metadata, etc.) are also updated through **their own feeds** -This creates a structure like: -```mermaid -flowchart TD +### Diagram 1 β€” High-Level Dynamic Blog Structure - A[Blog Index Feed Manifest
Stable URL] --> B(Blog Index Feed Entry
JSON List of Posts) +*(Homepage feed β†’ Post index feed β†’ Post feeds)* - B --> C1[Post #1 Feed Manifest
Stable URL] - B --> C2[Post #2 Feed Manifest
Stable URL] - B --> C3[Post #3 Feed Manifest
Stable URL] +```mermaid +flowchart TB + A["Home Page Feed (Stable URL)"] + B["config.json Feed Manifest (Stable URL)"] + C["Post Index Feed Manifest (JSON list of posts)"] + A --> B + A --> C + subgraph Post1["Post 1"] + P1HTML["Post 1 HTML Feed"] + P1CSS["Post 1 CSS Feed"] + P1JS["Post 1 JS Feed"] + P1IMG["Post 1 Image Feed"] + end + subgraph Post2["Post 2"] + P2HTML["Post 2 HTML Feed"] + P2CSS["Post 2 CSS Feed"] + P2JS["Post 2 JS Feed"] + P2IMG["Post 2 Image Feed"] + end + C --> Post1 + C --> Post2 +``` - C1 --> D1A[Post #1 HTML Feed] - C1 --> D1B[Post #1 CSS Feed] - C1 --> D1C[Post #1 JS Feed] - C1 --> D1D[Post #1 Image Feeds] - C2 --> D2A[Post #2 HTML Feed] - C2 --> D2B[Post #2 CSS Feed] - C2 --> D2C[Post #2 JS Feed] - C2 --> D2D[Post #2 Image Feeds] +### Diagram 2 β€” Internal Structure of a Single Post - C3 --> D3A[Post #3 HTML Feed] - C3 --> D3B[Post #3 CSS Feed] - C3 --> D3C[Post #3 JS Feed] - C3 --> D3D[Post #3 Image Feeds] +*(Each post is itself composed of multiple asset feeds)* +```mermaid +flowchart TB + subgraph POST["Post Feed
(Stable URL)"] + end + + POST --> HTML["HTML Feed"] + POST --> CSS["CSS Feed"] + POST --> JS["JS Feed"] + POST --> IMG1["Image Feed #1"] + POST --> IMG2["Image Feed #2"] ``` -This is a **literal feed of feed manifests**, but described in a way that’s directly tied to the practical outcome: a fully dynamic, updatable blog where *every moving part can change independently*. - ### Dynamic Blog Structure (Feed-Indexed Manifests) From d006bba7c196eeef6b191d41372a5f21669f77d8 Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Thu, 20 Nov 2025 07:32:25 +0700 Subject: [PATCH 6/8] add routing example --- docs/develop/host-your-website.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/develop/host-your-website.md b/docs/develop/host-your-website.md index 3d3f23da..f20823d7 100644 --- a/docs/develop/host-your-website.md +++ b/docs/develop/host-your-website.md @@ -482,7 +482,7 @@ You do not need to encode the hash or use any additional tools. `bzz://` i ## 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. +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. See the [routing project in the examples repo](https://github.com/ethersphere/examples/tree/main/routing) for a full working example implementation. ### Why Hash Based Client Side Routing? @@ -564,7 +564,7 @@ export function App() { return ( From 10aae04abeebed8fe9a93067358ad2f90698671e Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Sat, 22 Nov 2025 16:38:33 +0700 Subject: [PATCH 7/8] edits --- docs/develop/dynamic-content.md | 259 +++++++++----------------------- 1 file changed, 70 insertions(+), 189 deletions(-) diff --git a/docs/develop/dynamic-content.md b/docs/develop/dynamic-content.md index d83989ea..82ba1735 100644 --- a/docs/develop/dynamic-content.md +++ b/docs/develop/dynamic-content.md @@ -4,75 +4,56 @@ id: dynamic-content sidebar_label: Dynamic Content --- - -## Feeds for Dynamic Content +## Feeds for Dynamic Content :::info -Although all data on Swarm is immutable, feeds provide an updatable reference that enables dynamic content, simulating a mutable resource which always resolves to its latest update through a static feed manifest reference. +Although all data on Swarm is immutable, feeds provide an updatable reference that lets you simulate dynamic content. A feed is an append‑only sequence of updates that always resolves to its latest entry through a stable feed manifest. ::: :::tip -This guide relies heavily on the use of Swarm feeds. If you are not already familiar with feeds, it's recommended to familiarize yourself with them. See the [bee-docs feeds section](/docs/develop/tools-and-features/feeds/) for a high level overview and then check the [bee-js-docs feeds section](https://bee-js.ethswarm.org/docs/soc-and-feeds/#feeds) for a more detailed explanation and example implementation. +If you are not familiar with feeds, read: +- Bee docs: /docs/develop/tools-and-features/feeds/ +- Bee-JS docs: https://bee-js.ethswarm.org/docs/soc-and-feeds/ ::: +Single‑page applications (SPAs) deployed on Swarm work best when their static assets can be updated independently. Instead of reuploading the entire site when one file changes, you can create a separate feed manifest for each asset. Each asset feed provides a stable URL that always resolves to the latest version of that file. -This guide shows how to create **per-asset feeds** so you can update individual files (header images, CSS, JS bundles, logos, site config, etc.) without re-uploading the entire website. - -This technique effectively turns Swarm into a **decentralized CDN**: -each asset gets its own permanent reference, and updates flow atomically through feeds. - -### Why Use Per-Asset Feeds? - -Traditional upload-and-replace workflows require re-deploying everything, even if only one asset changes. With Swarm feeds: - -- Every file can have its own independent "channel" (feed topic). -- Each feed has a **stable manifest hash** that never changes. -- When you update an asset, only that feed is updated. - -This gives you: +## Why Use Per‑Asset Feeds -- Faster and smaller updates -- No-downtime asset changes -- A decentralized CDN-like architecture +- Each React/Vite build artifact (HTML, JS, CSS, images) becomes individually updatable. +- Every asset has a dedicated feed manifest with its own stable Swarm URL. +- Updating a single file only updates its feed; the rest of the site stays untouched. +- This keeps deployments small, fast, and cost‑efficient. -### Architecture Overview +## Architecture Overview -For a typical site: +| Asset | Feed Topic | Purpose | +|----------------------|-----------------|-----------------------------| +| Main site bundle | `website` | HTML/JS/CSS entry point | +| CSS theme | `main-css` | Global styling | +| JS bundle | `main-js` | Application logic | +| Images | `img-*` | Media resources | -| Asset | Feed Topic | Purpose | -|----------------|-------------------|---------| -| Main website | `website` | Full site HTML/JS/CSS bundle | -| Header image | `header-image` | Frequently changed graphic | -| CSS theme | `main-css` | Style updates | -| JS bundle | `main-js` | Client logic | -| Config JSON | `site-config` | Dynamic data | +Each asset has: +- a private key +- a feed topic +- a feed manifest +- a stable Swarm URL (`bzz:///`) -Each of these gets: - -- its own private key -- its own topic -- its own feed manifest -- its own permanent URL (using the static reference from the feed manifest) - - -### Generate Publisher Keys - -Every feed should have its own publishing key. These must be saved and stored securely as they grant the ability to make updates to their respective asset feeds. +## Generate a Publisher Key ```js import crypto from "crypto"; import { PrivateKey } from "@ethersphere/bee-js"; -const hexKey = '0x' + crypto.randomBytes(32).toString('hex'); -const pk = new PrivateKey(hexKey); +const hex = "0x" + crypto.randomBytes(32).toString("hex"); +const pk = new PrivateKey(hex); console.log("Private key:", pk.toHex()); console.log("Address:", pk.publicKey().address().toHex()); ``` -### Create a Feed Per Asset - -Example: header image. +## Create a Feed for an Asset ```js import { Bee, PrivateKey, Topic } from "@ethersphere/bee-js"; @@ -80,186 +61,86 @@ import { Bee, PrivateKey, Topic } from "@ethersphere/bee-js"; const bee = new Bee("http://localhost:1633"); const batchId = ""; -const pk = new PrivateKey(""); -const topic = Topic.fromString("header-image"); +const pk = new PrivateKey(""); +const topic = Topic.fromString("main-js"); const owner = pk.publicKey().address(); const writer = bee.makeFeedWriter(topic, pk); ``` -### Upload Asset + Publish Feed Update +## Upload an Asset and Publish a Feed Update ```js -const upload = await bee.uploadFile(batchId, "./assets/header.jpg"); -await writer.uploadReference(batchId, upload.reference); +const upload = await bee.uploadFile(batchId, "./dist/assets/index-398a.js"); +await writer.upload(batchId, upload.reference); const manifest = await bee.createFeedManifest(batchId, topic, owner); -console.log("Header Manifest:", manifest.toHex()); +console.log("JS Manifest:", manifest.toHex()); ``` Stable URL: ``` -bzz:/// +bzz:/// ``` +This URL never changes, even when you replace the underlying file. -### Updating the Asset +## Updating an Existing Asset ```js -const newUpload = await bee.uploadFile(batchId, "./assets/header-new.jpg"); -await writer.uploadReference(batchId, newUpload.reference); +const nextUpload = await bee.uploadFile(batchId, "./dist/assets/index-new.js"); +await writer.upload(batchId, nextUpload.reference); ``` -Manifest stays the same. +No new manifest is created. The old URL now resolves to the updated file. +## Referencing Asset Feeds in Your SPA -### Reference Asset Feeds in HTML +Rather than referencing a static build hash, point your SPA to feed manifests. + +Example `index.html`: ```html -Header + - -``` - -## Using Nested Feeds for Multi-Page Sites - -In this pattern, you build a **fully dynamic blog** where: - -* The blog homepage is served from **one feed manifest** -* The list of posts comes from **another feed** (the "post index") -* Each blog post lives at its own **independent feed manifest** -* And each post’s internal assets (HTML, CSS, JS, images, metadata, etc.) are also updated through **their own feeds** - - -### Diagram 1 β€” High-Level Dynamic Blog Structure - -*(Homepage feed β†’ Post index feed β†’ Post feeds)* - -```mermaid -flowchart TB - A["Home Page Feed (Stable URL)"] - B["config.json Feed Manifest (Stable URL)"] - C["Post Index Feed Manifest (JSON list of posts)"] - A --> B - A --> C - subgraph Post1["Post 1"] - P1HTML["Post 1 HTML Feed"] - P1CSS["Post 1 CSS Feed"] - P1JS["Post 1 JS Feed"] - P1IMG["Post 1 Image Feed"] - end - subgraph Post2["Post 2"] - P2HTML["Post 2 HTML Feed"] - P2CSS["Post 2 CSS Feed"] - P2JS["Post 2 JS Feed"] - P2IMG["Post 2 Image Feed"] - end - C --> Post1 - C --> Post2 ``` +Example React image reference: -### Diagram 2 β€” Internal Structure of a Single Post - -*(Each post is itself composed of multiple asset feeds)* - -```mermaid -flowchart TB - subgraph POST["Post Feed
(Stable URL)"] - end - - POST --> HTML["HTML Feed"] - POST --> CSS["CSS Feed"] - POST --> JS["JS Feed"] - POST --> IMG1["Image Feed #1"] - POST --> IMG2["Image Feed #2"] +```jsx +Hero ``` +This makes your deployment resilient to Vite’s changing file names, because Swarm fetches the latest version through the feed instead of the literal file path. -### Dynamic Blog Structure (Feed-Indexed Manifests) - -A dynamic blog is built by combining multiple levels of Swarm feeds: - -#### **1. Top-level: Blog Feed Manifest (your main website URL)** - -This is the stable URL you give to users. It loads your SPA (React + hash routing). -Your frontend then fetches the blog index. - -#### **2. Blog Index Feed (list of posts)** - -This feed contains a JSON array of **feed manifest addresses**, one per post. - -Example `posts.json` stored inside the index feed: - -```json -{ - "posts": [ - "0xPOST1_MANIFEST", - "0xPOST2_MANIFEST", - "0xPOST3_MANIFEST" - ] -} -``` - -Update this feed whenever you add or remove posts. - -#### **3. Blog Post Manifests (one per post)** - -Each post gets its own feed manifest which always points to its latest version. - -Each post contains: - -* the post HTML (also a feed) -* post-specific CSS (feed) -* post-specific JS (feed) -* post images (feeds) -* metadata JSON (feed) - -Your structure might look like: - -``` -post-1/ - post.html β†’ feed - style.css β†’ feed - main.js β†’ feed - cover.jpg β†’ feed - metadata.json β†’ feed -``` - -All bundled into **one feed manifest for the post**, so the post has a single stable URL. - -#### **4. Optional: ENS Integration** - -Each post can also have an ENS subdomain if desired: - -``` -post1.blog.eth β†’ post 1 feed manifest -post2.blog.eth β†’ post 2 feed manifest -``` - - -### How the Frontend Uses These Feeds - -Inside your React app (using [`HashRouter`](/docs/develop/host-your-website#client-side-routing)), you would: +## Example Deployment Workflow -1. Load the **post index feed** -2. Display the list of posts -3. When the user navigates to `/#/post/`, load that post’s feed manifest -4. Render its HTML + assets +1. Build Vite: + ``` + npm run build + ``` -This gives you: +2. For each file in `dist/`: + - assign (or reuse) a feed topic + - upload the file + - update its feed + - store the feed manifest hash in a hard‑coded list inside your SPA -* **fully dynamic content** -* posts you can update individually -* a blog you can expand infinitely -* stable URLs for *every* level of content -* no need to reupload the main site for a new post +3. Rebuild your SPA to reference: + - `bzz:///` + - `bzz:///` + - `bzz:///` +4. Upload only the *main* SPA entrypoint (often a small static HTML + JS shell) using `swarm-cli feed upload`. -### In Summary +This gives you a fully working dynamic SPA with lightweight incremental updates. -> A dynamic blog is built using a *feed-indexed set of page manifests*: -> one feed acts as the post index, and each post has its own feed manifest that points to its latest version. Inside each post, the HTML, CSS, JS, images, and metadata are each stored in their own feeds so everything can update independently. +## Summary +- Each build artifact gets its own updatable feed. +- Your SPA uses stable feed manifest URLs instead of build‑hashed filenames. +- Only changed files need to be uploaded. +- This keeps deployments fast while ensuring long‑lived URLs remain valid. +The next section (not included here) expands this into a registry‑based system for large dynamic sites. From 3e2b6c68beb18fe818261231b8c52d77271bb27e Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Mon, 24 Nov 2025 04:42:12 +0700 Subject: [PATCH 8/8] manifests for directories --- docs/develop/directories-routing.md | 747 ++++++++++++++++++++++++++++ sidebars.js | 1 + 2 files changed, 748 insertions(+) create mode 100644 docs/develop/directories-routing.md diff --git a/docs/develop/directories-routing.md b/docs/develop/directories-routing.md new file mode 100644 index 00000000..0e136e5b --- /dev/null +++ b/docs/develop/directories-routing.md @@ -0,0 +1,747 @@ +--- +title: Directories & Routing (Manifests) +id: directories-routing +sidebar_label: Directories & Routing +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Bee nodes β€” along with tools used for working with them like `bee-js` and `swarm-cli` β€” let you upload whole folders of files to Swarm. + +Swarm doesn’t have a traditional filesystem, but can _act like one_ using **manifests**, which map readable paths (like `/images/cat.jpg`) to immutable Swarm references. + +:::info +The `bee-js` [`MantarayNode` class](https://github.com/ethersphere/bee-js?tab=readme-ov-file#swarm-primitives) is the main way to work with manifests in NodeJS. + +The name comes from an older (now deprecated) library, so you may still see manifests referred to as **β€œMantaray manifests.”** +::: + +A manifest is stored as a compact, binary-encoded **prefix trie**. Each node in the trie represents part of a file path and may contain: + +- a path segment +- a trie fork +- a reference to the root chunk of the file’s Swarm hash +- file metadata (content type, filename, etc.) + +:::info +A **trie** is a special type of tree that stores data based on **shared prefixes**. +This makes lookups fast and avoids repeating long path segments. +::: + +Manifests give Swarm two powerful features: + +- A **filesystem-like structure** that preserves the directory layout of uploaded folders. +- **Clean, customizable website routing**, such as mapping `/about`, `/about/`, and `/about.html` to the same file or redirecting old paths to new ones. + +:::info +Manifests are stored on Swarm as raw binary data. +To work with them, these bytes must be **unmarshalled** (decoded) into a structured form. + +Although `bee-js` provides this functionality through the `MantarayNode` class, although in theory could be done with any language as long as it preserves the trie data. + +After unmarshalling, the data is still quite low-level (for example, many fields are `Uint8Array` values) and usually needs additional processing to make it human-readable. You can find a [script for this in the `ethersphere/examples` repo](https://github.com/ethersphere/examples/blob/main/manifests/printManifestJson.js). +::: + +## Introduction to Manifests + +Whenever you upload a folder using Bee’s `/bzz` endpoint (and tools built on top of it such as `bee-js` and `swarm-cli`), Bee automatically creates a manifest that records: + +- every file inside the folder +- the file’s relative path +- metadata (content type, filename, etc.) +- optional website metadata (index document, error document) + +Uploads made through the Bee API using `/bytes` or `/chunks` **do not** create manifests. +However, most developers rarely use these endpoints directly unless they’re building for some custom use-case. + +Because `bee-js` and `swarm-cli` call `/bzz` when appropriate, **you get a manifest automatically** whenever you upload a directory. + +:::important +Although working with a manifest may _feel_ like moving or deleting files in a regular filesystem, **no data on Swarm is ever changed**, because all content is immutable. + +When you "modify" a manifest, you’re actually creating a _new_ manifest based on the previous one. +Removing an entry only removes it from the manifest β€” the underlying file remains available as long as its postage batch is valid. +::: + +:::tip +You can find complete examples of all manifest scripts in the ethersphere/examples repo under +[`/manifests`](https://github.com/ethersphere/examples/tree/main/manifests). + +The `bee-js` [`cheatsheet.ts`](https://github.com/ethersphere/bee-js/blob/master/cheatsheet.ts) and +[manifest source code](https://github.com/ethersphere/bee-js/blob/master/src/manifest/manifest.ts) +are also excellent references. +::: + +## Manifest Structure Explained + +The printed output below shows a **decoded Mantaray manifest** (printed using the `printManifestJson` method from the [`manifestToJson.js` script in the examples repo](https://github.com/ethersphere/examples/blob/main/manifests/manifestToJson.js)), represented as a tree of nodes and forks. Each part plays a specific role in describing how file paths map to Swarm content. Here’s what each piece means. + +```json +{ + "path": "/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "folder/": { + "path": "folder/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "nested.txt": { + "path": "nested.txt", + "target": "0x9442e445c0d58adea58e0a8afcdcc28ed7642d7a4ff9a253e8f1595faafbb808", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "nested.txt" + }, + "forks": {} + }, + "subfolder/deep.txt": { + "path": "subfolder/deep.txt", + "target": "0x6aa935879ad2a547e57ea6350338bd04ad758977b542e86b31c159f31834b8fc", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "deep.txt" + }, + "forks": {} + } + } + }, + "root.txt": { + "path": "root.txt", + "target": "0x98e63f7e826a01634881874246fc873cdf06bb5409ff5f9ec61d1e2de1dd3bf6", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "root.txt" + }, + "forks": {} + } + } +} + +``` + +#### **Node:** + +A **Node** represents a position within the manifest trie. +Each node corresponds to a **path prefix**β€”a segment of the full file path. + +For example: + +- A node with `path: folder/` represents the prefix `"folder/"`. +- A node with `path: nested.txt` represents a leaf for the file `"nested.txt"`. + +Every node may contain: + +- a path segment (`path`) +- zero or more child connections (`forks`) +- optional metadata (e.g., content-type, filename) +- a `target` (if the node corresponds to an actual file) + +#### **Forks:** + +A **fork** is an edge from one node to another. +It is how the trie branches when file paths diverge. + +For example, under `folder/`, you see: + +```bash +forks: + nested.txt/ + subfolder/deep.txt/ +``` + +This means: + +- the `folder/` node has two children +- those children represent different path continuations (i.e., different files) + +Forks are how shared prefixes are stored only once. Everything that starts with `"folder/"` branches from the same node. + +#### **Path:** + +`path` is the **path segment stored at that node**. + +Examples: + +- `path: root.txt` +- `path: nested.txt` +- `path: subfolder/deep.txt` + +These are _not_ full paths. +They represent only the part needed at that position in the trie. +The full path is reconstructed by walking from the root down through forks. + +#### **Target:** + +The `target` field holds the **Swarm hash of the file the node points to**. + +Example: + +``` +target: 0x9442e445c0d58a... +``` + +This hash is the immutable reference of the actual content uploaded to Swarm. + +**Why is the target sometimes `0x000000...000`?** + +Because **not every node corresponds to a file**. + +Nodes represent **prefixes**, not necessarily files. + +For example: + +- The root node (`Node:` at the top) has no file associated so its `target` is zero. +- The node for `folder/` also has no file associated β†’ target is zero. + It is just an internal directory-like prefix. + +Only **leaf nodes**, where a file actually exists, have a non-zero target (the file’s Swarm reference). + +So: + +| Node type | Example | Has file? | Target | +| ----------------------- | ------------- | --------- | ------------- | +| Internal directory node | `folder/` | No | `0x000...000` | +| Leaf node | `nested.txt` | Yes | real hash | +| Root node | (top of tree) | No | `0x000...000` | + +:::warning +The `target` field in a manifest points to the raw file root chunk, not a manifest. `bee-js` and `swarm-cli` file download functions expect a file manifest, even for single-file uploads, so downloading using the raw target hash will not work properly. +Instead, download files by using the top-level directory manifest and the file’s path within it. + +Example: + +````bash +curl http://127.0.0.1:1633/bzz/4d5e6e3eb532131e128b1cd0400ca249f1a6ce5d4005c0b57bf848131300df9d/folder/subfolder/deep.txt +::: + +```bash +DEEP +```` + +**Metadata:** + +Metadata stores information about a file, such as: + +- Content-Type +- Filename +- Website index/error docs (if configured) + +Example: + +```yaml +metadata: + Content-Type: text/plain; charset=utf-8 + Filename: nested.txt +``` + +Only **file nodes** (leaf nodes) normally have metadata. +Internal nodes generally do not. + +Metadata helps: + +- gateways set HTTP headers (e.g., correct MIME type) +- browsers display files correctly +- filesystem-like behavior + +#### **Putting it together:** + +Let’s interpret a branch: + +``` +folder/ + nested.txt +``` + +This means: + +1. There is a prefix node representing `"folder/"`. +2. Inside it, there is a file `"nested.txt"`. +3. The file node has: + + - a target (its Swarm content hash) + - metadata (filename + content-type) + +Meanwhile, `"folder/"` has **no file itself**, so its target is zero. + +## Directories (bee-js) + +In this section we explain how to inspect and modify manifests for non-website directories. You can find the completed [example scripts on GitHub](https://github.com/ethersphere/examples/tree/main/manifests). + +In the following guides we will explain how to: + +1. Upload a directory and print its manifest +2. Add a new file +3. Move a file (delete + add new entry) + +#### Pre-requisites: + +- NodeJS and npm +- Linux or WSL preferred but most commands should work from windows Powershell with slight modifications +- git +- The RPC endpoint for a currently running Bee node (either on your machine or remote, try [Swarm Desktop](https://www.ethswarm.org/build/desktop) for a no-hassle way to get started) + +If you'd like to follow along with the guides shown below, clone the [`ethersphere/examples` repo](https://github.com/ethersphere/examples/) and navigate to the `/manifests` folder: + +```bash +git clone git@github.com:ethersphere/examples.git +cd examples/manifests/ +``` + +Print the file tree to confirm you're in the right place: + +```bash +user@machine:~/examples/manifests$ tree +. +β”œβ”€β”€ directory +β”‚Β Β  β”œβ”€β”€ folder +β”‚Β Β  β”‚Β Β  β”œβ”€β”€ nested.txt +β”‚Β Β  β”‚Β Β  └── subfolder +β”‚Β Β  β”‚Β Β  └── deep.txt +β”‚Β Β  └── root.txt +β”œβ”€β”€ env +β”œβ”€β”€ manifestToJson.js +β”œβ”€β”€ package-lock.json +β”œβ”€β”€ package.json +β”œβ”€β”€ script-01.js +β”œβ”€β”€ script-02.js +└── script-03.js +``` + +If you're using Powershell you can use the `tree /f` command instead and the output file tree should look similar. + +After confirming, run `npm install` to install dependencies: + +```bash +npm install +``` + +Locate the `env` file and add a period/full stop to change the file name to a standard `dotenv` file (`.env`). Then modify the file to replace `` with your RPC endpoint and `` with your own postage batch ID: + +```bash +BEE_RPC_URL= // Default: http://localhost:1633 +POSTAGE_BATCH_ID= +``` + +Great! Now you're all set up and ready to go. + +### Uploading a Directory and Printing Its Manifest + +In our first script, we will simply upload our sample directory and print its contents: + +**script-01.js (initial upload script)** + +```js +import { Bee, MantarayNode } from "@ethersphere/bee-js"; +import path from "path"; +import { fileURLToPath } from "url"; +import "dotenv/config"; +import { printManifest } from "./printManifest.js"; + +// Recreate __dirname for ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const bee = new Bee(process.env.BEE_RPC_URL); +const postageBatchId = process.env.POSTAGE_BATCH_ID; + +// Build the folder path safely +const directoryPath = path.join(__dirname, "directory"); + +async function uploadDirectory() { + try { + console.log("Uploading directory:", directoryPath); + + // Upload using the resolved directory and get manifest reference + const { reference } = await bee.uploadFilesFromDirectory( + postageBatchId, + directoryPath + ); + + console.log("Directory uploaded successfully!"); + console.log("Manifest reference:", reference.toHex()); + + // Download each file using its relative path as recorded by the manifest + const root = await bee.downloadFile(reference, "root.txt"); + const nested = await bee.downloadFile(reference, "folder/nested.txt"); + const deep = await bee.downloadFile(reference, "folder/subfolder/deep.txt"); + + // Print out file contents + console.log("root.txt:", root.data.toUtf8()); + console.log("folder/nested.txt:", nested.data.toUtf8()); + console.log("folder/subfolder/deep.txt:", deep.data.toUtf8()); + + // Load the generated manifest + const node = await MantarayNode.unmarshal(bee, reference); + await node.loadRecursively(bee); + + // Print manifest in human readable format + console.log("\n--- Manifest Tree ---"); + printManifest(node); + } catch (error) { + console.error("Error during upload or download:", error.message); + } +} + +uploadDirectory(); +``` + +Note that in the script when downloading our files individually we must use the same relative paths that match the directory we uploaded: + +```js +// Download each file using its relative path as recorded by the manifest +const root = await bee.downloadFile(reference, "root.txt"); +const nested = await bee.downloadFile(reference, "folder/nested.txt"); +const deep = await bee.downloadFile(reference, "folder/subfolder/deep.txt"); +``` + +Run the script: + +```bash +node script-01.js +``` + +If you've set up everything properly, you should see the file contents printed to the terminal followed by the manifest tree: + +```json +Uploading directory: /home/user/examples/manifests/directory +Directory uploaded successfully! +Manifest reference: 4d5e6e3eb532131e128b1cd0400ca249f1a6ce5d4005c0b57bf848131300df9d +root.txt: ROOT +folder/nested.txt: NESTED +folder/subfolder/deep.txt: DEEP + +--- Manifest Tree --- +{ + "path": "/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "folder/": { + "path": "folder/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "nested.txt": { + "path": "nested.txt", + "target": "0x9442e445c0d58adea58e0a8afcdcc28ed7642d7a4ff9a253e8f1595faafbb808", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "nested.txt" + }, + "forks": {} + }, + "subfolder/deep.txt": { + "path": "subfolder/deep.txt", + "target": "0x6aa935879ad2a547e57ea6350338bd04ad758977b542e86b31c159f31834b8fc", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "deep.txt" + }, + "forks": {} + } + } + }, + "root.txt": { + "path": "root.txt", + "target": "0x98e63f7e826a01634881874246fc873cdf06bb5409ff5f9ec61d1e2de1dd3bf6", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "root.txt" + }, + "forks": {} + } + } +} +``` + +Note and record the manifest reference returned before the manifest tree is printed: + +```bash +Manifest reference: 4d5e6e3eb532131e128b1cd0400ca249f1a6ce5d4005c0b57bf848131300df9d +``` + +We will use this in the next section when adding a file manually to the manifest. + +### Adding a New File to the Manifest + +This script uploads a **new file** (e.g. `newfile.txt`) and then updates the existing manifest so the new file becomes part of the directory structure. + +**script-02.js** + +*The following script is almost identical to script-01.js, only the changed sections will be highlighted. Remember you can always refer to the complete version of the script in the examples repo.* + +```js + +import "dotenv/config" +import { Bee, MantarayNode } from "@ethersphere/bee-js" +import { printManifestJson } from './manifestToJson.js' + +const bee = new Bee(process.env.BEE_RPC_URL) +const batchId = process.env.POSTAGE_BATCH_ID + +// We specify the manifest returned from script-01.js here +const ROOT_MANIFEST = '4d5e6e3eb532131e128b1cd0400ca249f1a6ce5d4005c0b57bf848131300df9d' + +async function addFileToManifest() { + try { + // Load the generated manifest from script-01.js + const node = await MantarayNode.unmarshal(bee, ROOT_MANIFEST) + await node.loadRecursively(bee) + + // File details for new file + const filename = 'new.txt' + const content = "Hi, I'm new here." + const bytes = new TextEncoder().encode(content) + + // Upload raw file data + // Note we use "bee.uploadData()", not "bee.uploadFile()", since we need the root reference hash of the content, not a manifest reference. + const { reference } = await bee.uploadData(batchId, bytes) + console.log('Uploaded raw reference:', reference.toHex()) + + // Metadata must be a plain JS object β€” NOT a Map or Uint8Array + const metadata = { + 'Content-Type': 'text/plain; charset=utf-8', + 'Filename': filename, + } + + // Insert the new file data into our new manifest + node.addFork(filename, reference, metadata) + + // Save and print updated manifest + const newManifest = await node.saveRecursively(bee, batchId) + const newReference = newManifest.reference + console.log('Updated manifest hash:', newReference.toHex()) + printManifestJson(node) + + // Download new file and print its contents + const newFile = await bee.downloadFile(newReference, "new.txt") + console.log("new.txt:", newFile.data.toUtf8()) + + } + catch (error) { + console.error("Error during upload or download:", error.message) + } +} + +addFileToManifest() +``` + +Terminal output: + +```bash +noah@NoahM16:~/examples/manifests$ node script-02.js +Uploaded raw reference: 3515db2f5e3c075b7546d7dd7dea1680c3e0785c6584e66b7e4f56fc344a0a78 +Updated manifest hash: 4f67218844a814655c8d81aae4c4286a142318d672113973360c33c7930ce2f5 +{ + "path": "/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "folder/": { + "path": "folder/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "nested.txt": { + "path": "nested.txt", + "target": "0x9442e445c0d58adea58e0a8afcdcc28ed7642d7a4ff9a253e8f1595faafbb808", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "nested.txt" + }, + "forks": {} + }, + "subfolder/deep.txt": { + "path": "subfolder/deep.txt", + "target": "0x6aa935879ad2a547e57ea6350338bd04ad758977b542e86b31c159f31834b8fc", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "deep.txt" + }, + "forks": {} + } + } + }, + "root.txt": { + "path": "root.txt", + "target": "0x98e63f7e826a01634881874246fc873cdf06bb5409ff5f9ec61d1e2de1dd3bf6", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "root.txt" + }, + "forks": {} + }, + "new.txt": { + "path": "new.txt", + "target": "0x3515db2f5e3c075b7546d7dd7dea1680c3e0785c6584e66b7e4f56fc344a0a78", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "new.txt" + }, + "forks": {} + } + } +} +new.txt: Hi, I'm new here. +``` + +This produces a new manifest where `/new.txt` is now accessible as a root level entry. + +### Moving a File to a Subfolder + +This script: + +1. Removes `/new.txt` from the manifest +2. Adds it back under `/nested/deeper/new.txt` +3. Prints the updated manifest + +**script-03.js** + +```js +import "dotenv/config" +import { Bee, MantarayNode } from "@ethersphere/bee-js" +import { printManifestJson } from './manifestToJson.js' + +const bee = new Bee(process.env.BEERPC_URL || process.env.BEE_RPC_URL) +const batchId = process.env.POSTAGE_BATCH_ID + +// Manifest returned from script-02.js +const ROOT_MANIFEST = 'SCRIPT_2_MANIFEST' + +async function moveFileInManifest() { + try { + // Load manifest generated in script-02 + const node = await MantarayNode.unmarshal(bee, ROOT_MANIFEST) + await node.loadRecursively(bee) + + // Reload manifest to capture original file reference *before* deletion + const original = await MantarayNode.unmarshal(bee, ROOT_MANIFEST) + await original.loadRecursively(bee) + + const existing = original.find("new.txt") + if (!existing) { + throw new Error("Could not retrieve file reference for new.txt β€” run script-02.js first.") + } + + const fileRef = existing.targetAddress + + // STEP 1 β€” Remove /new.txt + node.removeFork("new.txt") + console.log("Removed /new.txt from manifest.") + + // STEP 2 β€” Re-add under /nested/deeper/new.txt + const newPath = "nested/deeper/new.txt" + + node.addFork( + newPath, + fileRef, + { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "new.txt" + } + ) + + console.log(`Added file under /${newPath}`) + + // STEP 3 β€” Save updated manifest + const updated = await node.saveRecursively(bee, batchId) + const newManifestRef = updated.reference.toHex() + + console.log("Updated manifest hash:", newManifestRef) + + // STEP 4 β€” Print JSON + printManifestJson(node) + + // STEP 5 β€” Download the file from its new location and print contents + const downloaded = await bee.downloadFile(updated.reference, newPath) + console.log(`\nContents of /${newPath}:`) + console.log(downloaded.data.toUtf8()) + + } catch (error) { + console.error("Error while modifying manifest:", error.message) + } +} + +moveFileInManifest() +``` + +Terminal output: + +```bash +user@machine:~/examples/manifests$ node script-03.js +Removed /new.txt from manifest. +Added file under /nested/deeper/new.txt +Updated manifest hash: 656ea924fb4d98b7fa327eb9e4d98ece6c2f4370515d23b40dfca71bc99a08a6 +{ + "path": "/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "folder/": { + "path": "folder/", + "target": "0x0000000000000000000000000000000000000000000000000000000000000000", + "metadata": null, + "forks": { + "nested.txt": { + "path": "nested.txt", + "target": "0x9442e445c0d58adea58e0a8afcdcc28ed7642d7a4ff9a253e8f1595faafbb808", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "nested.txt" + }, + "forks": {} + }, + "subfolder/deep.txt": { + "path": "subfolder/deep.txt", + "target": "0x6aa935879ad2a547e57ea6350338bd04ad758977b542e86b31c159f31834b8fc", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "deep.txt" + }, + "forks": {} + } + } + }, + "root.txt": { + "path": "root.txt", + "target": "0x98e63f7e826a01634881874246fc873cdf06bb5409ff5f9ec61d1e2de1dd3bf6", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "root.txt" + }, + "forks": {} + }, + "nested/deeper/new.txt": { + "path": "nested/deeper/new.txt", + "target": "0x3515db2f5e3c075b7546d7dd7dea1680c3e0785c6584e66b7e4f56fc344a0a78", + "metadata": { + "Content-Type": "text/plain; charset=utf-8", + "Filename": "new.txt" + }, + "forks": {} + } + } +} + +Contents of /nested/deeper/new.txt: +Hi, I'm new here. +``` + + +Now the file appears under: + +``` +/nested/deeper/root.txt +``` + +Note that the only new method we used was `node.removeFork()` to remove the entry from the manifest. + +```js +// STEP 1 β€” Remove /new.txt + node.removeFork("new.txt") + console.log("Removed /new.txt from manifest.") +``` + diff --git a/sidebars.js b/sidebars.js index 79216e52..50d8b61f 100644 --- a/sidebars.js +++ b/sidebars.js @@ -90,6 +90,7 @@ module.exports = { 'develop/introduction', 'develop/upload-and-download', 'develop/host-your-website', + 'develop/directories-routing', 'develop/dynamic-content', 'develop/act', ],