內容匯入

本頁內容

EmDash 的匯入系統採用可插拔的來源架構。每個來源負責探測、分析並從特定平台擷取內容。

匯入來源

來源 ID平台探測OAuth完整匯入
wxrWordPress 匯出檔
wordpress-comWordPress.com
wordpress-rest自架 WordPress僅探測

WXR 檔案上傳

最完整的匯入方式。在管理後台直接上傳 WordPress eXtended RSS(WXR)匯出檔。

能力:

  • 所有文章類型(含自訂)
  • 所有 meta 欄位
  • 草稿與私密文章
  • 完整分類階層
  • 媒體附件中繼資料

取得 WXR 檔案:

  1. 在 WordPress 後台前往 工具 → 匯出
  2. 選擇 所有內容 或特定文章類型
  3. 點選 下載匯出檔
  4. .xml 上傳至 EmDash

WordPress.com OAuth

託管於 WordPress.com 的網站可透過 OAuth 連線,無需手動匯出。

  1. 輸入 WordPress.com 網站 URL
  2. 點選 連線 WordPress.com
  3. 在 WordPress.com 彈窗中授權 EmDash
  4. 選擇要匯入的內容

包含:

  • 已發布與草稿內容
  • 私密文章(需授權)
  • 透過 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:準備結構描述

點選 建立結構描述並匯入 以:

  1. 透過 SchemaRegistry 建立新集合
  2. 以正確的欄位型別補上缺少欄位
  3. 建立含索引的內容資料表

步驟 4:執行匯入

內容依序匯入:

  • Gutenberg / HTML 轉為 Portable Text
  • WordPress 狀態對應至 EmDash 狀態
  • WordPress 作者對應至所有權(authorId)與顯示署名
  • 建立並連結分類法
  • 可重複使用區塊(wp_block)以 Sections 匯入
  • 即時顯示進度

作者匯入行為:

  • 若作者對應指向 EmDash 使用者,則所有權歸該使用者,並為同一使用者建立或重用連結署名。
  • 若無使用者對應,則依 WordPress 作者身分建立或重用訪客署名。
  • 匯入項目取得有序署名列表,第一筆為 primaryBylineId

步驟 5:匯入媒體(選用)

內容匯入後,可選擇匯入媒體:

  1. 分析 — 依類型顯示附件數量

    Media found:
    ├── Images: 75 files
    ├── Video: 10 files
    └── Other: 4 files
  2. 下載 — 從 WordPress URL 串流並顯示進度

    Importing media...
    ├── 45 of 89 (50%)
    ├── Current: vacation-photo.jpg
    └── Status: Uploading
  3. 改寫 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],
			},
		}),
	],
});

下一步