進階
NIP-23 長文內容
深入了解 Nostr 的長文內容標準,使用 kind 30023 發布 Markdown 格式的部落格文章。
10 分鐘
什麼是 NIP-23?
NIP-23 定義了在 Nostr 上發布長文內容的標準。與普通貼文(kind 1)不同, 長文內容使用 kind 30023,支援 Markdown 格式、標題、摘要、封面圖片等中繼資料, 適合發布部落格文章、教學文件和深度內容。
可替換事件: Kind 30023 是「可參數化替換事件」(Parameterized Replaceable Event), 同一作者的相同 d-tag 文章會被新版本替換,實現文章更新功能。
事件結構
{
"kind": 30023,
"content": "# 文章標題\n\n這是文章內容,支援 **Markdown** 格式...",
"tags": [
["d", "my-article-slug"], // 唯一識別符(必須)
["title", "我的文章標題"], // 標題
["summary", "這是文章摘要..."], // 摘要
["image", "https://example.com/cover.jpg"], // 封面圖片
["published_at", "1704067200"], // 首次發布時間
["t", "bitcoin"], // 標籤(可多個)
["t", "nostr"]
],
"pubkey": "<作者公鑰>",
"created_at": 1704153600, // 最後更新時間
"id": "...",
"sig": "..."
} 標籤說明
| 標籤 | 必須 | 說明 |
|---|---|---|
| d | 是 | 唯一識別符,通常是 URL 友善的 slug |
| title | 建議 | 文章標題,用於列表顯示 |
| summary | 可選 | 文章摘要,用於預覽 |
| image | 可選 | 封面圖片 URL |
| published_at | 可選 | 首次發布的 Unix 時間戳 |
| t | 可選 | 主題標籤,可以有多個 |
草稿 vs 發布
NIP-23 區分草稿和已發布的文章:
// 草稿(kind 30024)
{
"kind": 30024, // 草稿使用不同的 kind
"content": "# 草稿標題\n\n還在編輯中...",
"tags": [
["d", "draft-article-slug"],
["title", "草稿標題"]
]
}
// 發布的文章(kind 30023)
{
"kind": 30023,
"content": "# 正式標題\n\n完成的文章內容...",
"tags": [
["d", "article-slug"],
["title", "正式標題"],
["published_at", "1704067200"]
]
} 程式碼範例
發布文章
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools'
function createArticle({
title,
slug,
content,
summary,
image,
tags = []
}) {
const now = Math.floor(Date.now() / 1000)
const event = {
kind: 30023,
content: content, // Markdown 格式
created_at: now,
tags: [
['d', slug],
['title', title],
['published_at', now.toString()]
]
}
// 添加可選標籤
if (summary) {
event.tags.push(['summary', summary])
}
if (image) {
event.tags.push(['image', image])
}
for (const tag of tags) {
event.tags.push(['t', tag])
}
return event
}
// 使用
const article = createArticle({
title: 'Nostr 入門指南',
slug: 'nostr-getting-started',
content: `# Nostr 入門指南
## 什麼是 Nostr?
Nostr 是一個去中心化的社交協議...
## 開始使用
1. 首先,你需要一對密鑰...
2. 然後選擇一個客戶端...
`,
summary: '學習如何開始使用 Nostr 去中心化社交網路',
image: 'https://example.com/nostr-cover.jpg',
tags: ['nostr', 'tutorial', 'beginner']
})
// 簽名並發布
const signedEvent = await window.nostr.signEvent(article)
await relay.publish(signedEvent) 更新文章
// 更新文章:使用相同的 d-tag 發布新版本
function updateArticle(existingArticle, newContent, newTitle) {
const now = Math.floor(Date.now() / 1000)
// 保留原始發布時間
const publishedAt = existingArticle.tags
.find(t => t[0] === 'published_at')?.[1] || now.toString()
const dTag = existingArticle.tags.find(t => t[0] === 'd')?.[1]
return {
kind: 30023,
content: newContent,
created_at: now, // 更新時間
tags: [
['d', dTag], // 相同的 d-tag = 替換舊版本
['title', newTitle || existingArticle.tags.find(t => t[0] === 'title')?.[1]],
['published_at', publishedAt], // 保留原始發布時間
// ... 其他標籤
]
}
}
// 使用
const updatedArticle = updateArticle(
existingArticle,
'# 更新後的內容\n\n這是修改過的版本...',
'更新後的標題'
)
const signed = await window.nostr.signEvent(updatedArticle)
await relay.publish(signed)
// 中繼器會用新版本替換舊版本 查詢文章
// 查詢某作者的所有文章
function getArticlesByAuthor(relay, authorPubkey) {
return new Promise((resolve) => {
const articles = []
const sub = relay.subscribe([{
kinds: [30023],
authors: [authorPubkey],
limit: 50
}])
sub.on('event', (event) => {
articles.push(parseArticle(event))
})
sub.on('eose', () => {
sub.close()
// 按發布時間排序
articles.sort((a, b) => b.publishedAt - a.publishedAt)
resolve(articles)
})
})
}
// 查詢特定文章(by naddr)
function getArticleByAddress(relay, pubkey, dTag) {
return new Promise((resolve) => {
const sub = relay.subscribe([{
kinds: [30023],
authors: [pubkey],
'#d': [dTag]
}])
sub.on('event', (event) => {
sub.close()
resolve(parseArticle(event))
})
sub.on('eose', () => {
sub.close()
resolve(null)
})
})
}
// 解析文章事件
function parseArticle(event) {
const getTag = (name) => event.tags.find(t => t[0] === name)?.[1]
const getTags = (name) => event.tags.filter(t => t[0] === name).map(t => t[1])
return {
id: event.id,
pubkey: event.pubkey,
slug: getTag('d'),
title: getTag('title') || '無標題',
summary: getTag('summary'),
image: getTag('image'),
content: event.content,
tags: getTags('t'),
publishedAt: parseInt(getTag('published_at') || event.created_at),
updatedAt: event.created_at
}
} 搜尋文章
// 按標籤搜尋文章
function getArticlesByTag(relay, tag, limit = 20) {
return new Promise((resolve) => {
const articles = []
const sub = relay.subscribe([{
kinds: [30023],
'#t': [tag],
limit: limit
}])
sub.on('event', (event) => {
articles.push(parseArticle(event))
})
sub.on('eose', () => {
sub.close()
resolve(articles)
})
})
}
// 使用 NIP-50 搜尋(如果中繼器支援)
function searchArticles(relay, query, limit = 20) {
return new Promise((resolve) => {
const articles = []
const sub = relay.subscribe([{
kinds: [30023],
search: query,
limit: limit
}])
sub.on('event', (event) => {
articles.push(parseArticle(event))
})
sub.on('eose', () => {
sub.close()
resolve(articles)
})
})
} Markdown 支援
NIP-23 的 content 欄位使用 Markdown 格式,常見語法:
# 一級標題
## 二級標題
### 三級標題
**粗體** 和 *斜體*
- 無序列表項目
- 另一個項目
1. 有序列表
2. 第二項
[連結文字](https://example.com)

\`行內代碼\`
\`\`\`javascript
// 代碼區塊
function hello() {
console.log('Hello, Nostr!')
}
\`\`\`
> 引用區塊
---
| 表格 | 標題 |
|------|------|
| 內容 | 內容 | NIP-23 專用客戶端
Habla.news
專業的長文內容平台
Yakihonne
文章與 Curations
Blogstack
部落格發布平台
Highlighter
閱讀與標記工具
Flycat
支援長文的客戶端
Nostr.build
媒體與內容託管
使用 naddr 分享文章
由於 kind 30023 是可替換事件,應該使用 naddr 而非 note 來分享:
import { nip19 } from 'nostr-tools'
// 建立文章的 naddr
const naddr = nip19.naddrEncode({
kind: 30023,
pubkey: authorPubkey,
identifier: 'article-slug', // d-tag
relays: ['wss://relay.example.com']
})
// 結果類似:naddr1qqxnzdesxqmn...
// 可以用來分享和引用文章
// 解碼 naddr
const decoded = nip19.decode(naddr)
// {
// type: 'naddr',
// data: {
// kind: 30023,
// pubkey: '...',
// identifier: 'article-slug',
// relays: [...]
// }
// } 與 kind 1 的比較
| 特性 | Kind 1(貼文) | Kind 30023(長文) |
|---|---|---|
| 用途 | 短訊息、社交貼文 | 部落格文章、長文 |
| 可替換 | 否(每則獨立) | 是(同 d-tag 替換) |
| 格式 | 純文字 | Markdown |
| 中繼資料 | 無 | 標題、摘要、封面等 |
| 分享方式 | note / nevent | naddr |
最佳實踐
建議做法
- • 使用有意義的 slug 作為 d-tag
- • 總是提供 title 和 summary
- • 保留原始 published_at 時間
- • 使用相關的 t 標籤便於發現
- • 發布到多個中繼器確保可用性
避免做法
- • 不要使用隨機的 d-tag
- • 不要忽略 Markdown 格式規範
- • 不要在更新時改變 d-tag
- • 不要使用過大的圖片 URL
提示: 許多 NIP-23 客戶端會自動處理 Markdown 渲染、目錄生成和閱讀時間估算。 專注於內容創作,讓客戶端處理呈現細節。
已複製連結