NIP-5A
Static Websites (nsites)
draft optional
This NIP describes a method by which static websites can be hosted from Blossom assets.
Site Manifests
A site manifest event MUST be a regular, replaceable, or an addressable event as defined in NIP-01.
There are two types of nsite manifest event kinds:
- Root site: Uses kind
15128and MUST NOT include adtag. This is a single replaceable event per pubkey and serves as the root site for the pubkey. - Named sites: Uses kind
35128. These can be smaller websites under a pubkey and can be thought of as subdomains.
Named Sites
A named site uses kind 35128 and MUST include a
d tag containing the site identifier.
The d tag identifies the named site and is used as
the site’s identifier within the author’s namespace.
For canonical named-site URLs, the d tag value
MUST match ^[a-z0-9-]{1,13}$ and MUST NOT end with
-.
Because DNS labels are limited to 63 characters and
pubkeyB36 uses 50 of them, the d tag
value MUST be 1-13 characters.
Nsite Event Tags
| Tag | Role | Required |
|---|---|---|
path |
Maps an absolute path to the sha256 hash of the file served at that path. | Yes, one or more |
x |
Provides the aggregate hash of the full manifest so the site version is indexable and discoverable. | Optional, recommended |
d |
Short identifier for the named site. This tag is used only for
kind 35128 events. |
Required for named sites only |
a |
References the immediate parent nsite from which this copied site was made. | Required for copied sites only |
A |
References the origin nsite of a copied site’s lineage. | Required for copied sites only |
server |
Hints which blossom servers can be used to retrieve the blobs. | No |
title |
Provides a human-readable site title. | No |
description |
Provides a short human-readable description of the site. | No |
source |
Points to the site’s source code repository or source archive. | No |
The event MUST include one or more path tags that
map absolute paths to sha256 hashes. Each path tag
MUST have the format
["path", "/absolute/path", "sha256hash"] where:
- The first element is the literal string
"path" - The second element is an absolute path ending with a filename and extension
- The third element is the sha256 hash of the file that will be served under this path
The event SHOULD include an x tag containing the
site aggregate hash as defined below. The x tag MUST
have the format
["x", "<sha256-hex>", "aggregate"], where
<sha256-hex> is the lowercase hexadecimal
aggregate hash.
The event MAY include server tags that hint at
which blossom servers can be used to find the blobs associated
with the hashes.
The event MAY include title and
description tags that provide simple site
information.
The event MAY include a source tag that links to
the site’s source code repository or source archive. The
source tag MUST have the format
["source", "<url>"]. For source code
repositories, <url> MAY be a
nostr:// git URL as defined by NIP-34, or an absolute
https:// git URL. For source archives,
<url> MUST be an absolute https://
URL.
Manifest Snapshots
A manifest snapshot uses kind 5128 and is a
regular event. Its purpose is to capture the state of a root site
or named site at a particular point in time.
When publishing root sites or named sites, authors MAY also
publish manifest snapshot events alongside them as additional
events in order to preserve version history and make historical
versions addressable by snapshot event id. For a given site, the
snapshot event’s created_at timestamp is the version
timestamp of that snapshot.
For snapshotting an existing root site or named site, the
manifest snapshot event MUST copy the source manifest’s
path tags, MUST include exactly one x
tag whose value exactly matches the source manifest’s aggregate
x tag, and MUST include exactly one a
tag referencing the source root site or named site. If the source
manifest includes an A tag, the snapshot event MUST
copy that A tag unchanged. If the source manifest
does not include an A tag, the snapshot event SHOULD
omit it. The referenced site identifies the snapshotted nsite,
while the snapshot event’s own created_at identifies
the version. Other valid nsite tags such as title,
description, source, app,
server, or relay hints MAY also be copied if the
author intends to preserve them, but they are optional.
Aggregate Hash
The site aggregate hash is a deterministic hash of the site’s
path tags. It identifies a specific site version and
can be used to identify equivalent site manifests published by
different pubkeys.
The aggregate hash MUST be computed using only
path tags. It MUST NOT depend on the order of tags in
the manifest, and it MUST ignore all other tags and event fields,
including app tags.
Two site manifest events are equivalent if and only if they produce the same aggregate hash.
A site manifest SHOULD include this value in an x
tag with the format
["x", "<sha256-hex>", "aggregate"] so the
aggregate hash is indexable for discoverability and lookup.
Manifest snapshot events of kind 5128 MUST include
exactly one such tag, and it MUST exactly match the parent nsite’s
aggregate x tag.
To compute the aggregate hash:
- Collect every
pathtag. - For each tag, produce a line in the exact format
<sha256hash> <absolute-path>\n. - Sort all lines in ascending lexicographic order.
- Concatenate the sorted lines as UTF-8 bytes.
- Compute the SHA-256 hash of the concatenated bytes.
The resulting digest MUST be encoded as lowercase hexadecimal.
Example inputs:
186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99 /index.html
fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 /favicon.ico
Example calculations:
nak req -k 15128 -a <pubkey> --limit 1 wss://relay.example.com | jq -r '.tags[] | select(.[0] == "path") | "\(.[2]) \(.[1])"' | sort | sha256sum
const aggregateHash = sha256(event.tags.filter(t => t[0] === "path").map(([, path, hash]) => `${hash} ${path}\n`).sort().join(""))
Upstream App Descriptors
An nsite MAY include one or more app tags that
reference upstream app descriptor events that the manifest is part
of. The app tag MUST have the format
["app", "<kind>:<pubkey>:<d-tag>", "<relay>"],
where the second element is an addressable event reference to an
app descriptor and the third element is a relay hint for locating
that event.
This linkage is optional. It is intended to make room for application-level discovery and packaging flows without requiring app creators to extend nsite manifests themselves or overload them with non-manifest concerns.
The referenced app descriptor kind is intentionally generic so nsites can reference NIP-89 applications and other present or future forms of app descriptors. Discoverability, app store metadata, runtime compatibility, and similar application-level concerns belong in those app descriptor events, while the nsite manifest remains focused on describing the site’s files.
Copying nsites
An nsite MAY be created by copying another root site or named site in order to pin that site under a different author’s namespace at a specific state.
The purpose of copying is to preserve and republish an existing
nsite while allowing the new author to maintain it independently.
For this reason, authors creating a copied nsite SHOULD copy all
tags from the source nsite whenever they remain applicable,
including path, x, app,
server, title, description,
and source.
Copying an nsite does not require preserving the original
site’s kind or identifier. A copied root site MAY become a named
site, a copied named site MAY become a root site, and a copied
named site MAY use a different d tag than its
parent.
When a site is copied from another nsite, the new manifest MUST
include exactly one lowercase a tag referencing the
immediate parent nsite from which it was copied.
Copied sites MUST also include exactly one uppercase
A tag referencing the origin nsite of the copy
lineage. If a site is copied directly from an original nsite, the
a and A tags MAY reference the same
nsite. If a site is copied from another copied site, the
A tag MUST be copied unchanged from the parent
site.
Because copied sites may change kind or identifier, the
a and A tags are the stable, indexable
references that preserve the lineage of the copied site.
Sites that were not copied from another nsite SHOULD NOT
include a or A tags.
Host server implementation
A host server is a HTTP server that is responsible for serving pubkey static websites
Address Formats
For interoperability, host servers SHOULD use the following canonical URL formats:
- Root site:
<npub>.nsite-host.com - Snapshot:
v<snapshotIdB36>.nsite-host.com - Named site:
<pubkeyB36><dTag>.nsite-host.com
pubkeyB36 is the author’s raw 32-byte pubkey
encoded with base36 (lowercase, digits 0-9 then
letters a-z, no padding) and is always exactly 50
characters.
snapshotIdB36 is the raw 32-byte snapshot event id
encoded with base36 (lowercase, digits 0-9 then
letters a-z, no padding) and is always exactly 50
characters.
dTag is the site identifier (d tag
value) as plain text. It is appended directly after
pubkeyB36 with no separator.
This single-label format avoids wildcard certificate limitations with multi-level subdomains.
If the host server is using subdomain routing it MAY serve
anything at its own root domain nsite-host.com (a
landing page for example).
Example subdomains:
- Root site:
npub10phxfsms72rhafrklqdyhempujs9h67nye0p67qe424dyvcx0dkqgvap0e.nsite-host.com - Snapshot:
v<50-char-snapshotIdB36>.nsite-host.com - Named site:
<50-char-pubkeyB36><dTag>.nsite-host.com
For canonical subdomain formats, the host server MUST parse the left-most DNS label as follows:
- If the label is a valid
npub, decode it and resolve the root site manifest. - Otherwise, if the label matches
^v[0-9a-z]{50}$, treat it as a snapshot label where:
snapshotIdB36is the final 50 characters after the leadingv- decode
snapshotIdB36to the 32-byte event id - resolve the manifest snapshot for that event id
- Otherwise, if the label matches
^[0-9a-z]{50}[a-z0-9-]{1,13}$and does not end with-, treat it as a named-site label where:
pubkeyB36is the first 50 charactersdTagis the remaining 1-13 characters- decode
pubkeyB36to a 32-byte pubkey - use
dTagas the identifier (dtag value) - resolve the named site manifest for that pubkey and identifier
If parsing fails, the host server MUST treat the site as not found.
Resolving Paths
Once the site manifest event is found, the host server MUST
extract the path-to-hash mappings from the path tags
in the manifest. The host server SHOULD look for a
path tag where the second element matches the
requested path.
The host server SHOULD verify that the sha256 hash of any
served blob matches the hash in the corresponding
path tag. If the blob hash does not match, the host
server SHOULD treat the site as not found.
Index Pages
If the request path does not end with a filename, the host
server MUST fall back to using the index.html
filename.
For example: / -> /index.html or
/blog/ -> /blog/index.html
Once the host server has found the site manifest event and
located the matching path tag for the requested path,
it should use the sha256 hash defined in the third element of the
path tag to retrieve the file.
The host server SHOULD prioritize using server
tags from the site manifest event as hints for which blossom
servers to query. If the manifest includes server
tags, the host server SHOULD attempt to retrieve the file from
those servers first.
If the pubkey has a 10063 BUD-03 user servers event, the server MUST
attempt to retrieve the file from the listed servers using the
path defined in BUD-01.
If a pubkey does not have a 10063 event and no
server tags are found in the manifest, the host
server MUST respond with a status code 404.
The host server MUST forward the Content-Type and
Content-Length headers from the Blossom server. If
neither is defined, the host server MAY set
Content-Type from the file extension in the requested
path.
Not Found
If a host server is unable to find a site manifest event or a
matching path tag for the requested path, it MUST use
/404.html as a fallback path.
Examples
Root Site Manifest
{
"content": "",
"created_at": 1727373475,
"id": "5324d695ed7abf7cdd2a48deb881c93b7f4e43de702989bbfb55a1b97b35a3de",
"kind": 15128,
"pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/about.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["path", "/favicon.ico", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"],
["x", "<site-aggregate-sha256>", "aggregate"],
["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-app", "wss://relay.example.com"],
["app", "32267:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:com.example.webapp", "wss://relay.example.com"],
["server", "https://blossom.example.com"],
["title", "My Nostr Site"],
["description", "A static website hosted on Nostr"],
["source", "https://github.com/example/my-nostr-site"]
]
}
Named Site Manifest
{
"content": "",
"created_at": 1727373475,
"id": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"kind": 35128,
"pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
["d", "blog"],
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/post.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["x", "<site-aggregate-sha256>", "aggregate"],
["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-blog-app", "wss://relay.example.com"],
["app", "32267:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:com.example.blog", "wss://relay.example.com"],
["server", "https://blossom.example.com"],
["title", "My Blog"],
["description", "A blog hosted on Nostr"],
["source", "https://github.com/example/my-nostr-blog"]
]
}
Copied Named Site Manifest
{
"content": "",
"created_at": 1727373475,
"id": "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"kind": 35128,
"pubkey": "9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
["d", "blog"],
// copied directly from the original named site
["a", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],
// origin of this copy lineage; same as `a` for a direct copy
["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/post.html", "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["x", "<site-aggregate-sha256>", "aggregate"],
["title", "My Copied Blog"],
["description", "A copy of another named nsite"]
]
}
Copied Site Derived From Another Copied Site
{
"content": "",
"created_at": 1727374475,
"id": "c1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"kind": 35128,
"pubkey": "7a6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4bbbb",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
["d", "blog"],
// copied from another copied site, not directly from the origin
["a", "35128:9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff:blog"],
// still points at the original site where this lineage began
["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/post.html", "c1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["x", "<site-aggregate-sha256>", "aggregate"],
["title", "My Derived Blog"],
["description", "A copy of a copied nsite that preserves the original lineage"]
]
}
Manifest Snapshot
{
"content": "",
"created_at": 1727373475,
"id": "9f8e7d6c5b4a39281716151413121110ffeeddccbbaa99887766554433221100",
"kind": 5128,
"pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
["a", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/post.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["x", "<site-aggregate-sha256>", "aggregate"],
["server", "https://blossom.example.com"],
["title", "My Blog v1"],
["description", "An immutable snapshot of a blog hosted on Nostr"],
["source", "https://github.com/example/my-nostr-blog"]
]
}
Manifest Snapshot Of A Copied Site
{
"content": "",
"created_at": 1727375475,
"id": "8f8e7d6c5b4a39281716151413121110ffeeddccbbaa99887766554433221100",
"kind": 5128,
"pubkey": "9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff",
"sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",
"tags": [
// the site manifest this snapshot captures
["a", "35128:9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff:blog"],
// copied unchanged from the snapshotted site's origin lineage
["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],
["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],
["path", "/post.html", "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],
["x", "<site-aggregate-sha256>", "aggregate"],
["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-blog-app", "wss://relay.example.com"],
["title", "My Copied Blog v1"],
["description", "An immutable snapshot of a copied nsite"]
]
}
Warning
unrecommended: too complicated, try NIP-99 instead