EmDash 的匯入系統採用可插拔的來源架構。每個來源負責探測、分析並從特定平台擷取內容。
匯入來源
| 來源 ID | 平台 | 探測 | OAuth | 完整匯入 |
|---|---|---|---|---|
wxr | WordPress 匯出檔 | 否 | 否 | 是 |
wordpress-com | WordPress.com | 是 | 是 | 是 |
wordpress-rest | 自架 WordPress | 是 | 否 | 僅探測 |
WXR 檔案上傳
最完整的匯入方式。在管理後台直接上傳 WordPress eXtended RSS(WXR)匯出檔。
能力:
- 所有文章類型(含自訂)
- 所有 meta 欄位
- 草稿與私密文章
- 完整分類階層
- 媒體附件中繼資料
取得 WXR 檔案:
- 在 WordPress 後台前往 工具 → 匯出
- 選擇 所有內容 或特定文章類型
- 點選 下載匯出檔
- 將
.xml上傳至 EmDash
WordPress.com OAuth
託管於 WordPress.com 的網站可透過 OAuth 連線,無需手動匯出。
- 輸入 WordPress.com 網站 URL
- 點選 連線 WordPress.com
- 在 WordPress.com 彈窗中授權 EmDash
- 選擇要匯入的內容
包含:
- 已發布與草稿內容
- 私密文章(需授權)
- 透過 API 取得的媒體檔
- REST API 公開的自訂欄位
WordPress REST API 探測
輸入 URL 後,EmDash 會探測網站、辨識 WordPress 並顯示可用內容:
Detected: WordPress 6.4
├── Posts: 127 (published)
├── Pages: 12 (published)
└── Media: 89 files
Note: Drafts and private content require authentication
or a full WXR export.
REST 探測僅供參考。完整匯入建議使用 WXR 上傳或(針對 WordPress.com)OAuth。
匯入流程
所有來源遵循相同流程:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Connect │────▶│ Analyze │────▶│ Prepare │────▶│ Execute │
│ (probe/ │ │ (schema │ │ (create │ │ (import │
│ upload) │ │ check) │ │ schema) │ │ content) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
步驟 1:連線
輸入要探測的 URL,或直接上傳檔案。
URL 探測會並行執行所有已註冊來源。信賴度最高的比對決定建議的下一步:
- WordPress.com 網站 → 提供 OAuth 連線
- 自架 WordPress → 顯示匯出說明
- 未知 → 建議上傳檔案
步驟 2:分析
來源解析內容並檢查結構描述相容性:
Post Types:
├── post (127) → posts [New collection]
├── page (12) → pages [Existing, compatible]
├── product (45) → products [Add 3 fields]
└── revision (234) → [Skip - internal type]
Required Schema Changes:
├── Create collection: posts
├── Add fields to pages: featured_image
└── Create collection: products
每種文章類型會顯示狀態:
| 狀態 | 意義 |
|---|---|
| Ready | 集合已存在且欄位相容 |
| New collection | 將自動建立 |
| Add fields | 集合已存在,將補上缺少的欄位 |
| Incompatible | 欄位型別衝突(需手動處理) |
步驟 3:準備結構描述
點選 建立結構描述並匯入 以:
- 透過 SchemaRegistry 建立新集合
- 以正確的欄位型別補上缺少欄位
- 建立含索引的內容資料表
步驟 4:執行匯入
內容依序匯入:
- Gutenberg / HTML 轉為 Portable Text
- WordPress 狀態對應至 EmDash 狀態
- WordPress 作者對應至所有權(
authorId)與顯示署名 - 建立並連結分類法
- 可重複使用區塊(
wp_block)以 Sections 匯入 - 即時顯示進度
作者匯入行為:
- 若作者對應指向 EmDash 使用者,則所有權歸該使用者,並為同一使用者建立或重用連結署名。
- 若無使用者對應,則依 WordPress 作者身分建立或重用訪客署名。
- 匯入項目取得有序署名列表,第一筆為
primaryBylineId。
步驟 5:匯入媒體(選用)
內容匯入後,可選擇匯入媒體:
-
分析 — 依類型顯示附件數量
Media found: ├── Images: 75 files ├── Video: 10 files └── Other: 4 files -
下載 — 從 WordPress URL 串流並顯示進度
Importing media... ├── 45 of 89 (50%) ├── Current: vacation-photo.jpg └── Status: Uploading -
改寫 URL — 自動以新 URL 更新內容
媒體匯入使用內容雜湊(xxHash64)去重。多篇文章共用同一圖片時只存一份。
來源介面
匯入來源實作標準介面:
interface ImportSource {
/** Unique identifier */
id: string;
/** Display name */
name: string;
/** Probe a URL (optional) */
probe?(url: string): Promise<SourceProbeResult | null>;
/** Analyze content from this source */
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
/** Stream content items */
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
}
輸入類型
來源可接受不同輸入:
// File upload (WXR)
{ type: "file", file: File }
// URL with optional token (REST API)
{ type: "url", url: string, token?: string }
// OAuth connection (WordPress.com)
{ type: "oauth", url: string, accessToken: string }
正規化輸出
所有來源產出相同的正規化格式:
interface NormalizedItem {
sourceId: string | number;
postType: string;
status: "publish" | "draft" | "pending" | "private" | "future";
slug: string;
title: string;
content: PortableTextBlock[];
excerpt?: string;
date: Date;
author?: string;
authors?: string[];
categories?: string[];
tags?: string[];
meta?: Record<string, unknown>;
featuredImage?: string;
}
API 端點
匯入系統公開下列端點:
探測 URL
POST /_emdash/api/import/probe
Content-Type: application/json
{ "url": "https://example.com" }
傳回偵測到的平台與建議動作。
分析 WXR
POST /_emdash/api/import/wordpress/analyze
Content-Type: multipart/form-data
file: [WordPress export .xml]
傳回文章類型分析與結構描述相容性。
準備結構描述
POST /_emdash/api/import/wordpress/prepare
Content-Type: application/json
{
"postTypes": [
{ "name": "post", "collection": "posts", "enabled": true }
]
}
建立集合與欄位。
執行匯入
POST /_emdash/api/import/wordpress/execute
Content-Type: multipart/form-data
file: [WordPress export .xml]
config: { "postTypeMappings": { "post": { "collection": "posts" } } }
將內容匯入指定集合。
匯入媒體
POST /_emdash/api/import/wordpress/media
Content-Type: application/json
{
"attachments": [{ "id": 123, "url": "https://..." }],
"stream": true
}
在下載/上傳期間以 NDJSON 串流回傳進度。
改寫 URL
POST /_emdash/api/import/wordpress/rewrite-urls
Content-Type: application/json
{
"urlMap": { "https://old.com/image.jpg": "/_emdash/media/abc123" }
}
以新媒體 URL 更新 Portable Text 內容。
錯誤處理
可復原錯誤
- 網路逾時 — 遞減延遲重試
- 單筆剖析失敗 — 記錄並略過,匯入繼續
- 媒體下載失敗 — 標記為需手動處理
致命錯誤
- 無效的檔案格式 — 匯入停止並顯示錯誤
- 資料庫連線中斷 — 匯入暫停,可恢復
- 儲存配額超過 — 匯入停止並顯示用量
錯誤報告
匯入結束後:
Import Complete
✓ 125 posts imported
✓ 12 pages imported
✓ 85 media references recorded
⚠ 2 items had warnings:
- Post "Special Characters ñ" - title encoding fixed
- Page "About" - duplicate slug renamed to "about-1"
✗ 1 item failed:
- Post ID 456 - content parsing error (saved as draft)
失敗項目會以草稿儲存,原始內容留在 _importError 供複查。
建置自訂來源
為其他平台建立來源:
import type { ImportSource } from "emdash/import";
export const mySource: ImportSource = {
id: "my-platform",
name: "My Platform",
description: "Import from My Platform",
icon: "globe",
canProbe: true,
async probe(url) {
// Check if URL matches your platform
const response = await fetch(`${url}/api/info`);
if (!response.ok) return null;
return {
sourceId: "my-platform",
confidence: "definite",
detected: { platform: "my-platform" },
// ...
};
},
async analyze(input, context) {
// Parse and analyze content
// Return ImportAnalysis
},
async *fetchContent(input, options) {
// Yield NormalizedItem for each content piece
for (const item of items) {
yield {
sourceId: item.id,
postType: "post",
title: item.title,
content: convertToPortableText(item.body),
// ...
};
}
},
};
在 EmDash 設定中註冊來源:
import { mySource } from "./src/import/custom-source";
export default defineConfig({
integrations: [
emdash({
import: {
sources: [mySource],
},
}),
],
});
下一步
- WordPress Migration — 完整 WordPress 遷移指南
- Plugin Porting — 將 WordPress 外掛移植到 EmDash