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.

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.

Something unclear on this page? Tell us →