Apa itu Openrouter dan Cara Integrasi ke Blog Dengan Next JS Modern

Kalau kamu pernah develop aplikasi yang menggunakan AI, pasti familiar dengan situasi ini: untuk pakai GPT-4 harus daftar di OpenAI, untuk Claude harus daftar di Anthropic, untuk Gemini harus ke Google AI Studio. Setiap provider punya API format berbeda, billing system berbeda, dan API key berbeda. Belum lagi kalau mau eksperimen dengan model open-source seperti Llama atau Mistral.

Bayangkan harus manage 5-6 API keys berbeda, masing-masing dengan dashboard billing terpisah, dan code yang berbeda untuk setiap provider. Capek, kan?

Di sinilah OpenRouter hadir sebagai solusi.

Apa itu OpenRouter?

OpenRouter adalah unified API gateway yang memberikan akses ke ratusan AI model dari berbagai provider melalui satu endpoint dan satu API key. Bayangkan seperti "Gojek untuk AI models" — kamu tidak perlu hubungi setiap driver (provider) langsung, cukup lewat satu aplikasi yang handle semuanya.

Beberapa angka yang menunjukkan skala OpenRouter saat ini:

MetrikAngka
Monthly Tokens Processed25+ Trillion
Global Users5+ Million
Active Providers60+
Available Models300+

Yang membuat OpenRouter powerful adalah API-nya fully compatible dengan OpenAI SDK. Artinya, kalau kamu sudah punya code yang menggunakan OpenAI, kamu cukup ganti baseURL dan langsung bisa akses ratusan model lain tanpa ubah logic apapun.

// Sebelum: Direct OpenAI
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Sesudah: OpenRouter (akses 300+ models!)
const openai = new OpenAI({
  baseURL: '<https://openrouter.ai/api/v1>',
  apiKey: process.env.OPENROUTER_API_KEY,
});

Sesimple itu.

Kenapa Pakai OpenRouter?

Ada beberapa alasan kenapa developer memilih OpenRouter dibanding langsung ke masing-masing provider:

1. One API Key untuk Semua Model

Tidak perlu lagi juggle multiple API keys. Satu key OpenRouter = akses ke GPT-5, Claude, Gemini, Llama, DeepSeek, dan ratusan model lainnya.

2. Switch Model Tanpa Ubah Code

Mau ganti dari Claude ke GPT? Cukup ubah string model name:

// Ganti model semudah ganti string
model: 'anthropic/claude-sonnet-4'     // Claude
model: 'openai/gpt-4o'                 // GPT-4o
model: 'google/gemini-2.5-flash'       // Gemini
model: 'meta-llama/llama-3.1-70b-instruct'  // Llama
model: 'deepseek/deepseek-chat'        // DeepSeek (murah!)

3. Better Uptime dengan Automatic Fallback

OpenRouter punya distributed infrastructure yang otomatis switch ke provider lain kalau satu provider down. Jadi kalau OpenAI lagi maintenance, request kamu bisa di-route ke provider backup.

4. No Subscription, Pay-as-You-Go

Tidak ada monthly fee atau commitment. Kamu beli credits, pakai sesuai kebutuhan. Credits berlaku 1 tahun sejak pembelian.

5. Transparent Pricing

OpenRouter menggunakan pass-through pricing — harga yang kamu bayar sama dengan harga dari provider asli, plus small platform fee saat beli credits.

Model yang Tersedia

OpenRouter menyediakan akses ke model dari semua major AI labs. Berikut beberapa yang populer:

Frontier Models (Most Capable):

ModelProviderKeunggulan
GPT-5.2OpenAILatest & most capable GPT
Claude Opus 4.5AnthropicExcellent reasoning & writing
Gemini 3 ProGoogleStrong multimodal
Grok 4xAIReal-time knowledge

Cost-Effective Options:

ModelProviderKeunggulan
GPT-4o MiniOpenAIBalanced performance/cost
Claude Sonnet 4AnthropicGreat for most tasks
Gemini 2.5 FlashGoogleVery fast, cheap
DeepSeek V3DeepSeekExcellent coding, very cheap

Free Tier Models:

ModelProviderRate Limit
Llama 3.1 8BMeta50 req/day (free), 1000/day (paid)
DeepSeek R1DeepSeek50 req/day (free)
Gemini Flash (free)GoogleLimited
Mistral 7BMistralLimited

💡 Pro tip: Untuk development dan testing, gunakan free models. Untuk production, pilih model sesuai use case — tidak selalu harus yang paling mahal!

Pricing: Bagaimana Cara Kerjanya?

OpenRouter menggunakan credit system:

  1. Beli Credits — Deposit ke akun OpenRouter (minimum ~$5)
  2. Pakai Model Apapun — Credits dipotong sesuai usage
  3. Pricing per Million Tokens — Setiap model punya harga berbeda

Contoh estimasi biaya untuk 1 juta tokens:

ModelInput CostOutput Cost
Llama 3.1 8BFREEFREE
Gemini 2.5 Flash$0.10$0.40
GPT-4o Mini$0.15$0.60
Claude Sonnet 4$3.00$15.00
GPT-4o$2.50$10.00
Claude Opus 4.5$15.00$75.00

Untuk blog dengan traffic moderate (katakanlah 1000 AI requests per hari dengan average 500 tokens per request), menggunakan Gemini Flash akan cost sekitar $0.50/hari atau ~$15/bulan. Sangat affordable untuk fitur AI yang powerful.

Sekarang setelah paham apa itu OpenRouter dan kenapa worth it, mari kita setup project Next.js dan mulai integrasi.

