Astroでブログを作った
記事執筆後、ブログの大幅な改修を行いました。記事の内容は一部古い情報が含まれています。近日中に記事を更新する予定です。
概要
このサイト(blog.eno1220.dev)を、AstroとTailwind CSS、Cloudflare Pagesを使って作成しました。本記事では、詳細な実装内容やプラグインの紹介を行います。
採用した技術スタック
Astro
Markdownを書いてビルドし、高パフォーマンスなサイトを実現したいと考えていたので、フレームワークとしてAstroを選択しました。
AstroはMarkdownを基本にContents Collectionを用いてコンテンツを管理したり、サイトマップやRSSフィードを生成するためのプラグインも提供したりしているので、ブログを作るのには十分な機能を持っています。
まあブログを書くだけなのにSSRするフレームワークを使って複雑な実装をしたくない、という気持ちが大きいですが…
Tailwind CSS
スタイルをつける方法(CSS in JSとかCSS modulesとか)は色々あるそうで、このブログを作るにあたって色々なライブラリを調べてみましたがよく分からね〜という気持ちになったので、以前にも使ったことがあるTailwind CSSを使うことにしました。
Tailwindの使用には様々な意見があることを承知していますが、このサイトの継続性などは考えていないし、どうせ1日あれば作り直せるだろうということで、Tailwindを使うことにしました。
Cloudflare Pages
昨年Coogleドメインがなくなる、というのでドメインをCloudflareに移行しました。それ以来、Cloudflareの各種サービス(Cloudflare WorkersやCloudflare Zero Trustなど)を使い倒していて、今回もCloudflare Pagesを使ってみました。GitHubのリポジトリを連携すると、リポジトリにpushされた際に自動でビルドしてデプロイしてくれます。また、Cloudflareが世界中に持っているCDNを使って、高速にサイトを配信できます。無料で使えるので、個人ブログには十分すぎるサービスだと思います。それにしてもCloudflareはどうやって収益を上げているんですかね…?
ライブラリ・プラグイン
記事検索
記事の検索はPagefindを導入しています。Pagefindは、静的なサイトに対する全文検索エンジンとUIを提供してくれるサービスです。
検索ページにアクセスして、記事を検索してみてください。
コードブロック
コードブロックは、Expressive Codeというプラグインを導入しています。シンタックスハイライトのほか、ファイル名や行番号、コピーボタン、diff表示などがこのプラグインでまとめて提供されているので、とても便利です。
なおAstroについてはIntegrationが提供されているので、以下のコマンドで簡単に導入できます。
pnpm astro add astro-expressive-code
pnpm add @expressive-code/plugin-line-numbers # 行番号を表示する場合
↑のようなターミナル風ブロックのほか、ファイル名、行番号、コピーボタン、diff表示などがこのプラグインで提供されています。
テーマはmin-dark
を使用しています。Expressive Codeのテーマはこちらから選ぶことができるほか、カスタムテーマも作成できます。
1#include <stdio.h>2
3int main() {4 char str[] = "Hello, world!";5 printf("%s\n", str);6 printf("Hello, World!\n");7 return 0;8}
フォント
フォントは、Inter
とJetBrains Mono
(コードブロック用)を使用しています。Fontsourceというライブラリを使用しています。
コールアウト
コールアウトを表示するためremark-calloutを導入しています。
Callout body
Callout body
> [!note] Callout note> Callout body
> [!warning] Callout warning> Callout body
リンク
デフォルトではリンクをクリックすると同じタブで開かれますが、外部リンクを別タブで開くようにするため、rehypejs/rehype-external-linksを導入しています。
rehypePlugins: [ [ rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'], }, ],],
また、Markdown内の単独のリンクを下記のようなリンクカードを表示するためのremarkプラグインを自作しました。
このプラグインは、ビルド時にリンク先のタイトル・説明文・faviconを取得して、リンクカードを生成します。既存のプラグインではOG画像まで取得していましたが、個人的にはfaviconだけで十分に見栄えが良いと感じたため、プラグインを自作することとしました。
なお、実装についてはremark-link-cardを参考にさせていただきました。
RSS
RSSフィードを作成しています。Contents Collectionを使用して記事を取得し、それを元にRSSフィードを生成しています。
import rss from '@astrojs/rss';import { getCollection } from 'astro:content';
export async function GET(context: any) { const posts = await getCollection('posts'); return rss({ title: 'blog.eno1220.dev', description: 'eno1220のブログ', site: context.site, items: posts.map((post) => ({ title: post.data.title, pubDate: post.data.pubDate, description: post.data.description, link: `/posts/${post.id}/`, })), });}
サイトマップ
サイトマップを作成しています。タグページやカテゴリページはサイトマップに含めないようにしています。
作成したサイトマップは、Google Search Consoleなどに登録することで、検索エンジンにサイトをクロールしてもらうことができます。
export default defineConfig({ // ... integrations: [ sitemap({ filter: (page) => { return !page.includes('tags') && !page.includes('categories'); }, }), ],});
そのほか
アイコンを表示するためのライブラリです。Open Source Icon Sets - Iconifyからアイコンを選択して、以下のように記述することでアイコンを表示できます。
<Icon name="fluent:line-horizontal-4-search-16-regular" />
:emoji:
のような記法を絵文字に変換するプラグインです。以下のように記述することで絵文字を表示できます。
:tada: :tada: :tada:
🎉 🎉 🎉
Markdown内の改行をHTMLの<br>
に変換するプラグインです。以下のように記述することで改行ができます。
改行できる
改行
できる
Markdown内の見出しをセクションに変換するプラグインです。作者ご本人が解説されている記事がわかりやすいです。
本ブログが対応している記法については以下を参照してください。
実装のポイント
OG画像
Astroの提供する静的エンドポイントを使って、ビルド時にOG画像(↑のようにTwitter等で表示される画像)を生成しています。生成には@vercel/ogを使用しています。
また、google/budouxを用いてタイトルの分かち書きを行い、日本語のタイトルでも適切なレイアウトを生成できるようにしています。
View Transitions
人によって好みが分かれると思いますが、私はページ遷移時にふんわりとしたアニメーションがあるのが良いと思い、AstroのView Transitionsのfade
を使用しています。これは、ブラウザのView Transitions
APIを使用しており、またブラウザがサポートしていない場合は自動でフォールバック用の動作の制御を行ってくれます。
---import { ViewTransitions } from 'astro:transitions';---
<html> <head> <title>My Blog</title> <ViewTransitions /> </head> <body> <slot /> </body></html>
また、ページ遷移直後に実行されるようなjavascriptを書きたい場合は、astro:after-swap
を使用すると良いようです。(自分もよくわかっておらず、たまにバグっているのでご自身で調べていただけると幸いです)
<script> async function isGeminiAvailable() { if ((await ai.assistant.capabilities()).available === 'readily') { document.querySelector('.gemini')?.classList.remove('hidden'); } }
isGeminiAvailable(); document.addEventListener('astro:after-swap', () => { isGeminiAvailable(); });</script>
シェアボタン
記事の最下部にあるシェアボタンの実装には、ウェブ共有 APIを使用しています。ユーザの選択した先にリンクを共有することができます。(OSやブラウザごとに見た目は異なります)
document.getElementById('share')?.addEventListener('click', () => { navigator.share({ title: document.title, text: document .querySelector('meta[name="description"]') ?.getAttribute('content') || '', url: location.href, });});
Gemini Nanoによる要約
記事の最下部にある「要約」ボタン1をクリックすると、Chrome組み込みのLLMであるGemini Nanoを使用して、記事の要約を表示します。
詳細は別記事にまとめましたので、そちらを参照してください。
最終更新日
hikaliumさんのWebサイトでGitのコミットハッシュを表示しているのを見て、フッター部分に導入しています。ブログが最後に更新された日時がわかるのが良いと思っています。
実装については、StackOverflowを参考にしています。
// ...import child_process from 'child_process';
const commitHash = child_process .execSync('git rev-parse HEAD') .toString() .trim();const commitDate = child_process .execSync('git log -1 --format=%cd') .toString() .trim();
export default defineConfig({ // ... vite: { define: { 'import.meta.env.COMMIT_HASH': JSON.stringify(commitHash), 'import.meta.env.COMMIT_DATE': JSON.stringify(commitDate), }, },});
Cloudflareでビルドする際にタイムゾーンを指定しないとUTCで表示されるため注意。
<p> Commit Hash: {import.meta.env.COMMIT_HASH}</p><p> Last Update: { new Date(import.meta.env.COMMIT_DATE).toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo', }) }</p>
参考記事
脚注
-
Gemini Nanoが有効になっている環境でのみ表示されます。 ↩
記事の要約を生成(beta)
※要約はGemini Nanoによって生成されたものです。内容の正確性を保証するものではありません。
Chrome組み込みのLLM「Gemini Nano」を利用して記事の要約を生成します。 生成には数十秒程度かかることがあります。 デバイスのCPUやメモリ、バッテリーの使用率が上昇することがありますのでご注意ください。