Site
Product page (landing)
Product landing sections with capability cards, workflow cards, code examples, and an FAQ.
$ npx context-ds add product-landingUsage
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.