ข้ามไปยังเนื้อหา

File Conventions และ Special Files

ใน App Router โฟลเดอร์นิยาม route segment และชื่อไฟล์ที่จองไว้ชุดเล็ก ๆ ให้พฤติกรรมกับแต่ละโฟลเดอร์ — โฟลเดอร์จะเข้าถึงเป็น URL ได้ก็ต่อเมื่อมี page อยู่ข้างใน

โฟลเดอร์ใต้ app/ คือ URL segment แต่โฟลเดอร์เปล่า ๆ ไม่ทำอะไร สิ่งที่ทำถูกตัดสินโดยไฟล์ที่จองไว้ข้างใน ชื่อไฟล์แต่ละอันมีหน้าที่เดียว —

  • page.tsx — UI ของ route และเป็นตัวที่ทำให้ segment นั้น route ได้แบบ public
  • layout.tsx — wrapper ที่ใช้ร่วมกัน คงอยู่ข้ามการ navigate และ nest ซ้อนใน parent layout
  • loading.tsx — Suspense fallback ที่แสดงระหว่าง segment กำลัง stream เข้ามา
  • error.tsx — error boundary ของ segment และต้องเป็น Client Component
  • not-found.tsx — UI ที่ render เมื่อ notFound() ถูกเรียก
  • route.ts — Route Handler คือ API endpoint แทนที่จะเป็น page
  • template.tsx — คล้าย layout แต่ re-mount instance ใหม่ทุกครั้งที่ navigate
app/
layout.tsx # root layout — wraps everything
page.tsx # "/"
dashboard/
layout.tsx # wraps everything under /dashboard
loading.tsx # fallback while /dashboard loads
error.tsx # catches errors in /dashboard
page.tsx # "/dashboard"
api/
users/
route.ts # "/api/users" — a Route Handler, not a page

page รับ route param และ search param แล้ว return UI ของ segment —

app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <article>Reading: {slug}</article>;
}

layout ครอบ segment และทุกอย่างที่อยู่ข้างใต้ และเก็บ state ไว้ข้ามการ navigate ระหว่าง sibling page ดังนั้น chrome ที่ใช้ร่วมกันอย่าง sidebar จึงไม่ remount —

app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>Dashboard nav</nav>
{children}
</section>
);
}

ทุก App Router app ต้องมี root layout หนึ่งอันที่ app/layout.tsx เป็น layout เดียวที่ render tag <html> และ <body> เพราะ Next.js ไม่ได้ปล่อย tag พวกนี้ให้คุณ —

app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

มีสองกฎที่ตามมาจากทั้งหมดนี้ กฎแรก โฟลเดอร์ที่ไม่มี page.tsx (หรือ route.ts) จะ route ไม่ได้ — คุณ nest โฟลเดอร์เพื่อจัดระเบียบไฟล์ล้วน ๆ ได้ และจะไม่ตอบสนองต่อ URL เลย กฎที่สอง error.tsx ต้องขึ้นต้นด้วย "use client" เพราะ error boundary พึ่งพา React ฝั่ง client

flowchart LR
  Folder[dashboard folder] --> Layout[layout wraps and persists]
  Layout --> Loading[loading is the Suspense fallback]
  Layout --> Error[error catches failures]
  Layout --> Page[page is the routable UI]
  Page --> URL[/dashboard is reachable]
special file แต่ละไฟล์ map กับ route segment ยังไง

page กับ layout คือสองอันที่คุณใช้ทุกวัน แต่ App Router จองชื่อไฟล์ไว้อีกยาวกว่านั้นมาก แต่ละอันตรวจสอบแล้วกับ Next.js docs ที่ bundle มา (node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/) —

ไฟล์ทำอะไรสถานะ
page.tsxUI ของ segment ทำให้มัน route ได้แบบ publicStable
layout.tsxwrapper ที่ใช้ร่วมกัน คงอยู่ข้าม sibling navigation อันที่เป็น root ต้อง render <html>/<body>Stable
template.tsxเหมือน layout แต่ re-mount instance ใหม่ทุกครั้งที่ navigate แทนที่จะคงอยู่Stable
loading.tsx<Suspense> fallback ที่โชว์ระหว่าง segment กำลัง stream เข้ามาStable
error.tsxerror boundary แบบ Client Component ของ segmentStable
not-found.tsxUI ที่ render เมื่อ notFound() ถูกเรียก หรือ segment ไม่ matchStable
default.tsxUI สำรองของ slot ใน Parallel Routes เมื่อ Next.js กู้ active state คืนไม่ได้ตอน hard navigationStable
global-error.tsxerror boundary ระดับ root — ตัวเดียวที่จับ error จาก root layout/template เองได้ ต้อง define <html>/<body> เอง และ export metadata ไม่ได้Stable
forbidden.tsxrender เมื่อ forbidden() ถูกเรียก Next.js คืน 403Experimental
unauthorized.tsxrender เมื่อ unauthorized() ถูกเรียก Next.js คืน 401Experimental
route.tsRoute Handler — GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS — แทนที่จะเป็น pageStable
proxy.tsรันก่อน request จะเสร็จ บนทุก route ที่ match (ไฟล์ที่ Next.js 16 เปลี่ยนชื่อจาก middleware.ts) ดู Proxy and the Request LifecycleStable
instrumentation.tsexport register() (optional, รันครั้งเดียวต่อ server instance) และ onRequestError() (optional) สำหรับ observability อยู่ที่ root เท่านั้นStable
instrumentation-client.tsคู่หูฝั่ง client — รันก่อน app จะ interactive สำหรับ monitoring/analytics/polyfill ไม่ต้อง export อะไรเป็นพิเศษStable
mdx-components.tsxจำเป็นสำหรับใช้ @next/mdx กับ App Router export ฟังก์ชัน useMDXComponents() เดียวStable
sitemap.ts / sitemap.xmlgenerate (หรือ define แบบ static) /sitemap.xmlStable
robots.ts / robots.txtgenerate (หรือ define แบบ static) /robots.txt แบบ generate ถูก cache โดย default เหมือน Route Handler ทั่วไปStable
opengraph-image.tsx / twitter-image.tsxgenerate รูปสำหรับแชร์บน social ของ segment ด้วย ImageResponse หรือใส่ไฟล์รูป static ชื่อเดียวกันStable

