clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / source-nips / 5A.md
5A.md 19.8K rendered
1NIP-5A2======34Static Websites (nsites)5------------------------67`draft` `optional`89This NIP describes a method by which static websites can be hosted from Blossom assets.1011### Site Manifests1213A site manifest event MUST be a regular, replaceable, or an addressable event as defined in [NIP-01](01.md).1415There are two types of nsite manifest event kinds:1617- **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.18- **Named sites**: Uses kind `35128`. These can be smaller websites under a pubkey and can be thought of as subdomains.1920### Named Sites2122A named site uses kind `35128` and MUST include a `d` tag containing the site identifier.2324The `d` tag identifies the named site and is used as the site's identifier within the author's namespace.2526For canonical named-site URLs, the `d` tag value MUST match `^[a-z0-9-]{1,13}$` and MUST NOT end with `-`.2728Because DNS labels are limited to 63 characters and `pubkeyB36` uses 50 of them, the `d` tag value MUST be 1-13 characters.2930#### Nsite Event Tags313233| Tag | Role | Required |34| ------------- | --------------------------------------------------------------------------------------------------- | ------------------------------ |35| `path` | Maps an absolute path to the sha256 hash of the file served at that path. | Yes, one or more |36| `x` | Provides the aggregate hash of the full manifest so the site version is indexable and discoverable. | Optional, recommended |37| `d` | Short identifier for the named site. This tag is used only for kind `35128` events. | Required for named sites only |38| `a` | References the immediate parent nsite from which this copied site was made. | Required for copied sites only |39| `A` | References the origin nsite of a copied site's lineage. | Required for copied sites only |40| `server` | Hints which blossom servers can be used to retrieve the blobs. | No |41| `title` | Provides a human-readable site title. | No |42| `description` | Provides a short human-readable description of the site. | No |43| `source` | Points to the site's source code repository or source archive. | No |4445The 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:4647- The first element is the literal string `"path"`48- The second element is an absolute path ending with a filename and extension49- The third element is the sha256 hash of the file that will be served under this path5051The 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.5253The event MAY include `server` tags that hint at which blossom servers can be used to find the blobs associated with the hashes.5455The event MAY include `title` and `description` tags that provide simple site information.5657The 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](34.md), or an absolute `https://` git URL. For source archives, `<url>` MUST be an absolute `https://` URL.5859### Manifest Snapshots6061A 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.6263When 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.6465For 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.6667### Aggregate Hash6869The 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.7071The 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.7273Two site manifest events are equivalent if and only if they produce the same aggregate hash.7475A 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.7677To compute the aggregate hash:78791. Collect every `path` tag.802. For each tag, produce a line in the exact format `<sha256hash> <absolute-path>\n`.813. Sort all lines in ascending lexicographic order.824. Concatenate the sorted lines as UTF-8 bytes.835. Compute the SHA-256 hash of the concatenated bytes.8485The resulting digest MUST be encoded as lowercase hexadecimal.8687Example inputs:8889```text90186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99 /index.html91fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 /favicon.ico92```9394Example calculations:9596```bash97nak req -k 15128 -a <pubkey> --limit 1 wss://relay.example.com | jq -r '.tags[] | select(.[0] == "path") | "\(.[2]) \(.[1])"' | sort | sha256sum98```99100```javascript101const aggregateHash = sha256(event.tags.filter(t => t[0] === "path").map(([, path, hash]) => `${hash} ${path}\n`).sort().join(""))102```103104### Upstream App Descriptors105106An 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.107108This 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.109110The referenced app descriptor kind is intentionally generic so nsites can reference [NIP-89](89.md) 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.111112### Copying nsites113114An 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.115116The 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`.117118Copying 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.119120When 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.121122Copied 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.123124Because 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.125126Sites that were not copied from another nsite SHOULD NOT include `a` or `A` tags.127128### Host server implementation129130A host server is a HTTP server that is responsible for serving pubkey static websites131132#### Address Formats133134For interoperability, host servers SHOULD use the following canonical URL formats:135136- Root site: `<npub>.nsite-host.com`137- Snapshot: `v<snapshotIdB36>.nsite-host.com`138- Named site: `<pubkeyB36><dTag>.nsite-host.com`139140`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.141142`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.143144`dTag` is the site identifier (`d` tag value) as plain text. It is appended directly after `pubkeyB36` with no separator.145146This single-label format avoids wildcard certificate limitations with multi-level subdomains.147148If the host server is using subdomain routing it MAY serve anything at its own root domain `nsite-host.com` (a landing page for example).149150Example subdomains:151152- Root site: `npub10phxfsms72rhafrklqdyhempujs9h67nye0p67qe424dyvcx0dkqgvap0e.nsite-host.com`153- Snapshot: `v<50-char-snapshotIdB36>.nsite-host.com`154- Named site: `<50-char-pubkeyB36><dTag>.nsite-host.com`155156For canonical subdomain formats, the host server MUST parse the left-most DNS label as follows:1571581. If the label is a valid `npub`, decode it and resolve the root site manifest.1592. Otherwise, if the label matches `^v[0-9a-z]{50}$`, treat it as a snapshot label where:160 - `snapshotIdB36` is the final 50 characters after the leading `v`161 - decode `snapshotIdB36` to the 32-byte event id162 - resolve the manifest snapshot for that event id1633. 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:164 - `pubkeyB36` is the first 50 characters165 - `dTag` is the remaining 1-13 characters166 - decode `pubkeyB36` to a 32-byte pubkey167 - use `dTag` as the identifier (`d` tag value)168 - resolve the named site manifest for that pubkey and identifier169170If parsing fails, the host server MUST treat the site as not found.171172#### Resolving Paths173174Once 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.175176The 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.177178##### Index Pages179180If the request path does not end with a filename, the host server MUST fall back to using the `index.html` filename.181182For example: `/` -> `/index.html` or `/blog/` -> `/blog/index.html`183184Once 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.185186The 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.187188If the pubkey has a `10063` [BUD-03 user servers](https://github.com/hzrd149/blossom/blob/master/buds/03.md) event, the server MUST attempt to retrieve the file from the listed servers using the path defined in [BUD-01](https://github.com/hzrd149/blossom/blob/master/buds/01.md#get-sha256---get-blob).189190If 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.191192The 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.193194##### Not Found195196If 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.197198### Examples199200#### Root Site Manifest201202```yaml203{204 "content": "",205 "created_at": 1727373475,206 "id": "5324d695ed7abf7cdd2a48deb881c93b7f4e43de702989bbfb55a1b97b35a3de",207 "kind": 15128,208 "pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",209 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",210 "tags": [211 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],212 ["path", "/about.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],213 ["path", "/favicon.ico", "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"],214 ["x", "<site-aggregate-sha256>", "aggregate"],215 ["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-app", "wss://relay.example.com"],216 ["app", "32267:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:com.example.webapp", "wss://relay.example.com"],217 ["server", "https://blossom.example.com"],218 ["title", "My Nostr Site"],219 ["description", "A static website hosted on Nostr"],220 ["source", "https://github.com/example/my-nostr-site"]221 ]222}223```224225#### Named Site Manifest226227```yaml228{229 "content": "",230 "created_at": 1727373475,231 "id": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",232 "kind": 35128,233 "pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",234 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",235 "tags": [236 ["d", "blog"],237 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],238 ["path", "/post.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],239 ["x", "<site-aggregate-sha256>", "aggregate"],240 ["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-blog-app", "wss://relay.example.com"],241 ["app", "32267:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:com.example.blog", "wss://relay.example.com"],242 ["server", "https://blossom.example.com"],243 ["title", "My Blog"],244 ["description", "A blog hosted on Nostr"],245 ["source", "https://github.com/example/my-nostr-blog"]246 ]247}248```249250#### Copied Named Site Manifest251252```jsonc253{254 "content": "",255 "created_at": 1727373475,256 "id": "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",257 "kind": 35128,258 "pubkey": "9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff",259 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",260 "tags": [261 ["d", "blog"],262 // copied directly from the original named site263 ["a", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],264 // origin of this copy lineage; same as `a` for a direct copy265 ["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],266 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],267 ["path", "/post.html", "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],268 ["x", "<site-aggregate-sha256>", "aggregate"],269 ["title", "My Copied Blog"],270 ["description", "A copy of another named nsite"]271 ]272}273```274275#### Copied Site Derived From Another Copied Site276277```jsonc278{279 "content": "",280 "created_at": 1727374475,281 "id": "c1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",282 "kind": 35128,283 "pubkey": "7a6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4bbbb",284 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",285 "tags": [286 ["d", "blog"],287 // copied from another copied site, not directly from the origin288 ["a", "35128:9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff:blog"],289 // still points at the original site where this lineage began290 ["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],291 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],292 ["path", "/post.html", "c1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],293 ["x", "<site-aggregate-sha256>", "aggregate"],294 ["title", "My Derived Blog"],295 ["description", "A copy of a copied nsite that preserves the original lineage"]296 ]297}298```299300#### Manifest Snapshot301302```jsonc303{304 "content": "",305 "created_at": 1727373475,306 "id": "9f8e7d6c5b4a39281716151413121110ffeeddccbbaa99887766554433221100",307 "kind": 5128,308 "pubkey": "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",309 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",310 "tags": [311 ["a", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],312 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],313 ["path", "/post.html", "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],314 ["x", "<site-aggregate-sha256>", "aggregate"],315 ["server", "https://blossom.example.com"],316 ["title", "My Blog v1"],317 ["description", "An immutable snapshot of a blog hosted on Nostr"],318 ["source", "https://github.com/example/my-nostr-blog"]319 ]320}321```322323#### Manifest Snapshot Of A Copied Site324325```jsonc326{327 "content": "",328 "created_at": 1727375475,329 "id": "8f8e7d6c5b4a39281716151413121110ffeeddccbbaa99887766554433221100",330 "kind": 5128,331 "pubkey": "9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff",332 "sig": "f4e4a9e785f70e9fcaa855d769438fea10781e84cd889e3fcb823774f83d094cf2c05d5a3ac4aebc1227a4ebc3d56867286c15a6df92d55045658bb428fd5fb5",333 "tags": [334 // the site manifest this snapshot captures335 ["a", "35128:9f6815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd4000ff:blog"],336 // copied unchanged from the snapshotted site's origin lineage337 ["A", "35128:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:blog"],338 ["path", "/index.html", "186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99"],339 ["path", "/post.html", "b1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"],340 ["x", "<site-aggregate-sha256>", "aggregate"],341 ["app", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:my-blog-app", "wss://relay.example.com"],342 ["title", "My Copied Blog v1"],343 ["description", "An immutable snapshot of a copied nsite"]344 ]345}346```