Setup Project Next.js + OpenRouter

Sekarang mari kita setup project dari nol. Kita akan buat blog sederhana dengan fitur AI chatbot yang bisa menjawab pertanyaan tentang konten blog.

Prerequisites

Sebelum mulai, pastikan kamu sudah punya:

  • Node.js 18+ — Cek dengan node -v
  • npm atau pnpm — Package manager
  • Code editor — VS Code recommended
  • Akun OpenRouter — Gratis, daftar di openrouter.ai

Step 1: Buat Akun OpenRouter

  1. Kunjungi openrouter.ai
  2. Klik Sign Up (bisa pakai Google, GitHub, atau MetaMask)
  3. Setelah login, pergi ke Keys di sidebar
  4. Klik Create Key untuk generate API key baru
  5. Copy API key — formatnya seperti sk-or-v1-xxxxxxxxxxxxxxxx

⚠️ Penting: Simpan API key dengan aman. Jangan commit ke Git atau expose di client-side code!

Untuk testing, kamu bisa langsung pakai free models tanpa add credits. Tapi kalau mau akses model premium seperti GPT-4o atau Claude, kamu perlu Add Credits (minimum ~$5).

Step 2: Create Next.js Project

Buka terminal dan jalankan:

# Create new Next.js project dengan App Router
npx create-next-app@latest my-ai-blog

# Pilih options berikut saat prompted:
# ✔ Would you like to use TypeScript? Yes
# ✔ Would you like to use ESLint? Yes
# ✔ Would you like to use Tailwind CSS? Yes
# ✔ Would you like your code inside a `src/` directory? No
# ✔ Would you like to use App Router? Yes
# ✔ Would you like to use Turbopack? Yes
# ✔ Would you like to customize the import alias? No

# Masuk ke directory project
cd my-ai-blog

Step 3: Install Dependencies

Kita akan pakai Vercel AI SDK dengan OpenRouter provider — ini adalah cara paling recommended untuk integrasi OpenRouter di Next.js.

# Install AI SDK dan OpenRouter provider
npm install ai @ai-sdk/react @openrouter/ai-sdk-provider

# Install zod untuk schema validation (optional, untuk structured output)
npm install zod

Penjelasan packages:

PackageFungsi
aiCore Vercel AI SDK — streaming, generateText, dll
@ai-sdk/reactReact hooks — useChat, useCompletion
@openrouter/ai-sdk-providerOpenRouter provider untuk AI SDK
zodSchema validation untuk structured output

Alternative: Kalau kamu prefer pakai OpenAI SDK langsung (karena OpenRouter OpenAI-compatible), bisa install:

npm install openai

Tapi untuk tutorial ini, kita pakai official OpenRouter provider karena lebih seamless dengan AI SDK.

Step 4: Setup Environment Variables

Buat file .env.local di root project:

# .env.local
OPENROUTER_API_KEY=sk-or-v1-your-api-key-here

Ganti sk-or-v1-your-api-key-here dengan API key yang kamu dapat dari OpenRouter dashboard.

Pastikan .env.local sudah ada di .gitignore (Next.js sudah handle ini by default):

# .gitignore
.env*.local

Step 5: Setup OpenRouter Provider

Buat file untuk initialize OpenRouter provider. Ini akan dipakai di semua API routes.

// lib/openrouter.ts

import { createOpenRouter } from '@openrouter/ai-sdk-provider';

// Create OpenRouter instance
export const openrouter = createOpenRouter({
  apiKey: process.env.OPENROUTER_API_KEY!,
});

// Helper untuk ganti model dengan mudah
export const models = {
  // Free models (untuk development)
  free: 'meta-llama/llama-3.1-8b-instruct:free',

  // Cost-effective (untuk production budget-friendly)
  cheap: 'google/gemini-2.5-flash',
  deepseek: 'deepseek/deepseek-chat',

  // Balanced (good quality, reasonable price)
  balanced: 'anthropic/claude-sonnet-4',
  gpt4o: 'openai/gpt-4o',

  // Premium (best quality)
  premium: 'anthropic/claude-opus-4.5',
} as const;

Dengan setup ini, kamu bisa easily switch model:

import { openrouter, models } from '@/lib/openrouter';

// Development - pakai free model
model: openrouter.chat(models.free)

// Production - pakai yang lebih capable
model: openrouter.chat(models.balanced)

Step 6: Struktur Project

Setelah setup, struktur project kamu akan terlihat seperti ini:

my-ai-blog/
├── app/
│   ├── api/
│   │   ├── chat/
│   │   │   └── route.ts       # Chat API endpoint
│   │   └── summarize/
│   │       └── route.ts       # Summarize API endpoint
│   ├── page.tsx               # Homepage
│   ├── layout.tsx             # Root layout
│   └── globals.css            # Global styles
├── components/
│   ├── ChatWidget.tsx         # AI Chat widget
│   └── SummaryButton.tsx      # Content summarizer
├── lib/
│   └── openrouter.ts          # OpenRouter provider setup
├── .env.local                 # Environment variables
├── package.json
└── tsconfig.json

Step 7: Verify Setup

Mari test apakah setup sudah benar dengan membuat simple API route.

// app/api/test/route.ts

import { generateText } from 'ai';
import { openrouter, models } from '@/lib/openrouter';

