How we shipped aiso-hub on Astro, Sanity, Cloudflare
The stack, the timeline, the trade-offs, and the one decision we'd make differently.
TL;DR
- Stack: Astro 5, Tailwind 4, @sanity/client, Cloudflare Pages, Resend, PostHog, Pagefind, self-hosted Inter Variable.
- Scope: 30+ pages, full i18n (en / pt-pt / fr), Sanity-CMS editorial blog, lead form, newsletter, exit-intent, citation-schema JSON-LD.
- Timeline: skeleton to production in ~6 weeks with iterative content load.
- Lighthouse: 95+ performance / 100 SEO / 95+ accessibility on the core templates.
- One reversal: we’d self-host the fonts on day one, not week three.
What we were solving
aiso-hub.com is AISO Group’s AI search optimization practice. The site needs to do three things well:
- Rank and be citable. It sells against Google AI Overviews, Perplexity, ChatGPT web search, and an emerging AISO category. It has to walk the walk.
- Host long-form editorial. Hub’s content pillar is long, evergreen authority content.
- Ship multilingual (en / pt-pt / fr) without a CMS that turns editorial into a chore.
The old site was a single-page Next.js hero. Fast to ship, impossible to grow. The brief was a textbook custom AI web build: static performance budget, editorial CMS, multilingual routing, and AI-citation hygiene baked in.
The stack choices
Framework: Astro 5.
Static by default, component islands where we need interactivity. No hydration penalty on article pages. Astro’s content model (src/content/ + Sanity) let us co-locate marketing pages and CMS-backed editorial without a second app. It’s also trivially crawlable - important when you’re optimizing for both Google and AI citation.
Styling: Tailwind 4 + a small utility layer.
We promoted a handful of patterns into global.css - .section, .container, .kicker, .btn-primary, .lead, .h3, .insight-card__*, .article-body. The rest is Tailwind inline. This keeps the design system small enough to read in one sitting and big enough that new pages don’t drift.
CMS: Sanity. Editorial content on Sanity, marketing pages on Astro. The split works because editors live in Sanity Studio (fast, structured, good image handling) and the marketing team commits marketing-copy changes via PR (good for versioning and review). Sanity’s GROQ + @sanity/client integrate into Astro’s static build cleanly.
Schemas we settled on: insightArticle, author, faq, contentPiece, plus localeString, localeText, localeMarkdown for i18n. One dataset per environment (production + preview), one project.
Infra: Cloudflare Pages + Workers. Pages for the static build. Workers for the lead form endpoint (Resend-backed) and the newsletter signup. Global CDN is free and the preview-URL-per-PR flow changed how we review content.
Analytics: PostHog. Self-hosted on the EU region, no third-party tracking scripts on the page.
Search: Pagefind. Client-side search across the whole site, zero infra. Prebuilt at build time. Blows away any in-page search we’ve used before.
Architecture at a glance
┌─────────────────┐
│ Sanity Studio │ (editors)
└────────┬────────┘
│ GROQ
▼
┌────────────────┐ Astro build ┌──────────────────┐
│ Content/ MDX │────────────────▶│ Static HTML + │
│ (marketing) │ │ per-locale dirs │
└────────────────┘ └─────────┬────────┘
│ deploy
▼
┌──────────────────┐
│ Cloudflare Pages│
└──────────────────┘
│
┌──────────────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌──────────┐
│ Pagefind │ │ CF Workers │ │ PostHog │
│ (static) │ │ (forms) │ │ (EU) │
└──────────┘ └────────────┘ └──────────┘
The timeline
- Week 1 - Design + IA. Wireframes, page inventory, content model, component spec. Locked on the
.section/.containerprimitive before a single component was written. - Weeks 2-3 - Skeleton. BaseLayout, Nav, Footer, hero, solution cards, article template, insight card grid, Sanity schemas. First Cloudflare preview URL live on day 10.
- Weeks 4-5 - Content + features. Lead form, newsletter signup, exit-intent, breadcrumb, TOC auto-builder on article pages, author panel, related articles, citation schema, multilingual routing.
- Week 6 - Perf, a11y, polish. Self-host Inter Variable (we’d started on Google Fonts - mistake, see below), axe-core a11y sweep, image optimization, Lighthouse tuning.
- Ongoing - content load. Editorial team ships 1-2 long-form posts per month. Marketing copy iterates via PR.
Lighthouse numbers
On the home, solutions, and article templates:
- Performance: 95-99
- Accessibility: 95-98
- Best Practices: 100
- SEO: 100
LCP sits under 1.2s on a fast connection; CLS is near zero (self-host the fonts, ship width/height on images); TBT stays negligible because Astro doesn’t hydrate content pages.
The same shape ships well across our AI implementation services - the build below is a working reference, not a one-off.
What broke (or almost did)
- Google Fonts as a
<link>. First week’s build loaded four Inter weights via Google’s CDN. CLS was terrible on slow 3G and we were leaking referrer to Google. Fix:@fontsource-variable/inter, one file, self-hosted,font-display: swap. Lesson: self-host from day one. - Sanity datasets. We started with a single dataset and paid for the
productionaccidentally-edited-by-preview pain. Moved toproduction+previewdatasets on day 14. No regrets. - i18n routing. Astro’s i18n story is young. We ended up with explicit per-locale directories (
/en/,/pt-pt/,/fr/) and a centralized locale strings table, rather than middleware-driven. Simpler to read, trivially static. - Mobile menu a11y. We shipped v0 without
aria-expanded/aria-controlson the toggle. A11y sweep caught it week 6. Lesson: add the attributes when the toggle is first written, not retroactively.
One decision we’d reverse
Starting on Google Fonts. It cost a week of chasing CLS and LCP issues and gave us nothing. Self-host from day one - the @fontsource-variable/inter package is three lines of setup.
What the stack doesn’t do
This is the set that fits marketing + editorial. We wouldn’t ship a high-interaction SaaS dashboard on Astro alone - we’d pair it with a Next.js app for the authenticated surface and use Astro for the marketing frontend. Use the right tool for the surface.
The files that did the heavy lifting
src/styles/global.css- the utility layer.src/layouts/BaseLayout.astro- canonical URL, OG/Twitter/Citation meta, JSON-LD, PostHog, Resend wiring.src/components/InsightCard.astro+src/pages/insights/[slug].astro- the editorial template.src/components/LeadForm.astro- Cloudflare Worker-backed form with Resend.scripts/build-pagefind.sh- postbuild search index.
Reusing this
We ship the same stack for external clients - if you’d rather hire AI developers than spin up an in-house team for one project, this is the shape of the engagement. Prices are on the pricing page. The full build log for the multi-agent orchestrator is next up.
Want the same shape? Scope a custom web build →
Prefer a scoping call first? Free AI Readiness Audit →
From insight to system
These posts come out of real client work. Here is how that work is scoped.
AI implementation services for SMBs
Audits, agentic systems, data cleanup, custom builds. Fixed scope, fixed price.
Published retainers and project bands
From EUR 990 per month or EUR 2,500 fixed scope. No discovery-call paywall.
Free AI Readiness Audit
30-minute scoping call plus a written assessment. You get value either way.
Got a project that should be a build log?
Scope a project. We'll ship it. You get a system; we may write about it (with permission).