Product page (landing)Design system
Site

Product page (landing)

Product landing sections with capability cards, workflow cards, code examples, and an FAQ.

npx context-ds add product-landing
On the site

Usage

Use it for

The main product pages (Scrape, Map, Batches, Monitors, Answers, Brand, Style guide). Open with the site's Hero and LogoStrip, then ProductStorySection (capabilities), ProductIntegration, ProductStorySection columns={2} (workflows), ProductFaq, and close with ClosingCta. The alternatives pages reuse ProductFaq.

Avoid

  • Cards without the soft blue ring
  • A black pill in the code section (the docs link is the outline pill)
  • Opening every FAQ answer
  • Centered section headings

Dependencies: button, product-grid, section, lucide-react

Example

examples/product-landing.tsx
import { CodeBlock, CodeWindow, WindowCopyButton } from "@/components/site/code-window";import { ProductFaq, ProductIntegration, ProductStorySection } from "@/components/site/product-landing";import { ActionsStory } from "@/components/visuals/stories/actions-story";import { BatchStory } from "@/components/visuals/stories/batch-story";import { FreshnessStory } from "@/components/visuals/stories/freshness-story";import { OutputStory } from "@/components/visuals/stories/output-story";import { RagStory } from "@/components/visuals/stories/rag-story";const CODE = `import ContextDev from 'context.dev';const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });const page = await client.web.scrape({  url: 'https://linear.app',  formats: { markdown: true, screenshot: true },});`;export function Site() {  return (    <div className="w-full">      <ProductStorySection        title="Any page, any format."        accent="One request."        description="Choose the outputs your product needs and get them back together, from clean Markdown to a full-page screenshot."        items={[          { title: "Every output format", description: "Markdown, HTML, links, images, JSON, and screenshots from the same request.", story: <OutputStory className="w-full" /> },          { title: "Browser actions", description: "Click, scroll, or wait for a selector before the page is captured.", story: <ActionsStory className="w-full" /> },          { title: "Fresh or cached", description: "Read the live page, or reuse a recent copy when speed matters more.", story: <FreshnessStory className="w-full" /> },        ]}      />      <ProductIntegration        title="Scrape your first page"        accent="in a few lines."        description="Install the SDK, pass a URL and the formats you want, and get everything back in one response."        install="npm install context.dev"        docsHref="https://docs.context.dev"        code={          <CodeWindow label="TypeScript" actions={<WindowCopyButton value={CODE} />}>            <div className="overflow-x-auto">              <CodeBlock code={CODE} />            </div>          </CodeWindow>        }      />      <ProductStorySection        columns={2}        title="From a single page"        accent="to your next feature."        description="Feed your knowledge pipeline, or collect pages across a much larger job with the same API key."        items={[          { title: "Ground RAG in fresh content", description: "Crawl a sitemap, convert every page to clean Markdown, and send it straight into your embeddings.", story: <RagStory className="w-full" /> },          { title: "Run batches at scale", description: "Scrape thousands of URLs in one job, then collect the results when the batch completes.", story: <BatchStory className="w-full" /> },        ]}      />      <ProductFaq        id="scrape"        docsHref="https://docs.context.dev"        faqs={[          { question: "Does it render JavaScript?", answer: "Yes. Every request loads in a real browser, so single-page apps come back fully rendered." },          { question: "What does a scrape cost?", answer: "One credit per page for the base formats. Screenshots and JSON extraction add a few credits." },          { question: "Can I scrape PDFs?", answer: "Yes. Pass a PDF's URL and get its text back as Markdown." },        ]}      />    </div>  );}

Source

