Menu Lain
Daftar BacaGanti TemaCari
Reading List

Queue · 0 items

Daftar baca Anda kosong. Simpan artikel untuk membacanya nanti.

Start Reading

Kenapa AI Crawler Tidak Bisa Baca Site Next.js App Router

Iwan Efendi3 min

Next.js App Router output-nya RSC Flight payload, bukan HTML biasa. Crawler HTTP fetch seperti Claude web_fetch tidak bisa baca konten. Ini temuan dan solusinya.

This article is available in EnglishRead in English →
Read in EnglishEN
Saya iseng minta Claude fetch salah satu artikel SnipGeek langsung dari URL-nya. Yang balik cuma title tag. Isi artikel kosong sama sekali. Reaksi pertama saya: pasti ada yang salah di kode saya.

Diagnosis Pertama: Client-Side Rendering?

Dugaan awalnya simpel — mungkin halaman artikel masih client-side rendered. HTML yang dikirim server cuma shell kosong, konten baru muncul setelah JavaScript jalan. Ini kesalahan klasik Next.js kalau "use client" nyasar ke page component. Saya minta Antigravity audit seluruh codebase. Hasilnya bersih:
  • [locale]/blog/[slug]/page.tsx → ✅ Server Component
  • [locale]/notes/[slug]/page.tsx → ✅ Server Component
  • MDX dikompilasi server-side via next-mdx-remote/rsc → ✅
  • generateStaticParams sudah ada → ✅
Semuanya benar. (Kalau penasaran bagaimana struktur komponen site ini dibangun dari awal, saya pernah tulis di Membangun Header Pil Mengambang dengan Next.js & Tailwind.) Tapi kalau begitu, kenapa kontennya kosong?

Diagnosis Kedua: RSC Flight Format

Saya jalankan diagnostic lebih dalam langsung ke URL live:
curl -s https://snipgeek.com/notes/how-to-read-ai-build-failed-logs | grep -i 'article\|content\|body\|prose' | head -20
Response-nya 101KB — bukan halaman kosong. Kata kunci content, article, prose semua ditemukan ratusan kali. Tapi waktu dicek lebih dalam, ini yang saya lihat:
{"className":"text-lg text-foreground/80 prose-content","children":"$L1d"}
$L1d bukan teks artikel. Itu referensi ke React Server Component chunk — format streaming khusus Next.js App Router yang disebut RSC Flight format. Konten artikelnya ada, tapi terkemas dalam payload yang hanya bisa didecode oleh React runtime. Konfirmasi:
curl -s https://snipgeek.com/notes/how-to-read-ai-build-failed-logs | grep '<p>'
# Total <p> tags: 0
# Total <h2> tags: 0
Nol tag HTML biasa. Semua konten ada di dalam RSC payload.

Contoh Fetch yang Dapat Direproduksi

Untuk melihat hal ini secara langsung, kamu dapat menjalankan skrip Node.js sederhana berikut. Skrip ini akan mengambil file HTML mentah dan memeriksa keberadaan tag paragraph (<p>):
// test-fetch.js
fetch("https://snipgeek.com/notes/how-to-read-ai-build-failed-logs")
  .then(res => res.text())
  .then(html => {
    console.log("Ukuran HTML:", (html.length / 1024).toFixed(1) + " KB");
    console.log("Memiliki Tag Paragraf (<p>):", html.includes("<p>"));
    console.log("Memiliki Data Payload RSC:", html.includes("__next_f"));
  });
Ketika kamu mengeksekusi kode di atas, hasilnya adalah:
Ukuran HTML: 101.2 KB
Memiliki Tag Paragraf (<p>): false
Memiliki Data Payload RSC: true
Ini menegaskan bahwa dokumen HTML dimuat menggunakan script block stream __next_f yang mirip JSON, bukan tag paragraf HTML standar.

Ini Bukan Bug — Ini Trade-off Arsitektur

