mcpField
S
spree-catalog

spree-catalog

github.com/spree開発10229 回インストール4 週間前に更新
リポジトリ

spree-catalog について

spree-catalogは、Spree 6のECカタログデータのモデル化と管理を説明するリファレンススキルです。商品、バリアント、商品企画、カスタムフィールド、メディア、ストアフロント検索を扱う開発者向けです。

主な機能

  1. 01商品と購入可能なバリアントを管理
  2. 02カテゴリーとコレクションで分類
  3. 03型付きカスタムフィールドを定義
  4. 04商品メディアとチャネル表示を管理
  5. 05カタログ検索とフィルターを設定

活用シーン

  1. 01バリアントと価格付きの商品を作成
  2. 02ナビゲーションや商品企画向けに分類
  3. 03店舗管理の商品属性を追加
  4. 04チャネルに商品が表示されない原因を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/spree/agent-skills --skill spree-catalog

インストール後、Claude Code で/spree-catalogを入力して読み込まれたか確認します。

詳細

Spree カタログ

カタログには、販売するものすべてが含まれます。商品(掲載情報)、バリアント(実際に購入する対象。SKU、価格、在庫)、オプションタイプ/値(バリアントを区別する要素)、カテゴリーとコレクション(商品のグループ分け)、カスタムフィールド、メディア、そして商品を検索可能にする検索プロバイダーです。

Spree 5.x(マスターバリアント、タクソン、メタフィールド、presentation)から移行する場合は、名称変更の対応表を spree-upgrade-5-to-6 スキルで確認してください。このスキルでは、Spree 6 の仕組みを説明します。

カタログのグラフ

