All proposals
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 15128 and MUST NOT include a d tag. 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:

  1. Collect every path tag.
  2. For each tag, produce a line in the exact format <sha256hash> <absolute-path>\n.
  3. Sort all lines in ascending lexicographic order.
  4. Concatenate the sorted lines as UTF-8 bytes.
  5. 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:

  1. If the label is a valid npub, decode it and resolve the root site manifest.
  2. Otherwise, if the label matches ^v[0-9a-z]{50}$, treat it as a snapshot label where:
  • snapshotIdB36 is the final 50 characters after the leading v
  • decode snapshotIdB36 to the 32-byte event id
  • resolve the manifest snapshot for that event id
  1. 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:
  • pubkeyB36 is the first 50 characters
  • dTag is the remaining 1-13 characters
  • decode pubkeyB36 to a 32-byte pubkey
  • use dTag as the identifier (d tag 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"]
  ]
}