Astroでブログサイトを作成した

# はじめに

こんにちは、tardigradeです。
普段は競技プログラミングや組合せ最適化などをしています。

前々から、「勉強したこと」や「経験したこと」を記事にまとめてアップするブログサイトが欲しかったので、春休みに入ったこのタイミングで作成しようと思います。
本記事はその備忘録です。

最初に予防線を張ります。自分はフロントエンド開発にはあまり明るくないので、ところどころ不自然な表現があるかもしれません。
また、コードはChatGPT・GitHub Copilotと二人三脚で書いています。

気になる点を見つけた方はコメント等でそっと教えていただけると助かります。

---

作成したサイトは[こちら](https://aktardigrade13-blog.pages.dev/)。

https://aktardigrade13-blog.pages.dev/

リポジトリは[こちら](https://github.com/akTARDIGRADE13/myBlog2025_3)。

https://github.com/akTARDIGRADE13/myBlog2025_3

# 1. 求めていること

- markdownベースで記事が書ける
    - 記事は書いてなんぼなので、時間のかかるhtmlべた書きなどはしたくない
    - ゴテゴテな装飾は要らないけど、Qiitaでいう`:::note info`くらいは欲しいかも
- LaTeXのphysicsパッケージなどの記法を使いたい
    - 自分は物理学徒なので物理の記事をそれなりに書くことが予想されるが、デフォルトの記法だけで書くのはかなりしんどい
    - 普段のレポートはmarkdownで書いてそれをpandocで変換しています（参考：[レポート執筆環境を整える](https://qiita.com/TARDIGRADE/items/60ee139b840bbfffdf17)）
- カテゴリーやタグで分類したい
    - とくにジャンルを選ばず記事を書こうと思っているので、記事にはカテゴリーやタグをつけられると嬉しい
    - 内容によってはカテゴリーに分けるのが難しかったりするだろうし、タグのほうが便利かな？

# 2. 基本方針

- 自分の周りで評判が良いのでフレームワークはAstroを採用
- デプロイ先は Cloudflare Pages
- tailwindcssを書きます
- 数式表示にはKaTeXを利用
- 記事はMDX

physicsパッケージ云々に関しては、pandocをそのまま使えたら楽だな～と思っていたのですが、なんだか難しそうなのでマクロを書いて対応します。

こうしてみると、「既存のサービス（はてなブログとか）で不十分ですか」と言われたら、そんなことはない気もします。
フロントエンドよわよわerな自分が、わざわざ手間暇かけて手作りする一番の理由は「自分で作ってみたいから」ですね。

院に進むつもりなので就活はもう少し先ですが、将来的にはポートフォリオとしての役割も兼ねられるかもと考えています。

# 3. ディレクトリ構成

いでよ！ChatGPT！

```bash
my-project/
├── astro.config.mjs           # Astro の全体設定
├── package.json               # 依存パッケージとスクリプト
├── tsconfig.json              # TypeScript 設定ファイル
├── README.md                  # ドキュメント
├── public/                    # 静的ファイル (画像、OGP用画像など)
├── src/
│   ├── components/            # UIコンポーネント
│   │   ├── Layout/            # 共通レイアウト (Header, Footer, Layout)
│   │   └── Blog/              # ブログ関連コンポーネント (記事カード、タグフィルター)
│   ├── config/                # サイト全体の設定 (site.config.ts)
│   ├── content/               # Markdown 形式のコンテンツ (ブログ記事)
│   │   └── blog/
│   │       ├── first-post.md
│   │       └── second-post.md
│   ├── layouts/               # ページレイアウトテンプレート (ブログ記事詳細用)
│   ├── pages/                 # Astro のルーティング (Home, Blog 一覧, 記事詳細)
│   ├── styles/                # グローバル CSS, Tailwind CSS の設定
│   ├── utils/                 # ユーティリティ関数 (KaTeX カスタムマクロ)
└── .gitignore                 # Git 管理除外対象
```

なるほど、良さそう（よくわからん）。

# 4. 最初のいろいろ

## 4.1 プロジェクトの作成

```bash
$ npm create astro@latest
$ cd ProjectName
$ npm install
```

wslを使っていて、かつwindows側にディレクトリを配置している場合は、下のように記述するとホットリロードが有効になります。

```ts:astro.config.mts
// @ts-check
import { defineConfig } from "astro/config";

export default defineConfig({
    vite: {
        server: {
            watch: {
                usePolling: true,
            },
        },
    },
});
```

## 4.2 パッケージ

とりあえず今の時点で必要そうなものだけ。astroの機能でどこまで出来るのかよくわかってない。

```bash
$ npm install katex rehype-katex remark-math
$ npx astro add mdx
$ npx astro add tailwind
```

```css:global.css
@import "tailwindcss";
```

```ts:astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";

import mdx from "@astrojs/mdx";

import tailwindcss from "@tailwindcss/vite";

// https://astro.build/config
export default defineConfig({
    integrations: [
        mdx(),
    ],
    vite: {
        plugins: [tailwindcss()],
    },
    markdown: {
        remarkPlugins: [remarkMath],
        rehypePlugins: [rehypeKatex],
    },
});
```

## 4.3 Cloudflare Pagesへデプロイ

GitHubにリポジトリを作ってpushしたのち、Cloudflare Pagesにデプロイします。

Cloudflare Pagesを利用するのは初めてだったので、以下の記事（ドキュメント）を参考にしました。

- [AstroサイトをCloudflare Pagesにデプロイする #gitを使ったサイトのデプロイ方法](https://docs.astro.build/ja/guides/deploy/cloudflare/#git%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AE%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E6%96%B9%E6%B3%95)
- [Astro.jsで作ったWEBサイトをCloudflare Pagesにデプロイする](https://bou7254.com/posts/astro-cloudflare-pages-deploy)

# 5. 各ページの実装

Astroを全く触ったことが無かったので勉強したことを簡単にまとめておきます。大体はドキュメントかChatGPTの受け売りです。

Astroファイル（.astro）はファイルの先頭でロジックや変数の定義を行い、そのあとにHTMLライクなマークアップを書くシングルファイルコンポーネントの形式になっています。

```html
---
const title = "Astroの魅力";
const description = "Astroは高速なウェブサイトを構築するためのフレームワークです。";
---
<html>
  <head>
    <title>{title}</title>
  </head>
  <body>
    <h1>{title}</h1>
    <p>{description}</p>
  </body>
</html>
```

今回は何も使いませんが、React、Vue、Svelteなど複数のフレームワークのコンポーネントを同一プロジェクト内で利用できます。

あとは、「アイランドアーキテクチャ」が特徴的です。

> Astroは、アイランド（Islands）と呼ばれるフロントエンドアーキテクチャを開拓し、普及させました。アイランドアーキテクチャは、モノリシックなJavaScriptのパターンを避け、ページから不要なJavaScriptをすべて自動的に削除することで、フロントエンドのパフォーマンスを向上させます。開発者は、Astroと一緒にお気に入りのUIコンポーネントやフレームワークを使い続けながらも、なおこうしたメリットを得られます。
> 
> https://docs.astro.build/ja/concepts/islands/ より

複数のUIコンポーネントを独立に管理していて、さらに、必要なものはクライアントサイドで動的に動作させられます。複数のフレームワークを使えるのもこれのおかげらしい。

## 5.1 HeaderとFooterとHomeと

ChatGPTやドキュメントと格闘しながら作成。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/fa26de64-fa03-4fe1-8a11-cb3f51aff3cf.png)

画像の結合には下記のサイトを利用しました。

[フォトコンバイン](https://photocombine.net/cb/)

Homeではページの概要を、Blogではブログ記事の一覧を、Updatesでは更新情報の一覧を見られるようにするつもりで書いています。

あとはProfileを追加する予定ですが、これはGitHubのProfileと合わせて作成したいので今回は実装しません。また気が向いたときにやります。


## 5.2 Updatesページ

更新情報もmarkdownで書くことにします。

Astroのコンテンツコレクションを利用します。[公式ドキュメント](https://docs.astro.build/ja/guides/content-collections/#%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%AE%E5%AE%9A%E7%BE%A9)にならって`src/content/config.ts`を次のようにします。

```ts:src/content/config.ts
// 1. `astro:content`からユーティリティをインポート
import { z, defineCollection } from 'astro:content';

// 2. コレクションを定義
const updatesCollection = defineCollection({ 
    type: 'content',
    schema: z.object({
        title: z.string(),
        date: z.string(),
    }),
});

// 3. コレクションを登録するために、単一の`collections`オブジェクトをエクスポート
//    このキーは、"src/content"のコレクションのディレクトリ名と一致する必要があります。
export const collections = {
    'updates': updatesCollection,
};
```

更新情報をまとめて取得して並び変えたいときはこんな感じ。

```ts
const updates: CollectionEntry<'updates'>[] = await getCollection('updates');
updates.sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime());
```

Updatesページでは10個ずつ表示するようにします。ページネーションの実装はChatGPTに丸投げです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/bad609a9-fe92-420c-8198-56b828f22044.png)

ページネーションはいろんなところで再利用しそうなので、[コンポーネント化](https://github.com/akTARDIGRADE13/myBlog2025_3/blob/main/src/components/Pagination.astro)しておきます。

## 5.3 Blogページの実装

コレクションを定義します。

```ts:src/content/config.ts
/*
------------- 前略 -------------
*/

const blogCollection = defineCollection({
    schema: z.object({
        id: z.string(),
        title: z.string(),
        date: z.date(),
        updated: z.date(),
        eyecatch: z.string(),
        tags: z.array(z.string()),
    }),
});      

export const collections = {
    'updates': updatesCollection,
    'blog': blogCollection,
};
```

Updatesページと似たような感じで実装をしました。ポストカードをダラーっと並べるやつも[コンポーネント化](https://github.com/akTARDIGRADE13/myBlog2025_3/blob/main/src/components/BlogPostCard.astro)しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/bb73314e-366b-4623-92f6-371f7660af02.png)

デフォルトのアイキャッチ画像はこちらから拝借しました。

- OKUMONO https://sozaino.site
- https://sozaino.site/archives/18989

## 5.4 タグページ

タグのフィルタリングを実装します。
今回は、クライアントサイドで動的にAND/OR検索による絞り込みを行うのではなく、あらかじめ各タグごとのページを生成しておく方法を採用します。

ディレクトリ構成はこんな感じ。

```bash
my-project/
└── src/
    └── pages/
        └── tags/
            └── [tag]/
                ├── [page].astro
                └── index.astro 
```

`src/pages/tags/[tag]/[page].astro`や`src/pages/tags/[tag]/index.astro`の実装はコンポーネントを使い回していて、`blog/`直下のそれらとほとんど同じものになっています。
もっときれいにまとめられる気もするけど、思いつかなかった…。

Blogページの先頭にタグ一覧を置き、そこからタグページに飛べるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/e8a9bdb4-848d-4bb1-ac30-7c0a018c6063.png)

## 5.5 記事ページ

コンテンツ（md,mdx）をHTMLに変換する部分はAstro側がやってくれます。

参考：[コンテンツをHTMLにレンダリングする](https://docs.astro.build/ja/guides/content-collections/#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%92html%E3%81%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B)

何も手を加えていない状態。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/e4e8c578-6764-4d5b-9ab4-fbe02893cee3.png)

### 5.5.1 proseクラス

tailwindcssの`prose`クラスを使うと、見栄えが整います。

```cs:global.css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
```

```html
<!-- 記事本文 -->
<div class="prose prose-lg max-w-none">
    <slot />
</div>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/c88ae34c-de4e-44e3-9e4e-b2fdb2fd18bc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/2f246318-db91-4d07-9ce3-d22e8ff9d749.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/c1d7a530-d4bc-4cd2-8aa4-b4f3a2c33cc6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/5ef68f41-2dbf-4f83-8600-21d37b07560c.png)

### 5.5.2 数式

```ts
import 'katex/dist/katex.min.css';
```

をすると、数式表示ができるようになりますが、当然このままではphysicsパッケージなどの記法は使えません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/4f0bc164-6515-404f-a700-18b340b751dd.png)

力業ではありますが、rehypeに「physicsパッケージ」と「katex記法」を対応させるマクロを渡すことで解決できます。

```ts
markdown: {
    remarkPlugins: [remarkMath],
    rehypePlugins: [
        [rehypeKatex, { macros: katexMacros }],
    ]
},
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/af8b2684-af69-43e8-bc2b-9d4e46963cc3.png)

マクロを自分で書くのは非常に面倒な作業になりそうだったので、今回は偉大な友人が作成したものをそのままパクらせていただきました。便利すぎる！

https://github.com/xiupos/xiupos/blob/main/astro.config.mts

### 5.5.3 リンクカード

デフォルトだとリンクはこのように表示されます。

```md
### リンク

[リンクの例](https://github.com/akTARDIGRADE13/myBlog2025_3)

https://github.com/akTARDIGRADE13/myBlog2025_3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/71b05146-bf35-4816-b4ac-2e90ca5efc18.png)

一応リンクであることはわかりますが、欲を言えばリンクカードを表示するようにしたいです。これについて[良さげな記事](https://zenn.dev/laddge/articles/bb7686a5371b5c)を見つけたので、書かれていることを実行します。

https://zenn.dev/laddge/articles/bb7686a5371b5c

そのままだと動かなかったので、ChatGPTに聞いて修正しました。

```ts:astro.cofig.mts
[
    rehypeExternalLinks,
    {
        target: "_blank",
        // 以下を追記
        filter: (node: any) => {
            const href = node.properties && node.properties.href;
            return typeof href === "string" && /^https?:\/\//.test(href);
        },
    },
],
```

CSSも頑張って書かせるといい感じに。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/ce88b637-02c2-4ea2-b7f3-44932d99b5f8.png)

### 5.5.4 シンタックスハイライト

`prose`クラスでも十分ですが、AstroはShikiとPrismをビルトインでサポートしているらしいので、それを使ってみます。

参考：[シンタックスハイライト](https://docs.astro.build/ja/guides/markdown-content/#%E3%82%B7%E3%83%B3%E3%82%BF%E3%83%83%E3%82%AF%E3%82%B9%E3%83%8F%E3%82%A4%E3%83%A9%E3%82%A4%E3%83%88)

```ts:astro.cofig.mts
markdown: {
    shikiConfig: {
        theme: 'plastic',
        langs: [],
        wrap: true,
    },
    // 略
},
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570491/d61943cb-23b9-4454-8f64-7bfe20fb569a.png)

めっちゃ簡単でびっくり。

# 6. おわりに

とりあえず最低限の機能は実装したような気がするので、一旦ここまでで終わりにしようと思います。気づけばもう春休みも終わりますね。

今後やりたいこととしてパッと思いつくのは

- note記法っぽいものを使えるようにする
- 目次を追加する
- サイト内検索機能を実装する

あたりですが、4年の前期はめちゃめちゃ忙しそうなのでしばらく放置です。
記事はいっぱい書きたい。

最後まで読んでいただきありがとうございました。