AI answer engines — ChatGPT, Perplexity, Gemini, Claude — are becoming a real referral channel. When someone asks "who can build me an AI voice agent in Dubai?", the model answering that question has to decide what your site is from whatever it managed to crawl.
llms.txt is the emerging convention for making that easy: a single markdown file at the root of your domain that gives language models a clean, curated map of your site — what you do, what your best pages are, and why they're worth citing. Think of it as a sitemap written for readers instead of crawlers.
We shipped one for our studio site this week. Here's the working example, and the entire implementation.
Live example: techpotions.com/llms.txt
The shape of a good llms.txt
The spec is deliberately simple: an # H1 with your name, a > blockquote summary, then markdown sections of links with one-line descriptions. Ours has four sections:
- Services — the commercial pillars, one line each
- Selected work — real client case studies (the proof an AI engine can cite)
- Comparisons — our "X vs Y" evergreen pages, which are exactly the kind of content answer engines love to pull from
- Company — about, contact, how to start a project
The mistake to avoid: dumping your entire sitemap into it. The value is curation — high-signal pages with honest descriptions, not 4,000 URLs.
The Next.js implementation
We generate it in an App Router route handler instead of a static file, for one reason: it should never drift from the site. It's built from the same data sources as the pages themselves — static config for services and comparisons, the CMS (Payload) for published posts and case studies. Publish a new case study, and it shows up in llms.txt within the hour, automatically.
app/llms.txt/route.ts:
export const dynamic = 'force-static'
export const revalidate = 3600 // crawler-only route: hourly ISR is plenty
function bullet(label: string, url: string, desc?: string | null): string {
const d = oneLine(desc) // collapse whitespace, trim to ~160 chars
return `- [${label}](${url})${d ? `: ${d}` : ''}`
}
export async function GET() {
const lines: string[] = []
lines.push(`# ${SITE_NAME}`, '', `> ${DEFAULT_SEO.description}`, '')
lines.push('## Services', '')
for (const s of SERVICES) {
lines.push(bullet(s.title, canonical(`/services/${s.slug}`), s.summary))
}
// Published case studies + posts from the CMS — wrapped in try/catch so
// the route still emits the static sections if the DB is unreachable.
try {
const { posts, cases } = await fetchDocs() // Payload: _status = published
lines.push('', '## Selected work (real client builds)', '')
for (const c of cases) {
lines.push(bullet(c.title, canonical(`/work/${c.slug}`), c.summary))
}
// ...same pattern for comparisons and posts
} catch {
// DB down at build time? Ship the static sections anyway.
}
return new Response(lines.join('\n'), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400',
},
})
}
A few decisions worth stealing:
-
force-static+ hourlyrevalidate. No model needs a real-time view of your site, and this keeps the CMS off the hot path entirely. Same caching strategy as oursitemap.ts. - Generate from the same sources as the site. A hand-maintained llms.txt is stale within a month. Ours can't be — it reads the exact config objects and CMS queries the pages render from.
-
Fail open. If the database is unreachable during a build, the
catchblock still emits the static sections. A degraded llms.txt beats a 500. - One-line descriptions, truncated. Models don't need your whole meta description; ~160 clean characters per link keeps the file scannable end to end.
Does it work?
Honest answer: it's early, and nobody outside the answer-engine companies can tell you the citation weight of llms.txt today. What we can say is that the file costs about a hundred lines of code, removes any guesswork about how models summarize you, and the convention has real momentum. For a marketing site, that's an easy trade.
We're TechPotions, a software & AI studio — we build production Next.js apps and AI agents (see our work). If you're weighing a build, our comparison guides are a good place to start.
Top comments (2)
actually a smart way to handle this, way better than just hoping the crawler gets the sitemap right
Thanks Dmitry! The sitemap comparison is exactly it - a sitemap tells a crawler what exists, but nothing about what matters. The curation (one honest line per page) is the part that actually changes how assistants describe you. If you end up wiring one into your own site, happy to answer anything.