Astro is the easy case. By default it renders every page to HTML at build time and ships no client-side JavaScript unless a component asks for it, so server-rendered parity, the check that costs most sites the most, tends to pass without anyone having thought about it. The checks an Astro site fails by default are the ones no framework does for you: the text files at the root, the structured data in the head, and the links that let a model confirm who you are. This post goes through what the default gives you, where it can be undone, and the remaining checks in order of effort.
Everything said here about the framework is limited to documented behaviour at docs.astro.build as of this writing.
What the default gives you
An Astro page is a template that runs at build time, or on request if you choose an adapter and server output, and produces plain HTML. Interactivity comes from islands: components from React, Vue, Svelte or another framework that are rendered to HTML on the server and then, only if you add a client: directive, hydrated in the browser. The rest of the page is inert markup.
Through the scanner's eyes that means the raw fetch and the rendered fetch are close to identical. A7 divides visible text in the raw HTML by visible text after render, wants a ratio of 0.8 or better for its five points, and on a typical Astro site reads at or near 1. B5, the clean text ratio, benefits too: no framework runtime, no hydration payload, and markup that is roughly what you wrote. B3 depends on your templates rather than the framework, but nothing in Astro fights a single h1 and real landmarks.
The one way to undo this is client:only. That directive skips server rendering for the island entirely, so its content exists only after hydration. A page that is one large client:only island is a single-page application with an Astro wrapper, and A7 will say so. If you need a component to be client-only for a technical reason, keep it small and keep the copy an agent needs outside it. Measuring server-rendered parity has a script for checking a route before and after.
What it misses by default
Four checks are worth thirteen points between them and are absent from a fresh Astro project, because they are files and fields rather than rendering behaviour.
| Check | Points | Effort | What is missing by default |
|---|---|---|---|
| A4 | 4 | Low | llms.txt and llms-full.txt |
| D6 | 2 | Low | /.well-known/security.txt |
| D2 | 4 | Low | Organization schema with sameAs |
| B1 | 5 | Medium | Any JSON-LD at all |
There is also robots.txt (A1, A2) and the sitemap (A3), which Astro does not write on its own either; an official sitemap integration is documented, and a robots.txt is a static file. The Astro-specific point is only that you must add them.
Low effort, static files
Astro serves anything in the public/ directory verbatim at the matching path. That is the whole mechanism for three of the four.
security.txt. Create public/.well-known/security.txt with a Contact line and an Expires line dated in the future. The scanner checks both, and an Expires in the past fails the file even though it was correct when written, so put a reminder in the calendar or generate the file at build time with a rolling date. security.txt in ten minutes covers the RFC and the rolling-window approach.
llms.txt, the static version. Create public/llms.txt with an h1, a one-line summary and a list of links, each with a description after a colon. A4 awards three points once at least three links are described, and one more for an llms-full.txt beside it. A bare list of URLs scores one. How to write an llms.txt that earns its place has a complete example.
The static version is the right first step. It has one weakness, which the next section fixes.
Medium effort, a generated llms.txt from content collections
A hand-written llms.txt goes stale the day someone publishes a page and forgets it. Astro's content collections are a documented way to define a typed set of content, blog posts, docs, case studies, with frontmatter you control, and to query that set at build time. Astro also supports endpoints: a file under src/pages/ that returns a response rather than a page, so a route like /llms.txt can be produced by code at build time.
Put those two together and the file writes itself. The endpoint queries the collection, filters out drafts and future-dated entries, and emits one described link per entry using the title and description already in the frontmatter. The output looks like this:
# Acme
> Accounts payable software for firms with 20 to 200 staff.
## Product
- [Pricing](https://www.example.com/pricing): Three plans, per-seat, annual and monthly.
- [Security](https://www.example.com/security): Data residency, SOC 2 status, subprocessors.
## Guides
- [Closing the month in two days](https://www.example.com/guides/month-end): A checklist for a controller running month-end for the first time.
- [Approval chains that scale](https://www.example.com/guides/approvals): How to set thresholds so nothing waits on the CFO.
The same pattern produces llms-full.txt by rendering each entry's body to plain text and concatenating, which earns the fourth A4 point. Our own llms.txt and llms-full.txt are generated from a single loader on the same principle, so a post cannot appear on one and be missing from the other. The exact collection and endpoint APIs are in the Astro docs and have changed between major versions, which is why this describes the shape rather than the code.
Medium effort, JSON-LD in the layout
Astro does not emit structured data. B1 wants a parseable application/ld+json block on the homepage, worth three points, and on at least half of crawled pages, worth two more. B2 wants the types to fit the site: Organization or WebSite sitewide, plus Article, Product, FAQPage or similar for the page type.
The place to do this is the layout, not the page. A base layout that renders an Organization block on every page satisfies the sitewide half of B2 and the coverage half of B1 in one edit. Page-type layouts add the specific block: the blog layout emits an Article with datePublished and dateModified from the collection entry.
Astro's set:html directive is the documented way to write a serialised object into a script element without it being escaped. Serialise with JSON.stringify and let the scanner's parser confirm it: an invalid block scores zero, exactly as if it were absent, and JSON-LD that parses lists the ways a template usually breaks one.
Low effort, sameAs
D2 gives two points for a discoverable contact page or mailto link, which most sites already have, and two for Organization schema carrying at least one sameAs URL. Once the Organization block is in the layout, sameAs is one array:
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Acme",
"url": "https://www.example.com/",
"sameAs": [
"https://www.linkedin.com/company/example",
"https://github.com/example"
]
}
One entry is enough; the point of the field is that a model can follow a link to a profile you do not control and find the same name there. Verifiable identity covers what makes a good sameAs target.
What needs an architectural change
Almost nothing. The High effort checks in the rubric are A7, C4 and C5, and Astro gives you A7 for free unless you take it away with client:only. C4, machine endpoints, is not a framework question; an agents.json in public/ is a static file, and an OpenAPI spec for a site with no API is another. The architectural decisions that do matter are upstream of the framework: what sits in front of the origin, and whether it refuses crawlers before Astro serves a byte. Cloudflare blocks AI crawlers by default is the usual cause of an Astro site that renders perfectly and scores badly.
Check your own site
Run the free scan. An Astro site with the static files and layout blocks above in place should have nothing left on the report but the checks that depend on your copy. The check definitions are on the methodology page under discovery and access for A4 and A7, and trust and freshness for D2 and D6.