入門
NIP-21 nostr: URI
Nostr URI 協議標準,定義 nostr: 連結格式,實現跨客戶端的連結互通。
8 分鐘
概述
NIP-21 定義了 nostr: URI 協議,讓 Nostr 實體可以在網頁、應用程式和 作業系統之間無縫連結。當用戶點擊
nostr: 連結時, 作業系統會自動開啟已註冊的 Nostr 客戶端來處理。
跨平台互通
就像 mailto: 連結開啟郵件客戶端,nostr: 連結 會開啟用戶偏好的 Nostr 應用程式,實現去中心化的連結體驗。
URI 格式
nostr: URI 由協議前綴加上 NIP-19 編碼組成:
{`nostr:`} 支援的編碼類型
| 前綴 | 說明 | 用途 |
|---|---|---|
| npub | 公鑰 | 連結到用戶資料 |
| nprofile | 公鑰 + 中繼器 | 更可靠的用戶連結 |
| note | 事件 ID | 連結到貼文 |
| nevent | 事件 ID + 中繼資料 | 更可靠的事件連結 |
| naddr | 可替換事件地址 | 文章、列表等 |
安全注意
nsec(私鑰)不應該用於 nostr: URI。 私鑰絕對不應該出現在連結中。
URI 範例
用戶連結
{`# 簡單公鑰連結
nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
# 帶中繼器資訊(推薦)
nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p`} 事件連結
{`# 簡單事件 ID 連結
nostr:note1fntxtkcy9pjwucqwa9mddn7v03wwwsu9j330jj350nvhpky2tuaspk6nqc
# 帶中繼器和作者資訊(推薦)
nostr:nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetd9ehx7um5wge5wqhh5...`} 可替換事件連結
{`# 長文章連結
nostr:naddr1qqxnzdesxqmnxvpexqunzvpcqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hj7qg4waehxw309ahx7um5wghx6at5d9h8jampd3kx2apwvdhk6...`} 實作
建立 nostr: URI
{`import { nip19 } from 'nostr-tools';
// 建立用戶連結
function createUserUri(pubkey: string, relays?: string[]): string {
if (relays && relays.length > 0) {
const nprofile = nip19.nprofileEncode({ pubkey, relays });
return \`nostr:\${nprofile}\`;
}
const npub = nip19.npubEncode(pubkey);
return \`nostr:\${npub}\`;
}
// 建立事件連結
function createEventUri(
id: string,
relays?: string[],
author?: string
): string {
if (relays || author) {
const nevent = nip19.neventEncode({
id,
relays: relays || [],
author,
});
return \`nostr:\${nevent}\`;
}
const note = nip19.noteEncode(id);
return \`nostr:\${note}\`;
}
// 建立可替換事件連結
function createAddressUri(
kind: number,
pubkey: string,
identifier: string,
relays?: string[]
): string {
const naddr = nip19.naddrEncode({
kind,
pubkey,
identifier,
relays: relays || [],
});
return \`nostr:\${naddr}\`;
}
// 使用範例
const userUri = createUserUri(
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
['wss://relay.damus.io', 'wss://nos.lol']
);
// nostr:nprofile1...
const eventUri = createEventUri(
'e0b0f76c...',
['wss://relay.example.com'],
'3bf0c63f...'
);
// nostr:nevent1...
const articleUri = createAddressUri(
30023, // long-form content
'3bf0c63f...',
'my-article-slug',
['wss://relay.example.com']
);
// nostr:naddr1...`} 解析 nostr: URI
{`import { nip19 } from 'nostr-tools';
interface ParsedNostrUri {
type: 'npub' | 'nprofile' | 'note' | 'nevent' | 'naddr';
data: any;
}
function parseNostrUri(uri: string): ParsedNostrUri | null {
// 驗證 URI 格式
if (!uri.startsWith('nostr:')) {
return null;
}
// 移除協議前綴
const encoded = uri.slice(6);
try {
const decoded = nip19.decode(encoded);
// 拒絕私鑰
if (decoded.type === 'nsec') {
throw new Error('nsec is not allowed in URIs');
}
return decoded as ParsedNostrUri;
} catch (e) {
console.error('Invalid nostr URI:', e);
return null;
}
}
// 處理不同類型
function handleNostrUri(uri: string) {
const parsed = parseNostrUri(uri);
if (!parsed) return;
switch (parsed.type) {
case 'npub':
// 打開用戶資料頁面
openProfile(parsed.data);
break;
case 'nprofile':
// 使用指定的中繼器打開用戶資料
openProfile(parsed.data.pubkey, parsed.data.relays);
break;
case 'note':
// 打開事件/貼文
openEvent(parsed.data);
break;
case 'nevent':
// 使用指定的中繼器打開事件
openEvent(parsed.data.id, parsed.data.relays, parsed.data.author);
break;
case 'naddr':
// 打開可替換事件
openAddress(
parsed.data.kind,
parsed.data.pubkey,
parsed.data.identifier,
parsed.data.relays
);
break;
}
}`} 網頁整合
在 HTML 中使用 <link> 標籤關聯網頁內容與 Nostr 實體:
關聯作者
{`
`} 關聯內容
{`
`} 身份驗證
{`
`} HTML 連結
在網頁中建立可點擊的 Nostr 連結:
{`
@fiatjaf
查看這則貼文
在 Nostr 上查看
`} 應用程式註冊
客戶端應用程式需要向作業系統註冊為 nostr: 協議處理器:
網頁應用
{`// 註冊為協議處理器(需要 HTTPS)
if ('registerProtocolHandler' in navigator) {
navigator.registerProtocolHandler(
'web+nostr', // 注意:標準要求 web+ 前綴
'https://your-client.com/open?uri=%s',
'Your Nostr Client'
);
}
// 處理傳入的 URI
const url = new URL(window.location.href);
const nostrUri = url.searchParams.get('uri');
if (nostrUri) {
handleNostrUri(nostrUri);
}`} 桌面應用(macOS Info.plist)
{`CFBundleURLTypes
CFBundleURLName
Nostr Protocol
CFBundleURLSchemes
nostr
`} Android(AndroidManifest.xml)
{`
`} React 組件範例
{`import { nip19 } from 'nostr-tools';
interface NostrLinkProps {
uri: string;
children?: React.ReactNode;
fallbackUrl?: string;
}
function NostrLink({ uri, children, fallbackUrl }: NostrLinkProps) {
const parsed = parseNostrUri(uri);
// 生成顯示文字
const displayText = children || (() => {
if (!parsed) return uri;
switch (parsed.type) {
case 'npub':
case 'nprofile':
const pubkey = parsed.type === 'npub'
? parsed.data
: parsed.data.pubkey;
return \`@\${pubkey.slice(0, 8)}...\`;
case 'note':
case 'nevent':
const id = parsed.type === 'note'
? parsed.data
: parsed.data.id;
return \`note:\${id.slice(0, 8)}...\`;
case 'naddr':
return parsed.data.identifier || 'article';
default:
return uri;
}
})();
const handleClick = (e: React.MouseEvent) => {
// 嘗試開啟 nostr: URI
window.location.href = uri;
// 如果沒有處理器,fallback 到網頁版
if (fallbackUrl) {
setTimeout(() => {
window.location.href = fallbackUrl;
}, 500);
}
};
return (
{displayText}
);
}
// 使用範例
@fiatjaf
`} 網頁閘道器
對於沒有安裝 Nostr 客戶端的用戶,可以使用網頁閘道器來顯示內容:
- njump.me - 通用 Nostr 網頁閘道器
- snort.social - 社交客戶端
- primal.net - 社交客戶端
{`// 將 nostr: URI 轉換為網頁閘道器連結
function toWebUrl(nostrUri: string): string {
const encoded = nostrUri.replace('nostr:', '');
return \`https://njump.me/\${encoded}\`;
}
// 範例
toWebUrl('nostr:npub1...')
// https://njump.me/npub1...`} 最佳實踐
- 優先使用 nprofile/nevent:包含中繼器資訊,確保內容可發現
- 提供 fallback:為沒有 Nostr 客戶端的用戶提供網頁版連結
- 驗證輸入:解析 URI 時要處理錯誤情況
- 拒絕 nsec:永遠不要在 URI 中包含私鑰
- 顯示友善:將冗長的編碼轉換為可讀的顯示文字
相關 NIP
參考資源
已複製連結