はじめに

個人ブログを新しく構築しました。技術選定から細かいこだわりまで、色々と試行錯誤した結果をまとめておきます。

技術スタック

今回のブログは以下の構成で作りました。

カテゴリ技術
フレームワークAstro
コンテンツ形式Markdown
OG画像生成satori + resvg
文章校正textlint
CIGitHub Actions
フォントLINE Seed JP

Astroを選んだ理由

ブログのようなコンテンツ中心のサイトにはAstroが合っている印象です。デフォルトでJavaScriptを最小限にしてくれるので、ページの表示速度も速い。静的サイト生成(SSG)でビルドして配信するだけなので運用コストもかかりません。

コードブロックのカスタマイズ

コードブロックのシンタックスハイライトにはShikiを使っていて、Draculaテーマを適用しています。加えてカスタムのShikiトランスフォーマーを2つ自作しました。

タイトル表示

```ts title="example.ts" のようにmetaにtitleを指定すると、コードブロックの上部にファイル名が表示されます。ShikiのASTを操作してヘッダー要素を差し込むカスタムトランスフォーマーを書いています。

行番号

もうひとつのトランスフォーマーで行番号を表示しています。CSSでuser-select: noneをつけているので、コードをコピーするときに行番号が含まれないようになっています。

OG画像の自動生成

各記事にOG画像を自動生成しています。satoriでJSXからSVGを生成し、resvgでPNGに変換する構成です。

src/pages/og/[...slug].png.ts
1const svg = await satori( 2 { 3 type: 'div', 4 props: { 5 style: { 6 width: '100%', 7 height: '100%', 8 display: 'flex', 9 justifyContent: 'center', 10 alignItems: 'center', 11 backgroundColor: '#fafafa', 12 }, 13 children, 14 }, 15 }, 16 { 17 width: 1200, 18 height: 630, 19 fonts: [ 20 { 21 name: 'Noto Sans JP', 22 data: fontData, 23 weight: 700, 24 style: 'normal', 25 }, 26 ], 27 } 28); 29 30const resvg = new Resvg(svg, { 31 fitTo: { mode: 'width', value: 1200 }, 32}); 33const pngBuffer = resvg.render().asPng();

記事ごとに設定した絵文字をOG画像の中央に表示しています。AstroのSSGと組み合わせることで、ビルド時に全記事分のOG画像が自動的に生成されます。

デザインのこだわり

フォント

本文にはLINE Seed JPを使っています。可読性が高く、技術ブログの長文でも読みやすい印象です。コードにはJetBrains Monoを指定しています。

レイアウト

最大幅は580pxにしています。正直もう少し広くてもいいかなという気持ちもありますが、1行の文字数が多すぎると読みにくくなるので、このくらいがちょうどいい気がしています。

textlintによるCI校正

PRを作成すると、GitHub Actionsでtextlintが自動実行されます。

.github/workflows/lint-blog.yml
1name: Blog Lint 2 3on: 4 pull_request: 5 paths: 6 - 'src/content/blog/**' 7 8permissions: 9 contents: write 10 11jobs: 12 textlint: 13 runs-on: ubuntu-latest 14 steps: 15 - uses: actions/checkout@v4 16 with: 17 ref: ${{ github.head_ref }} 18 19 - uses: actions/setup-node@v4 20 with: 21 node-version: '22' 22 cache: 'npm' 23 24 - run: npm ci 25 26 - name: Run textlint --fix 27 run: npm run lint:fix || true 28 29 - name: Commit auto-fixed changes 30 run: | 31 git config user.name "github-actions[bot]" 32 git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 33 git add -A 34 git diff --cached --quiet || git commit -m "fix: textlint auto-fix" 35 git push 36 37 - name: Run textlint 38 run: npm run lint:text

ポイントは、まずtextlint --fixで自動修正を試み、変更があればbotが自動でコミット・pushするところです。その後にtextlintを再実行して、自動修正できなかったエラーが残っていればCIを落とします。

日本語校正

textlint-rule-preset-ja-technical-writingで冗長表現、助詞の重複、文体の混在などを検出しています。一文の長さの上限はデフォルトの100文字から150文字に緩和しました。技術記事だとどうしても一文が長くなりがちなので。

LLM臭さの検出

@textlint-ja/textlint-rule-preset-ai-writingでAI生成っぽい表現を検出しています。このブログはClaude Codeのセッションでの学びを記事にしているので、LLM臭くならないようにチェックするのは大事です。

検出される表現の例を挙げると、以下のようなものがあります。

  • 革命的な ゲームチェンジャー のような誇張表現
  • 箇条書きでの絵文字使用
  • することが可能です のような冗長な言い回し

textlint --fixで自動修正できるルールもあり、ローカルではnpm run lint:fixを先に実行してからnpm run lint:textで残りを確認するという運用にしています。CIでも同様に自動修正が走るので、ローカルで修正し忘れてもPRに自動でfixコミットが積まれます。

デプロイ

ホスティングにはCloudflare Pagesを使っています。GitHubリポジトリと連携しているので、mainにマージされると自動で本番デプロイが走ります。PRを作るとプレビューデプロイも自動で作られるので、マージ前に実際の見た目を確認できます。

Claude Codeとの連携

このブログの記事はClaude Codeのセッションでの学びをもとに書いています。記事の執筆自体もClaude Codeのカスタムスキルとして定義してあり、/blog [トピック]と打つだけで以下が一気に実行されます。

  1. トピックの調査(Web検索、公式ドキュメント参照)
  2. 記事の執筆・保存
  3. textlint --fixで自動修正
  4. textlintで残りのエラーを確認・修正
  5. ブランチ作成、コミット、PR作成

スキルの中には自分の過去の記事から抽出した文体ルールも定義していて、「〜らしいので、手元で試してみました」のような自分らしい表現を使うように指定しています。ちなみにこの記事もそのスキルで書いています。

PRが作られたらプレビュー環境で見た目を確認して、気になるところがあればPRにレビューコメントを書いてClaude Codeに修正させる、という流れです。

ブログ執筆にAIを使うのは正直若干の抵抗もあります。ただ、個人の自己満で誰にもみられなくてもいいやくらいのノリなので、まあいいかなと。それよりもアウトプットを習慣化することの方が大事だと考えています。

最後に

Astroはモダンで軽量なので使っています。エコシステムも充実していて快適に進められました。

textlintのCI校正とプレビューデプロイのおかげで、記事を書いてからマージするまでの品質チェックが自動化できたのはいい仕組みだなと感じています。今後はAgentic workflowにも興味があるので、記事の執筆からレビュー、修正までをもっと自律的に回せる仕組みにも挑戦してみたいです。