export async function GET() {
  try {
    const result = await generateText({
      model: openrouter.chat(models.free), // Pakai free model untuk test
      prompt: 'Say "Hello from OpenRouter!" in Indonesian',
      maxTokens: 50,
    });

    return Response.json({
      success: true,
      message: result.text
    });
  } catch (error) {
    console.error('OpenRouter test failed:', error);
    return Response.json({
      success: false,
      error: 'Failed to connect to OpenRouter'
    }, { status: 500 });
  }
}

Jalankan development server:

npm run dev

Buka browser dan akses http://localhost:3000/api/test. Kalau setup benar, kamu akan lihat response seperti:

{
  "success": true,
  "message": "Halo dari OpenRouter!"
}

🎉 Selamat! OpenRouter sudah terkoneksi dengan Next.js project kamu.

Kalau dapat error, cek beberapa hal:

  • API key sudah benar di .env.local
  • Sudah restart dev server setelah tambah env variables
  • Kalau pakai paid model, pastikan sudah ada credits di akun

Sekarang project sudah siap. Di bagian selanjutnya, kita akan build fitur chat yang sebenarnya menggunakan Vercel AI SDK dengan streaming response.

Integrasi dengan Vercel AI SDK

Sekarang kita masuk ke bagian seru — build fitur chat dengan streaming response. Vercel AI SDK membuat ini sangat mudah dengan hooks seperti useChat yang handle semua complexity di balik layar.

Kenapa Vercel AI SDK?

Vercel AI SDK adalah toolkit resmi untuk building AI applications di Next.js. Beberapa keunggulannya:

FeatureBenefit
Built-in StreamingResponse muncul kata per kata, UX lebih baik
React HooksuseChat, useCompletion — state management otomatis
Type-safeFull TypeScript support
Edge ReadyBisa deploy di Edge Runtime untuk latency rendah
Provider AgnosticSupport OpenRouter, OpenAI, Anthropic, dll

OpenRouter punya official provider (@openrouter/ai-sdk-provider) yang terintegrasi sempurna dengan AI SDK.

Buat Chat API Route

Pertama, kita buat API endpoint yang handle chat requests dengan streaming.

// app/api/chat/route.ts

import { streamText } from 'ai';
import { openrouter, models } from '@/lib/openrouter';

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

export async function POST(req: Request) {
  try {
    const { messages } = await req.json();

    const result = await streamText({
      model: openrouter.chat(models.balanced), // Claude Sonnet 4
      system: `Kamu adalah AI assistant yang helpful untuk blog tentang web development.
Jawab pertanyaan dengan jelas dan ringkas dalam bahasa Indonesia.
Jika ditanya tentang topik di luar web development, tetap jawab dengan sopan.`,
      messages,
      maxTokens: 1000,
    });

    return result.toDataStreamResponse();
  } catch (error) {
    console.error('Chat API error:', error);
    return Response.json(
      { error: 'Terjadi kesalahan saat memproses chat' },
      { status: 500 }
    );
  }
}

Penjelasan code:

  • streamText — Function dari AI SDK untuk generate text dengan streaming
  • system — System prompt yang define personality dan behavior AI
  • messages — Conversation history dari client
  • toDataStreamResponse() — Convert stream ke format yang compatible dengan useChat hook

Buat Chat Widget Component

Sekarang kita buat React component untuk UI chat. Ini akan jadi floating widget di corner blog.

// components/ChatWidget.tsx

'use client';

import { useChat } from '@ai-sdk/react';
import { useState, useRef, useEffect } from 'react';

export function ChatWidget() {
  const [isOpen, setIsOpen] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const {
    messages,
    input,
    handleInputChange,
    handleSubmit,
    isLoading,
    error
  } = useChat({
    api: '/api/chat',
  });

  // Auto-scroll ke message terbaru
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <>
      {/* Toggle Button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="fixed bottom-6 right-6 w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all z-50"
        aria-label={isOpen ? 'Tutup chat' : 'Buka chat'}
      >
        {isOpen ? (
          <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
          </svg>
        ) : (
          <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
          </svg>
        )}
      </button>

      {/* Chat Window */}
      {isOpen && (
        <div className="fixed bottom-24 right-6 w-96 h-[500px] bg-white rounded-2xl shadow-2xl flex flex-col z-50 border border-gray-200">
          {/* Header */}
          <div className="px-4 py-3 border-b bg-blue-600 text-white rounded-t-2xl">
            <h3 className="font-semibold">AI Assistant</h3>
            <p className="text-xs text-blue-100">Tanya apa saja tentang blog ini</p>
          </div>

          {/* Messages Area */}
          <div className="flex-1 overflow-y-auto p-4 space-y-4">
            {/* Welcome message */}
            {messages.length === 0 && (
              <div className="text-center text-gray-500 mt-8">
                <div className="text-4xl mb-2">👋</div>
                <p className="text-sm">Hai! Ada yang bisa saya bantu?</p>
              </div>
            )}

            {/* Chat messages */}
            {messages.map((message) => (
              <div
                key={message.id}
                className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
              >
                <div
                  className={`max-w-[80%] px-4 py-2 rounded-2xl ${
                    message.role === 'user'
                      ? 'bg-blue-600 text-white rounded-br-md'
                      : 'bg-gray-100 text-gray-800 rounded-bl-md'
                  }`}
                >
                  <p className="text-sm whitespace-pre-wrap">{message.content}</p>
                </div>
              </div>
            ))}

            {/* Loading indicator */}
            {isLoading && (
              <div className="flex justify-start">
                <div className="bg-gray-100 px-4 py-2 rounded-2xl rounded-bl-md">
                  <div className="flex space-x-1">
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
                  </div>
                </div>
              </div>
            )}

            {/* Error message */}
            {error && (
              <div className="bg-red-50 text-red-600 px-4 py-2 rounded-lg text-sm">
                Oops! Terjadi kesalahan. Coba lagi ya.
              </div>
            )}

            <div ref={messagesEndRef} />
          </div>

          {/* Input Area */}
          <form onSubmit={handleSubmit} className="p-4 border-t">
            <div className="flex gap-2">
              <input
                value={input}
                onChange={handleInputChange}
                placeholder="Ketik pesan..."
                className="flex-1 px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
                disabled={isLoading}
              />
              <button
                type="submit"
                disabled={isLoading || !input.trim()}
                className="px-4 py-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
              >
                <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
                </svg>
              </button>
            </div>
          </form>
        </div>
      )}
    </>
  );
}

