1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【完全自動化】認証をClerkに移行し、ローカルLLMで毎朝ブログを自動生成する仕組みを作った

1
Posted at

はじめに

個人ポートフォリオサイトの認証システムを NextAuth.js → Clerk に移行し、
さらに Gemini APIが死んでも、自宅PCのローカルLLM(LM Studio)が自動でブログ記事を書いてくれる 仕組みを構築しました。

最終的には、Windows Task Scheduler で毎朝8時に自動実行されるようにして、
完全なる「寝てる間にブログが更新される」システムが完成しました。

この記事では、その全体像と具体的な実装を解説します。


🎯 今回やったこと(全体像)

┌─────────────────────────────────────────────────────┐
│                  Before                              │
│  認証: NextAuth.js (Google OAuth 設定地獄)           │
│  記事: Gemini API のみ (無料枠尽きたら終了)          │
│  運用: 手動                                          │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│                  After                               │
│  認証: Clerk (OAuth設定ほぼゼロ)                     │
│  記事: Gemini → LM Studio フォールバック             │
│  運用: Task Scheduler で毎朝8時に自動生成            │
└─────────────────────────────────────────────────────┘

1️⃣ NextAuth → Clerk 移行

なぜ移行したか

NextAuth.jsでGoogle OAuthを設定していましたが、redirect_uri_mismatch エラーが頻発。
GCPコンソールでRedirect URIを正しく設定しても、環境(localhost / Vercel)ごとに挙動が違い、デバッグに時間を取られていました。

Clerkなら、ダッシュボードでGoogleを有効にするだけ。 Redirect URIの設定すら不要です。

変更したファイル

