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

App Router & layout

apps/web/app/layout.tsxroot layout ที่ทุก route ในคอร์สนี้เรนเดอร์อยู่ข้างใน: <html>/<body>, header พร้อม nav ที่ลิงก์ไปที่ Home และ Admin, <main>{children}</main>, และ footer — พร้อม apps/web/app/globals.css เบื้องต้น ทั้งสองไฟล์มีอยู่แล้วในฐานะ default ของ scaffold จาก Frontend init บทเรียนนี้แทนที่เนื้อหา placeholder ในสองไฟล์นั้นด้วย shell จริงของ DevBlog ไม่มีอะไรต้องติดตั้งใหม่ตรงนี้ มีแค่สองไฟล์ที่ทุกหน้าใน app/ ต่อจากนี้จะเรนเดอร์อยู่ข้างใน

layout.tsx คือ Server Component ที่เป็นค่าเริ่มต้นของ App Router สำหรับทุกไฟล์ใน app/ เว้นแต่ไฟล์นั้นจะขึ้นต้นด้วย directive 'use client' ไม่ต้องเพิ่ม import และไม่ต้องตั้ง config flag ใด ๆ เพื่อให้ได้ค่าเริ่มต้นนี้ ไฟล์ .tsx ใต้ app/ เป็นแบบนี้อยู่แล้วจนกว่าคุณจะสั่งเป็นอย่างอื่น

เรื่องนี้สำคัญแบบเป็นรูปธรรมตรงนี้: header, nav link และ footer ในไฟล์นี้ไม่เคยส่งโค้ดคอมโพเนนต์ไปที่ browser เลย เรนเดอร์ครั้งเดียวออกมาเป็น HTML ล้วน ๆ ไม่ว่าจะบนเซิร์ฟเวอร์ต่อ request หรือ — เพราะไม่มีอะไรในไฟล์นี้เรียก dynamic API — อาจจะครั้งเดียวตอน build time ถ้า Next ตัดสินว่า route นี้เป็น static เต็มรูปแบบ ไม่ว่าทางไหน browser ก็ได้ markup ที่เสร็จแล้ว ไม่ใช่ bundle ของโค้ด React ที่ต้องประกอบใหม่ฝั่ง client

Architecture ขีดเส้นไว้แล้วซึ่ง file convention ของบทเรียนนี้ทำให้เป็นรูปธรรม: หน้าบล็อกสาธารณะที่จะเรนเดอร์อยู่ใต้ layout นี้เป็น Server Component ที่ดึงข้อมูลจาก GraphQL API บนฝั่งเซิร์ฟเวอร์ พร้อม ISR แคชผลลัพธ์ไว้ (Module 9) หน้า /admin ที่จะเรนเดอร์อยู่ใต้ layout เดียวกันนี้จะกลายเป็น Client Component ทันทีที่ต้องการสิ่งที่มีแค่ browser ให้ได้ — JWT ที่เก็บใน localStorage, event handler onClick, useState สำหรับฟอร์ม (Module 10) เส้นแบ่งระหว่างสองแบบนี้ไม่ใช่โฟลเดอร์แยกหรือนามสกุลไฟล์ที่ต่างกัน แต่คือการมีหรือไม่มี 'use client' เป็นบรรทัดแรกของไฟล์ล้วน ๆ layout นี้ยังเป็น server เพราะไม่มีอะไรข้างในต้องการ browser ส่วนลิงก์ Admin ด้านล่างชี้ไปที่ route ที่ยังไม่ทำอะไรจริงจัง Module 10 คือจุดที่ route นั้นจะกลายเป็นหน้า login ที่ใช้งานได้จริง — แต่ nav ต้องมีอยู่ตั้งแต่ตอนนี้ เพราะทุกหน้าในภายหลังของคอร์สนี้เรนเดอร์อยู่ใน header และ footer เดียวกันนี้

Server Components (ค่าเริ่มต้นของ App Router) เทียบกับ Client Components ('use client') Server Component ประกาศเป็น async แล้ว await การดึงข้อมูลได้ตรง ๆ ใน body ไม่เคยส่ง JavaScript ของตัวเองไปที่ browser และถือความลับ (API key, database connection string) ได้อย่างปลอดภัยแบบที่ Client Component ทำไม่ได้ เพราะโค้ดไม่เคยออกจากเซิร์ฟเวอร์ ต้นทุนก็มีจริง: ไม่มี useState, ไม่มี useEffect, ไม่มี event handler, ไม่มี browser API (window, localStorage) อะไรที่ต้อง interactive เป็นข้อห้ามหมด ส่วน Client Component ได้ทุกอย่างเหล่านั้นคืนมา ทั้ง state, effect, onClick, localStorage แลกกับการส่งโค้ดที่ compile แล้วไปที่ browser เป็นส่วนหนึ่งของ JS bundle และ await การดึงข้อมูลตรง ๆ ใน render body ไม่ได้เหมือน Server Component สำหรับ DevBlog เส้นแบ่งตกอยู่ตรงจุดที่บทเรียน architecture บอกไว้พอดี: บล็อกสาธารณะเป็น read-only และได้ประโยชน์จาก client JS ที่เป็นศูนย์และ HTML ที่แคชได้ จึงคงเป็น server ส่วน admin dashboard นั้น stateful และ interactive โดยธรรมชาติ — session, ฟอร์ม, การ redirect ฝั่ง client หลัง login — จึงเลือกใช้ 'use client' อย่างจงใจ ทีละไฟล์ เริ่มที่ Module 10

