Marketing pages
Edit the landing page, the pricing page and the shared marketing shell, and understand the warm-paper surface used by every public page.
The public side of Launch Now is a landing page at /, a pricing page at /pricing, and a set of content pages (blog, changelog, status, legal) that share one frame: the marketing shell. All the copy describes Pulse, a placeholder product analytics app. Replace it with your own product's story before you launch.
How it works
| File | Role |
|---|---|
app/page.tsx | The landing page. Composes the sections below and sets the home metadata. |
features/marketing/marketing-shell.tsx | MarketingShell: nav, footer, page rails and the .marketing theme scope. |
features/marketing/nav.tsx | Fixed top nav with anchor links and a mobile menu. |
features/marketing/nav-auth.tsx | Sign in / "Start free trial" for visitors, "Open Pulse" once signed in. |
features/marketing/footer.tsx | Footer columns, newsletter form, cookie settings link. |
features/marketing/brand.tsx | BrandMark (logo + name) and product, both read from siteConfig. |
features/marketing/primitives.tsx | Section, SectionHeader, Eyebrow, Headline, Lede, Accent, Highlight. |
features/marketing/page-rails.tsx | Decorative hatched rails on each side of the content column (xl screens only). |
app/pricing/page.tsx | The pricing page, backed by the plans in the database. |
features/pricing/* | Pricing cards, subscribe button and the feature comparison table. |
public/marketing/ | Images used by the landing page, auth layout and sidebar announcements. |
MarketingShell is used by /pricing, /blog, /changelog, /status and /legal. The landing page renders the same pieces (PageRails, MarketingNav, MarketingFooter) inline:
const MarketingShell = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => (
<div className="marketing flex min-h-screen w-full flex-col">
<PageRails />
<MarketingNav />
<main className={className ?? "marketing-layer flex-1"}>{children}</main>
<MarketingFooter />
</div>
)To add a new public page, wrap its content in MarketingShell and leave some top padding (the existing pages use pt-32) so the content clears the fixed nav.
The landing page
app/page.tsx renders the sections in this order:
| Section | File | Anchor |
|---|---|---|
| Hero | features/marketing/hero.tsx | |
| Partner logos | features/marketing/logos.tsx | |
| Features grid | features/marketing/features.tsx | #features |
| How it works | features/marketing/steps.tsx | #how |
| Testimonials | features/marketing/testimonials.tsx | #customers |
| Pricing | features/marketing/pricing.tsx | #pricing |
| FAQ | features/marketing/faq.tsx | #faq |
| Call to action | features/marketing/cta.tsx |
The nav links (features/marketing/nav.tsx) and the footer columns point at these anchors, so keep them in sync if you rename or remove a section.
export const metadata: Metadata = {
title: { absolute: `${siteConfig.name} — ${siteConfig.tagline}` },
description: siteConfig.description,
alternates: { canonical: "/" },
}
export default function RoutePage() {
return (
<div className="marketing flex min-h-screen w-full flex-col">
<PageRails />
<MarketingNav />
<main className="marketing-layer flex-1">
<HeroSection />
<LogosSection />
<FeaturesSection />
<StepsSection />
<TestimonialsSection />
<PricingSection />
<FaqSection />
<CtaSection />
</main>
<MarketingFooter />
</div>
)
}To remove a section, delete its line here. To reorder, move the lines.
Edit the copy
The product name, tagline and description come from siteConfig in lib/config/site-config.ts, so renaming Pulse there updates the nav, footer, hero paragraph, headings and page metadata at once. See Configuration.
Everything else is hardcoded in the section files as plain arrays at the top of each file, which makes it quick to edit:
hero.tsx: the "New" announcement pill, the headline, the two buttons and the "14-day free trial" line under them.logos.tsx: thelogosarray of placeholder partner wordmarks (from@thesvg/react).features.tsx: thefeaturesarray. Each item has anicon(fromlucide-react), atitle, abody, and optionallywide: truewith avisualto span two columns.steps.tsx: the threesteps.testimonials.tsx: thequotesarray. Avatars are placeholders fromi.pravatar.cc.pricing.tsx: theplansarray (see below).faq.tsx: thefaqsarray ofq/apairs.cta.tsx: the final call to action.
Headlines use two small helpers from primitives.tsx. Accent colors a phrase with the primary color, and Highlight turns a word into an inline chip with an optional icon:
<h1 className="...">
See how <Highlight icon={Users}>customers</Highlight> really use your{" "}
<Highlight icon={BarChart3}>product</Highlight>
</h1>New sections should use Section (hairline top rule, vertical spacing, scroll-mt-20 when it has an id) and SectionHeader (eyebrow, title, optional lede) to match the rhythm of the page:
<Section id="how">
<SectionHeader
eyebrow="How it works"
title={
<>
Up and running <Accent>before lunch.</Accent>
</>
}
lede="No data team, no week-long setup. Three steps and your first insight is on screen."
/>
{/* ... */}
</Section>The brand mark
BrandMark in features/marketing/brand.tsx renders a lucide-react Activity icon in a primary-colored square next to siteConfig.name. Swap the icon or replace the whole component with your logo; the nav and footer both use it.
Navigation and signed-in visitors
features/marketing/nav-auth.tsx reads the session with authClient.useSession():
- Visitors see Sign in (
/auth/signin) and Start free trial (/auth/signup). - Signed-in users see an Open Pulse button. The first time per tab session, a popover offers to go to the dashboard, with an Always open Pulse checkbox.
Checking that box sets the open_app cookie (lib/open-app-preference.ts). proxy.ts then redirects signed-in visitors from / straight to /dashboard. The proxy only runs on / when the open_app cookie equals 1 and there is no stay query parameter, so /?stay always shows the landing page.
export function proxy(request: NextRequest) {
if (getSessionCookie(request)) {
return NextResponse.redirect(new URL("/dashboard", request.url))
}
return NextResponse.next()
}
export const config = {
matcher: [
{
source: "/",
has: [{ type: "cookie", key: "open_app", value: "1" }],
missing: [{ type: "query", key: "stay" }],
},
],
}The footer
features/marketing/footer.tsx holds three link columns (Product, Resources, Company). The X / Twitter and GitHub links come from siteConfig.links, and the legal links point at /legal/terms, /legal/privacy, /legal/cookies and /legal/legal-notice.
The footer also renders:
- The newsletter signup form, only when
newsletterEnabled()returns true, which requiresRESEND_AUDIENCE_ID. See Newsletter. - A Cookie settings button that reopens the consent dialog. See Legal pages.
- An oversized, faded wordmark with the product name.
The pricing page
There are two pricing surfaces, and they are independent:
| Surface | File | Data source |
|---|---|---|
| Landing page section | features/marketing/pricing.tsx | Hardcoded plans array |
/pricing | app/pricing/page.tsx | The plan table, via getAllPlans() |
The landing section is static marketing copy (Starter, Pro, Business, with a monthly/yearly toggle) and every button links to /auth/signup. Update its plans array so it matches your real offer.
/pricing is a server component that loads the plans from the database (sorted by sortOrder) and the current user's plan, then renders one PricingCard per plan:
<PricingCard
key={plan.name}
plan={plan}
isCurrentPlan={currentPlanName === plan.name}
hasActiveSubscription={Boolean(activeSubscription)}
activeSubscriptionId={activeSubscription?.stripeSubscriptionId ?? null}
isAuthenticated={Boolean(session)}
successUrl="/account/billing?subscribed=1"
cancelUrl="/pricing"
returnUrl="/account/billing"
/>PricingSubscribeButton (features/pricing/pricing-subscribe-button.tsx) picks the right action for each card:
- Signed out: paid plans redirect to
/auth/signinwith acallbackURLback to the pricing page; the free plan links to/auth/signup. - Current plan with an active subscription: Manage subscription, linking to
/account/billing. - Another paid plan: calls
authClient.subscription.upgrade()and redirects to Stripe Checkout, or switches the plan in place when no redirect is needed.
Plan names, prices and features shown in the cards come from the database, so edit them where you manage plans. See Billing.
The comparison table
Below the cards, PricingComparison renders a feature-by-plan table. Its rows are hardcoded in features/pricing/pricing-comparison-data.ts, grouped in sections (Usage & limits, Collaboration, Analytics, Integrations & API, Security & compliance, Support). Each row has one value per plan column, in plan order:
export type ComparisonRow = {
feature: string
/** Short explanation shown in the info tooltip. */
info?: string
values: [ComparisonCell, ComparisonCell, ComparisonCell]
}A cell is true (included), false (not included) or a string to display. The table uses the first three plans from the database; with fewer than three plans in the database it falls back to fallbackPlans (Free, Pro, Ultra). The second column is highlighted as the popular plan. On phones, only the selected plan's column is shown, with a sticky switcher.
The marketing surface
Public pages do not follow the app's light/dark theme. The .marketing class in app/globals.css sets color-scheme: light and redefines every shadcn color token (--background, --foreground, --primary, --border and so on) with a fixed warm-paper palette, plus brand tokens such as --brand-ink, --brand-flare and --marketing-coral. Because the tokens are set on the wrapper, components inside it render in that palette whatever theme is active on <html>.
.marketing {
color-scheme: light;
/* brand */
--brand-ink: #1a1614;
--brand-flare: #f30;
/* ... */
/* shadcn surface */
--background: #f8f7f4;
--foreground: #1a1614;
--primary: #f30;
/* ... */
}The same scope also provides:
- A paper-grain texture (
.marketing::before). Content that should sit above it needs.marketing-layer. .marketing-plain, which keeps the tokens but drops the grain. The auth layout and/docsuse it.- Entrance animations (
.marketing-rise), the frosted nav (.marketing-nav-frost) and the raised primary button style, all disabled underprefers-reduced-motionwhere relevant.
Good to know: Only the CSS tokens are fixed. Tailwind dark: variants still follow the class on <html>, so a component with explicit dark: classes (like the status page badges) changes in dark mode even inside .marketing. Pressing the D key toggles the app theme anywhere outside a text field. To change the public palette, edit the .marketing block. See Theming.
Assets
Images live in public/marketing/:
| File | Used by |
|---|---|
dashboard-demo-light.png | Hero screenshot, sidebar announcements |
hero-poster.webp | CTA section background, the auth layout panel, sidebar announcements |
better-auth.svg | Not referenced in the code |
cloudflare.png | Not referenced in the code |
Replace dashboard-demo-light.png with a screenshot of your product. The hero renders it with next/image at a fixed width={2337} and height={1266}, so update those props if your image has a different aspect ratio.