SSG

Bài 7 – Khái niệm Static Site Generation và cách sử dụng trong Next.js

18/11/2025 DaiPhan
Bài 8 / 18

SSG – Static Site Generation

SSG (Static Site Generation) là cơ chế render trang ngay trong quá trình build. Kết quả là file HTML tĩnh được phân phối qua CDN, đảm bảo tốc độ tải trang rất nhanh, giảm tải cho server và tối ưu SEO. Đây là lựa chọn hiệu quả cho nội dung ít thay đổi hoặc có chu kỳ cập nhật rõ ràng.


1. SSG là gì?

1.1. Định nghĩa và Ưu điểm

Static Site Generation (SSG) là kỹ thuật pre-render HTML tại build time:

  • ⚡ Performance: Tốc độ tải trang cực nhanh (HTML tĩnh)
  • 🔒 Security: Không có server-side code execution
  • 💰 Cost-effective: Có thể host trên CDN với chi phí thấp
  • 🎯 SEO: Perfect cho SEO với HTML đầy đủ
  • 📈 Scalability: Handle traffic lớn dễ dàng

1.2. SSG Flow trong Next.js

Build Time → Fetch Data → Generate HTML → Deploy to CDN → Serve Static Files

2. SSG trong App Router

2.1. SSG mặc định (Static Rendering)