forbidden.tsx กับ unauthorized.tsx เป็นสองตัวเดียวในตารางนี้ที่ยังมาร์ก experimental ใน docs ที่ bundle มา — ที่เหลือทั้งหมด รวมถึง global-error, instrumentation-client และ metadata file conventions stable แล้วใน Next.js 16.3

component hierarchy ถูกกำหนดตายตัว และมันไม่ใช่ลำดับที่ไฟล์วางอยู่ใน file tree ของ editor — มันคือลำดับที่ React ซ้อนมันรอบ page ของคุณ จากนอกสุดไปในสุด —

flowchart TD
  Layout["layout.tsx (outermost)"] --> Template["template.tsx"]
  Template --> Error["error.tsx (error boundary)"]
  Error --> Loading["loading.tsx (Suspense boundary)"]
  Loading --> NotFound["not-found.tsx"]
  NotFound --> Page["page.tsx (innermost)"]
ลำดับการซ้อน component สำหรับหนึ่ง route segment

สิ่งนี้ระบุไว้ตรง ๆ ทีละไฟล์ใน docs ที่ bundle มา — layout.js “wraps template.js, error.js, loading.js, not-found.js, and page.js”; template.js “renders between layout.js and error.js… wraps error.js, loading.js, not-found.js, and page.js, but does not wrap the layout.js”; error.js “wraps loading.js, not-found.js, page.js, and nested layout.js files… does not wrap the layout.js or template.js above it”; loading.js “wraps not-found.js, page.js, and nested layout.js files in a <Suspense> boundary… does not wrap the layout.js, template.js, or error.js”; และ page.js “is the innermost file convention” ผลที่ตามมาที่ควรจำไว้ — เพราะ error.js อยู่ ข้างใน layout.js ในลำดับนี้ error ที่ throw จาก layout เองจึงไม่มีวันถูกจับโดย error.tsx ของ segment เดียวกัน — มีแค่ global-error.tsx (หรือ error.tsx ของ parent segment) เท่านั้นที่จับได้

route segment หนึ่งไม่ใช่ “ไฟล์เดียว” — มันคือการซ้อน wrapper component ที่ optional สูงสุดหกชั้นรอบ page ของคุณ ตามลำดับที่ framework บังคับไว้ ไม่ใช่ลำดับที่ file tree ของคุณสื่อ การอ่านโฟลเดอร์คือการถามสำหรับแต่ละชื่อที่จองไว้ที่มีอยู่ — มันอยู่ชั้นไหนของการซ้อนนั้น และมันเข้าไม่ถึงอะไรบ้าง — layout ไม่เคยเห็น error ของ template/error ที่เป็น child ของมันเองข้างบน และ loading boundary ไม่เคยครอบ layout ที่มันแขวนอยู่

