WordPressからContentfulへの移行メモ
WordPressからAstro+Contentful+Cloudflareに移行したのでその作業メモ。
なぜ移行したの?
- IE6+jQueryの時代(2010年前後)でフロントエンドの知識が止まっているためアップデートしたかった。
- Twitterの経営が不安定なこともあり、SNS以外の発信媒体を見直す良い機会だった。(Twitterが消えてもブログがあるという謎の安心感)
構成
Astroで始める爆速個人サイト開発 - Speaker Deck を参考に、
- 静的サイトジェネレータ: Astro
- CMS: Contentful
- ホスティング: Cloudflare Pages
を選択しました。何もわからないので猿真似です。ブログということもあり、動的な処理は必要ないため静的サイトとして構築します。
作業
基本はGetting Started 🚀 Astro Documentationを参考に進めます。
プロジェクトの作成
とりあえずJavaScriptパッケージマネージャーが必要らしいのでインストールします。最近は npm
よりも pnpm
がよいとのことなので pnpm
を入れてみます。
$ brew instal pnpm
$ pnpm create astro@latest
上のコードを実行すると、 pnpm
がインストールされた後、以下のように対話プロンプトが実行されるので適当にプロジェクト名などを設定します。 今回は ./ki_chi_blog
をプロジェクト名として新規作成しました。
../Library/pnpm/store/v3/tmp/dlx-22412 | +141 ++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: /Users/kiichi/Library/pnpm/store/v3
Virtual store is at: ../Library/pnpm/store/v3/tmp/dlx-22412/node_modules/.pnpm
../Library/pnpm/store/v3/tmp/dlx-22412 | Progress: resolved 141, reused 0, downloaded 141, added 141, done
╭─────╮ Houston:
│ ◠ ◡ ◠ Let's make the web a better place!
╰─────╯
astro v2.0.10 Launch sequence initiated.
dir Where should we create your new project?
./ki_chi_blog
tmpl How would you like to start your new project?
Include sample files
✔ Template copied
deps Install dependencies?
Yes
✔ Dependencies installed
git Initialize a new git repository?
Yes
✔ Git initialized
ts Do you plan to write TypeScript?
Yes
use How strict should TypeScript be?
Strict
✔ TypeScript customized
next Liftoff confirmed. Explore your project!
Enter your project directory using cd ./ki_chi_blog
Run pnpm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.
Stuck? Join us at https://astro.build/chat
╭─────╮ Houston:
│ ◠ ◡ ◠ Good luck out there, astronaut! 🚀
╰─────╯
以下のコマンドを実行すると、ローカルサーバーが立ち上がり、 localhost:3000
で接続できるようになります。
$ pnpm run dev
> [email protected] dev /Users/kiichi/projects/ki_chi_blog
> astro dev
🚀 astro v2.0.10 started in 23ms
┃ Local http://localhost:3000/
┃ Network use --host to expose
エディタ設定 (Emacs)
エディタの設定ですが、Emacsで拡張子 .astro
のファイルを web-mode
で開けるようにしておけば良さそうです。LSPも動きます。 ついでにTypeScriptの設定もしておきます。 私は leaf.el
を使っているので、以下を init.el
に追記しました。
;; TypeScript
(leaf typescript-mode
:ensure t
:custom ((typescript-indent-level . 2))
:mode "\\.ts\\'"
:hook ((typescript-mode-hook . lsp-deferred))
)
;; Astro
(leaf web-mode
:ensure t
:mode "\\.astro\\'"
:custom ((web-mode-markup-indent-offset . 2)
(web-mode-css-indent-offset . 2)
(web-mode-code-indent-offset . 2))
:hook ((web-mode-hook . lsp-deferred))
)
最初にAstroファイルを開いた時にLSPで以下のようなwarningが出ました。
Unable to find typescript server path for astro-ls. Guessed: [node-module以下の長いパス]
この場合は typescript
をインストールしてあげればOKのようです。
$ pnpm install -D typescript
テンプレートの編集
初期テンプレートが src
ディレクトリ以下にあるのでここを参考にしつついじっていきます。
ContentfulでMarkdownで入力した結果をHTMLに変換
Contentfulの記事はリッチテキストではなくMarkdownで書きたいので、Contentfulから受け取ったMarkdown文字列をHTMLに変換します。 また remark-prism
を使ってコードのハイライトをしたり、 rehype-rewrite
で画像の最適化のためのタグの置換も行います。 これらを可能にする remark
や rehype
パッケージ群についても今回初めて知りました。以下サイトが参考になりました。
例えば、以下の関数で引数 markdown
で与えられる文字列をHTML文字列にします。
export async function processMdImg(markdown) {
const result = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkMath)
.use(remarkPrism)
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeSlug)
.use(rehypeRewrite, {
selector: "img",
rewrite: (node, index, parent) => {
const imgWidths = [640, 800, 1400];
if(node.type == 'element') {
const imgSrc = node.properties.src;
const imgAlt = node.properties.alt;
const imgUrl = new URL("https:" + imgSrc);
if(imgUrl.hostname == 'images.ctfassets.net') {
// Rewrite node
node.tagName = "picture";
node.properties = {};
node.children = [
// webp
{
type: "element",
tagName: "source",
properties: {
type: "image/webp",
srcset: imgWidths.map(w => imgSrc + "?w=" + w + "&fm=webp " + w + "w").join(","),
sizes: "(max-width: 768px) calc(100vw - 3em), (max-width: 1376px) calc(75vw - 8em), 800px",
},
},
// fallback
{
type: "element",
tagName: "img",
properties: {
srcset: imgWidths.map(w => imgSrc + "?w=" + w + "&fm=jpg&fl=progressive " + w + "w").join(","),
sizes: "(max-width: 768px) calc(100vw - 3em), (max-width: 1376px) calc(75vw - 8em), 800px",
src: imgSrc,
alt: imgAlt,
loading: "lazy",
decoding: "async"
},
}
];
// link to original image
parent.children = [{type: 'element', tagName: 'a', properties: {href: imgSrc}, children: parent.children}]
}
}
}
})
.use(rehypeFormat)
.use(rehypeStringify)
.process(markdown)
return result.toString()
}
上記関数によって、Contentfulから供給されたMarkdown形式の文字列をHTMLに変換します。
個人的に嬉しいのは rehypeKatex
によって数式を事前にタイプセットできるという点です。 今までWordpressでもJaveScriptライブラリの MathJax
を読み込めばLaTeX形式で書かれた数式を表示させることはできたのですが、 これだとページ表示時にそこそこ大きいJavaScriptをロードする必要があり、遅延が気になっていました。 rehypeKatex
はビルド時に数式もタイプセットしてくれるのでそのストレスが無くなります。
また、上のコードでそこそこ記述が多いのは画像に関する処理です。Contentfulの画像はURLにクエリを与えることでサイズ変更ができるので、以下のページを参考に画像をレスポンシブ対応にしました。
コンポーネントの作成
Astroはコンポーネントベースのフレームワークらしいので、各コンポーネントを作成していきます。 正直コンポーネント志向がよくわかっていないので、これでいいのか全く自信ありませんが、例えば以下のコード PostsList.astro
は記事のリストを生成するコンポーネントとなります:
---
import { contentfulClient } from "@lib/contentful";
import type { blogPost } from "@lib/contentful";
const entries = await contentfulClient.getEntries<blogPost>({content_type: "blogPost"});
const posts = entries.items.map((item) => {
const { title, category, tags, content, publishedAt, slug, fromWordPress, wordPressPermalink } = item.fields;
const regex = /(^|\s).+?(。)/;
const firstSentence = content.match(regex)?.[0] || "";
return {
title,
category,
tags,
content,
firstSentence,
publishedAt: new Date(publishedAt).toISOString().split('T')[0], // YYYY-MM-DD
slug,
fromWordPress,
wordPressPermalink
};
});
const sortedPosts = posts.sort((a, b) => {
return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
});
// トップページに表示する記事の数
const numberOfShownPosts = 10;
---
<ul>
{sortedPosts.slice(0, numberOfShownPosts).map((post) => (
<li>
<time>{post.publishedAt}</time>
<h2><a href={`/posts/${post.slug}/`}>{post.title}</a></h2>
<div><p>{post.firstSentence}</p></div>
</li>
))}
</ul>
<style>
ul {
list-style: none;
}
li {
padding: 1.5rem 1rem 2rem;
border-top: 1px solid #ddd;
}
li:last-child {
border-bottom: 1px solid #ddd;
}
li time {
display: block;
font-size: 1.4rem;
color: #666;
}
li h2 {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
li h2 a {
font-size: 1.4rem;
font-weight: bold;
}
li div {
word-break: break-all;
}
li div p {
color: #666;
}
</style>
上で定義された <PostsList>
を他のAstroファイルから呼ぶことでコンポーネントとして扱うことができます。本ブログのトップページの記事一覧をこれで作成しています。 各コンポーネントやレイアウトの中で定義される <style>
はそれぞれがCSS Moduleとして独立した名前空間で定義されます。 そのため、コンポーネントAを修飾するためのCSSとコンポーネントBを就職するためのCSSが両方のコンポーネントを呼び出す際に衝突することを避けられます。
データの移行
過去のWordPressで作ったデータをContentfulに移行します。 jonashcroft/wp-to-contentful: Script to Migrate WordPress Posts to Contentfulを使いました。基本的にはREADMEの通りで動きます。 (一部、リージョンが en-GB
固定になっている部分があったので、Contentfulのモデルに合わせて en-US
や ja-JP
に変えました)
git clone
して、内部を適当に書き換えて実行するだけでHTML形式の記事がちゃんとMarkdown形式でContentfulに登録されていました。
Cloudflareにデプロイ
デプロイ先はCloudflare Pagesにしました。GitHubと簡単に接続できるので、上記コードを入れたレポジトリを指定してあげるだけでデプロイはOKです。 デフォルトの設定で master
(or main
)ブランチにpushされる度にデプロイされるようになっています。
Cloudflare Pages Functionを使ったリダイレクトの設定
今回からブログのURLを https://ki-chi.jp/posts/hogehoge/
のような形式、つまり /posts
以下へパーマリンクをつける形式に変更しました。 今までは各投稿のURLは https://ki-chi.jp/?p=3
というWordPressデフォルトの /?=(ページID)
の形式でした。
URLの形式を単純に変更してしまうと過去のリンクがリンク切れになってしまうため、以前の形式のURLに対するアクセスを新形式のURLへリダイレクトする必要があります。 例えば上の https://ki-chi.jp/?p=3
へアクセスした場合あれば、新しいブログの https://ki-chi.jp/posts/wp3/
へリダイレクトされてほしいわけです。
まず初めにこちらの記事を参考に redirects
ファイルに記述するやり方を試してみました。 ただリダイレクト元(例: /?p=3
)にクエリ文字列の"?"が含まれていたせいか、なぜか動かず。次善の策として、Cloudflare Pages FunctionとCloudflare Workers KVを使ってリダイレクトを実装しました。
具体的には、JavaScriptファイルを _workers.js
としてルートディレクトリにデプロイしました。これでCloudflare Pages Functionとして認識されるようです。 中身は下記の通りで、リクエストURLのクエリ文字列がKVに入っているkeyに一致すれば、それに対応したvalueに指定されたパスへ301で転送するというシンプルなものです。 Cloudflare Pages Functionの詳しい使い方はこちらのドキュメントを参考にしました。また、下記のJavaScriptはこちらの記事を参考にしました。
export default {
async fetch(request, env) {
const url = new URL(request.url);
const origin = url.origin; // "http(s)://example.com"
const pathname = url.pathname; // "/"
// "?" 以降を取り除いた文字列を得る
const queryString = url.search; // "?p=x"
const path = pathname + queryString; // "/?p=x"
const redirect_path = await env.REDIRECT_LIST.get(path); // "/posts/wpX/"
if (redirect_path) {
return new Response(null, {
'status': 301,
'headers': {
'Location': origin + redirect_path,
}
});
}
// Otherwise, serve the static assets.
// Without this, the Worker will error and no assets will be served.
return env.ASSETS.fetch(request);
},
};
Cloudflare Workers KVへのデータ登録
なお、今回は100記事程度のリダイレクト定義があり、KVの管理画面から手作業で打ち込むには厳しい量だったので、Cloudflare用のCLIツール wrangler
を使ってJSONファイルをKVに一括登録しました。
$ wrangler login
$ wrangler kv:bulk put redirects.json --namespace-id [KVのNamespace ID]
この redirects.json
は下記フォーマットで記述されるJSONファイルです。
[
{"key": "/?p=3", "value": "/posts/wp3/"},
{"key": "/?p=15", "value": "/posts/wp15/"},
...
{"key": "/?p=1119", "value": "/posts/wp1119/"}
]
上記のJSONファイルをKVに登録後、無事リダイレクトが動作するようになりました。
その他
最後に細かい設定をいくつか行いました。
- Webhookの登録
- GitHubへのpushだけでなく、Contentfulでの記事更新によってCloudflareへビルド&デプロイされるために、Cloudflareで発行したWebhook URLをContentfulへ登録しました
- 独自ドメインの使用
- もともと使っていたドメイン会社のDNSサーバーをCloudflareのDNSサーバーに切り替えました
やりのこし
上記で一通りの移行は終わりました。今回の移行はだいぶJavaScript周りの勉強になりました。
以下は手が空いた時に順次対応していこうと思います:
- タグごとにまとめた記事のページ
- おそらくこのページの通りにやれば作れるはず
- HTMLの埋め込みに対応したMarkdown投稿。Twitter投稿の埋め込みとか。
- おそらく
remark-raw
というのを使えばいいはず
- おそらく
- RSSフィードの対応
- ここを見る限り、
@astrojs/rss
というのを使えばよさそう
- ここを見る限り、