Routing & Pages
folder คือ route
หัวข้อที่มีชื่อว่า “folder คือ route”ใน SvelteKit โครงสร้าง folder ใต้ src/routes/ คือ โครงสร้าง URL ไฟล์ +page.svelte ทำให้ folder ของตัวเองกลายเป็นเพจที่เข้าถึงได้
src/routes/├── +page.svelte → /├── about/│ └── +page.svelte → /about├── blog/│ ├── +page.svelte → /blog│ └── [slug]/│ └── +page.svelte → /blog/:slug (dynamic)└── +layout.svelte → wraps every pageflowchart LR f1["routes/+page.svelte"] --> u1["/"] f2["routes/about/+page.svelte"] --> u2["/about"] f3["routes/blog/[slug]/+page.svelte"] --> u3["/blog/:slug"]
dynamic parameter
หัวข้อที่มีชื่อว่า “dynamic parameter”วงเล็บเหลี่ยมในชื่อ folder จะจับ segment ของ URL ไว้ src/routes/blog/[slug]/+page.svelte จะ match /blog/hello และค่านั้นจะอยู่ที่ params.slug (ใน load function — บทเรียนถัดไป) รูปแบบต่าง ๆ:
[slug]— parameter ที่บังคับต้องมี[...rest]— rest parameter ที่ match ได้หลาย segment (/docs/a/b/c)[[optional]]— optional parameter ที่ match ได้แม้ไม่มีค่า
param matcher: validate segment ก่อน match
หัวข้อที่มีชื่อว่า “param matcher: validate segment ก่อน match”[id] ธรรมดา match segment อะไรก็ได้ matcher — function ใน src/params/ — ทำให้ route match เฉพาะเมื่อ segment ผ่านเงื่อนไข ทำให้ static route ที่ไม่เกี่ยวข้องข้างล่างชนะแทนได้:
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => /^\d+$/.test(param);// src/routes/products/[id=integer]/+page.server.jsexport function load({ params }) { return { id: params.id }; // only reached when params.id is all digits}พิสูจน์ทั้งการ match และการ fallthrough กับ route จริงที่รันอยู่ (/products/[id=integer] กับ static /products/latest ธรรมดา):
$ curl -s http://localhost:5196/products/42 | grep -o '<h1[^<]*</h1>'<h1 data-testid="product">product 42</h1>
$ curl -s http://localhost:5196/products/latest | grep -o '<h1[^<]*</h1>'<h1 data-testid="latest">latest products (non-numeric, matcher rejects it so this static route wins)</h1>/products/42 ผ่าน regex ของ integer เลยไปโดน dynamic route; /products/latest ไม่ผ่าน SvelteKit จึง fallthrough ไปที่ products/latest/+page.svelte ธรรมดาแทนที่จะเป็น 404 — matcher ช่วยแยกความกำกวมของสอง route ที่ปกติจะชนกันที่ segment เดียวกันได้
route group: จัดระเบียบโดยไม่กระทบ URL
หัวข้อที่มีชื่อว่า “route group: จัดระเบียบโดยไม่กระทบ URL”การครอบชื่อ folder ด้วยวงเล็บ — (app) — สร้าง route group: จัดระเบียบ route (และมี +layout.svelte ร่วมได้) โดยไม่เพิ่ม segment ให้ URL
src/routes/└── (app)/ └── grouped/ └── +page.svelte → /grouped (NOT /app/grouped or /(app)/grouped)พิสูจน์กับ route จริง:
$ curl -s http://localhost:5196/grouped | grep -o '<h1[^<]*</h1>'<h1 data-testid="grouped">grouped page, no (app) segment in the URL</h1>URL คือ /grouped ไม่ใช่ /app/grouped — group folder เป็นแค่การจัดระเบียบล้วน ๆ (มักใช้ให้ทั้งเซกชัน เช่นทุก route ที่ต้อง login มี layout ร่วมกันโดยไม่โผล่ใน URL เลย)
layout ครอบ page
หัวข้อที่มีชื่อว่า “layout ครอบ page”+layout.svelte มีผลกับ folder ของตัวเองและทุก route ที่อยู่ข้างใต้ — เป็นที่วางองค์ประกอบที่ใช้ร่วมกันอย่าง header และ footer โดย layout render child route ผ่าน children snippet:
<script> let { children } = $props(); // the active child page</script>
<nav>…site navigation…</nav>
<main> {@render children()} <!-- the current page renders here --></main>
<footer>© 2026</footer>layout ซ้อนกันได้ (nest): layout ที่อยู่ลึกกว่าในต้นไม้จะถูกครอบด้วยตัวที่อยู่เหนือขึ้นไป คุณจึงประกอบ global shell กับ shell เฉพาะแต่ละส่วนได้
reset ลำดับ layout ด้วย @
หัวข้อที่มีชื่อว่า “reset ลำดับ layout ด้วย @”บางครั้งเพจเดียวต้องการ ข้าม layout ทั้งหมดข้างบนไปเลย — เพจ landing แบบเต็มจอ, print view ตั้งชื่อไฟล์ [email protected] (หรือ [email protected]) จะ reset กลับไปที่ root ข้ามทุก layout ระหว่างทาง; +page@(group).svelte จะ reset ไปที่ ancestor ที่ตั้งชื่อไว้แทนที่จะเป็น root ตรง ๆ regex ของไฟล์ route ใน SvelteKit เองยืนยัน syntax นี้ว่าเป็น page(@(.*))? / layout(@(.*))? — @ ตามด้วยชื่อ group ที่จะใส่หรือไม่ก็ได้
<!-- src/routes/(app)/wide/+layout.svelte — the (app) section's shared shell --><script lang="ts"> let { children } = $props();</script><div> <nav>app nav</nav> {@render children()}</div><!-- src/routes/(app)/wide/[email protected] — @ resets to root, skipping (app)'s own layout --><h1>wide page — reset with @, skips (app)/+layout.svelte and the root layout's own markup</h1>พิสูจน์ด้วยการ curl HTML ที่ render จริง — <nav>app nav</nav> ของ layout (app) ไม่มีอยู่ใน response เลย มีแค่ <h1> ของเพจเอง:
$ curl -s http://localhost:5197/wide | grep -o '<h1[^<]*</h1>\|<nav>[^<]*</nav>'<h1 data-testid="wide">wide page — reset with @, skips (app)/+layout.svelte and the root layout's own markup</h1>error page และลำดับชั้น boundary
หัวข้อที่มีชื่อว่า “error page และลำดับชั้น boundary”+error.svelte ใน folder หนึ่งจะ render เมื่อ load (หรือตัวเพจ/layout เอง) throw ใน subtree นั้น — เป็น error boundary แบบ scoped ที่อ่าน error ผ่าน page.error (จาก $app/state) SvelteKit เดินขึ้นบนต้นไม้ route หา +error.svelte ที่ใกล้ที่สุด เริ่มจาก route ที่ throw
พิสูจน์ด้วยสอง route: errors/deep throw ใต้ folder ที่มี errors/+error.svelte ของตัวเอง และ errors2/deep2 throw ใต้ folder ที่ไม่มี — มันเลยเด้งขึ้นไปถึง root src/routes/+error.svelte:
import { error } from '@sveltejs/kit';export function load() { error(500, 'boom from errors/deep');}// src/routes/errors2/deep2/+page.server.js — no scoped +error.svelte anywhere in errors2/import { error } from '@sveltejs/kit';export function load() { error(500, 'boom from errors2/deep2');}$ curl -s http://localhost:5196/errors/deep | grep -o '<h1[^<]*</h1>'<h1 data-testid="err">SCOPED-ERROR errors/+error.svelte: boom from errors/deep</h1>
$ curl -s http://localhost:5196/errors2/deep2 | grep -o '<h1[^<]*</h1>'<h1 data-testid="err">ROOT-ERROR +error.svelte: boom from errors2/deep2</h1>error แบบเดียวกัน (error(500, ...)) throw ที่ความลึกเท่ากัน — แต่ +error.svelte ตัวไหน render ขึ้นกับว่า folder ancestor มี +error.svelte จริง ๆ หรือเปล่าเท่านั้น
- navigation ใช้ลิงก์
<a href="/blog">ปกติ SvelteKit จะ intercept ไว้เพื่อทำ client-side navigation ที่เร็ว (ไม่ reload ทั้งหน้า) ส่วนการ navigate แบบ programmatic ให้เรียกgoto('/blog')จาก$app/navigation
<script> import { goto } from '$app/navigation';</script>
<a href="/about">About</a> <!-- client-side nav --><button onclick={() => goto('/dashboard')}>Go</button>อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด routing ที่ generate มา เช็คสิ่งเหล่านี้:
- สมมติว่า
+error.svelteที่ “อยู่ใกล้ ๆ กันในโครงสร้าง folder” จะจับ error ได้ โดยไม่เช็คว่า folder นั้นเป็น ancestor จริงของ route ที่ throw หรือเปล่า? พิสูจน์แล้วข้างบน:+error.svelteของ sibling subtree ไม่เคยจับอะไรได้เลย — มีแค่ ancestor เท่านั้น ถ้าไม่มีเลยมันจะเด้งไปที่ root [id]route บัง static route ที่เจาะจงกว่าแบบเงียบ ๆ (หรือกลับกัน) เพราะไม่มี param matcher มาแยกความกำกวมหรือเปล่า?(group)โผล่เป็น segment จริงใน<a href>หรือgoto()ที่ generate มา เหมือนวงเล็บเป็นส่วนหนึ่งของ URL หรือเปล่า? มันไม่เคยเป็นแบบนั้น — พิสูจน์แล้วข้างบนด้วย/grouped- ใช้
[email protected]โดยไม่รู้ว่ามันข้าม layout ทั้งหมด กลับไปถึงจุด reset — รวมถึงอันที่เพจยังต้องการอยู่ (เช่น<svelte:boundary>รายงาน error ร่วม, analytics beacon ที่อยู่ใน layout ที่ถูกข้ามไป) หรือเปล่า? - navigation ใช้
window.location.href = '/path'(reload เต็มหน้า) ทั้งที่ตั้งใจจะใช้goto('/path')(client-side พิสูจน์แล้วข้างบน) หรือเปล่า?
🐛 หา bug ในโค้ดนี้
// project layout:// errors/// +error.svelte <- exists here// deep/+page.server.js// errors2/// deep2/+page.server.js <- NO +error.svelte anywhere in errors2/
// src/routes/errors2/deep2/+page.server.jsimport { error } from '@sveltejs/kit';export function load() {error(500, 'something failed in errors2/deep2');// "the errors/+error.svelte will catch this too, it's right there in routes/"}เฉลย
errors/+error.svelte ครอบคลุมเฉพาะ folder ของตัวเองกับ folder ที่ซ้อนอยู่ข้างใต้เท่านั้น — errors2/ เป็น sibling ไม่ใช่ descendant boundary นั้นจึงไม่เห็น error นี้เลย พิสูจน์แล้วข้างบนด้วย curl จริง: การยิงไปที่ /errors2/deep2 render root src/routes/+error.svelte ไม่ใช่ errors/+error.svelte เพราะ SvelteKit เดินขึ้นจาก route ที่ throw หา +error.svelte ที่เป็น ancestor ที่ใกล้ที่สุด — และ errors2/deep2 ไม่มี ancestor ที่มี +error.svelte เลยจนถึง root ทางแก้คือเพิ่ม errors2/+error.svelte เพื่อจัดการแบบ scoped หรือยอมรับ (และออกแบบให้รองรับ) ว่า root error page จะจับมันแทน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”routing คือต้นไม้ และมีสามสิ่งเกี่ยวกับ request ที่แต่ละอันถูก resolve ด้วยการเดินต้นไม้เดียวกันนี้ แต่หยุดคนละจุด: page คือ leaf ที่ match พอดี; layout คือทุก ancestor จาก root ลงมาถึง leaf นั้น แต่ละอันซ้อนอยู่ข้างในตัวที่อยู่เหนือขึ้นไป (เว้นแต่ @ จะ reset การเดินก่อนถึงจุดนั้น); error boundary คือ ancestor ที่ใกล้ที่สุด (เดินจาก leaf ย้อนขึ้นไป) ที่บังเอิญมี +error.svelte group ((name)) กับ matcher ([x=name]) ไม่เปลี่ยนรูปร่างต้นไม้นี้ต่อ URL หรือการ render — มันแค่เปลี่ยนวิธีจัดระเบียบ folder (group) หรือว่า segment นั้นมีสิทธิ์ match หรือเปล่า (matcher)
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
param matcher ([id=integer]) | สอง route แชร์ตำแหน่ง segment เดียวกันได้โดยไม่ชนกัน (พิสูจน์แล้วข้างบน) | ต้องดูแลไฟล์เพิ่ม (src/params/x.ts) ต่อเงื่อนไขหนึ่งอัน |
route group (name) | แชร์ layout/จัดระเบียบโดยไม่เสีย URL เลย (พิสูจน์แล้วข้างบน) | ลืมง่ายว่าวงเล็บไม่โผล่ใน link — href="/(app)/grouped" ที่ generate มาผิดตรง ๆ |
reset ด้วย [email protected] | คุมได้เต็มที่ว่าจะข้าม chrome ของ ancestor ที่ไม่ต้องการ (พิสูจน์แล้วข้างบน) | ข้ามทุกอย่างกลับไปถึงจุด reset รวมถึงของมีประโยชน์ที่อยู่ใน layout เหล่านั้นด้วย |
+error.svelte แบบ scoped ต่อ folder | จำกัดวงระเบิดของความล้มเหลวให้อยู่แค่เซกชันนั้น | ทุก folder ที่ไม่มีจะพึ่ง ancestor บางตัวแบบเงียบ ๆ — ควรรู้ว่าตัวไหน พิสูจน์แล้วข้างบนว่าไม่ใช่เสมอตัวที่ “ดูใกล้ที่สุด” |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
+error.svelteตัวไหนก็ตามในไฟล์ route เดียวกันที่อยู่ใกล้ ๆ จะจับ error ได้” พิสูจน์แล้วข้างบน: มีแค่+error.svelteของ folder ที่เป็น ancestor เท่านั้นที่จับได้ — ของ sibling subtree ไม่เคยจับได้เลย ไม่ว่าจะดูใกล้แค่ไหนใน file tree - “route group
(name)เพิ่ม segment/nameให้ URL” พิสูจน์แล้วข้างบน: ได้/groupedไม่ใช่/app/grouped— group เป็นแค่การจัดระเบียบล้วน ๆ - “
[id]ธรรมดากับ static route ที่ตำแหน่งเดียวกันอยู่ด้วยกันไม่ได้” param matcher ทำให้อยู่ด้วยกันได้: พิสูจน์แล้วข้างบน/products/latestfallthrough ไปที่ static route เพราะ matcher ของintegerปฏิเสธมัน - “
[email protected]ข้ามแค่ parent layout ที่ใกล้ที่สุด” พิสูจน์แล้วข้างบน:@เปล่า ๆ reset กลับไปถึง root เลย ข้ามทุก layout ระหว่างทาง ไม่ใช่แค่ระดับเดียว