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_homepageandcms_page - Storefront API access token configured
Metaobject content model
cms_homepage
| Field handle | Type | Description |
|---|---|---|
title |
single_line_text | Page title |
meta_title |
single_line_text | SEO title override |
meta_description |
single_line_text | SEO description override |
hero_headline |
single_line_text | Hero banner headline |
hero_subheadline |
single_line_text | Hero banner subheadline |
hero_image |
file | Hero background image |
hero_cta_text |
single_line_text | Hero call-to-action label |
hero_cta_link |
single_line_text | Hero call-to-action URL |
sections |
json | Array of content section definitions |
cms_page
| Field handle | Type | Description |
|---|---|---|
slug |
single_line_text | URL slug |
title |
single_line_text | Page title |
locale |
single_line_text | Locale code (e.g. en-US) |
meta_title |
single_line_text | SEO title override |
meta_description |
single_line_text | SEO description override |
hero_headline |
single_line_text | Hero banner headline |
hero_subheadline |
single_line_text | Hero banner subheadline |
hero_image |
file | Hero background image |
hero_cta_text |
single_line_text | Hero call-to-action label |
hero_cta_link |
single_line_text | Hero call-to-action URL |
sections |
json | Array of content section definitions |
Implementation steps
1. Create lib/shopify/operations/cms.ts
Implement three operations that return domain types from lib/types.ts:
import { defaultLocale } from "@/lib/i18n";
import { assertStorefrontOk } from "@/lib/shopify/errors";
import { storefront } from "@/lib/shopify/storefront";
import type { Homepage, MarketingPage } from "@/lib/types";
export async function getHomepage({
locale = defaultLocale,
}: {
locale?: string;
} = {}): Promise<Homepage | null> {
"use cache";
cacheLife("max");
cacheTag("cms:all", "cms:homepage");
// Query cms_homepage metaobject, transform to Homepage type
}
export async function getMarketingPage({
slug,
locale = defaultLocale,
}: {
slug: string;
locale?: string;
}): Promise<MarketingPage | null> {
"use cache";
cacheLife("max");
cacheTag("cms:all", "cms:pages", `cms:page:${slug}`);
// 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:all", "cms:pages");
// Query all cms_page metaobjects, return slugs
}
2. Write GraphQL queries
Use Shopify AI Toolkit's custom-data skill first, then validate the final Storefront GraphQL documents with its Storefront skill. 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
sectionsJSON field intoContentSection[] - Resolve product references in sections to
ProductCard[]usinggetProductsByIds - Map hero fields to
HeroSection - Map image references to
MarketingImage
4. Wire into routes
Update app/page.tsx to use the CMS operation:
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 to use getMarketingPage and getAllMarketingPageSlugs. To index CMS pages in the sitemap, add a cms-pages-{n} shard type to app/sitemap/[shard]/route.ts and include it in app/sitemap.xml/route.ts.
The homepage and marketing-page reads use plain "use cache" so their stable bodies can be included coherently in prerendered shells. The slug enumeration uses "use cache: remote" because sitemap and discovery reads can run at request time and share results.
5. Use the existing cache invalidation webhook
Keep metaobject invalidation in app/api/webhooks/shopify/route.ts, which already verifies Shopify's signature and maps metaobjects/* topics to cms:all, cms:homepage, cms:pages, and page-specific tags. Extend that mapping if the content model adds another CMS metaobject type; do not create a second unverified CMS webhook endpoint.
Guardrails
- Return the exact domain types from
lib/types.ts—Homepage,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
alternatesfield onMarketingPage— this powers locale-aware URL switching. - Keep CMS cache tags aligned with
app/api/webhooks/shopify/route.ts.