components/site/product-landing.tsx
import * as React from "react";import { Minus, Plus } from "lucide-react";import { Button } from "@/components/ui/button";import { ProductMedia } from "@/components/site/product-grid";import { SectionHeaderRule, SectionHeading, SiteSection } from "@/components/site/section";import { cn } from "@/lib/utils";// A product's landing page on the home page's design (the Scrape, Map, Batches, Monitors, Answers, Brand and Style// guide pages): the site's Hero, the logo strip, then these sections in the ruled column. Each opens with the site's// section heading over a header rule, and every card is white in the soft blue ring with a demo window at its foot. The// closing call to action is the site's ClosingCta.export type ProductStoryCardItem = { title: string; description: React.ReactNode; story?: React.ReactNode };/** A capability or workflow card: the title at h4, a line of muted text, and the demo window filling the rest. */function ProductStoryCard({ title, description, story, className }: ProductStoryCardItem & { className?: string }) {  return (    <li data-slot="product-story-card" className={cn("-m-2 flex rounded-card bg-ring-tint p-2", className)}>      <div className="flex h-full min-w-0 flex-1 flex-col gap-6 rounded-card bg-surface p-6 ring-1 ring-hairline md:p-8">        <div className="flex flex-col gap-3">          <h3 className="text-h4 leading-snug font-normal tracking-normal text-fg">{title}</h3>          <p className="text-body leading-normal text-fg-secondary">{description}</p>        </div>        <ProductMedia className="[&>div]:px-4 [&>div]:py-8 sm:[&>div]:px-9">{story}</ProductMedia>      </div>    </li>  );}/** * A section of story cards under a heading: three across for capabilities, two for workflows. `title` and `accent` * read as one sentence, the accent in brand blue. */function ProductStorySection({  title,  accent,  description,  items,  columns = 3,  className,  ...props}: Omit<React.ComponentProps<"section">, "title"> & {  title: React.ReactNode;  accent?: React.ReactNode;  description?: React.ReactNode;  items: ProductStoryCardItem[];  columns?: 2 | 3;}) {  return (    <SiteSection lines className={className} {...props}>      <SectionHeading title={title} accent={accent} description={description} />      <SectionHeaderRule />      <ul className={cn("grid grid-cols-1 gap-6", columns === 3 ? "lg:grid-cols-3" : "md:grid-cols-2")}>        {items.map((item) => (          <ProductStoryCard key={item.title} {...item} />        ))}      </ul>    </SiteSection>  );}/** The install command as a hairline pill: a faint prompt and the command in mono. */function InstallChip({ command, className }: { command: string; className?: string }) {  return (    <div data-slot="install-chip" className={cn("flex items-center gap-2 rounded-pill px-4 py-2 font-mono text-body-sm leading-5 text-fg-nav ring-1 ring-line-strong ring-inset", className)}>      <span aria-hidden="true" className="text-fg-faint">        $      </span>      <code>{command}</code>    </div>  );}/** * The code section: the heading, a line of muted text, the install command and the docs pill on the left; the code * sample (a CodeWindow) on the right. */function ProductIntegration({  title,  accent,  description,  install,  docsHref,  docsLabel = "Read the API reference",  code,  className,  ...props}: Omit<React.ComponentProps<"section">, "title"> & {  title: React.ReactNode;  accent?: React.ReactNode;  description?: React.ReactNode;  /** The install command, shown as a pill ("npm install context.dev"). */  install?: string;  docsHref?: string;  docsLabel?: React.ReactNode;  /** The code sample: a CodeWindow or CodeTabs. */  code: React.ReactNode;}) {  return (    <SiteSection lines className={cn("pt-16 md:pt-20", className)} {...props}>      <div className="grid items-center gap-10 lg:grid-cols-[minmax(0,26rem)_minmax(0,1fr)] lg:gap-16">        <SectionHeading title={title} accent={accent} description={description} className="min-w-0 gap-6">          {install && <InstallChip command={install} />}          {docsHref && (            <Button asChild variant="pill-outline">              <a href={docsHref} target="_blank" rel="noopener noreferrer">                {docsLabel}              </a>            </Button>          )}        </SectionHeading>        <div className="min-w-0">{code}</div>      </div>    </SiteSection>  );}export type ProductFaqItem = { question: string; answer: React.ReactNode };/** * The questions as one white card in the soft blue ring, hairlines between them, the first one open. Native details * share a `name`, so opening one closes the others with no script. */function ProductFaqList({ id, faqs, className }: { id: string; faqs: ProductFaqItem[]; className?: string }) {  return (    <div data-slot="product-faq-list" className={cn("-m-2 min-w-0 rounded-card bg-ring-tint p-2", className)}>      <div className="rounded-card bg-surface px-6 ring-1 ring-hairline md:px-8">        {faqs.map((faq, index) => (          <details key={faq.question} name={`${id}-questions`} open={index === 0} className="group border-t border-hairline py-5 first:border-0 md:py-6">            <summary className="flex cursor-pointer list-none items-center justify-between gap-5 rounded-sm text-body-lg leading-snug text-fg outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 [&::-webkit-details-marker]:hidden">              {faq.question}              <Plus aria-hidden="true" className="size-4 shrink-0 text-fg-brand group-open:hidden" />              <Minus aria-hidden="true" className="size-4 shrink-0 text-fg-brand group-not-open:hidden" />            </summary>            <div className="mt-3 text-body leading-relaxed text-fg-secondary">{faq.answer}</div>          </details>        ))}      </div>    </div>  );}/** The FAQ section: the heading and a docs line on the left, the ringed question card on the right. */function ProductFaq({  id,  faqs,  title = "A few things",  accent = "to know.",  description,  docsHref,  className,  ...props}: Omit<React.ComponentProps<"section">, "title" | "id"> & {  id: string;  faqs: ProductFaqItem[];  title?: React.ReactNode;  accent?: React.ReactNode;  /** Under the heading. Defaults to a line pointing at `docsHref`. */  description?: React.ReactNode;  docsHref?: string;}) {  const line =    description ??    (docsHref ? (      <>        More details in the{" "}        <a href={docsHref} target="_blank" rel="noopener noreferrer" className="rounded-sm text-fg-brand underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-focus-ring">          API documentation        </a>        .      </>    ) : undefined);  return (    <SiteSection lines className={cn("pt-16 md:pt-20", className)} {...props}>      <div className="grid gap-10 lg:grid-cols-[minmax(0,22rem)_minmax(0,1fr)] lg:gap-16">        <SectionHeading title={title} accent={accent} description={line} className="gap-6" />        <ProductFaqList id={id} faqs={faqs} />      </div>    </SiteSection>  );}export { ProductStorySection, ProductStoryCard, ProductIntegration, InstallChip, ProductFaq, ProductFaqList };

Reference: src/components/sections/products/product-landing-page.tsx (+ product-landing-content.ts, landing-faq-list.tsx); src/components/sections/home/product-demo-panel.tsx; src/components/sections/home/story-stage.tsx.