ได้อะไรประโยชน์ต้นทุน
ชื่อไฟล์ที่จองไว้แทน router configfile tree คือ routing table เลย ไม่มีอะไรอื่นต้อง syncต้องจำว่าชื่อไหนมีความหมาย พิมพ์ผิด (Page.tsx) กลายเป็นไฟล์เฉย ๆ
ลำดับการซ้อนตายตัว (layout → template → error → loading → not-found → page)error/loading boundary ต่อ segment คาดเดาได้ ประกอบกันได้error ของ layout เองมองไม่เห็นจาก error.tsx ของ segment ตัวเอง — สมมติผิดง่าย
global-error.tsx สำหรับ failure ระดับ rootทางเดียวที่จับ error ที่ throw จาก root layout เองได้ต้อง duplicate <html>/<body> เองและใช้ metadata ไม่ได้
forbidden.tsx / unauthorized.tsxUI 403/401 ที่ทำมาเฉพาะ ผูกกับ forbidden()/unauthorized()ยัง experimental — API อาจเปลี่ยนได้อีก
  • “error.tsx จับ error ที่ throw จาก layout.tsx ของตัวเองได้” จับไม่ได้ — ในลำดับการซ้อน error.tsx อยู่ ข้างใน layout.tsx error จาก render ของ layout เองจึงข้ามมันไป มีแค่ global-error.tsx (root) หรือ error.tsx ของ parent segment เท่านั้นที่จับ error ของ layout ได้
  • “ชื่อไฟล์ที่จองไว้ใช้ได้ทุกที่ ทุกความลึก” บางตัวอยู่ที่ root เท่านั้น (instrumentation.ts, instrumentation-client.ts) และบางตัวต้องพึ่ง convention อื่นที่ตั้งไว้แล้ว (mdx-components.tsx มีความหมายก็ต่อเมื่อติดตั้ง @next/mdx)
  • “forbidden.tsx/unauthorized.tsx stable เท่า error.tsx” ทั้งคู่ยังมาร์ก experimental ใน docs ที่ bundle มา ต่างจากที่เหลือในตารางนี้
  • “โฟลเดอร์ที่มีไฟล์อยู่ข้างในกลายเป็น route อัตโนมัติ” มีแค่ page.tsx (หรือ route.ts) เท่านั้นที่ทำให้ segment เข้าถึงได้ — โฟลเดอร์ที่มีแค่ layout.tsx หรือ subfolder จัดระเบียบล้วน ๆ ไม่ตอบสนองต่อ URL เอง

เวลาโค้ดที่ generate มาอ้างว่าไฟล์ “ทำงานเองได้” เพราะชื่อของมัน เช็คสิ่งเหล่านี้ก่อนเชื่อ —

  • คาดหวังให้ error.tsx จับ failure ที่ throw ตรงจาก layout.tsx ใน segment เดียวกัน หรือเปล่า? จับไม่ได้ — ต้องใช้ global-error.tsx หรือ error.tsx ของ parent segment
  • convention ที่เป็น root-only (instrumentation.ts, instrumentation-client.ts) ถูกวางไว้ในโฟลเดอร์ route ที่ nest ลึกแทนที่จะเป็น root โปรเจกต์ (หรือ src/) หรือเปล่า?
  • global-error.tsx ข้าม 'use client' หรือพยายาม export metadata หรือเปล่า? ทั้งสองอย่างจำเป็น/ห้ามตามลำดับ และไฟล์ต้อง render <html>/<body> เอง
  • forbidden.tsx/unauthorized.tsx ถูกนำเสนอเหมือนเป็น API ที่ stable มานาน ทั้งที่ docs ที่ bundle มายังมาร์กทั้งคู่ experimental หรือเปล่า?
  • route.ts วางอยู่ข้าง ๆ page.tsx ใน segment เดียวกัน โดยคาดหวังให้ทั้งคู่ทำงานหรือเปล่า? segment หนึ่งจะ resolve ไปที่อันใดอันหนึ่ง ไม่ใช่ทั้งคู่ สำหรับ path เดียวกัน

🐛 หา bug ในโค้ดนี้

app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const config = getDashboardConfig() // throws if the config file is missing
return (
<section>
<nav>{config.title}</nav>
{children}
</section>
)
}
// app/dashboard/error.tsx
'use client'
export default function DashboardError({ error }: { error: Error }) {
return <p>Dashboard failed to load: {error.message}</p>
}
เฉลย

ดูเหมือนเป็นคู่ที่จับกันพอดี แต่ error.tsx อยู่ ข้างใน layout.tsx ในลำดับการซ้อน — มันครอบ page.tsx (และ layout.tsx ที่ nest ต่อ) ไม่ใช่ครอบ layout.tsx ใน segment ของตัวเอง ถ้า getDashboardConfig() throw ตอน DashboardLayout เองกำลัง render app/dashboard/error.tsx จะไม่เห็น error นี้เลย — มันจะลอยขึ้นไปหา error.tsx ของ parent segment ที่ใกล้ที่สุด หรือไปที่ global-error.tsx ของ root ถ้าไม่มี boundary ของ parent เลย ทางแก้คือย้าย call ที่เสี่ยงไปไว้ใน page.tsx (ซึ่ง error.tsx นี้ครอบอยู่จริง) หรือพึ่งพา error boundary ของ parent segment โดยตั้งใจออกแบบให้เป็นแบบนั้น

ไฟล์ไหนทำให้ route segment เข้าถึงได้แบบ public ที่ URL
อะไรคือสิ่งพิเศษของ error.tsx
อะไรที่อยู่เฉพาะใน root layout เท่านั้น
โฟลเดอร์ที่ไม่มีทั้ง page.tsx และ route.ts คือ:
ในลำดับการซ้อน layout → template → error → loading → not-found → page ข้อไหนถูกต้อง
ไฟล์นี้ผิดตรงไหน
// app/global-error.tsx
export default function GlobalError({ error }: { error: Error }) {
  return <p>Something went wrong: {error.message}</p>
}
Next.js docs ที่ bundle มากับ 16.3 ยังมาร์ก file convention คู่ไหนว่า experimental