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 devcấ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 markdownPhầ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
.mdcó frontmatter (title, date, excerpt). - Render: SSG cho bài viết +
revalidate(ISR) để cập nhật khi sửa nội dung. - SEO:
generateMetadatatheo 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.
