個人ブログをGatsbyからAstroに移行した

ほとんど更新していないブログだが、今年はがんばって更新したいと思っているので心機一転で Astro に移行してみた。

最近何かと話題の Astro だが、実際に触ってみると良さを実感したのでメモっておく。
Astro の主な機能などは公式 Document を見れば分かる内容だし、いろんな解説記事があるので個人的な所感を書いている。
https://docs.astro.build/ja/getting-started/

Gatsby と Astro の個人的比較

まず Gatsby は自分の使い方で静的サイト(主にブログ)を作る上では Too Much だった。 GraphQL を使って markdown ファイル を取得するが、ただの静的ファイルを表示するだけでそこまでしなくてもいいなと前から思っていた。 もちろん Gatsby の GraphQL API は、コンテンツを外部に持っている場合はまだ使い勝手が良いかもしれないが、内部のファイルシステムに markdown ファイルを持ってそれをレンダリングするだけだとかなり不必要に感じる。 Astro に移行したときに感じたのも正直これがすべてであった。

Gatsby 公式 Document のWhy Gatsby Uses GraphQLを読んでみても、パッとしたことは書いてないように思う。
https://www.gatsbyjs.com/docs/why-gatsby-uses-graphql/

GraphQL を利用するので、component 設計的にも GraphQL の実行箇所はなるべく素の component とは分けて疎結合にしたくなる。Presentational / Container パターンのように Container 層的な場所で GraphQL を利用し、結果を props で子 component に渡していく実装をしていたが、ただのブログサイトでそこまでする必要はないと思う。 実際、Astro では Page:Markdown = 1:1 の構成を取っているので、素直に Page component 内部で Markdown を取得してそれを描画している。

---
import Page from "@layouts/page";
import { getEntryBySlug } from "astro:content";

const about = await getEntryBySlug("about", "about");
const { Content } = await about.render();
---

<Page title={about.data.title}>
  <h1>{about.data.title}</h1>
  <Content />
</Page>

また、Gatsby は plugin ecosystem が豊富でnpm install gatsby-plugin-xxxとすればほとんどのことをやれるものの、package.json の dependencies が肥大化して package 管理がかなり辛くなっていた。 以下は Gatsby と Astro 間の package.json における dependencies の差分である。 実際ブログを作り直してみて、全く同じ機能を作ったわけではなく、削ぎ落とした plugin などもあったり、Astro の方には prettier や eslint を入れていないので一概に比較はできないが、それでも Astro は達成したい構成をかなりシンプルに作れる。

