Launch NowDocs

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

FileRole
app/page.tsxThe landing page. Composes the sections below and sets the home metadata.
features/marketing/marketing-shell.tsxMarketingShell: nav, footer, page rails and the .marketing theme scope.
features/marketing/nav.tsxFixed top nav with anchor links and a mobile menu.
features/marketing/nav-auth.tsxSign in / "Start free trial" for visitors, "Open Pulse" once signed in.
features/marketing/footer.tsxFooter columns, newsletter form, cookie settings link.
features/marketing/brand.tsxBrandMark (logo + name) and product, both read from siteConfig.
features/marketing/primitives.tsxSection, SectionHeader, Eyebrow, Headline, Lede, Accent, Highlight.
features/marketing/page-rails.tsxDecorative hatched rails on each side of the content column (xl screens only).
app/pricing/page.tsxThe 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:

features/marketing/marketing-shell.tsx
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:

SectionFileAnchor
Herofeatures/marketing/hero.tsx
Partner logosfeatures/marketing/logos.tsx
Features gridfeatures/marketing/features.tsx#features
How it worksfeatures/marketing/steps.tsx#how
Testimonialsfeatures/marketing/testimonials.tsx#customers
Pricingfeatures/marketing/pricing.tsx#pricing
FAQfeatures/marketing/faq.tsx#faq
Call to actionfeatures/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.

app/page.tsx
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: the logos array of placeholder partner wordmarks (from @thesvg/react).
  • features.tsx: the features array. Each item has an icon (from lucide-react), a title, a body, and optionally wide: true with a visual to span two columns.
  • steps.tsx: the three steps.
  • testimonials.tsx: the quotes array. Avatars are placeholders from i.pravatar.cc.
  • pricing.tsx: the plans array (see below).
  • faq.tsx: the faqs array of q / a pairs.
  • cta.tsx: the final call to action.
The logos, testimonials and FAQ answers are placeholders that make claims about a product that does not exist (partners, customers, data residency, script size). Replace or remove them before you go live.

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:

features/marketing/hero.tsx
<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:

features/marketing/steps.tsx
<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.

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.

proxy.ts
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" }],
    },
  ],
}

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 requires RESEND_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:

SurfaceFileData source
Landing page sectionfeatures/marketing/pricing.tsxHardcoded plans array
/pricingapp/pricing/page.tsxThe 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:

app/pricing/page.tsx
<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/signin with a callbackURL back 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:

features/pricing/pricing-comparison-data.ts
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>.

app/globals.css
.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 /docs use it.
  • Entrance animations (.marketing-rise), the frosted nav (.marketing-nav-frost) and the raised primary button style, all disabled under prefers-reduced-motion where 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/:

FileUsed by
dashboard-demo-light.pngHero screenshot, sidebar announcements
hero-poster.webpCTA section background, the auth layout panel, sidebar announcements
better-auth.svgNot referenced in the code
cloudflare.pngNot 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.

Next steps