Skip to main content

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ầnLựa chọn
Frontend & backendNext.js (App Router: Route Handlers, Server Actions)
StylingTailwind CSS
StateReact Context + useReducer
Kéo-thảdnd-kit
DatabaseMongoDB (Mongoose)
Xác thựcAuth.js (NextAuth)
DeployVercel, 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ầnTrọng tâmMục tiêu
1Thiết lập & giao diệnKhở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
2Trình chỉnh sửa & live previewState 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ì
3Backend, lưu trữ & formSchema MongoDB; API CRUD cho trang; lưu/tải trang; API nhận dữ liệu form
4Hoàn thiệnXuấ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​

types.ts
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.

lib/export.ts
const escapeHtml = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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>`;
}
};
warning

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ànhChi tiết
Chia nhỏ componentMỗi block một file; giữ component nhỏ, dễ tái sử dụng
Hiệu năngReact.memo, useCallback để tránh render thừa; đo bằng React Profiler
Code sạchTypeScript, ESLint, Prettier
Bảo mật APIKiể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 khaiValidate, giới hạn kích thước và tần suất gửi, thêm honeypot chống spam
READMEMô tả, ảnh chụp, cách cài đặt, chạy local và deploy
DeployVercel 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.