mirror of
https://github.com/nostr-protocol/nips.git
synced 2026-07-22 07:48:25 +00:00
347 lines
20 KiB
Markdown
347 lines
20 KiB
Markdown
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](01.md).
|
|
|
|
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](34.md), 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:
|
|
|
|
```text
|
|
186ea5fd14e88fd1ac49351759e7ab906fa94892002b60bf7f5a428f28ca1c99 /index.html
|
|
fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321 /favicon.ico
|
|
```
|
|
|
|
Example calculations:
|
|
|
|
```bash
|
|
nak req -k 15128 -a <pubkey> --limit 1 wss://relay.example.com | jq -r '.tags[] | select(.[0] == "path") | "\(.[2]) \(.[1])"' | sort | sha256sum
|
|
```
|
|
|
|
```javascript
|
|
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](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.
|
|
|
|
### 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
|
|
3. 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](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).
|
|
|
|
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
|
|
|
|
```yaml
|
|
{
|
|
"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
|
|
|
|
```yaml
|
|
{
|
|
"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
|
|
|
|
```jsonc
|
|
{
|
|
"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
|
|
|
|
```jsonc
|
|
{
|
|
"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
|
|
|
|
```jsonc
|
|
{
|
|
"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
|
|
|
|
```jsonc
|
|
{
|
|
"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"]
|
|
]
|
|
}
|
|
```
|