Tambahkan Chat Widget ke Layout

Supaya chat widget muncul di semua halaman, tambahkan ke root layout:

// app/layout.tsx

import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ChatWidget } from '@/components/ChatWidget';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'My AI Blog',
  description: 'Blog dengan AI Assistant',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="id">
      <body className={inter.className}>
        {children}
        <ChatWidget />
      </body>
    </html>
  );
}

Test Chat Feature

Jalankan development server:

npm run dev

Buka http://localhost:3000 dan klik tombol chat di pojok kanan bawah. Coba kirim pesan seperti:

  • "Apa itu React hooks?"
  • "Jelaskan perbedaan SSR dan SSG"
  • "Bagaimana cara deploy Next.js?"

Kamu akan lihat response muncul secara streaming — kata per kata, bukan sekaligus. Ini memberikan UX yang jauh lebih baik karena user tidak perlu menunggu seluruh response selesai.

Switch Model dengan Mudah

Salah satu keunggulan OpenRouter adalah kemudahan switch model. Misalnya, untuk development kamu mau pakai free model, tapi production pakai yang lebih capable:

// app/api/chat/route.ts

import { openrouter, models } from '@/lib/openrouter';

export async function POST(req: Request) {
  const { messages } = await req.json();

  // Pilih model berdasarkan environment
  const selectedModel = process.env.NODE_ENV === 'production'
    ? models.balanced  // Claude Sonnet untuk production
    : models.free;     // Llama free untuk development

  const result = await streamText({
    model: openrouter.chat(selectedModel),
    messages,
    // ... rest of config
  });

  return result.toDataStreamResponse();
}

Atau biarkan user memilih model (untuk app yang lebih advanced):

// Request body bisa include model preference
const { messages, preferredModel } = await req.json();

const modelMap: Record<string, string> = {
  'fast': models.cheap,      // Gemini Flash
  'balanced': models.balanced, // Claude Sonnet
  'premium': models.premium,   // Claude Opus
};

const result = await streamText({
  model: openrouter.chat(modelMap[preferredModel] || models.balanced),
  messages,
});

Sekarang blog kamu sudah punya AI chatbot yang functional! Di bagian selanjutnya, kita akan tambahkan fitur-fitur AI lain yang berguna untuk blog.

Implementasi Fitur AI untuk Blog

Selain chatbot, ada banyak fitur AI yang bisa menambah value ke blog. Di bagian ini kita akan implementasi tiga fitur praktis: Content Summarizer, SEO Meta Generator, dan Writing Assistant.

Fitur 1: Content Summarizer

Fitur ini berguna untuk artikel panjang — pembaca bisa dapat ringkasan sebelum memutuskan untuk baca full article. Kita pakai Gemini Flash karena cepat dan murah.

API Route:

// app/api/summarize/route.ts

import { generateText } from 'ai';
import { openrouter } from '@/lib/openrouter';

export async function POST(req: Request) {
  try {
    const { content, language = 'id' } = await req.json();

    // Validasi input
    if (!content || content.length < 200) {
      return Response.json(
        { error: 'Konten terlalu pendek untuk diringkas (minimal 200 karakter)' },
        { status: 400 }
      );
    }

    // Limit content untuk menghemat tokens
    const truncatedContent = content.slice(0, 8000);

    const result = await generateText({
      model: openrouter.chat('google/gemini-2.5-flash'), // Fast & cheap
      prompt: `Buatkan ringkasan dari artikel berikut dalam bahasa ${language === 'id' ? 'Indonesia' : 'Inggris'}.

Ringkasan harus:
- Terdiri dari 2-3 paragraf
- Mencakup poin-poin utama
- Mudah dipahami
- Tidak lebih dari 200 kata

Artikel:
${truncatedContent}

Ringkasan:`,
      maxTokens: 400,
    });

    return Response.json({
      summary: result.text,
      originalLength: content.length,
      model: 'gemini-2.5-flash'
    });
  } catch (error) {
    console.error('Summarize error:', error);
    return Response.json(
      { error: 'Gagal membuat ringkasan. Silakan coba lagi.' },
      { status: 500 }
    );
  }
}

React Component:

// components/SummaryButton.tsx

'use client';

import { useState } from 'react';

interface SummaryButtonProps {
  content: string;
}

export function SummaryButton({ content }: SummaryButtonProps) {
  const [summary, setSummary] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isOpen, setIsOpen] = useState(false);

  const handleSummarize = async () => {
    if (summary) {
      setIsOpen(true);
      return;
    }

    setIsLoading(true);
    try {
      const response = await fetch('/api/summarize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content }),
      });

      const data = await response.json();

      if (data.error) {
        alert(data.error);
        return;
      }

      setSummary(data.summary);
      setIsOpen(true);
    } catch (error) {
      alert('Gagal membuat ringkasan');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <>
      <button
        onClick={handleSummarize}
        disabled={isLoading}
        className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
      >
        {isLoading ? (
          <>
            <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
              <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
              <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
            </svg>
            Meringkas...
          </>
        ) : (
          <>
            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
            </svg>
            {summary ? 'Lihat Ringkasan' : 'Ringkas dengan AI'}
          </>
        )}
      </button>

      {/* Summary Modal */}
      {isOpen && summary && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
          <div className="bg-white rounded-xl max-w-lg w-full p-6 shadow-xl">
            <div className="flex justify-between items-center mb-4">
              <h3 className="text-lg font-semibold">📝 Ringkasan AI</h3>
              <button
                onClick={() => setIsOpen(false)}
                className="text-gray-500 hover:text-gray-700"
              >
                <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            </div>
            <div className="prose prose-sm">
              <p className="text-gray-700 whitespace-pre-wrap">{summary}</p>
            </div>
            <p className="text-xs text-gray-400 mt-4">Dibuat dengan Gemini Flash via OpenRouter</p>
          </div>
        </div>
      )}
    </>
  );
}

Fitur 2: SEO Meta Tags Generator

Fitur ini otomatis generate title, description, dan keywords berdasarkan konten artikel. Sangat berguna untuk content creators yang ingin optimize SEO tanpa effort manual.

// app/api/generate-meta/route.ts

import { generateObject } from 'ai';
import { openrouter } from '@/lib/openrouter';
import { z } from 'zod';

// Schema untuk structured output
const metaTagsSchema = z.object({
  title: z.string().describe('SEO title, maksimal 60 karakter, menarik dan deskriptif'),
  description: z.string().describe('Meta description, maksimal 155 karakter, ringkas dan engaging'),
  keywords: z.array(z.string()).describe('5-8 keywords relevan untuk artikel'),
  ogTitle: z.string().describe('Open Graph title untuk social sharing'),
  ogDescription: z.string().describe('Open Graph description untuk social sharing'),
});

export async function POST(req: Request) {
  try {
    const { content, existingTitle } = await req.json();

    if (!content || content.length < 100) {
      return Response.json(
        { error: 'Konten terlalu pendek untuk generate meta tags' },
        { status: 400 }
      );
    }

    // Ambil portion awal artikel untuk konteks
    const excerpt = content.slice(0, 3000);

    const result = await generateObject({
      model: openrouter.chat('meta-llama/llama-3.1-70b-instruct'), // Free & good!
      schema: metaTagsSchema,
      prompt: `Berdasarkan artikel berikut, generate SEO meta tags dalam bahasa Indonesia.

${existingTitle ? `Judul asli: ${existingTitle}` : ''}

Konten artikel:
${excerpt}

Generate meta tags yang optimal untuk SEO dan social media sharing.`,
    });

    return Response.json(result.object);
  } catch (error) {
    console.error('Meta generation error:', error);
    return Response.json(
      { error: 'Gagal generate meta tags' },
      { status: 500 }
    );
  }
}

Penggunaan di Blog Admin:

// components/MetaGenerator.tsx

'use client';

import { useState } from 'react';

interface MetaTags {
  title: string;
  description: string;
  keywords: string[];
  ogTitle: string;
  ogDescription: string;
}

interface MetaGeneratorProps {
  content: string;
  existingTitle?: string;
  onGenerated: (meta: MetaTags) => void;
}

