Docs

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.

Quickstart Guide

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.

1

Create Next.js App & Install Dependencies

Initialize a blank Next.js App Router project (or use an existing project) and install contextual-ui, the static connector, and zod.

Create Next.js App (Optional)
$pnpm create next-app@latest my-app --yes && cd my-app
Terminal
$pnpm add contextual-ui contextual-ui-connector-static zod
2

Define your Site Schema (SSOT)

Create data/site.schema.ts. Using defineSchema, register pre-built Schema.org registries (organization, website, webpage, navbar, footer, faq) or any custom Zod schemas.

data/site.schema.ts
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'),
    }),
  },
});
3

Configure Server Connector & App Instance

Create data/site.server.ts. Bind your schema with createContextualApp and a connector (static configuration, headless CMS, or database ORM).

data/site.server.ts
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>;
Schema Accuracy GuardrailsCentralizing site metadata prevents data drift between your visual UI, metadata tags, and search engine graphs. In step 6, <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.
4

Wrap Root Layout with ContextualSite

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.

app/layout.tsx
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>
  );
}
5

Implement Headless Navbar & Footer Client Components

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!

components/Navbar.tsx
'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>
  );
}
6

Render WebPage & Route-Specific Content

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.

app/page.tsx
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>
  );
}
7

Add Automated Sitemap & Robots.txt

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 & app/robots.ts
// 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/'],
  });
}
8

Expose AI Knowledge Graph API

Optional

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.

app/api/graph.json/route.ts
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',
  },
});

Schema Registries & defineSchema

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.

Complete Schema Definition Example

data/site.schema.ts
data/site.schema.ts
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>;

Built-in Registries Reference8 Registries

Select a registry below to inspect its Schema.org specification, field requirements, and usage examples.

websiteRegistry()

schema.org/WebSite

Declares domain-level website metadata, site display title, description, canonical URL, and search action.

site.schema.ts
// data/site.schema.ts
import { defineSchema, websiteRegistry } from 'contextual-ui/server';

export const siteSchema = defineSchema({
  website: websiteRegistry(),
});

Schema Reference & Contract

5 fields
FieldTypeRequirementSchema.org MappingDescription
namestringRequirednamePrimary display name of the website
urlstringRequiredurlCanonical root domain URL
descriptionstringOptionaldescriptionWebsite meta description for search engines
inLanguagestringOptionalinLanguageLanguage code (e.g. "en-US")
publisherReferenceOptionalpublisherCross-reference pointing to the Organization entity

Custom Schema Extensibility

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.

data/site.schema.ts (Custom Schemas)
// 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(),
      })
    ),
  },
});

<ContextualSite /> Provider

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.

Wrap your root layout with ContextualSite to provide data across all components.
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>
  );
}

Schema Reference & Contract

7 fields
FieldTypeRequirementSchema.org MappingDescription
dataSiteDataRequired@graphDomain-level data object providing website, navbar, footer, and FAQ configurations.
data.websiteWebsiteDataOptionalWebSiteSite-level metadata including name, url, and meta description.
data.navbarNavbarDataOptionalSiteNavigationElementNavigation brand and menu links automatically inferred by <Navbar.Root />.
data.footerFooterDataOptionalWPFooterFooter structure, copyright, and social links automatically inferred by <Footer.Root />.
data.faqFaqItem[]OptionalFAQPageFAQ question-answer pairs automatically inferred by <Faq.Root />.
graphContextualGraphOptional@graphPre-compiled Schema.org JSON-LD graph produced by siteApp.getGraph().
childrenReactNodeRequiredChild components rendered within ContextualSite context.

<WebPage /> Wrapper

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.

Wrap individual routes in page.tsx with WebPage from contextual-ui/server.
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>
  );
}

Schema Reference & Contract

6 fields
FieldTypeRequirementSchema.org MappingDescription
appContextualAppOptional@graphContextualApp instance to compile and inject the route-specific Schema.org JSON-LD graph.
namestringOptionalWebPage.nameRoute-specific page title/name for search engines and AI agents.
urlstringOptionalWebPage.urlRoute-specific canonical pathname (e.g. "/docs").
descriptionstringOptionalWebPage.descriptionRoute-specific meta description.
graphJsonLdGraphResultOptional@graphPre-computed Schema.org JSON-LD graph (optional explicit override).
disableJsonLdScriptbooleanOptionalDisables script tag rendering when set to true.

FAQ

The FAQ component organizes collapsible question-and-answer pairs with automatic Schema.org FAQPage structured data injection.

Interactive Data Source · FAQ
Collapsible FAQ layout with trigger buttons and content sections.
<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>

Schema Reference & Contract

4 fields
FieldTypeRequirementSchema.org MappingDescription
dataFaqItem[]RequiredmainEntity: Question[]Array of FAQ question and answer items.
data[].idstringRequiredUnique identifier for the FAQ item.
data[].questionstringRequiredQuestion.nameThe question string for users and search indexing.
data[].answerstringRequiredQuestion.acceptedAnswer.textThe accepted answer text content.

