Landing pages follow a proven formula. Visitors arrive with a question — "can this solve my problem?" — and every section either moves them toward "yes" or loses them. This guide walks through the 8-section formula, how to compose those sections in Next.js App Router, and the technical patterns that keep the result fast and maintainable.
The Landing Page Formula
Research from conversion optimization studies converges on the same section order. Here is the sequence and why each section exists:
| # | Section | Job | |---|---------|-----| | 1 | Hero | State the value proposition. Capture attention. | | 2 | Logo Carousel | Establish social proof immediately after the hook. | | 3 | Features | Show how the product delivers on the promise. | | 4 | Social Proof | Testimonials or case studies — proof from real people. | | 5 | Product Showcase | Visual demo, screenshot carousel, or walkthrough. | | 6 | Pricing | Remove the "how much?" objection. | | 7 | FAQ | Address the last objections before purchase. | | 8 | CTA | One final, low-friction invitation to act. |
This is not a rigid formula — a B2B enterprise product might skip public pricing and replace it with "Contact sales". A consumer app might lead with social proof. But this order has the highest baseline conversion rate for self-serve SaaS.
Composing Sections in Next.js App Router
In Next.js 15 with App Router, your landing page lives at app/[locale]/(public)/page.tsx. Each section is a separate file in src/registry/sections/:
// app/[locale]/(public)/page.tsx
import HeroCentered from "@/registry/sections/hero/hero-centered/HeroCentered";
import LogoCarousel from "@/registry/sections/logo-carousel/logo-carousel-marquee/LogoCarouselMarquee";
import FeaturesGrid from "@/registry/sections/features/features-grid/FeaturesGrid";
import Testimonials from "@/registry/sections/testimonials/testimonials-cards/TestimonialsCards";
import PricingToggle from "@/registry/sections/pricing/pricing-toggle/PricingToggle";
import FAQ from "@/registry/sections/faq/faq-accordion/FaqAccordion";
import CTABanner from "@/registry/sections/cta/cta-centered/CtaCentered";
import { heroMock } from "@/registry/sections/hero/hero-centered/mock";
import { pricingMock } from "@/registry/sections/pricing/pricing-toggle/mock";
export default function HomePage() {
return (
<main>
<HeroCentered {...heroMock} />
<LogoCarousel />
<FeaturesGrid id="features" />
<Testimonials id="social-proof" />
<PricingToggle id="pricing" {...pricingMock} />
<FAQ id="faq" />
<CTABanner />
</main>
);
}
Server Components render instantly at build time (or on request). The "use client" directive only appears on sections that need interactivity (toggle, animations). This means most of your landing page is static HTML — great for Core Web Vitals.
Section Dividers
Sections that use the same background color blur together visually. A divider component creates breathing room and visual separation without adding empty <div>s:
// components/SectionDivider.tsx
interface SectionDividerProps {
variant?: "line" | "wave" | "angle" | "none";
flip?: boolean;
}
export function SectionDivider({ variant = "line", flip = false }: SectionDividerProps) {
if (variant === "none") return null;
if (variant === "line") {
return (
<div
style={{
height: "1px",
background: "var(--color-border)",
margin: "0 var(--container-padding-x)",
}}
/>
);
}
if (variant === "wave") {
return (
<div
style={{
overflow: "hidden",
transform: flip ? "scaleY(-1)" : "none",
lineHeight: 0,
}}
>
<svg
viewBox="0 0 1440 56"
style={{ display: "block", width: "100%" }}
aria-hidden
>
<path
d="M0,28 C360,56 1080,0 1440,28 L1440,56 L0,56 Z"
fill="var(--color-background-alt)"
/>
</svg>
</div>
);
}
return null;
}
Use it between sections with different backgrounds:
<HeroCentered />
<SectionDivider variant="wave" />
<FeaturesGrid />
Anchor Navigation
For single-page landing pages, anchor links let users jump to sections. Add id attributes to section wrappers and link to them with #pricing, #features, etc.:
// Pass an id to each section component
interface SectionProps {
id?: string;
}
// Inside the section component
<section
id={id}
style={{
scrollMarginTop: "4rem", // Account for the fixed navbar height
paddingTop: "var(--section-padding-y)",
paddingBottom: "var(--section-padding-y)",
background: "var(--color-background)",
}}
>
The scroll-margin-top CSS property is critical — without it, the fixed navbar overlaps the section heading when the anchor link fires.
In the navbar:
const navLinks = [
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
{ label: "FAQ", href: "#faq" },
];
For smooth scrolling, add to your global CSS:
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
Lazy Loading Below-the-Fold Sections
The hero and the first visible section should load immediately. Everything below the fold can be lazy-loaded using Next.js dynamic:
import dynamic from "next/dynamic";
// Load immediately (above the fold)
import HeroCentered from "@/registry/sections/hero/hero-centered/HeroCentered";
import LogoCarousel from "@/registry/sections/logo-carousel/logo-carousel-marquee/LogoCarouselMarquee";
// Lazy load (below the fold)
const FeaturesGrid = dynamic(
() => import("@/registry/sections/features/features-grid/FeaturesGrid"),
{ loading: () => <SectionSkeleton height={500} /> }
);
const Testimonials = dynamic(
() => import("@/registry/sections/testimonials/testimonials-cards/TestimonialsCards"),
{ loading: () => <SectionSkeleton height={400} /> }
);
const PricingToggle = dynamic(
() => import("@/registry/sections/pricing/pricing-toggle/PricingToggle"),
{ loading: () => <SectionSkeleton height={600} /> }
);
The SectionSkeleton component reserves vertical space to prevent layout shifts while the section loads:
function SectionSkeleton({ height }: { height: number }) {
return (
<div
style={{
height,
background: "var(--color-background)",
animation: "pulse 1.5s ease-in-out infinite",
}}
/>
);
}
This approach reduces the initial JavaScript bundle and moves non-critical JS to subsequent network requests, improving Time to Interactive (TTI).
Section Animation Pattern
Each section should animate in when it enters the viewport. The canonical pattern uses Framer Motion's whileInView with viewport={{ once: true }}:
import { motion } from "framer-motion";
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
// Section header
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease }}
viewport={{ once: true, margin: "-80px" }}
>
<h2>Section heading</h2>
</motion.div>
The margin: "-80px" triggers the animation 80px before the element enters the viewport, so it's already animating when the user sees it — not after.
Putting It Together
A complete Next.js landing page structure:
app/[locale]/(public)/
├── page.tsx ← section composition
└── layout.tsx ← shared layout (navbar, footer)
src/registry/sections/
├── hero/hero-centered/
├── logo-carousel/logo-carousel-marquee/
├── features/features-grid/
├── testimonials/testimonials-cards/
├── pricing/pricing-toggle/
├── faq/faq-accordion/
└── cta/cta-centered/
Each section folder contains three files: the component (PascalCase.tsx), its metadata (meta.ts), and its mock data (mock.ts). This makes each section independently portable — copy the three files to any other project and it works.
Browse All Sections
The Incubator catalog has 449 section variants across 66 types — heroes, features, testimonials, pricing, FAQs, CTAs, footers, and more. Every variant is built with the same CSS token system and is copy-paste ready for Next.js projects.