JSON-LD

Generate canonical metadata and structured data for documentation, changelogs, and API reference pages.

Included in starter templates

This functionality is already configured in the starter templates. Follow the complete setup in the Quickstart.

The generated application metadata describes the documentation to crawlers and social previews without requiring per-page hand-written tags. The templates use the page's MDX frontmatter, the navigation model, and OpenAPI metadata to emit canonical URLs, Open Graph and Twitter fields, and JSON-LD.

Set a production siteUrl first. Heyo Docs removes a trailing slash and uses the value as the base for canonical URLs, breadcrumbs, sitemap entries, and structured-data links.

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

siteUrl must be an HTTP(S) base URL without a query string or fragment. If it is omitted, the pages remain renderable, but canonical and absolute structured data URLs are not emitted.

Metadata by page type

For regular MDX documentation, Heyo Docs emits a TechArticle plus a BreadcrumbList. The article title and description come from frontmatter; when the description is empty, the site-level description is used.

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

Changelog pages become CollectionPage records. Each <Update> entry is listed as a TechArticle in hasPart, including its tags as keywords. An OpenAPI operation becomes an APIReference with an EntryPoint that includes the HTTP method, URL template, and request or response content type when the schema supplies them.

All successful documentation routes declare robots: index, follow. The framework adapters return a noindex response for a route that does not match an MDX page or generated endpoint.

Root metadata

The root document is a WebSite. Templates also include the site title and description, og:type=website, a summary Twitter card, a referrer policy, and an RSS discovery link when a changelog group exists.

The JSON-LD string replaces < with \u003c before it is inserted into a script tag. This prevents page content from ending the script element.

React Router

Create a project

The React Router template configures root metadata in app/root.tsx and per-page metadata in app/lib/seo.ts with app/routes/docs.tsx.

bun create @heyo-sh/heyo-docs my-docs --template react-router

Manual, minimal configuration

Resolve the current page from the documentation model, then return metadata from the route's meta function. The generated helper adds title, description, canonical, Open Graph, Twitter, JSON-LD, and breadcrumbs.

app/routes/docs.tsx
import {
  changelogGroupForPage,
  createDocsModel,
  findDocsPage,
  findOpenApiEndpoint,
} from "@heyo-sh/heyo-docs";
import type { MetaFunction } from "react-router";

import config from "../../heyo-docs.config";
import { docsSeoMeta } from "../lib/seo";
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,
  });
};

Use the template's app/lib/seo.ts as the helper implementation. Its output is a React Router meta descriptor that includes the script:ld+json entry.

Next.js

Create a project

The Next.js template uses the Metadata API for standard tags and injects the structured-data array from app/lib/seo.ts on each static docs page.

bun create @heyo-sh/heyo-docs my-docs --template next

Manual, minimal configuration

Generate metadata and JSON-LD from the same resolved docs context:

app/[[...slug]]/page.tsx
import { docsContext, pathnameForSegments } from "../lib/docs";
import { docsSeo } from "../lib/seo";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug } = await params;
  const pathname = pathnameForSegments(slug);
  const context = docsContext(pathname);
  if (!context.page && !context.endpoint)
    return { robots: { index: false, follow: false } };
  return docsSeo({ ...context, pathname }).metadata;
}

export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug } = await params;
  const pathname = pathnameForSegments(slug);
  const context = docsContext(pathname);
  const { structuredData } = docsSeo({ ...context, pathname });

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(structuredData).replace(/</g, "\\u003c"),
      }}
    />
  );
}

The root app/layout.tsx adds the site-wide WebSite JSON-LD and Next metadata, including metadataBase when siteUrl is configured.

Astro

Create a project

The Astro template calls docsSeo() for every emitted page and passes its result to DocsLayout.

bun create @heyo-sh/heyo-docs my-docs --template astro

Manual, minimal configuration

Resolve page data in the dynamic route and spread the metadata into a layout that renders the standard tags and JSON-LD script:

src/pages/[...slug].astro
---import DocsLayout from "../layouts/docs-layout.astro";import { docsContext, pathnameForSlug } from "../lib/docs";import { docsSeo } from "../lib/seo";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: context.config.description,      structuredData: [],    };---<DocsLayout {...seo} robots={exists ? "index, follow" : "noindex"}>  <!-- documentation application --></DocsLayout>

DocsLayout serialises structuredData with the same < escaping and adds canonical, Open Graph, Twitter, RSS, and robots tags around the documentation application.