170 lines
6.9 KiB
Markdown
170 lines
6.9 KiB
Markdown
# Long-Form Note Metadata Strategy
|
|
|
|
## Goal
|
|
|
|
When publishing a `.md` file as a NIP-23 long-form note (kind `30023`), the
|
|
author needs to control metadata that is **not** part of the body content:
|
|
|
|
- `d` tag — the parameterized-replaceable identifier (slug)
|
|
- `title` — note title
|
|
- `summary` — short abstract shown in feeds
|
|
- `image` — cover/hero image URL
|
|
- `t` tags — topics / hashtags
|
|
- `published_at` — first-publish timestamp (set once, preserved on updates)
|
|
- `alt` — accessibility/relay hint text
|
|
|
|
We need a way to express these per-document. Two complementary mechanisms are
|
|
proposed: **front-matter** (primary, authoring-time) and a **publish dialog**
|
|
(secondary, last-mile override + confirmation).
|
|
|
|
## NIP-23 tag reference
|
|
|
|
| Tag | NIP-23 meaning | Source of value |
|
|
|---|---|---|
|
|
| `d` | identifier | front-matter `slug`, else filename stem, else prompt |
|
|
| `title` | title | front-matter `title`, else filename stem |
|
|
| `summary` | abstract | front-matter `summary`, else first paragraph |
|
|
| `image` | cover image URL | front-matter `image` |
|
|
| `t` | topic hashtag | front-matter `tags` (array) |
|
|
| `published_at` | first publish unix ts | auto-managed by extension, persisted in workspace state keyed by `d` |
|
|
| `alt` | relay hint | auto-generated: "Long-form post: <title>" |
|
|
|
|
## Mechanism 1 — YAML front-matter (primary)
|
|
|
|
A fenced YAML block at the very top of the `.md` file, delimited by `---`.
|
|
|
|
```markdown
|
|
---
|
|
slug: my-first-post
|
|
title: My First Post
|
|
summary: A short abstract for feeds.
|
|
image: https://cdn.example.com/cover.png
|
|
tags: [nostr, longform, writing]
|
|
---
|
|
|
|
# My First Post
|
|
|
|
Body content starts here. The front-matter block above is parsed by the
|
|
extension, used to build event tags, and **stripped** from the published
|
|
content so readers never see it.
|
|
```
|
|
|
|
### Parsing rules
|
|
|
|
1. The extension only treats a leading `---\n...\n---\n` block as front-matter.
|
|
A `---` later in the document (e.g. a horizontal rule) is left alone.
|
|
2. Unknown keys are ignored (forward-compatible).
|
|
3. `tags` accepts either a YAML inline array `[a, b]` or a comma-separated
|
|
string `a, b`. Each value is lowercased and trimmed; `#` prefix is optional
|
|
and stripped. Empty entries dropped.
|
|
4. `slug` is sanitized: lowercase, spaces → `-`, strip non-`[a-z0-9-]`.
|
|
5. If `slug` is absent, default to the filename stem (same sanitization).
|
|
6. If `title` is absent, default to the filename stem.
|
|
7. If `summary` is absent, default to the first non-empty, non-heading line of
|
|
the body, truncated to 280 chars.
|
|
8. If `image` is absent, no `image` tag is emitted.
|
|
|
|
### Stripping on publish
|
|
|
|
The front-matter block is removed from the `content` field of the signed event.
|
|
The on-disk file is **not** modified — the author keeps editing with the
|
|
front-matter intact. Stripping happens in-memory at publish time only.
|
|
|
|
### Front-matter validation command
|
|
|
|
A `Nostr: Validate Front-Matter` command parses the active document and shows
|
|
an information message: which tags will be emitted, any unknown keys, and the
|
|
final `d` slug. Useful as a pre-publish sanity check without opening the dialog.
|
|
|
|
## Mechanism 2 — Publish dialog (secondary, last-mile)
|
|
|
|
Triggered by `Nostr: Publish Current File as Long-Form Note`. A
|
|
`QuickPick`-style multi-step dialog (or a webview for richer UI) that:
|
|
|
|
1. Shows a **preview** of the parsed event: `d`, `title`, `summary`, `image`,
|
|
tags list, body length, target relays, signer backend, and the resolved
|
|
pubkey (npub).
|
|
2. Lets the user **override** any field for this publish only (does not write
|
|
back to the file):
|
|
- slug
|
|
- title
|
|
- summary
|
|
- image URL
|
|
- add/remove tags
|
|
3. Shows the `published_at` behavior: "First publish — setting
|
|
published_at=now" or "Update — preserving published_at=<existing>".
|
|
4. Confirms target relays (checkboxes, defaults from config + front-matter).
|
|
5. A final `Publish` action signs and relays the event.
|
|
|
|
Overrides are session-only. If the user wants a permanent change, they edit
|
|
the front-matter. This keeps the file as the source of truth while allowing
|
|
one-off tweaks (e.g. fixing a typo right before publishing).
|
|
|
|
### Why both mechanisms
|
|
|
|
- **Front-matter** makes the document self-describing and version-controllable.
|
|
The same file produces the same event every time. Good for git workflows and
|
|
re-publishing updates.
|
|
- **Publish dialog** handles the human-in-the-loop confirmation that matters
|
|
for a broadcast operation, and lets the author catch a missing `image` or
|
|
wrong `slug` before it goes on-chain. It also surfaces the signer backend
|
|
and relay set, which front-matter should not control (those are
|
|
machine/environment concerns, not document concerns).
|
|
|
|
## `published_at` lifecycle
|
|
|
|
NIP-23 expects `published_at` to reflect the **first** time the note was
|
|
published, and to remain stable across updates (edits use the same `d` tag).
|
|
|
|
The extension stores a map `{ dSlug -> publishedAtUnix }` in workspace state
|
|
(`context.workspaceState`). On publish:
|
|
|
|
- If `dSlug` is present in the map → reuse the stored timestamp.
|
|
- If absent → set `published_at = now` and store it.
|
|
|
|
This survives editor reloads (workspace state is persisted by VSCode) but is
|
|
per-workspace, which matches the per-document intent. A command
|
|
`Nostr: Reset Published Timestamps` clears the map for cases where the author
|
|
wants a clean slate (e.g. changing the `d` slug intentionally).
|
|
|
|
## Image handling
|
|
|
|
Front-matter `image` is a URL string. The extension does **not** upload images
|
|
in Phase 1/2. A future Phase 3 could add Blossom (NIP-96) upload via the
|
|
`blossom` project already in `~/lt/blossom`, then rewrite local image paths to
|
|
the uploaded URL before signing. For now, the author is responsible for
|
|
hosting the image and pasting the URL.
|
|
|
|
Local relative paths in the body (``) are left as-is in
|
|
Phase 1/2 — they will not render on relays. This is documented in the README
|
|
as a known limitation with a pointer to the future Blossom phase.
|
|
|
|
## Tag emission order
|
|
|
|
NIP-23 does not mandate tag order, but for relay compatibility we emit in
|
|
this order:
|
|
|
|
1. `d`
|
|
2. `title`
|
|
3. `summary`
|
|
4. `image`
|
|
5. `published_at`
|
|
6. `t` (one per topic)
|
|
7. `alt`
|
|
|
|
## Open questions for the user
|
|
|
|
1. **Front-matter format**: YAML (`---` fences) vs TOML (`+++` fences) vs
|
|
JSON front-matter. Recommendation: YAML — most common in markdown tooling,
|
|
human-friendly, supports arrays cleanly.
|
|
2. **Publish dialog UI**: lightweight multi-step QuickPick (faster to build,
|
|
native feel) vs a Webview panel (richer preview, more code). Recommendation:
|
|
start with QuickPick for Phase 1/2, upgrade to Webview if the preview feels
|
|
too cramped.
|
|
3. **`published_at` storage scope**: workspace state (per-workspace, survives
|
|
reload) vs global state (survives workspace switches). Recommendation:
|
|
workspace state — a post belongs to the workspace that authored it.
|
|
4. **Unknown front-matter keys**: ignore silently vs warn in the publish
|
|
dialog. Recommendation: warn (non-blocking) so authors catch typos like
|
|
`tag:` instead of `tags:`.
|