jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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'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'<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 use fill with a sized parent) — this reserves space {Luôn cung cấp width/height (hoặc dùng fill với cha có kích thước) — để giữ chỗ}.
  • Use priority for the LCP image only; never for everything {Dùng priority chỉ cho ảnh LCP; đừng cho mọi ảnh}.
  • For remote images, whitelist the host in next.config.ts {Cho ảnh từ xa, whitelist host trong next.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 + description riêng}.
  • OG + Twitter tags so links preview nicely {Tag OG + Twitter để link preview đẹp}.
  • sitemap.ts + robots.ts present {Có sitemap.ts + robots.ts}.
  • Canonical URLs set via alternates.canonical for duplicate-prone pages {URL canonical đặt qua alternates.canonical cho trang dễ trùng lặp}.
  • Semantic HTML and real <a href>/<Link> (crawlers follow links, not onClick) {HTML ngữ nghĩa và <a href>/<Link> thật (crawler theo link, không phải onClick)}.
  • 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 generateStaticParams so key pages are prerendered {Dùng generateStaticParams để trang quan trọng được prerender}.

9. Exercises {Bài tập}

  1. Title template {Mẫu tiêu đề}: set a title.template in the root layout and a per-page title; confirm the composed <title> in the rendered HTML {đặt title.template ở root layout và title mỗi page; xác nhận <title> được ghép trong HTML}.

  2. Dynamic metadata {Metadata động}: add generateMetadata to a [slug] route that pulls title/description from the post {thêm generateMetadata vào route [slug] lấy title/description từ bài}.

  3. OG image {Ảnh OG}: add opengraph-image.tsx to your blog route and preview it by visiting /blog/<slug>/opengraph-image {thêm opengraph-image.tsx vào route blog và xem bằng cách vào /blog/<slug>/opengraph-image}.

  4. Sitemap & robots {Sitemap & robots}: generate a sitemap.ts from your posts and a robots.ts that disallows /dashboard {tạo sitemap.ts từ các bài và robots.ts chặn /dashboard}.

  5. Image audit {Kiểm tra ảnh}: replace a raw <img> with next/image, add priority to the LCP image, and confirm CLS drops in Lighthouse {thay <img> thô bằng next/image, thêm priority cho ảnh LCP, và xác nhận CLS giảm trong Lighthouse}.

  6. Font {Font}: adopt next/font/google, then check the Network tab shows the font self-hosted (no request to fonts.googleapis.com) {áp dụng next/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}.