1. Documentation
  2. Tutorials
  3. JSON-LD
ReadmeGitHub
  • Introduction
  • Quickstart
  • Text
  • Code
  • Lists
  • Tables
  • Accordion
  • Badge
  • Button
  • Callout
  • Code Block
  • Code Block Group
  • Code Snippet
  • Columns
  • Custom components
  • GitHub
  • Hover Card
  • Mermaid
  • Properties
  • Related Topics
  • Tabs
  • Tree
  • Images
  • Video
  • Files
  • Grain
  • Shade
  • Moss
  • Configuration
  • Content
  • Navigation
  • Site Identity
  • Appearance
  • Header and Footer
  • Fonts
  • Icons
  • Integrations
  • Search
  • OpenAPI
  • AI Chat
  • React Router
  • Astro
  • Next.js
  • Cloudflare
  • Vercel
  • robots.txt
  • sitemap.xml
  • JSON-LD
  • rss.xml
  • llms.txt
  • llms-full.txt
  • .md endpoints

JSON-LD

Generate canonical metadata, structured data, and breadcrumbs from the Heyo Docs page model.

Loading documentation…

sitemap.xml< Previousrss.xmlNext >

Powered by heyo

On this page

Configure the site identityAdd page metadataAdd the framework integrationReact RouterAstroNext.js
Included with create-heyo-docs

This is already configured in projects created with create-heyo-docs. No action is required if you used the creator to install Heyo Docs.

Heyo Docs creates metadata and JSON-LD from the site configuration, MDX frontmatter, navigation, and OpenAPI model. This keeps canonical URLs, breadcrumbs, social previews, and structured data aligned with the pages that are actually published.

Configure the site identity

Set a production siteUrl, title, and description first. siteUrl must be a clean HTTP(S) base URL without a query string or fragment.