export function MetaGenerator({ content, existingTitle, onGenerated }: MetaGeneratorProps) {
  const [isLoading, setIsLoading] = useState(false);
  const [meta, setMeta] = useState<MetaTags | null>(null);

  const handleGenerate = async () => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/generate-meta', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content, existingTitle }),
      });

      const data = await response.json();

      if (data.error) {
        alert(data.error);
        return;
      }

      setMeta(data);
      onGenerated(data);
    } catch (error) {
      alert('Gagal generate meta tags');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="border rounded-lg p-4 bg-gray-50">
      <div className="flex justify-between items-center mb-4">
        <h3 className="font-semibold">🔍 SEO Meta Tags</h3>
        <button
          onClick={handleGenerate}
          disabled={isLoading}
          className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700 disabled:opacity-50"
        >
          {isLoading ? 'Generating...' : 'Generate dengan AI'}
        </button>
      </div>

      {meta && (
        <div className="space-y-3 text-sm">
          <div>
            <label className="block text-gray-600 mb-1">Title ({meta.title.length}/60)</label>
            <input
              type="text"
              value={meta.title}
              readOnly
              className="w-full px-3 py-2 border rounded bg-white"
            />
          </div>
          <div>
            <label className="block text-gray-600 mb-1">Description ({meta.description.length}/155)</label>
            <textarea
              value={meta.description}
              readOnly
              rows={2}
              className="w-full px-3 py-2 border rounded bg-white"
            />
          </div>
          <div>
            <label className="block text-gray-600 mb-1">Keywords</label>
            <div className="flex flex-wrap gap-1">
              {meta.keywords.map((keyword, i) => (
                <span key={i} className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
                  {keyword}
                </span>
              ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Fitur 3: Writing Assistant (Inline Suggestions)

Fitur ini membantu penulis dengan memberikan suggestions untuk improve tulisan mereka. Bisa digunakan untuk expand paragraph, fix grammar, atau generate alternative phrasing.

// app/api/writing-assist/route.ts

import { generateText } from 'ai';
import { openrouter } from '@/lib/openrouter';

type AssistType = 'expand' | 'simplify' | 'fix-grammar' | 'rephrase' | 'continue';

const prompts: Record<AssistType, string> = {
  'expand': 'Expand dan jelaskan lebih detail paragraf berikut, tambahkan contoh jika relevan:',
  'simplify': 'Sederhanakan paragraf berikut agar lebih mudah dipahami, gunakan bahasa yang lebih simple:',
  'fix-grammar': 'Perbaiki grammar dan ejaan dari teks berikut, pertahankan makna aslinya:',
  'rephrase': 'Tulis ulang paragraf berikut dengan cara yang berbeda tapi maknanya sama:',
  'continue': 'Lanjutkan tulisan berikut dengan 1-2 paragraf yang relevan:',
};

export async function POST(req: Request) {
  try {
    const { text, assistType } = await req.json() as {
      text: string;
      assistType: AssistType;
    };

    if (!text || !assistType) {
      return Response.json(
        { error: 'Text dan assistType diperlukan' },
        { status: 400 }
      );
    }

    const prompt = prompts[assistType];
    if (!prompt) {
      return Response.json(
        { error: 'Tipe assist tidak valid' },
        { status: 400 }
      );
    }

    const result = await generateText({
      model: openrouter.chat('anthropic/claude-sonnet-4'), // Best for writing
      prompt: `${prompt}

"${text}"

Hasil (dalam bahasa Indonesia):`,
      maxTokens: 800,
    });

    return Response.json({
      result: result.text,
      assistType,
      model: 'claude-sonnet-4'
    });
  } catch (error) {
    console.error('Writing assist error:', error);
    return Response.json(
      { error: 'Gagal memproses permintaan' },
      { status: 500 }
    );
  }
}

Toolbar Component:

// components/WritingToolbar.tsx

'use client';

import { useState } from 'react';

interface WritingToolbarProps {
  selectedText: string;
  onResult: (result: string) => void;
}

const assistOptions = [
  { type: 'expand', label: '📝 Expand', description: 'Jelaskan lebih detail' },
  { type: 'simplify', label: '✨ Simplify', description: 'Sederhanakan' },
  { type: 'fix-grammar', label: '✓ Fix Grammar', description: 'Perbaiki ejaan' },
  { type: 'rephrase', label: '🔄 Rephrase', description: 'Tulis ulang' },
  { type: 'continue', label: '➡️ Continue', description: 'Lanjutkan' },
] as const;

export function WritingToolbar({ selectedText, onResult }: WritingToolbarProps) {
  const [isLoading, setIsLoading] = useState(false);
  const [activeType, setActiveType] = useState<string | null>(null);

  const handleAssist = async (assistType: string) => {
    if (!selectedText.trim()) {
      alert('Pilih teks terlebih dahulu');
      return;
    }

    setIsLoading(true);
    setActiveType(assistType);

    try {
      const response = await fetch('/api/writing-assist', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: selectedText, assistType }),
      });

      const data = await response.json();

      if (data.error) {
        alert(data.error);
        return;
      }

      onResult(data.result);
    } catch (error) {
      alert('Gagal memproses');
    } finally {
      setIsLoading(false);
      setActiveType(null);
    }
  };

  return (
    <div className="flex flex-wrap gap-2 p-2 bg-gray-100 rounded-lg">
      {assistOptions.map((option) => (
        <button
          key={option.type}
          onClick={() => handleAssist(option.type)}
          disabled={isLoading}
          className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
            activeType === option.type
              ? 'bg-blue-600 text-white'
              : 'bg-white hover:bg-gray-200 text-gray-700'
          } disabled:opacity-50`}
          title={option.description}
        >
          {activeType === option.type ? 'Processing...' : option.label}
        </button>
      ))}
    </div>
  );
}

Perbandingan Model untuk Setiap Fitur

Memilih model yang tepat sangat penting untuk balance antara quality dan cost:

FiturRecommended ModelAlasanEst. Cost/1K requests
Chat/Q&AClaude Sonnet 4Nuanced, conversational~$3-5
SummarizerGemini 2.5 FlashFast, cheap, good enough~$0.30
Meta GeneratorLlama 3.1 70BFree tier available!FREE
Writing AssistClaude Sonnet 4Best writing quality~$3-5
Simple TasksDeepSeek ChatVery cheap, decent~$0.50

💡 Pro tip: Mulai dengan free/cheap models untuk development. Upgrade ke premium models hanya untuk features yang benar-benar butuh quality tinggi.

Dengan ketiga fitur ini, blog kamu sudah punya AI capabilities yang comprehensive. Di bagian terakhir, kita akan bahas best practices untuk production deployment.

Best Practices dan Tips

Sebelum deploy ke production, ada beberapa best practices yang perlu kamu perhatikan untuk memastikan aplikasi AI-mu reliable, secure, dan cost-effective.

1. Pilih Model yang Tepat untuk Use Case

Tidak semua task butuh model paling mahal. Berikut panduan pemilihan:

Use CaseRecommendedAvoidAlasan
Simple Q&AGemini Flash, Llama 3.1GPT-5, Claude OpusOverkill, buang biaya
Creative WritingClaude Sonnet/OpusLlama smallClaude unggul di nuance
Code GenerationDeepSeek Coder, GPT-4oGeminiSpecialized models lebih baik
SummarizationGemini FlashClaude OpusSpeed > quality untuk summary
Complex ReasoningClaude Opus, GPT-5Free modelsButuh capability tinggi
TranslationGPT-4o, Gemini ProSmall modelsMulti-language understanding

Rule of thumb: Mulai dengan model termurah yang bisa handle task, upgrade hanya kalau quality tidak memadai.

2. Implement Proper Error Handling

API calls bisa gagal karena berbagai alasan. Handle dengan graceful:

// lib/api-utils.ts

import { APIError } from 'openai';

export async function handleAIRequest<T>(
  requestFn: () => Promise<T>,
  fallbackFn?: () => Promise<T>
): Promise<T> {
  try {
    return await requestFn();
  } catch (error) {
    // Rate limit exceeded
    if (error instanceof APIError && error.status === 429) {
      console.warn('Rate limit hit, waiting before retry...');

      // Wait and retry once
      await new Promise(resolve => setTimeout(resolve, 2000));

      try {
        return await requestFn();
      } catch (retryError) {
        // If still failing and fallback exists, use it
        if (fallbackFn) {
          console.log('Using fallback model...');
          return await fallbackFn();
        }
        throw retryError;
      }
    }

    // Model unavailable - try fallback
    if (error instanceof APIError && error.status === 503) {
      if (fallbackFn) {
        console.log('Primary model unavailable, using fallback...');
        return await fallbackFn();
      }
    }

    throw error;
  }
}

Penggunaan:

// app/api/chat/route.ts

const result = await handleAIRequest(
  // Primary: Claude Sonnet
  () => streamText({
    model: openrouter.chat('anthropic/claude-sonnet-4'),
    messages,
  }),
  // Fallback: GPT-4o Mini (lebih murah, selalu available)
  () => streamText({
    model: openrouter.chat('openai/gpt-4o-mini'),
    messages,
  })
);

3. Implement Rate Limiting untuk Users

Jangan biarkan single user menghabiskan semua credits-mu:

// lib/rate-limit.ts

const WINDOW_MS = 60 * 1000; // 1 minute
const MAX_REQUESTS = 10; // 10 requests per minute per user

const requestCounts = new Map<string, { count: number; resetTime: number }>();

export function checkRateLimit(userId: string): { allowed: boolean; retryAfter?: number } {
  const now = Date.now();
  const userLimit = requestCounts.get(userId);

  if (!userLimit || now > userLimit.resetTime) {
    // Reset window
    requestCounts.set(userId, { count: 1, resetTime: now + WINDOW_MS });
    return { allowed: true };
  }

  if (userLimit.count >= MAX_REQUESTS) {
    const retryAfter = Math.ceil((userLimit.resetTime - now) / 1000);
    return { allowed: false, retryAfter };
  }

  userLimit.count++;
  return { allowed: true };
}

// app/api/chat/route.ts

import { checkRateLimit } from '@/lib/rate-limit';

export async function POST(req: Request) {
  // Get user identifier (IP, session, atau user ID)
  const userId = req.headers.get('x-forwarded-for') || 'anonymous';

  const { allowed, retryAfter } = checkRateLimit(userId);

  if (!allowed) {
    return Response.json(
      { error: `Terlalu banyak request. Coba lagi dalam ${retryAfter} detik.` },
      { status: 429, headers: { 'Retry-After': String(retryAfter) } }
    );
  }

  // Continue with AI request...
}

4. Cache Responses untuk Hemat Biaya

Untuk queries yang sama, tidak perlu hit API lagi:

// lib/cache.ts

const cache = new Map<string, { data: string; expiry: number }>();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

export function getCached(key: string): string | null {
  const cached = cache.get(key);
  if (cached && Date.now() < cached.expiry) {
    return cached.data;
  }
  cache.delete(key);
  return null;
}

export function setCache(key: string, data: string): void {
  cache.set(key, { data, expiry: Date.now() + CACHE_TTL });
}

// Generate cache key dari content
export function generateCacheKey(type: string, content: string): string {
  // Simple hash untuk content
  const hash = content.slice(0, 100).replace(/\\s/g, '').toLowerCase();
  return `${type}:${hash}`;
}

// app/api/summarize/route.ts

import { getCached, setCache, generateCacheKey } from '@/lib/cache';

export async function POST(req: Request) {
  const { content } = await req.json();

  // Check cache first
  const cacheKey = generateCacheKey('summary', content);
  const cached = getCached(cacheKey);

  if (cached) {
    return Response.json({ summary: cached, fromCache: true });
  }

  // Generate new summary
  const result = await generateText({ ... });

  // Cache the result
  setCache(cacheKey, result.text);

  return Response.json({ summary: result.text, fromCache: false });
}

💡 Pro tip: Untuk production, gunakan Redis atau Vercel KV sebagai cache store yang persistent dan bisa di-share antar instances.

5. Security Best Practices

NEVER expose API key di client-side:

// ❌ SALAH - API key di client
const response = await fetch('<https://openrouter.ai/api/v1/chat>', {
  headers: {
    'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OPENROUTER_KEY}` // BAHAYA!
  }
});

// ✅ BENAR - Selalu lewat API route
const response = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages })
});

