跳至主要內容
進階

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)

![圖片說明](https://example.com/image.jpg)

\`行內代碼\`

\`\`\`javascript
// 代碼區塊
function hello() {
  console.log('Hello, Nostr!')
}
\`\`\`

> 引用區塊

---

| 表格 | 標題 |
|------|------|
| 內容 | 內容 |

NIP-23 專用客戶端

使用 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 渲染、目錄生成和閱讀時間估算。 專注於內容創作,讓客戶端處理呈現細節。

已複製連結
已複製到剪貼簿