TL;DR
- Shopify Theme(Liquid)はサーバーサイドで外部APIを呼び出せないため、外部CMSとの連携はクライアントサイドになりパフォーマンス・SEOに悪影響
- Hydrogenなら自由にサーバーサイド処理ができるが、既存テーマからの移行コストが大きい
- Webhook + SSRで事前にHTMLを生成し、Shopifyの記事として保存するアプローチで解決
- 外部CMS(Contentful)のリッチな編集体験を活かしつつ、パフォーマンスを犠牲にしない
はじめに
「Shopifyの標準ブログエディタが物足りないから、ContentfulやmicroCMSを使いたい...」 「でも、Shopify ThemeからAPIを叩くとクライアントサイドになってしまう...」 「SEOやCore Web Vitalsのスコアが心配...」
こんな悩みを抱えていませんか?
Shopify Theme(Liquid)は優れたテンプレートエンジンですが、サーバーサイドで外部APIを呼び出す機能がありません。これが外部CMSとの連携を難しくしている根本的な原因です。
Shopify Themeの技術的制約
Liquidからは外部APIを呼べない
Shopify ThemeのLiquidテンプレートはサーバーサイドでレンダリングされますが、Liquid内から外部のREST APIやGraphQL APIを呼び出すことはできません。
つまり、外部CMS(Contentful、Sanity、microCMS等)のコンテンツを表示したい場合、以下のような実装になります:
// クライアントサイドでAPIを叩くしかない
fetch('https://cdn.contentful.com/spaces/xxx/entries/yyy')
.then(res => res.json())
.then(data => {
document.getElementById('blog-content').innerHTML = renderContent(data);
});
クライアントサイド取得の問題点
この方法には深刻な問題があります:
| 問題 | 影響 |
|---|---|
| 初期表示が遅い | ページロード後にAPIコール→レンダリングとなり、FCP/LCPが悪化 |
| SEOに不利 | 検索エンジンのクローラーがコンテンツを認識しにくい |
| レイアウトシフト | コンテンツが遅れて表示され、CLSスコアが悪化 |
| ローディング表示 | ユーザーに「読み込み中...」を見せることになる |
ブログ記事のようなコンテンツ重視のページで、これらの問題は致命的です。
Headless構成なら解決できるが...
Shopify Themeを使わず、独自のフロントエンドを構築するHeadless構成であれば、この問題は発生しません。
Headless Shopifyの実現方法はいくつかあります:
- Hydrogen: Shopify公式のReactフレームワーク(Remixベース)
- Next.js + Storefront API: 独自にNext.jsなどでフロントエンドを構築
- その他のフレームワーク: Nuxt、Astro、SvelteKitなど任意の技術スタック
いずれの方法でも、サーバーサイドで自由に外部API(Contentful等)を呼び出せます。
しかし、Headless構成への移行は:
- 既存のShopify Themeを捨てることになる
- テーマカスタマイズやアプリ連携の資産が使えなくなる(多くのアプリはTheme前提)
- フロントエンドをゼロから構築する必要がある
既存のShopify Themeで運用中のサイトにとっては、現実的でないケースも多いでしょう。
解決策:Webhook + SSRによる事前HTML生成
本記事で紹介するアプローチは、Shopify Themeの制約を回避しつつ、外部CMSを活用する方法です。
基本的な考え方
- 外部CMS(Contentful)でコンテンツを作成・編集
- 公開時にWebhookで自社サーバーに通知
- サーバーサイドでCMSからコンテンツを取得し、ReactでSSR
- 生成したHTMLをShopify Admin API経由で記事として保存
- Shopify Themeは通常通り記事を表示するだけ
ポイントは、Shopify Themeがレンダリングする時点では、すでにHTMLが完成しているということです。クライアントサイドでのAPI呼び出しは一切不要になります。
なぜContentfulを選んだか
外部CMSとしてContentfulを選んだ理由は以下の通りです:
- Rich Textエディタの充実: 画像、動画、カスタムコンポーネントの埋め込みが可能
- Images API: URLパラメータで画像のリサイズ、フォーマット変換、品質調整が可能
- Webhook機能: コンテンツの公開・更新時に自動で通知を送信
- TypeScriptサポート: 型定義が充実しており、開発体験が良い
代表的なHeadless CMSには、Contentful、Sanity、Strapi、microCMSなどがありますが、同様のアプローチはどのCMSでも実現可能です。
アーキテクチャ概要
実装したシステムの全体像は以下の通りです:

