Freenet can host static websites – HTML, CSS, JavaScript, images – with no server required. Your site is distributed across the peer-to-peer network, served through any Freenet gateway, and can only be updated by you.
No programming is required. If you have a folder of static files with an index.html, you can
publish it to Freenet in three commands.
How It Works
Your website files are compressed into an archive, signed with your private key, and stored as a Freenet contract. The contract enforces two rules:
- Authentication: only someone with your signing key can publish or update the site
- Versioning: updates must have a higher version number (no rollbacks)
The contract’s key (a hash of the WASM code and your public key) becomes your website’s permanent address. Anyone running a Freenet node can access it through their local gateway.
Prerequisites
Install Freenet using the quickstart guide, which includes the fdev tool and a
running Freenet node.
1. Generate a Signing Keypair
Choose a name for your website and run:
fdev website init my-blog
This creates an Ed25519 keypair at ~/.config/freenet/website-keys/my-blog.toml and prints your
website’s contract key and URL:
Keypair 'my-blog' generated and saved to: /home/you/.config/freenet/website-keys/my-blog.toml
Your website contract key: 3ZZ98ojKWUJsixNyJsgRwkBZhLxN4CV2Z5AT8dVWJh48
Website URL: http://127.0.0.1:7509/v1/contract/web/3ZZ98ojKWUJsixNyJsgRwkBZhLxN4CV2Z5AT8dVWJh48/
To publish: fdev website publish ./my-site/ --key my-blog
To update: fdev website update ./my-site/ --key my-blog
IMPORTANT: Back up your key file! Losing it means you can never update your website.
The contract key is derived from your public key and the contract code. It is your website’s permanent address – it will not change when you update the site content.
Back up your key file. The signing key is the only thing that authorizes updates to your website. If you lose it, the site becomes permanently read-only. There is no recovery mechanism. Copy the
.tomlfile to a password manager (1Password, Bitwarden, etc.), an encrypted USB drive, or any other secure backup you trust.
2. Publish Your Website
Point fdev at a directory containing your website files. The directory must contain an
index.html at its root.
fdev website publish ./my-site/ --key my-blog
This compresses the directory, signs it, and publishes it to your local Freenet node. The node then distributes it across the network.
Compressed ./my-site/ -> 48231 bytes (12 files)
Publishing website as contract 3ZZ98ojKWUJsixNyJsgRwkBZhLxN4CV2Z5AT8dVWJh48 (version 29523847)
Website published successfully!
URL: http://127.0.0.1:7509/v1/contract/web/3ZZ98ojKWUJsixNyJsgRwkBZhLxN4CV2Z5AT8dVWJh48/
Visit the URL in your browser to see the site served from Freenet.
Publishing to a remote node
By default fdev connects to 127.0.0.1:7509. To publish through a remote gateway (e.g., from
CI), use --node-url:
fdev --node-url "ws://gateway.example.com:7520/v1/contract/command?encodingProtocol=native" \
website publish ./my-site/ --key my-blog
3. Update Your Website
Edit your files, then run:
fdev website update ./my-site/ --key my-blog
The version number increments automatically. The contract rejects any update that doesn’t have a higher version than the current one, so only forward progress is possible.
Static Site Generators
Any static site generator works – Hugo, Jekyll, Eleventy, Astro, mkdocs, or plain HTML.
Important: You must set the base URL to your contract’s gateway path so that CSS, JS, images,
and internal links resolve correctly. Get your contract key from fdev website list, then pass it
to your generator:
# Hugo
CONTRACT_KEY=$(fdev website list | awk '{print $2}')
hugo --minify --baseURL "/v1/contract/web/${CONTRACT_KEY}/"
fdev website publish ./public/ --key my-blog
# Eleventy (set pathPrefix in .eleventy.js)
npx @11ty/eleventy
fdev website publish ./_site/ --key my-blog
# Plain HTML (no base URL needed if all paths are relative)
fdev website publish ./my-site/ --key my-blog
Rewriting hardcoded paths
Static site generators use their base URL for links generated by templates, but raw HTML in content
files (e.g., <img src="/img/photo.jpg"> in a Markdown file) won’t be rewritten automatically.
These absolute paths need post-processing.
A simple approach: after building, rewrite all href="/..." and src="/..." attributes to include
the contract base path. For example, the
freenet.org build script
handles this with a Python script that runs after Hugo.
What works and what doesn’t
Sites are served through a Freenet gateway inside a sandboxed iframe. Here’s what to expect:
Works well:
- Multi-page navigation – the gateway automatically intercepts link clicks within the iframe and navigates to the target page (as of v0.2.41)
- CSS, JavaScript, images, fonts bundled with your site
- Single-page apps (React, Vue, Dioxus) – routing handled in JavaScript
- Large sites – the archive is compressed with xz; the contract supports up to 100MB
Won’t work:
- Traditional server-side logic – the website contract serves static files only. For dynamic behavior (user accounts, real-time updates, data storage), build a full dApp with contracts and delegates instead.
- External CDN resources – Google Fonts, external images, and CDN-hosted libraries are blocked by the iframe’s Content Security Policy. Bundle all fonts and assets locally.
- External API calls –
fetch()to GitHub, Stripe, analytics services, etc. are blocked by CSP. If your site has optional API features, make them degrade gracefully. - Absolute paths without the contract prefix –
href="/about/"will break;href="/v1/contract/web/KEY/about/"will work. Use your generator’s URL functions or post-process paths as described above.
Using a Custom Contract
If you have an existing website contract (e.g., River’s web container) and want to keep its contract
key, use the --contract-wasm flag:
fdev website publish ./my-site/ --key my-blog --contract-wasm ./my-contract.wasm
This uses your custom WASM for the contract while still handling compression, signing, and publishing automatically.
Key Management
How keys are stored
Each website gets its own key file at ~/.config/freenet/website-keys/<name>.toml. The name you
choose during fdev website init identifies the key for all subsequent publish/update commands.
To see all your website keys and their contract addresses:
fdev website list
Multiple websites
Each name produces a different keypair and contract key:
fdev website init blog
fdev website init docs
fdev website publish ./blog/public/ --key blog
fdev website publish ./docs/site/ --key docs
Backing up your keys
Your signing key is the only thing that authorizes updates to your website. If you lose it, the site becomes permanently read-only with no recovery mechanism.
The key file is a small .toml text file. Back it up wherever you keep important credentials:
- Password manager (1Password, Bitwarden, KeePassXC) – store the file contents as a secure note
- Encrypted backup – copy the
~/.config/freenet/website-keys/directory to an encrypted drive - Version control (private repo) – if you trust your private repo’s access controls
Do not commit key files to public repositories.
How It Works (Technical Details)
The website container contract is a standard Freenet contract with a specific state format:
[metadata_length: u64 BE][metadata: CBOR][web_length: u64 BE][web: tar.xz archive]
The metadata contains a version number and an Ed25519 signature over version_bytes || archive_bytes.
The contract parameters are the 32-byte Ed25519 verifying key.
On validate_state, the contract verifies the signature. On update_state, it additionally checks
that the new version is strictly greater than the current version. The state synchronization methods
(summarize_state, get_state_delta) use the version number for efficient peer sync.
The contract source code is in the freenet-website-contract crate.