skills/catalog-inventory/product-bundles-kits/SKILL.md
Sell grouped products as bundles or kits with automatic inventory deduction, bundle pricing, and display logic using platform apps
npx skillsauth add finsilabs/awesome-ecommerce-skills product-bundles-kitsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Product bundles let you sell multiple products together — optionally at a discount — as a single purchasable unit. This increases average order value and is one of the most effective upsell mechanisms in e-commerce. Dedicated bundle apps handle the inventory tracking, pricing, and display logic that platforms don't support natively. Only build a custom bundle system if your kit configuration requirements (dynamic assembly, real-time pricing, component substitution) exceed what apps offer.
| Platform | Recommended Tool | Why | |----------|-----------------|-----| | Shopify | Bundler — Product Bundles or Bundle Builder | Bundler is the most popular; handles fixed bundles, mix-and-match, and inventory deduction per component | | WooCommerce | WooCommerce Product Bundles (WooCommerce extension) | Official WooCommerce extension; supports fixed and configurable bundles, per-component pricing, and inventory | | BigCommerce | Product Bundler app or native "Frequently Bought Together" | BigCommerce App Marketplace has several bundle apps; native "Frequently Bought Together" for simple cross-sells | | Custom / Headless | Build bundle data model with component inventory deduction | Required when bundle logic (real-time pricing, dynamic component substitution) exceeds app capabilities |
Option A: Bundler — Product Bundles (recommended)
For "Frequently Bought Together":
Important for Shopify Plus:
WooCommerce Product Bundles (official extension):
For "Frequently Bought Together":
Bundle apps from the App Marketplace:
Native cross-sell / "Frequently Bought Together":
For headless storefronts, build a bundle model where components are stored as separate cart line items (simplifies inventory, tax, and fulfillment):
// lib/bundles.ts
interface BundleComponent { variantId: string; quantity: number; unitPrice: number; }
interface BundlePricing { componentSum: number; bundlePrice: number; savings: number; savingsPct: number; }
// Calculate bundle price dynamically from current component prices
export async function calculateBundlePrice(bundle: Bundle, selectedVariants: BundleComponent[]): Promise<BundlePricing> {
const componentSum = selectedVariants.reduce((total, c) => total + c.unitPrice * c.quantity, 0);
let bundlePrice: number;
switch (bundle.pricingType) {
case 'fixed': bundlePrice = bundle.pricingValue; break;
case 'discount_pct': bundlePrice = +(componentSum * (1 - bundle.pricingValue / 100)).toFixed(2); break;
case 'discount_abs': bundlePrice = Math.max(0, +(componentSum - bundle.pricingValue).toFixed(2)); break;
default: bundlePrice = componentSum;
}
const savings = +(componentSum - bundlePrice).toFixed(2);
return { componentSum, bundlePrice, savings, savingsPct: componentSum > 0 ? Math.round((savings / componentSum) * 100) : 0 };
}
// Check availability for all bundle components atomically
export async function checkBundleAvailability(selectedVariants: BundleComponent[]) {
const unavailable = [];
for (const { variantId, quantity } of selectedVariants) {
const level = await db.inventoryLevels.findFirst({ where: { variantId } });
const available = (level?.onHand ?? 0) - (level?.reserved ?? 0);
if (available < quantity) {
unavailable.push({ variantId, requested: quantity, available: Math.max(0, available) });
}
}
return { available: unavailable.length === 0, unavailable };
}
// Add bundle to cart as separate line items (grouped by bundle ID for UI display)
export async function addBundleToCart(cartId: string, bundleId: string, selectedVariants: BundleComponent[]) {
const bundle = await db.productBundles.findUnique({ where: { id: bundleId } });
const pricing = await calculateBundlePrice(bundle, selectedVariants);
const { available, unavailable } = await checkBundleAvailability(selectedVariants);
if (!available) throw new Error(`Bundle not available: ${unavailable.map(u => u.variantId).join(', ')}`);
// Create a bundle group record to keep line items visually associated
const bundleGroup = await db.cartBundleGroups.create({
data: { cartId, bundleId, bundlePrice: pricing.bundlePrice, bundleSavings: pricing.savings },
});
// Pro-rate the discount across components proportionally to their share of the total
const lineItems = selectedVariants.map(({ variantId, quantity, unitPrice }) => {
const itemSubtotal = unitPrice * quantity;
const discountShare = pricing.savings * (itemSubtotal / pricing.componentSum);
const discountedUnitPrice = +(unitPrice - discountShare / quantity).toFixed(4);
return { cartId, variantId, quantity, bundleGroupId: bundleGroup.id, unitPrice: discountedUnitPrice };
});
await db.cartItems.createMany({ data: lineItems });
return bundleGroup;
}
Bundle display checklist:
For Shopify/Bundler: The app generates a bundle product page automatically — customize the template in Bundler → Customize
For WooCommerce Product Bundles: The plugin renders a bundle-specific product page layout; style it using the built-in CSS settings or child theme overrides
After setting up bundles, test that inventory deducts correctly for each component:
| Problem | Solution |
|---------|----------|
| Bundle discount applied incorrectly at checkout | Use the app's built-in discount logic; don't layer additional discount codes on top of bundle discounts without testing the interaction |
| One bundle component goes out of stock mid-cart | Re-check availability at checkout; display which specific component is unavailable so the customer can adjust |
| Bundle pricing stale when component prices change | Recalculate bundle price on each cart refresh and at checkout, not just at add-to-cart time |
| Fulfillment system confused by bundle line items | For headless builds, ensure each line item has a standard variant_id and quantity; use bundle_group_id only for UI grouping |
| Inventory not decremented for all bundle components | After setup, always test with a real order and verify each component SKU decremented by the correct quantity |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion