Shopify CMS

Wire Shopify metaobjects as the CMS for homepage and marketing page content.

How to use

bash
/vercel-shop:enable-shopify-cms

Enable Shopify CMS

Add Shopify metaobject-based CMS support to the shop template. This replaces the hardcoded homepage content with Shopify-managed content using cms_homepage and cms_page metaobject definitions.

Prerequisites

  • Shopify store with metaobject definitions for cms_homepage and cms_page
  • Storefront API access token configured

Metaobject content model

cms_homepage

Field handleTypeDescription
titlesingle_line_textPage title
meta_titlesingle_line_textSEO title override
meta_descriptionsingle_line_textSEO description override
hero_headlinesingle_line_textHero banner headline
hero_subheadlinesingle_line_textHero banner subheadline
hero_imagefileHero background image
hero_cta_textsingle_line_textHero call-to-action label
hero_cta_linksingle_line_textHero call-to-action URL
sectionsjsonArray of content section definitions

cms_page

Field handleTypeDescription
slugsingle_line_textURL slug
titlesingle_line_textPage title
localesingle_line_textLocale code (e.g. en-US)
meta_titlesingle_line_textSEO title override
meta_descriptionsingle_line_textSEO description override
hero_headlinesingle_line_textHero banner headline
hero_subheadlinesingle_line_textHero banner subheadline
hero_imagefileHero background image
hero_cta_textsingle_line_textHero call-to-action label
hero_cta_linksingle_line_textHero call-to-action URL
sectionsjsonArray of content section definitions

Implementation steps

1. Create lib/shopify/operations/cms.ts

Implement three operations that return domain types from lib/types.ts:

ts
import { shopifyFetch } from "@/lib/shopify/client";
import type { Homepage, MarketingPage } from "@/lib/types";

export async function getHomepage(locale: string): Promise<Homepage | null> {
  "use cache: remote";
  cacheLife("max");
  cacheTag("cms-content");
  // Query cms_homepage metaobject, transform to Homepage type
}

export async function getMarketingPage(
  slug: string,
  locale: string,
): Promise<MarketingPage | null> {
  "use cache: remote";
  cacheLife("max");
  cacheTag("cms-content");
  // Query cms_page metaobject by slug, transform to MarketingPage type
}

export async function getAllMarketingPageSlugs(): Promise<
  Array<{ slug: string; updatedAt: string }>
> {
  "use cache: remote";
  cacheLife("max");
  cacheTag("cms-content");
  // Query all cms_page metaobjects, return slugs
}

2. Write GraphQL queries

Validate field names with shopify-ai-toolkit or vercel-shop:fetch-shopify-schema. Use metaobjects(type: "cms_homepage") and metaobjects(type: "cms_page") queries with @inContext locale directives.

3. Transform metaobject responses

Create lib/shopify/transforms/cms.ts to convert raw metaobject fields into the domain types:

  • Parse the sections JSON field into ContentSection[]
  • Resolve product references in sections to ProductCard[] using getProductsByIds
  • Map hero fields to HeroSection
  • Map image references to MarketingImage

4. Wire into routes

Update app/page.tsx to use the CMS operation:

ts
import { getHomepage } from "@/lib/shopify/operations/cms";
import { MarketingPageRenderer } from "@/components/cms/page-renderer";

export default async function HomePage() {
  const locale = await getLocale();
  const page = await getHomepage(locale);
  if (!page) return <FallbackHomepage />;
  return (
    <Container>
      <MarketingPageRenderer page={page} />
    </Container>
  );
}

Update app/pages/[slug]/page.tsx and app/sitemap.ts to use getMarketingPage and getAllMarketingPageSlugs.

5. Add cache invalidation webhook

Create app/api/webhooks/shopify-cms/route.ts:

ts
import { updateTag } from "next/cache";

export async function POST(request: Request) {
  // Verify Shopify webhook signature
  updateTag("cms-content");
  return new Response("OK");
}

Guardrails

  • Return the exact domain types from lib/types.tsHomepage, MarketingPage, ContentSection.
  • Resolve product references to ProductCard[] — components expect ready-to-render product data.
  • Handle locale fallback gracefully — return default locale content if requested locale is unavailable, never throw.
  • Support the alternates field on MarketingPage — this powers locale-aware URL switching.

Chat

Tip: You can open and close chat with I

0 / 1000