Validate dan sanitize input:

// app/api/chat/route.ts

export async function POST(req: Request) {
  const { messages } = await req.json();

  // Validate messages structure
  if (!Array.isArray(messages) || messages.length === 0) {
    return Response.json({ error: 'Invalid messages format' }, { status: 400 });
  }

  // Limit message count (prevent context stuffing)
  const limitedMessages = messages.slice(-20); // Max 20 messages

  // Limit content length per message
  const sanitizedMessages = limitedMessages.map(msg => ({
    ...msg,
    content: typeof msg.content === 'string'
      ? msg.content.slice(0, 4000) // Max 4000 chars per message
      : msg.content
  }));

  // Continue with sanitized messages...
}

6. Monitor Usage dan Costs

OpenRouter menyediakan dashboard untuk monitor usage, tapi kamu juga bisa track di aplikasi:

// lib/usage-tracker.ts

interface UsageLog {
  timestamp: Date;
  model: string;
  inputTokens: number;
  outputTokens: number;
  userId?: string;
}

const usageLogs: UsageLog[] = [];

export function logUsage(log: UsageLog) {
  usageLogs.push(log);

  // Optional: Send to analytics service
  console.log(`[AI Usage] Model: ${log.model}, Tokens: ${log.inputTokens + log.outputTokens}`);
}