AutoForm & formRegistry

<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.

Open Forms Studio ↗
1

Declare formRegistry in your Site Schema

Add forms: formRegistry() inside defineSchema. This sets up the runtime validation contract and links form entities into the single source of truth.

data/site.schema.ts
// 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
});
2

Define Forms in Connector / Headless CMS Data

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.

data/site.server.ts (CMS Connector)
// 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.',
  },
]
3

Render Dynamic Headless Forms with <AutoForm>

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.

app/contact/page.tsx
// 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>
  );
}
4

Automated Schema.org PotentialAction Graph for AI Agents

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.

Generated JSON-LD (@graph snippet)
// 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
    }
  ]
}

Interactive Live Demo (<AutoForm>)

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.

Customize in Studio ↗

Get in Touch

Send our team a direct message. Submissions are dynamically validated and Agentic AI ready.

<AutoForm> Props Reference10 Props

Complete props contract for configuring dynamic forms, custom slot overrides, and submission callbacks.

PropTypeRequirementDescription
dataFormDataYes (or form)Ingested forms data from connector/registry (single FormEntity or FormEntity[] array).
formIdstringOptionalMatches a specific form by its id when data contains multiple forms.
formFormEntityOptionalExplicit form entity object override (bypassing data lookup).
componentsAutoFormCustomComponentsOptionalCustom UI slots for Form, Field, Label, Input, TextArea, Select, Checkbox, ErrorMessage, Submit, Section.
actionstringOptionalOverrides the form submit endpoint (defaults to form.endpoint).
method'POST' | 'GET' | 'PUT' | 'PATCH'OptionalOverrides HTTP method (defaults to form.method or "POST").
onSubmit(values, form) => void | Promise<void>OptionalCustom submit handler. If omitted, AutoForm performs a JSON POST fetch to the endpoint automatically.
onSuccess(result) => voidOptionalCallback invoked after successful form submission.
onError(error: ZodError) => voidOptionalCallback invoked when client-side validation fails.
submitLabelstringOptionalOverrides the submit button text (defaults to form.submitLabel or "Submit").

createForm (Static Form Factory)

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.

1

Define your Validation Schema with Zod

Define the shape and validation constraints using standard Zod types (z.string(), z.number(), z.enum()).

schemas/contact.ts
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>;
2

Initialize Form Components with createForm

Pass your schema into createForm(schema) to produce compound components strictly typed to your schema keys.

components/ContactForm.tsx (Init)
import { createForm } from 'contextual-ui';
import { contactSchema } from '@/schemas/contact';

// 2. Instantiate type-safe compound form components
export const ContactForm = createForm(contactSchema);
3

Compose Headless Form Layout & Field Controls

Wrap input controls in <ContactForm.Field name="...">. Child components share context and validation automatically.

components/ContactForm.tsx (UI)
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>
  );
}
4

Automatic Blur Validation & Submit Lifecycle

Validation runs on field blur and on form submit. Submitting state is automatically managed during async promises.

components/ContactForm.tsx (Lifecycle)
// 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>

Static Form Live Demo

Test the static compound form below. Blur fields to test validation feedback.

createForm Compound Subcomponents Reference8 Subcomponents

Every component produced by createForm is completely headless and composable with Radix UI Slot (asChild) support.

SubcomponentProps / ContextDescription
Form.RootonSubmit, onError?, className?, id?Top-level context provider for static forms. Manages state, errors, blur validation, and async lifecycle.
Form.Fieldname: keyof Schema, className?Scopes field context by name. Strictly type-checked against schema keys at compile time.
Form.LabelasChild?, className?, style?Accessible <label> automatically bound to the input through the field htmlFor attribute.
Form.InputasChild?, ...InputHTMLAttributesControlled input element bound to field value, onChange, onBlur validation, and data-invalid attribute.
Form.TextAreaasChild?, ...TextareaHTMLAttributesControlled multi-line textarea with automatic onBlur validation and data-invalid attribute binding.
Form.ErrorMessageasChild?, className?, style?Conditionally renders active validation error strings for the scoped field.
Form.SubmitasChild?, ...ButtonHTMLAttributesSubmit button automatically disabled while an async onSubmit promise is pending.
Form.Sectiontitle?, description?, asChild?, className?Semantic container for grouping related fields with an optional title and description header.

Connectors & Data Layer

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.

Built-in static connector for hardcoded configs or JSON files.
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',
});

Helpers: siteApp.getMetadata()

Next.js App Router

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.

Zero Duplication

Pulls title, description, and canonical URL from the same schema powering <WebPage /> and /api/graph.json.

Canonical & Base URL

Automatically attaches metadataBase: new URL(baseUrl) and binds canonical alternates seamlessly across relative paths.

