Dựa trên video hướng dẫn của Paul Bratslavsky
Bạn đang muốn xây dựng một website đầy đủ tính năng nhưng không biết bắt đầu từ đâu? Bạn đang tìm kiếm một stack công nghệ hiện đại giúp vừa phát triển nhanh vừa tạo ra sản phẩm chất lượng cao?
Trong bài viết này, mình sẽ tóm tắt một hướng dẫn tuyệt vời về cách kết hợp Next.js 15 với Strapi 5 để xây dựng một website camp hè hoàn chỉnh. Bài viết sẽ giúp bạn hiểu cách tận dụng sức mạnh của React server components kết hợp với headless CMS để tạo ra website động, thân thiện với developer và dễ quản lý đối với client.
Nội Dung Chính
- Tổng quan về dự án và công nghệ (00:00-10:30)
- Cài đặt và kết nối Next.js với Strapi (10:31-25:45)
- Xây dựng trang chủ động với Block Renderer (25:46-45:30)
- Tạo trang Experience với Dynamic Routes (45:31-60:00)
- Xây dựng Header và Footer (60:01-75:15)
- Thiết lập trang Blog và Featured Articles (75:16-90:30)
- Form đăng ký Newsletter với Server Actions (90:31-110:00)
- Tạo trang Blog chi tiết và hiển thị nội dung (110:01-130:45)
- Chức năng tìm kiếm và phân trang (130:46-145:00)
- Hoàn thiện trang Events và Form đăng ký sự kiện (145:01-170:00)
Tổng quan về dự án và công nghệ
Trong khóa học này, chúng ta sẽ xây dựng một website quảng bá cho trại hè lướt sóng từ đầu đến cuối, kết hợp Next.js 15 và Strapi 5 – một headless CMS. Với những kiến thức thu được, bạn có thể áp dụng để tạo ra các website cho doanh nghiệp nhỏ hoặc cho các dự án cá nhân.
Điểm Chính:
- Website sẽ bao gồm trang chủ, trang experience, trang blog, và trang sự kiện
- Sử dụng Next.js 15 với React server components cho phần frontend
- Tận dụng Strapi 5 làm headless CMS để quản lý nội dung
- Kiến trúc decoupled giúp frontend và backend độc lập với nhau
- Sử dụng SASS cho styling thay vì Tailwind hay các component libraries
Ý Kiến Của Mình:
Kiến trúc headless CMS như Strapi mang lại nhiều lợi thế vượt trội so với WordPress. Bạn được tự do xây dựng frontend với bất kỳ công nghệ nào, trong khi vẫn cung cấp giao diện quản trị thân thiện cho khách hàng. Đây là xu hướng hiện đại trong phát triển website mà các developer nên nắm bắt.
Cài đặt và kết nối Next.js với Strapi
Để bắt đầu, chúng ta cần cài đặt cả Next.js và Strapi. Đầu tiên là cài đặt Next.js với lệnh npx create-next-app@latest
và đặt tên dự án là “client”. Tiếp theo là cài đặt Strapi với lệnh npx create-strapi-app@latest server
. Sau khi cài đặt xong, chúng ta tạo một collection type đầu tiên trong Strapi để lưu trữ dữ liệu cho trang chủ.
Điểm Chính:
- Tạo project Next.js với app router, TypeScript và không sử dụng Tailwind
- Cài đặt Strapi với SQLite làm database mặc định
- Tạo một “Single Type” Homepage trong Strapi với các trường title và description
- Cài đặt quyền truy cập trong Users & Permissions để có thể gọi API từ Next.js
- Tạo hàm loader trong Next.js để lấy dữ liệu từ Strapi API
// Loader function để lấy dữ liệu từ Strapi
async function loader() {
const URL = new URL('api/homepage', 'http://localhost:1337');
const res = await fetch(URL);
const data = await res.json();
return data;
}
// Sử dụng loader trong route
export default async function HomeRoute() {
const data = await loader();
console.log(data);
return (
{data.data.attributes.title}
{data.data.attributes.description}
); }
Ý Kiến Của Mình:
Việc tạo ra các utility function cho fetch API là cực kỳ hữu ích, giúp code clean hơn và dễ bảo trì. Trong dự án thực tế, mình luôn tạo các wrapper function cho API calls để xử lý lỗi và format response data một cách nhất quán.
Xây dựng trang chủ động với Block Renderer
Phần quan trọng nhất của dự án này là cách chúng ta xây dựng trang web động, cho phép client thêm/xóa/sửa các section trên website. Để làm điều này, chúng ta sẽ tạo các components trong Strapi như HeroSection và InfoBlock, sau đó kết hợp chúng thành Dynamic Zone để client có thể tùy chỉnh.
Điểm Chính:
- Tạo các reusable components trong Strapi như Logo và Link
- Xây dựng HeroSection component với logo, image, heading, CTA và theme
- Tạo InfoBlock component với các tùy chọn reversed, headline, content, image và CTA
- Thêm Dynamic Zone “blocks” vào Homepage type trong Strapi
- Cấu hình populate query để lấy dữ liệu đầy đủ từ Strapi API
- Xây dựng BlockRenderer trong Next.js để render các components động
// BlockRenderer component
export default function BlockRenderer({ blocks }) {
return (
<>
{blocks.map((block) => {
switch (block.__component) {
case 'blocks.hero-section':
return ;
case 'blocks.info-block':
return ;
default:
return null;
}
})}
</>
);
}
Ý Kiến Của Mình:
Pattern BlockRenderer là một trong những pattern mạnh mẽ nhất khi làm việc với headless CMS. Nó cho phép bạn tạo ra các trang hoàn toàn có thể tùy chỉnh mà không cần viết thêm code. Đây là điểm mà Strapi vượt trội so với nhiều CMS khác – khả năng tạo ra cấu trúc dữ liệu linh hoạt theo nhu cầu dự án.
Tạo trang Experience với Dynamic Routes
Để tái sử dụng các components đã tạo và xây dựng thêm các trang mới, chúng ta sẽ tạo một Collection Type “Page” trong Strapi và Dynamic Routes trong Next.js. Điều này cho phép chúng ta tạo nhiều trang với cùng một cấu trúc nhưng nội dung khác nhau.
Điểm Chính:
- Tạo Collection Type “Page” trong Strapi với các fields: title, description, slug và blocks
- Tạo một trang Experience trong Strapi với các blocks tùy chỉnh
- Xây dựng Dynamic Route [slug] trong Next.js
- Tạo hàm getPageBySlug để lấy dữ liệu trang theo slug
- Sử dụng notFound() khi không tìm thấy trang để hiển thị 404
// [slug]/page.tsx
import { notFound } from 'next/navigation';
import { getPageBySlug } from '@/data/loaders';
import BlockRenderer from '@/components/BlockRenderer';
export default async function DynamicPage({ params }) {
const { slug } = params;
const data = await getPageBySlug(slug);
if (!data) {
return notFound();
}
const { blocks } = data;
return (
); }
Ý Kiến Của Mình:
Dynamic Routes trong Next.js kết hợp với Collection Types trong Strapi tạo nên một hệ thống cực kỳ mạnh mẽ. Bạn có thể tạo ra vô số trang với cùng một template mà không cần viết thêm code. Đặc biệt hữu ích cho các website có nhiều trang sản phẩm, danh mục, hay bài viết.
Xây dựng Header và Footer
Website cần có header và footer nhất quán trên tất cả các trang. Chúng ta sẽ tạo một Single Type “Global” trong Strapi để lưu trữ dữ liệu cho header và footer, sau đó render chúng trong root layout của Next.js.
Điểm Chính:
- Tạo Single Type “Global” trong Strapi với các components Header và Footer
- Tạo Header component với Logo, Navigation và CTA
- Xây dựng Footer component với Logo, Navigation, Policies và Copy text
- Tạo hàm getGlobalSettings để lấy dữ liệu header và footer
- Render Header và Footer trong RootLayout của Next.js
- Thêm logic để hiển thị header khác nhau dựa trên current path
// layout.tsx
import { getGlobalSettings } from '@/data/loaders';
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
export default async function RootLayout({ children }) {
const globalData = await getGlobalSettings();
if (!globalData) {
throw new Error('Failed to fetch global settings');
}
const { header, footer } = globalData;
return (
);
}
Ý Kiến Của Mình:
Việc lưu trữ dữ liệu header và footer trong một Single Type riêng biệt là một pattern rất hiệu quả. Nó cho phép client dễ dàng cập nhật navigation, logo, hay thông tin liên hệ mà không cần phải chỉnh sửa từng trang một. Đồng thời, việc render chúng trong RootLayout đảm bảo sự nhất quán xuyên suốt website.
Thiết lập trang Blog và Featured Articles
Blog là một phần quan trọng của website. Chúng ta sẽ tạo trang blog với featured article, section đăng ký newsletter, và danh sách bài viết. Tất cả đều có thể quản lý từ Strapi.
Điểm Chính:
- Tạo các blocks mới trong Strapi: FeaturedArticle và Subscribe
- Thêm blocks mới vào Collection Type “Page”
- Tạo trang Blog trong Strapi với các blocks tùy chỉnh
- Xây dựng FeaturedArticle và Subscribe components trong Next.js
- Tạo trang Blog riêng biệt trong Next.js thay vì dùng dynamic route
- Cập nhật BlockRenderer để hỗ trợ các blocks mới
// components/blocks/FeaturedArticle.tsx
import Link from 'next/link';
import StrapiImage from '@/components/StrapiImage';
export default function FeaturedArticle({ headline, excerpt, image, link }) {
return (
{headline}
{link && ( {link.text} )}
{image && (
)}
); }
Ý Kiến Của Mình:
Việc tạo ra các blocks chuyên biệt như FeaturedArticle giúp trang blog trở nên linh hoạt và dễ quản lý. Client có thể thay đổi bài viết nổi bật, cập nhật copy của form đăng ký mà không cần can thiệp vào code. Đây là cách tiếp cận rất hiệu quả cho các website cần cập nhật nội dung thường xuyên.
Form đăng ký Newsletter với Server Actions
Một trong những tính năng mới nhất của Next.js 15 là Server Actions, cho phép chúng ta xử lý form submission trực tiếp trong Next.js mà không cần API routes. Chúng ta sẽ dùng tính năng này để tạo form đăng ký newsletter.
Điểm Chính:
- Tạo Collection Type “NewsletterSignup” trong Strapi để lưu email
- Cài đặt quyền trong Users & Permissions để cho phép create entry
- Tạo Server Action sử dụng ‘use server’ directive trong Next.js
- Tích hợp Zod để validate form data
- Sử dụng useActionState hook để quản lý form state
- Hiển thị thông báo lỗi và thành công cho user
// actions.ts
'use server';
import { z } from 'zod';
import { subscribeService } from './services';
const subscribeSchema = z.object({
email: z.string().email('Please enter a valid email address')
});
export async function subscribeAction(previousState, formData) {
const email = formData.get('email');
// Validate input
const validatedFields = subscribeSchema.safeParse({ email });
if (!validatedFields.success) {
return {
...previousState,
zodErrors: validatedFields.error.flatten().fieldErrors,
strapiErrors: null,
errorMessage: 'Validation failed',
successMessage: null
};
}
// Submit to Strapi
const responseData = await subscribeService(validatedFields.data);
if (!responseData) {
return {
...previousState,
zodErrors: null,
strapiErrors: null,
errorMessage: 'Oops! Something went wrong. Please try again later',
successMessage: null
};
}
if (responseData.error) {
return {
...previousState,
zodErrors: null,
strapiErrors: responseData.error,
errorMessage: 'Failed to subscribe',
successMessage: null
};
}
return {
...previousState,
zodErrors: null,
strapiErrors: null,
errorMessage: null,
successMessage: 'Successfully subscribed!'
};
}
Ý Kiến Của Mình:
Server Actions là một trong những tính năng tiến bộ nhất của Next.js 15. Nó giúp đơn giản hóa việc xử lý form submission bằng cách cho phép chúng ta viết server-side logic trực tiếp trong component. Kết hợp với validation library như Zod, chúng ta có thể tạo ra form submission flow an toàn và user-friendly.
Tạo trang Blog chi tiết và hiển thị nội dung
Sau khi có trang danh sách bài viết, chúng ta cần tạo trang chi tiết cho từng bài blog. Chúng ta sẽ sử dụng Dynamic Zones trong Strapi để cho phép client tùy chỉnh cấu trúc bài viết với nhiều loại block khác nhau.
Điểm Chính:
- Tạo Collection Type “Article” trong Strapi với các fields: title, description, slug, author, image, featured và blocks
- Tạo các components mới: Heading, Paragraph, FullImage, và ParagraphWithImage
- Tạo Dynamic Route [slug] trong blog folder để hiển thị bài viết chi tiết
- Xây dựng hàm getContentBySlug để lấy dữ liệu bài viết theo slug
- Tạo các components trong Next.js tương ứng với các blocks trong Strapi
- Hiển thị table of contents tự động dựa trên các heading blocks
- Hiển thị các bài viết liên quan ở cuối trang
// blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getContentBySlug } from '@/data/loaders';
import HeroSection from '@/components/blocks/HeroSection';
import ArticleOverview from '@/components/ArticleOverview';
import BlockRenderer from '@/components/BlockRenderer';
import ContentList from '@/components/ContentList';
import BlogCard from '@/components/BlogCard';
export default async function SingleBlogRoute({ params }) {
const { slug } = params;
const data = await getContentBySlug('articles', slug);
if (!data) {
return notFound();
}
const { blocks, article } = data;
// Extract headings for table of contents
const tableOfContent = blocks
.filter(block => block.__component === 'blocks.heading')
.map(block => ({
heading: block.heading,
linkId: block.linkId
}));
return (
); }
Ý Kiến Của Mình:
Khả năng tạo nội dung blog linh hoạt chính là điểm mạnh của headless CMS. Thay vì bị giới hạn trong một cấu trúc cố định như WordPress, Strapi cho phép content editor tùy chỉnh cấu trúc bài viết với các block đa dạng. Pattern này rất phù hợp cho các trang content marketing cần sự sáng tạo trong trình bày nội dung.
Chức năng tìm kiếm và phân trang
Để nâng cao trải nghiệm người dùng, chúng ta sẽ thêm chức năng tìm kiếm và phân trang cho danh sách bài viết blog. Thay vì sử dụng state management, chúng ta sẽ tận dụng URL search params để lưu trạng thái tìm kiếm và phân trang.
Điểm Chính:
- Tạo Search component với useDebounce để tối ưu số lượng request
- Sử dụng URLSearchParams để lưu trạng thái search và pagination
- Tạo Pagination component với prev/next navigation
- Cập nhật getContent loader để hỗ trợ query và pagination
- Cập nhật ContentList component để nhận và hiển thị dữ liệu phân trang
- Tích hợp search và pagination vào trang Blog
// components/Search.tsx
'use client';
import { useRouter } from 'next/navigation';
import { usePathname, useSearchParams } from 'next/navigation';
import { useDebounceCallback } from 'usehooks-ts';
export default function Search() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const handleSearch = useDebounceCallback((term) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
router.replace(`${pathname}?${params.toString()}`);
}, 300);
return (
); }
Ý Kiến Của Mình:
Việc sử dụng URL search params để lưu trạng thái search và pagination là một pattern rất hiệu quả. Nó cho phép user có thể bookmark hoặc chia sẻ URL với kết quả tìm kiếm cụ thể. Ngoài ra, nó cũng giúp giải quyết vấn đề về trạng thái khi user refresh trang – các tham số tìm kiếm và phân trang vẫn được duy trì. Đây là một kỹ thuật mình thường xuyên áp dụng trong các dự án thực tế.
Hoàn thiện trang Events và Form đăng ký sự kiện
Phần cuối cùng của dự án là xây dựng trang Events và form đăng ký tham gia sự kiện. Chúng ta sẽ tạo Collection Type “Event” trong Strapi và “EventSignup” để lưu thông tin đăng ký, sau đó liên kết chúng với nhau thông qua relation.
Điểm Chính:
- Tạo Collection Type “Event” với các fields: title, description, slug, image, featured, price, start date và blocks
- Tạo Collection Type “EventSignup” để lưu thông tin người đăng ký: first name, last name, email, telephone và relation với Event
- Xây dựng trang Events hiển thị tất cả sự kiện với search và pagination
- Tạo Dynamic Route để hiển thị chi tiết từng sự kiện
- Xây dựng form đăng ký sự kiện với validation đầy đủ
- Tạo Server Action để xử lý form submission và gửi dữ liệu đến Strapi
- Hiển thị các sự kiện nổi bật ở cuối trang
// components/EventSignupForm.tsx
'use client';
import { useState } from 'react';
import { useActionState } from 'react-dom';
import { eventSubscribeAction } from '@/data/actions';
import BlockRenderer from '@/components/BlockRenderer';
import SubmitButton from '@/components/SubmitButton';
const initialState = {
zodErrors: null,
strapiErrors: null,
errorMessage: null,
successMessage: null,
formData: {}
};
function TextInput({ label, name, type = 'text', errors, defaultValue }) {
return (
{errors?.[name] &&
{errors[name]}
}
); } export default function EventSignupForm({ event, blocks }) { const [formState, formAction] = useActionState(eventSubscribeAction, initialState); const { zodErrors, strapiErrors, successMessage } = formState; return (
{event.price && (
Price
{event.price}
)} {event.startDate && (
Start Date
{new Date(event.startDate).toLocaleDateString()}
)}
Sign up for this event
); }
Ý Kiến Của Mình:
Form đăng ký sự kiện kết hợp với validation và relation trong Strapi tạo ra một system hoàn chỉnh và chuyên nghiệp. Điều tuyệt vời là tất cả logic form submission đều xử lý phía server thông qua Server Actions, giúp tăng cường bảo mật và giảm JavaScript phía client. Client có thể dễ dàng xem danh sách người đăng ký cho mỗi sự kiện trong Strapi admin panel.
Tổng kết
Qua bài hướng dẫn này, chúng ta đã xây dựng một website camp hè hoàn chỉnh sử dụng Next.js 15 và Strapi 5. Website bao gồm đầy đủ các tính năng cần thiết cho một business website: trang chủ động, trang blog với chức năng tìm kiếm và phân trang, form đăng ký newsletter, và form đăng ký sự kiện.
Những công nghệ và kỹ thuật quan trọng đã học:
- Next.js 15 với React Server Components
- Strapi 5 Headless CMS
- Dynamic routes và Dynamic zones
- Server Actions cho form submission
- Zod validation
- Search và pagination với URL search params
- SASS styling
- BlockRenderer pattern
Ý Kiến Của Mình:
Stack công nghệ Next.js + Strapi là một lựa chọn tuyệt vời cho các freelancer và teams nhỏ muốn xây dựng website chuyên nghiệp với CMS mạnh mẽ. Với Block system linh hoạt và Server Components, bạn có thể tạo ra các trang dynamic mà vẫn đảm bảo hiệu suất cao. Dự án này đã trình bày một cách tiếp cận có thể áp dụng cho nhiều loại website khác nhau: từ trang corporate, e-commerce, đến blog và portfolio.