Dit Petje – Multi-brand product discovery platform

One Symfony codebase, three brands: Dit Petje, Dit Horloge and Dit Parfum

Stay in the loop

One email when something matters. No tracking, easy to unsubscribe.

Sister brands: dithorloge.nl, ditparfum.nl

Your data is only used for the confirmation email and updates. One-click unsubscribe.

Dit Petje

About the platform

Dit Petje is a multi-channel product discovery platform where visitors browse, compare and click through to providers per category. Under the hood it runs on MFP (Multi Feed Processing): a single Symfony codebase serving three consumer brands — ditpetje.nl (caps and hats), dithorloge.nl (watches) and ditparfum.nl (perfume) — and architected to support N channels via dynamic discovery (no hardcoded limit).

Collaboration between brand and builder

Concept, marketing and innovation plans originate from ten Bruggencate Marketing; development, ongoing engineering and operations are ours to deliver. The split keeps market insight and technical execution evolving in step, without either discipline blocking the other.

Architecture in one line

Feed sources → MFP Dashboard → channel sites → connected socials. Products are ingested centrally, filtered and scored per channel, and only surface once they pass the display gates.

Five feed sources, one pipeline

  • Awin (6h), Bol.com (12h), Amazon (24h), TradeTracker (12h) and Zalando (12h) — each source has its own schedule, validators and feed status (active / blocked / paused).
  • A global-sync orchestrator (feed:sync:global) downloads every source once and routes products to all relevant channels — preventing duplicate downloads as channels grow.
  • Per-source parsing via a shared FeedServiceTrait with central relevance detection (Dutch compound keywords like sportpet, winterpet are explicitly supported) — no copy-and-paste per source.
  • Blocked feeds automatically mark existing products as unavailable; paused feeds skip sync without losing data.

Strict channel separation

  • EAN lookups include channelId — prevents cross-channel matching when the same product appears on multiple channels.
  • Per-channel config in channel.{id}.yaml — its own price floor/ceiling, keyword allowlists, display thresholds and validation rules.
  • Per-channel Matomo site IDs — analytics stays clean and per-brand aggregation is correct.
  • Display gates per channelmin_display_score and require_image_for_display determine whether a product becomes visible; admin views see everything, the frontend only the quality filter.
  • Composite scoring combines price, availability, imagery and feed reliability into a single ranking score per product per channel.

Three brands, three design systems

Each brand has its own brand design system (colour, typography, tone) — served from the same Symfony application via a ChannelService that scopes templates, routes and configuration. Brand names, domain names and channel defaults are never hardcoded; everything routes through a channel context. That makes adding a fourth brand a config change, not a rewrite.

Editorial and SEO/GEO/WCAG

  • Editorial guides per brand with channel-specific author defaults — managed in a dedicated admin environment.
  • Product structured data with offers, price, priceCurrency and availability; products without imagery emit no schema (a Google requirement).
  • CLS-aware typography — Inter with display=optional, icon font with explicit dimensions to prevent layout shift.
  • Mobile-first design and build discipline on consumer pages; SEO, GEO and WCAG are applied during development, not bolted on afterwards.
  • Pinterest export feed and per-channel social integrations — content flows from the site to the connected socials.

Operational resilience

  • Auto-maintenance — channels with zero available products go into 503 mode automatically (cached 60s); prevents empty product pages during import gaps.
  • /health endpoint always returns 200 JSON, used by the Docker health check (skips the maintenance and rate-limit subscribers).
  • Redis for sessions and the live activity feed in the dashboard — graceful degradation if Redis is briefly unavailable.
  • Async messenger (two transports: feed_sync + enrichment) keeps feed imports and AI enrichment off the request cycle.
  • HTTP smoke tests per channel domain as a mandatory part of the release flow — PHPUnit alone does not catch Traefik/Apache config issues.

Technology and quality

  • Symfony 7.4 on PHP 8.3, Doctrine ORM, Twig 3.23 and Bootstrap 4.6 — production version v3.15.0.
  • 1,693 tests / 4,159 assertions via PHPUnit 11; PHPStan level 6 with a 0-error baseline across 439 files; PHP-CS-Fixer (PER-CS2) and phplint per release.
  • Bitbucket Pipelines runs phplint, CS-Fixer, PHPStan and PHPUnit on every master merge and then deploys via SSH.
  • Docker + Traefik dev and production stack; MySQL with a custom config; OPcache tuned to the codebase (14k+ files, max_accelerated_files=20000) to avoid silent recompilation.

Result

Three brands, one maintainable codebase, a steady release cadence (current major already on v3.x) and direct lines between marketing strategy and technical execution. Dit Petje also serves as a proving ground for patterns that resurface in client work — such as the multi-brand approach used at Stenenwinkeltje and in the Shopware Plugin Suite.