product-information-management について
Eコマースや商品運用チーム向けのPIM連携スキルです。AkeneoまたはSalsifyを商品データの基準として各コマースプラットフォームへ同期する方法を、項目マッピング、差分更新、チャネル別変換とともに説明します。
主な機能
- 01PIMを商品データの基準に設定
- 02Shopifyなどのプラットフォームと同期
- 03属性をチャネル項目にマッピング
- 04差分またはイベント起点の同期
- 05商品データ形式の変換
活用シーン
- 01ストアとマーケット間の商品情報統一
- 02PIMの商品情報をストアへ反映
- 03ヘッドレスストアへの商品データ提供
- 04カタログ内の不足属性の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill product-information-managementインストール後、Claude Code で/product-information-managementを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/akeneo-client-auth-and-incremental-sync/criteria.jsontask.mdevals/product-transformation-attribute-options/criteria.jsontask.mdevals/salsify-integration-and-data-quality-rep/criteria.jsontask.md詳細
商品情報管理
概要
商品情報管理(PIM)システムは、すべてのチャネル(ウェブサイト、マーケットプレイス、印刷カタログ)にわたる商品データ(名称、説明、画像、属性、デジタルアセット)の信頼できる唯一の情報源です。Akeneo と Salsify は主要な PIM プラットフォームです。このスキルでは、商品情報の充実化における正規の情報源として PIM に接続する方法、PIM とコマースプラットフォーム間の同期を実装する方法、そして PIM データをチャネル固有の形式に変換するパイプラインを構築する方法について説明します。
このスキルを使用する場面
- ウェブサイト、マーケットプレイスの掲載情報、社内システム間で商品データに一貫性がない場合
- マーチャンダイジングチームが PIM で商品コンテンツを管理しており、コマースプラットフォームにも反映する必要がある場合
- 商品情報を充実化するための情報源が必要な、新しいヘッドレスストアフロントを構築する場合
- チャネル固有の商品データが必要な新しい販売チャネル(マーケットプレイス、B2B ポータル)を追加する場合
- 商品データの品質を監査し、カタログ全体で不足している属性を特定する場合
基本手順
ステップ 1:プラットフォームと PIM 連携方法を決定する
| プラットフォーム | PIM 連携オプション | 同期される内容 |
|---|---|---|
| Shopify | Akeneo の公式 Shopify コネクター(無料、Akeneo Marketplace)または Salsify Syndication for Shopify | 商品名、説明、画像、属性 → Shopify の商品メタフィールド、Shopify のバリエーションを Akeneo の商品モデルにマッピング |
| WooCommerce | Akeneo WooCommerce Connector(オープンソース、GitHub)またはカスタム REST API 同期 | Products REST API 経由で WooCommerce に商品データをプッシュ、画像を WordPress のメディアライブラリにアップロード |
| BigCommerce | Akeneo BigCommerce Connector(Akeneo Marketplace)または Salsify for BigCommerce | 商品属性を BigCommerce のカスタムフィールドとバリエーションにプッシュ、画像を BigCommerce CDN に配信 |
| カスタム/ヘッドレス | Akeneo または Salsify の REST API との直接連携 | データマッピングを完全に制御、updated フィルターによる差分同期、同期中に画像を CDN にアップロード |
ステップ 2:プラットフォーム固有の PIM 連携
Shopify
公式コネクターを使って Akeneo を Shopify に接続します:
- Akeneo インスタンスで、Connect → Marketplace に移動し、Shopify Connector(Akeneo 提供、無料)をインストールします
- Akeneo の Connect → Connections で Connection を設定し、Products、Media files、Attribute options の読み取り権限を付与します
- コネクターの設定で、Akeneo のチャネル/ロケールを Shopify のマーケットにマッピングします(例:Akeneo の
en_USスコープ → Shopify のデフォルト言語) - Akeneo の属性コードを Shopify のフィールドにマッピングします:
name→ Shopify の商品タイトルdescription→ Shopify の body HTMLprice→ Shopify のバリエーション価格- カスタム属性 → Shopify の商品メタフィールド(Shopify 管理画面の Settings → Custom data で設定)
- 初回の全件同期を実行し、その後は差分同期をスケジュールします。コネクターは
updated > last_sync_atの商品だけを取得します
同期を確認します:
- Shopify 管理画面で商品を開き、タイトル、説明、画像が Akeneo の内容と一致していることを確認します
- Shopify の商品ページの Metafields セクションでメタフィールドを確認します
WooCommerce
REST API を使用して Akeneo を WooCommerce と同期する:
オープンソースの Akeneo WooCommerce Connector(github.com/akeneo/woocommerce-connector で入手可能)は出発点として役立ちますが、多くのマーチャントはカスタム同期スクリプトを構築しています。
-
次の処理を行う cron ジョブを設定します(日次または毎時)。
GET /api/rest/v1/products?search={"updated":[{"operator":">","value":"..."}]}を使用し、前回の同期以降に更新された商品を Akeneo から取得します。GET /wp-json/wc/v3/products?sku={sku}を使用し、商品が WooCommerce に存在するか確認します。POSTまたはPUT /wp-json/wc/v3/products/{id}を使用して WooCommerce の商品を作成または更新します。
-
Akeneo のフィールドを WooCommerce のフィールドにマッピングします。
- Akeneo の
name(en_US) → WooCommerce のname - Akeneo の
description→ WooCommerce のdescription - Akeneo の
images→ WordPress のメディアライブラリにダウンロードしてアップロードし、WooCommerce 商品画像として設定します。 - Akeneo のカスタム属性 → WooCommerce の商品属性または ACF カスタムフィールド
- Akeneo の
-
各同期の後、WP-CLI 経由で
wp transient delete-allを実行して WooCommerce の一時キャッシュを消去し、更新された商品がすぐに表示されるようにします。
BigCommerce
BigCommerce コネクターを使用して Akeneo に接続する:
- Akeneo Marketplace で BigCommerce Connector をインストールし、BigCommerce API 認証情報(Advanced Settings → API Accounts にある Client ID、Client Secret、Access Token)を設定します。
- コネクターの設定で、Akeneo のファミリーを BigCommerce の商品タイプにマッピングします。
- 属性のマッピングを設定します。
- Akeneo の属性 → BigCommerce のカスタムフィールドまたはバリアントのオプションセット
- Akeneo のカテゴリー → BigCommerce のカテゴリー階層
- コネクターの設定から定期的な差分同期をスケジュールします。
カスタム/ヘッドレス
Akeneo REST API に接続する:
// lib/akeneo/client.ts
export class AkeneoClient {
private accessToken: string | null = null;
private tokenExpiry: number = 0;
constructor(private config: {
baseUrl: string; clientId: string; clientSecret: string;
username: string; password: string;
}) {}
async getToken(): Promise<string> {
if (this.accessToken && this.tokenExpiry > Date.now() + 60000) return this.accessToken;
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
const res = await fetch(`${this.config.baseUrl}/api/oauth/v1/token`, {
method: 'POST',
headers: { 'Authorization': `Basic ${credentials}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'password', username: this.config.username, password: this.config.password }),
});
const data = await res.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + data.expires_in * 1000;
return this.accessToken!;
}
async getAll(path: string): Promise<any[]> {
const items: any[] = [];
let nextUrl: string | null = path;
while (nextUrl) {
const token = await this.getToken();
const res = await fetch(`${this.config.baseUrl}${nextUrl}`, {
headers: { 'Authorization': `Bearer ${token}` },
});
const page = await res.json();
items.push(...(page._embedded?.items ?? []));
nextUrl = page._links?.next?.href?.replace(this.config.baseUrl, '') ?? null;
}
return items;
}
}
export const akeneo = new AkeneoClient({
baseUrl: process.env.AKENEO_BASE_URL!,
clientId: process.env.AKENEO_CLIENT_ID!,
clientSecret: process.env.AKENEO_CLIENT_SECRET!,
username: process.env.AKENEO_USERNAME!,
password: process.env.AKENEO_PASSWORD!,
});
Akeneo のロケール/スコープ別属性形式を、フラットなストアフロント商品データに変換する:
// lib/akeneo/product-transformer.ts
export function transformAkeneoProduct(akeneoProduct: any, locale = 'en_US', scope = 'ecommerce') {
const getValue = (attrCode: string, defaultValue: any = null) => {
const values = akeneoProduct.values[attrCode] ?? [];
const match = values.find(v => v.locale === locale && v.scope === scope)
?? values.find(v => v.locale === locale && v.scope === null)
?? values.find(v => v.locale === null && v.scope === scope)
?? values.find(v => v.locale === null && v.scope === null);
return match?.data ?? defaultValue;
};
return {
sku: akeneoProduct.identifier,
name: getValue('name', '') as string,
description: getValue('description', '') as string,
brand: getValue('brand', '') as string,
categories: akeneoProduct.categories,
attributes: {
color: getValue('color'),
size: getValue('size'),
material: getValue('material'),
},
enabled: akeneoProduct.enabled,
};
}
差分同期ジョブ(更新された商品のみを取得):
// jobs/akeneo-sync.ts
export async function syncAkeneoProducts() {
const lastSyncAt = await db.syncState.getLastSync('akeneo_products');
const syncStartTime = new Date();
const updatedAt = lastSyncAt?.toISOString() ?? '2020-01-01T00:00:00+00:00';
const products = await akeneo.getAll(
`/api/rest/v1/products?search={"updated":[{"operator":">","value":"${updatedAt}"}]}&limit=100&with_attribute_options=true`
);
let synced = 0, errors = 0;
for (const akeneoProduct of products) {
try {
const storefrontProduct = transformAkeneoProduct(akeneoProduct);
// Validate required fields before upserting
if (!storefrontProduct.name) {
console.warn(`Skipping ${akeneoProduct.identifier}: missing name`);
continue;
}
await db.products.upsert(storefrontProduct.sku, {
...storefrontProduct,
akeneoUpdatedAt: new Date(akeneoProduct.updated),
});
synced++;
} catch (err: any) {
errors++;
await db.syncErrors.insert({ productId: akeneoProduct.identifier, error: err.message });
}
}
await db.syncState.updateLastSync('akeneo_products', syncStartTime);
console.log(`Akeneo sync complete: ${synced} synced, ${errors} errors`);
}
Akeneo が商品を公開したときに Webhook で同期を開始する(Akeneo の Event API):
// Register the webhook endpoint in Akeneo under: Connect → Webhooks
// POST /api/webhooks/akeneo
export async function POST(req: NextRequest) {
const event = await req.json();
if (event.event_type === 'product.updated' || event.event_type === 'product.created') {
const sku = event.data.resource.identifier;
const akeneoProduct = await akeneo.getAll(`/api/rest/v1/products/${sku}?with_attribute_options=true`);
const storefrontProduct = transformAkeneoProduct(akeneoProduct);
await db.products.upsert(sku, storefrontProduct);
// Purge CDN cache for this product's page
await revalidateProductPage(storefrontProduct.slug);
}
return NextResponse.json({ received: true });
}
ベストプラクティス
- PIM を信頼できる唯一の情報源として扱い、コマースから PIM に商品コンテンツを書き戻さないでください — データは PIM からコマースに流します。PIM が明示的に管理するデータ(例:プラットフォームが生成する SEO メタデータ)のみを PIM にプッシュバックします。
- 全件同期ではなく差分同期を使用してください — 15 分ごとにすべての商品を取得するのはコストがかかります。Akeneo の
updatedフィルターを使って、変更された商品のみを取得します。 - 同期中に画像を独自の CDN にアップロードしてください — Akeneo のメディアファイル URL は認証が必要な内部 API URL です。顧客に直接配信しないでください。同期中に S3/R2/Cloudinary にアップロードします。
- 属性オプションをローカルにキャッシュしてください — 色、サイズ、素材のオプションラベルの参照先はほとんど変更されません。Redis にキャッシュし、毎時更新することで、商品ごとの API 呼び出しを避けます。
- ストアフロントに同期する前に必須属性を検証してください — 名前またはメイン画像のない商品は公開しないでください。アップサートの前に検証を追加します。
よくある落とし穴
| 問題 | 解決策 |
|---|---|
| 必須属性が欠けている商品で同期が失敗する | 商品ごとの同期を try/catch で囲み、エラーとともに商品識別子を記録します。同期全体を中断するのではなく、その商品をスキップして続行します。 |
| 同期後に画像を利用できない | Akeneo のメディア URL には認証が必要です。同期中にダウンロードして CDN に再アップロードしてください。akeneo-base-url/api/rest/v1/media-files/... を直接配信しないでください。 |
| Akeneo API のレート制限 | バッチリクエストを使用し、アクセスが少ない時間帯に同期を実行します。属性オプションをローカルにキャッシュして、商品ごとの API 呼び出しを減らします。 |
| PIM の再編成後、カテゴリーのマッピングが同期されていない | 商品同期の前に実行するカテゴリー同期ジョブを作成します。Akeneo のカテゴリーコードに対応するマッピングがコマースプラットフォームにない場合は、アラートを出します。 |
| Shopify コネクターがメタフィールドを同期しない | コネクターの設定にあるメタフィールドの名前空間とキーが、Shopify admin → Settings → Custom data → Products の設定と一致していることを確認します。 |
Related Skills
- @marketplace-connectors
- @webhook-architecture
- @erp-integration
- @analytics-integration