1. Documentation
  2. Resources
  3. SEO
  • README
  • Docs
  • Github
  • 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
  • Steps
  • Tabs
  • Tree
  • Images
  • Video
  • Files
  • Grain
  • Shade
  • Moss
  • Configuration
  • OpenAPI
  • AI Chat
  • Integrations
  • Site Identity
  • Content
  • Navigation
  • Appearance
  • Header and Footer
  • Icons
  • Fonts
  • SEO
  • Search
  • AI Agents
  • React Router
  • Astro
  • Next.js
  • Cloudflare
  • Vercel

SEO

Generate robots.txt, sitemap.xml, JSON-LD, and RSS from your documentation.

robots.txt

Included in starter templates

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

Publish robots.txt at the site root to tell crawlers that documentation pages may be indexed while keeping Heyo Docs' internal Markdown resource route out of search results. Every generated template serves the same response at /robots.txt:

text
User-agent: *Allow: /Disallow: /__heyo-docs/Sitemap: https://docs.example.com/sitemap.xml

Set siteUrl in heyo-docs.config.ts before deployment. It is used for the absolute sitemap URL. During a preview, the route falls back to the origin of the incoming request.

heyo-docs.config.ts
export default heyoDocs({
  siteUrl: "https://docs.example.com",
  // ...the rest of the configuration
});

robots.txt is advisory: it does not protect a route or remove an already public document from the web. Keep private content outside the published documentation directory and protect private application routes separately.

React Router

Create a project

The React Router template includes the resource route and prerenders it for static deployments.

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

Manual, minimal configuration

Register the resource route before the documentation catch-all:

app/routes.ts
route("robots.txt", "routes/robots.ts"),
route("*", "routes/docs.tsx"),

Return a plain-text response from the route. The configured site URL is used when available, so the same implementation works in local previews.

app/routes/robots.ts
import type { LoaderFunctionArgs } from "react-router";

import config from "../../heyo-docs.config";

export function loader({ request }: LoaderFunctionArgs) {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    [
      "User-agent: *",
      "Allow: /",
      "Disallow: /__heyo-docs/",
      "",
      `Sitemap: ${siteUrl}/sitemap.xml`,
      "",
    ].join("\n"),
    { headers: { "content-type": "text/plain; charset=utf-8" } },
  );
}

For static hosting, include /robots.txt in the array returned by prerender() in react-router.config.ts.

Next.js

Create a project

The Next.js template includes an App Router Route Handler at app/robots.txt/route.ts.

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

Manual, minimal configuration

After exposing config from the server-only docs helper, add this handler:

app/robots.txt/route.ts
import { config } from "../lib/docs";

export function GET(request: Request) {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    [
      "User-agent: *",
      "Allow: /",
      "Disallow: /__heyo-docs/",
      "",
      `Sitemap: ${siteUrl}/sitemap.xml`,
      "",
    ].join("\n"),
    { headers: { "content-type": "text/plain; charset=utf-8" } },
  );
}

Astro

Create a project

The Astro template emits the root endpoint from src/pages/robots.txt.ts.

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

Manual, minimal configuration

Add an API route under src/pages:

src/pages/robots.txt.ts
import type { APIRoute } from "astro";

import config from "../../heyo-docs.config";

export const GET: APIRoute = ({ request }) => {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    [
      "User-agent: *",
      "Allow: /",
      "Disallow: /__heyo-docs/",
      "",
      `Sitemap: ${siteUrl}/sitemap.xml`,
      "",
    ].join("\n"),
    { headers: { "content-type": "text/plain; charset=utf-8" } },
  );
};

Sitemap

Included in starter templates

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

Heyo Docs generates /sitemap.xml from the same documentation model that renders navigation and pages. It includes every MDX page and every generated OpenAPI endpoint route, so the sitemap stays aligned with the site after a content or schema change.

The response is an XML sitemap with one absolute <loc> entry per route:

xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://docs.example.com/quickstart</loc></url>
  <url><loc>https://docs.example.com/api/widgets/list-widgets</loc></url>
</urlset>

Set siteUrl in heyo-docs.config.ts so production URLs are deterministic. Without it, the route uses the current request origin, which is useful for a preview but should not replace a canonical production URL.

heyo-docs.config.ts
export default heyoDocs({
  siteUrl: "https://docs.example.com",
  // ...the rest of the configuration
});

sitemapXml() escapes generated XML and accepts a base URL plus an array of paths. It deliberately publishes URLs only; it does not infer lastmod, priority, or change frequency.

React Router

Create a project

The React Router template registers /sitemap.xml and adds it to its static prerender list.

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

Manual, minimal configuration

Register the route ahead of the documentation catch-all:

app/routes.ts
route("sitemap.xml", "routes/sitemap.ts"),
route("*", "routes/docs.tsx"),

Build the model with the compiled MDX page registry and OpenAPI documents, then serialise both sets of routes:

app/routes/sitemap.ts
import { createDocsModel, sitemapXml } from "@heyo-sh/heyo-docs";
import type { LoaderFunctionArgs } from "react-router";

import config from "../../heyo-docs.config";
import { pages } from "virtual:heyo-docs-content";
import { openApiDocuments } from "virtual:heyo-docs-openapi";

export function loader({ request }: LoaderFunctionArgs) {
  const model = createDocsModel(config, pages, openApiDocuments);
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    sitemapXml(
      siteUrl,
      [...model.pages, ...model.endpoints].map((page) => page.slug),
    ),
    { headers: { "content-type": "application/xml; charset=utf-8" } },
  );
}

Add /sitemap.xml to prerender() when deploying the React Router app as static files.

Next.js

Create a project

The Next.js template generates the docs model before development and builds, then exposes this Route Handler automatically.

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

Manual, minimal configuration

First configure the server-only docs helper to export config and docsModel. Then add the handler:

app/sitemap.xml/route.ts
import { sitemapXml } from "@heyo-sh/heyo-docs/node";

import { config, docsModel } from "../lib/docs";

export function GET(request: Request) {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    sitemapXml(
      siteUrl,
      [...docsModel.pages, ...docsModel.endpoints].map((page) => page.slug),
    ),
    { headers: { "content-type": "application/xml; charset=utf-8" } },
  );
}

Astro

Create a project

The Astro template includes the static API route at src/pages/sitemap.xml.ts.

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

Manual, minimal configuration

Build the model from Astro's virtual registries and return its URLs:

src/pages/sitemap.xml.ts
import type { APIRoute } from "astro";
import { createDocsModel, sitemapXml } from "@heyo-sh/heyo-docs";

import config from "../../heyo-docs.config";
import { openApiDocuments } from "virtual:heyo-docs-openapi";
import { pages } from "virtual:heyo-docs-content";

export const GET: APIRoute = ({ request }) => {
  const model = createDocsModel(config, pages, openApiDocuments);
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;

  return new Response(
    sitemapXml(
      siteUrl,
      [...model.pages, ...model.endpoints].map((page) => page.slug),
    ),
    { headers: { "content-type": "application/xml; charset=utf-8" } },
  );
};

JSON-LD

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.

RSS

Included in starter templates

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

Heyo Docs publishes an RSS 2.0 feed at /rss.xml for changelog updates. It reads the same MDX <Update> entries that power the changelog UI, so feed items, tags, descriptions, and anchors stay in sync with the rendered page.

Only pages selected by a changelog group are included. Ordinary documentation pages are deliberately excluded from the feed.

Define changelog updates

Configure a group with type: "changelog" and point updates at the MDX pages that contain the entries:

heyo-docs.config.ts
export default heyoDocs({
  siteUrl: "https://docs.example.com",
  groups: [
    {
      group: "Changelog",
      type: "changelog",
      updates: ["changelog"],
    },
  ],
  // ...the rest of the configuration
});

Each <Update> needs a label. tags and the ISO 8601 date are optional, but a date allows the feed to emit an RSS pubDate.

