ai-blog アーキテクチャ

概要

システム構成(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) 記事閲覧

  1. / および /posts/[slug]getAllPosts / getPostBySlug を実行
  2. src/lib/posts.ts が S3 優先、なければ /tmp → バンドルの順でフォールバック
  3. Markdown → HTML 変換(remark + remarkGfm)後、後処理:
  4. ISR で配信(revalidate = 60

2) 管理画面から記事生成(推奨パス)

  1. ブラウザ → /admin/api/auth で Bearer token 発行
  2. /api/generate が GitHub の workflow_dispatch を呼ぶだけ(直接生成しない)
  3. GitHub Actions 上で scripts/scheduled-generate.mjs が走る
  4. 完了後 ISR キャッシュが切れたら公開面に反映
  5. CloudFront / Lambda 29 秒タイムアウト回避のためにこの構成を取る

2′) 旧フロー(/api/generate-article

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 を吐く問題への対策

  1. プライマリ呼び出し(gemini-2.5-flash で本文 JSON を生成、grounding tool 付き)
  2. 必ずフォールバック検索を gemini-2.5-flash で再実行(grounding 安定取得用)
  3. mergeSourceUrls(primary, fallback) で URI ベース重複排除してマージ
  4. URL がある段落の末尾に 出典: [name](url) — <url> をラウンドロビンで挿入
  5. 末尾に ## 参考文献 を番号付きリストで追加(フロント側で / 区切りに整形)
  6. groundingChunks 由来の URL のみ採用(Gemini が JSON 内に書いた URL は 404 になることがあるので使用禁止)

モデル利用箇所

永続化レイアウト

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" # JST(一覧表示用) datetime: "2026-04-29T09:00:00+09:00" # JST(ソート用) summary: "..." tags: ["AI", "LLM"] source: "AI Auto-Generated" coverImage: "/api/images/images/{slug}-cover.webp" # 画像生成成功時のみ ---

本文中の {{IMAGE_1}} などのマーカーが画像生成完了後に ![alt](url) に置換される。失敗マーカーは posts.ts::parseMarkdown() の正規表現で除去(安全策)。

SEO / 構造化データ

セキュリティと運用

環境変数

変数 用途 必須条件
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 利用時