Pages Router lama menghasilkan HTML murni: <p>, <h2>, konten terbaca langsung di response. App Router beralih ke RSC Flight — format streaming yang dioptimalkan untuk performa hydration, tapi tidak bisa dibaca tanpa React runtime. Untuk SEO, ini tidak masalah:
CrawlerBisa Baca Konten?Alasan
GooglebotHeadless Chrome, render JS penuh
BingbotSama — render JS penuh
AI crawler (GPTBot, ClaudeBot)⚠️Tergantung — ada yang render JS, ada yang tidak
Claude via web_fetchPlain HTTP fetch, tanpa eksekusi JavaScript
Google bisa baca semua artikel. Masalahnya spesifik ke crawler yang hanya mengandalkan plain HTTP tanpa render JavaScript.

Pola Mitigasi untuk Situs App Router

Ketika menghadapi masalah ini, kamu memiliki dua pilihan pola mitigasi utama.

Pola 1: Endpoint API JSON/Markdown (Pendekatan SnipGeek)

Solusi paling bersih adalah menawarkan alternatif endpoint yang ramah mesin (machine-readable). Saya menambahkan Route Handler di Next.js yang menyajikan konten artikel sebagai JSON biasa — tanpa format RSC, tanpa butuh JavaScript:
GET /api/posts/[slug]?locale=en   → JSON artikel bahasa Inggris
GET /api/posts/[slug]?locale=id   → JSON artikel bahasa Indonesia
GET /api/notes/[slug]?locale=en   → JSON notes bahasa Inggris
GET /api/notes/[slug]?locale=id   → JSON notes bahasa Indonesia
Berikut adalah versi sederhana dari Route Handler Next.js (src/app/api/notes/[slug]/route.ts) yang menerapkan pola ini:
import { NextResponse } from "next/server";
import { getNoteBySlug } from "@/lib/notes";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;
  const { searchParams } = new URL(request.url);
  const locale = searchParams.get("locale") || "en";

  try {
    const note = await getNoteBySlug(slug, locale);
    if (!note) {
      return NextResponse.json({ error: "Not Found" }, { status: 404 });
    }

    return NextResponse.json(
      {
        slug: note.frontmatter.slug,
        title: note.frontmatter.title,
        description: note.frontmatter.description,
        content: note.content, // Raw MDX/Markdown string
      },
      {
        headers: {
          "X-Robots-Tag": "noindex", // Sangat penting: hindari masalah duplikasi konten SEO
          "Cache-Control": "public, max-age=3600",
        },
      }
    );
  } catch (err) {
    return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
  }
}

Pola 2: Headless Browser Rendering untuk AI User Agent

Jika kamu tidak dapat menyediakan API khusus, kamu dapat mengonfigurasi server atau reverse proxy untuk mengarahkan AI user agent (seperti ChatGPT-User atau ClaudeBot) melalui layanan prerendering (seperti Puppeteer atau Prerender.io). Layanan ini akan menjalankan browser headless, mengeksekusi bundle React, dan mengembalikan HTML yang ter-render sepenuhnya. Beberapa keputusan yang saya ambil saat implementasi rute API ini:
  • Locale fallback — kalau versi id belum ada, fallback ke en dengan isFallback: true di response.
  • X-Robots-Tag: noindex — supaya API route tidak diindex Google sebagai duplicate dari halaman utama.
  • Cache-Control: public, max-age=3600 — cache response supaya tidak trigger serverless invocation berulang.
  • translationUrls — field yang berisi URL lengkap untuk tiap locale yang tersedia, berguna untuk tool yang consume API ini.
