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

Metadata และ SEO

คุณอธิบาย <head> ของหน้าเป็น data — object metadata แบบ static หรือฟังก์ชัน generateMetadata แบบ dynamic — แล้ว Next.js render tag ที่ถูกต้องบน server จึงไม่ต้องมาจัดการ <head> ด้วยมืออีก

คุณ export object metadata จาก layout หรือ page ไหนก็ได้ แล้ว Next.js เปลี่ยนค่านั้นเป็น <title>, <meta> และ link tag ขั้นตอนนี้รันบน server ดังนั้น tag จึงอยู่ใน HTML ชุดแรกที่ crawler อ่าน

app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
description: 'Who we are and what we build.',
openGraph: { title: 'About Acme', images: ['/og.png'] },
}
export default function AboutPage() {
return <h1>About</h1>
}

เมื่อ tag ขึ้นกับ data — blog post หรือ product — ให้ export generateMetadata แทน function นี้รับ params ของ route รันบน server และ return รูปแบบ Metadata เดียวกัน การ fetch ตรงนี้ถูก dedupe กับที่อยู่ในหน้าของคุณ คุณจึงไม่จ่ายซ้ำสองครั้ง

app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) => r.json())
return { title: post.title, description: post.excerpt }
}

layout กำหนด title.template ได้เพื่อให้ทุกหน้าลูกได้ suffix ที่สม่ำเสมอ และต้องมี default คู่กับ template เสมอ

// app/layout.tsx — every child title becomes "<page> | Acme"
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Acme',
default: 'Acme',
},
}

metadata บางอย่างเป็นไฟล์ ไม่ใช่ field วางไฟล์ที่ตั้งชื่อแบบพิเศษลงใน route segment แล้ว Next.js ต่อให้เอง opengraph-image.tsx สร้าง social preview, icon.png กลายเป็น favicon ส่วน sitemap.ts และ robots.ts กลายเป็น route ของตัวเอง route พิเศษเหล่านี้ถูก cache โดย default

// app/opengraph-image.tsx — generated, cached social image
import { ImageResponse } from 'next/og'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function Image() {
return new ImageResponse(<div style={{ fontSize: 96 }}>Acme</div>, size)
}
// app/sitemap.ts — becomes /sitemap.xml
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [{ url: 'https://acme.com', lastModified: new Date() }]
}

เพราะทุกอย่างเป็น data หรือเป็นไฟล์ จึงไม่มี <head> ให้คอย sync ด้วยมือ — template merge ทั้ง parent และ child และ tag ที่ซ้ำกันก็ไม่มีทางเพี้ยนออกจากกัน

flowchart TD
  A["metadata object"] --> M[Merge parent + child]
  B["generateMetadata(params)"] --> M
  C["opengraph-image / sitemap / robots files"] --> M
  M --> Head["Server-rendered head tags"]
How the Metadata API builds the head
ควรใช้ generateMetadata แทน object metadata แบบ static เมื่อไหร่
title.template ต้องมีอะไรคู่กัน
เพิ่ม sitemap ใน App Router ยังไง
ทำไม Metadata API ดีกว่าการจัดการ head ด้วยมือ