サムネイル

ブログをAstroに移行しました

#Astro#GitHub#Web開発#Windows
投稿日:

今までこのブログでは独自に開発したCMSと静的サイトジェネレーターを使っていましたが、Astroに移行しました。この記事では、Astroに移行した理由と、つまづいたところを紹介します。

Astroとは?

Astroは、Markdownを用いて記事を執筆できる静的サイトジェネレーターです。Astroデフォルトでスクリプトが不要なので、高速なWebサイトを構築できます。詳しくは公式ドキュメントをご覧ください。

独自システムを辞めた理由

今までこのブログでは、独自に開発したCMSと静的サイトジェネレーターを使っていました。このシステムは2021年6月に実施した全面リニューアルで導入し、2022年2月に再構築したものです。

Markdownで書いた記事を、テンプレートを使ってHTMLに変換するというシステムです。その過程で、HTMLや画像の最適化をしていました。

しかし、このシステムにはいくつか問題がありました。

まず、主に画像の最適化処理において多重forループがあり、ソースコードが複雑になっていました。また、新しい記事のデータベースへの登録はコマンドラインから可能ですが、既存の記事に関するデータの変更や削除には、手動でデータベースを編集する必要がありました。

さらに、そもそも独自開発したものなので、信頼性とパフォーマンスの面で不安がありました。

以上の理由から、独自システムを辞めることにしました。

Astroを選んだ理由

では、数あるCMSや静的サイトジェネレーターの中から、なぜAstroを選んだのでしょうか。

まず、システムを選定する上で必須の条件として、次のようなものがありました。

  1. 静的サイトジェネレーターであること
  2. Markdownで記事を執筆できること
  3. 特定のライブラリーに依存しないこと
  4. 拡張性やカスタマイズ性が高いこと

「静的サイトジェネレーターであること」については、このブログで利用しているGitHub Pagesが静的サイトにのみ対応しているためです。

「Markdownで記事を執筆できること」については、理由が2つあります。1つ目は、既存の記事がMarkdownで作成されているためです。2つ目は、Markdownだとさまざまな環境で気軽に執筆できるためです。

「特定のライブラリーに依存しないこと」については、これによってJavaScriptのコードの量を減らせ、サイトのパフォーマンスを向上できるためです。ライブラリーを使用すると、その分だけページの読み込みが遅くなります。

主要な静的サイトジェネレーターとしては、Next.jsNuxt.jsなどがあります。しかし、Next.jsはReactベースですし、Nuxt.jsはVue.jsベースです。この時点で「特定のライブラリーに依存しないこと」に当てはまらないため、選択肢から外しました。

静的サイトジェネレーターを選定する上で参考になったのが、Jamstackのサイトです。Jamstackのサイトでは、主要な静的サイトジェネレーターを条件に合わせて絞り込めるようになっています。

このページで[Templates]としてMarkdownを指定すると、次のようになりました。

Markdownを使った静的サイトジェネレーターの一覧。

GitHubのスター順で並べると、「Docusaurus」「Slate」「Astro」「Docsify」「Eleventy」「mdBook」といったものが上位になりました。この中で、上から順番に調査しました。

まず、DocusaurusはReactベースなので除外しました。

Slateについては、ブログというよりもドキュメント用ですし、Rubyで作られているので除外しました。Ruby自体は悪くないのですが、ライブラリーのインストールなどに使っているNode.jsで統一できた方が便利だからです。

最終的に、条件に合致しておりGitHubのスター数も多いためAstroを選びました。

Astroへの移行でつまづいたところ

Astroはとても使いやすく、ブログの基礎部分の移行作業はたったの30分で完了しました。他の細かい部分の調整や移行などを含めると、最終的に6日間で移行できました。

基礎部分が30分で終わったのに最終的に6日間かかっていることからも分かるとおり、いくつかつまづいた点があるので紹介します。

画像の相対パス

Astroはv3.0で正式サポートされたassets機能により、Markdownファイル内で画像を相対パスで指定できます。

しかし、相対パスを記述する際、./は省略できないようです。つまり、次の例では、上は正しく動作しますが、下は正しく動作しません。

1
<!-- 正しく動く -->
2
![画像](./image.png)
3
4
<!-- 正しく動かない -->
5
![画像](image.png)

相対パスの./を省略すると、「Octal literal in strict mode (Note that you need plugins to import files that are not JavaScript)」というエラーが発生します。

Astroのソースコードを確認したところ、これはWindows環境で発生する問題のようです。軽くしか調査していませんが、おおむね次のような理由で発生しているようです。

この問題を回避するため、既存の記事で./が省略されている相対パスに./を追加するスクリプトを作って修正しました。

Note記法の対応

今までこのブログでは、MarkdownパーサーとしてMarkedを採用していました。また、QiitaのNote記法を使うために、qnote-parserというプラグインを開発して使っていました。

しかし、AstroではMarkdownパーサーとしてremarkを採用しており、このプラグインを利用できません。

