# Markdown pages

> Which pages of a Kismet site have Markdown versions, how the SDK builds them, the site route or rewrite that serves them, what they link to, and how to check every link resolves.




# Markdown pages

Every page a Kismet site declares has a Markdown twin at the same URL plus
`.md` — the version an AI agent reads instead of rendering HTML. `/llms.txt`
links the twins, so a broken twin or a twin that links a page the site does
not serve is a dead end for every agent that followed the index. This guide
says which pages have twins, how the SDK builds them, how a site serves them,
and how to check the links inside them.

## Which pages have a Markdown twin

The SDK's page-type registry is the only list of page types, and every type
in it carries a Markdown composition. The twin is per declared page: a page
exists for an agent when the site declares it with
[`definePage`](https://developers.kismet.travel/sdk.md#the-page-registry) and serves it.

| Page type | HTML route | Markdown twin |
| --- | --- | --- |
| Homepage | `/` | `/index.md` |
| Results (collection) | `/properties/` | `/properties/index.md` |
| Property | `/properties/{slug}` | `/properties/{slug}.md` |
| Property group | `/groups/{slug}` | `/groups/{slug}.md` |
| Group index | `/groups/` | `/groups/index.md` |
| Reviews | `/properties/{slug}/reviews` | `/properties/{slug}/reviews.md` |
| Rates | `/properties/{slug}/rates` | `/properties/{slug}/rates.md` |
| Blog index | `/blog/` | `/blog/index.md` |
| Blog post | `/blog/{slug}` | `/blog/{slug}.md` |
| FAQ, About, Contact, Owners | `/faq`, `/about`, `/contact`, `/owners` | same route + `.md` |
| Booking confirmation | `/booking-confirmation` | `/booking-confirmation.md` |
| Custom | the site declares it | the declared URL + `.md` |

A page marked `private` never serves a twin — the twin endpoint returns 404,
the same boundary the HTML page enforces elsewhere. Unlisted-but-public pages
keep twins; only `/llms.txt` decides what is advertised.

## How the SDK builds the twin

`markdownForPage(page)` renders the twin body, in this order:

1. Your `renderMarkdown` override, if `definePage` got one — it wins.
2. The page type's composition — the property twin carries the details, the
   long description, highlights and selling points, amenities, neighborhood
   and location, the pricing summary, the photos, and a Book section linking
   the property page and the rates twin; the group twin lists its members.
3. The fallback: title, `Canonical:` line, description.

Every twin starts with a `Canonical:` line naming the HTML page it mirrors,
and the HTML page carries `<link rel="alternate" type="text/markdown">`
pointing back at the twin.

## Wire the site

The SDK builds the Markdown string; the site decides which URL serves it.
Two supported shapes:

**Next.js — rewrite `.md` to one twin route.** Rewrite every declared page's
`.md` URL to a single route that calls the page adapter's `markdown` handler,
which resolves the page for the request URL and returns `markdownForPage`:

```ts
// next.config.ts
const nextConfig: NextConfig = {
  async rewrites() {
    return [
      { source: "/properties/:slug", destination: "/property-page" },
      { source: "/properties/:slug.md", destination: "/markdown-twin" },
      { source: "/groups/:slug.md", destination: "/markdown-twin" },
    ];
  },
};
```

The `markdown` handler is exact-host and same-origin: a twin URL that names
no declared page returns 404, so a twin exists only where the HTML page does.

**Static export — write the twins as files.** After the build, write each
twin next to its HTML (the homepage twin at `/index.md`). The checker probes
`<route>.md` then `<route>/index.md`, and `/` twins at `/index.md`.

Either shape, the site also serves:

- `/llms.txt` from `Kismet.llmsTxt` — it links every public twin. Pass
  `{ markdownTwins: false }` only if the site serves no twins at all; then
  the `site/markdown-twin` rule does not require them.
- `Accept: text/markdown` negotiation on the HTML routes, so an agent that
  asks for Markdown instead of HTML gets the twin body without the `.md` URL.

## What twins link

A twin links the pages its guest actions need, and every link must resolve
on that site:

- The `Canonical:` line points at the twin's own HTML page.
- A property twin links the rates twin (`…/rates.md`) and the property page.
- Group and index twins link their members.
- `/llms.txt` links every public twin.

The rule is absolute: **a twin may only link URLs the site actually serves.**
The SDK builds links from the page type's template; if the site does not
serve that page, the link is broken for agents. That exact failure shipped
on a live site — property twins linked `/properties/<slug>/rates.md` while
the site rewrote only property and group twins, so every rates link
returned 404 (KIS-21888). Serve the linked page, or remove the link, and
re-run the link check before go-live.

## Check before go-live

1. **Twin exists and is Markdown.** `curl` any declared page's `.md` URL and
   expect 200 with a `Canonical:` line:

   ```sh
   curl -fsSL https://example.com/properties/<slug>.md | head -5
   ```

2. **Every declared page has a twin.** Run the conformance check in
   production mode — the `site/markdown-twin` rule fails for each declared
   page-type route without a reachable twin:

   ```sh
   npx kismet check https://example.com --env production --json
   ```

3. **Every link inside every twin returns 200** on the live domain — the
   curl loop is [go-live checklist N7](https://developers.kismet.travel/guides/go-live-checklist.md#n-after-every-production-deploy).
   The checker's `site/internal-links` rule grades twin links it can read
   from a built directory; twins served through a rewrite need the live
   curl check, because no static file holds their body.
