Is Astro good for SEO and AI search, and what still needs setting up?
Astro is a good fit for AI search because its default static output is plain HTML that crawlers read without running JavaScript. What it doesn’t do for you: set site and add the sitemap integration, write robots.txt and llms.txt, give each page its title, description, canonical and JSON-LD, return a real 404, and keep important text out of client:only islands. katman.pro runs on Astro; this page shows how we set it up.
Why Astro starts ahead
Astro’s output option defaults to 'static': every page is prerendered at build time (Astro configuration reference, read 15 Oct 2026). Components without a client:* directive render to HTML “without JavaScript”. Crawlers that don’t run scripts, which in Vercel and MERJ’s 2024 log study included OpenAI’s and Anthropic’s (Do AI crawlers render JavaScript?), get the full text.
The exception is client:only, which “skips HTML server rendering, and renders only on the client” (Astro directives reference, read 15 Oct 2026). A React SPA dropped into Astro as one client:only island is still an empty div to those bots.
Set it up in six steps
- Set
siteand add the sitemap.npx astro add sitemapinstalls @astrojs/sitemap (3.7.4 on npm, 15 Oct 2026); the build writessitemap-index.xmlandsitemap-0.xml. Usefilterto drop thank-you and login pages.
// astro.config.mjs
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://yoursite.com",
integrations: [sitemap({ filter: (page) => !page.includes("/thanks") })],
});
- Write robots.txt in
public/robots.txt, or as an endpoint so it readssite:
// src/pages/robots.txt.ts
import type { APIRoute } from "astro";
const bots = ["OAI-SearchBot", "ChatGPT-User", "GPTBot", "Claude-SearchBot", "Claude-User", "ClaudeBot", "PerplexityBot"];
export const GET: APIRoute = ({ site }) =>
new Response(
["*", ...bots].map((b) => `User-agent: ${b}\nAllow: /\nDisallow: /app/\n`).join("\n") +
`\nSitemap: ${new URL("sitemap-index.xml", site)}\n`,
);
- Generate llms.txt from your content, so it can’t drift from the pages:
// src/pages/llms.txt.ts
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
export const GET: APIRoute = async ({ site }) => {
const pages = await getCollection("pages");
const lines = pages.map((p) => `- [${p.data.title}](${new URL(p.id, site)}): ${p.data.description}`);
return new Response(`# Yoursite\n\n> One sentence on what you do.\n\n## Pages\n\n${lines.join("\n")}\n`);
};
- Head tags in one layout. Title, meta description and
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site)} />from each page’s props. - JSON-LD with
<script type="application/ld+json" set:html={JSON.stringify(schema)} />, stating the same facts as the visible page. - A real 404.
src/pages/404.astrobuilds404.html; check that your host serves it with a 404 status, not a 200.
How katman.pro does it
This site is an Astro 7 static build served by a Cloudflare Worker. Three choices differ from the defaults:
- Sitemap from an endpoint, not the integration, so each
lastmodis the day a page’s words last changed and scheduled pages stay out until their time. - robots.txt with one group per AI bot, generated from a list of crawler tokens, plus a pointer to llms.txt.
- Markdown twins. The Worker answers
Accept: text/markdownwith the page’s Markdown, while the HTML page stays canonical.
Example, our own measurement on 15 Oct 2026:
$ curl -s -A "...GPTBot/1.4..." https://katman.pro/method | sed 's/<[^>]*>/ /g' | wc -w
3564
$ curl -s -o /dev/null -w '%{http_code} %{content_type}\n' -H 'Accept: text/markdown' https://katman.pro/method
200 text/markdown; charset=utf-8
$ curl -s -o /dev/null -w '%{http_code}\n' https://katman.pro/this-page-does-not-exist
404
Where Astro sites still go wrong
- Text inside
client:onlyislands or fetched by an island after load. Render it in the.astropage instead. - Server islands (
server:defer) render “outside the scope of the rest of the page”, in a later request; keep key text out of them. - Dynamic routes in
output: 'server'don’t reach the sitemap integration; list them in your own sitemap endpoint. - The CDN says no. Astro’s HTML doesn’t help if Cloudflare answers AI bots with a 403: see Cloudflare blocking AI crawlers.
Astro or Next.js for a site that needs to be found
| Astro | Next.js App Router | |
|---|---|---|
| Default output | Static HTML, no JavaScript unless an island asks for it | Server-rendered or prerendered HTML plus the React runtime |
| Sitemap | @astrojs/sitemap integration or your own endpoint |
app/sitemap.ts |
| robots.txt, llms.txt | public/ or src/pages/*.ts endpoints |
app/robots.ts; llms.txt in public/ (details) |
| Typical way to lose text | client:only islands |
Text fetched in useEffect; streamed metadata |
To a crawler, both can deliver the same complete page. Pick by what the rest of the product needs, not by SEO.
Moving from React? If only the marketing pages need to be found, a common split is Astro for public pages and the existing SPA under /app (React SPA options). For llms.txt expectations, see What is llms.txt?.
Where Katman fits, and where it doesn’t
Katman is an MCP server inside your AI coding tool. katman_scan_code recognizes Astro and reports which pages ship their text without JavaScript; katman_generate writes robots.txt, llms.txt and JSON-LD from one research file, plus a server-side bot counter for Astro; katman_audit_site checks the live pages as crawlers fetch them. Your coding tool applies the changes. Katman Pro is $99 a month for up to 3 sites, with fix prompts and page briefs uncapped (what the MCP does). A clean Astro build is readable; whether an assistant recommends you depends on the pages you write, which the method covers, and nobody can promise it.
Written by the Katman team, Rnify Limited. Astro docs and npm read 15 October 2026; katman.pro measured the same day.
Who this is not for
- Astro sites behind a login: keep them out of search.
- Teams choosing a framework for a web app: this page covers the public, content side.
- Starlight docs sites that already ship a sitemap: check what your theme generates before adding another.
Frequently asked questions
Is Astro better than Next.js for SEO?
Not by default in what a crawler receives: both can send complete HTML. Astro makes static HTML the default and ships no JavaScript unless you ask for it, so there are fewer ways to break it.
Do I need @astrojs/sitemap?
It is the quickest way: npx astro add sitemap, set site, and the build writes sitemap-index.xml. Write your own endpoint instead if you want per-page lastmod dates or need dynamic routes rendered on demand, which the integration can’t list.
Where does llms.txt go in Astro?
Either public/llms.txt for a hand-written file, or src/pages/llms.txt.ts, an endpoint that Astro turns into a static file at build time in static output.
Can AI crawlers read my React or Vue islands in Astro?
Islands with client:load or client:visible are rendered to HTML on the server and hydrated later, so their text is in the page. client:only skips server rendering, so its text isn’t.
Does output: server hurt AI visibility?
No. On-demand pages are still rendered to HTML on the server. Only the sitemap integration loses track of dynamic routes, so list them yourself.
How do I send IndexNow from an Astro site?
Astro has no built-in IndexNow. Host a key file in public/ and send changed URLs to the IndexNow API after each deploy, from CI or a small script.