Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .impeccable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Design Context

### Users
Broad audience spanning Vercel customers adding durable workflows, general JS/TS developers evaluating workflow solutions, enterprise teams building production systems, and AI agent builders who need suspend/resume semantics. They arrive with a task in mind — integrating workflows into an existing app or understanding a specific pattern — and want to get back to coding quickly.

### Brand Personality
**Technical, reliable, clean.** The site should project engineering rigor and visual clarity — a Vercel-grade quality bar. Confidence comes from precision, not decoration.

### Emotional Goal
**Confidence & clarity.** A visitor should immediately feel "I can trust this and understand it quickly." The interface should reduce cognitive load, not add to it.

### Aesthetic Direction
- **Visual tone**: Minimal, precise, high-contrast. Geist typography carries the hierarchy.
- **References**: Vercel docs — the quality bar and feel to match.
- **Anti-references**: No playful/gamified elements (mascots, excessive gradients, startup energy). No dense enterprise wiki walls of text.
- **Theme**: Light and dark mode, pure white/black backgrounds, OKLch color system for perceptual uniformity. The primary blue (`oklch(57.61% 0.2508 258.23)`) is the only chromatic accent outside data visualization.

### Design Principles

1. **Content-first** — Every element earns its place by serving the reader. Remove decoration that doesn't clarify.
2. **Scannable hierarchy** — Use Geist font weights, spacing, and muted foregrounds so developers can skim to the answer. Dense text means something is missing structure, not that it needs illustration.
3. **System consistency** — Use shadcn/ui (New York) components and CSS variable tokens everywhere. Custom one-offs signal a gap in the system, not a design opportunity.
4. **Accessible by default** — Follow WCAG AA, respect `prefers-reduced-motion`, lean on Radix primitives for keyboard/screen-reader support. The defaults should be correct.
5. **Dark mode parity** — Both themes are first-class. Design in both, not one then the other. OKLch tokens and transparent borders keep contrast ratios stable across themes.

### Technical Stack (Design-Relevant)
- **Fonts**: Geist Sans + Geist Mono (variable weight, loaded as `--font-sans` / `--font-mono`)
- **Colors**: OKLch CSS custom properties, switched via `.dark` class (next-themes)
- **Components**: shadcn/ui New York style, CVA variants, Radix primitives
- **Layout**: Tailwind CSS v4, container queries for responsive components
- **Docs framework**: Fumadocs with shadcn theme preset
- **Animation**: Motion (Framer), used sparingly — entrance transitions only
- **Icons**: Lucide React
152 changes: 152 additions & 0 deletions docs/app/[lang]/cookbook/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import type { Metadata } from 'next';
import dynamic from 'next/dynamic';
import { notFound } from 'next/navigation';
import type { ComponentProps } from 'react';
import {
rewriteCookbookUrl,
rewriteCookbookUrlsInText,
} from '@/lib/geistdocs/cookbook-source';
import { AskAI } from '@/components/geistdocs/ask-ai';
import { CopyPage } from '@/components/geistdocs/copy-page';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from '@/components/geistdocs/docs-page';
import { EditSource } from '@/components/geistdocs/edit-source';
import { Feedback } from '@/components/geistdocs/feedback';
import { getMDXComponents } from '@/components/geistdocs/mdx-components';
import { OpenInChat } from '@/components/geistdocs/open-in-chat';
import { ScrollTop } from '@/components/geistdocs/scroll-top';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { getLLMText, getPageImage, source } from '@/lib/geistdocs/source';

const LazyCookbookExplorer = dynamic(
() =>
import('@/components/geistdocs/cookbook-explorer').then(
(mod) => mod.CookbookExplorer
),
{
loading: () => (
<div
role="status"
aria-live="polite"
className="rounded-2xl border border-border bg-card p-6 text-sm text-muted-foreground"
>
Loading cookbook explorer&hellip;
</div>
),
}
);