Setelah deploy, test langsung:
curl -s "https://snipgeek.com/api/posts/ubuntu-26-04-beta-sudah-bisa-didownload?locale=id"
Response:
{
  "slug": "ubuntu-26-04-beta-sudah-bisa-didownload",
  "locale": "id",
  "isFallback": false,
  "translationAvailable": ["en", "id"],
  "translationUrls": {
    "en": "/api/posts/ubuntu-26-04-beta-sudah-bisa-didownload?locale=en",
    "id": "/api/posts/ubuntu-26-04-beta-sudah-bisa-didownload?locale=id"
  },
  "title": "Ubuntu 26.04 Beta Sudah Rilis — Tapi Jangan Buru-Buru Install",
  "description": "...",
  "date": "2026-03-30",
  "tags": ["ubuntu", "linux", "beta"],
  "content": "\nSaya nunggu beta Ubuntu 26.04 ini sambil setengah semangat..."
}
Konten artikel terbaca penuh sebagai plain text. Siapapun — termasuk AI, developer, atau tool apapun — bisa baca isi artikel SnipGeek tanpa butuh browser atau JavaScript.
Perubahan Aman
API route ini hidup sepenuhnya di namespace /api/* — terpisah dan tidak bisa mengganggu routing halaman yang sudah ada. Ini pure additive change — tidak menyentuh apapun yang sebelumnya.

Langkah Selanjutnya

Yang saya rencanakan berikutnya: implementasi llms.txt — standar baru mirip robots.txt tapi khusus untuk AI — yang berisi daftar semua URL konten SnipGeek dalam format yang mudah diproses LLM crawler. Kalau kamu juga punya site Next.js App Router dan frustrasi kenapa AI tidak bisa baca kontennya, coba tambah API route plain JSON seperti ini. Kalau di tengah proses debugging kamu menemui output build yang membingungkan, artikel Cara Memperbaiki Log Build Gagal pada AI dengan Cepat bisa membantu kamu membaca apa yang sebenarnya dikatakan toolchain. Hasilnya langsung terasa.

FAQ

T: Apakah masalah ini juga mempengaruhi Googlebot saat mengindex site Next.js App Router? Tidak. Googlebot menggunakan headless Chromium dan mengeksekusi JavaScript sepenuhnya sebelum mengindex halaman. RSC Flight payload bisa dibaca tanpa workaround apapun. Masalah ini spesifik ke crawler plain HTTP — termasuk banyak tool AI dan custom scraper — yang fetch response mentah tanpa menjalankan JavaScript. T: Apakah menambah route /api/posts/[slug] bisa merusak SEO karena konten duplikat? Tidak, selama kamu set header X-Robots-Tag: noindex di response API route tersebut. Dengan begitu, search engine tidak akan mengindeksnya. Halaman kanonik di /blog/[slug] tetap menjadi satu-satunya versi yang diindex. API route tidak terlihat oleh sistem ranking Google. T: Kenapa Next.js tidak langsung output HTML biasa untuk halaman statis? Ini trade-off yang disengaja. RSC Flight format memungkinkan streaming efisien, partial hydration, dan batas komponen server/client — fitur-fitur yang membuat App Router lebih cepat saat runtime. Output HTML biasa akan mengorbankan keuntungan tersebut. Untuk kebanyakan kasus, keuntungan performa sepadan, tapi memang menciptakan blind spot untuk crawler non-JS. T: Apakah pendekatan yang sama bisa dipakai untuk framework lain seperti Remix atau Astro? Format RSC Flight adalah hal spesifik Next.js App Router. Remix secara default sudah menghasilkan HTML biasa dari loader, dan output statis Astro juga sudah plain HTML. Kalau kamu pakai framework tersebut dan AI crawler tidak bisa baca kontennya, penyebabnya kemungkinan besar adalah konten yang diinjeksi JavaScript atau client router gaya SPA — bukan encoding RSC.

Referensi

  1. Next.js Route Handlers — Next.js Docs
  2. React Server Components — React Docs
  3. llms.txt — Emerging Standard for AI Crawlers
Topics

Topik dalam catatan

Jelajahi pembahasan serupa lewat topik-topik terkait berikut.

Bagikan artikel ini

Diskusi

Menyiapkan area komentar...

Anda Mungkin Juga Suka