Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
45ab938
feat(registry): per-family landing pages
bntvllnt Jun 27, 2026
0db5dc9
chore: re-trigger preview deploy
bntvllnt Jun 27, 2026
d40bd5c
feat(ui,registry): link sidebar families to their homepage
bntvllnt Jun 27, 2026
d179b53
feat(registry): inline previews for the remaining 167 components
bntvllnt Jun 28, 2026
434803b
feat(registry): shared ComponentCard + live previews on the AI hub
bntvllnt Jun 30, 2026
4ffdeaa
fix(registry): move AI components out of the learning family
bntvllnt Jun 30, 2026
c662555
fix(registry): scale gallery previews to fit their card (no clipping)
bntvllnt Jul 1, 2026
255935f
feat(registry): SEO copy + FAQ on every family page
bntvllnt Jul 2, 2026
e6f539c
feat(registry): maximize family-page SEO (branded title + keywords)
bntvllnt Jul 2, 2026
9554800
fix(registry): dedupe Organization/WebSite JSON-LD (rendered twice)
bntvllnt Jul 2, 2026
0ad652f
feat(registry): consolidate all family landings under /families
bntvllnt Jul 2, 2026
4762bd3
fix(registry): actually dedupe Org/WebSite JSON-LD
bntvllnt Jul 2, 2026
6325f65
feat(registry): unify every family landing on one grouped SEO template
bntvllnt Jul 2, 2026
dce9f03
test(registry): enforce family-groups↔registry integrity + fix stale …
bntvllnt Jul 2, 2026
00dac44
Merge remote-tracking branch 'origin/main' into feat/family-homepage
bntvllnt Jul 2, 2026
9c26dba
chore(registry): regenerate metadata after merge + changelog
bntvllnt Jul 2, 2026
c72901b
Merge remote-tracking branch 'origin/main' into feat/family-homepage
bntvllnt Jul 2, 2026
2e44244
fix(registry): re-apply AI recategorization + group new typography pr…
bntvllnt Jul 2, 2026
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ Release automation can regenerate this file from Conventional Commits with

### Added

