Skip to main content

Roadmap for building a Landing Page Tool with Next.js

A full-stack project a junior developer can finish in 2–4 weeks: a drag-and-drop tool for building landing pages (inspired by Carrd), with live preview, pages saved in a database, form-data collection, and export to a static HTML file. Detailed flow diagrams are on Landing Pages Workflows.

Stack​

PartChoice
Frontend and backendNext.js (App Router: Route Handlers, Server Actions)
StylingTailwind CSS
StateReact Context + useReducer
Drag and dropdnd-kit
DatabaseMongoDB (Mongoose)
AuthenticationAuth.js (NextAuth)
DeployVercel, MongoDB Atlas

One Next.js repo holds both the UI and the API — a full-stack monolith, manageable for one person.

Four-week roadmap​

WeekFocusGoal
1Setup and UIScaffold Next.js + Tailwind; the main layout (header, sidebar, canvas); static blocks (TextBlock, ImageBlock…); get used to the folder structure
2Editor and live previewState with Context; drag a block from the sidebar onto the canvas; a properties panel to edit the block; the preview updates immediately
3Backend, storage, and formsMongoDB schema; a CRUD API for pages; save and load a page; an API that receives form data
4FinishExport HTML/CSS; responsive layout for both the tool and the pages it creates; notifications, a README; deploy to Vercel

Folder structure​

Click an item to see what it is for.

Project structure

Details

PageContext.tsx

The single source of truth for the block list of the page being edited (Context + useReducer).

Core technical ideas​

Data model: a page is an array of blocks​

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 };

The editor, the public page, and the exported file all render from the same blocks array — store this JSON in MongoDB.

Live preview​

The reducer always returns a new object (it does not mutate state) so React notices the change. Wrap each block in React.memo so only the block that just changed re-renders — this matters when a page has many blocks.

HTML export​

A simple approach that works inside a Route Handler: each block type has a function that returns an HTML string.

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

Block content is entered by the user — always escape it when assembling HTML, or the exported page contains XSS. For images, accept only https: URLs. The exported file needs a small stylesheet written ahead of time, because Tailwind classes only work when a built CSS file is present.

Deployment and good practice​

PracticeDetail
Split components upOne file per block; keep components small and easy to reuse
PerformanceReact.memo and useCallback to avoid extra renders; measure with the React Profiler
Clean codeTypeScript, ESLint, Prettier
API securityCheck that the user owns the page before editing or deleting it; validate input on the server (zod, for example); session in an HttpOnly cookie
Public form endpointValidate, limit payload size and how often someone can submit, and add a honeypot against spam
READMEDescription, screenshots, how to install, run locally, and deploy
DeployVercel for Next.js, MongoDB Atlas for the database; set environment variables on Vercel

When you put the project on a CV: see Personal project for a junior — the example there is this project.