このブログの技術スタック

Real Macan Life は、ブログサービスでも WordPress でもなく、静的サイトジェネレーターで自作しています。そして実装はClaude Code(Anthropic の AI コーディングエージェント)で書いています。

このページは、その構成を技術メモとして残しておくものです。車の記事とは 毛色が違いますが、「どう作っているか」もこのブログの記録の一部という ことで。

構成の一覧

フレームワークAstro 7(完全静的出力)
ランタイムNode.js 22(ビルド時のみ)
記事Markdown / MDX(Content Collections + zod)
スタイル素の CSS(デザイントークン方式)
フォントInter + Noto Sans JP(ビルド時セルフホスト)
画像sharp(ビルド時に最適化)
アクセス解析Google アナリティクス(GA4)
CI/CDGitHub Actions(build → verify → rsync)
ホスティングさくらの VPS(1GB プラン)
Web サーバーCaddy(自動 HTTPS)
ドメインrealmacanlife.jp(Xserver ドメイン)
コーディングClaude Code(Anthropic)

アーキテクチャ: 全ページを事前ビルドする

Astro を output: 'static' で使っています。全ページをビルド時に HTML として書き出し、サーバーはそれを配信するだけです。データベースも アプリケーションサーバーもなく、リクエスト時に実行されるコードは 存在しません。

この構成にした理由は 3 つです。

  • 速い。サーバーの仕事は静的ファイルを返すことだけで、 レンダリングは全部ビルド時に終わっています。
  • 壊れにくい。管理画面もプラグインの自動更新もないので、 「気づいたら管理画面に入れない」類の事故が構造的に起きません。 攻撃面も静的ファイルの配信部分だけです。
  • 記事がテキストファイルなので、コードと同じリポジトリで git 管理できます。記事の変更履歴が commit ログとして残ります。

Astro は既定でクライアントに JavaScript を出力しません。このサイトで 実際に配信している JS は、モバイルのメニュー開閉とロゴの起動演出など 数えるほどで、記事ページはほぼ HTML と CSS だけです。

記事は Markdown + Content Collections

記事は 1 本 = 1 ファイルで、src/content/blog/ に置いた Markdown です。冒頭の frontmatter がメタデータになります。

---
title: '【愛車紹介】2020年式 Porsche Macanを購入。…'
description: '車両スペック、実車のオプション、…'
publishedAt: 2026-08-27
category: '購入'
tags: ['マカン', '認定中古車', 'Porsche Approved', '納車']
draft: false
---

この frontmatter は Astro の Content Collections で zod スキーマとして定義してあり、違反するとビルドエラーになります。

// src/content.config.ts(抜粋)
const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      description: z.string(),
      publishedAt: z.coerce.date(),
      category: z.enum(CATEGORIES), // 定義済みの 5 カテゴリ以外はエラー
      tags: z.array(z.string()).default([]),
      draft: z.boolean().default(false),
      image: image().optional(),
      imageAlt: z.string().optional(),
      // …シリーズ(購入記 #01 など)の指定もここで検証
    })
    .refine((data) => !data.image || data.imageAlt !== undefined, {
      message: 'image を指定した記事には imageAlt が必要です',
    }),
});

カテゴリの typo、代替テキストのない画像、章番号のないシリーズ記事は、 公開後に見つかるのではなくビルドの時点で止まります。draftフラグを立てた記事は本番ビルドから完全に除外され、ページも RSS もサイトマップも生成されません。

スタイル: 素の CSS とデザイントークン

CSS フレームワークは使っていません。色・文字・余白はすべて CSS カスタムプロパティとして tokens.css に定義し、 コンポーネントはトークンだけを参照します。生の hex 値をコンポーネントに書くことは禁止です。

/* src/styles/tokens.css(抜粋) */
:root {
  --c-canvas: #ffffff;
  --c-ink: #17191c;    /* 本文。コントラスト比 17.6:1 */
  --c-meta: #6b7075;   /* 日付・ラベル。5.0:1 */
  --c-accent: #a02128; /* 唯一のアクセント色。用途は 6 か所に固定 */
  /* … */
}

トークンの元になっているのは、配色からアニメーションの禁止事項までを 定めたデザインシステムのドキュメントです。「アクセント色は 1 色だけ、使ってよい場所は 6 か所」のような決め事がすべて文書化されて いて、実装はつねにそれに従います。後述する Claude Code との開発では、このドキュメントが仕様書の役割を果たします。

フォント(Inter と Noto Sans JP)は Astro の Fonts API でビルド時に取り込み、セルフホストしています。実行時に Google Fonts へのリクエストは発生しません。日本語フォントは unicode-range でサブセット分割されるので、ページが実際に使う文字のぶんしか ダウンロードされません。

// astro.config.mjs(抜粋)
fonts: [
  {
    provider: fontProviders.google(), // 取得元は Google、配信は自サイト
    name: 'Noto Sans JP',
    cssVariable: '--font-jp',
    weights: ['200 700'], // バリアブルフォント
    subsets: ['japanese', 'latin'],
    display: 'swap',
  },
  // Inter も同様
],

コーディングは Claude Code

このサイトのコードは、Anthropic の Claude Code で書いています。ターミナルで動く AI コーディングエージェントで、リポジトリのファイルを読み、コードを書き、 ビルドやテストの実行までを対話しながら進められます。