อัปเดต apps/web/app/globals.css ด้วยจุดเริ่มต้นเล็ก ๆ ก่อน แล้ว Styling จะขยายต่อเป็น design system จริง:

* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
color: #1a1a1a;
background: #ffffff;
}
a {
color: inherit;
}

สร้าง apps/web/app/layout.tsx:

import type { Metadata } from 'next';
import Link from 'next/link';
import './globals.css';
export const metadata: Metadata = {
title: 'DevBlog',
description: 'A small, real GraphQL-backed blog built with Next.js and NestJS.',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>
<Link href="/">DevBlog</Link>
<Link href="/">Home</Link>
<Link href="/admin">Admin</Link>
</nav>
</header>
<main>{children}</main>
<footer>
<p>&copy; {new Date().getFullYear()} DevBlog.</p>
</footer>
</body>
</html>
);
}
  • export const metadata คือ file-convention ของ App Router ที่แทนที่ <head> แบบเขียนมือ — Next อ่าน object นี้แล้วแทรกแท็ก <title>/<meta name="description"> ให้เอง layout.tsx หรือ page.tsx ระดับหน้าสามารถ export metadata ของตัวเองเพื่อ override ต่อ route ได้ ตอนนี้ยังไม่มีไฟล์ไหนทำแบบนั้น ทุก route จึงแสดง “DevBlog” เหมือนกันหมด
  • ไม่มี 'use client' ที่ไหนในไฟล์นี้เลย ไฟล์นี้จึงเป็น Server Component โดยค่าเริ่มต้น นั่นคือเหตุผลพอดีที่เรนเดอร์ <main>{children}</main> ได้อย่างปลอดภัย แล้วปล่อยให้ children จะเป็น Server หรือ Client Component ก็ได้ (หน้าโพสต์สาธารณะ, admin dashboard) mount อยู่ข้างใต้ โดยไฟล์นี้ไม่ต้องรู้เลยว่าเป็นแบบไหน
  • new Date().getFullYear() รันแค่ครั้งเดียว ไม่ว่าไฟล์นี้จะเรนเดอร์ที่ไหน เพราะไม่มีอะไรอื่นใน layout นี้เรียก dynamic API (การ fetch, การอ่าน cookie) Next อาจเลือก prerender ครั้งเดียวตอน build time ปีใน footer จึงอัปเดตตอน deploy ครั้งถัดไป ไม่ใช่อัตโนมัติตอนเที่ยงคืนวันส่งท้ายปีเก่า เป็นการแลกที่โอเคสำหรับบรรทัด copyright แต่ไม่โอเคสำหรับอะไรที่ต้อง live จริง ๆ
Terminal window
cd apps/web
npm run dev

เปิด http://localhost:3000 คุณควรเห็น header พร้อมลิงก์ “DevBlog”, “Home”, และ “Admin” สิ่งที่ app/page.tsx ปัจจุบันเรนเดอร์ใน <main> และ footer พร้อมปีปัจจุบัน

ยืนยันว่า header และ footer มาในรูป HTML จริง ไม่ใช่ React ที่เรนเดอร์ฝั่ง client โดยขอหน้านั้นโดยไม่ใช้ browser เลย:

Terminal window
curl -s http://localhost:3000 | grep -o '<footer>.*</footer>'
<footer><p>© 2026 DevBlog.</p></footer>

curl ไม่เคยรัน JavaScript เลย การเห็น markup ของ footer ใน response ดิบ ๆ ยืนยันว่า layout.tsx เรนเดอร์บนเซิร์ฟเวอร์ — ตรงตามที่ Server Component มีไว้ทำ

apps/web/app/layout.tsx คือ root layout: <html>/<body>, header nav ลิงก์ไปที่ Home และ Admin, และ footer ทั้งหมดในฐานะ Server Component โดยไม่มี 'use client' directive ที่ไหนในไฟล์เลย ค่าเริ่มต้นนั้น — ทุกไฟล์ใน app/ เป็น Server Component เว้นแต่จะเลือกออกเอง — คือสิ่งที่ทำให้บล็อกสาธารณะของคอร์สนี้ยัง server-rendered และแคชได้ ส่วน admin dashboard ค่อยเลือกใช้ Client Component เฉพาะจุดที่ต้องการ browser state จริง ๆ ตั้งแต่ Module 10 เป็นต้นไป ตอนนี้ globals.css มีแค่ reset กับ system font stack แล้ว Styling คือจุดที่ไฟล์นี้จะโตเป็น design system จริงของ DevBlog

ถัดไป: GraphQL client & auth →