Ceruvia Cosmetics
Clean makeup brand · Buenos Aires · Beauty & Fashion
Fixing a beautiful headless store that search engines couldn't see
6-month engagement · Signal (Technical SEO) · Latin America
The mirror showed a beautiful store. Google saw an empty case.
A headless rebuild looked stunning but rendered product content client-side, and organic traffic fell 46% within a month of launch.
Ceruvia's new storefront was a design triumph: fluid shade swatches, a virtual try-on, editorial product stories. But every word of it was assembled in the browser. Search engines fetched a blank <div>, queued the JavaScript for later and, for hundreds of shades, never came back. Best-sellers that had ranked for years slipped off page one within weeks, and the paid budget was quietly covering the hole.
Look past the reflection. Read the raw HTML.
We crawled 4,100 URLs twice, once as raw HTML and once fully rendered, and compared them line by line against three months of server logs. The gap between the two crawls was the whole story.
<!doctype html>
<html lang="es">
<head>
<title>Ceruvia</title>
<script src="/static/app.3f9c.js" defer></script>
<!-- 3.1 MB of JavaScript -->
</head>
<body>
<div id="root"></div>
</body>
</html>0 products · 0 links · 3.1 MB JS
- 01 · RenderingCritical
88% of URLs shipped an empty shell
Product, collection and shade pages returned <div id="root"></div> and a 3.1 MB bundle. Titles, prices and copy only existed after hydration, so the first crawl pass saw nothing to rank.
- 02 · IndexingCritical
560 products stuck in 'Discovered – not indexed'
Variant links were rendered by JavaScript click handlers, not <a href>. Googlebot found 640 product URLs in the sitemap but only 80 had crawlable internal links.
- 03 · PerformanceHigh
Mobile LCP of 6.2 s on the shade finder
The hero swatch image was lazy-loaded behind a client-side fetch, fonts blocked rendering and the full catalogue JSON was parsed on every page.
- 04 · Structured dataHigh
Product schema injected after load, half of it invalid
JSON-LD was written by a tag manager, missed offers on 70% of SKUs and duplicated brand entities, so no shade earned price or review rich results.
- 05 · RoutingMedium
Canonicals pointed every shade at the parent
A single canonical template collapsed 18 foundation shades into one URL and filtered collection URLs returned 200 with identical content.
- 06 · MigrationMedium
212 legacy URLs without redirects
The old Tiendanube paths for best-sellers and blog guides returned soft 404s, discarding years of earned links.
Four layers, applied in order, like a good base.
Built on our Signal plan: we kept the headless stack and the design, and changed only what the crawler receives.
- No. 01 · Foundation
Headless commerce
Kept the headless storefront the brand loved, but moved it onto a server-first architecture so every route answers with complete HTML.
- Product and collection routes server-rendered from the commerce API
- Incremental static regeneration for 640 PDPs, revalidated on stock change
- 212 legacy paths mapped with 301s at the edge
- No. 02 · Primer
JavaScript rendering
Removed the dependence on Google's deferred rendering queue: what the crawler sees on the first fetch is what shoppers see.
- Variant and shade links converted to real <a href> anchors
- Hydration split so reviews and the try-on widget load as islands
- Rendered-HTML diff checks added to every deploy
- No. 03 · Setting powder
Core Web Vitals
A performance budget per template, enforced in CI, so the store stays as fast as it looks.
- Hero swatch preloaded with fetchpriority=high and AVIF fallbacks
- JavaScript per PDP cut from 3.1 MB to 290 kB
- System font fallback with size-adjust to remove layout shift
- No. 04 · Highlighter
Structured data
Schema rendered on the server from the product source of truth, never by a tag manager after load.
- Product, Offer and AggregateRating on every shade variant
- ProductGroup with hasVariant for 18-shade ranges
- Organization and BreadcrumbList unified across the site
A 6-month palette, one pan at a time.
- 1
Month 1
Audit & triage
Rendered-vs-raw crawl of 4,100 URLs, log-file analysis, 212 redirects shipped in week two to stop the bleed.
- 2
Month 2
Server rendering
PDP and collection templates moved to SSR. Server-rendered share jumps from 12% to 61%.
- 3
Month 3
Crawl paths
Shade links become anchors, faceted URLs canonicalised, XML sitemaps split by type. Indexed products pass 300.
- 4
Month 4
Speed budget
Bundle split, hero preload, font fallback. Mobile LCP drops below the 2.5 s threshold for the first time.
- 5
Month 5
Rich results
Server-side ProductGroup schema goes live; price and rating snippets appear on 520 shades.
- 6
Month 6
Compound
100% of templates server-rendered, 640 products indexed, regression checks wired into every release.
Same store. Finally visible.
Modelled scenario for a fictional business — not a client result. Rankings and traffic are never guaranteed; see our Terms.
- Pages server-rendered12% → 100%
- Indexed products80 → 640
- Mobile LCP6.2s → 1.5s
- Organic revenue+154%
Shades back on page one
Google Argentina positions for priority queries, measured from a Buenos Aires location in the month before the engagement and again in month 6. A dash means the page was not in the top 100.
| Query | Monthly volume | Before | Month 6 |
|---|---|---|---|
| base de maquillaje vegana | 6,600 | — | 2 |
| clean makeup argentina | 2,900 | 38 | 1 |
| corrector natural tono medio | 2,400 | — | 3 |
| labial mate sin crueldad | 1,900 | 54 | 4 |
| rubor en crema orgánico | 1,300 | — | 2 |
| ceruvia skin tint shade 06 | 720 | 71 | 1 |
| maquillaje limpio buenos aires | 590 | 22 | 1 |
Your store, seen clearly
Get a plan like this
If your storefront looks right but ranks wrong, start with Signal: a rendering audit, a prioritised fix list and the engineering hours to ship it.
← All case studies