Market of Choice

Architecture Overview

What's where, what talks to what, and where it's hosted.

Two independent WordPress installations. Not multisite — separate databases, separate themes, separate repos, separate deploy pipelines. They share a Cloudflare R2 bucket for media, but that's it.

System Diagram

The Two Sites

Marketing SiteReservations Site
URLmarketofchoice.comreservations.marketofchoice.com
PurposeStore locations, specials, recipes, blog, maker profiles, careersCatering orders, Supper Club reservations, seasonal holiday ordering
WP Engine installmarketofchoicemarketofccat
ThemeAvada + Avada-Child-ThemeAvada + Moc-Avada
WooCommerceNoYes — but no payment processing

The reservations site is a reservation/ordering system with no payment processing. No Stripe, no PayPal, no payment gateway. Customers browse a menu, build a cart, and submit a pickup reservation. WooCommerce is overkill for this — the entire payments/shipping/taxes/coupons/refunds stack goes unused.

Media and CDN

All media is offloaded to Cloudflare R2 (S3-compatible object storage with free egress). This is not optional infrastructure — it's where the images live. If you don't understand R2, read the Decision Log before touching anything.

  • Marketing site media: static.marketofchoice.com
  • Reservations site media: static.marketofchoice.com/catering
  • PDFs are excluded from R2 offload on the reservations site
  • Credentials: S3_UPLOADS_* env vars in wp-config.custom.php, from .env locally and WP Engine's environment in production

Reservations Site — Where the Complexity Lives

MU-Plugin System

All custom business logic lives in src/wp-content/mu-plugins/. The yax-loader.php auto-loads plugins with priority-based dependency resolution. Disable any plugin by renaming it to .disabled.

The seasonal manager and Supper Club manager are completely independent systems. They must never be mixed in the same hook or filter. See Gotchas.

Core business plugins (moc-*):

PluginWhat It Does
moc-seasonal-manager.phpControls product availability across five seasons (standard, easter, thanksgiving, christmas, extendedChristmas). ~1000+ lines. Has a "lock" feature to prevent automated sweeps from overriding manual stock decisions.
moc-supper-club-manager.phpRolling weekly Supper Club menu across 14 store locations. Wednesday-based cycle. ~967 lines.
moc-pricing-manager.phpCentralized pricing: weight-based ("Estimated price based on $X.XX/lb"), per-lb, "Special Order" for $0, price overrides via _moc_price_override meta.
moc-currently-unavailable.phpOverrides "Out of Stock" text. Hides OOS from catalog. Shows "Currently Unavailable" for Supper Club.
moc-limited-quantity.phpPer-product max quantity (default: 4). Meta keys: _moc_max_quantity, _moc_enable_max_quantity.
moc-hide-quantity-field.phpHides quantity input for weight-based products.
moc-raw-product.phpAuto-appends "(Raw - requires cooking at home)" to titles.
moc-supper-club-datalayer.phpPushes supper_club boolean to GA4 data layer for audience segmentation via GTM4WP.
seasonal-product-prefix.phpPrefixes unpublished seasonal products with the season name for admin clarity.
disable-cart-fragments.phpDisables WooCommerce cart fragment AJAX for performance.

Infrastructure plugins (yax-*): yax-loader.php (auto-discovery + dependency resolution), yax-common/ (service container), yax-cleanup/ (admin declutter), yax-disable-comments/, yax-plugin-modifications/, yax-version-sync/.

CDN/Media: s3-r2-support.php (R2 config), uploads-to-cdn.php (URL rewriting), exclude-pdf-from-s3.php (PDFs stay local).

Theme: Moc-Avada

Child theme at src/wp-content/themes/Moc-Avada/ with 24 PHP files in functions/:

Notable Plugins (Regular)

Custom MoC plugins in src/wp-content/plugins/:

PluginWhat It Does
moc-reportsSix custom report types: Pickup, Supper Club Pickup, Sales, Supper Club Sales, Turkey Orders, Financial. Includes cache-bypass fallback after EverCache incident.
moc-woo-scheduled-stockScheduled stock status changes at specific dates. Meta keys: _oos_at, _is_at.
moc-order-statusCustom order statuses (2 Day Reminder, auto-completion).
moc-allergen-infoAllergen display on product pages.
moc-supper-clubSupper Club frontend display.
moc-supper-club-mailchimp-tagTags SC customers in MailChimp.
moc-continue-shoppingContinue Shopping button in cart.
moc-previously-frozen-productMarks products as previously frozen.
moc-structured-contentSchema.org structured data markup.

Key third-party plugins:

PluginWhat It Does
woocommerceCore commerce engine (used for cart/checkout/orders — no payment processing).
google-listings-and-adsSyncs products to Google Merchant Center with gla_ prefix.
duracelltomi-google-tag-managerGTM4WP — generates dataLayer ecommerce events.
weight-based-pricing-for-woocommerceWeight-based pricing support.
flexible-checkout-fields + flexible-checkout-fields-proCustom checkout fields (store selection, pickup date).
woocommerce-pdf-invoices-packing-slips + proPDF packing slips for store staff.
woocommerce-order-status-managerCustom order status definitions.
woocommerce-sequential-order-numbers-proSequential order numbering.
woo-schedule-stock-managerAdditional stock scheduling.
wordpress-seoYoast SEO (also on reservations site, not just marketing).
mailchimp-for-woocommerceMailChimp integration for checkout email signups.
mailgunTransactional email delivery.
s3-uploadsS3/R2 media offload (Composer dependency).
wp-crontrolWP-Cron management and debugging.
side-cart-woocommerceSide cart/mini cart UI.
admin-columns-proEnhanced admin list table columns.
capability-manager-enhancedUser role/capability management.

Product Data Model

  • SKU (PLU): The canonical product identifier. Always use SKU, never WP post IDs. The client references everything by PLU number. API: wc/v3/products?sku=XXXXX.
  • product_availability: ACF meta field (field_6722be5565a68). Array of seasons like ["standard", "easter"]. NULL = standard.
  • Weight-based pricing: Identified by weight_based_pricing meta = 1. Two fields must stay in sync: _moc_price_override (per-lb display) and regular_price (per_lb x max_weight_range).
  • Allergens: Stored in TWO places — the Allergens product attribute (for filtering) AND the <strong>Allergens:</strong> line in the description HTML. Both must match.
  • Category IDs: Bakery=193, Kitchen=197, Appetizer=192.
  • Supper Club tags: Supper Club (222), Standard (249), Chef's Favorite (248), Add-ons (229).

Tracking and Analytics

SystemDetails
GTMContainer GTM-MTKDTZ4. Injected via theme JS (Moc-Avada/js/google-tag-manager.js), NOT via GTM4WP plugin setting.
GTM4WPGenerates dataLayer ecommerce events. Product ID prefix must be gla_ to match Merchant Center.
Google AdsAccount 470-848-8665. Merchant Center 5592882755. Products synced via Google Listings & Ads plugin with gla_ prefix.
MNTNPixel on both sites. Conversion pixel on /order-received.
MetaPixel on both sites.

Marketing Site — Simpler but Avada-Heavy

The marketing site is mostly static content managed through Avada's Fusion Builder. The child theme (Avada-Child-Theme) has custom templates and a functions/ directory, but the majority of page content lives in the WordPress database as serialized Fusion Builder shortcodes in post content — not in template files. See Notes for what this means in practice.

Key Plugins

PluginWhat It Does
Yoast SEOSEO meta. MadFish Digital provides recommendations.
WP Ultimate Post GridPowers dynamic content on store location pages (manager photos, featured makers).
Gravity FormsContact forms, vendor application form at /new-vendor-information/.
PublishPress RevisionsScheduled content updates for weekly specials.
Custom Post Type UICustom post types for specials, recipes.
Google Site KitGA4 integration.

Content Patterns

  • Store locations: Avada pages with WPUPG grid shortcodes at marketofchoice.com/locations-{slug}/. Dynamic content (manager, maker, blog) pulled from regular posts.
  • Weekly specials: Custom post type. PDF + CSV + Issuu embed. Updated weekly.
  • Recipes: Custom post type with ACF fields (Serves, Difficulty, Prep Time, Cook Time).
  • Maker profiles: Regular posts. When removing, scrub all mentions — home page, local photos, Amplify pages, specialty pages.

Local Development

Six Docker containers: WordPress FPM, MySQL 8.0, Nginx (serves static files + FastCGI proxy), Caddy (HTTPS termination), WP-CLI, Dozzle (log viewer). No Redis locally — WP Engine uses Memcached in production, which isn't replicated in the local dev stack.

npm run setup          # First-time: generates Caddyfile, updates /etc/hosts
npm run start          # Start all containers
npm run stop           # Stop containers
npm run logs           # All container logs
npm run logs:wordpress # PHP-FPM only
npm run shell          # Bash into WordPress container
npm run wp -- <cmd>    # WP-CLI (e.g., npm run wp -- plugin list)
npm run build          # Build dist/ for deployment
  • Local URL: https://reservations.marketofchoice.localhost
  • DB: localhost:3306
  • Dozzle: http://localhost:8888

Three containers: WordPress (Apache), MariaDB, Caddy. No Nginx, no Redis, no WP-CLI container.

docker compose up -d   # Start containers
docker compose down    # Stop containers
  • Local URL: https://marketofchoice.localhost
  • DB: MariaDB 10.6

On this page