Building a site that answer engines can actually read
Rebuilding useion.ai as a static Astro site: what changed, why every page ships as real HTML, and the difference between writing for search engines and writing for the things that answer questions about you.

The Ion site started as one hand-written index.html: a logo, a headline, an email
box, and a canvas animation behind it all. That is a perfectly good landing page. It
is a terrible thing to try to rank, and a worse thing to try to get quoted.
This post is what I changed and why, mostly so future-me remembers the reasoning when the temptation to add a client-side router arrives.
The problem with a one-page splash
A splash page fails on two fronts at once.
The search engine problem is boring and well understood: one URL, one heading, no internal links, no feed, nothing to index beyond a tagline. There is nothing there to match a query against.
The second problem is newer. Increasingly the thing describing your product to someone is not a search result — it is an assistant answering “what is Ion?” in prose. That system needs a sentence it can lift, attributed to a URL it can cite. A hero image with the tagline burned into it gives it nothing. Neither does a paragraph that only makes sense after reading the two before it.
So the rebuild had one rule: every claim the site makes should be a complete sentence, in the HTML, on a URL.
Why Astro
The stack had to satisfy three things that pull in different directions.
| Requirement | Why it constrains the choice |
|---|---|
| Real HTML for every page | Rules out a client-rendered SPA |
| A blog with Markdown files | Rules out hand-writing pages |
| Room for Vue later | Rules out a pure static-site generator with no component story |
Astro does all three. Pages are pre-rendered to static HTML at build time, so a crawler — or a model with a fetch tool and no JavaScript runtime — sees the finished document. Content collections turn a folder of Markdown into typed, validated entries. And when the blog eventually needs an editor UI backed by Mongo, Vue components drop in as islands without converting the rest of the site into an app.
The signup form is already one of those islands. It hydrates after the browser is idle, and everything around it is inert HTML.

What “AEO” actually means in files
“Answer engine optimisation” gets used loosely. Concretely, here is what it turned into on this site.
A FAQ that is content, not decoration. Seven questions, each with a
self-contained answer, rendered as visible text and emitted as a
schema.org/FAQPage node. The answers are written to survive being quoted with no
surrounding context. Where the honest answer is “not announced yet”, that is what
it says — a made-up launch date is a claim that gets repeated back at you.
Structured data as one graph, not scattered tags. Every page emits a single
@graph containing Organization, WebSite, and the page node, all with stable
@ids. That is what lets a crawler merge every URL into one entity instead of
seeing a new anonymous publisher each time.
/llms.txt and /llms-full.txt. The first is a Markdown map of the site; the
second inlines every post body. Both are generated from the same content
collection as the HTML, so they cannot drift.
A robots.txt that lets the AI crawlers in. This one is a judgement call
rather than a technique. Blocking GPTBot and ClaudeBot does not remove you from
anything already trained; it removes you from answers being composed today. For a
product nobody has heard of yet, that trade is not close.

The unglamorous half
Most of the actual work was not any of the above.
- Fonts are self-hosted at build time instead of fetched from a third-party stylesheet on the critical path.
- The two-glyph icon font is gone, replaced by inline SVG.
- The theme is applied by a tiny blocking script before first paint, so choosing dark does not mean watching a cream flash on every navigation.
- The logo swaps by CSS rather than by rewriting
img.srcafter load. - The background animation stops when the tab is hidden, and never starts at all
under
prefers-reduced-motion.
None of that shows up in a screenshot. All of it shows up in Core Web Vitals, and Core Web Vitals are the part of ranking you can actually control by editing files.
One real bug, found on the way
The old script picked its API origin at runtime:
if (host === 'www.useion.ai') {
return 'https://api.useion.ai';
}
return 'https://nataly-unsneering-pinnately.ngrok-free.dev';
www.useion.ai does not resolve. The site is served from the apex domain, so the
condition never matched, and every production signup was being posted to a personal
ngrok tunnel. It is now resolved at build time from an environment variable, with
the dev build pointing at the local FastAPI backend through the dev server proxy.

That is the recurring shape of this kind of work: you sit down to improve how a page is indexed, and half of what you find is something that was quietly broken in a way nobody was going to notice from the outside.
What is next
The backend is running but idle — FastAPI and Mongo, wired up and serving the signup endpoints, waiting for the blog to need an editor rather than a folder of Markdown files. When that happens the posts move into Mongo and this page keeps its URL.
Until then: files in a folder, one build command, static HTML out the other end.