Menambahkan llms.txt di Next.js App Router Tanpa Konten Drift
Dua route handler llms.txt untuk situs bilingual Next.js App Router, konten digenerate dari modul yang sama dengan halaman supaya nggak drift.
Ringkasan
Biar situsnya bilingual, gw bikin dua route handler buat serve llms.txt per bahasa, bukan file statis yang cuma bisa satu bahasa. Isinya auto-generate dari data yang sama persis sama halaman, jadi dijamin nggak pernah beda cerita alias zero drift. Search engine sih masih cuek, tapi coding agent dan tool IDE tuh konsumen utamanya.
Muncul plain text di tab browser
Setelah commit sore terkirim, hal pertama yang saya lakukan cuma membuka /llms.txt langsung di address bar. Yang keluar bukan halaman HTML: teks polos dengan header Content-Type: text/plain dan Cache-Control: public, max-age=3600. Nggak ada 404, nggak ada drama. Misi kelihatannya selesai, padahal justru di titik ini keputusan desainnya baru mau saya ceritakan.
Situs company profile ini bilingual: Indonesia sebagai default, Inggris di subtree /en. Ide pertama saya dulu selalu "taruh aja file statis di folder public". Masalahnya satu file statis itu satu bahasa untuk semua subtree, dan saya nggak mau agent yang membaca /en dapat ringkasan berbahasa Indonesia. Rewrite lewat middleware? Terlalu berat buat tugas secanggih ini, plus rawan bug caching yang susah dilacak. Saya butuh mekanisme yang native sama framework-nya.
Dua route handler, satu sumber data
Solusinya dua route handler kecil. app/llms.txt/route.ts melayani locale default di root. app/[locale]/llms.txt/route.ts melayani tiap subtree bahasa, dengan params sebagai Promise yang harus di-await dan guard hasLocale yang fallback ke id.
// app/[locale]/llms.txt/route.ts
import { hasLocale } from "@/config/nav";
import { llmsTxt } from "@/lib/llms";
export const dynamic = "force-static";
export async function GET(
_req: Request,
{ params }: { params: Promise<{ locale: string }> },
) {
const { locale } = await params;
return new Response(llmsTxt(hasLocale(locale) ? locale : "id"), {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=3600",
},
});
}Dokumentasi Next.js memang memosisikan route handler sebagai cara resmi melayani konten non-UI lewat Web Request/Response API, dan menyebut sitemap.xml plus robots.txt sebagai contoh pola yang sama [2]. Konfigurasi segment kayak dynamic yang mengatur perilaku caching-nya juga bagian dari kontrak itu [2].
Kontrak formatnya udah jelas
Format llms.txt versi 2 di llmstxt.org punya aturan yang sederhana: file boleh duduk di root atau di subpath mana pun, dia mencakup semua URL di bawah path-nya, dan kalau beberapa file berlaku sekaligus, agent pilih yang paling spesifik [1]. Struktur isinya: H1 nama situs sebagai satu-satunya section wajib, blockquote ringkasan opsional, lalu section H2 berisi daftar link, dengan ## Optional untuk link yang bisa dilewati [1]. Versi 2 bahkan sudah merekomendasikan link relation rel="alternate" type="text/markdown" untuk versi markdown tiap halaman dan rel="describedby" yang menunjuk ke file llms.txt pencakupnya [1].
Buat saya justru menarik bahwa format ini murni markdown. Proposal awalnya dari Answer.AI bulan September 2024, lahir karena menyusun konteks sebuah website untuk LLM itu ambigu: crawl semua atau kurasi manual [3]. Markdown dipilih supaya file-nya tetap bisa diparsing parser biasa atau regex, tanpa dependency aneh [3].
Anti-drift itu pelajaran sebenarnya
Bagian yang paling menentukan nggak ada di route handler-nya, tapi di lib/llms.ts. Modul ini ngimpor modul konten yang sama persis dengan yang dipakai merender halaman: judul dan deskripsi tiap page diambil dari sumber yang sama. File markdown buat agent otomatis nggak mungkin beda cerita dengan yang dilihat manusia di browser. Auto-generate dari data terpusat = nol drift, dan itu alasan saya nggak mau file statis yang diketik manual.
Satu hal yang saya sampaikan apa adanya: search engine konvensional sampai hari ini masih mengabaikan file ini. Konsumen nyatanya sekarang adalah coding agent dan tool IDE yang butuh konteks cepat pas inference. Tapi biaya implementasinya juga miring: dua file route handler kecil dan satu modul data. Kalau filenya sudah pasti harus digenerate, digenerate dari satu sumber kebenaran yang sama dengan halaman. Itu keputusan arsitekturnya, sisanya cuma format.
Sources
[1] llmstxt.org: The /llms.txt file, v2
[2] Next.js docs: route.js / Route Handlers
[3] Answer.AI: /llms.txt proposal