DEV Community

Cover image for From Headless CMS to Shopify: What a CMS Migration Taught Me About Storefront Architecture
Leonel Oliveira
Leonel Oliveira

Posted on

From Headless CMS to Shopify: What a CMS Migration Taught Me About Storefront Architecture

I've spent the last few years living inside headless CMS platforms: Contentful, Strapi, content modeling, migrations, and the React and Next.js frontends that sit on top of them. Lately I've been pointing that experience at Shopify, and the surprising part is how little of it I had to unlearn.

Commerce platforms and content platforms solve different business problems, but the frontend problems are the same ones: where does the data come from, who controls the layout, and how do you keep the UI from depending on a vendor's response shape?

Here is how I map the two worlds.

Two ways to build on Shopify

Before writing any code, the first decision is the approach.

Diagram comparing a Liquid theme with a Hydrogen storefront: who edits the layout, where it is hosted, and when each approach fits best

A Liquid theme is the default path. The store is rendered and hosted by Shopify's Online Store, and merchants rearrange pages in the theme editor. A Hydrogen storefront is a custom React app built on React Router, talking to the Storefront API and deployed to Oxygen, Shopify's edge hosting.

Neither is "better". A theme is often the right answer when editors need control and the design fits the model. Hydrogen earns its extra engineering when the experience needs to go beyond what a theme can express. Being comfortable in both is what makes the recommendation honest.

Themes: templates are data, sections are components

Online Store 2.0 themes are built from JSON templates, sections, and blocks:

Diagram of a Shopify theme page: a JSON template lists sections, and each section contains blocks that merchants can arrange

A JSON template lists which sections appear on a page and in what order. Each section is a Liquid file with its own schema. Blocks are the smaller pieces inside a section that merchants can add, remove, and reorder. If you have used flexible components or dynamic zones in a headless CMS, this will feel familiar.

Here is a small section with two block types:

<section class="rich-text">
  <h2>{{ section.settings.heading }}</h2>

  {%- for block in section.blocks -%}
    {%- case block.type -%}
      {%- when 'text' -%}
        <div {{ block.shopify_attributes }}>{{ block.settings.text }}</div>
      {%- when 'button' -%}
        <a {{ block.shopify_attributes }} class="button" href="{{ block.settings.link }}">
          {{ block.settings.label }}
        </a>
    {%- endcase -%}
  {%- endfor -%}
</section>

{% schema %}
{
  "name": "Rich text",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Hello" }
  ],
  "blocks": [
    {
      "type": "text",
      "name": "Text",
      "settings": [{ "type": "richtext", "id": "text", "label": "Text" }]
    },
    {
      "type": "button",
      "name": "Button",
      "settings": [
        { "type": "text", "id": "label", "label": "Label", "default": "Shop now" },
        { "type": "url", "id": "link", "label": "Link" }
      ]
    }
  ],
  "presets": [{ "name": "Rich text" }]
}
{% endschema %}
Enter fullscreen mode Exit fullscreen mode

The schema is the contract between the developer and the merchant. It decides what the editor can change, and everything else stays consistent. That is the same idea as a content model, just expressed next to the markup.

Headless: the same adapter idea

In my previous post I argued that your UI should depend on your application's data model, not the CMS's. That rule holds for Shopify too.

A Storefront API query for a product:

query Product($handle: String!) {
  product(handle: $handle) {
    id
    title
    description
    featuredImage {
      url
      altText
    }
    priceRange {
      minVariantPrice {
        amount
        currencyCode
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And an adapter that maps the response to the same Product model the components already use:

type Product = {
  id: string;
  title: string;
  description: string;
  image: { url: string; alt: string };
  price: { amount: string; currency: string };
};

function mapShopifyProduct(p: ShopifyProduct): Product {
  return {
    id: p.id,
    title: p.title,
    description: p.description,
    image: {
      url: p.featuredImage?.url ?? "",
      alt: p.featuredImage?.altText ?? p.title,
    },
    price: {
      amount: p.priceRange.minVariantPrice.amount,
      currency: p.priceRange.minVariantPrice.currencyCode,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

<ProductCard product={product} /> doesn't know or care whether the data came from Shopify, Strapi, or Contentful. If the product data moves later, only the adapter changes.

What transfers

Table mapping headless CMS concepts from Contentful and Strapi to Shopify equivalents: metaobjects, metafields, sections and blocks, and the Storefront API

The vocabulary changes, the thinking doesn't. Content types become metaobject definitions, custom fields become metafields, flexible page composition becomes sections and blocks, and the delivery API becomes the Storefront API. Add migration experience on top (mapping one content model to another without breaking the frontend) and a lot of Shopify work starts to look familiar.

What I care about when building storefronts

  • Performance. Product and collection pages are where Core Web Vitals turn into revenue, so image handling, script weight, and render cost get attention early.
  • Accessibility. Variant pickers, carts, and checkout entry points have to work with a keyboard and a screen reader.
  • Merchant experience. A schema that makes sense to an editor is part of the product, not an afterthought.
  • Boundaries. Keep platform-specific data at the edge, and let the UI depend on your own model.

Where I'm headed

I'm currently looking for frontend roles, with a focus on Shopify: theme development with Liquid, and headless storefronts with React and the Storefront API. If you're building in that space, I'd like to hear how you split the work between themes and headless.

Which do you reach for first on a new store: a Liquid theme, or Hydrogen? What tipped the decision?

Happy coding! 🚀

shopify #frontend #react #webdev #liquid #hydrogen #typescript #headlesscms

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate

The adapter boundary is the part I wish more storefront migrations treated as a first-class deliverable. One practical addition: keep a small set of contract fixtures from both systems and run them through the mapper in CI. The fixtures should include missing images, products with no variants available, translated fields, and metafields that changed type during migration. It catches response-shape drift before a product page becomes the test environment.

For the Liquid vs Hydrogen choice, I would also ask who owns day-two changes. If merchandisers need to rearrange the page weekly, a theme with a disciplined section schema often beats a more flexible stack that requires an engineer for every change.