そこで、qiita-to-mdというパッケージの内部で使われているプラグインを利用することにしました。astro.config.mjsに次のように記述することで、Note記法を利用できるようになります。

1
import { defineConfig } from 'astro/config';
2
import { RemarkNotePlugin } from "@masatomakino/qiita-to-md/bin/plugin/RemarkNotePlugin";
3
4
export default defineConfig({
5
site: "<サイトのURL>",
6
markdown: {
7
remarkPlugins: [RemarkNotePlugin.plugin],
8
remarkRehype: {
9
handlers: {
10
note: RemarkNotePlugin.rehypeNoteHandler
11
}
12
}
13
}
14
});

注意点として、記事執筆時点でこのプラグインには改段落やリストを含むNote記法を正しく変換できないバグがあります。

また、このプラグインはQiitaの記事をダウンロードしてMarkdownとHTMLとして保存するためのものなので、単体で使うことが想定されていません。そのため、改行コードとして\r\nではなく\nを使用する必要があります。これについても、スクリプトでまとめて置き換えました。

frontmatterの画像のパス

Astroのコンテンツコレクションと画像についてのドキュメントで、「ブログ記事のカバー画像など、コンテンツコレクションのエントリに関連付けられた画像を、現在のフォルダからの相対パスを使ってフロントマターに宣言できます」と書かれています。

ドキュメントでは例として、次のコードが掲載されています。

1
---
2
import { Image } from "astro:assets";
3
import { getCollection } from "astro:content";
4
const allBlogPosts = await getCollection("blog");
5
---
6
7
{
8
allBlogPosts.map((post) => (
9
<div>
10
<Image src={post.data.cover} alt={post.data.coverAlt} />
11
<h2>
12
<a href={"/blog/" + post.slug}>{post.data.title}</a>
13
</h2>
14
</div>
15
))
16
}

このことから、てっきり<Image />コンポーネントを<img>タグに置き換えても同様に動作すると思っていました。しかし、実際には次のように記述する必要があります。

1
---
2
import { Image } from "astro:assets";
3
import { getCollection } from "astro:content";
4
const allBlogPosts = await getCollection("blog");
5
---
6
7
{
8
allBlogPosts.map((post) => (
9
<div>
10
<img src={post.data.cover.src} alt={post.data.coverAlt} />
11
<h2>
12
<a href={"/blog/" + post.slug}>{post.data.title}</a>
13
</h2>
14
</div>
15
))
16
}

<img>タグに渡すときにはpost.data.coverではなくpost.data.cover.srcとする必要があるようです。

これは、Astroが単純にfrontmatterの画像のパスを解決しているだけでなく、画像の最適化もしており、post.data.coverにパス以外の情報も含まれているためです。

View Transitions機能が画像へのリンクを破壊する

Astroでは、たった2行のコードを追加するだけで、ブラウザーのView Transitions APIを利用できるようになります。また、この機能をサポートしていないブラウザーのためのフォールバックも提供されています。

View Transitionsを使うと、MPAでSPAのようなシームレスなページ遷移を実現できます。

しかし、この機能は画像やPDFなど、非ページへのリンクを破壊します。非ページへのリンクをクリックすると、画像やPDFの代わりに画面に奇妙な文字列が表示されます。

これを回避するには、非ページへのリンクにdata-astro-reload属性を付与します。この属性が付与されたリンクでは、View Transitionsが無効になります。

このブログでは、フロントエンドのJavaScriptを用いて、href属性の末尾が/で終わっていないリンクに対してdata-astro-reload属性を付与するようにしています。

この解決策は、ページへのリンクがhttps://example.com/page-title/という形式になっているため動作します。https://example.com/page-titleという形式やhttps://example.com/page-title.htmlという形式の場合は別の対応が必要です。

.astroファイル内での条件分岐

.astroファイル内で特定の変数の値に応じて要素を追加したりしなかったりするには、たとえば次のようにします。

条件分岐に使う変数 && (追加する要素)という形式で記述します。

1
{
2
Astro.props.frontmatter.showThumbnail && Astro.props.frontmatter.thumbnail && (
3
<img
4
src={Astro.props.frontmatter.thumbnail.src}
5
alt="サムネイル"
6
width={Astro.props.frontmatter.thumbnail.width}
7
height={Astro.props.frontmatter.thumbnail.height}
8
/>
9
)
10
}

まとめ

この記事では、ブログをAstroに移行した理由と、つまづいたところを紹介しました。

いくつかつまづいた点はありましたが、Astroは使いやすく高速です。また、今まで手動でコマンドを実行したり編集したりする必要があった、記事一覧や、トップページの新着記事と新着動画などを自動で更新できるようになりました。

カスタマイズ性も高いので、ぜひAstroを使ってみてください。

Twitterのアイコン LINEのアイコン Threadsのアイコン Misskeyのアイコン Misskeyのアイコン
著者のアイコン画像