Skip to content
SoyGarfield
Research #02

How I built a headless corporate site with Sanity and Next.js: the QBO Imagen case

Can a client get real autonomy to edit content, with no server of their own to maintain, without losing speed or visibility on Google and on AI search engines?

By Pietro Fiorillo ·

In this article
Comparison diagram. Traditional site: client requests a change, ticket to a developer, edit code and test, manual deploy, change visible days later. Headless architecture: client edits in Sanity Studio, build with Next.js and GROQ, static export, Firebase Hosting, change visible minutes later.
From depending on a developer for every change to a headless CMS the client manages alone. Original diagram.

The problem: a studio that couldn't depend on a developer for every change

QBO Imagen is a graphic design, signage and applied marketing studio in Neuquén, Argentina. Its team needed to update the portfolio, services, testimonials and contact details on its own, without messaging me every time a new job came in or a phone number changed.

On top of that came two technical requirements: the site had to load fast across the whole country, since most of QBO's clients are remote, and it had to compete for visibility not only on Google but also on the AI search engines that gained ground through 2026.

Why I chose Sanity and Next.js over a traditional CMS

A traditional CMS (WordPress, for example) mixes content, presentation logic and a server in the same installation. It works, but every update, every plugin and every theme is one more attack surface and one more point of failure to maintain.

I chose to separate the two. Content lives in Sanity Studio, a visual editor QBO opens from the browser, and code lives in a Next.js repository I maintain. Next.js fetches Sanity's content at build time, via GROQ, Sanity's query language, and generates the site as static HTML, CSS and JS with output: export. Those files are what end up on Firebase Hosting.

The most important consequence: there is no Node server running in production. What the visitor sees is HTML served from a CDN. There is no database to attack, no runtime to patch, no cold start.

How it works in practice: from the Studio to Firebase Hosting

The full flow has three distinct moments. The client edits content in Sanity Studio whenever they want. The build runs when someone (for now, me) runs npm run build: at that point Next.js fetches six GROQ queries in parallel (site settings, services, portfolio, stats, testimonials and clients) and assembles every page from that data. The deploy pushes the resulting /out folder to Firebase Hosting with firebase deploy.

Between one build and the next, the public site never queries Sanity again. If QBO uploads a new piece to the portfolio at noon, it won't show up on the site until someone runs the next build. That is the least automated part of the system, and I document it without dressing it up in the technical debt section below.

What happens at each layer of the architecture. Own work based on the project's code.
LayerWhat it doesWhen it updates
Sanity StudioThe client edits portfolio, services, testimonials and contact detailsAny time, from the browser
Build (Next.js + GROQ)Reads Sanity's content and generates static HTML, CSS and JSWhen someone runs npm run build
Firebase HostingServes the static files from a global CDNOn every firebase deploy

How I validated QBO Imagen's architecture

For this article I didn't just describe the architecture from memory: I split the review into four areas and left a verifiable trail for each. This matches the project's state as documented in October 2026; pending improvements are in the final section.

Static generation. I ran next build locally (Next.js 16.2.9 with Turbopack, Node 24.11.1) and inspected the resulting /out folder to check what content actually ends up in the HTML before deploy.

Content management. I reviewed how Sanity Studio models services, portfolio, testimonials and contact details, and read the real GROQ queries that fetch them at build time, in sanity/lib/queries.ts.

Fault tolerance. I read the real implementation of safeFetch and its fallbacks. It does exactly what I say it does, no more and no less: it keeps a failed Sanity query from breaking the build, but it doesn't protect against other build or deploy errors.

Technical SEO. I reviewed the real metadata, structured data, sitemap and robots.txt in the repository. That these elements exist doesn't by itself prove the site is indexed or that it shows up in AI system responses: I didn't measure that in this entry.

async function safeFetch<T>(query: string, fallback: T): Promise<T> {
  if (!isSanityConfigured) return fallback;
  try {
    return await client.fetch(query, {}, { next: { revalidate: 60 } });
  } catch {
    return fallback;
  }
}
Real code from sanity/lib/queries.ts in the project's repository, October 2026.
const nextConfig: NextConfig = {
  ...(process.env.NODE_ENV === "production" ? { output: "export" as const } : {}),
  images: {
    unoptimized: true,
    // remotePatterns: cdn.sanity.io, img.youtube.com
  },
};
Real code from next.config.ts. Static export only activates in production; in development the site runs on the normal Next.js server.

Measurable results: what it measures and what it doesn't

I measured the following on October 18, 2026, against the production hosting on Firebase. This is lab data (Lighthouse, with simulated throttling), not field data from real users (CrUX), and I'm being explicit about that because it's a distinction that's almost never explained.

Real metrics measured on October 18, 2026. Lighthouse 13.5.0, mobile form factor, simulated throttling.
MetricResultEnvironment / evidence
Lighthouse Performance (mobile, lab)64 / 100Lighthouse 13.5.0, simulated throttling
LCP (lab)9.9 sSame Lighthouse run, not field (CrUX) data
CLS (lab)0Same Lighthouse run
Local build time28.1 snext build with Turbopack, Next.js 16.2.9, Node 24.11.1
Static export size33 MB across 205 files/out folder generated by that build
URLs indexed on GoogleNot measured in this entryRequires Search Console access, which I didn't use for this article
Build to published timeNot measured in this entryI measured the local build; the firebase deploy step wasn't timed here

