Về danh sách bài học
Dự án · Next.js

Blog với NextJS

Dự án tổng hợp: dựng một blog cá nhân bằng Next App Router — danh sách bài, trang chi tiết theo slug, nội dung viết bằng Markdown, render tĩnh (SSG) + làm mới (ISR), metadata động cho SEO, ảnh tối ưu và deploy. Áp dụng lại mọi thứ đã học.

Phần 1

Tạo dự án & cấu trúc

Next dùng create-next-app.

terminal
# Tạo dự án Next (dùng create-next-app, KHÔNG dùng Vite cho Next)
npx create-next-app@latest my-blog
cd my-blog
npm run dev
cấu trúc
my-blog/
 ├─ posts/                     # nội dung bài viết dạng .md / .mdx
 │   ├─ nextjs-la-gi.md
 │   └─ hoc-react.md
 ├─ app/
 │   ├─ page.tsx               # trang chủ: danh sách bài
 │   ├─ blog/[slug]/page.tsx   # trang chi tiết bài viết
 │   ├─ layout.tsx             # metadata gốc + template title
 │   ├─ sitemap.ts             # sinh sitemap.xml
 │   └─ robots.ts
 └─ lib/posts.ts               # đọc file markdown
Phần 2

Yêu cầu chức năng

Bắt buộc

  • Trang chủ (/): liệt kê bài viết (tiêu đề, mô tả ngắn, ngày) — dùng SSG.
  • Chi tiết (/blog/[slug]): route động, đọc bài theo slug; generateStaticParams để build sẵn.
  • Nội dung Markdown: mỗi bài là 1 file .md có frontmatter (title, date, excerpt).
  • Render: SSG cho bài viết + revalidate (ISR) để cập nhật khi sửa nội dung.
  • SEO: generateMetadata theo bài + sitemap.ts, robots.ts.
  • Ảnh: ảnh bìa dùng next/image.

Kiến thức áp dụng

App Router, route động & params, Server Component + data fetching, render modes (SSG/ISR), metadata & SEO, tối ưu ảnh — chính là các bài trong khoá.

Phần 3

Trang danh sách bài

app/page.tsx
1// app/page.tsx — danh sách bài (SSG: dựng lúc build)
2import Link from "next/link";
3import { getAllPosts } from "@/lib/posts";
4
5export default function Home() {
6  const posts = getAllPosts(); // [{ slug, title, excerpt, date }]
7  return (
8    <ul>
9      {posts.map((p) => (
10        <li key={p.slug}>
11          <Link href={"/blog/" + p.slug}>{p.title}</Link>
12        </li>
13      ))}
14    </ul>
15  );
16}
Phần 4

Trang chi tiết + metadata + ISR

app/blog/[slug]/page.tsx
1// app/blog/[slug]/page.tsx — trang chi tiết + metadata động
2import { getPost, getAllSlugs } from "@/lib/posts";
3
4// Dựng sẵn mọi trang bài viết lúc build (SSG)
5export async function generateStaticParams() {
6  return getAllSlugs().map((slug) => ({ slug }));
7}
8
9// SEO: title/description theo từng bài
10export async function generateMetadata({ params }) {
11  const post = getPost(params.slug);
12  return { title: post.title + " | Blog", description: post.excerpt };
13}
14
15export const revalidate = 3600; // ISR: làm mới mỗi giờ
16
17export default function PostPage({ params }) {
18  const post = getPost(params.slug);
19  return (
20    <article>
21      <h1>{post.title}</h1>
22      <div dangerouslySetInnerHTML={{ __html: post.html }} />
23    </article>
24  );
25}
Phần 5

Đọc Markdown

lib/posts.ts
1// lib/posts.ts — đọc markdown bằng gray-matter + marked
2// npm i gray-matter marked
3import fs from "fs";
4import path from "path";
5import matter from "gray-matter";
6import { marked } from "marked";
7
8const DIR = path.join(process.cwd(), "posts");
9
10export function getAllSlugs() {
11  return fs.readdirSync(DIR).map((f) => f.replace(/\.md$/, ""));
12}
13export function getPost(slug) {
14  const raw = fs.readFileSync(path.join(DIR, slug + ".md"), "utf8");
15  const { data, content } = matter(raw);          // tách frontmatter
16  return { slug, ...data, html: marked(content) };
17}
Phần 6

Gợi ý & mở rộng

Nên làm
  • Dùng SSG + ISR cho bài viết (nhanh + vẫn cập nhật được).
  • Mỗi bài có title/description riêng qua generateMetadata.
  • Ảnh bìa dùng next/image, đặt kích thước để tránh CLS.
  • Thêm sitemap.ts + robots.ts cho SEO.
Nên tránh
  • Đừng fetch nội dung ở client bằng useEffect — mất SEO.
  • Đừng để mọi trang chung một title.

Nâng cao (tuỳ chọn)

  • Dùng MDX để nhúng component vào bài viết.
  • Trang danh mục/tag, tìm kiếm bài viết.
  • Ảnh Open Graph tự sinh (opengraph-image.tsx).
  • Deploy lên Vercel.
Phần 7

Bản mẫu tham khảo

Tự dựng trước; bí quá mới mở.

Hãy tự dựng theo yêu cầu ở trên. Cần đối chiếu thì mở bản mẫu.