Store ─┬─ Product  (prod_, belongs_to :store — single owner; status draft/active/archived)
       │    ├── default_variant  (real FK default_variant_id — no master variant)
       │    ├── Variant × n  (variant_) — sku, barcode, weight…, Price per currency, StockLevel per location
       │    │     └── OptionValue × n (optval_)  via option_value_variants
       │    ├── ProductOptionType → OptionType (opt_) — name, label, kind (dropdown|color_swatch|buttons)
       │    ├── ProductCategory → Category (ctg_) — nested tree, permalink "clothing/shirts"
       │    ├── ProductCollection → Collection (coll_) — flat; manual or automatic (rules)
       │    ├── ProductPublication → Channel — visibility per channel, optional time window
       │    ├── Media (media_) — product gallery; primary_media = thumbnail
       │    ├── CustomField (cf_) → CustomFieldDefinition (cfdef_, store-owned)
       │    ├── ProductType (pt_) — template it was created from (optional)
       │    ├── DeliveryProfile — how it ships (required; auto-assigned)
       │    └── Seller — marketplace owner (nil = the operator's own catalog)
       └── Catalog (cat_) — audience-specific assortment + price list (see spree-pricing / spree-b2b)

商品

属性備考
statusdraft(デフォルト)、active、archived。ストアフロントに表示されるのは active のみです。マーケットプレイスでは proposed/rejected(出品者の審査。spree-marketplace を参照)も追加されます。
name、slug、description、meta_*翻訳可能です(Mobility)。description はリッチテキストで、API は description(プレーンテキスト)と description_html を返します。
available_on、discontinue_onstatus に加えて、販売期間を指定します。
storebelongs_to :store — 商品は所有元のストアを1つだけ持ちます(Product.for_store(store))。デフォルトは Spree::Current.store/Spree::Store.default です。
delivery_profile_id必須です。作成時に商品タイプのプロファイルが設定され、ない場合はストアのデフォルトプロファイルが設定されます。
product_type_id、tax_category_id、seller_id、tags、metadata運用フィールドです(Admin API のみ)。

ステータスはステートマシンではなく、単純な文字列カラム(Spree::HasStatus)です。product.active?、Product.published(status: 'active')、Product.not_archived を使用できます。

商品の作成

Admin API が標準の書き込み経路です(商品ワークフローを実行し、プレフィックス付き ID をデコードします)。

import { createAdminClient } from '@spree/admin-sdk'
const admin = createAdminClient({ baseUrl: 'https://store.example.com', secretKey: 'sk_xxx' })

// Single-variant product: prices go straight on the product (forwarded to its one variant)
await admin.products.create({
  name: 'The Spree Handbook',
  status: 'active',
  prices: [{ currency: 'USD', amount: '19.99' }],
})

// Product with options: send variants; options are matched by name/value (no ID lookups)
await admin.products.create({
  name: 'Premium T-Shirt',
  status: 'draft',
  variants: [{
    sku: 'TEE-S-NAVY',
    options: [{ name: 'size', value: 'Small' }, { name: 'color', value: 'navy' }],
    prices: [{ currency: 'USD', amount: '29.99' }],
    stock_levels: [{ stock_location_id: 'sloc_xxx', count_on_hand: 50 }],
  }],
  product_publications: [{ channel_id: 'ch_xxx' }], // Admin API does NOT auto-publish
})
spree api post /products -d '{"name":"The Spree Handbook","status":"active","prices":[{"currency":"USD","amount":"19.99"}]}'
spree api post /products/prod_xxx/clone          # duplicate as a new draft
spree api delete /products/prod_xxx              # soft delete

トップレベルの prices と variants の両方を送信しないでください。一括エンドポイント(bulkStatusUpdate、bulkAddToCategories、bulkAddTags、bulkDestroy)もあります。

Ruby での作成(シード、タスク、スペック):

Spree::Current.store = Spree::Store.default   # outside a request — Store.default can be nil in a fresh DB
product = Spree::Product.create!(name: 'Tote', status: 'active', sku: 'TOTE-1')  # sku= builds the default variant
product.default_variant.set_price('USD', 19.99)     # prices are per currency — see spree-pricing
product.set_custom_field('properties.material', 'Canvas')

バリアントとデフォルトバリアント

すべての商品には少なくとも1つのバリアントがあり、すべてのバリアントは実在し、購入可能です。隠れたマスターはありません。 オプションのない本でも、SKU、価格、在庫を持つバリアントが1つあります。

  • product.default_variant — 永続化される外部キー default_variant_id。商品を代表するバリアントです(掲載価格や、選択前のカート追加に使用)。明示的に設定されていない場合は最初のバリアントになり、削除された場合は別のバリアントが自動的に昇格します。
  • 商品レベルの sku、barcode、weight、price_in、amount_in、track_inventory…はデフォルトバリアントに委譲されます(セッターを使うと必要に応じてデフォルトバリアントが構築されます)。
  • product.variants はすべてのバリアントを返します(位置順)。product.has_multiple_variants? は variant_count カウンターキャッシュを使用します。
  • buy_box_variant_id は、複数の出品者が商品を出品している場合に、マーケットプレイスが掲載対象として選ぶバリアントです。
await admin.products.variants.create('prod_xxx', {
  sku: 'TEE-L-R',
  options: [{ name: 'size', value: 'Large' }, { name: 'color', value: 'Red' }],
  prices: [{ currency: 'USD', amount: '24.99' }],
})

オプションタイプと値

size  = Spree::OptionType.create!(name: 'size',  label: 'Size',  kind: 'buttons')
color = Spree::OptionType.create!(name: 'color', label: 'Color', kind: 'color_swatch')
color.option_values.create!(name: 'navy', label: 'Navy', color_code: '#1f2a44')

color.color_swatch?        # kind == 'color_swatch'
variant.options_text       # "Size: Large, Color: Red"
variant.option_value('size') # "Large" (the value's label)
  • 表示用フィールドは presentation ではなく label(翻訳可能)です。where(label: ...) で検索し、API では q[label_cont] フィルターを使います。
  • kind は dropdown、color_swatch、buttons のいずれかです(Spree::OptionType::KINDS)。
  • Admin API でオプションタイプに option_values を送信すると、値のセット全体が置き換えられます。維持したい値はすべて含めてください。
  • オプションタイプはストアに所有されます。商品が使用できるのは自分のストアのオプションタイプのみです(ProductOptionType が検証します)。バリアントの options: [{ name:, value: }] も、商品と同じストア内のオプションタイプに一致するか、同ストア内に作成されます。Spree::OptionValue.for_store(store) は、オプションタイプを通じて値をスコープします。

商品タイプ(テンプレート)

Spree::ProductType(pt_)には、「すべての靴にサイズと色が必要で、フットウェアに属し、標準プロファイルで配送され、素材を尋ねる」という内容を記録します。

項目動作
オプションタイプ、カテゴリー、配送プロファイル商品の作成時にコピーされ、その後は独立します。シード処理は追加のみで、削除は行いません。
カスタムフィールド定義参照によるライブ反映です。商品フォームには常に現在のタイプが反映されます。requiredは参考情報であり、書き込み時には強制されません。
const shoes = await admin.productTypes.create({
  name: 'Footwear',
  option_type_ids: ['opt_size', 'opt_color'],
  category_ids: ['ctg_footwear'],
  delivery_profile_id: 'fp_standard',
  custom_field_definitions: [{ id: 'cfdef_material', required: true, sort_order: 0 }],
})
await admin.products.create({ name: 'Trail Runner', product_type_id: shoes.id })
await admin.productTypes.applyToProducts(shoes.id)   // explicit, background, additive re-seed

タイプを編集しても、既存の商品は書き換えられません。商品が紐付いているタイプは削除できません。

カテゴリーとコレクション

Spree::Category(ctg_)Spree::Collection(coll_)
形式入れ子のツリー(parent、descendants)、ストア所有フラット、ストア所有
メンバーシップ割り当て(product.category_ids = [...])手動、またはルール(available_on、sale、tagのルールタイプ、一致条件はall/any)による自動
URLpermalink=完全なパス。例:clothing/shirtspermalink
用途ナビゲーションマーチャンダイジング:「2026年夏」「50ドル未満」「ベストセラー」
shirts = Spree::Category.for_store(store).find_by!(permalink: 'clothing/shirts')
shirts.active_products_with_descendants
Spree::Product.in_category(shirts)            # includes descendants
Spree::Product.in_collection(collection)
  • category_ids=/collection_ids=が受け付けるのは、その商品のストアに属するIDのみです(他ストアのIDは通知なく除外されます)。collection_ids=が管理するのは手動のメンバーシップのみです。自動メンバーシップはルールから導出され、維持されます。
  • ブランド:ナビゲーションに含める場合はカテゴリー、ランディングページにする場合はコレクションとしてモデル化し、必要に応じてbrandカスタムフィールドを追加します。それ以上の独自データがブランドに必要な場合にのみ、Spree::Brandモデル(spree-resource)を作成してください。
  • Spree::Taxon/Taxonomyとproduct.taxonsは非推奨のエイリアスです(6.1で削除)。

ストア API:client.categories.get('clothing/shirts')、client.categories.products.list(...)、client.collections.products.list('summer-2026')。プロモーションではcategoryルールを使用してカテゴリーを対象にします(spree-promotionsを参照)。

カスタムフィールド

型付きの、マーチャントが管理するデータです。マイグレーションもデコレーターも不要です。定義(ストア所有のSpree::CustomFieldDefinition)で形式を宣言し、カスタムフィールド(Spree::CustomField)で各レコードの値を保持します。

const def = await admin.customFieldDefinitions.create({
  resource_type: 'Spree::Product',
  namespace: 'properties', key: 'material', label: 'Material',
  field_type: 'short_text',          // short_text | long_text | rich_text | number | boolean | json
  storefront_visible: true,          // false = absent from the Store API entirely
  searchable: true,                  // also: sortable — both make it filterable
})
await admin.products.customFields.create('prod_xxx', { custom_field_definition_id: def.id, value: '100% Cotton' })
product.set_custom_field('properties.material', '100% Cotton')   # blank value deletes the row
product.get_custom_field('properties.material')&.value
product.storefront_custom_fields                                  # storefront_visible only
store.custom_field_definitions                                    # read definitions through the store

ストア API:?expand=custom_fields。cf_キーでフィルター/並べ替えを行います:q[cf_properties_material_i_cont]=wool、sort=-cf_properties_weight。カスタムフィールドは人が使うためのもので、metadata(JSON)はシステム向けであり、ストア APIには出力されません。

メディア

Spree::Media(media_。以前のSpree::Asset)はストア所有で、メディアライブラリに保存されます。ファイルは配置せずにアップロードし、後から商品、バリアント、カテゴリー、コレクションに添付できます。再利用時に共有されるのはBlobであり、レコードではありません。

  • media_typeはimage、video(アップロードしたMP4/WebM/QuickTime。poster_signed_idは任意)、external_video(YouTube/Vimeoのexternal_video_url。video_embed_urlは保存時に生成されます)のいずれかです。
  • product.primary_media/variant.primary_mediaはサムネイル(最初にレンダリング可能な画像)です。product.gallery_mediaはバリアントの画像にフォールバックします。ストア APIのthumbnail_urlは常に存在するため、一覧ページでexpand=mediaは使用しないでください。
  • focal_point_x/yでトリミングを考慮したサムネイルを指定できます。名前付きサイズにはActive Storageのバリアントを使用します。
  • 使用中のメディアファイルを削除すると、使用箇所とともに422が返されます(admin.media.usage(id))。
await admin.products.media.create('prod_xxx', { signed_id, alt: 'Front' })  // after a direct upload
await admin.products.media.create('prod_xxx', { media_type: 'external_video', external_video_url: 'https://youtu.be/…' })

チャネルの表示(「商品が表示されない」)

商品がチャネルに属するのは、ProductPublication で両者が関連付けられている場合のみです(published_at/unpublished_at の期間は任意です)。ダッシュボードでは新しい商品がデフォルトのチャネルに自動公開されますが、Admin API では公開されません。PATCH /products/:id { product_publications: [...] } で全件を指定してください(含まれていないチャネルとの関連付けは解除されます)。または、POST /channels/:id/add_products/remove_products で一括操作してください。

次の項目を確認してください。

  1. status == 'active'(最初のゲート)であり、available_on/discontinue_on の期間内ですか?
  2. このストアが所有していますか?Spree::Product.for_store(store).exists?(product.id)
  3. リクエストのチャネルで公開されていますか?product.publication_for(channel)/Spree::Product.for_channel(channel) を確認し、公開期間内かどうかも確認してください。
  4. リクエストの通貨で基本価格が設定されていますか?product.default_variant.price_in('EUR').persisted? — 現在の通貨で価格が設定されていない商品は非表示になります。
  5. B2B の場合、購入者のカタログによって品揃えが制限されることがあります(Spree.products_for_context_service)。詳しくは spree-b2b を参照してください。同じ表示対象の制限は、買い物客からバリアントを受け取る処理(ウィッシュリストの項目など)にも適用されます。商品一覧に表示されないバリアントは拒否されます。
  6. Meilisearch のインデックスが古くなっていませんか?spree rake spree:search:reindex

検索

Spree.search_provider(クラス名の文字列)は、Store API の商品一覧におけるテキスト検索、フィルター、ファセット、並べ替え、ページネーションを処理します。コントローラーがまずセキュリティ/表示範囲を構築し、その後に検索プロバイダーへ処理を委譲します。

プロバイダー設定
Spree::SearchProvider::Database(デフォルト)設定不要です。ILIKE と Ransack を使用します。小規模なカタログに適しています。インデックス作成は行いません。
SpreeMeilisearch::SearchProvidergem 'spree_meilisearch' と spree bundle install を追加し、MEILISEARCH_URL(本番環境では MEILISEARCH_API_KEY も)を設定します。config/initializers/spree.rb で Spree.search_provider = 'SpreeMeilisearch::SearchProvider' を設定し、インデックスを再作成します。入力ミスの許容、関連度、ファセットの高速化に対応します。
独自のプロバイダーSpree::SearchProvider::Base をサブクラス化し、search_and_filter(scope:, query:, filters:, sort:, page:, limit:)(SearchResult を返します)、self.indexing_required?、index、remove、remove_by_id、index_batch、reindex を実装します。
spree rake spree:search:reindex        # per store; no-op work for the Database provider
  • インデックスドキュメントの構造は Spree::Dependencies.search_product_presenter によって決まります(Gem によって SpreeMeilisearch::ProductPresenter に設定されます。コアには付属しません)。フィールドを追加するには、これを差し替えてください。product.search_presentation でドキュメントをプレビューできます(ロケール × 通貨ごとに 1 件)。
  • インデックスには接頭辞付き ID(prod_…、ctg_…)を使用し、生の ID は使用しないでください。
  • 一括インポート後、プロバイダーの切り替え後、またはカスタムフィールドの searchable/sortable フラグの変更後に、インデックスを再作成してください。
  • Ruby スコープ:Product.search(query)、.with_option_value(option, value)、.with_option_value_ids、.in_category、.in_categories、.in_collection、.price_between、.in_stock。独自のスコープは通常の scope で定義してください(add_search_scope はありません)。

デジタルアセット

バリアントは、配送プロファイルがデジタル用の場合にデジタルと見なされます。Spree::DigitalAsset を通じてファイルを保持します(dig_、バリアントに属し、ファイルはプライベートストレージに保存されます。直接アップロードをリクエストする際は private: true を指定してください)。注文が確定すると、アセットごと・数量ごとに Spree::DigitalLink が 1 件付与されます(トークン、authorized_clicks のデフォルトは 5、authorized_days のデフォルトは 7)。イベント:digital_asset.*、digital_link.downloaded。ライセンスキーなどのカスタム納品物はデジタルアセットプロバイダーとして実装します。spree-providers と docs/developer/how-to/custom-digital-asset-provider を参照してください。

インポート/エクスポート

CSV のインポートとエクスポートは Admin API(admin.imports、admin.exports、type: 'products' など)を通じて実行します。カスタムフィールドの列には、custom_field.<namespace>.<key> というプレフィックスを使用します。種類ごとにテンプレートをダウンロードすると、必要な列を確認できます。docs/developer/core-concepts/imports-exports を参照してください。

注意点

  • variant.price / product.price= は存在しません。価格は通貨ごとに設定します(price_in、set_price)。spree-pricing を参照してください。
  • Product#default_image、#featured_image、#primary_image は廃止されました → primary_media を使用してください。OptionType#color? → color_swatch?。
  • Spree::Product.all ではなく Spree::Product.for_store(store) を使用してください。また、ジョブやテストで Spree::Store.default が存在すると決めつけないでください。Spree::Current.store を設定してください。
  • カスタム商品属性を追加する場合は、カスタムフィールドを優先してください。どうしてもカラムが必要な場合は、カラムを追加し、Spree::Product.additional_permitted_attributes += [:brand_id] で許可パラメーターを拡張してください(配列は凍結されているため、<< は使用しないでください)。
  • 管理 API では、リソースの解決にプレフィックス付き ID のみを使用します。Store API ではスラッグまたは ID を使用できます。古いスラッグは friendly_id_slugs に保持されますが、Store API では 404 になるため、ストアフロントでリダイレクトしてください。
  • 出品者が送信した商品のステータスは、出品者が変更できません。送信/承認/却下を使用してください。spree-marketplace を参照してください。

さらに詳しく読むには

  • node_modules/@spree/docs/dist/developer/core-concepts/products.md
  • node_modules/@spree/docs/dist/developer/core-concepts/media.md
  • node_modules/@spree/docs/dist/developer/core-concepts/metafields.md(カスタムフィールド)
  • node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md、.../slugs.md、.../imports-exports.md
  • node_modules/@spree/docs/dist/developer/how-to/custom-search-provider.md
  • 関連スキル:spree-pricing、spree-inventory、spree-b2b、spree-marketplace、spree-i18n、spree-typescript-sdk

コメント