// app/blog/page.tsx – SSG tự động
export default async function BlogPage() {
  // Mặc định là SSG nếu không có cache: 'no-store' hoặc dynamic functions
  const res = await fetch("https://api.example.com/blogs", {
    cache: "force-cache" // hoặc bỏ hoàn toàn, Next.js tự động cache
  });
  const blogs = await res.json();

  return (
    <div className="blog-container">
      <h1 className="text-3xl font-bold mb-6">📝 Danh sách bài viết</h1>
      <div className="blog-grid">
        {blogs.map((blog) => (
          <article key={blog.id} className="blog-card">
            <h2 className="text-xl font-semibold">{blog.title}</h2>
            <p className="text-gray-600 mt-2">{blog.excerpt}</p>
            <div className="blog-meta mt-4">
              <span className="text-sm text-gray-500">
                📅 {new Date(blog.publishedAt).toLocaleDateString('vi-VN')}
              </span>
              <span className="text-sm text-gray-500 ml-4">
                👤 {blog.author}
              </span>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

2.2. SSG với Dynamic Routes

// app/blog/[slug]/page.tsx – SSG cho dynamic routes
export default async function BlogPost({ params }) {
  const res = await fetch(`https://api.example.com/blogs/${params.slug}`);
  const post = await res.json();

  return (
    <article className="blog-post">
      <header className="post-header">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
        <div className="post-meta">
          <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
          <span>•</span>
          <span>{post.readingTime} min read</span>
        </div>
      </header>
      <div className="post-content">
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </div>
    </article>
  );
}

// Generate static params cho tất cả blog posts
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/blogs');
  const posts = await res.json();
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

2.3. SSG với Multiple Data Sources

// app/docs/[category]/[slug]/page.tsx
export default async function DocPage({ params }) {
  // Fetch từ nhiều sources
  const [docRes, sidebarRes] = await Promise.all([
    fetch(`https://api.example.com/docs/${params.category}/${params.slug}`),
    fetch(`https://api.example.com/docs/${params.category}/sidebar`)
  ]);
  
  const [doc, sidebar] = await Promise.all([
    docRes.json(),
    sidebarRes.json()
  ]);

  return (
    <div className="docs-layout">
      <aside className="docs-sidebar">
        <nav>
          {sidebar.items.map((item) => (
            <a key={item.slug} href={`/docs/${params.category}/${item.slug}`}>
              {item.title}
            </a>
          ))}
        </nav>
      </aside>
      <main className="docs-content">
        <h1>{doc.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: doc.content }} />
      </main>
    </div>
  );
}

// Generate static params cho nested routes
export async function generateStaticParams() {
  const categories = await fetch('https://api.example.com/docs/categories').then(r => r.json());
  
  const params = [];
  
  for (const category of categories) {
    const docs = await fetch(`https://api.example.com/docs/${category.slug}`).then(r => r.json());
    
    for (const doc of docs) {
      params.push({
        category: category.slug,
        slug: doc.slug
      });
    }
  }
  
  return params;
}

3. SSG trong Pages Router (Legacy)

3.1. getStaticProps cơ bản

// pages/blog/index.tsx (Pages Router)
import { GetStaticProps } from 'next'

interface Blog {
  id: string
  title: string
  excerpt: string
  publishedAt: string
  author: string
}

interface BlogPageProps {
  blogs: Blog[]
}

export default function BlogPage({ blogs }: BlogPageProps) {
  return (
    <div className="blog-page">
      <h1 className="text-3xl font-bold mb-6">Blog Posts</h1>
      <div className="blog-list">
        {blogs.map((blog) => (
          <article key={blog.id} className="blog-item">
            <h2 className="text-xl font-semibold">{blog.title}</h2>
            <p className="text-gray-600">{blog.excerpt}</p>
            <div className="blog-meta">
              <span>{blog.author}</span>
              <span>•</span>
              <time>{new Date(blog.publishedAt).toLocaleDateString()}</time>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

export const getStaticProps: GetStaticProps<BlogPageProps> = async () => {
  const res = await fetch('https://api.example.com/blogs')
  const blogs = await res.json()
  
  return {
    props: {
      blogs
    }
  }
}

3.2. getStaticPaths cho Dynamic Routes

// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next'

interface BlogPost {
  id: string
  title: string
  content: string
  publishedAt: string
}

interface BlogPostProps {
  post: BlogPost
}

export default function BlogPost({ post }: BlogPostProps) {
  return (
    <article className="blog-post">
      <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
      <time className="text-gray-500">{new Date(post.publishedAt).toLocaleDateString()}</time>
      <div className="content mt-6">
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </div>
    </article>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/blogs')
  const posts = await res.json()
  
  const paths = posts.map((post) => ({
    params: { slug: post.slug }
  }))
  
  return {
    paths,
    fallback: 'blocking' // Hoặc false, true
  }
}

export const getStaticProps: GetStaticProps<BlogPostProps> = async ({ params }) => {
  const res = await fetch(`https://api.example.com/blogs/${params.slug}`)
  const post = await res.json()
  
  return {
    props: {
      post
    }
  }
}

4. So sánh SSG vs SSR vs ISR

4.1. Comparison Matrix

FeatureSSGSSRISR
Build Time✅ Có❌ Không✅ Có
Request Time❌ Không✅ Có✅ (Revalidate)
Performance🚀 Nhanh nhất⚡ Vừa phải🚀 Nhanh
SEO✅ Tốt nhất✅ Tốt✅ Tốt
Data Freshness📅 Static🔄 Real-time🔄 Configurable
Server Cost💰 Thấp nhất💰💰💰 Cao💰💰 Vừa phải
CDN Friendly✅ Hoàn hảo❌ Không✅ Có

4.2. Khi nào dùng SSG?

// ✅ NÊN dùng SSG:
// - Marketing pages
// - Blog posts
// - Documentation
// - Product catalogs (ít thay đổi)
// - Landing pages
// - About pages

// ❌ KHÔNG NÊN dùng SSG:
// - User dashboards
// - Real-time data
// - Authentication required
// - A/B testing pages

5. Performance Optimization

5.1. Image Optimization với SSG

// app/products/page.tsx
import Image from 'next/image'

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  
  return (
    <div className="products-grid">
      {products.map((product) => (
        <div key={product.id} className="product-card">
          <Image
            src={product.image}
            alt={product.name}
            width={300}
            height={300}
            priority={index < 4} // Ưu tiên load ảnh đầu tiên
          />
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  )
}

5.2. Code Splitting và Bundle Optimization

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* Preconnect to external domains */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
        
        {/* Preload critical resources */}
        <link rel="preload" href="/fonts/main-font.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
      </head>
      <body>{children}</body>
    </html>
  )
}

6. Real-world Examples

6.1. Documentation Site với SSG

// app/docs/[[...slug]]/page.tsx – Catch-all routes
export default async function DocsPage({ params }) {
  const slug = params.slug?.join('/') || 'index'
  const doc = await fetch(`https://api.example.com/docs/${slug}`).then(r => r.json())
  
  return (
    <div className="docs-container">
      <nav className="docs-nav">
        {/* Navigation */}
      </nav>
      <main className="docs-content">
        <h1>{doc.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: doc.content }} />
      </main>
    </div>
  )
}

// Generate tất cả docs pages
export async function generateStaticParams() {
  const docs = await fetch('https://api.example.com/docs/all').then(r => r.json())
  
  return docs.map((doc) => ({
    slug: doc.path.split('/').filter(Boolean)
  }))
}

6.2. Portfolio Site với SSG

// app/portfolio/page.tsx
export default async function PortfolioPage() {
  const [projects, skills, testimonials] = await Promise.all([
    fetch('https://api.example.com/portfolio/projects').then(r => r.json()),
    fetch('https://api.example.com/portfolio/skills').then(r => r.json()),
    fetch('https://api.example.com/portfolio/testimonials').then(r => r.json())
  ])
  
  return (
    <div className="portfolio">
      <section className="hero">
        <h1>John Doe</h1>
        <p>Full Stack Developer</p>
      </section>
      
      <section className="skills">
        <h2>Skills</h2>
        <div className="skills-grid">
          {skills.map((skill) => (
            <div key={skill.id} className="skill-item">
              {skill.name}
            </div>
          ))}
        </div>
      </section>
      
      <section className="projects">
        <h2>Projects</h2>
        <div className="projects-grid">
          {projects.map((project) => (
            <div key={project.id} className="project-card">
              <h3>{project.title}</h3>
              <p>{project.description}</p>
            </div>
          ))}
        </div>
      </section>
    </div>
  )
}

7. Best Practices

7.1. Content Management

// lib/content.ts – Content management utilities
export async function getAllBlogs() {
  const blogs = await fetch('https://api.example.com/blogs').then(r => r.json())
  
  return blogs.map((blog) => ({
    ...blog,
    readingTime: calculateReadingTime(blog.content),
    excerpt: generateExcerpt(blog.content, 150)
  }))
}

export async function getBlogBySlug(slug: string) {
  const blog = await fetch(`https://api.example.com/blogs/${slug}`).then(r => r.json())
  
  return {
    ...blog,
    readingTime: calculateReadingTime(blog.content),
    relatedPosts: await getRelatedPosts(blog.id)
  }
}

7.2. Build-time Optimization

// next.config.js
module.exports = {
  images: {
    domains: ['api.example.com'],
    formats: ['image/avif', 'image/webp'],
  },
  
  // Optimize builds
  experimental: {
    optimizeCss: true,
    optimizePackageImports: ['lodash', 'date-fns']
  },
  
  // Generate sitemap at build time
  webpack: (config, { isServer }) => {
    if (isServer) {
      require('./scripts/generate-sitemap')
    }
    return config
  }
}

8. Bài tập thực hành

Bài tập 1: Personal Blog với SSG

Tạo blog với:

  • Danh sách bài viết
  • Chi tiết bài viết
  • Categories và tags
  • Reading time calculator
  • Responsive design

Bài tập 2: Company Website

Tạo company site với:

  • Homepage với hero section
  • About page
  • Services page
  • Contact page
  • Team members showcase

Bài tập 3: Documentation Site

Tạo documentation với:

  • Nested navigation
  • Search functionality
  • Code syntax highlighting
  • Table of contents
  • Mobile-responsive layout

9. Kết luận bài học

  • SSG: Phương pháp tối ưu cho static content với performance cao nhất
  • Use Cases: Blog, docs, marketing pages, portfolios – nơi content ít thay đổi
  • Benefits: Nhanh, an toàn, SEO-friendly, cost-effective
  • Trade-offs: Không phù hợp cho real-time data hoặc user-specific content

🔑 GHI NHỚ QUAN TRỌNG:

  • Luôn dùng SSG cho content tĩnh để tối ưu performance
  • Plan your content structure trước khi implement
  • Optimize images và assets để giảm bundle size
  • Consider ISR nếu cần periodic updates mà vẫn giữ benefits của static
  • Test build performance với large amount of pages
18 bài học
Bài 8
Tiến độ hoàn thành 44%

Đã hoàn thành 8/18 bài học