実装のポイント
1. Contentful Clientの初期化
まず、Contentfulからコンテンツを取得するクライアントを設定します。
import { createClient } from 'contentful';
const client = createClient({
space: process.env.CMS_SPACE_ID ?? '',
environment: process.env.CMS_ENVIRONMENT ?? 'master',
accessToken: process.env.CMS_ACCESS_TOKEN ?? '',
host: 'preview.contentful.com', // Preview APIで下書き確認可能
});
export const getCMSPost = async (postId: string) => {
const result = await client.getEntries({
content_type: 'blogPost',
'sys.id': postId,
include: 3, // 3階層までネストされた参照を解決
});
return convertCMSItemToPost(result.items[0]);
};
include: 3パラメータがポイントです。これにより、記事に埋め込まれた著者情報、関連画像、関連記事などのネストされた参照を一度のAPIコールで取得できます。
2. Webhookハンドラーの実装
Contentfulでコンテンツが公開されたとき、Shopifyに自動同期するWebhookハンドラーを実装します。
import type { RequestHandler } from 'express';
type CMSWebhookTopic =
| 'ContentManagement.Entry.create'
| 'ContentManagement.Entry.publish'
| 'ContentManagement.Entry.unpublish';
export const postWebhookHandler: RequestHandler = async (req, res) => {
const topic = req.headers['x-contentful-topic'] as CMSWebhookTopic;
const entryId = req.body.sys.id;
switch (topic) {
case 'ContentManagement.Entry.create':
case 'ContentManagement.Entry.update':
await saveArticle(entryId);
break;
case 'ContentManagement.Entry.publish':
await updatePublishStatus(entryId, true);
break;
case 'ContentManagement.Entry.unpublish':
await updatePublishStatus(entryId, false);
break;
}
res.json({ message: 'OK' });
};
ContentfulのWebhookはx-contentful-topicヘッダーでイベント種別を通知します。これを使って、作成・更新・公開・非公開を適切にハンドリングします。
3. Rich TextからReactコンポーネントへの変換
ContentfulのRich Textフィールドは、構造化されたJSONとして返されます。これをReactコンポーネントに変換します。
import { documentToReactComponents, Options } from '@contentful/rich-text-react-renderer';
import { BLOCKS, INLINES } from '@contentful/rich-text-types';
const renderOptions: Options = {
renderNode: {
// 外部リンクの処理
[INLINES.HYPERLINK]: ({ content, data }) => {
const text = content[0].value;
const { uri } = data;
return (
<a
href={uri}
target="_blank"
rel="noreferrer nofollow"
>
{text}
</a>
);
},
// 見出しにアンカーリンクを自動付与
[BLOCKS.HEADING_2]: ({ content }) => {
const text = content[0].value;
return (
<>
<a id={text} className="anchor" />
<h2>{text}</h2>
</>
);
},
// レスポンシブ画像の生成
[BLOCKS.EMBEDDED_ASSET]: ({ data }) => {
const asset = data.target;
return (
<img
src={`https:${asset.fields.file.url}?w=1024&fm=webp&q=70`}
srcSet={`
https:${asset.fields.file.url}?w=640&fm=webp&q=70 640w,
https:${asset.fields.file.url}?w=1024&fm=webp&q=70 1024w
`}
sizes="(min-width: 1024px) 1024px, 100vw"
alt={asset.fields.title}
/>
);
},
},
};
export const PostBody = ({ content }) => {
return (
<div className="post-body">
{documentToReactComponents(content, renderOptions)}
</div>
);
};
このアプローチの利点は、コンテンツ編集者はWYSIWYGエディタで自由に編集でき、開発者は出力されるHTMLを完全にコントロールできることです。
4. 画像の自動最適化
ContentfulのImages APIを活用して、レスポンシブ画像を自動生成します。
export const generateResponsiveImage = (asset, preset = 'content') => {
const sizes = {
content: [320, 640, 1024, 1280],
hero: [640, 1024, 1920],
thumbnail: [200, 400],
};
const srcSet = sizes[preset]
.map(width => {
const url = `https:${asset.fields.file.url}?w=${width}&fm=webp&q=70`;
return `${url} ${width}w`;
})
.join(', ');
return {
src: `https:${asset.fields.file.url}?w=${sizes[preset][0]}&fm=webp&q=70`,
srcSet,
sizes: '(min-width: 1024px) 1024px, 100vw',
};
};
WebP形式への自動変換と、デバイスに応じた適切なサイズの配信により、画像のファイルサイズを大幅に削減できます。
5. Shopifyテーマとの連携
Shopify側では、Metafieldを使ってCMS管理の記事を識別し、条件分岐で表示を切り替えます。
{%- if article.metafields.custom.is_cms_managed -%}
{%- comment -%}
CMS管理記事用のスタイルを読み込み
{%- endcomment -%}
{{ 'cms-article.css' | asset_url | stylesheet_tag }}
<div class="cms-article">
{{ article.content }}
</div>
{%- else -%}
{%- comment -%}
通常のShopify記事
{%- endcomment -%}
<article class="standard-article">
{{ article.content }}
</article>
{%- endif -%}
この方法なら、既存のShopify記事とCMS管理の記事を同じテーマで共存させられます。段階的な移行も可能です。
このアプローチで得られるメリット
パフォーマンス問題の解消
クライアントサイドでのAPI呼び出しが不要になるため:
- FCP/LCPの改善: ページロード時点でコンテンツが存在
- CLSの改善: レイアウトシフトが発生しない
- SEOに有利: クローラーがコンテンツを正しく認識
| 指標 | クライアントサイド取得 | 本アプローチ |
|---|---|---|
| FCP | ページ読込 + API + レンダリング | ページ読込のみ |
| LCP | 大幅に遅延 | 通常通り |
| CLS | シフト発生 | シフトなし |
| SEO | クローラーが認識しにくい | 完全に認識 |
外部CMSの編集体験を活用
- WYSIWYGエディタで直感的に編集可能
- 画像のドラッグ&ドロップでアップロード
- プレビュー機能で公開前に確認
- 複数人での同時編集が可能
- カスタムコンポーネントの埋め込み
既存のShopify Theme資産を維持
- テーマカスタマイズがそのまま使える
- Shopifyアプリとの連携も維持
- 段階的な移行が可能(記事単位でCMS管理に切り替え)
導入時の課題と解決策
CSSスコーピング問題
課題: React生成のHTMLとShopifyテーマのスタイルが衝突する
解決策: CSS Modulesを使用し、クラス名をコンポーネント単位でスコーピング。esbuildのプラグインでComponentName_className形式の命名規則を適用。
Webhookの信頼性
課題: ネットワーク障害でWebhookが失敗する可能性がある
解決策: べき等な操作として実装。「存在確認→なければ作成、あれば更新」のロジックにより、リトライしても安全。
まとめ
Shopify Themeを使っていて、外部CMSを導入したいけどパフォーマンスが心配...という方は、本記事で紹介したWebhook + SSRによる事前HTML生成のアプローチを検討してみてください。
このアプローチが向いているケース
- Shopify Themeで運用中だが、ブログ編集機能に不満がある
- 外部CMSのリッチな編集体験を使いたい
- パフォーマンス(Core Web Vitals)やSEOを犠牲にしたくない
- Hydrogenへの全面移行は現実的でない
向いていないケース
- 新規構築でゼロから始められる → Headless構成(Hydrogen、Next.js等)を検討
- リアルタイム性が必要なコンテンツ → 別のアプローチが必要
- 連携システムを構築・運用するリソースがない
Shopify Themeの「サーバーサイドで外部APIを呼べない」という制約は、工夫次第で回避できます。既存のテーマ資産を活かしながら、外部CMSの編集体験とパフォーマンスの両立を目指してみてはいかがでしょうか。