heyo-docs.config.ts
export default heyoDocs({  title: "Acme API documentation",  description: "Reference documentation for the Acme API.",  siteUrl: "https://docs.example.com",  // ...});

The URL is used for canonical links, Open Graph and Twitter metadata, breadcrumb URLs, and structured-data links. If it is omitted, pages still render, but absolute canonical and JSON-LD URLs are not emitted.

Add page metadata

Give each reader-facing MDX page a specific title and description. They become the TechArticle name and description for regular documentation pages:

mdx
---title: Configure webhooksdescription: Verify signed webhook deliveries from the API.---# Configure webhooks

Heyo Docs emits a TechArticle and BreadcrumbList for an MDX page. A changelog page becomes a CollectionPage; generated OpenAPI operations become APIReference entries with an EntryPoint when the schema provides the required details. Unknown documentation paths should receive noindex metadata.

Add the framework integration

The SEO data is generated by Heyo Docs, but each framework has its own metadata and document-head API. Use the implementation for the framework that renders your documentation routes. Do not maintain a second set of hand-written canonical tags or JSON-LD in MDX.

React Router

Add site-wide metadata in app/root.tsx:

app/root.tsx
import { siteSeoMeta } from "@heyo-sh/heyo-docs/seo/react-router";import type { MetaFunction } from "react-router";import { config } from "virtual:heyo-docs-config";export const meta: MetaFunction = () => siteSeoMeta(config);

In the documentation route, resolve the current page or OpenAPI endpoint from the same model passed to DocsApp, then return docsSeoMeta() from meta:

app/routes/docs.tsx
import {  createDocsModel,  findDocsPage,  findOpenApiEndpoint,} from "@heyo-sh/heyo-docs/model";import { changelogGroupForPage } from "@heyo-sh/heyo-docs/navigation";import { docsSeoMeta } from "@heyo-sh/heyo-docs/seo/react-router";import type { MetaFunction } from "react-router";import { config } from "virtual:heyo-docs-config";import { pages } from "virtual:heyo-docs-content";import { openApiEndpoints } from "virtual:heyo-docs-openapi/index";export const meta: MetaFunction = ({ params }) => {  const pathname = params["*"] ? `/${params["*"]}` : "/";  const model = createDocsModel(config, pages, [], openApiEndpoints);  const page = findDocsPage(model.pages, pathname);  const endpoint = findOpenApiEndpoint(model.endpoints, pathname);  if (!page && !endpoint)    return [      { title: `Not found | ${config.title}` },      { name: "robots", content: "noindex" },    ];  return docsSeoMeta({    config,    pathname,    page,    endpoint,    navigation: model.navigation,    changelogGroup: page      ? changelogGroupForPage(config.groups, page, model.pages)      : undefined,  });};

siteSeoMeta() and docsSeoMeta() return React Router metadata descriptors, including safely serialized JSON-LD. The generated React Router starter already includes both integrations.

Astro

Use docsSeo() for each docs page and pass the returned title, description, canonical URL, and structured data into the Astro layout. The generated Astro template does this in src/pages/[...slug].astro:

src/pages/[...slug].astro
---import { docsSeo, siteSeo } from "@heyo-sh/heyo-docs/seo";import DocsLayout from "../layouts/docs-layout.astro";import { docsContext, pathnameForSlug } from "../lib/docs";const pathname = pathnameForSlug(Astro.params.slug);const context = docsContext(pathname);const exists = Boolean(context.page || context.endpoint);if (!exists) Astro.response.status = 404;const seo = exists  ? docsSeo({ ...context, pathname })  : {      title: `Not found | ${context.config.title}`,      description: siteSeo(context.config).description,      structuredData: siteSeo(context.config).structuredData,    };---<DocsLayout {...seo} robots={exists ? "index, follow" : "noindex"}>  <!-- Render the documentation application here. --></DocsLayout>

The layout owns the document head. Serialize the structured data with the Heyo Docs helper instead of interpolating JSON by hand:

src/layouts/docs-layout.astro
---import { serializeJsonLd } from "@heyo-sh/heyo-docs/seo";interface Props {  title: string;  description: string;  canonical?: string;  robots?: "index, follow" | "noindex";  structuredData: unknown[];}const {  title,  description,  canonical,  robots = "index, follow",  structuredData,} = Astro.props;const jsonLd = serializeJsonLd(structuredData);---<head>  <meta name="robots" content={robots} />  <title>{title}</title>  <meta name="description" content={description} />  {canonical && <link rel="canonical" href={canonical} />}  {canonical && <meta property="og:url" content={canonical} />}  <script is:inline type="application/ld+json" set:html={jsonLd}></script></head>

docsContext() should resolve the page, endpoint, navigation, and changelog group from createDocsModel(), as in the Astro starter. Apply the same pattern to src/pages/index.astro for the root documentation page.

Next.js

Create the site-wide metadata in the root layout with nextSiteSeo() and serialize its site-level JSON-LD:

app/layout.tsx
import type { Metadata } from "next";import { nextSiteSeo } from "@heyo-sh/heyo-docs/seo/next";import { serializeJsonLd } from "@heyo-sh/heyo-docs/seo";import config from "../heyo-docs.config";const siteSeo = nextSiteSeo(config);export const metadata: Metadata = siteSeo.metadata;export default function RootLayout({ children }: { children: React.ReactNode }) {  return (    <html lang="en">      <head>        <script          type="application/ld+json"          dangerouslySetInnerHTML={{            __html: serializeJsonLd(siteSeo.structuredData),          }}        />      </head>      <body>{children}</body>    </html>  );}

On the App Router docs page, use nextDocsSeo() in generateMetadata() and render its page-specific structured data. docsContext() below is the server-only helper that resolves the current page or OpenAPI endpoint from the same generated model as the docs UI:

app/[[...slug]]/page.tsx
import type { Metadata } from "next";import { notFound } from "next/navigation";import { nextDocsSeo } from "@heyo-sh/heyo-docs/seo/next";import { serializeJsonLd } from "@heyo-sh/heyo-docs/seo";import { docsContext, pathnameForSegments } from "../lib/docs";interface PageProps {  params: Promise<{ slug?: string[] }>;}export async function generateMetadata({ params }: PageProps): Promise<Metadata> {  const { slug } = await params;  const pathname = pathnameForSegments(slug);  const context = docsContext(pathname);  if (!context.page && !context.endpoint)    return {      title: `Not found | ${context.config.title}`,      robots: { index: false, follow: false },    };  return nextDocsSeo({ ...context, pathname }).metadata;}export default async function DocsPage({ params }: PageProps) {  const { slug } = await params;  const pathname = pathnameForSegments(slug);  const context = docsContext(pathname);  if (!context.page && !context.endpoint) notFound();  const { structuredData } = nextDocsSeo({ ...context, pathname });  return (    <>      <script        type="application/ld+json"        dangerouslySetInnerHTML={{          __html: serializeJsonLd(structuredData),        }}      />      {/* Render the documentation application here. */}    </>  );}

The generated Next.js starter already provides docsContext() and pathnameForSegments() in app/lib/docs.ts, backed by its generated server-side content registry.