Replacing GemPages with a custom Shopify Horizon theme
We pulled GemPages off a Shopify store and rebuilt every landing page as Horizon sections. Theme customization, Liquid custom sections, and the numbers.

We inherited a Shopify store running eleven landing pages on GemPages. The store did real revenue, the pages converted, and the performance was abysmal. Over three weeks in December we rebuilt every one of those pages as Shopify Horizon sec
We inherited a Shopify store running eleven landing pages on GemPages. The store did real revenue, the pages converted, and the performance was abysmal. Over three weeks in December we rebuilt every one of those pages as Shopify Horizon sections, ripped GemPages out, and shipped the result two days before Christmas. Shopify Horizon theme customization, done right, replaces a page builder with a small library of Liquid custom sections you compose in the theme editor — you lose the drag-drop canvas, you gain everything else. This is the playbook we'd follow again: what we built, what broke, and what Horizon actually gives you over the previous Dawn-era themes.
Why this move, now
GemPages, Shogun, PageFly — the whole page-builder category exists because old Shopify themes were rigid. Dawn was a step toward flexibility but still felt conservative. Horizon is the first reference theme that treats sections, blocks, and metaobjects as the primary composition primitive, with full theme-editor support. The page-builder category now exists mostly to sell a drag-drop canvas to merchandisers; the underlying rendering model is a liability.
The Meta CPM drop is the number that decided the project. Meta's bid logic reads landing-page experience signals, and a GemPages page that loads 400KB of JavaScript before the first paint gets bid up accordingly. We rebuilt the pages; the CPM fell the week after the cutover. That single line item paid for the three weeks of theme work in roughly twenty-two days.
What Horizon actually gives you
Three things, in rising order of usefulness.
Section groups. A section group is a named collection of sections you assemble in JSON — header, footer, or an arbitrary layout. It lets you reuse layouts across page templates without copying Liquid.
Metaobject-native blocks. Sections can render blocks driven by metaobjects, not just hand-edited JSON. This means your merchandisers maintain content in a proper data model (in the Shopify admin), and the theme just iterates over it.
The Section Rendering API. Given a section handle, Shopify will render only that section server-side. This is how you build interactive features — filters, quick-add, swatches — without shipping a client-side re-render. We use it three times in the new theme.
{%- comment -%}
sections/hero-editorial.liquid — a composable landing hero.
Runs on metaobjects so merchandisers edit content in admin.
{%- endcomment -%}
{%- assign hero = section.settings.featured_hero.value -%}
<section class="hero-editorial" data-section-id="{{ section.id }}">
<header class="hero-editorial__eyebrow">
<span class="dot" style="background: {{ hero.accent }}"></span>
<span>{{ hero.eyebrow | upcase }}</span>
</header>
<h1 class="hero-editorial__title h1-art">
{{ hero.title }}
</h1>
{%- if hero.description != blank -%}
<p class="hero-editorial__lede">{{ hero.description }}</p>
{%- endif -%}
{%- if hero.cta_label != blank and hero.cta_url != blank -%}
<a href="{{ hero.cta_url }}" class="btn-pill">
{{ hero.cta_label }}
</a>
{%- endif -%}
</section>
{% schema %}
{
"name": "Hero — editorial",
"settings": [
{
"type": "metaobject",
"id": "featured_hero",
"label": "Hero content",
"metaobject_type": "hero"
}
],
"presets": [{ "name": "Hero — editorial" }]
}
{% endschema %}That's the entire section. Content lives in a hero metaobject the merchandiser edits in the Shopify admin, with a predictable schema of fields (eyebrow, title, description, cta_label, cta_url, accent). The merchandiser gets a structured editor; the developer gets a typed surface; neither of them gets 400KB of page-builder runtime.
The library we built, section by section
We shipped fourteen sections. Some were direct replacements for GemPages blocks; a few were new layouts we didn't have before.
| Section | Purpose | LoC |
|---|---|---|
hero-editorial | Big headline + CTA landing hero | 92 |
product-hero | PDP-style above-fold with swatches | 148 |
feature-grid | 2/3/4-column feature blocks | 61 |
testimonial-rail | Horizontal scroller with reviews | 114 |
press-logos | Monochrome logo row | 38 |
comparison-table | Us vs. them, responsive | 197 |
faq-accordion | Progressive disclosure, JSON-LD | 89 |
cta-band | Full-width call-to-action | 44 |
image-text | Alternating image + text, reversible | 106 |
press-quote | Large pull quote, editorial | 57 |
collection-rail | Horizontal product scroller | 132 |
video-hero | Above-fold looping MP4/WebM | 78 |
bundle-callout | Product bundle with add-to-cart | 163 |
lookbook | Two-column image grid | 71 |
Total: about fourteen hundred lines of Liquid. For context, the GemPages pages they replaced were nineteen in number and produced roughly two megabytes of combined JavaScript on every page load.
The cutover, start to finish
The cutover plan was three steps, stretched across three days so we could watch the dashboards.
Day one: stage the theme. We published the new theme as unpublished, then pushed it to a Shopify preview URL. Merchandisers rebuilt two of the eleven landing pages using the new section library and compared them side-by-side with the GemPages originals. They flagged four pixel-level issues; we fixed three and decided one of them was actually an improvement.
Day two: swap the theme. We cut over at 10am local, watched Core Web Vitals for four hours, and did not revert. The pages rendered. Traffic continued.
Day three: uninstall GemPages. This is the part that scared us. GemPages proxies its pages through its own app; uninstalling the app mid-traffic would 404 every GemPages URL. We pre-built a redirect map from each GemPages slug to either a Shopify product page, a Shopify page, or the collection that replaced the landing, and imported it into Shopify's URL redirects.
// scripts/export-gempages-redirects.ts
import { gempages } from './clients'
import fs from 'node:fs/promises'
const pages = await gempages.listPages({ shop: SHOP_DOMAIN })
const redirects = pages.map((p) => ({
path: p.slug, // e.g. "/pages/holiday-lookbook"
target: MAP[p.slug] ?? '/collections/all', // hand-curated map
}))
await fs.writeFile(
'redirects.json',
JSON.stringify(redirects, null, 2),
)We uploaded the CSV to Shopify's Redirects admin and uninstalled GemPages at 3pm. Organic traffic to the affected URLs was within two percent of trend the next morning. The URLs that gained were the ones where the new Horizon page outperformed the old GemPages landing on LCP, which was most of them.
What Horizon doesn't give you
A drag-drop canvas. Merchandisers who miss this will not be happy on day one. We closed the gap by building a rich section library and investing a morning on internal documentation. Two of our three merchandisers said, inside a week, that they preferred the new editor. One still prefers GemPages. She is outvoted.
Pixel-perfect design mirroring. Horizon's theme editor cannot do per-breakpoint spacing with the same control a page builder has. We solved this by baking responsive rules into section CSS and exposing only the toggles that matter (spacing scale: tight/normal/loose; columns: 2/3/4).
Third-party integrations with drag-drop embeds. If you run a store where every landing includes an embedded quiz or a third-party review widget that only drops a <div> via a page-builder plugin, Horizon is harder. You'll be writing Liquid to render those integrations cleanly.
— our head of merchandising, three days after the cutoverI miss the canvas. I don't miss waiting eight seconds for a preview to load. I don't miss the page builder telling me it saved when it didn't. On balance, I'm staying.
What we'd do differently
We would have cut the section library in half on day one. We shipped fourteen sections because we were mirroring GemPages's page inventory; we'd now argue that seven of them would have done the same job with tighter composition. If you're doing this, start with a five-section library and add only when a landing genuinely needs something new.
We would have written a theme linter. We build-checked Liquid by running the storefront through a preview URL, which is a slow feedback loop. The Shopify CLI has shopify theme check but it's conservative; we'd extend it next time with rules specific to our section schemas — something like "every section must have a preset" and "no section may reference a metaobject type it doesn't declare."
We would not have done this in December. The whole thing shipped two days before Christmas, which meant we ran the next three weeks of Black-Friday-into-New-Year traffic on a theme that had been in prod for seventy-two hours. It worked, but we were lucky about several things we'd rather not have been lucky about. Do this in March, not December.
The general-case lesson is simple: for stores running more than ten landing pages, Shopify Horizon theme customization is a serious alternative to the page-builder category, and the performance math is not subtle. For one-landing stores, keep your page builder and your afternoon.
Three more from the log.

Why we moved our blog off Webflow and what it cost us
After three years on Webflow we shipped a static MDX blog in a long weekend. Here are the Webflow alternatives we considered, the math, and the cost.
Oct 08, 2025 · 5 min
Building a production MDX blog with Next.js 16 and Velite
How we shipped a production MDX blog on Next.js 16 with Velite: the content layer, typed frontmatter, and why Velite vs Contentlayer was a short debate.
Nov 04, 2025 · 6 min
Claude Code vs Cursor vs v0: honest comparison after 6 months
I used all three every day for half a year across four businesses. Here's the claude code vs cursor verdict, the v0 vs cursor verdict, and what I wish I'd known.
Mar 15, 2026 · 9 min