Social & SEO Ready

Auto-populates openGraph and twitter cards with brand logos, handles, and OpenGraph website types.

Interactive Route Selector

Select a page configured in your schema connector to inspect its generated Next.js metadata:

SERP & OpenGraph Preview·https://contextual.site/privacy
Privacy Policy - Contextual UI
Privacy policy and data protection information for Contextual UI.
app/privacy/page.tsx: Zero duplication with export const generateMetadata.
// 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>
  );
}

API Parameters & Return Types

ParameterTypeRequirementDescription
pageIdOrOptionsstring | GetMetadataOptionsOptionalPage identifier (e.g. "privacy", "home") or options object. Defaults to "home" or root website when omitted.
overridesPartial<Metadata>OptionalCustom metadata overrides (e.g. title, openGraph images, twitter card, keywords, robots).
returnsPromise<Metadata>RequiredNext.js App Router-compatible Metadata object with metadataBase, title, description, alternates, openGraph, and twitter.

Helpers: siteApp.getSitemap() & XML Generation

Sitemap 0.9 Protocol

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.

Schema Route Discovery

Automatically indexes every route configured in your schema connector with canonical URL formatting against baseUrl.

Smart Priority & Frequency

Defaults root (/) to priority 1.0 and daily crawl, while subpages default to 0.8 and weekly crawl.

Exclusions & Wildcards

Easily omit private paths via glob patterns (e.g. exclude: ['/cms', '/cms/*']) or custom filter functions.

Interactive Filter Simulator

Toggle exclusions and priorities to see the live output update dynamically:

Priority:
Indexed Routes: 6 pages
https://contextual.site/sitemap.xml
Zero-boilerplate export for Next.js App Router app/sitemap.ts.
// 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,
  });
}

API Parameters & Return Types

ParameterTypeRequirementDescription
options.baseUrlstringOptionalBase canonical domain (e.g. "https://example.com"). Defaults to siteApp.baseUrl or data.website.url.
options.excludestring[]OptionalPaths or glob patterns to omit (e.g. ["/cms", "/cms/*", "/admin", "/studio*"]).
options.additionalRoutesSitemapItem[]OptionalAdditional routes or dynamic records outside the primary schema to append with deduplication.
options.defaultPrioritynumberOptionalDefault priority (0.0 to 1.0). Root "/" defaults to 1.0, subpages default to 0.8.
options.defaultChangeFrequencySitemapChangeFrequencyOptional"always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never". Root defaults to "daily", subpages to "weekly".
returns (getSitemap)Promise<MetadataRoute.Sitemap>RequiredArray of items typed directly for Next.js App Router app/sitemap.ts export.
returns (generateSitemapXml)Promise<string>RequiredRFC-compliant XML string conforming to Sitemaps 0.9 specification with entity escaping.
returns (createSitemapHandler){ GET: (req: Request) => Promise<Response> }RequiredStandard Web Response route handler for Next.js, Remix, Astro, or TanStack.

Helpers: siteApp.getRobots() & AI Agent Controls

RFC 9309 & Agentic AI

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).

Auto Sitemap & Host

Automatically extracts domain host and advertises Sitemap: https://.../sitemap.xml based on your connector URL.

Agentic AI Presets

One-line toggle to allow or disallow all major AI bots, with granular per-bot override capabilities.

Multi-Framework

Drop into Next.js App Router app/robots.ts, or export standard Web API handlers for Remix, Astro, and TanStack.

Interactive AI Policy Simulator

Configure AI crawler policies to see the generated rules in real time:

All AI Bots:
PerplexityBot:
Target Endpoint: /robots.txt
Host: contextual.site
Zero-boilerplate export for Next.js App Router app/robots.ts.
// 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/'],
  });
}

API Parameters & Return Types

ParameterTypeRequirementDescription
options.baseUrlstringOptionalBase canonical domain. Used to formulate "Sitemap: ${baseUrl}/sitemap.xml" and "Host: hostname".
options.disallowstring | string[]OptionalPaths forbidden for standard crawlers (e.g. ["/cms", "/cms/"]).
options.allowstring | string[]OptionalPaths explicitly permitted (defaults to "/").
options.aiRobotsAiOptionsOptionalPreset policies for AI training and search bots (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, Bytespider).
options.sitemapstring | string[] | booleanOptionalSitemap URL directive. Defaults to true ("${baseUrl}/sitemap.xml"). Set false to suppress.
returns (getRobots)Promise<MetadataRoute.Robots>RequiredStructured object typed directly for Next.js App Router app/robots.ts export.
returns (generateRobotsTxt)Promise<string>RequiredRFC 9309 compliant robots.txt plain-text representation.
returns (createRobotsHandler){ GET: (req: Request) => Promise<Response> }RequiredStandard Web Response route handler for Next.js, Remix, Astro, or TanStack.