Next.js 16 from Zero to Senior · Part 8 — Rendering, Metadata, SEO & Assets
Control how pages render (static, dynamic, PPR), generate metadata and OG images, ship sitemaps and robots, and use the built-in optimizations: next/image, next/font, and next/script — for fast, discoverable pages.
A page can be perfect and still fail if Google can’t read it, the social preview is blank, or the hero image janks the layout {Một trang có thể hoàn hảo mà vẫn thất bại nếu Google không đọc được, preview mạng xã hội trống, hay ảnh hero làm giật layout}. This part covers the “make it fast and findable” layer of Next.js: rendering strategies, metadata/SEO, and the built-in asset optimizations {Phần này nói về tầng “nhanh và dễ tìm” của Next.js: chiến lược render, metadata/SEO, và các tối ưu asset tích hợp}.
1. The three rendering strategies {Ba chiến lược render}
Every route resolves to one of three behaviors {Mỗi route quy về một trong ba hành vi}:
- Static — prerendered at build time, served as cached HTML. Fastest, cheapest. Use for content that’s the same for everyone {Tĩnh — prerender lúc build, phục vụ dưới dạng HTML đã cache. Nhanh nhất, rẻ nhất. Dùng cho nội dung giống nhau với mọi người}.
- Dynamic — rendered per request. Use when output depends on the request (cookies, headers, fresh data) {Động — render theo từng request. Dùng khi output phụ thuộc request (cookie, header, dữ liệu mới)}.
- Partial Prerendering (PPR) — a static shell with dynamic holes streamed in (Part 4). The Next.js 16 default sweet spot {Partial Prerendering (PPR) — vỏ tĩnh với lỗ động được stream vào (Phần 4). Điểm ngọt mặc định của Next.js 16}.
What makes a route dynamic {Cái gì làm một route động}: reading cookies(), headers(), searchParams, or uncached data {đọc cookies(), headers(), searchParams, hoặc dữ liệu không cache}. What keeps it static/cached: 'use cache' and generateStaticParams {Cái gì giữ nó tĩnh/cache: 'use cache' và generateStaticParams}.
┌─────────────── PPR page ───────────────┐
instant → │ static shell ('use cache') │
│ └─ <Suspense> dynamic hole (stream) │ ← personalized
└────────────────────────────────────────┘
You rarely set this manually now — you compose it with 'use cache' and <Suspense>, and Next.js figures out the rest {Bạn hiếm khi set thủ công nữa — bạn kết hợp bằng 'use cache' và <Suspense>, và Next.js lo phần còn lại}.
2. Static metadata {Metadata tĩnh}
Export a metadata object from any layout.tsx or page.tsx {Export một object metadata từ bất kỳ layout.tsx hay page.tsx}:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Pricing — Senior Next',
description: 'Simple, transparent pricing.',
openGraph: {
title: 'Pricing — Senior Next',
description: 'Simple, transparent pricing.',
images: ['/og/pricing.png'],
},
twitter: { card: 'summary_large_image' },
};
Metadata merges down the layout tree, so the root layout sets defaults and pages override them {Metadata gộp xuống theo cây layout, nên root layout đặt mặc định và page ghi đè}.
Title templates {Mẫu tiêu đề}
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: 'Senior Next',
template: '%s · Senior Next', // child pages fill %s
},
};
A page exporting title: 'Pricing' becomes Pricing · Senior Next {Một page export title: 'Pricing' trở thành Pricing · Senior Next}.
3. Dynamic metadata with generateMetadata {Metadata động với generateMetadata}
For data-driven pages (a blog post, a product), generate metadata async {Cho trang dựa trên dữ liệu (bài blog, sản phẩm), tạo metadata bất đồng bộ}:
// 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 getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}
The data fetch here is deduped with the page’s own fetch (Part 3), so it doesn’t cost an extra request {Lời fetch ở đây được dedup với fetch của chính page (Phần 3), nên không tốn thêm request}.
4. File-based metadata: OG images, sitemap, robots {Metadata theo file: ảnh OG, sitemap, robots}
Next.js generates metadata from specially named files {Next.js tạo metadata từ các file đặt tên đặc biệt}.
Dynamic OG image {Ảnh OG động}
Generate social preview images at the edge with ImageResponse {Tạo ảnh preview mạng xã hội ở edge bằng ImageResponse}:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OG({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return new ImageResponse(
(
<div style={{ fontSize: 64, background: '#0a0a0a', color: '#c8ff00', width: '100%', height: '100%', display: 'flex', alignItems: 'center', padding: 80 }}>
{post.title}
</div>
),
size
);
}
Sitemap & robots {Sitemap & robots}
// app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
return [
{ url: 'https://example.com', lastModified: new Date() },
...posts.map((p) => ({ url: `https://example.com/blog/${p.slug}`, lastModified: p.updatedAt })),
];
}
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/dashboard/' },
sitemap: 'https://example.com/sitemap.xml',
};
}
Also useful {Cũng hữu ích}: app/manifest.ts (PWA manifest), app/icon.png / app/apple-icon.png (favicons) {app/manifest.ts (manifest PWA), app/icon.png / app/apple-icon.png (favicon)}.
5. next/image — no more layout shift {next/image — hết giật layout}
next/image automatically resizes, serves modern formats (AVIF/WebP), lazy-loads, and reserves space to prevent CLS {next/image tự động resize, phục vụ định dạng hiện đại (AVIF/WebP), lazy-load, và giữ chỗ để tránh CLS}:
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Product hero"
width={1200}
height={600}
priority // preload above-the-fold images; skips lazy-loading
/>
);
}
Rules {Quy tắc}:
- Always provide
width/height(or usefillwith a sized parent) — this reserves space {Luôn cung cấpwidth/height(hoặc dùngfillvới cha có kích thước) — để giữ chỗ}. - Use
priorityfor the LCP image only; never for everything {Dùngprioritychỉ cho ảnh LCP; đừng cho mọi ảnh}. - For remote images, whitelist the host in
next.config.ts{Cho ảnh từ xa, whitelist host trongnext.config.ts}:
// next.config.ts
const nextConfig: NextConfig = {
images: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] },
};
6. next/font — zero-layout-shift fonts {next/font — font không giật layout}
next/font self-hosts fonts at build time, removing the network request to Google and the FOUT/CLS that comes with it {next/font tự host font lúc build, loại bỏ request mạng tới Google và FOUT/CLS đi kèm}:
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
Local fonts use next/font/local with the same benefits {Font local dùng next/font/local với cùng lợi ích}.
7. next/script — load third-party scripts safely {next/script — nạp script bên thứ ba an toàn}
Control when a third-party script loads so it doesn’t block your page {Kiểm soát khi nào một script bên thứ ba nạp để nó không chặn trang}:
import Script from 'next/script';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<Script src="https://example.com/analytics.js" strategy="afterInteractive" />
</>
);
}
Strategies {Chiến lược}: beforeInteractive (critical, rare), afterInteractive (default, analytics), lazyOnload (low priority, chat widgets) {beforeInteractive (quan trọng, hiếm), afterInteractive (mặc định, analytics), lazyOnload (ưu tiên thấp, widget chat)}.
8. An SEO checklist {Checklist SEO}
- Every page has a unique
title+description{Mỗi trang cótitle+descriptionriêng}. - OG + Twitter tags so links preview nicely {Tag OG + Twitter để link preview đẹp}.
sitemap.ts+robots.tspresent {Cósitemap.ts+robots.ts}.- Canonical URLs set via
alternates.canonicalfor duplicate-prone pages {URL canonical đặt quaalternates.canonicalcho trang dễ trùng lặp}. - Semantic HTML and real
<a href>/<Link>(crawlers follow links, notonClick) {HTML ngữ nghĩa và<a href>/<Link>thật (crawler theo link, không phảionClick)}. - Keep important content server-rendered, not client-fetched after load {Giữ nội dung quan trọng render-server, không fetch client sau khi tải}.
- Use
generateStaticParamsso key pages are prerendered {DùnggenerateStaticParamsđể trang quan trọng được prerender}.
9. Exercises {Bài tập}
-
Title template {Mẫu tiêu đề}: set a
title.templatein the root layout and a per-pagetitle; confirm the composed<title>in the rendered HTML {đặttitle.templateở root layout vàtitlemỗi page; xác nhận<title>được ghép trong HTML}. -
Dynamic metadata {Metadata động}: add
generateMetadatato a[slug]route that pulls title/description from the post {thêmgenerateMetadatavào route[slug]lấy title/description từ bài}. -
OG image {Ảnh OG}: add
opengraph-image.tsxto your blog route and preview it by visiting/blog/<slug>/opengraph-image{thêmopengraph-image.tsxvào route blog và xem bằng cách vào/blog/<slug>/opengraph-image}. -
Sitemap & robots {Sitemap & robots}: generate a
sitemap.tsfrom your posts and arobots.tsthat disallows/dashboard{tạositemap.tstừ các bài vàrobots.tschặn/dashboard}. -
Image audit {Kiểm tra ảnh}: replace a raw
<img>withnext/image, addpriorityto the LCP image, and confirm CLS drops in Lighthouse {thay<img>thô bằngnext/image, thêmprioritycho ảnh LCP, và xác nhận CLS giảm trong Lighthouse}. -
Font {Font}: adopt
next/font/google, then check the Network tab shows the font self-hosted (no request to fonts.googleapis.com) {áp dụngnext/font/google, rồi kiểm tra tab Network thấy font tự host (không request tới fonts.googleapis.com)}.
What’s next {Phần tiếp theo}
Your pages now render with the right strategy, preview beautifully when shared, rank well, and load fast with optimized images and fonts {Trang của bạn giờ render đúng chiến lược, preview đẹp khi chia sẻ, xếp hạng tốt, và tải nhanh với ảnh và font tối ưu}.
Part 9 tackles authentication and security: sessions and cookies, a Data Access Layer, protecting routes and Server Actions, using proxy.ts for redirects, and the security headers and practices every Next.js app needs {Phần 9 xử lý xác thực và bảo mật: session và cookie, Data Access Layer, bảo vệ route và Server Action, dùng proxy.ts để chuyển hướng, và các header cùng thực hành bảo mật mọi app Next.js cần}.