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