A 9.9 second LCP is slow, even for a static site. The most likely cause is in the code itself: next.config.ts forces images.unoptimized: true, a requirement of static export, so the hero carousel and the portfolio images never go through any image optimization pipeline. That's a real pending improvement, not a minor caveat, and I've added it to the next steps below.

The fallback first pattern: what it protects and what it doesn't

If Sanity doesn't respond during a build, or an environment variable is missing, the fallback first pattern keeps that specific problem from breaking the site's generation. Every component that depends on external content (the hero, the portfolio, the testimonials, the footer) carries its own hand written fallback content inside the component.

The function that handles this, safeFetch, always follows the same rule: if Sanity isn't configured, it returns the fallback without attempting the network call; if the call fails for any reason, it catches the error silently and returns the same fallback.

This protects against one specific problem: Sanity not responding. It doesn't protect against a TypeScript error, a broken dependency or a failure in the build process itself. The fallback first pattern is a bounded safety net, not a guarantee that the site will never break.

SEO isn't enough: what the robots.txt controls, and what it doesn't

The site invests in visibility for two different kinds of crawler. For traditional search engines, there's structured data for Organization, WebSite and LocalBusiness, a sitemap with priorities by page type and dynamic metadata per service landing.

For AI crawlers, the robots.txt explicitly allows GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended and others, following the robots exclusion protocol, while blocking aggressive SEO scrapers like AhrefsBot or SemrushBot.

One of those tokens is worth being precise about: Google-Extended is not Googlebot and doesn't decide what gets indexed in Google Search. According to Google's own documentation, it controls whether crawled content can be used to train Gemini models and to ground answers with the Search index; it doesn't affect a site's inclusion in Google Search and isn't a ranking signal. Enabling it isn't the same as improving how a site appears in AI Overviews, and I haven't measured anything that supports that link.

Content autonomy isn't a feature you bolt on at the end, it's an architecture decision you make at the start.

Pietro Fiorillo

My part in this project

To be clear about what I mean by «built»: my part in QBO Imagen included designing the headless architecture, integrating Sanity CMS, building the frontend with Next.js, configuring the static export, deploying to Firebase Hosting and the technical SEO and GEO strategy, as part of my work at MANYA Digital.

What I'd do differently: technical debt without the gloss

I documented this debt in detail in the project's architecture notes, and I repeat it here because showing it feels more honest than hiding it. Every page declares revalidate: 60, which is ISR semantics, but static export doesn't support ISR: that value has no real effect, content only updates with a manual build.

Two server components (the footer and the navbar) fetch their own data independently of the rest of the page, so the same information gets requested twice in the same build. And the sitemap lists the service landing slugs by hand: if a new landing is created in Sanity, the route works but it won't show up in the sitemap until someone adds it to the file.

None of these three points breaks the site today. I leave them documented because they're exactly the kind of decision a new developer needs to understand before touching the code, and because they're part of how this was actually built, not how I'd like to tell it.

What I'd test next

The next technical step is connecting a Sanity webhook to a GitHub Action that triggers the build and deploy automatically when the client publishes a change, removing the manual step that depends on me today. I'd also optimize image loading for the hero and portfolio to bring down the measured 9.9s LCP, generate the sitemap dynamically from the real landings instead of keeping the list by hand, and use sanity typegen so TypeScript types come from the real schema instead of being written by hand.

Until that happens, the site still works well for what QBO needs: content editing autonomy, a technical base that's measured, not just promised, and a setup ready for both Google and AI search engines.

Sources

Frequently asked questions

What is a headless architecture?

A headless architecture separates content management from its presentation. In QBO Imagen, Sanity acts as the CMS and Next.js generates the pages visitors receive. This lets content be managed from an editor that is independent of the site's code.

Why Sanity and not WordPress?

I chose Sanity because I needed to model structured content and keep the public interface separate from the editorial system. WordPress can also be used as a headless CMS, so this isn't a limitation of WordPress: it was a decision based on this project's specific requirements.

What does static export mean and why does it matter for SEO?

Static export generates HTML files at build time. This delivers the main content without depending on browser rendering and reduces the need to run server logic per request. It doesn't guarantee better Google rankings on its own.

How does the site get visibility on ChatGPT, Perplexity or Google AI Overviews?

The architecture makes it easier for authorized crawlers to access the HTML content, links, metadata and structured data. Allowing crawling doesn't guarantee that AI systems will retrieve, recommend or cite the site: that depends on factors I don't control and didn't measure here.

What happens if Sanity goes down during a build?

The safeFetch function returns fallback content when it can't get data from the CMS, so those sections can still be generated. It doesn't protect against other possible build errors, like a TypeScript or dependency error.

How much does it cost to maintain a site like this?

It depends on Sanity usage, traffic, storage, builds and the services in place. This project doesn't need its own Node server to serve public pages, but there are hosting, CMS, domain and technical maintenance costs.