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.

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. 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. Ba chiến lược render

Mỗi route quy về một trong ba hành vi:

  • 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.
  • Động — render theo từng request. Dùng khi output phụ thuộc request (cookie, header, dữ liệu mới).
  • 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.

Cái gì làm một route động: đọc cookies(), headers(), searchParams, hoặc dữ liệu không cache. Cái gì giữ nó tĩnh/cache: 'use cache'generateStaticParams.

              ┌─────────────── PPR page ───────────────┐
  instant →   │  static shell ('use cache')             │
              │     └─ <Suspense> dynamic hole (stream) │ ← personalized
              └────────────────────────────────────────┘

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. Metadata tĩnh

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 gộp xuống theo cây layout, nên root layout đặt mặc định và page ghi đè.

Mẫu tiêu đề

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: 'Senior Next',
    template: '%s · Senior Next', // child pages fill %s
  },
};

Một page export title: 'Pricing' trở thành Pricing · Senior Next.


3. Metadata động với generateMetadata

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] },
  };
}

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. Metadata theo file: ảnh OG, sitemap, robots

Next.js tạo metadata từ các file đặt tên đặc biệt.

Ảnh OG động

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

// 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',
  };
}

Cũng hữu ích: app/manifest.ts (manifest PWA), app/icon.png / app/apple-icon.png (favicon).


5. next/image — hết giật layout

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
    />
  );
}

Quy tắc:

  • Luôn cung cấp width/height (hoặc dùng fill với cha có kích thước) — để giữ chỗ.
  • Dùng priority chỉ cho ảnh LCP; đừng cho mọi ảnh.
  • 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 — font không giật layout

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>
  );
}

Font local dùng next/font/local với cùng lợi ích.


7. next/script — nạp script bên thứ ba an toàn

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" />
    </>
  );
}

Chiến lược: beforeInteractive (quan trọng, hiếm), afterInteractive (mặc định, analytics), lazyOnload (ưu tiên thấp, widget chat).


8. Checklist SEO

  • Mỗi trang có title + description riêng.
  • Tag OG + Twitter để link preview đẹp.
  • sitemap.ts + robots.ts.
  • URL canonical đặt qua alternates.canonical cho trang dễ trùng lặp.
  • HTML ngữ nghĩa và <a href>/<Link> thật (crawler theo link, không phải onClick).
  • Giữ nội dung quan trọng render-server, không fetch client sau khi tải.
  • Dùng generateStaticParams để trang quan trọng được prerender.

9. Bài tập

  1. Mẫu tiêu đề: đặt title.template ở root layout và title mỗi page; xác nhận <title> được ghép trong HTML.

  2. Metadata động: thêm generateMetadata vào route [slug] lấy title/description từ bài.

  3. Ảnh OG: thêm opengraph-image.tsx vào route blog và xem bằng cách vào /blog/<slug>/opengraph-image.

  4. Sitemap & robots: tạo sitemap.ts từ các bài và robots.ts chặn /dashboard.

  5. Kiểm tra ảnh: 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: á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).


Phần tiếp theo

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.

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.