因為我原先使用的 CMS 是 Contentful,之前寫了也有 5x 篇文章,當然是可以手動一篇一篇去搬,不過可以的話還是能找到直接 Contentful 匯出再匯入 Sanity 的方法比較好。
Sanity 官方有個套件叫做 contentful-to-sanity,專門就是來做從 Contentful 的轉移到 Sanity 的工作,用法也不是太複雜。
因為這工具不是常常會用的,個人覺得只要使用 npx 在需要的時候及時安裝、使用就好了
npx contentful-to-sanity@latest --help
Usage: contentful-to-sanity [options] [command]
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
export [options] <outdir>
schema [options] <outdir>
dataset [options] <outdir>
batch [options] <outdir> Runs the export, schema and dataset commands in sequence.
help [command] display help for command
第一件事情要做的是將 Contentful 的資料匯出
匯出的資料路徑是在我們的 Sanity 專案
.
├── next-app
└── sanity-project // <- 資料匯出到這
執行匯出指令:
npx contentful-to-sanity -s <space-id> -t <management-token> -a <access-token> ./sanity-project
匯出後會走以下幾個檔案:contentful.json
, contentful.published.json
, dataset.ndjson
, schema.ts
,而要這次用到的只有 dataset.ndjson
這個檔案:
檔案產生後再到檔案所在目錄 ( 也是 Sanity 專案目錄 )
cd sanity-project
// 執行 Sanity import 指令,並且將 ndjson 作為導入檔案
sanity dataset import ./dataset.ndjson
✅ [100%] Fetching available datasets
? Select target dataset (Use arrow keys)
❯ Create new dataset // <- 可以開一個新的 dataset 作為測試
──────────────
production // <- 也可以直接匯入到 production 環境
執行後檔案跟文章內容就都匯入了。
sanity dataset import ./dataset.ndjson
✅ [100%] Fetching available datasets
? Select target dataset Create new dataset
? Name your dataset: from-contentful
╭───────────────────────────────────────────────╮
│ │
│ Importing to: │
│ projectId: zxxxxxxg │
│ dataset: production. │
│ │
╰───────────────────────────────────────────────╯
✅ [100%] Reading/validating data file (144ms)
✅ [100%] Importing documents (1.85s)
✅ [100%] Importing assets (files/images) (36.84s)
✅ [100%] Setting asset references to documents (1.06s)
✅ [100%] Strengthening references (604ms)
Done! Imported 56 documents to dataset "from-contentful"
但其實在匯入的時候是有些 Contentful 跟 Sanity 格式不符合的問題。
相對於 Sanity,Contentful 的格式是很複雜的。像是 Contentful 的 Markdown 格式會變成像這樣:
{
"body": [
{
"_key": "8c383bccf605",
"children": [
{
"_type": "span",
"marks": [],
"text": "@WebServlet",
"_key": "8c383bccf6050"
}
],
"markDefs": [],
"_type": "block",
"style": "h2"
},
{
"_key": "8947703c7e75",
"children": [
{
"_type": "span",
"marks": [
"code"
],
"text": "@WebServlet",
"_key": "8947703c7e750"
},
{
"_type": "span",
"marks": [],
"text": " 是用來定義這個 Servlet 的屬性, 被定義的 class 通常繼承 javax.servlet.http.HttpServlet 這個 class",
"_key": "8947703c7e751"
}
],
"markDefs": [],
"_type": "block",
"style": "normal"
},
// ...
}
可以看得出來被 Contentful 做了一些格式的處裡,這是沒辦法直接 import 到 Sanity 的,必須要有個轉譯的方法才能匯入 Sanity。
我這邊的解法也不怕大家笑,我是直接到 Contentful 的後台將格式從 Markdown
改成 Multiple line
,這樣再輸出一次就會是字串格式了。
再來是有些欄位可能 Sanity 跟 Contentful 的名稱沒有對齊,像是:
content
,但是 Contentful 中叫做 body
,之類的欄位名稱沒對齊slug
欄位的資料多了一層 { current: "..." }
等等的資料結構問題這些 export, import 套件並沒有現成的方法支援解決,要解決這些問題也就必須手動更改名稱或是自己另外處理掉了。
由於匯入 Sanity 的資料格式是 ndjson
格式,不是很難,簡單說就是用分行來隔開資料的陣列格式吧,所以只要解析 ndjson
之後做些資料格式的處裡跟欄位對應,做完後再輸出回去成一個新的 ndjson
檔案就完成了。
一樣不怕大家笑我,這種一次性的操作就沒再跟他注意什麼程式碼的漂亮了。
我的作法是用 node 寫一個簡單的腳本來處裡,
因為不是什麼複雜的操作,在這邊就不特別解釋了,直接腳本把原始碼放上來。
import fs from "fs";
import ndjson from "ndjson";
import moment from "moment";
// @ts-ignore - 沒有支援的 Typescript 型別
import arrayToNdjson from "array-to-ndjson";
type SanityType = {
_type: "blogPost";
_id?: string;
title: string;
slug: string;
subtitle: string;
heroImage: any;
content: string;
publishedAt: string;
tags: string[];
};
async function getContentfulJson() {
const arr: SanityType[] = [];
return new Promise((resolve, reject) => {
const promises: any[] = [];
fs.createReadStream("dataset.ndjson", "utf8")
.pipe(ndjson.parse())
.on(
"data",
(contentfulObj: {
_type: string;
_id: string;
slug: { current: string };
title: string;
body: any;
description: { children: { text: string }[] }[];
heroImage: any;
publishDate: string;
tags: string[];
}) => {
if (contentfulObj._type === "blogPost") {
const sanityObj: SanityType = {
_type: "blogPost",
title: "",
slug: "",
subtitle: "",
heroImage: null,
content: "",
publishedAt: "",
tags: [],
};
sanityObj._id = contentfulObj._id;
sanityObj.title = contentfulObj.title;
sanityObj.slug = contentfulObj.slug.current;
sanityObj.subtitle = contentfulObj.description[0].children[0].text;
sanityObj.heroImage = contentfulObj.heroImage;
sanityObj.content = contentfulObj.body;
sanityObj.publishedAt = moment(contentfulObj.publishDate).format(
"YYYY-MM-DD"
);
sanityObj.tags = contentfulObj.tags;
arr.push(sanityObj);
}
}
)
.on("end", async () => {
resolve(arr);
});
});
}
async function main() {
const contenfulNDJson: any = await getContentfulJson();
arrayToNdjson(contenfulNDJson).pipe(fs.createWriteStream("newDataset.ndjson"));
}
main();
最後再把 newDataset.ndjson
匯入到 Sanity 就完成了。
花花綠綠的文章都進來了~~