const Page = async ({ params }: PageProps<'/[lang]/cookbook/[[...slug]]'>) => {
const { slug, lang } = await params;

// Prepend 'cookbook' to resolve from the docs source
const resolvedSlug = slug ? ['cookbook', ...slug] : ['cookbook'];
const page = source.getPage(resolvedSlug, lang);

if (!page) {
notFound();
}

const publicUrl = rewriteCookbookUrl(page.url);
const publicPage = { ...page, url: publicUrl } as typeof page;

const markdown = rewriteCookbookUrlsInText(await getLLMText(page));
const MDX = page.data.body;

const RelativeLink = createRelativeLink(source, publicPage);
const PublicCookbookLink = (props: ComponentProps<typeof RelativeLink>) => {
const href =
typeof props.href === 'string'
? rewriteCookbookUrl(props.href)
: props.href;
return <RelativeLink {...props} href={href} />;
};

return (
<DocsPage
full={page.data.full}
tableOfContent={{
style: 'clerk',
footer: (
<div className="my-3 space-y-3">
<Separator />
<EditSource path={page.path} />
<ScrollTop />
<Feedback />
<CopyPage text={markdown} />
<AskAI href={publicUrl} />
<OpenInChat href={publicUrl} />
</div>
),
}}
toc={page.data.toc}
>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDX
components={getMDXComponents({
a: PublicCookbookLink,
Badge,
Step,
Steps,
Tabs,
Tab,
CookbookExplorer: () => <LazyCookbookExplorer lang={lang} />,
})}
/>
</DocsBody>
</DocsPage>
);
};

export const generateStaticParams = () => {
// Generate params for all cookbook pages
const allParams = source.generateParams();
return allParams
.filter((p) => Array.isArray(p.slug) && p.slug[0] === 'cookbook')
.map((p) => ({
...p,
slug: (p.slug as string[]).slice(1), // Remove 'cookbook' prefix
}));
};

export const generateMetadata = async ({
params,
}: PageProps<'/[lang]/cookbook/[[...slug]]'>) => {
const { slug, lang } = await params;
const resolvedSlug = slug ? ['cookbook', ...slug] : ['cookbook'];
const page = source.getPage(resolvedSlug, lang);

if (!page) {
notFound();
}

const publicPath = rewriteCookbookUrl(page.url);

const metadata: Metadata = {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
alternates: {
canonical: publicPath,
types: {
'text/markdown': `${publicPath}.md`,
},
},
};

return metadata;
};

export default Page;
13 changes: 13 additions & 0 deletions docs/app/[lang]/cookbook/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DocsLayout } from '@/components/geistdocs/docs-layout';
import { getCookbookTree } from '@/lib/geistdocs/cookbook-source';

const Layout = async ({
children,
params,
}: LayoutProps<'/[lang]/cookbook'>) => {
const { lang } = await params;

return <DocsLayout tree={getCookbookTree(lang)}>{children}</DocsLayout>;
};

export default Layout;
86 changes: 86 additions & 0 deletions docs/app/[lang]/cookbook/v1/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Link from 'next/link';
import { ArrowRightIcon } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
categoryLabels,
categoryOrder,
getRecipeHref,
getRecipesByCategory,
recipes,
type RecipeCategory,
} from '@/lib/cookbook-tree';

const totalCount = Object.keys(recipes).length;

const Page = async ({ params }: PageProps<'/[lang]/cookbook/v1'>) => {
const { lang } = await params;

return (
<div className="space-y-12 pb-16">
<header className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight">Cookbook</h1>
<p className="text-muted-foreground">
{totalCount} recipes across {categoryOrder.length} categories. Find
the right workflow pattern for your use case.
</p>
</header>

<nav aria-label="Category navigation" className="flex flex-wrap gap-2">
{categoryOrder.map((cat) => (
<a key={cat} href={`#${cat}`} className="inline-flex">
<Badge variant="outline">
{categoryLabels[cat]}{' '}
<span className="ml-1 text-muted-foreground">
{getRecipesByCategory(cat).length}
</span>
</Badge>
</a>
))}
</nav>

{categoryOrder.map((cat) => {
const catRecipes = getRecipesByCategory(cat);
return (
<section key={cat} id={cat} className="scroll-mt-20 space-y-4">
<div className="flex items-baseline gap-3">
<h2 className="text-xl font-semibold">{categoryLabels[cat]}</h2>
<span className="text-sm text-muted-foreground">
{catRecipes.length} recipes
</span>
</div>

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{catRecipes.map((recipe) => (
<Link
key={recipe.slug}
href={getRecipeHref(lang, recipe.slug)}
className="group"
>
<Card className="h-full transition-colors hover:border-primary/40">
<CardHeader>
<CardTitle className="flex items-center justify-between text-sm">
{recipe.title}
<ArrowRightIcon className="size-3.5 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
</CardTitle>
<CardDescription className="line-clamp-2">
{recipe.description}
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
</section>
);
})}
</div>
);
};

export default Page;
Loading
Loading