Skip to main content
Skill web app · Pelajaran 7 dari 11 · 14 menit

Tutorial Middleware Next.js: Matcher, Rewrite, dan Accept

Belajar middleware Next.js lewat file nyata: matcher, rewrite vs redirect, dan content negotiation header Accept yang melayani Markdown di URL yang sama.

Oleh Matthews WongDitinjau pada

Poin utama

Middleware Next.js adalah fungsi yang berjalan di server sebelum route dirender, sehingga bisa me-rewrite, me-redirect, atau menjawab sebuah request. Matcher menentukan request mana yang sampai ke sana. Situs ini memakainya untuk membaca header Accept dan diam-diam me-rewrite agen yang meminta Markdown ke route terpisah, di URL yang sama.

Setelah pelajaran ini, Anda bisa

  • Menulis matcher yang melewatkan file statis dan API route.
  • Memilih antara rewrite dan redirect, dan menjelaskan apa yang dilihat klien.
  • Melayani dua representasi di satu URL dengan header Accept, dan paham kenapa Vary penting.
Lihat cara kerjanya

Ikuti diagramnya langkah demi langkah

Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.

Satu URL, dua jawaban: HTML atau Markdown

Request melewati matcher ke middleware yang membaca Accept: browser lanjut ke next-intl dan halaman HTML, sedangkan text/markdown di-rewrite ke app/agent-md yang menjawab dengan Vary: Accept.

Langkah 1 dari 6. Satu URL, dua pembaca. Middleware berdiri di depan setiap halaman, membaca request, dan menentukan route mana dari dua pilihan yang menjawab.

Tombol: ← → langkah · Home / End · K putar atau jeda

  1. Satu URL, dua pembaca. Middleware berdiri di depan setiap halaman, membaca request, dan menentukan route mana dari dua pilihan yang menjawab.

  2. Matcher melewatkan api, _next, dan path yang mengandung titik. Path halaman seperti /en/about/ lolos, jadi middleware berjalan sebelum apa pun dirender.

    matcher: ["/((?!api|_next|.*\\..*).*)"],
  3. Browser menyebut text/html dan mencantumkan wildcard di q=0.8. Markdown tidak pernah disebut, jadi middleware menyerahkan request ke next-intl dan halaman biasa.

    const response = intlMiddleware(request);Tebak langkah berikutnya Berikutnya, sebuah agen mengirim Accept: text/markdown. Apakah alamatnya berubah? (Ya, diarahkan ke /agent-md · Tidak, di-rewrite dan URL-nya tetap)
  4. Markdown disebut dan peringkatnya setidaknya setara HTML, jadi middleware memanggil NextResponse.rewrite ke app/agent-md. URL klien tidak berubah dan tidak ada redirect locale.

    return NextResponse.rewrite(url);
  5. Di dalam route hasil rewrite, request.nextUrl masih memuat URL yang diminta klien. Handler membaca path dari params miliknya sendiri.

    const page = resolvePageMarkdown(requested);Tebak langkah berikutnya Route menjawab dalam Markdown di URL halaman itu sendiri. Header mana yang membuat cache bisa membedakannya dari HTML? (Vary: Accept · Content-Language · X-Content-Type-Options)
  6. Balasannya text/markdown dengan Vary: Accept, sehingga cache bersama mengunci pada Accept dan tidak menyajikan Markdown ke browser di URL yang sama.

    Vary: "Accept",

Apa itu middleware Next.js, dan kapan ia berjalan?

Middleware adalah file di root proyek, sejajar dengan folder app, yang mengekspor satu fungsi. Next.js menjalankannya di server sebelum request diselesaikan, dan ia bisa me-rewrite request, me-redirect, mengubah header, atau langsung menjawab. Dokumentasi menyebutnya lugas: ia berjalan sebelum route dirender, sehingga cocok untuk keputusan lintas halaman seperti autentikasi, logging, dan redirect.