export function getUsageStats() {
  const totalTokens = usageLogs.reduce(
    (sum, log) => sum + log.inputTokens + log.outputTokens,
    0
  );
  return { totalRequests: usageLogs.length, totalTokens };
}

Quick Reference: Environment Setup

# .env.local untuk development
OPENROUTER_API_KEY=sk-or-v1-xxx

# Optional: Default model
DEFAULT_AI_MODEL=anthropic/claude-sonnet-4

# Optional: Fallback model
FALLBACK_AI_MODEL=openai/gpt-4o-mini

# Rate limiting
RATE_LIMIT_REQUESTS=10
RATE_LIMIT_WINDOW_MS=60000

Penutup

Selamat! 🎉 Kamu sudah berhasil mengintegrasikan OpenRouter ke blog Next.js dengan berbagai fitur AI:

FiturStatus
✅ AI ChatbotStreaming responses dengan useChat
✅ Content SummarizerAuto-ringkas artikel panjang
✅ SEO Meta GeneratorGenerate title, description, keywords
✅ Writing AssistantExpand, simplify, rephrase teks
✅ Error HandlingRetry logic dan fallback models
✅ Rate LimitingProtect dari abuse
✅ CachingHemat biaya dengan cache

Kenapa OpenRouter Worth It?

Setelah melalui tutorial ini, kamu bisa lihat keunggulan OpenRouter:

  1. Satu API untuk semua — Tidak perlu manage multiple API keys
  2. Flexibility — Switch model semudah ganti string
  3. Cost-effective — Pilih model sesuai budget dan kebutuhan
  4. Reliability — Automatic fallback untuk better uptime
  5. Developer-friendly — OpenAI-compatible, easy migration

Next Steps

Beberapa ide untuk expand fitur AI di blog-mu:

  • RAG (Retrieval-Augmented Generation) — Chatbot yang bisa jawab berdasarkan konten blog
  • AI Image Generation — Thumbnail generator dengan DALL-E atau Stable Diffusion
  • Voice Interface — Text-to-speech untuk artikel
  • Multi-language — Auto-translate konten ke berbagai bahasa
  • Personalization — Rekomendasi artikel berdasarkan reading history

Belajar Lebih Dalam di BuildWithAngga

Tutorial ini memberikan fondasi integrasi AI ke Next.js. Untuk menguasai web development secara lebih mendalam dan komprehensif, kamu bisa melanjutkan pembelajaran di BuildWithAngga.

Kenapa Belajar di BuildWithAngga?

BenefitDeskripsi
Akses SelamanyaSekali bayar, akses kelas selamanya. Update materi juga gratis tanpa biaya tambahan.
Magang Online DibayarKesempatan magang di project nyata dengan bayaran. Bangun portfolio sambil dapat penghasilan.
Konsultasi dengan MentorStuck di project? Tanya langsung ke mentor yang sudah expert di bidangnya. Tidak perlu struggle sendirian.
Portfolio ReviewMentor akan review portfolio-mu dan kasih feedback untuk meningkatkan peluang dapat kerja atau client.
Komunitas AktifGabung dengan ribuan member lain yang juga sedang belajar. Networking, sharing, dan support system.
Sertifikat CompletionDapat sertifikat yang bisa ditambahkan ke LinkedIn dan CV sebagai bukti kompetensi.

Kelas yang Relevan

Untuk melanjutkan journey dari tutorial ini, berikut kelas-kelas yang direkomendasikan:

Frontend & React:

  • Kelas React.js untuk Pemula — Kuasai fundamental React sebelum deep dive ke Next.js
  • Kelas Next.js Full-Stack — Build full-stack app dengan App Router, Server Actions, dan database
  • Kelas Tailwind CSS — Styling modern yang dipakai di tutorial ini

Backend & API:

  • Kelas Node.js & Express — Pahami backend fundamentals
  • Kelas Laravel API Development — Alternative backend dengan PHP
  • Kelas Database Design — MySQL, PostgreSQL, dan MongoDB

Advanced & Career:

  • Kelas TypeScript Mastery — Level up JavaScript skills
  • Kelas Docker untuk Developer — Containerization dan deployment
  • Kelas Menjadi Freelancer Sukses — Monetize skills-mu

Mulai Sekarang

  1. Kunjungi buildwithangga.com
  2. Explore kelas sesuai learning path-mu
  3. Mulai dari fundamental, naik ke advanced
  4. Praktekkan di project nyata
  5. Konsultasi dengan mentor kalau stuck

Dengan kombinasi tutorial gratis seperti ini dan pembelajaran terstruktur di kelas, kamu akan punya skill set lengkap untuk menjadi developer yang tidak hanya bisa coding, tapi juga bisa build AI-powered applications yang production-ready.


"The best time to start learning was yesterday. The second best time is now."

Sekarang kamu sudah punya knowledge untuk build AI features dengan OpenRouter dan Next.js. Pertanyaannya: apa yang akan kamu build selanjutnya?

Happy coding! 🚀