ファイル 変更内容
src/proxy.ts clerkMiddleware でルート保護
src/middleware.ts 🗑️ 旧 NextAuth ミドルウェア削除
src/components/Providers.tsx SessionProviderClerkProvider
src/app/login/page.tsx Clerk <SignIn> コンポーネント
src/app/admin/page.tsx useUser() + <UserButton />
src/app/api/admin/*/route.ts auth() + clerkClient()
src/app/api/auth/[...nextauth]/ 🗑️ 削除

ミドルウェアの設計ポイント

最初の失敗として、ミドルウェアで管理者メールチェックもやろうとしました。
しかし Clerkの sessionClaims にはデフォルトでメールが含まれません。

// ❌ NG: sessionClaims.email は undefined
export default clerkMiddleware(async (auth, req) => {
    const { sessionClaims } = await auth();
    const email = (sessionClaims as any)?.email; // → undefined!
});

正解は「ミドルウェアは認証のみ、管理者チェックはページ/API側」 です:

// ✅ proxy.ts - 認証チェックのみ
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isProtectedRoute = createRouteMatcher(["/members(.*)", "/admin(.*)"]);

export default clerkMiddleware(async (auth, req) => {
    if (isProtectedRoute(req)) {
        const { userId } = await auth();
        if (!userId) {
            return NextResponse.redirect(new URL("/login", req.url));
        }
    }
});
// ✅ admin/page.tsx - ページ側で管理者チェック
const { user } = useUser();
const userEmail = user?.emailAddresses?.[0]?.emailAddress || "";
const adminEmail = process.env.NEXT_PUBLIC_ADMIN_EMAIL || "";

if (adminEmail && userEmail !== adminEmail) {
    return <AccessDenied />;
}

APIルートの認証

// 旧: NextAuth
import { getServerSession } from "next-auth";
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

// 新: Clerk
import { auth, clerkClient } from "@clerk/nextjs/server";
const { userId } = await auth();
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

// 管理者チェック(APIルート側)
const client = await clerkClient();
const user = await client.users.getUser(userId);
const email = user.emailAddresses?.[0]?.emailAddress || "";
if (email !== process.env.ADMIN_EMAIL) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

必要な環境変数

# Clerk(Vercel + .env.local)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxx

# 管理者メール(Vercel + .env.local)
ADMIN_EMAIL=your-admin@example.com
NEXT_PUBLIC_ADMIN_EMAIL=your-admin@example.com

⚠️ APIキーは必ず .env.local に置き、.gitignore で除外してください。


2️⃣ ローカルLLMフォールバック

問題:Gemini無料枠が枯渇

gemini-2.0-flashgemini-2.0-flash-lite の順に試しましたが、
どちらも 429 Too Many Requests で全滅。無料枠のリミットに到達していました。

解決:LM Studio をフォールバック先に

自宅PCで LM Studio を起動し、
gemma-3-4b(Google製 4Bモデル)をフォールバック先にしました。

article-generator.ts のフォールバックチェーン:

  ① Gemini API (gemini-2.0-flash-lite)
       ↓ 429エラー or タイムアウト
  ② LM Studio (localhost:1234)
       ↓ 接続不可(Vercel上では到達不能)
  ③ エラーレポート

実装(フォールバック部分)

// article-generator.ts

let responseContent = '';
let usedProvider = '';

// Strategy 1: Gemini API
try {
    responseContent = await generateWithGemini(prompt);
    usedProvider = 'Gemini';
} catch (e) {
    console.warn(`⚠️ Gemini failed: ${e.message}`);
}

// Strategy 2: Local LM Studio (fallback)
if (!responseContent) {
    try {
        responseContent = await generateWithLocalLLM(prompt);
        usedProvider = 'Local LLM';
    } catch (e) {
        console.warn(`⚠️ Local LLM failed: ${e.message}`);
    }
}

if (!responseContent) {
    throw new Error('All AI providers failed.');
}

ローカルLLM呼び出し

LM StudioはOpenAI互換APIを提供するので、fetch で簡単に呼べます:

async function generateWithLocalLLM(prompt: string): Promise<string> {
    const url = process.env.LM_STUDIO_URL || "http://127.0.0.1:1234";
    const model = process.env.LM_STUDIO_MODEL || "google/gemma-3-4b";

    const response = await fetch(`${url}/v1/chat/completions`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            model,
            messages: [
                { role: "system", content: "..." },
                { role: "user", content: prompt }
            ],
            temperature: 0.7,
            max_tokens: 4096,
        }),
    });

    const data = await response.json();
    return data.choices?.[0]?.message?.content || "";
}

3️⃣ 3ステップ生成方式(小さなモデル向け)

4Bモデルに「タイトル、本文、翻訳、タグを全部JSONで返して」と言うと、
JSONが壊れたり、途中で切れたりします。

そこで 3ステップに分割 しました:

Step 1: 日本語の記事本文を生成(Markdown出力のみ)
Step 2: タイトル・要約・タグを生成(構造化テキスト)
Step 3: 英語翻訳を生成(Markdown出力のみ)

各ステップのプロンプトは 短くシンプル にし、4Bモデルでも安定して出力できるようにしています。

// Step 1: 本文のみ(JSON不要)
const contentJa = await callLMStudio(
    "あなたは技術ブログの執筆者です。日本語でMarkdown記事を書いてください。",
    `テーマ: ${theme}\n\n記事本文だけを書いてください。`,
    3000
);

// Step 2: メタデータ(行ベースのパース)
const metaResponse = await callLMStudio(
    "ブログ記事のタイトルと要約を考えてください。",
    `以下のフォーマットで回答:\nTITLE_JA: ...\nEXCERPT_JA: ...\nTAGS: ...`,
    500
);

// Step 3: 翻訳
const contentEn = await callLMStudio(
    "Translate the following to English.",
    contentJa,
    3000
);

ポイント: JSON出力を要求しない。行ベースの構造化テキストなら、パースが遥かに安定する。


4️⃣ Task Scheduler で完全自動化

最後のピースは Windows Task Scheduler です。

バッチファイル

@echo off
chcp 65001 >nul

:: ログ設定
set LOGFILE=%~dp0logs\generate_%date:~0,4%%date:~5,2%%date:~8,2%.log
if not exist "%~dp0logs" mkdir "%~dp0logs"

:: LM Studio の起動チェック
curl -s http://127.0.0.1:1234/v1/models >nul 2>&1
if %errorlevel% neq 0 (
    echo LM Studio が起動していません >> "%LOGFILE%"
    exit /b 1
)

:: 記事生成
cd /d "C:\path\to\your\next_app"
call npx tsx scripts/generate-article-local.ts >> "%LOGFILE%" 2>&1

タスク登録(1コマンド)

schtasks /create /tn "DailyArticleGenerator" /tr "C:\path\to\scripts\daily-generate.bat" /sc daily /st 08:00 /f

これで 毎朝8時 に自動実行されます。

前提条件

条件 詳細
LM Studio PC起動時に自動起動設定推奨
PC スリープしていないこと
DB接続 .env.local にNeon DBのURLが設定済み

🏗️ 最終アーキテクチャ

┌──────────────────────────────────────────────────────┐
│  Vercel (クラウド)                                    │
│                                                       │
│  Next.js App Router                                   │
│  ├─ 認証: Clerk (proxy.ts)                           │
│  ├─ 管理画面: /admin (メールベースの管理者チェック)    │
│  ├─ API: /api/admin/* (auth() + clerkClient())       │
│  └─ 記事生成: Gemini API → (LM Studio fallback)      │
│                                                       │
│  Neon PostgreSQL ←──── Prisma ORM                     │
└──────────────────────────────────────────────────────┘
          ↑ DB書き込み
┌──────────────────────────────────────────────────────┐
│  ローカルPC (自宅)                                    │
│                                                       │
│  Windows Task Scheduler                               │
│  └─ 毎朝 8:00                                        │
│      └─ daily-generate.bat                            │
│          └─ npx tsx generate-article-local.ts          │
│              └─ LM Studio (gemma-3-4b)                │
│                  ├─ Step 1: 日本語記事生成             │
│                  ├─ Step 2: メタデータ生成             │
│                  └─ Step 3: 英語翻訳                  │
│              └─ Prisma → Neon DB に保存               │
└──────────────────────────────────────────────────────┘

💡 学んだこと

1. 認証は「マネージドサービス」が正義

NextAuth.jsは柔軟だけど、OAuthの設定が面倒。Clerkなら 5分で完了
個人開発では「設定の少なさ」が正義です。

2. 無料APIは「いつか死ぬ」前提で設計する

Geminiの無料枠はある日突然尽きました。
ローカルLLMというフォールバックがあれば、コスト0でも運用が止まらない

3. 小さなモデルは「小さなタスク」で活かす

4Bモデルに複雑なJSON生成を求めるのは酷。
タスクを分割すれば、小さなモデルでも十分に実用的な出力が得られます。

4. 自動化の最後のピースは「スケジューラ」

スクリプトを書いただけでは自動化とは呼べない。
Task Scheduler(Linux なら cron)で定期実行して初めて「完全自動化」です。


🎉 まとめ

項目 Before After
認証 NextAuth.js Clerk
OAuth設定 GCPコンソールで手動設定 Clerkダッシュボードのみ
記事生成AI Gemini API のみ Gemini + LM Studio
コスト API依存 ローカルLLMで無料運用可
運用 手動 Task Scheduler で完全自動

「認証はClerk、AIはフォールバック付き、運用はスケジューラ」
── 個人開発の自動化、ここに完結。

次は LM Studio の PC起動時自動起動設定と、生成記事の品質モニタリングを考えています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?