spree-catalog について
spree-catalogは、Spree 6のECカタログデータのモデル化と管理を説明するリファレンススキルです。商品、バリアント、商品企画、カスタムフィールド、メディア、ストアフロント検索を扱う開発者向けです。
主な機能
- 01商品と購入可能なバリアントを管理
- 02カテゴリーとコレクションで分類
- 03型付きカスタムフィールドを定義
- 04商品メディアとチャネル表示を管理
- 05カタログ検索とフィルターを設定
活用シーン
- 01バリアントと価格付きの商品を作成
- 02ナビゲーションや商品企画向けに分類
- 03店舗管理の商品属性を追加
- 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)
商品
| 属性 | 備考 |
|---|---|
status | draft(デフォルト)、active、archived。ストアフロントに表示されるのは active のみです。マーケットプレイスでは proposed/rejected(出品者の審査。spree-marketplace を参照)も追加されます。 |
name、slug、description、meta_* | 翻訳可能です(Mobility)。description はリッチテキストで、API は description(プレーンテキスト)と description_html を返します。 |
available_on、discontinue_on | status に加えて、販売期間を指定します。 |
store | belongs_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)による自動 |
| URL | permalink=完全なパス。例:clothing/shirts | permalink |
| 用途 | ナビゲーション | マーチャンダイジング:「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 で一括操作してください。
次の項目を確認してください。
status == 'active'(最初のゲート)であり、available_on/discontinue_onの期間内ですか?- このストアが所有していますか?
Spree::Product.for_store(store).exists?(product.id) - リクエストのチャネルで公開されていますか?
product.publication_for(channel)/Spree::Product.for_channel(channel)を確認し、公開期間内かどうかも確認してください。 - リクエストの通貨で基本価格が設定されていますか?
product.default_variant.price_in('EUR').persisted?— 現在の通貨で価格が設定されていない商品は非表示になります。 - B2B の場合、購入者のカタログによって品揃えが制限されることがあります(
Spree.products_for_context_service)。詳しくはspree-b2bを参照してください。同じ表示対象の制限は、買い物客からバリアントを受け取る処理(ウィッシュリストの項目など)にも適用されます。商品一覧に表示されないバリアントは拒否されます。 - Meilisearch のインデックスが古くなっていませんか?
spree rake spree:search:reindex
検索
Spree.search_provider(クラス名の文字列)は、Store API の商品一覧におけるテキスト検索、フィルター、ファセット、並べ替え、ページネーションを処理します。コントローラーがまずセキュリティ/表示範囲を構築し、その後に検索プロバイダーへ処理を委譲します。
| プロバイダー | 設定 |
|---|---|
Spree::SearchProvider::Database(デフォルト) | 設定不要です。ILIKE と Ransack を使用します。小規模なカタログに適しています。インデックス作成は行いません。 |
SpreeMeilisearch::SearchProvider | gem '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.mdnode_modules/@spree/docs/dist/developer/core-concepts/media.mdnode_modules/@spree/docs/dist/developer/core-concepts/metafields.md(カスタムフィールド)node_modules/@spree/docs/dist/developer/core-concepts/search-filtering.md、.../slugs.md、.../imports-exports.mdnode_modules/@spree/docs/dist/developer/how-to/custom-search-provider.md- 関連スキル:
spree-pricing、spree-inventory、spree-b2b、spree-marketplace、spree-i18n、spree-typescript-sdk