---
title: "SEO and AI search · Leiho docs"
description: "Structured data, llms.txt and Markdown from your fields."
url: https://leiho.dev/docs/seo-and-ai-search
lang: en
updated: 2026-11-05
---
Guides

# SEO and AI search

Structured data, llms.txt and Markdown from your fields.

Search engines and AI assistants (ChatGPT, Claude, Perplexity, Google’s AI answers) read a page better when it says what it is. Leiho builds that from fields you already have: no SEO tab for editors, nothing stored twice.

## Describe a collection

In **Collections → a collection → Search and AI**, or in `leiho.schema.json`:

```json
{
  "id": "doc",
  "path": "/docs/{slug}",
  "seo": { "type": "TechArticle", "description": "summary", "image": "cover", "faq": "faq" },
  "fields": [ … ]
}
```

- **type**: the schema.org type. Pages: `WebPage` (default), `AboutPage`, `ContactPage`, `CollectionPage`, `FAQPage`, `ProfilePage`. Articles: `Article`, `BlogPosting` (default for posts), `NewsArticle`, `TechArticle`. Subjects: `Product`, `Service`, `Event`, `Course`, `SoftwareApplication`, `Person`, `Organization`, `LocalBusiness`, `Place`, `Recipe`, `CreativeWork`.
- **title**, **description**, **image**: the fields that hold them. Without a description field, Leiho takes a text field named `description`, `summary`, `intro`, `excerpt`, `lead`, `subtitle` or `tagline`, then the first long enough text, then the meta description of Yoast, Rank Math, SEOPress or All in One SEO, then the excerpt.
- **faq**: a list with `question` and `answer` fields, published as an FAQ.
- **llms: false** keeps the collection out of `llms.txt`.

## What the content API serves

Every item gets a `seo` block, in the language of the request:

```json
"seo": {
  "type": "TechArticle",
  "title": "Install",
  "description": "Install the plugin, connect your site, annotate your markup.",
  "image": { "url": "…", "alt": "…" },
  "faq": [ { "question": "…", "answer": "…" } ],
  "llms": true,
  "jsonld": [ { "@type": "WebPage", … }, { "@type": "TechArticle", … }, { "@type": "FAQPage", … } ]
}
```

The filter `leiho_seo` adds what only you know, such as a product’s price or an event’s place.

## Print it on the page

```ts
import { leihoGraph, jsonLd, seoMeta } from 'leiho/seo';

const graph = leihoGraph({
  url: 'https://example.com/docs/install',
  item,
  site: { url: 'https://example.com', name: 'Example', locales: ['en', 'fr'] },
  organization: { name: 'Example Inc.', logo: '/logo.png' },
  breadcrumbs: [{ name: 'Docs', url: '/docs' }, { name: item.title }],
});
const { title, description, meta } = seoMeta(item, { url, siteName: 'Example' });
// <script type="application/ld+json">{jsonLd(graph)}</script>
```

`leihoGraph()` puts the item’s nodes on the page’s canonical address and links them to your site, organization and breadcrumbs. `seoMeta()` returns the description and Open Graph tags to render with your framework.

## llms.txt and Markdown

A static site gets them from its own pages after the build: `page.md` next to each page, `llms.txt` and `llms-full.txt` per language, and a `sitemap.xml` with each page’s other languages and date.

```js
// astro.config.mjs
import { aiReady } from 'leiho/ai-ready';

export default defineConfig({
  site: 'https://example.com',
  integrations: [aiReady()],
});
```

```bash
# Any other static build
npx leiho ai-ready --site https://example.com --dir out
```

The Markdown comes from each page’s `<main>`. Navigation, buttons, `svg`, `canvas` and anything with `aria-hidden="true"` or `data-md-skip` are left out.

A front end rendered on request can ask WordPress instead:

| Endpoint | Returns |
| --- | --- |
| `GET /wp-json/leiho/v1/llms.txt` | The site’s llms.txt. `?full=1` adds every entry’s text, `?md=1` links to `page.md`, `?locale=fr` in a language. |
| `GET /wp-json/leiho/v1/markdown?path=/docs/install` | One entry as Markdown, with a canonical `Link` header. |

## Answer Accept: text/markdown

To serve the Markdown on the page’s own address, rewrite on the server when the client asks for it. With nginx:

```nginx
map $http_accept $wants_markdown {
  default 0;
  "~*text/markdown" 1;
}

location / {
  if ($wants_markdown) {
    rewrite ^/$ /index.md last;
    rewrite "^((?:/[^./]+)+)$" $1.md last;
  }
  add_header Vary Accept;
  try_files $uri $uri/index.html =404;
}
```

## Let AI crawlers in

Being cited starts with being read. A `robots.txt` that names the assistants you welcome (GPTBot, OAI-SearchBot, ClaudeBot, Claude-SearchBot, PerplexityBot, Google-Extended…) and points at your sitemap is enough.

Updated November 5, 2026[Something unclear on this page? Tell us →](https://github.com/pixelersagency/leiho/issues/new?title=Docs%3A%20SEO%20and%20AI%20search)