Baca selengkapnya (2)

Urutan penting. Dokumentasi mencantumkannya: headers dan redirects dari next.config lebih dulu, lalu middleware, lalu rewrite beforeFiles, lalu route filesystem seperti app dan public, kemudian rewrite afterFiles, route dinamis, dan terakhir rewrite fallback. Jadi rewrite di middleware terjadi sebelum Next.js mencari halamannya.

Catatan penamaan. Repositori ini memakai Next.js 15.5, tempat file-nya bernama middleware.ts. Dokumentasi terbaru menyebut Next.js 16 mendeprekasi konvensi itu dan menggantinya jadi proxy, lengkap dengan codemod untuk migrasi dan konsep yang sama. Dokumentasi juga menyarankan middleware sebagai pilihan terakhir, jadi pakai ketika rewrite di next.config atau sebuah route tidak cukup.

Bagaimana matcher menentukan request mana yang menjalankannya?

Tanpa matcher, middleware berjalan pada setiap request, termasuk file statis, optimasi gambar, dan seisi folder public. Dokumentasi memperingatkan bahwa logika seperti autentikasi atau redirect bisa memblokir CSS, JS, atau gambar. Solusinya adalah export config berisi matcher: satu path, array path, atau regular expression.

Baca selengkapnya (1)

Situs ini menginginkan semua halaman, tetapi bukan API, bagian internal Next.js, atau file berekstensi, jadi memakai negative lookahead: semuanya kecuali path berawalan api atau _next, dan kecuali path yang memuat titik. Klausa terakhir itulah yang menjaga robots.txt, sitemap.xml, dan gambar tetap di luar. Dokumentasi menambahkan aturan yang perlu diingat: source harus diawali slash, pola dianchor ke awal path, dan nilai matcher harus konstanta karena dianalisis saat build.

Rewrite atau redirect: apa yang dilihat klien?

NextResponse menyediakan keduanya. Redirect mengirim request ke URL lain. Rewrite menampilkan isi sebuah URL sebagai pengganti URL yang diminta. Dokumentasi menyebut dua cara menghasilkan respons dari middleware: rewrite ke sebuah route, baik halaman maupun route handler, atau langsung mengembalikan NextResponse.

Baca selengkapnya (1)

Itu sebabnya situs ini memakai rewrite, bukan redirect. Request Markdown di-rewrite ke app/agent-md, sebuah route handler yang merender Markdown halaman dari data yang sama dengan HTML. Agen tetap memakai URL yang ia minta dan mendapat 200 di sana, tanpa redirect locale di tengah, karena rewrite berjalan sebelum middleware next-intl.

Bagaimana header Accept memilih HTML atau Markdown?

Ini adalah content negotiation yang digerakkan server. MDN menjelaskannya sebagai klien mengirim header yang menggambarkan preferensinya, dan server memakainya sebagai petunjuk; algoritma pemilihan diserahkan ke server. Header Accept mencantumkan tipe media yang bisa diproses klien, masing-masing dengan faktor kualitas q opsional yang mengurutkannya.

Baca selengkapnya (2)

RFC 9110 menyatakan penerima sebaiknya memperlakukan parameter bernama q sebagai bobot, dan bila lebih dari satu media range berlaku untuk sebuah tipe, rujukan yang paling spesifik didahulukan. Fungsi prefersMarkdown di repositori mengikuti keduanya. Ia mem-parse setiap range beserta q-nya, mencari text/markdown, dan membandingkannya dengan kualitas yang diberikan klien untuk text/html, memakai range yang paling spesifik.

Lalu ia menambahkan aturan pengaman sendiri: Markdown harus disebut secara eksplisit, jadi wildcard tidak pernah memilihnya. Header biasa browser menyebut text/html dan menaruh wildcard any-type di q=0.8, sehingga browser dan klien HTTP polos tetap mendapat HTML. Jika seri, Markdown menang karena klien menyebutnya.

