A bilingual visa information platform engineered to stay current and be cited by AI search
In immigration content a single policy change can quietly break a page, and wrong guidance is a legal exposure. We built a bilingual visa information platform server-rendered through OpenNext on Cloudflare Workers, with a versioned timeliness layer, per-template structured data, and per-language metadata so search engines and AI answer engines both cite it without doubling the maintenance surface.
Updated 2026-06-20
Outcomes
- Server-rendered
- Content
- Every page
- Structured data
- Open and tracked
- AI crawler access
Services
- SEO
- White-hat GEO
Stack
The build
The pain anyone in immigration content already knows
The shelf life of immigration content is measured in weeks. A processing time shifts, a visa subclass closes, an eligibility rule gets reworded — and a page that was correct on Monday is quietly wrong by Friday. Nobody gets an alert. The page still ranks, still gets read, and now it is handing out guidance that is a legal exposure rather than a help. Anyone who has run a visa or migration site knows the real fear isn’t a slow month of traffic; it’s a confidently outdated page that someone acts on.
On top of that, you have to be visible in two places that pull in opposite directions: rank in classic search, and get cited inside AI answers — in more than one language, and without ever looking like you’re gaming it. Most content sites are client-rendered SPAs that read fine in a browser and look completely blank to the crawlers that decide both of those things, and they have no mechanism at all for telling a reader, or an engine, how fresh a page actually is.
The constraints that shaped the build
Three things were non-negotiable. The content had to be machine-readable to bots that do not run JavaScript. It had to carry its own freshness so an out-of-date page could be caught and labeled instead of quietly served. And it had to do both in two languages without doubling the maintenance surface.
The first one settles the architecture on its own. AI retrieval crawlers mostly do not render client-side JavaScript — an analysis of more than half a billion GPTBot fetches found that AI crawlers don’t render client-side JavaScript, so a page that paints its content in the browser is, to them, blank. A client-rendered SPA was out before we wrote a line of code.
The other two are less about a framework choice and more about discipline that has to survive every future edit. Freshness is only useful if it can’t be forgotten, and bilingual parity is only cheap if the two languages share one source of truth rather than drifting into two sites that happen to look alike. Both of those turned into structural decisions, not conventions we asked authors to remember.
The constraint underneath all three is legal. On most content sites a stale or mistranslated page is an embarrassment; here it’s advice someone may act on at a border or in an application. That reframes ordinary tradeoffs. When freshness and convenience conflict, freshness wins. When a fast-but-fuzzy automated translation conflicts with a slower reviewed one, the reviewed one ships. The whole build leans toward failing loudly and toward never letting an unverified claim look authoritative.
How we built it
The site runs on Next.js with the App Router, rendered through OpenNext on Cloudflare Workers. Content is server-rendered, and static wherever the data allows, so every route ships complete HTML in the first response. A search crawler and a non-rendering AI bot see the same full text a human does — that is the whole point, and it is the thing a SPA can’t give you.
We chose OpenNext on Workers over the more common Node hosting deliberately. The pages are read-heavy and edit-light: a guide changes when a policy changes, not on every request. That profile wants aggressive caching at the edge and cheap, globally distributed delivery, which is exactly what Workers gives us, while OpenNext lets us keep writing standard App Router code instead of hand-rolling an edge runtime. The cost we accepted is a tighter runtime — no arbitrary Node APIs, watch the bundle, mind cold paths — but for a content platform that’s a fair trade for HTML that’s already at the edge when a crawler asks for it.
The trap with the App Router is that metadata fails silently. Pull content into a client component and the canonical link, the hreflang alternates, and the per-page title quietly fall back to defaults, and nothing errors. On a bilingual site that breaks the language signals search engines rely on. So each route’s metadata lives in its server shell, generated from the same content record that renders the body, and we verify it from the rendered HTML rather than trusting the framework to carry it through.
The timeliness layer
Freshness gets its own layer rather than a footnote, because “remember to update the page” is not a system. Each piece of guidance carries a structured record of when its underlying policy was last confirmed and which source it was checked against, separate from the cosmetic “last edited” timestamp a CMS gives you for free. Editing a typo does not reset the policy clock; only re-confirming the rule does. That distinction matters — it’s the difference between “we touched this file” and “we checked this is still true.”
From that record the system does two things. It prints an explicit “as of” date on the page, which does double duty: freshness is a ranking and a citation signal, and for a reader it draws the line between current rules and rules that need re-checking. And it runs a review cadence — once a policy’s confirmation date crosses a threshold, the page is flagged for review rather than left to rot, so a human looks at it before a reader does. The threshold is tuned per topic, because processing-time pages go stale faster than a stable eligibility definition.
The labeling is conservative by design. On visa topics a wrong date is worse than no date, so the layer would rather flag a page for review than let a possibly-stale one pass as current. We considered auto-expiring or hiding flagged pages outright and decided against it: a page that vanishes loses its rankings and its inbound links, and a clearly-dated page under review is more honest to a reader than a 404. So flagged pages stay live, carry their date plainly, and move up the editorial queue.
Two languages, one source
Bilingual parity is enforced by routing, not by goodwill. Locale is a route segment, each piece of content is one record with two language fields, and the page is built from that single record. There is no second site to keep in sync; a structural change to a template lands in both languages at once. What stays language-specific is exactly what should be — the prose and, critically, the metadata.
Each locale emits its own JSON-LD and its own hreflang set, generated from that shared record, so an engine gets correct per-language structured data and a clean reciprocal map between the two URLs. Get the hreflang reciprocity wrong and search engines treat the pair as duplicates or serve the wrong language; because both sides are generated from one record rather than maintained by hand, they can’t fall out of agreement. The “as of” dating carries per language too, so a rule re-confirmed in one language doesn’t silently imply the translation was re-checked — each side owns its own freshness.
Getting cited, and measuring it
Ranking gets you a link in a list; getting quoted inside an answer is a different problem with concrete levers. Every template carries JSON-LD structured data — Article, FAQ, Breadcrumb, chosen per template type rather than sprayed uniformly — so an engine reads the facts off the markup instead of inferring them from prose. The original GEO research found that adding citations and sources is among the strongest levers for getting surfaced in AI answers, which is exactly what the markup and the “as of” dating give an engine to work with: a dated, sourced, machine-readable claim is easier to lift into an answer than the same fact buried in a paragraph.
GA4 routes AI-answer-engine referrals into their own channel, so the citation work is measured rather than assumed. We can see which pages actually earn references from answer engines and write more of those, instead of guessing which structured-data and freshness choices paid off. It turns GEO from a belief into a feedback loop.
We also shipped an llms.txt and opened the AI crawlers in robots. We treat that as a cheap hedge, not a lever. A study across 300,000 domains found llms.txt shows no measurable effect on AI citations, with adoption near 10%. It costs a few minutes, so it stays — but the citations are earned by the server-rendered HTML and the structured data, not by that file. Opening the AI crawlers was its own deliberate call: some publishers block them, but for a platform whose goal is to be cited, the bots you want quoting you are the ones you have to let in.
The result
Every page ships server-rendered HTML with structured data, in both languages, readable by search crawlers and non-rendering AI bots alike. The content carries its own per-language “as of” date and is flagged for review the moment its policy clock runs out, so stale guidance gets caught instead of quietly served. The two languages stay in lockstep because they’re built from one record, not two sites. And it is instrumented, so the team can see which pages get cited and keep writing the ones that do — built for a field where being found and being current are the same job.