役割分担ははっきりさせています。

  • 何を作るか、どう見せるかは自分で決める。デザインシステムの ドキュメントと編集方針がその成果物です。
  • 実装は Claude Code に指示して書かせる。Claude Code はドキュメントを読んだうえでコードを書くので、ページを またいでもデザインがぶれません。
  • 出てきたものをローカルで表示して確認し、直したい点をまた伝える。 この繰り返しです。

AI がコードを書く以上、人間の見落としも AI の書き間違いも起こる前提で設計しています。だからこそ、次の節の 機械的な検証をデプロイの手前に置いています。スキーマ検証・ ビルドエラー・公開前チェックの三段構えは、「AI と組んで作る」ことへの答えでもあります。

ちなみに、このページも Claude Code に書いてもらいました。

なお、記事の元になっている体験・金額・資料はすべて管理人自身の ものです。何を書いて何を書かないかはこのサイトについてにまとめています。

インフラ: さくらの VPS と Caddy

ホスティングはさくらの VPS 1 台(1GB プラン)です。静的サイトなら Netlify や Cloudflare Pages のようなホスティングサービスが定番ですが、読者はほぼ国内で、 配信するのは静的ファイルだけ。VPS 1 台で十分な規模です。

Web サーバーには nginx ではなく Caddy を使っています。決め手は自動 HTTPS です。TLS 証明書の取得も更新も Caddy が自動でやるので、certbot の cron を管理したり、証明書切れを心配したりする必要がありません。 静的サイトの配信なら、設定もこの程度で済みます。

# Caddyfile(概形)
realmacanlife.jp {
  root * /var/www/real-macan-life/public
  encode zstd gzip
  file_server
}

運用面で決めていることは次のとおりです。

  • サーバー上で動かすのは Caddy だけ。アプリケーションの ランタイムがないので、保守対象は OS と Caddy に限られます。
  • root ではログインしません。デプロイは配信ディレクトリだけを 更新できる専用ユーザーで行います。
  • 認証は SSH の鍵のみ。デプロイ用の鍵は GitHub Actions の Secrets に登録してあり、リポジトリには置きません。

サーバーに置かれるのは HTML・CSS・画像・フォントの静的ファイルだけ です。実行時に外部ドメインへ出ていくリクエストも、GA4(有効時)を 除けばゼロです。

ドメインと運用コスト

ドメインの realmacanlife.jp は Xserver ドメインで取得しました。DNS は A レコードを VPS の IP に向けているだけの素朴な構成です。かかっている費用は次のとおり。 このブログの主義に合わせて、実額で載せておきます。

ドメイン取得(.jp)0円(Xserver ドメイン)
ドメイン更新3,102円/年
さくらの VPS(1GB)880円/月
年間合計13,662円(月あたり約1,139円)

取得 0 円はキャンペーン価格で、実質のランニングコストは更新料と VPS 代です。VPS は固定額で転送量による課金がないので、アクセスが 増えても費用はこのまま変わりません。月あたり 1,100 円ほど—— マカンの維持費に比べれば、誤差みたいなものです。

デプロイ: push から公開まで

公開は GitHub の main ブランチへの push をトリガーに、GitHub Actions がすべて自動で行います。 手作業のアップロードはありません。

# .github/workflows/deploy.yml(抜粋・整形)
- run: npm ci
- run: npm run build
- run: npm run verify   # NG が 1 件でもあればここで終了

- name: Deploy dist/ to VPS
  run: |
    rsync -rlvz --delete --chmod=D755,F644 \
      -e "ssh -i ~/.ssh/deploy_key -p $PORT" \
      dist/ "$USER@$HOST:$DOCROOT/"

公開ゲート: npm run verify

ビルドが通っただけでは「コンパイルできた」ことしか分かりません。 そこで、生成された dist/ を自作の Node スクリプトで検査し、これを通過したビルドだけがサーバーに載ります。 チェックしているのは次の 7 点です。

  1. 公開記事のページがすべて生成され、下書きのページは 1 枚もない
  2. サイトマップに下書きの URL が入っていない
  3. RSS フィードに下書きの URL・タイトルが入っていない
  4. 開発環境でだけ表示するはずの文字列が本番 HTML に残っていない
  5. 記事一覧の「全 n 本」表示が実際の公開記事数と一致している
  6. 全 HTML の内部リンク(href / src)が dist/に実在するファイルへ解決する — 関連記事や導線が下書きを 指しているとここで落ちる
  7. 公開前に使っていたパスワードゲートの痕跡が、設定フラグと 矛盾する形で残っていない
$ npm run verify

本番ビルド検証
  · 記事 7 本 — 公開 2 / 下書き 5
  · メンテナンスモード: OFF
  · HTML 23 ファイルを検査

OK — 下書きの漏れ・リンク切れはありません

実はこのページの初稿も、4 番のチェックに落ちてビルドが止まりました。 下書きフラグの書き方を frontmatter のままの形で本文に載せたら、それが「開発用の文字列の残留」として 検出されたためです。検査を緩めるのではなく本文の表記を変えて通しました。 ゲートが機械的に働いている、ということでもあります。

rsync --delete で同期

転送は rsync の差分同期です。--deleteを付けているので、消した記事やリネームしたページはサーバー側からも 消え、サーバーの中身はつねに最新のビルド結果と 1 対 1 で一致します。「消したはずの古いページが残っていた」が起きません。 転送が終わった時点で公開完了です。