Kenapa Vary: Accept penting, dan di mana bisa dipasang?

URL yang sama kini bisa punya dua isi. MDN menjelaskan bahwa header Vary dari server mencantumkan header request yang dipakai untuk memilih, sehingga cache dapat mereproduksi keputusan itu. RFC 9110 lebih tegas: cache tidak boleh memakai ulang respons tersimpan untuk request berikutnya kecuali field header yang tercantum cocok, atau origin memvalidasi pemakaian ulang itu. Tanpa Vary: Accept, cache bisa menyajikan Markdown ke browser.

Baca selengkapnya (1)

Di repo ini route handler mengirim Vary: Accept pada balasan Markdown, dan middleware menambahkannya ke respons next-intl. Namun halaman HTML yang dirender tidak bisa membawanya. Komentar kode mencatat bahwa app page handler Next mengganti header Vary saat render, dan handler di node_modules/next (15.5.7) memang memanggil setHeader dengan nilainya sendiri. Saat ini belum ada cache di depan situs, jadi rencananya header ditambahkan di nginx jika cache bersama muncul.

Cara situs ini melakukannya

Konfigurasi nyata di balik pelajaran ini

Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.

middleware.ts:24-35
export default function middleware(request: NextRequest): NextResponse {
  if (prefersMarkdown(request.headers.get("accept"))) {
    const url = request.nextUrl.clone();
    url.pathname = `${MARKDOWN_ROUTE}${request.nextUrl.pathname}`;
    return NextResponse.rewrite(url);
  }
  const response = intlMiddleware(request);
  response.headers.append("Vary", "Accept");
  return response;
}

