内容导入

本页内容

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],
			},
		}),
	],
});

下一步