シンジです。クラウドネイティブの技術ブログ(blog.cloudnative.co.jp)を、WordPress から Astro 5.x(Static Site Generator)+ Notion の構成に完全移行しました。先日、AIがコーポレートサイトの完全移行を行ったので、その流れでブログサイトも切り替えました。そのときの記事はこちらです。
PageSpeed Insights スコアは軒並み90点台後半になり、WAF も標準装備。WordPress のプラグイン更新やセキュリティパッチ、EC2の運用から解放されます。記事を書く人がいちいち WordPress にログインしなければならなかったのもダルかった。社内で日々利用している Notion をCMSとして採用、Notionに記事を書くとそのままブログとして生成される仕組みです。
なぜ WordPress を捨てたのか
運用負荷が高すぎる
WordPress は「とりあえずブログを始める」には最高のツールです。ただし運用に課題が多すぎる。
プラグインの更新地獄。 セキュリティアップデートが頻繁に降ってくる。放置すれば脆弱性の温床。対応すれば互換性の問題。どっちに転んでも辛い。
PHP + MySQL の保守。 AMIMOTO AMI(WordPress 最適化済みの AWS Marketplace AMI)を使っていたので、初期構築こそ楽だったが、OS パッチやPHP・MySQL のバージョンアップは結局自分たちで管理する必要がある。AMI が面倒を見てくれるのは初期セットアップまでで、運用フェーズの負荷は変わらない。
CloudFront + EC2 の複雑な構成。 オリジンサーバーの前に CloudFront を置く構成。キャッシュの invalidation、SSL 証明書の更新、ヘルスチェック。
表示速度の限界。 どれだけキャッシュを効かせても、動的生成の WordPress には構造的な速度の壁がある。
兎にも角にもセキュリティリスク
WordPress は世界で最も攻撃されている CMS です。wp-admin/、wp-login.php、xmlrpc.php への不正アクセスは日常茶飯事。WAF を入れても、そもそもアタックサーフェスが広すぎる。
今回採用したSSG(Static Site Generation)にすれば、サーバーサイドの実行環境がそもそも存在しない。攻撃対象が HTML/CSS/JS の静的ファイルだけになるので、根本的にセキュリティリスクが下がります。これはゼロトラストの考え方にも通じる話で、「守るべき対象を減らす」のが一番効果的なセキュリティ対策です。
移行先の技術選定
なぜ Astro なのか
SSG フレームワークの選択肢はいくつかありました:
| フレームワーク | 検討結果 |
|---|---|
| Next.js | SSR/ISR は強力だが、ブログには過剰。サーバーが必要になる。 |
| Hugo | ビルドは爆速だが、Go テンプレートの記述力に限界。エコシステムが小さい。 |
| Gatsby | React ベースだが、開発が停滞気味。ビルドが遅い。 |
| Astro | コンテンツサイトに最適化。JS ゼロ配信がデフォルト。Island Architecture。 |
コーポレートサイトも Astro 採用ですが、ここでも Astro を選んだ理由は4つ。
ゼロ JS がデフォルト。 クライアントに JavaScript を一切送らない。必要な部分だけ Island として hydrate できる。ブログのような読み物コンテンツには理想的です。
ビルドパフォーマンス。 Vite ベースで高速。
フレームワーク非依存。 React も Vue も Svelte も使える。が、今回は Astro コンポーネントだけで十分でした。
コンテンツ向け機能。 sitemap、RSS、OGP 画像生成など、ブログに必要な機能が揃っている。
なぜ CMS が Notion なのか
ヘッドレス CMS は Contentful、Strapi、microCMS など選択肢が豊富ですが、Notion を選びました。
すでに全員が使っている。 クラウドネイティブでは社内ドキュメントを Notion で管理している。Notion の販売パートナーでもあるので、新しいツールの学習コストがゼロ。
リッチなエディタ。 Notion のブロックエディタは、技術記事を書くのに十分な表現力がある。コードブロック、テーブル、Callout、Toggle、埋め込み。全部使える。
追加コストなし。 API 利用は 契約プラン内。新しい SaaS を増やさなくていい。
Notion AI が優秀。 記事のレビューをそのままのUI内で実行可能
ワークフロー。 Status プロパティ(Draft → Published → Archived)でコンテンツ管理。Published にしたら自動でビルドが走る仕組みなどを作れるオートメーションが標準実装
アーキテクチャ全体像
┌──────────────────────────────────────────────────────────┐
│ Notion Database │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Title | Slug | Category | Tags | Status | Author... │ │
│ │ ──────────────────────────────────────────────────── │ │
│ │ 記事1 | xxx | AI | ... | Published | ... │ │
│ │ 記事2 | yyy | SaaS | ... | Draft | ... │ │
│ │ ... (753 articles) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ Status → Published で Webhook 発火 │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ AWS Amplify (Build) │
│ │
│ 1. git checkout -- .notion-cache/ (キャッシュ復元) │
│ 2. npm install │
│ 3. astro build │
│ ├── Notion API → 記事取得 → HTML レンダリング │
│ ├── 画像ダウンロード → WebP 変換 → 最適化 │
│ ├── OGP 画像生成 (satori + sharp) │
│ ├── RSS / sitemap / llms.txt 生成 │
│ └── .notion-cache/ 更新 │
│ 4. pagefind でクライアントサイド検索インデックス生成 │
│ │
│ 成果物: dist/ (静的 HTML/CSS/JS/画像) │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ AWS Amplify Hosting │
│ │
│ ├── CloudFront CDN (自動) │
│ ├── ACM 証明書 (自動管理) │
│ ├── WAF (AWS マネージドルール) │
│ │ ├── AmazonIpReputationList │
│ │ ├── AWSManagedRulesCommonRuleSet │
│ │ └── AWSManagedRulesKnownBadInputsRuleSet │
│ ├── カスタムヘッダー (CSP, HSTS, X-Frame-Options...) │
│ └── 301 リダイレクトルール (28件) │
└──────────────────────────────────────────────────────────┘
│
▼
blog.cloudnative.co.jp全体像はシンプルです。Notion に書く → Amplify がビルドする → 静的ファイルが CloudFront で配信される。WordPress 時代の「EC2 + RDS + CloudFront + バックアップ + 監視」という構成と比べると、圧倒的にシンプルです。
Notion の 記事ポータル
Notion に ブログの管理ポータルを作りましたが、これも Notion MCP を接続した Claude Code が生成しています。
Notion には オートメーションの機能があり、それを使って一定の自動化処理を行っています。
Notion Automation
自動化処理の中でちょっぴり複雑な出力をしようとすると、数式で記述する必要があるのですが、その数式は Notion AI が生成してくれるので、やりたいことを書くだけです。Slack 連携も Notion が標準装備しているので、通知は Slack の対象チャンネルに飛ぶようになってます。 このあたりは Notion MCP でも Notion API でも操作できない範囲になるので、AI の指示に従いながら手動で設定する必要があります。分からないことがあったら画面のスクリーンショットを撮って AI に聞くだけです。
記事作成は Notion UI そのまま
記事の作成自体は、Notion の UI そのままです。書くだけです。画像はビルド時に自動でブログ用に圧縮処理するので、重たい画像でも気にせず貼り放題です。Status を Publishに 変更すれば公開処理まで自動で走ります。Excerpt と Slug のところに「AI」って書いてあるの分かりますか。ここは Notion AI が生成してくれるってことです。ポチッと押すと、手動で AI をトリガーすることも出来ます。
Notion → HTML レンダリングエンジン
ブログの中核は、Notion のブロックデータを HTML に変換するレンダリングエンジンです。これをフルスクラッチで実装しました。AIが。
ブロックレンダラー
src/lib/notion/blockRenderer.ts(約500行)が Notion の全ブロックタイプに対応しています:
| Notion ブロック | HTML 出力 |
|---|---|
heading_1 | <h2> (id 付き、TOC 対象) |
heading_2 | <h3> (id 付き、TOC 対象) |
heading_3 | <h4> |
paragraph | <p> |
bulleted_list_item | <ul><li> (連続ブロック自動ラップ) |
numbered_list_item | <ol><li> (連続ブロック自動ラップ) |
code | shiki でシンタックスハイライト (github-dark) |
image | WebP 変換 + lazy loading + lightbox |
callout | <aside> (アイコン + カラー対応) |
table | <table> (ヘッダー行/列対応) |
bookmark | カード型リンク |
embed | YouTube/Twitter/SpeakerDeck 自動検出 |
video | YouTube iframe or <video> |
toggle | <details><summary> |
column_list | CSS Grid カラム |
equation | 数式表示 |
Notion のブロックタイプはめちゃ多い。とはいえブログで実際に使うものを全部カバーしないと、移行した753記事のどこかで表示が壊れる可能性があるので全部対応します。
リッチテキストレンダラー
richTextRenderer.ts がインラインの装飾を処理します。太字、イタリック、取り消し線、インラインコード、下線、カラーテキスト(テキスト色 + 背景色)、リンク、インライン数式。
リンクについては javascript: / data: / vbscript: スキームをブロックしています。セキュリティの観点です。
Notion のデータベースには複数の著者がリンクを自由に挿入できるため、悪意がなくても危険な URL が混入する可能性があるからです。たとえば Notion のリッチテキストに javascript:alert(document.cookie) のようなリンクが含まれていた場合、そのままHTMLに変換して <a href="javascript:alert(document.cookie)"> として出力すると、クリックした読者のブラウザ上で任意の JavaScript が実行される。典型的な Stored XSS(格納型クロスサイトスクリプティング)です。
data: スキームも同様に危険で、data:text/html,<script>...</script> のような形でスクリプトを実行させることができる。vbscript: は IE 系のレガシーブラウザ向けの攻撃ベクタですが、念のため塞いでいます。
WP 移行記事の特殊処理
WordPress から Notion に移行した記事には、Notion ネイティブとは異なるデータ構造が入っています。特に厄介だったのが コードブロック。
WP の移行ツールが出力するデータでは、コードが Notion の code ブロックではなく、paragraph ブロック内の annotations.code = true + 改行付きテキストとして格納されます。つまり「段落内のインラインコード」として複数行のコードが入ってくる。
これを検出して <pre class="a-code-block"> として分離レンダリングする処理を入れました。
// WP移行記事: 段落内に改行付きインラインcodeがある場合、コードブロックとして分離
const hasMultiLineCode = richText.some(
(rt) => rt.annotations.code && rt.plain_text.includes('\n'),
);こういう「移行元の癖」を吸収するコードが、移行プロジェクトでは大量に必要になります。
3層キャッシュで Notion API コールを最小化
753記事を毎回 Notion API から取得していたら、ビルドに数時間かかります。そこで3層のキャッシュ戦略を実装しました。
層1: ファイルキャッシュ(.notion-cache/)
.notion-cache/
├── 28057.json (WP移行記事: 旧WP ID がファイル名)
├── 28084.json
├── ...
└── 30711.json (753ファイル、合計12MB)各 JSON ファイルには lastEditedTime(Notion の最終編集日時)、レンダリング済み HTML、目次データ(TOC)、記事の全メタデータを保存しています。
lastEditedTime が一致すればキャッシュヒット。Notion API コール 0 で記事を返却。
層2: メモリキャッシュ
fetchPublishedArticles() で取得した PageObjectResponse をメモリ上に保持。同一ビルド内で複数ページが同じ記事データを参照する場合(一覧ページと詳細ページなど)に API コールを節約します。
層3: Slug ロック
slug-lock.json がページ ID → Slug の不変マッピングを管理。Notion の AI Autofill が Slug を勝手に変更しても、一度確定した URL は絶対に変わらない。
{
"12345678-1234-1234-1234-123456789012": "my-first-article",
"...": "..."
}URL が変わったら被リンクが死んで、SEO 的にも致命的なのでこれは必須機能です。
キャッシュの効果
| API コール数 | ビルド時間 | |
|---|---|---|
| キャッシュなし(初回) | ~4,000+ | 2-3時間 |
| キャッシュあり(変更なし) | ~8 (一覧取得のみ) | ~3分 |
| 1記事更新 | ~15 | ~3-4分 |
キャッシュなしだと2-3時間、ありだと3分。初回ビルドはまじで地獄でした。
Git 管理という英断
.notion-cache/ は Git リポジトリに直接コミットしています。12MB のキャッシュファイル群を Git に入れるのは一般的ではないですが、理由があります。
Amplify の cache.paths で永続化すると、古いビルドキャッシュが Git の最新ファイルを上書きしてしまい、タイムスタンプの不整合が起きる。結果、キャッシュミスが頻発してビルド時間が爆発する。何度もこれにやられました。
Git 管理なら、常に「最新のキャッシュ」がデプロイされる。Amplify のビルドでは git checkout -- .notion-cache/ を最初に実行して、ビルドキャッシュの復元による汚染を防いでいます。
画像最適化パイプライン
Notion の画像 URL は署名付き S3 URL で、1時間で期限切れします。つまりビルド時にダウンロードしてローカルに保存するのが必須。
処理フロー
Notion 署名付き URL (prod-files-secure.s3.us-west-2.amazonaws.com/...)
│
▼
fetch (30秒タイムアウト、20MB サイズ上限)
│
▼
sharp: リサイズ (max 1200px) + WebP 変換 (quality 80)
│
▼
public/assets/images/notion/{md5-hash}.webp
│
├── キャッシュキー: URL のクエリパラメータ除去後の MD5 先頭12文字
├── 既存ファイルがあればスキップ
└── dist/ にも直接コピー (Astro のコピータイミングの問題回避)キャッシュ済み記事の画像処理
キャッシュヒットした記事の HTML 内にまだ Notion の署名付き URL が残っている場合(古いキャッシュ)、optimizeCachedImages() が正規表現で URL を抽出して一括置換します。
また、ローカル画像パス(/assets/images/notion/*.webp)が HTML に含まれているが実際のファイルが存在しない場合(CI/CD 環境での初回ビルドなど)、hasMissingLocalImages() がキャッシュを無効化して Notion API から再取得。新鮮な署名付き URL で画像をダウンロードし直します。
OGP 画像の動的生成
SNS でシェアされた時に表示される OGP 画像を、記事ごとに自動生成しています。手動でも設定できますし、その方が綺麗な画像がでるので基本的には手動作成ではあります。
/og/articles/{slug}.png → satori (JSX → SVG) → sharp (SVG → PNG)サイズは 1200x630px。フォントは IBM Plex Sans JP(node_modules から直接読み込み)。デザインは赤のグラデーション背景 + 白文字、カテゴリバッジ、著者名。フォールバックとして、ローカルにフォントがなければ Google Fonts API から取得する仕組みも入れています。
OGP 画像は PNG のまま配信しています。WebP にしない理由は、Twitter/Facebook/LINE などの SNS クローラーが WebP の OGP 画像に対応していないケースがあるため。コンテンツ画像とは戦略が異なります。
URL 設計と 301 リダイレクト
新旧 URL の互換性
WordPress 時代の URL を1つも壊さないことが最優先でした。ここを妥協すると、長年積み上げた被リンクとSEOが全部吹き飛びます。
| URL パターン | 用途 |
|---|---|
/{wpId}/ (例: /28057/) | WP 移行記事(数字 ID でアクセス) |
/articles/{slug}/ | 新規記事 |
/category/{slug}/ | カテゴリ一覧 |
/tag/{tag}/ | タグ一覧 |
/author/{name}/ | 著者別一覧 |
WP 移行記事は数字の ID がそのまま slug として使われるため、getArticleHref() ユーティリティが数字 slug なら /{slug}/、それ以外なら /articles/{slug}/ を返すようにしています。
301 リダイレクトルール(28件)
Amplify の rewrite/redirect ルールで、旧 WordPress の URL パターンをすべてハンドリングしています:
WP カテゴリ URL(日本語 URL エンコード)→ 新 slug への 301。例えば /%e3%82%b3%e3%83%a9%e3%83%a0/ → /category/column/ のような変換。
RSS フィード: /feed/ → /feed.xml
WP 画像: /wp-content/uploads/<*> → 旧 CloudFront ドメインへ 301
WP 管理画面: /wp-json/、/wp-admin/、/wp-login.php → / へ 301。これはセキュリティ的にも重要で、旧 WordPress のエンドポイントに対するボットのアクセスを適切にハンドリングしています。
フォールバック: マッチしない URL → /404.html (404-200)
セキュリティ
静的サイトだからセキュリティは不要かといえば、「やれることはすべてやる」のが正しいスタンスです。静的サイトでも、ヘッダー設計やWAFの設定で防御力は大きく変わります。
HTTP セキュリティヘッダー(amplify.yml)
- key: 'Strict-Transport-Security'
value: 'max-age=31536000; includeSubDomains; preload'
- key: 'X-Content-Type-Options'
value: 'nosniff'
- key: 'X-Frame-Options'
value: 'SAMEORIGIN'
- key: 'Referrer-Policy'
value: 'strict-origin-when-cross-origin'
- key: 'Permissions-Policy'
value: 'camera=(), microphone=(), geolocation=(), payment=(), ...'
- key: 'Cross-Origin-Opener-Policy'
value: 'same-origin'
- key: 'Content-Security-Policy'
value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com ..."HSTS preload、CSP、Permissions-Policy まで入れています。ヘッダーを足すだけでコストゼロなのでやらない理由がない。
AWS WAF
Amplify のファイアウォール機能で、以下の AWS マネージドルールを有効化:
- AmazonIpReputationList: 悪意のある IP アドレスをブロック
- AWSManagedRulesCommonRuleSet: OWASP Top 10 の一般的な攻撃パターン
- AWSManagedRulesKnownBadInputsRuleSet: Log4Shell、既知の悪意あるペイロード
コードレベルのセキュリティ
HTML エスケープ。 escapeHtml() でシングルクォートも含めた5文字をエスケープ。
URL バリデーション。 isSafeUrl() で javascript:、data:、vbscript: スキームをブロック。
iframe サンドボックス。 YouTube 埋め込みに sandbox="allow-scripts allow-same-origin allow-presentation allow-popups" を適用。
画像ダウンロード安全策。 30秒タイムアウト(AbortController)+ 20MB サイズ上限。
フレームワークバージョン非開示。 <meta name="generator"> タグを削除。攻撃者にフレームワーク情報を渡さない。
Pagefind によるクライアントサイド検索
ブログの検索機能には Pagefind を採用しました。
astro build && npx pagefind --site dist --glob '**/*.html'ビルド後の HTML をクロールしてインデックスを生成し、ブラウザ上で完全にクライアントサイドで検索を実行します。サーバーサイドの検索 API が不要なので、追加のインフラコストはゼロ。Algolia とか入れなくていい。その代わりあんまり融通は効きませんけどね。
記事本文には data-pagefind-body 属性でスコープを限定し、ヘッダーやフッターが検索結果に混ざらないようにしています。
LLM / AEO 対応
検索エンジンだけでなく、LLM からのクロールにも対応しています。AI時代ですね。
/llms.txt: サイト概要、カテゴリ一覧、最新20記事へのリンク/llms-full.txt: 全記事の詳細情報
これは llms.txt 仕様に基づいたもので、AI エージェントがサイトの構造を効率的に理解できるようにするためのファイルです。SEO も大事だけど AEO(AI Engine Optimization)はもっと大事。
Notion ワークフロー自動化
記事の公開プロセスは完全に自動化しています。人間がやることは「Notion で記事を書いて、Status を Published にする」だけ。(でもOGP画像は作ってほしいなって思ってるけど、そろそろNotion AIが画像生成するらしいので、ある程度は自動化できそう)
- 記事執筆: Notion で記事を書く
- メタデータ自動生成: Notion AI Autofill が Slug と Excerpt を自動生成
- 公開: Status プロパティを
Publishedに変更 - Published Date 自動設定: Notion Automation が公開日を自動記入
- ビルド発火: Status 変更が Amplify の incoming webhook を叩く
- SSG ビルド: Astro が Notion API から記事を取得 → 静的 HTML 生成
- デプロイ: Amplify が自動デプロイ
- ビルド通知: EventBridge → SNS → AWS Chatbot → Slack
Notion の Status を変えてから3分後にはブログに公開されます。
記事の非公開化も同様に、Status を Archived に変更するだけ。SSG ビルドでページが生成されなくなり、キャッシュも自動削除(purgeOrphanedCache())されます。
パフォーマンス
PageSpeed Insights
静的サイトなので、パフォーマンスは圧倒的です
最適化のポイント
ゼロ JavaScript(ほぼ)。 Astro のデフォルト。GA4 と Clarity は setTimeout で2.5秒遅延読み込み。
フォント最適化。 Latin サブセットのみプリロード。日本語フォントは非同期読み込み(media="print" onload="this.media='all'")。font-display: swap で FOIT 回避。
画像最適化。 全画像を WebP 変換(max 1200px, quality 80)。loading="lazy" decoding="async" でオフスクリーン画像の遅延読み込み。
HTML 圧縮。 compressHTML: true で Astro が自動圧縮。
キャッシュ戦略:
_astro/*:max-age=31536000, immutable(ハッシュ付きアセット)assets/images/*:max-age=86400(1日)*/*.html:no-cache(常に最新チェック)
コスト比較
Before: WordPress on EC2
| 項目 | 月額(USD) |
|---|---|
| EC2 (t3.small) | ~$25 |
| EBS (30GB gp3) | ~$3 |
| AMIMOTO Marketplace 料金 | ~$0-5 |
| CloudFront | ~$5-10 |
| Route 53 ホストゾーン | ~$0.50 |
| SSL 証明書 (ACM) | $0 |
| バックアップ/スナップショット | ~$3-5 |
| 運用工数(パッチ、監視、障害対応) | (priceless) |
| 合計 | ~$40-70/月 |
After: Astro + Amplify
| 項目 | 月額(USD) |
|---|---|
| Amplify Hosting (静的) | ~$1-3 |
| Amplify Build (月数回) | ~$1-2 |
| WAF (Amplify Firewall) | ~$3-5 |
| Route 53 ホストゾーン | ~$0.50 |
| 運用工数 | ほぼゼロ |
| 合計 | ~$5-10/月 |
月額コスト: 年間で $400-700 の削減。
Notion のコストあるやんけという突っ込みもその通りですが、Notion なかったら代わりのCMS基板を作っただろうなって話で、Notion は多目的用途ですでに使っていて、追加コストなかったというのが今回のポイントです。
本当の価値はコスト削減よりも、運用工数がほぼゼロになったこと、Notionに書くだけで記事公開できることです。
WordPress 時代は EC2 の OS パッチを定期的に当て、PHP のバージョンアップに追従し、MySQL のバックアップとリストア手順を維持し、WordPress コアとプラグインのアップデートを検証し、CloudFront のキャッシュ invalidation を管理し、セキュリティインシデントに備えていた。
WordPress側には個別のユーザー設定があって、SSOさせて、そっちでまた管理して、、、
これらすべてがゼロになりました。
移行で苦労したこと
ここからは、同じことをやろうとしている人のために、ハマりどころを書きます。
1. Notion SDK v5.x の破壊的変更
@notionhq/client v5.x で databases.query() が削除され、dataSources.query() に変更されました。が、新しい API はまだ database ID での直接クエリに対応していない。
仕方なく raw fetch で https://api.notion.com/v1/databases/{id}/query を直接叩く実装にしました。
const response = await fetch(
`https://api.notion.com/v1/databases/${database_id}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${NOTION_API_KEY}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);SDK を使っているのに raw fetch が必要になるのは本末転倒感がありますが、動くものが正義。
2. Notion API のレートリミット
Notion API は秒間3リクエストの制限。753記事をビルドするには数千の API コールが必要。初回ビルドには文字通り数時間かかりました。クラメソさんのブログくらい大規模になるとさらに戦略的にやらないといけないですね。正直、たったの753記事だったから出来たってだけです。
今回は対策として 334ms 間隔のスロットリング を全リクエストに挿入。429/5xx で最大5回リトライする 指数バックオフ(2秒 → 4秒 → 8秒 → 16秒 → 32秒)も実装。そして前述の 3層キャッシュ で2回目以降は劇的に高速化。
3. WP 移行記事の著者情報
WordPress から Notion に記事をインポートすると、Notion の People 型プロパティには「インポートを実行したユーザー」が入ります。つまり全記事の著者がシンジになってしまう。
解決策として、Author Display Name(rich_text 型)プロパティを追加し、WP のオリジナル著者名を格納。コード側では rich_text を最優先で取得し、なければ People 型にフォールバックする2段階の著者解決を実装しました。
アバター URL も同様の問題があり、Author Avatar URL(url 型)プロパティで WP のアバター URL を保持。WP REST API からバッチで取得するスクリプト(scripts/update-author-avatars.mjs)も作っています。
4. Notion AI Autofill による Slug 書き換え
Notion の AI Autofill 機能は便利ですが、Slug を勝手に更新してしまうことがあります。Slug が変わると URL が変わり、被リンクが切れ、SEO が崩壊する。
これを防ぐために slug-lock.json を導入しました。一度確定した Slug は二度と変わらない。ビルド時にロックファイルの値が常に Notion の値より優先される仕組みです。
AI 機能は便利だけど、こういう「勝手にやってくれる」系は時に致命的な副作用がある。Notion に限らず、AI Autofill 系の機能はロック機構とセットで使うべきです。
Notion AI で生成した Slug は、一度自動生成したら固定化したかったのですが、Notion で完結できそうになかったので諦めてこのような実装になりました。
5. Astro の public → dist コピータイミング
Astro は public/ ディレクトリの内容を dist/ にコピーしてからページをレンダリングします。しかし、画像の最適化はレンダリング中に行われるため、public/assets/images/notion/ に保存した画像が dist/ に反映されない。
解決策として、画像を public/ に保存すると同時に、dist/ が存在する場合は dist/ にも直接コピーする処理を追加しました。力技ですが、確実に動く。
スタック全体のまとめ
| レイヤー | 技術 |
|---|---|
| フレームワーク | Astro 5.x (SSG) |
| CMS | Notion Database + API |
| ホスティング | AWS Amplify |
| CDN | CloudFront (Amplify 付属) |
| WAF | AWS WAF (Amplify Firewall) |
| 検索 | Pagefind (クライアントサイド) |
| シンタックスハイライト | shiki (github-dark) |
| 画像処理 | sharp (WebP 変換) |
| OGP 生成 | satori + sharp |
| フォント | IBM Plex Sans JP + Noto Sans JP |
| CSS | SCSS (FLOCSS + BEM) |
| RSS | 自前実装 (feed.xml) |
| サイトマップ | @astrojs/sitemap |
| ビルド通知 | EventBridge → SNS → Slack |
| CI/CD | Amplify (GitHub 連携、自動ビルド) |
| ソースコード | TypeScript (全72ファイル、約7,300行) |
| npm パッケージ | 依存12、devDep3(node_modules 内 345パッケージ) |
全部で TypeScript 7,300行。依存パッケージ数は 345。WordPress のプラグイン地獄と比べれば、はるかに見通しが良い。だって AI がやってくれるんだもの。
WordPress を削除できる喜び
WordPress は素晴らしいプラットフォームです。でも、エンジニアがブログを運営するなら、SSG + ヘッドレス CMS の方が圧倒的に合理的かなーと思います。
サーバー管理不要。 パッチ当て、バージョンアップ、障害対応から解放。
セキュリティリスク激減。 アタックサーフェスが根本的に小さい。
表示速度。 静的ファイル配信は最速。
コスト。 月額 $5-10。
開発体験。 TypeScript + Astro の DX は最高。
コンテンツ管理。 Notion の方が書きやすい。
おわりに
WordPress → Astro + Notion 移行も結局全部 AI がやりました。Notion の作り込みも Claude Code に接続した Notion MCP と Notion API で全部 AI がやってます。ちなみに、シンジがそのときに使った Claude Code 環境はこちらを使えば一発でセットアップできます。
移行後の世界は快適そのものです。Notion で記事を書いて Status を Published にするだけ。3分後にはブログに公開される。
AI ってすごいね。