- **Component family landing pages** - every component family has a standalone,
SEO-oriented landing at `/families/[category]`, plus a `/families` index. One
shared template renders a hero with CTAs, per-family SEO sub-groups with
editorial copy and live component previews, an agent-readable surface
(`/llms.txt`, `/llms-full.txt`, `/r/<name>.json`), and a FAQ. Each page ships
`CollectionPage`/`ItemList`, `FAQPage`, and `BreadcrumbList` JSON-LD and is
listed in the sitemap.
- **Typography foundation primitives** — `Text`, `Heading`, `Display`, and
`Prose` in the `core` family. Font family (`--font-sans` + a new
`--font-display`), heading/display weight (`--font-weight-heading`,
Expand All @@ -23,6 +30,10 @@ Release automation can regenerate this file from Conventional Commits with

### Changed

- **AI family consolidated onto the shared template** - the bespoke `/ai` hub is
gone; `/ai` now permanently redirects to `/families/ai`, which renders through
the same family template. AI components are recategorized out of the
`learning` family into `ai`.
- `HeadingProps` exported from `@vllnt/ui` now refers to the `Heading` primitive
(adds optional `level`/`size`/`ref`); the plain heading-element alias is
available as `TypographyHeadingProps`. (#465)
Expand Down
17 changes: 16 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ Convention: phases are kebab-case outcome slugs, ordered DONE → ACTIVE → PLA

---

## family-pages [ACTIVE — code-complete on `feat/family-homepage`, pending merge]

**Goal:** A per-family landing at `/families/[category]` — the SEO / agent-surface destination for the Family breadcrumb crumb (#461) and the sidebar drill-down.
**Exit criteria:** 12 family landings on one shared `/families/[category]` template (`ai` included), each hero + CTAs + count + SEO sub-groups (`family-groups.ts`) + editorial copy + FAQ + agent surface; sitemap'd; `CollectionPage`/`ItemList` + `FAQPage` JSON-LD; `/components` headings + component-page family crumb link in via `familyPath`.
**Verify:** `/families/form` renders the Form grid + FAQ; `/ai` permanently redirects to `/families/ai`; Google Rich Results clean on a family page. Moves to the DONE cluster on merge to main + canary publish.

- [x] family-pages.1 `/families/[category]` route: hero + description + count + data-driven grid, zero nav maintenance — #463
- [x] family-pages.2 Internal links: `/components` family headings + component-page family crumb → family page via `familyPath` — #463
- [x] family-pages.3 sitemap `familyRoutes()` + `collectionPageLd` (CollectionPage/ItemList) + `faqPageLd` — #463
- [x] family-pages.4 `ai` reconciled: consolidated under `/families/ai` (shared template, grouped), `/ai` → permanent redirect, `familyPath` helper — #463
- [x] family-pages.5 Per-family SEO copy + FAQ (`family-copy.ts`) + branded titles / keywords — #463
- [x] family-pages.6 Validate: e2e `family-homepage.spec.ts` + unit `component-categories.test.ts` (E2E)
- [x] family-pages.7 One shared landing template for every family incl. `ai` (hero + CTAs + agent surface); per-family SEO sub-groups (`family-groups.ts`); `/families` index — #463

## component-sidebar [ACTIVE]

**Goal:** Navigate 309 components via a single-pane **drill-down** — families list ⇄ one family's components — synced to the breadcrumb, so the sidebar never shows more than one family at once.
Expand Down Expand Up @@ -269,6 +283,7 @@ Falls out of API-first (`studio.11`) — registration is just another API — pl
Unscheduled — pull into a phase when prioritized.

- **react-doctor-90** — push score 71 → 90 (sweep issues closed but target not fully met; see `codebase-health` caveat).
- **typography-primitives** (#465) — foundation `Text` / `Heading` / `Display` (/`Prose`) with token-driven font-family, heading-weight, and type-scale, all theme-overridable. Feasible (~12–18 files; an existing `typography` component — H1–H4/P/Lead/… — to model or extend), **but needs 3 triage decisions before scheduling**: (1) `--font-display` is a net-new token → conflicts with "no new design tokens beyond DESIGN.md baseline" (Out of scope) — resolve by adding a display family to the DESIGN.md baseline, or drop `--font-display` and map `Display` to `--font-sans`; (2) wire `--font-sans` as the actual default — today the body font is `font-mono` (`packages/ui/src/styles.css`), not sans, so "default stays semibold sans" isn't literally true; (3) reconcile the default heading weight — existing `typography.tsx` h1 uses `font-extrabold` (800) vs DESIGN.md's 600 semibold.
- **ai-elements-voice** (6) — AudioPlayer, MicSelector, Persona, SpeechInput, Transcription, VoiceSelector.
- **ai-elements-ide** (9) — Commit, EnvironmentVariables, FileTree, PackageInfo, SchemaDisplay, Snippet, StackTrace, Terminal, TestResults.
- **ai-elements-canvas** (5) — Canvas, Connection, Controls, Node, Panel (verify against existing `@xyflow`-based components first).
Expand Down Expand Up @@ -508,7 +523,7 @@ Semver discipline starts hard at `1.0.0`. Until then, `0.x.0` may include breaki
- React Compiler adoption.
- Right-to-left language support.
- Backend services for `/report` or `/request-component` (prefilled GitHub URLs — no backend).
- New design tokens beyond DESIGN.md baseline.
- New design tokens beyond DESIGN.md baseline (this is what gates `typography-primitives` #465's `--font-display` — see Later).
- Versioned docs (deferred until breaking changes appear).
- Component analytics (privacy-respecting telemetry — separate decision).

Expand Down
253 changes: 12 additions & 241 deletions apps/registry/app/[locale]/ai/page.tsx
Original file line number Diff line number Diff line change
@@ -1,253 +1,24 @@
import { Sidebar } from "@vllnt/ui";
import { ArrowRight, Sparkles, Terminal } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { permanentRedirect } from "next/navigation";
import { setRequestLocale } from "next-intl/server";

import { Footer } from "@/components/footer/footer";
import type { Locale } from "@/i18n/routing";
import {
AI_COMPONENT_GROUPS,
getAiComponentSlugs,
resolveAiComponent,
} from "@/lib/ai-seo";
import { breadcrumbLd, faqPageLd, jsonLdScriptAttributes } from "@/lib/jsonld";
import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og";
import { canonical, languageAlternates, localizePathname } from "@/lib/seo";
import { getSidebarSections } from "@/lib/sidebar-sections";
import { type Locale, routing } from "@/i18n/routing";
import { localizePathname } from "@/lib/seo";

type Props = {
readonly params: Promise<{ locale: Locale }>;
};

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai";
const PATHNAME = "/ai";

const TITLE = "AI Agent UI Components — chat, streaming, tools, citations";
const DESCRIPTION =
"The open-source UI design system for AI agents and AI-first apps. React components for AI chat, streaming text, tool calls, citations, agent activity, and artifacts. Install via the shadcn CLI.";

const FAQ = [
{
answer:
"An agent-first design system gives you the UI primitives that AI products actually need — chat input, message bubbles, streaming text, tool-call displays, source citations, agent activity timelines, and artifacts — and also ships every component as machine-readable JSON so AI coding agents can install it without scraping HTML. VLLNT UI is open source (MIT) and installs with the shadcn CLI.",
question: "What is an agent-first UI design system?",
},
{
answer:
"At minimum: AI Chat Input for the composer, AI Message Bubble to render each turn, and AI Streaming Text for token-by-token output. Add AI Tool Call Display and Agent Activity for agentic flows, AI Source Citation for RAG, and AI Artifact for canvas-style output.",
question: "Which components do I need to build an AI chat app in React?",
},
{
answer:
"Three surfaces: /llms.txt (a concise index per the llmstxt.org spec), /llms-full.txt (the full registry context in one fetch), and /r/<name>.json (a machine-readable descriptor per component with props, accessibility schema, and examples). Coding agents like Claude, Cursor, Cline, and Continue read these directly.",
question: "How do AI agents consume VLLNT UI?",
},
{
answer:
"Yes. VLLNT UI is open source under the MIT license. You own the source after install, with no backend and no tracking.",
question: "Is VLLNT UI free to use?",
},
];

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const ogParameters = {
description: DESCRIPTION,
title: TITLE,
type: "page" as const,
};

return {
alternates: {
canonical: canonical(PATHNAME, locale),
languages: languageAlternates(PATHNAME),
},
description: DESCRIPTION,
openGraph: generateOGMetadata(ogParameters, {
locale,
pathname: PATHNAME,
}),
title: `${TITLE} | VLLNT UI`,
twitter: generateTwitterMetadata(ogParameters),
};
}

function ComponentCard({ locale, slug }: { locale: Locale; slug: string }) {
const resolved = resolveAiComponent(slug);
if (!resolved) {
return null;
}
return (
<li>
<Link
className="group flex h-full flex-col rounded-lg border border-border p-5 hover:border-foreground/40"
href={localizePathname(`/components/${resolved.name}`, locale)}
>
<p className="font-medium">{resolved.title}</p>
<p className="mt-2 flex-1 text-sm text-muted-foreground">
{resolved.description}
</p>
<span className="mt-4 inline-flex items-center gap-1 text-xs font-medium text-foreground">
View component
<ArrowRight className="size-3 transition-transform group-hover:translate-x-0.5" />
</span>
</Link>
</li>
);
export function generateStaticParams(): { locale: Locale }[] {
return routing.locales.map((locale) => ({ locale }));
}

export default async function AiHubPage({ params }: Props) {
/**
* The AI hub moved to `/families/ai` (all family landings now live under
* `/families`). Permanently redirect the old `/ai` URL to preserve its SEO
* equity and any external links.
*/
export default async function AiRedirect({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);

const componentCount = getAiComponentSlugs().length;

return (
<>
<script
{...jsonLdScriptAttributes([
breadcrumbLd([
{ name: "Home", url: SITE_URL },
{ name: "AI components", url: `${SITE_URL}${PATHNAME}` },
]),
faqPageLd(FAQ),
])}
/>
<Sidebar sections={getSidebarSections(undefined, locale)} />
<main className="flex-1 overflow-y-auto bg-background">
{/* Hero / manifesto */}
<section className="border-b border-border">
<div className="mx-auto max-w-7xl px-4 py-20 lg:px-8">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Sparkles className="size-4" />
UI for AI agents
</div>
<h1 className="mt-3 text-4xl font-semibold leading-tight md:text-5xl">
The UI design system for AI agents and AI-first apps.
</h1>
<p className="mt-6 max-w-2xl text-lg text-muted-foreground">
Every AI product needs the same surfaces — a chat composer,
message bubbles, streaming output, tool calls, citations, agent
activity, and artifacts. VLLNT UI ships {componentCount} of them
as accessible React components you own after install, each also
readable by AI coding agents as JSON.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
className="inline-flex h-11 items-center gap-2 rounded-md bg-foreground px-5 text-sm font-medium text-background hover:opacity-90"
href={localizePathname("/build/ai-chat-ui", locale)}
>
Build an AI chat UI
<ArrowRight className="size-4" />
</Link>
<Link
className="inline-flex h-11 items-center gap-2 rounded-md border border-border px-5 text-sm font-medium hover:bg-muted"
href={localizePathname("/docs/agents", locale)}
>
How agents consume the registry
</Link>
</div>
</div>
</section>

{/* Component family grouped by job-to-be-done */}
<section className="border-b border-border">
<div className="mx-auto max-w-7xl px-4 py-16 lg:px-8">
<h2 className="text-2xl font-semibold">The AI component family</h2>
<p className="mt-2 max-w-2xl text-muted-foreground">
Grouped by the job they do in an AI product. Mix and match — every
component installs independently with the shadcn CLI.
</p>

<div className="mt-10 space-y-12">
{AI_COMPONENT_GROUPS.map((group) => (
<div key={group.heading}>
<h3 className="text-lg font-semibold">{group.heading}</h3>
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
{group.blurb}
</p>
<ul className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{group.slugs.map((slug) => (
<ComponentCard key={slug} locale={locale} slug={slug} />
))}
</ul>
</div>
))}
</div>
</div>
</section>

{/* Agent surface */}
<section className="border-b border-border bg-muted/30">
<div className="mx-auto max-w-7xl px-4 py-16 lg:px-8">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Terminal className="size-4" />
Readable by agents
</div>
<h2 className="mt-2 text-2xl font-semibold">
Your AI agent can install these too.
</h2>
<p className="mt-4 max-w-2xl text-lg text-muted-foreground">
The registry is exposed as structured data, so coding agents pick
the right component without scraping HTML.
</p>
<div className="mt-8 grid gap-4 md:grid-cols-3">
<a
className="group rounded-lg border border-border p-5 hover:border-foreground/40"
href="/llms.txt"
rel="noreferrer"
target="_blank"
>
<p className="font-mono text-sm">/llms.txt</p>
<p className="mt-3 text-sm text-muted-foreground">
Concise index per the llmstxt.org spec — sections, links,
descriptions.
</p>
</a>
<a
className="group rounded-lg border border-border p-5 hover:border-foreground/40"
href="/llms-full.txt"
rel="noreferrer"
target="_blank"
>
<p className="font-mono text-sm">/llms-full.txt</p>
<p className="mt-3 text-sm text-muted-foreground">
Full registry context in one fetch — docs plus every component
descriptor.
</p>
</a>
<Link
className="group rounded-lg border border-border p-5 hover:border-foreground/40"
href={localizePathname("/docs/agents", locale)}
>
<p className="font-mono text-sm">/r/&lt;name&gt;.json</p>
<p className="mt-3 text-sm text-muted-foreground">
Per-component descriptor: props, accessibility schema, and
usage examples.
</p>
</Link>
</div>
</div>
</section>

{/* FAQ */}
<section>
<div className="mx-auto max-w-7xl px-4 py-16 lg:px-8">
<h2 className="text-2xl font-semibold">Frequently asked</h2>
<dl className="mt-8 space-y-8">
{FAQ.map((item) => (
<div className="max-w-3xl" key={item.question}>
<dt className="font-medium">{item.question}</dt>
<dd className="mt-2 text-muted-foreground">{item.answer}</dd>
</div>
))}
</dl>
</div>
</section>

<Footer />
</main>
</>
);
permanentRedirect(localizePathname("/families/ai", locale));
}
4 changes: 2 additions & 2 deletions apps/registry/app/[locale]/build/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default async function UseCasePage({ params }: Props) {
<p className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
<Link
className="hover:text-foreground"
href={localizePathname("/ai", locale)}
href={localizePathname("/families/ai", locale)}
>
UI for AI agents
</Link>
Expand Down Expand Up @@ -154,7 +154,7 @@ export default async function UseCasePage({ params }: Props) {
<div className="mt-14 border-t border-border pt-8">
<Link
className="inline-flex items-center gap-1 font-medium text-foreground underline"
href={localizePathname("/ai", locale)}
href={localizePathname("/families/ai", locale)}
>
Browse all AI agent components
<ArrowRight className="size-4" />
Expand Down
15 changes: 13 additions & 2 deletions apps/registry/app/[locale]/components/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { registry } from "@/lib/registry";
import { canonical, languageAlternates, localizePathname } from "@/lib/seo";
import { oembedUrl, withRef } from "@/lib/share";
import {
familyPath,
getCategoryForComponent,
getSidebarSections,
groupedComponents,
Expand Down Expand Up @@ -242,7 +243,17 @@ export default async function ComponentPage(props: Props) {
href: localizePathname("/components", locale),
label: "Components",
},
...(familyGroup ? [{ label: familyGroup.label }] : []),
...(familyGroup
? [
{
href: localizePathname(
familyPath(familyGroup.category),
locale,
),
label: familyGroup.label,
},
]
: []),
{ label: displayTitle },
]}
/>
Expand Down Expand Up @@ -280,7 +291,7 @@ export default async function ComponentPage(props: Props) {
</p>
<Link
className="mt-4 inline-flex items-center gap-1 text-sm font-medium text-foreground underline"
href={localizePathname("/ai", locale)}
href={localizePathname("/families/ai", locale)}
>
Browse all AI agent components
<ExternalLink className="size-3" />
Expand Down
Loading
Loading