export const config = {

Seluruh keputusannya ada di cabang pertama. Markdown di-rewrite ke route internal dan langsung return, sebelum next-intl sempat menambah redirect locale. Selain itu semua lewat next-intl, dan file ini juga menambahkan Vary: Accept ke respons tersebut.

middleware.ts:36-40
  // Match all pathnames except for
  // - API routes
  // - Static files
  // - _next (Next.js internals)
  matcher: ["/((?!api|_next|.*\\..*).*)"],

Sebuah negative lookahead: cocokkan setiap path kecuali yang diawali api atau _next, atau yang memuat titik. Backslash ganda adalah satu titik yang di-escape di string JavaScript. Klausa inilah yang menjauhkan file seperti sitemap.xml dari fungsi tersebut.

app/agent-md/[[...path]]/route.ts:15-19
  // From the route's own segments: after a middleware rewrite,
  // request.nextUrl still holds the URL the client asked for.
  const { path = [] } = await params;
  const requested = path.length ? `/${path.join("/")}/` : "/";
  const page = resolvePageMarkdown(requested);

Jebakannya dalam satu komentar. Setelah rewrite, handler bisa tergoda membaca request.nextUrl, tetapi itu masih URL asli. Ia membangun path yang diminta dari params, segmen milik route itu sendiri, sehingga merender halaman yang memang diminta.

lib/agent/content-negotiation.ts:89-99
export function prefersMarkdown(accept: string | null): boolean {
  const ranges = parseAccept(accept);
  const markdownQ = Math.max(
    0,
    ...ranges
      .filter((range) => MARKDOWN_ALIASES.has(`${range.type}/${range.subtype}`))
      .map((range) => range.q),
  );
  if (markdownQ === 0) return false;
  return markdownQ >= qualityFor(ranges, HTML_TYPE);
}

Hanya range text/markdown yang eksplisit yang dihitung, jadi wildcard tidak pernah memilihnya. Markdown menang bila q-nya setidaknya sama dengan yang diberikan klien untuk HTML, sehingga hasil seri jatuh ke Markdown.

Yang sering rusak

Kesalahan yang benar-benar membuang waktu

  • Tanpa matcher, atau matcher yang terlalu lebar

    Tanpa matcher, fungsi berjalan pada setiap request, termasuk file statis dan gambar, sehingga rewrite atau redirect bisa merusak CSS, JS, atau gambar. Tambahkan negative lookahead untuk api, _next, dan file berekstensi, dan jaga nilai matcher tetap konstanta.

  • Accept berisi wildcard memilih representasi yang salah

    Jika pengecekan Anda menganggap bintang-slash-bintang sebagai cocok, setiap browser dan panggilan curl mendapat Markdown. Wajibkan tipenya disebut, dan uji header yang benar-benar dikirim browser, bintang-slash-bintang polos, serta kasus q=0.

  • Membaca request.nextUrl di route hasil rewrite

    Repo ini mencatat bahwa setelah rewrite, request.nextUrl masih memuat URL yang diminta klien. Handler yang memakainya tidak pernah melihat path route-nya sendiri. Baca path dari params.

  • Mengira Vary di halaman akan bertahan

    Header yang Anda pasang di middleware pada halaman HTML yang dirender bisa tergantikan saat halaman dirender, karena handler halaman Next memasang Vary sendiri. Pasang pada respons yang Anda kendalikan, seperti route handler, atau tambahkan di proxy di depan aplikasi.

Coba sendiri

Lakukan di komputer Anda sendiri

Jalankan situs secara lokal dan lihat satu URL menjawab dua kali.

  1. Jalankan dev server, lalu minta sebuah halaman seperti browser dan baca content type-nya.

    npm run dev
    curl -s -o /dev/null -D - -H "Accept: text/html" http://localhost:3000/en/about/
  2. Minta URL yang sama dengan Accept: text/markdown. Content type dan header Vary berubah, URL tidak.

    curl -s -D - -H "Accept: text/markdown" http://localhost:3000/en/about/ | head -20
  3. Kirim header ala browser dengan wildcard dan pastikan tetap HTML, lalu beri q=0 pada Markdown.

    curl -s -o /dev/null -D - -H "Accept: text/html,*/*;q=0.8" http://localhost:3000/en/about/
    curl -s -o /dev/null -D - -H "Accept: text/markdown;q=0" http://localhost:3000/en/about/
  4. Jalankan tes negosiasi yang mengunci semua kasus tadi.

    node --test tests/agent-readiness.test.mjs
Kuis singkat

Tiga pertanyaan sebelum lanjut

Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.

  1. 1.Browser mengirim Accept: text/html, lalu wildcard bintang-slash-bintang di q=0.8. Apa hasil prefersMarkdown?
  2. 2.Kenapa situs ini memakai NextResponse.rewrite, bukan redirect, untuk request Markdown?
  3. 3.Matcher Anda /((?!api|_next).*). Sebuah request untuk /sitemap.xml datang. Apa yang terjadi?
Pikirkan dulu

Jawab dalam hati, lalu cek

  1. 1.Kenapa Anda tidak bisa mengandalkan middleware untuk memasang Vary di halaman HTML di sini?Lihat jawabannya

    Jawaban:Handler halaman app Next memasang header Vary sendiri saat halaman dirender dan menggantikan nilainya. Pasang di route handler milik Anda, atau di proxy di depan aplikasi jika cache bersama ditambahkan.

  2. 2.Di route Markdown, dari mana Anda membaca path yang diminta, dan kenapa?Lihat jawabannya

    Jawaban:Dari params milik route. Setelah rewrite middleware, request.nextUrl masih memuat URL yang diminta klien, seperti dicatat komentar di handler.

  3. 3.Apa yang berubah saat proyek ini pindah dari Next.js 15 ke 16?Lihat jawabannya

    Jawaban:Dokumentasi menyebut konvensi file middleware dideprekasi dan diganti nama menjadi proxy: file menjadi proxy.ts dan fungsinya bernama proxy, dengan codemod untuk penggantian nama. Konsep matcher dan rewrite tetap berlaku.

Pelajari lebih dalam

Bahasan mendalam terkait di situs ini