Lộ trình xây dựng Landing Page Tool với Next.js
Một dự án full-stack vừa sức cho Junior developer trong 2–4 tuần: công cụ kéo-thả để tạo landing page (lấy cảm hứng từ Carrd), có live preview, lưu trang vào database, thu thập dữ liệu form và xuất ra file HTML tĩnh. Sơ đồ luồng chi tiết của dự án nằm ở trang Landing Pages Workflows.
Công nghệ
| Phần | Lựa chọn |
|---|---|
| Frontend & backend | Next.js (App Router: Route Handlers, Server Actions) |
| Styling | Tailwind CSS |
| State | React Context + useReducer |
| Kéo-thả | dnd-kit |
| Database | MongoDB (Mongoose) |
| Xác thực | Auth.js (NextAuth) |
| Deploy | Vercel, MongoDB Atlas |
Một repo Next.js duy nhất chứa cả giao diện lẫn API — full-stack monolith, dễ quản lý cho một người.
Lộ trình 4 tuần
| Tuần | Trọng tâm | Mục tiêu |
|---|---|---|
| 1 | Thiết lập & giao diện | Khởi tạo Next.js + Tailwind; layout chính (header, sidebar, canvas); các block tĩnh (TextBlock, ImageBlock…); nắm cấu trúc thư mục |
| 2 | Trình chỉnh sửa & live preview | State bằng Context; kéo block từ sidebar vào canvas; panel thuộc tính để sửa block; preview cập nhật tức thì |
| 3 | Backend, lưu trữ & form | Schema MongoDB; API CRUD cho trang; lưu/tải trang; API nhận dữ liệu form |
| 4 | Hoàn thiện | Xuất HTML/CSS; responsive cho cả tool và trang được tạo; thông báo, README; deploy lên Vercel |
Cấu trúc thư mục
Nhấn vào từng mục để xem vai trò của nó.
Project structure
Details
PageContext.tsx
Nguồn sự thật duy nhất cho danh sách block của trang đang sửa (Context + useReducer).
Khái niệm kỹ thuật cốt lõi
Mô hình dữ liệu: trang là một mảng block
type Block =
| { id: string; type: "text"; props: { text: string; align: "left" | "center" } }
| { id: string; type: "image"; props: { src: string; alt: string } }
| { id: string; type: "form"; props: { fields: string[]; buttonLabel: string } };
type LandingPage = { pageId: string; ownerId: string; title: string; slug: string; blocks: Block[]; published: boolean };
Editor, trang công khai và file xuất đều render từ cùng một mảng blocks — chỉ cần lưu JSON này vào MongoDB.
Live preview
Reducer luôn trả về object mới (không sửa trực tiếp state) để React nhận ra thay đổi. Bọc từng block bằng React.memo để chỉ block vừa sửa render lại — quan trọng khi trang có nhiều block.
Xuất HTML
Cách đơn giản và chạy được trong Route Handler: mỗi loại block có một hàm trả về chuỗi HTML.
const escapeHtml = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!);
const renderBlock = (block: Block): string => {
switch (block.type) {
case "text":
return `<p style="text-align:${block.props.align}">${escapeHtml(block.props.text)}</p>`;
case "image":
return `<img src="${escapeHtml(block.props.src)}" alt="${escapeHtml(block.props.alt)}">`;
case "form":
return `<form>${block.props.fields.map((f) => `<input name="${escapeHtml(f)}" placeholder="${escapeHtml(f)}">`).join("")}<button>${escapeHtml(block.props.buttonLabel)}</button></form>`;
}
};
Nội dung trong block do người dùng nhập — luôn escape khi ghép HTML, nếu không trang xuất ra sẽ chứa XSS. Với ảnh, chỉ chấp nhận URL https:. File xuất cần kèm một stylesheet nhỏ viết sẵn, vì class Tailwind chỉ có tác dụng khi có file CSS đã build.
Triển khai và thực hành tốt
| Thực hành | Chi tiết |
|---|---|
| Chia nhỏ component | Mỗi block một file; giữ component nhỏ, dễ tái sử dụng |
| Hiệu năng | React.memo, useCallback để tránh render thừa; đo bằng React Profiler |
| Code sạch | TypeScript, ESLint, Prettier |
| Bảo mật API | Kiểm tra người dùng có sở hữu trang trước khi sửa/xóa; validate input ở server (ví dụ zod); session trong cookie HttpOnly |
| Endpoint form công khai | Validate, giới hạn kích thước và tần suất gửi, thêm honeypot chống spam |
| README | Mô tả, ảnh chụp, cách cài đặt, chạy local và deploy |
| Deploy | Vercel cho Next.js, MongoDB Atlas cho database; biến môi trường đặt trên Vercel |
Khi đưa dự án vào CV: xem Dự án cá nhân cho Junior — ví dụ trong đó chính là dự án này.