ai-blog アーキテクチャ
概要
- フレームワーク: Next.js 16 (App Router, Turbopack) + React 19 + TypeScript
- AIモデル:
gemini-2.5-flash(Google Search Grounding)+ imagen-4.0-generate-001
- 永続化: S3(Primary)/ Local FS(Fallback、
S3_BUCKET 未設定時)
- 実行基盤: AWS Amplify Hosting(Static + SSR、Lambda@Edge は不使用)
- 定期実行: GitHub Actions(5 ワークフロー)
システム構成(Context)
flowchart LR
Reader[読者] --> App[Next.js App on Amplify]
Admin[管理者] --> App
App -- workflow_dispatch --> GHA[GitHub Actions]
GHA -- 直接実行 --> Gen[Gemini / Imagen]
Gen --> S3[(S3: posts / images / index)]
App --> S3
Search[検索エンジン] --> App
Reader -- SNSシェア --> SNS[X / Facebook / LINE]
コンテナ構成(Container)
flowchart TB
Browser[Browser]
subgraph Amplify
UI[UI Layer\nsrc/app, src/components]
API[API Layer\nsrc/app/api/*]
Lib[Domain/Infra\nsrc/lib/posts.ts, src/lib/s3.ts]
end
subgraph GitHubActions
Scripts[scripts/*.mjs]
end
S3[(S3 Bucket)]
Local[(Local FS\ncontent/posts, public/generated-images)]
Gemini[gemini-2.5-flash\n+ googleSearch]
Imagen[imagen-4.0-generate-001]
Browser --> UI
UI --> Lib
API --> Lib
API -- workflow_dispatch --> Scripts
Scripts --> Gemini
Scripts --> Imagen
Scripts --> S3
Lib --> S3
Lib -.fallback.-> Local
主要フロー
1) 記事閲覧
/ および /posts/[slug] で getAllPosts / getPostBySlug を実行
src/lib/posts.ts が S3 優先、なければ /tmp → バンドルの順でフォールバック
- Markdown → HTML 変換(remark + remarkGfm)後、後処理:
- 未置換
{{IMAGE_N}} マーカーを除去
<img> に loading="lazy" decoding="async" を付与
## 参考文献 直後の <ol> を / 区切りの <p class="references-compact"> に変換
- 見出し(h2/h3)に id を付与(目次・アンカーリンク用)
- ISR で配信(
revalidate = 60)
2) 管理画面から記事生成(推奨パス)
- ブラウザ →
/admin → /api/auth で Bearer token 発行
/api/generate が GitHub の workflow_dispatch を呼ぶだけ(直接生成しない)
- GitHub Actions 上で
scripts/scheduled-generate.mjs が走る
- 完了後 ISR キャッシュが切れたら公開面に反映
- CloudFront / Lambda 29 秒タイムアウト回避のためにこの構成を取る
2′) 旧フロー(/api/generate-article)
- 直接 Gemini を呼んで生成。
maxDuration = 300 を設定済みだが、CloudFront 29 秒制限に当たる可能性があるため現在は非推奨
3) 定期バッチ生成(GitHub Actions cron)
| Workflow |
スケジュール |
スクリプト |
役割 |
generate-post.yml |
0 21 * * * (JST 06:00) |
scheduled-generate.mjs |
毎朝のデイリーサマリ |
generate-howto.yml |
0 10 * * * (JST 19:00) |
scheduled-generate.mjs (TOPIC 指定) |
毎晩のハウツー記事 |
generate-categories.yml |
0 8 * * 1,3,5 (JST 月水金 17:00) |
category-generate.mjs |
4 カテゴリ別(モデル / 企業 / 製品 / 規制) |
generate-on-demand.yml |
manual |
scheduled-generate.mjs |
管理画面の手動実行 |
generate-bulk.yml |
manual |
generate-multiple.mjs |
バルク生成(N 件) |
4) 参考文献(Grounding URL)取得フロー
Gemini が JSON 出力モードで googleSearch を発火させない / 架空 URL を吐く問題への対策
- プライマリ呼び出し(
gemini-2.5-flash で本文 JSON を生成、grounding tool 付き)
- 必ずフォールバック検索を
gemini-2.5-flash で再実行(grounding 安定取得用)
mergeSourceUrls(primary, fallback) で URI ベース重複排除してマージ
- URL がある段落の末尾に
出典: [name](url) — <url> をラウンドロビンで挿入
- 末尾に
## 参考文献 を番号付きリストで追加(フロント側で / 区切りに整形)
groundingChunks 由来の URL のみ採用(Gemini が JSON 内に書いた URL は 404 になることがあるので使用禁止)
モデル利用箇所
- 本文生成 + Grounding:
gemini-2.5-flash
scripts/scheduled-generate.mjs
scripts/category-generate.mjs
scripts/generate-multiple.mjs
scripts/_shared.mjs(共通ユーティリティ)
src/app/api/generate-article/route.ts(旧フロー)
- 画像生成:
imagen-4.0-generate-001(同上)
永続化レイアウト
S3
{S3_BUCKET}/
├── posts/{slug}.md
├── images/{slug}-cover.webp
├── images/{slug}-image_1.webp ... image_N.webp
└── posts-index.json # 最新 30 件のメタ(重複テーマ回避用)
Local(フォールバック / 開発時)
content/posts/{slug}.md
public/generated-images/{slug}-cover.webp
/{slug}-image_N.webp
src/lib/s3.ts::isS3Enabled() が S3_BUCKET の有無で切り替える。
API レイヤ(src/app/api/)
| エンドポイント |
役割 |
auth/ |
Base64 auth:{datetime}:{ADMIN_PASSWORD} を Bearer で受けて検証 |
generate/ |
GitHub workflow_dispatch を発火(推奨パス) |
generate-article/ |
直接生成(旧フロー、maxDuration = 300) |
generate-images/ |
画像のみ再生成 |
images/[...path]/ |
S3 画像のプロキシ配信(Block Public Access を維持) |
posts/ |
記事一覧 / 単体取得 |
s3-status/ |
S3 接続確認 |
contact/ |
お問い合わせフォーム |
UI コンポーネント(src/components/)
| ファイル |
役割 |
Header.tsx / Footer.tsx / Logo.tsx |
レイアウト |
PostFilter.tsx / PostPagination.tsx |
記事一覧の絞り込み・ページング |
ShareButtons.tsx |
SNSシェアボタン(X / Facebook / LINE / リンクコピー)— 記事本文の上に表示 |
CookieConsent.tsx |
Cookie 同意バナー |
Markdown frontmatter
---
title: "..."
date: "2026-04-29"
datetime: "2026-04-29T09:00:00+09:00"
summary: "..."
tags: ["AI", "LLM"]
source: "AI Auto-Generated"
coverImage: "/api/images/images/{slug}-cover.webp"
---
本文中の {{IMAGE_1}} などのマーカーが画像生成完了後に  に置換される。失敗マーカーは posts.ts::parseMarkdown() の正規表現で除去(安全策)。
SEO / 構造化データ
src/app/sitemap.ts / robots.ts を自動生成
/posts/[slug] で BreadcrumbList + BlogPosting JSON-LD を出力
- OGP / Twitter Card は
generateMetadata() で記事ごとに動的生成
/feed.xml を提供
セキュリティと運用
- 管理 API は
Authorization: Bearer ... 必須(verifyAuth() で検証)
/api/auth に IP 単位のログイン試行制限
- 画像は
/api/images/[...path] プロキシ経由で配信(S3 Block Public Access を維持、IAM 認証で取得)
amplify.yml の preBuild フェーズで .env.production を毎回新規生成(>> 追記による重複を回避)
環境変数
| 変数 |
用途 |
必須条件 |
GEMINI_API_KEY |
Gemini / Imagen API |
全パス |
ADMIN_PASSWORD |
管理画面認証 |
全パス |
NEXT_PUBLIC_SITE_URL |
OGP / canonical / シェアボタン |
全パス |
GITHUB_DISPATCH_TOKEN |
/api/generate の workflow_dispatch |
管理画面利用時 |
GITHUB_OWNER / GITHUB_REPO / GITHUB_WORKFLOW_FILE / GITHUB_WORKFLOW_REF |
dispatch 先の指定 |
同上 |
S3_BUCKET / S3_REGION / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY |
S3 永続化 |
S3 利用時 |