content/changelog.mdx
---title: Changelog---# Changelog<Update  label="Version 2.4"  date="2026-08-24"  tags={["New releases", "Search"]}>## Faster documentation searchImproved result ranking for API operations.</Update>

The feed creates an item URL from the documentation URL and the update anchor, for example https://docs.example.com/changelog#version-2-4. It includes the plain-text update body as the description and turns each tag into an RSS category. Text is XML-escaped before it is returned.

The document is served with:

text
content-type: application/rss+xml; charset=utf-8

When a changelog group exists, the root layout also advertises the feed with a rel="alternate" link so browsers and feed readers can discover it.

React Router

Create a project

The React Router template includes /rss.xml, adds it to prerender(), and places the discovery link in the root document whenever a changelog group is configured.

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

Manual, minimal configuration

Register the resource route before the documentation catch-all:

app/routes.ts
route("rss.xml", "routes/rss.ts"),
route("*", "routes/docs.tsx"),

Generate the feed from the server-side virtual MDX registry:

app/routes/rss.ts
import { rssXml } from "@heyo-sh/heyo-docs";
import type { LoaderFunctionArgs } from "react-router";

import config from "../../heyo-docs.config";
import { pages } from "virtual:heyo-docs-content/server";

export function loader({ request }: LoaderFunctionArgs) {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;
  return new Response(rssXml(pages, config, siteUrl), {
    headers: { "content-type": "application/rss+xml; charset=utf-8" },
  });
}

Include /rss.xml in the prerender() result for static deployments. Add the discovery link conditionally in app/root.tsx:

ts
export const links = () =>
  config.groups.some((group) => group.type === "changelog")
    ? [
        {
          rel: "alternate",
          href: "/rss.xml",
          type: "application/rss+xml",
          title: `${config.title} updates`,
        },
      ]
    : [];

Next.js

Create a project

The Next.js template adds an App Router Route Handler and declares the feed as an alternate type in root metadata when a changelog exists.

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

Manual, minimal configuration

After generating and exporting config and markdownPages from the server- only docs helper, add the route:

app/rss.xml/route.ts
import { rssXml } from "@heyo-sh/heyo-docs/node";

import { config, markdownPages } from "../lib/docs";

export function GET(request: Request) {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;
  return new Response(rssXml(markdownPages, config, siteUrl), {
    headers: { "content-type": "application/rss+xml; charset=utf-8" },
  });
}

For discovery, set alternates.types["application/rss+xml"] to "/rss.xml" in the root Metadata only when a changelog group is present.

Astro

Create a project

The Astro template includes a static src/pages/rss.xml.ts API route and an RSS alternate link in DocsLayout when the configuration has a changelog.

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

Manual, minimal configuration

Create the root API route and generate the feed from the server page registry:

src/pages/rss.xml.ts
import type { APIRoute } from "astro";
import { rssXml } from "@heyo-sh/heyo-docs";

import config from "../../heyo-docs.config";
import { pages } from "virtual:heyo-docs-content/server";

export const GET: APIRoute = ({ request }) => {
  const siteUrl = config.siteUrl ?? new URL(request.url).origin;
  return new Response(rssXml(pages, config, siteUrl), {
    headers: { "content-type": "application/rss+xml; charset=utf-8" },
  });
};
Fonts< PreviousSearchNext >

Powered by heyo-docs

On this page

robots.txtReact RouterCreate a projectManual, minimal configurationNext.jsCreate a projectManual, minimal configurationAstroCreate a projectManual, minimal configurationSitemapReact RouterCreate a projectManual, minimal configurationNext.jsCreate a projectManual, minimal configurationAstroCreate a projectManual, minimal configurationJSON-LDMetadata by page typeRoot metadataReact RouterCreate a projectManual, minimal configurationNext.jsCreate a projectManual, minimal configurationAstroCreate a projectManual, minimal configurationRSSDefine changelog updatesReact RouterCreate a projectManual, minimal configurationNext.jsCreate a projectManual, minimal configurationAstroCreate a projectManual, minimal configuration