p-hone p-hone.info

ブログをAstroに移行した

このブログ(p-hone.info)の基盤をAstroへ移行した。 以前にHugoへ移行してからの2回目の移行となる。

なぜか? 従来のHugoでも動作に問題はなく無理に変える必要はなかったが、 新しい技術で遊んでみたいという好奇心から個人サイト(ブログ)は ちょうどいい 練習台になるので早速実行に移したという流れだ。 とはいってもブログの外観はほぼ変わっていない。しかし内部的にはかなり変わったし、将来的に色々な追加要素を入れやすくなった(入れる予定です!)。

HugoもAstroもいわゆる静的サイトジェネレーター(SSG)であり、最終的にHTML(とCSS)を出力する点は同じだ。 しかし、Astroの方がより自由度が高い。自由度が高いということは、その分自前でやらなければならない箇所が多い。 たとえばHugoでは {{< youtube ... >}} のように書くと自動的にYouTubeの動画を埋め込めるが、Astroにはそのような機能がない。

代わりにAstroはMarkdownの変換をプラグインで拡張できる。 よって、プラグインを探したり自作する手間がかかる分、やろうと思えばなんでもできる自由度の高さがある。 お手軽さを取るか自由度を取るかは個人の好みが出るところだが、今回は新技術のお試しという動機があったのでAstroを取った。

移行にあたって特殊な対応をしたものをいくつかメモとして以下に記す。

YouTubeの埋め込み対応

前述の通りHugoでは {{< youtube ... >}} の記法でYouTube動画が埋め込めたが、Astroではそうはいかないのでプラグインを探した。 remark-embedder というプラグインが良さそうだ。特定パターンのURLを埋め込みに変換する機能を持つ。

このプラグイン単体ではURLを検出する枠組みだけなので、remark-embedderの変換ルール(Transformer)を作り、YouTubeのURLを変換するようにした。

// src/utils/youtube.ts
import { type Transformer } from '@remark-embedder/core'

// Thanks to eleventy-plugin-youtube-embed
// https://github.com/gfscott/eleventy-plugin-youtube-embed/blob/main/lib/extractMatches.js
const urlPattern =
	/(?=(\s*))\1(?:<a [^>]*?>)??(?=(\s*))\2(?:https?:\/\/)??(?:w{3}\.)??(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|shorts\/)??([A-Za-z0-9-_]{11})(?:[^\s<>]*)(?=(\s*))\4(?:<\/a>)??(?=(\s*))\5/;

export const YouTubeTransformer: Transformer = {
    name: 'youtube',
    shouldTransform(url) {
        return url.match(urlPattern) !== null
    },
    getHTML(url, config) {
        const videoId = extractVideoId(url)
        const iframeUrl = `https://www.youtube.com/embed/${videoId}`
        return `<div style="position: relative; padding-bottom: 56.25%"><iframe src="${iframeUrl}" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0; border: 0"></iframe></div>`
    },
}

function extractVideoId(url: string): string | undefined {
	const match = url.match(urlPattern);
	return match?.[3];
}

そして次のようにAstroの設定ファイルに組み込む。

// astro.config.mjs
+import remarkEmbedder from '@remark-embedder/core'
+import { YouTubeTransformer } from './src/utils/youtube';

export default defineConfig({
  markdown: {
+    remarkPlugins: [
+      [remarkEmbedder.default, {transformers: [YouTubeTransformer]}],
+    ],

これで、Markdown内のYouTube URLが自動的に埋め込み動画プレイヤーがに変換される。

https://www.youtube.com/watch?v=sgLj1GzWwnU

↑これが ↓こうなる

多少手間がかかったが一つ嬉しい点がある。 ブログ記事の生データから特定のSSG固有の記法を消せたことだ。 これで今後もし他の基盤に移行するとなっても、Astro固有の記法ではないため引っ越しがしやすくなる。

RSSフィードの生成

Hugoは何もしなくともRSSフィードを自動で作ってくれるが、 RSSリーダーで表示すると本文の文字だけが詰め込まれているような状態で読みづらかった。

AstroでRSSフィードを出すには専用の対応が必要だ。 しかし、MarkdownをHTMLに変換する作業を自前でカスタマイズできるため、本文をちゃんとしたHTMLで出力できるようになった。

次のようにmarkedというライブラリを使って本文をHTMLに変換したものをRSSフィードに乗せるようにした。

// src/pages/index.xml.ts
import rss from '@astrojs/rss'
import { title, description } from '../site-config'
import type { APIContext } from 'astro'
import { marked } from "marked";
import { getPosts } from '../utils/contents';

export async function GET(context: APIContext) {
    const posts = await getPosts({withDrafts: false})
    const items = await Promise.all(posts.map(async post => ({
        title: post.data.title,
        pubDate: post.data.date,
        description: post.data.description,
        link: `/posts/${post.slug}/`,
        content: await marked.parse(post.body),
    })))
    return rss({
        title,
        description,
        site: context.site?.origin ?? '',
        items,
    })
}