Explore Contextual UI site providers and components designed for humans, search engines, and AI agents. Use the interactive data source controls to customize component mock data, add or remove items, and see real-time UI rendering and Schema.org JSON-LD graph generation.
Learn how to install Contextual UI, define a single-source-of-truth schema, configure a data connector, and render headless SEO-ready components in your Next.js application in under 5 minutes.
Initialize a blank Next.js App Router project (or use an existing project) and install contextual-ui, the static connector, and zod.
Create data/site.schema.ts. Using defineSchema, register pre-built Schema.org registries (organization, website, webpage, navbar, footer, faq) or any custom Zod schemas.
import {
defineSchema,
organizationRegistry,
websiteRegistry,
webpageRegistry,
navbarRegistry,
faqRegistry,
footerRegistry,
} from 'contextual-ui/server';
import { z } from 'zod';
// 1. Define the Single Source of Truth (SSOT) schema
export const siteSchema = defineSchema({
organization: organizationRegistry(),
website: websiteRegistry(),
webpage: webpageRegistry(),
navbar: navbarRegistry(),
faq: faqRegistry(),
footer: footerRegistry(),
// Extend with custom typed Zod fields anytime:
announcement: {
schema: z.object({
enabled: z.boolean(),
message: z.string().describe('Announcement Banner Text'),
}),
},
});Create data/site.server.ts. Bind your schema with createContextualApp and a connector (static configuration, headless CMS, or database ORM).
import { siteSchema } from './site.schema';
import { staticConnector } from 'contextual-ui-connector-static';
import { createContextualApp, InferData } from 'contextual-ui/server';
const baseUrl = process.env.SITE_URL || 'https://example.com';
// 2. Configure a data connector (Static Config, Headless CMS, or Database)
const connector = staticConnector({
organization: {
name: 'Acme Corp',
url: baseUrl,
logo: '/images/logo.svg',
description: 'Creator of modern web tools.',
sameAs: ['https://github.com/acme', 'https://twitter.com/acme'],
},
website: {
name: 'Acme App',
url: baseUrl,
description: 'Headless UI with automated Schema.org SEO and Agentic AI graphs.',
},
webpage: [
{
id: 'home',
name: 'Acme App - Home',
url: '/',
description: 'Headless UI with automated Schema.org SEO and Agentic AI graphs.',
},
{
id: 'docs',
name: 'Acme App - Docs',
url: '/docs',
description: 'Documentation for Acme App.',
},
],
navbar: {
brand: { name: 'Acme', href: '/', logo: '/images/logo.svg' },
links: [
{ id: '1', label: 'Home', href: '/' },
{ id: '2', label: 'Docs', href: '/docs' },
],
},
faq: [
{
id: '1',
question: 'How does Contextual UI work?',
answer: 'It unifies your data layer, headless UI components, and Schema.org JSON-LD SEO graph.',
},
],
footer: {
brand: { name: 'Acme', href: '/' },
copyright: { holder: 'Acme Corp', year: 2026 },
},
});
// 3. Initialize the compiled Contextual App instance with baseUrl
export const siteApp = createContextualApp({
schema: siteSchema,
connector,
baseUrl,
});
export type SiteData = InferData<typeof siteSchema>;<WebPage app={siteApp} id="home"> scopes the Schema.org JSON-LD graph strictly to the current route—ensuring search engines only receive structured data for entities actually rendered on that page.In app/layout.tsx (Server Component), fetch shared data and wrap children in <ContextualSite data={data}>. This distributes validated site data (brand, links, copyright) to all layout components via React Context.
import type { Metadata } from 'next';
import { siteApp } from '@/data/site.server';
import { ContextualSite } from 'contextual-ui';
import { CustomNavbar } from '@/components/Navbar';
import { CustomFooter } from '@/components/Footer';
import './globals.css';
export const metadata: Metadata = {
title: 'My Next.js Application',
description: 'Built with Next.js and Contextual UI',
};
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Fetch validated data for global layout elements (Navbar, Footer, etc.)
const data = await siteApp.fetchData();
return (
<html lang="en" className="h-full">
<body className="min-h-full flex flex-col">
{/* ContextualSite distributes site data via React context to all client & server components */}
<ContextualSite data={data} className="min-h-full flex flex-col flex-1">
<CustomNavbar />
<div className="flex-1">{children}</div>
<CustomFooter />
</ContextualSite>
</body>
</html>
);
}Contextual UI components (<Navbar.Root>, <Footer.Root>) are headless client components. Because they sit inside <ContextualSite>, they automatically read brand and navigation data from context without needing explicit props!
'use client';
import { Navbar } from 'contextual-ui';
import type { NavbarData } from 'contextual-ui';
interface CustomNavbarProps {
data?: NavbarData; // Optional! Automatically read from ContextualSite if omitted
}
export function CustomNavbar({ data }: CustomNavbarProps = {}) {
// Contextual UI components are headless: style with Tailwind, CSS modules, or Radix
return (
<Navbar.Root data={data} className="fixed top-0 left-0 right-0 z-50 bg-white/90 dark:bg-black/90 backdrop-blur-md border-b border-zinc-200 dark:border-zinc-800">
<div className="max-w-7xl mx-auto flex items-center justify-between h-16 px-6">
<Navbar.Brand className="font-bold font-mono text-base flex items-center gap-2.5 text-zinc-900 dark:text-zinc-50" />
<Navbar.Links
className="hidden md:flex gap-6 items-center"
linkClassName="text-zinc-600 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50 no-underline text-sm font-medium transition-colors"
/>
<Navbar.Toggle className="md:hidden p-2 text-zinc-600 dark:text-zinc-400 focus:outline-none cursor-pointer rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" />
</div>
{/* Mobile Menu Dropdown */}
<Navbar.Menu
className="md:hidden bg-white/95 dark:bg-black/95 border-b border-zinc-200 dark:border-zinc-800 px-6 py-4 flex flex-col gap-2 shadow-xl"
linkClassName="text-zinc-700 dark:text-zinc-300 hover:text-zinc-950 dark:hover:text-zinc-50 text-base font-medium py-2 px-3 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800/60"
/>
</Navbar.Root>
);
}In app/page.tsx, export generateMetadata using siteApp.getMetadata('home') (zero duplication), and wrap your page in <WebPage app={siteApp} id="home"> to inject the route-specific Schema.org JSON-LD graph.
import { siteApp } from '@/data/site.server';
import { WebPage } from 'contextual-ui/server';
import { Faq } from 'contextual-ui';
// Zero duplication! Automatically pulls title, description, and canonical URL from SSOT
export const generateMetadata = () => siteApp.getMetadata('home');
export default async function HomePage() {
const data = await siteApp.fetchData();
return (
// Scopes the Schema.org JSON-LD graph specifically to this route
<WebPage app={siteApp} id="home">
<main className="max-w-4xl mx-auto px-6 py-12 space-y-10">
<header className="space-y-4">
<h1 className="text-4xl font-extrabold tracking-tight">Acme App</h1>
<p className="text-lg text-zinc-400">Headless UI with automated Schema.org SEO and Agentic AI graphs.</p>
</header>
{/* Headless FAQ Accordion automatically bound to schema data */}
<section className="space-y-4">
<h2 className="text-xl font-bold">Frequently Asked Questions</h2>
<Faq.Root className="border border-zinc-200 dark:border-zinc-800 rounded-xl p-6 shadow-sm">
{data?.faq?.map((item) => (
<Faq.Item key={item.id} id={item.id} className="mb-4 last:mb-0 border-b border-zinc-200 dark:border-zinc-800 last:border-b-0 pb-4 last:pb-0">
<Faq.Trigger className="bg-transparent border-none font-semibold text-base cursor-pointer text-left w-full hover:text-accent transition-colors py-1">
{item.question}
</Faq.Trigger>
<Faq.Content className="mt-2 text-zinc-400 text-sm leading-relaxed">
{item.answer}
</Faq.Content>
</Faq.Item>
))}
</Faq.Root>
</section>
</main>
</WebPage>
);
}Generate sitemap.xml and robots.txt in 3 lines each. Contextual UI automatically indexes all routes defined in your connector and manages AI crawler permissions (GPTBot, ClaudeBot, PerplexityBot).
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { siteApp } from '@/data/site.server';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Automatically derives canonical sitemap from connector routes
return siteApp.getSitemap({
exclude: ['/cms', '/cms/*'],
});
}
// app/robots.ts
import type { MetadataRoute } from 'next';
import { siteApp } from '@/data/site.server';
export default async function robots(): Promise<MetadataRoute.Robots> {
// Configures search & AI crawler permissions and auto-attaches sitemap & host
return siteApp.getRobots({
disallow: ['/cms', '/cms/'],
});
}Expose a machine-readable JSON-LD Knowledge Graph endpoint at app/api/graph.json/route.ts in 4 lines. AI Agents (Claude, ChatGPT, Perplexity) use this endpoint to understand your entire site hierarchy.
import { siteApp } from '@/data/site.server';
// Expose machine-readable Knowledge Graph for AI Agents, Perplexity & Claude
export const { GET } = siteApp.createGraphHandler({
includeAll: true, // Export all schema sections (or use excludeKeys / includeKeys)
graphOptions: {
flatten: true,
dedupeStrategy: 'merge',
},
});defineSchema allows you to compose pre-built, type-validated Schema.org registries and custom Zod schemas into a unified contract. Each registry automatically validates runtime data, generates compile-time TypeScript types, and compiles referentially linked Schema.org @graph JSON-LD nodes.
import {
defineSchema,
organizationRegistry,
websiteRegistry,
webpageRegistry,
navbarRegistry,
footerRegistry,
breadcrumbRegistry,
faqRegistry,
} from 'contextual-ui/server';
import { z } from 'zod';
export const siteSchema = defineSchema({
// Built-in Schema.org Registries
organization: organizationRegistry(),
website: websiteRegistry(),
webpage: webpageRegistry(),
navbar: navbarRegistry(),
footer: footerRegistry(),
breadcrumb: breadcrumbRegistry(),
faq: faqRegistry(),
forms: formRegistry(),
// Custom Typed Zod Schema Extension
announcement: {
schema: z.object({
enabled: z.boolean(),
message: z.string().describe('Announcement Banner Text'),
badge: z.string().optional(),
}),
},
});
// Automatically infer full TypeScript types with zero manual duplication:
export type SiteData = InferData<typeof siteSchema>;Select a registry below to inspect its Schema.org specification, field requirements, and usage examples.
Declares domain-level website metadata, site display title, description, canonical URL, and search action.
// data/site.schema.ts
import { defineSchema, websiteRegistry } from 'contextual-ui/server';
export const siteSchema = defineSchema({
website: websiteRegistry(),
});| Field | Type | Requirement | Schema.org Mapping | Description |
|---|---|---|---|---|
| name | string | Required | name | Primary display name of the website |
| url | string | Required | url | Canonical root domain URL |
| description | string | Optional | description | Website meta description for search engines |
| inLanguage | string | Optional | inLanguage | Language code (e.g. "en-US") |
| publisher | Reference | Optional | publisher | Cross-reference pointing to the Organization entity |
You are not limited to built-in registries. Any standard Zod object can be attached under any key inside defineSchema via { schema: z.object({...}) }. This custom data is automatically validated at runtime, typed via InferData<typeof siteSchema>, and supported in CMS dashboards and form generators.
// Extend with custom typed Zod schemas
export const siteSchema = defineSchema({
website: websiteRegistry(),
// Custom announcement banner
announcement: {
schema: z.object({
enabled: z.boolean(),
message: z.string().describe('Banner message text'),
badge: z.string().optional(),
link: z.string().url().optional(),
}),
},
// Custom blog posts collection
posts: {
schema: z.array(
z.object({
slug: z.string(),
title: z.string(),
publishedAt: z.string(),
author: z.string(),
})
),
},
});The root provider that coordinates domain-level data distribution to all contextual UI components. In single-page apps (SPAs), it compiles and injects the unified Schema.org JSON-LD @graph.
import { siteApp } from '@/data/site.server';
import { ContextualSite, Navbar, Faq, Footer } from 'contextual-ui';
// 1. Multi-Page Next.js (App Router)
// In layout.tsx: distributes data down to all pages
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const data = await siteApp.fetchData();
return (
<html lang="en">
<body>
<ContextualSite data={data}>
{children}
</ContextualSite>
</body>
</html>
);
}
// 2. Single Page Applications (SPAs / Landing Pages)
// In App.tsx: automatically compiles and injects the unified Schema.org JSON-LD graph
export function SinglePageApp() {
return (
<ContextualSite schema={siteSchema} data={siteData}>
<Navbar.Root />
<main>
<Faq.Root />
</main>
<Footer.Root />
</ContextualSite>
);
}| Field | Type | Requirement | Schema.org Mapping | Description |
|---|---|---|---|---|
| data | SiteData | Required | @graph | Domain-level data object providing website, navbar, footer, and FAQ configurations. |
| data.website | WebsiteData | Optional | WebSite | Site-level metadata including name, url, and meta description. |
| data.navbar | NavbarData | Optional | SiteNavigationElement | Navigation brand and menu links automatically inferred by <Navbar.Root />. |
| data.footer | FooterData | Optional | WPFooter | Footer structure, copyright, and social links automatically inferred by <Footer.Root />. |
| data.faq | FaqItem[] | Optional | FAQPage | FAQ question-answer pairs automatically inferred by <Faq.Root />. |
| graph | ContextualGraph | Optional | @graph | Pre-compiled Schema.org JSON-LD graph produced by siteApp.getGraph(). |
| children | ReactNode | Required | — | Child components rendered within ContextualSite context. |
The route-level React Server Component that coordinates page-level Schema.org metadata and automatically injects the canonical @graph script tag for that specific URL.
import { siteApp } from '@/data/site.server';
import { WebPage } from 'contextual-ui/server';
import { DocsClient } from './DocsClient';
export const generateMetadata = () => siteApp.getMetadata('docs');
export default async function DocsPage() {
const data = await siteApp.fetchData();
return (
<WebPage app={siteApp} id="docs">
<DocsClient data={data} />
</WebPage>
);
}| Field | Type | Requirement | Schema.org Mapping | Description |
|---|---|---|---|---|
| app | ContextualApp | Optional | @graph | ContextualApp instance to compile and inject the route-specific Schema.org JSON-LD graph. |
| name | string | Optional | WebPage.name | Route-specific page title/name for search engines and AI agents. |
| url | string | Optional | WebPage.url | Route-specific canonical pathname (e.g. "/docs"). |
| description | string | Optional | WebPage.description | Route-specific meta description. |
| graph | JsonLdGraphResult | Optional | @graph | Pre-computed Schema.org JSON-LD graph (optional explicit override). |
| disableJsonLdScript | boolean | Optional | — | Disables script tag rendering when set to true. |
The FAQ component organizes collapsible question-and-answer pairs with automatic Schema.org FAQPage structured data injection.
<Faq.Root data={faqData}>
{faqData.map((item) => (
<Faq.Item key={item.id} id={item.id}>
<Faq.Trigger>{item.question}</Faq.Trigger>
<Faq.Content>{item.answer}</Faq.Content>
</Faq.Item>
))}
</Faq.Root>| Field | Type | Requirement | Schema.org Mapping | Description |
|---|---|---|---|---|
| data | FaqItem[] | Required | mainEntity: Question[] | Array of FAQ question and answer items. |
| data[].id | string | Required | — | Unique identifier for the FAQ item. |
| data[].question | string | Required | Question.name | The question string for users and search indexing. |
| data[].answer | string | Required | Question.acceptedAnswer.text | The accepted answer text content. |
<AutoForm> unifies Headless CMS form definitions, dynamic in-memory Zod validation, and machine-readable Schema.org PotentialAction JSON-LD graphs for AI agents. Define your form structure in your CMS or connector, and render dynamic accessible UI without writing repetitive React field boilerplate.
Add forms: formRegistry() inside defineSchema. This sets up the runtime validation contract and links form entities into the single source of truth.
// 1. data/site.schema.ts
import { defineSchema, formRegistry } from 'contextual-ui/server';
export const siteSchema = defineSchema({
forms: formRegistry(), // Single Source of Truth for CMS forms
});Marketing teams or developers can define forms as JSON arrays in Sanity, Strapi, or static configs. Define endpoints, field types, labels, and validation rules without touching React JSX.
// 2. data/site.server.ts (or ingested from Headless CMS connector)
forms: [
{
id: 'contact-sales',
name: 'Contact Sales',
title: 'Get in Touch',
actionType: 'ContactAction',
endpoint: '/api/contact',
method: 'POST',
fields: [
{ name: 'name', type: 'text', label: 'Full Name', required: true, validation: { minLength: 2 } },
{ name: 'email', type: 'email', label: 'Work Email', required: true },
{
name: 'topic',
type: 'select',
label: 'Topic',
options: [
{ label: 'General Inquiry', value: 'general' },
{ label: 'Sales & Enterprise', value: 'sales' },
{ label: 'Technical Support', value: 'support' },
]
},
{ name: 'message', type: 'textarea', label: 'Message', required: true, validation: { minLength: 10 } },
],
submitLabel: 'Send Message',
successMessage: 'Thank you! Your message has been received.',
},
]Pass the connector forms payload to <AutoForm data={data.forms} formId="contact-sales" />. AutoForm dynamically compiles an in-memory Zod schema for blur & submit validation and renders the form fields. Custom UI slots can be passed via the components prop.
// 3. app/contact/page.tsx
import { AutoForm } from 'contextual-ui';
import { siteApp } from '@/data/site.server';
export default async function ContactPage() {
const data = await siteApp.fetchData();
return (
<div className="max-w-md mx-auto py-10">
{/* AutoForm builds dynamic Zod validation in-memory & renders inputs */}
<AutoForm
data={data.forms}
formId="contact-sales"
// Optional custom Tailwind component slots:
components={{
Input: (props) => (
<input className="w-full px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-white text-xs" {...props} />
),
Submit: ({ isSubmitting, children, ...props }) => (
<button className="w-full py-2.5 bg-accent hover:bg-accent-hover text-white font-medium rounded-lg text-xs" {...props}>
{isSubmitting ? 'Sending...' : children}
</button>
),
}}
/>
</div>
);
}The JSON-LD graph builder automatically maps the form entity to a Schema.org PotentialAction (e.g. ContactAction) with EntryPoint and PropertyValueSpecification fields. AI agents (Claude, Perplexity, OpenAI agents) can discover the API contract and interact programmatically without DOM clicking.
// 4. Automated Schema.org PotentialAction Graph for AI Agents
{
"@context": "https://schema.org",
"@type": "ContactAction",
"@id": "https://example.com/#action:form-contact-sales",
"name": "Contact Sales & Support",
"isPartOf": { "@id": "https://example.com/#webpage" },
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://example.com/api/contact",
"httpMethod": "POST",
"contentType": "application/json"
},
"object": [
{
"@type": "PropertyValueSpecification",
"valueName": "name",
"valueRequired": true,
"valueMinLength": 2
},
{
"@type": "PropertyValueSpecification",
"valueName": "email",
"valueRequired": true,
"valuePattern": "^.+@.+\\..+$"
},
{
"@type": "PropertyValueSpecification",
"valueName": "topic",
"valueRequired": true,
"valueOption": ["general", "sales", "support"]
},
{
"@type": "PropertyValueSpecification",
"valueName": "message",
"valueRequired": true,
"valueMinLength": 10
}
]
}The form below is generated directly from the starter kit's data.forms connector registry. Fill it out and submit to test real validation and the API POST handler.
Complete props contract for configuring dynamic forms, custom slot overrides, and submission callbacks.
| Prop | Type | Requirement | Description |
|---|---|---|---|
| data | FormData | Yes (or form) | Ingested forms data from connector/registry (single FormEntity or FormEntity[] array). |
| formId | string | Optional | Matches a specific form by its id when data contains multiple forms. |
| form | FormEntity | Optional | Explicit form entity object override (bypassing data lookup). |
| components | AutoFormCustomComponents | Optional | Custom UI slots for Form, Field, Label, Input, TextArea, Select, Checkbox, ErrorMessage, Submit, Section. |
| action | string | Optional | Overrides the form submit endpoint (defaults to form.endpoint). |
| method | 'POST' | 'GET' | 'PUT' | 'PATCH' | Optional | Overrides HTTP method (defaults to form.method or "POST"). |
| onSubmit | (values, form) => void | Promise<void> | Optional | Custom submit handler. If omitted, AutoForm performs a JSON POST fetch to the endpoint automatically. |
| onSuccess | (result) => void | Optional | Callback invoked after successful form submission. |
| onError | (error: ZodError) => void | Optional | Callback invoked when client-side validation fails. |
| submitLabel | string | Optional | Overrides the submit button text (defaults to form.submitLabel or "Submit"). |
The createForm factory generates headless, strictly type-safe React form components directly from a hardcoded Zod schema. Ideal for developer-centric custom forms with fixed field requirements, providing automatic blur validation, field name autocompletion, and zero-state boilerplate.
Define the shape and validation constraints using standard Zod types (z.string(), z.number(), z.enum()).
import { z } from 'zod';
// 1. Define your static Zod validation schema
export const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
message: z.string().min(10, 'Message must be at least 10 characters'),
});
export type ContactFormData = z.infer<typeof contactSchema>;Pass your schema into createForm(schema) to produce compound components strictly typed to your schema keys.
import { createForm } from 'contextual-ui';
import { contactSchema } from '@/schemas/contact';
// 2. Instantiate type-safe compound form components
export const ContactForm = createForm(contactSchema);Wrap input controls in <ContactForm.Field name="...">. Child components share context and validation automatically.
import { ContactForm } from '@/components/ContactForm';
export function ContactFormCard() {
return (
<ContactForm.Root
onSubmit={async (data) => {
// data is strictly typed: { name: string; email: string; message: string }
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}}
className="space-y-4 max-w-md mx-auto"
>
<ContactForm.Field name="name" className="space-y-1.5">
<ContactForm.Label className="block text-xs font-mono text-zinc-300">Full Name</ContactForm.Label>
<ContactForm.Input placeholder="Jane Doe" className="w-full px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-white" />
<ContactForm.ErrorMessage className="text-rose-400 text-xs font-mono block" />
</ContactForm.Field>
<ContactForm.Field name="email" className="space-y-1.5">
<ContactForm.Label className="block text-xs font-mono text-zinc-300">Email</ContactForm.Label>
<ContactForm.Input type="email" placeholder="jane@example.com" className="w-full px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-white" />
<ContactForm.ErrorMessage className="text-rose-400 text-xs font-mono block" />
</ContactForm.Field>
<ContactForm.Field name="message" className="space-y-1.5">
<ContactForm.Label className="block text-xs font-mono text-zinc-300">Message</ContactForm.Label>
<ContactForm.TextArea rows={3} placeholder="How can we help?" className="w-full px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-white" />
<ContactForm.ErrorMessage className="text-rose-400 text-xs font-mono block" />
</ContactForm.Field>
<ContactForm.Submit className="w-full py-2.5 bg-accent text-white font-medium rounded-lg">
Send Message
</ContactForm.Submit>
</ContactForm.Root>
);
}Validation runs on field blur and on form submit. Submitting state is automatically managed during async promises.
// Automatic blur & submit validation lifecycle
<ContactForm.Root
onSubmit={async (data) => {
// 1. safeParse passes -> data is typed & sanitized
// 2. isSubmitting state automatically activates
const res = await fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) });
if (!res.ok) throw new Error('Submission failed');
}}
onError={(error) => {
console.warn('Validation errors:', error.flatten().fieldErrors);
}}
>
{/* Field components */}
</ContactForm.Root>Test the static compound form below. Blur fields to test validation feedback.
Every component produced by createForm is completely headless and composable with Radix UI Slot (asChild) support.
| Subcomponent | Props / Context | Description |
|---|---|---|
| Form.Root | onSubmit, onError?, className?, id? | Top-level context provider for static forms. Manages state, errors, blur validation, and async lifecycle. |
| Form.Field | name: keyof Schema, className? | Scopes field context by name. Strictly type-checked against schema keys at compile time. |
| Form.Label | asChild?, className?, style? | Accessible <label> automatically bound to the input through the field htmlFor attribute. |
| Form.Input | asChild?, ...InputHTMLAttributes | Controlled input element bound to field value, onChange, onBlur validation, and data-invalid attribute. |
| Form.TextArea | asChild?, ...TextareaHTMLAttributes | Controlled multi-line textarea with automatic onBlur validation and data-invalid attribute binding. |
| Form.ErrorMessage | asChild?, className?, style? | Conditionally renders active validation error strings for the scoped field. |
| Form.Submit | asChild?, ...ButtonHTMLAttributes | Submit button automatically disabled while an async onSubmit promise is pending. |
| Form.Section | title?, description?, asChild?, className? | Semantic container for grouping related fields with an optional title and description header. |
Connectors decouple your data sources (Static JSON, Headless CMS, Database ORMs, or REST APIs) from your React UI components and SEO knowledge graphs. Any source that fulfills the simple { fetchData: () => Promise<T> } contract can be plugged into createContextualApp.
import { staticConnector } from 'contextual-ui-connector-static';
import { createContextualApp } from 'contextual-ui/server';
import { siteSchema } from './site.schema';
// 1. Static Configuration Connector (Built-in)
const connector = staticConnector({
website: {
name: 'Contextual UI Starter Kit',
url: 'https://contextual.site',
description: 'A headless UI and semantic SEO Knowledge Graph starter kit.',
},
navbar: {
brand: { name: 'Contextual UI', href: '/' },
links: [
{ id: '1', label: 'Home', href: '/' },
{ id: '2', label: 'Docs', href: '/docs' },
],
},
faq: [
{ id: '1', question: 'What is Contextual UI?', answer: 'A headless UI + SEO Knowledge Graph library.' },
],
});
export const siteApp = createContextualApp({
schema: siteSchema,
connector,
baseUrl: 'https://contextual.site',
});Next.js Metadata helper that eliminates duplication between your data connector, Schema.org JSON-LD graphs, and HTML <head> meta tags. Since siteApp already knows each page's title, description, canonical URL, and base URL from your Single Source of Truth (SSOT), siteApp.getMetadata(pageId) generates fully typed, route-accurate Next.js Metadata in a single line.
Pulls title, description, and canonical URL from the same schema powering <WebPage /> and /api/graph.json.
Automatically attaches metadataBase: new URL(baseUrl) and binds canonical alternates seamlessly across relative paths.
Auto-populates openGraph and twitter cards with brand logos, handles, and OpenGraph website types.
Select a page configured in your schema connector to inspect its generated Next.js metadata:
// app/privacy/page.tsx
import { siteApp } from '@/data/site.server';
import { WebPage } from 'contextual-ui/server';
// Zero duplication! Pulls title, description, and canonical from siteApp SSOT
export const generateMetadata = () => siteApp.getMetadata('privacy');
export default async function PrivacyPage() {
const data = await siteApp.fetchData();
return (
<WebPage app={siteApp} id="privacy">
<main className="max-w-4xl mx-auto px-6 py-12">
<h1 className="text-3xl font-bold">Privacy Policy - Contextual UI</h1>
<p className="mt-4 text-zinc-400">Privacy policy and data protection information for Contextual UI.</p>
</main>
</WebPage>
);
}| Parameter | Type | Requirement | Description |
|---|---|---|---|
| pageIdOrOptions | string | GetMetadataOptions | Optional | Page identifier (e.g. "privacy", "home") or options object. Defaults to "home" or root website when omitted. |
| overrides | Partial<Metadata> | Optional | Custom metadata overrides (e.g. title, openGraph images, twitter card, keywords, robots). |
| returns | Promise<Metadata> | Required | Next.js App Router-compatible Metadata object with metadataBase, title, description, alternates, openGraph, and twitter. |
Automated sitemap generator that derives route URLs directly from your connector schema (webpage: [...]). Eliminates maintaining hardcoded XML files or duplicate route lists. Provides typed Next.js App Router metadata, web-standard route handlers, and static XML formatting.
Automatically indexes every route configured in your schema connector with canonical URL formatting against baseUrl.
Defaults root (/) to priority 1.0 and daily crawl, while subpages default to 0.8 and weekly crawl.
Easily omit private paths via glob patterns (e.g. exclude: ['/cms', '/cms/*']) or custom filter functions.
Toggle exclusions and priorities to see the live output update dynamically:
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { siteApp } from '@/data/site.server';
const baseUrl = (process.env.SITE_URL || 'https://contextual.site').replace(/\/+$/, '');
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Derives all route items directly from your connector schema
return siteApp.getSitemap({
baseUrl,
exclude: ['/cms', '/cms/*'],
defaultPriority: 0.8,
});
}| Parameter | Type | Requirement | Description |
|---|---|---|---|
| options.baseUrl | string | Optional | Base canonical domain (e.g. "https://example.com"). Defaults to siteApp.baseUrl or data.website.url. |
| options.exclude | string[] | Optional | Paths or glob patterns to omit (e.g. ["/cms", "/cms/*", "/admin", "/studio*"]). |
| options.additionalRoutes | SitemapItem[] | Optional | Additional routes or dynamic records outside the primary schema to append with deduplication. |
| options.defaultPriority | number | Optional | Default priority (0.0 to 1.0). Root "/" defaults to 1.0, subpages default to 0.8. |
| options.defaultChangeFrequency | SitemapChangeFrequency | Optional | "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never". Root defaults to "daily", subpages to "weekly". |
| returns (getSitemap) | Promise<MetadataRoute.Sitemap> | Required | Array of items typed directly for Next.js App Router app/sitemap.ts export. |
| returns (generateSitemapXml) | Promise<string> | Required | RFC-compliant XML string conforming to Sitemaps 0.9 specification with entity escaping. |
| returns (createSitemapHandler) | { GET: (req: Request) => Promise<Response> } | Required | Standard Web Response route handler for Next.js, Remix, Astro, or TanStack. |
Configures search engine indexing policies, automatically links your canonical sitemap and host, and provides first-class controls for LLM search bots (PerplexityBot) and AI training crawlers (GPTBot, ClaudeBot, Google-Extended).
Automatically extracts domain host and advertises Sitemap: https://.../sitemap.xml based on your connector URL.
One-line toggle to allow or disallow all major AI bots, with granular per-bot override capabilities.
Drop into Next.js App Router app/robots.ts, or export standard Web API handlers for Remix, Astro, and TanStack.
Configure AI crawler policies to see the generated rules in real time:
// app/robots.ts
import type { MetadataRoute } from 'next';
import { siteApp } from '@/data/site.server';
const baseUrl = (process.env.SITE_URL || 'https://contextual.site').replace(/\/+$/, '');
export default async function robots(): Promise<MetadataRoute.Robots> {
// Configures crawler rules and auto-binds sitemap & host
return siteApp.getRobots({
baseUrl,
disallow: ['/cms', '/cms/'],
});
}| Parameter | Type | Requirement | Description |
|---|---|---|---|
| options.baseUrl | string | Optional | Base canonical domain. Used to formulate "Sitemap: ${baseUrl}/sitemap.xml" and "Host: hostname". |
| options.disallow | string | string[] | Optional | Paths forbidden for standard crawlers (e.g. ["/cms", "/cms/"]). |
| options.allow | string | string[] | Optional | Paths explicitly permitted (defaults to "/"). |
| options.ai | RobotsAiOptions | Optional | Preset policies for AI training and search bots (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, Bytespider). |
| options.sitemap | string | string[] | boolean | Optional | Sitemap URL directive. Defaults to true ("${baseUrl}/sitemap.xml"). Set false to suppress. |
| returns (getRobots) | Promise<MetadataRoute.Robots> | Required | Structured object typed directly for Next.js App Router app/robots.ts export. |
| returns (generateRobotsTxt) | Promise<string> | Required | RFC 9309 compliant robots.txt plain-text representation. |
| returns (createRobotsHandler) | { GET: (req: Request) => Promise<Response> } | Required | Standard Web Response route handler for Next.js, Remix, Astro, or TanStack. |