Files
codium_nostr_extension/plans/longform-metadata-strategy.md
2026-07-18 17:29:20 -04:00

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 (`![](./img/x.png)`) 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:`.