※ 最新の Gatsby の情報にだいぶキャッチアップしていないので、現在はより洗礼されている可能性あり。この記事を参考にする際は最新の Document を確認してください。
https://www.gatsbyjs.com/docs

  // Gatsbyで作ったブログのdependencies
  ...
  "dependencies": {
    "@emotion/react": "^11.1.5",
    "@emotion/styled": "^11.1.5",
    "gatsby": "3.14.1",
    "gatsby-plugin-canonical-urls": "^3.0.0",
    "gatsby-plugin-catch-links": "^3.0.0",
    "gatsby-plugin-disqus": "^1.2.3",
    "gatsby-plugin-emotion": "^6.1.0",
    "gatsby-plugin-feed": "^3.0.0",
    "gatsby-plugin-google-gtag": "^3.0.0",
    "gatsby-plugin-lodash": "^4.0.0",
    "gatsby-plugin-manifest": "^3.0.0",
    "gatsby-plugin-module-resolver": "^1.0.3",
    "gatsby-plugin-netlify": "^3.0.0",
    "gatsby-plugin-netlify-cms": "^5.0.0",
    "gatsby-plugin-nprogress": "^3.0.0",
    "gatsby-plugin-optimize-svgs": "^1.0.5",
    "gatsby-plugin-react-helmet": "^4.0.0",
    "gatsby-plugin-robots-txt": "^1.5.5",
    "gatsby-plugin-sharp": "^3.0.0",
    "gatsby-plugin-sitemap": "3.3.0",
    "gatsby-plugin-twitter": "^3.0.0",
    "gatsby-plugin-typegen": "^2.2.4",
    "gatsby-remark-autolink-headers": "^3.0.0",
    "gatsby-remark-code-titles": "^1.1.0",
    "gatsby-remark-copy-linked-files": "^3.0.0",
    "gatsby-remark-embed-youtube": "0.0.7",
    "gatsby-remark-external-links": "0.0.4",
    "gatsby-remark-footnotes": "^0.0.8",
    "gatsby-remark-images": "^4.0.0",
    "gatsby-remark-katex": "^4.0.0",
    "gatsby-remark-prismjs": "^4.0.0",
    "gatsby-remark-relative-images": "^2.0.2",
    "gatsby-remark-responsive-iframe": "^3.0.0",
    "gatsby-remark-smartypants": "^3.0.0",
    "gatsby-source-filesystem": "^3.0.0",
    "gatsby-transformer-remark": "^3.0.0",
    "gatsby-transformer-sharp": "^3.0.0",
    "katex": "^0.12.0",
    "lodash.kebabcase": "^4.1.1",
    "netlify-cms-app": "^2.14.40",
    "normalize.css": "^8.0.1",
    "postcss": "^8.0.0",
    "prismjs": "^1.23.0",
    "react": "^17.0.2",
    "react-disqus-comments": "^1.4.0",
    "react-dom": "^17.0.1",
    "react-helmet": "^6.1.0",
    "rimraf": "^3.0.2",
    "ts-node": "^10.9.1",
    "typescript": "^4.1.5"
  },
  "devDependencies": {
    "@types/eslint": "^7.2.7",
    "@types/eslint-plugin-prettier": "^3.1.0",
    "@types/katex": "^0.11.0",
    "@types/lodash.kebabcase": "^4.1.6",
    "@types/node": "^14.14.32",
    "@types/prettier": "^2.2.2",
    "@types/prismjs": "^1.16.3",
    "@types/react": "^17.0.2",
    "@types/react-dom": "^17.0.1",
    "@types/react-helmet": "^6.1.0",
    "@types/rimraf": "^3.0.0",
    "@typescript-eslint/eslint-plugin": "^4.15.2",
    "@typescript-eslint/parser": "^4.15.2",
    "eslint": "^7.21.0",
    "eslint-config-prettier": "^8.1.0",
    "eslint-import-resolver-typescript": "^2.4.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-react": "^7.22.0",
    "eslint-plugin-react-hooks": "^4.2.0",
    "husky": "^5.1.2",
    "lint-staged": "^10.5.4",
    "prettier": "^2.3.2",
    "prettier-plugin-md-nocjsp": "^1.1.1"
  }
  ...
  // Astroで作ったブログのdependencies
  ...
  "devDependencies": {
    "@astrojs/rss": "^2.1.0",
    "@astrojs/sitemap": "^1.0.0",
    "@tailwindcss/typography": "^0.5.9",
    "astro": "2.0.2",
    "autoprefixer": "^10.4.13",
    "rehype-autolink-headings": "^6.1.1",
    "rehype-slug": "^5.1.0",
    "shiki": "^0.12.1",
    "tailwindcss": "^3.2.4"
  },

Gatsby は TypeScript の公式なサポートが遅かった印象だが (workaround 的な方法はネットにいくつか見られていた)、Astro はそもそも TypeScript が builtin されているので、config などが隠蔽されており、それも良いポイントの 1 つだった。

また、何より Astro の Markdown ファイルのサポートが素晴らしい。 これのおかげで Markdown の扱いで面倒そうな部分を Astro が抽象化してくれている。
https://docs.astro.build/ja/guides/markdown-content/

Astro の構成

閲覧可能な page は記事一覧、記事詳細、about ページ、rss の 4 つだけというシンプルな構成。 Gatsby ブログのときはページネーションや admin 画面で記事を書けるような plugin を入れていたが、plugin の markdown editor の品質がなかなか悪く、使えるものではなかったので今回は普通にローカルでエディタを開いて記事を書くスタイルのみにしている。

codeblock の syntax highlight はshikiを利用している。 初めて使ったが、prism.jsと特に使い勝手は変わらなかったし、theme を設定しただけなので特に不満は無し。

style にはtailwindcssを利用した。 ずっと使いたいと思いつつ、趣味コードではデザインシステムのライブラリを利用 + css modules で少しだけ style の微調整をするくらいで、ほとんど style を書かなくてよい環境だったので、なかなか使うことがなかった。

tailwindcss の謎なデフォルトの styling にいくつか戸惑いつつも、総合的には使いやすくてよかった。 普通の web アプリケーションで採用するとなると css の bundle size が気になるところだが、今回は特に最適化はしていない。

src
├── components
│   ├── Footer.astro
│   ├── Head.astro
│   ├── Navbar.astro
│   ├── blog
│   │   └── preview.astro
│   ├── layouts
│   │   └── page.astro
│   └── utilities
│       └── Date.astro
├── content
│   ├── about
│   │   └── about.md
│   ├── drafts
│   │   └── xxx.md
│   └── posts
│       ├── ...
│       └── xxx.md
├── data
│   ├── nav.ts
│   └── post.ts
├── env.d.ts
├── pages
│   ├── 404.astro
│   ├── [post].astro
│   ├── about.astro
│   ├── index.astro
│   └── rss.xml.js
└── style
    └── tailwind.css