Shopware 6 · Theme · Data migration

Muskelkater Sport
rebuilt.

Muskelkater Sport in Cologne-Zollstock sells bicycles, skis and tennis rackets – and has done so online for years. Technically the shop ran cleanly, but it looked like an off-the-shelf kit. I rebuilt the interface completely: custom theme, new navigation, new home page, a rental calculator and a dark mode. It has been running in the real shop since August. Then came the second part – bringing over the stock of the previous shop. The slider fades between the two versions.

Completed · live since 2 August 2026
BEFORE AFTER

Before and after, drag to compare

Four pages from the same shop: on the left the version that was online until the switchover, on the right the one that is there now. The slider, a tap on the image or the arrow keys fade between the two versions.

Home page after the rebuild: calm welcome area with text on the left and an offer card on the right. Home page before the rebuild: colourful full-width advertising banner and a dense product grid.

Home page. The full-width advertising banner gives way to a welcome area that says in one sentence what it is about: buy online, get advice in the shop. Address, opening hours and phone number now sit at the top instead of at the very bottom. Below them follow offers, range, workshop and rental in a fixed order – instead of a long chain of identical-looking product rows.

Left: 1 August 2026, the last day of the old shop. Right: 2 August 2026, captured on the day of the switchover directly from the live site. Both in the same browser and at the same window width, so that the comparison holds.

The left-hand page no longer exists on the web. It only survives as a backup on my side – and that is exactly why it is shown here.

Dark mode, a second colour system

The easy way would have been to invert all brightness values: a filter over the page, done. The result is familiar – grey photos, washed-out colours and text stuck halfway between foreground and background. Instead, every surface, every line and every text colour got its own dark value. The slider compares both versions.

Home page in dark mode: deep green background, lighter brand colour, product images still on a light surface. The same home page in the light view.

Home page. The background does not turn black but a very dark green – the same family as the brand, just at the very bottom of the scale. The brand green goes the opposite way and becomes lighter; otherwise it would vanish into the background.

Set anew, not inverted

Background, cards, lines, text, brand, warning colours: every value has its own dark counterpart. That is more work than a filter – and the only way in which colours stay colours.

42 errors, measured

The first pass found 42 places below the AA standard. They could be traced back to nine causes – fixing those fixes all 42.

Photos stay light

Cut-out product images keep their light surface. Otherwise a bike on a white background looks like a forgotten clipping on a dark page.

The quiet corners too

Forms, tables, select fields, overlays, notice bars, the empty basket: the places people think of last in the dark, and where it shows first.

The choice stays

A toggle at the top right is all it takes. The choice is remembered and applied on the next visit, before the page is drawn – which is why nothing flashes white while loading.

The switch has a name

A moon icon alone tells a screen reader nothing. The toggle carries a label that still works when nobody sees the icon.

What is different in concrete terms

No new logo, no new brand – the substance of the shop stays. What has changed is everything that stands between visitor and goods.

AreaBeforeAfter
First impression Full-width advertising banner, products straight after One sentence explaining the shop, plus address, hours and phone
Header navigation Flat bar, subcategories only after several clicks Eight main items with seven drop-down menus, built from the real category tree
Mobile menu The same items several times, service hidden Quick access, collapsible service section, shop info at the end
Sidebar in categories The complete category tree from the root Only the open section – calculated from the active category
Buy button on mobile Not reachable – only appeared on mouse hover Always visible, without covering the price
Discount badges A bare “%” with no number The real value from the price data, e.g. −45 %
Price format “(45.01% gespart)” (“saved”) “(45 % günstiger)” (“cheaper”)
Typeface Named 73 times in the stylesheet – and never loaded Defined system font stack: available instantly, no request to third parties
Footer Four background surfaces stacked on top of each other One continuous surface, four ordered columns
Empty basket One line of text Heading, explanation and two routes back into the range
Dark mode Not available Complete, with measured contrasts
Rental prices Eight price tables to search through Three questions, one price – the calculator further down

The table has three columns – on a phone it can be swiped sideways.

Measured, not claimed

Design is open to debate, contrast is not. All visible text elements on the home, category and product pages were measured against the WCAG AA standard – before and after, with the same script.

2,276text elements measured on the home page alone
0contrast violations after the rebuild (before: 6)
1.04 → 18.2contrast ratio of the tabs on the product page
17page types with exactly one top-level heading
42contrast errors found and fixed in dark mode
0font requests to Google – the font comes from the device

Also a result: Four of the errors found were not design questions at all, but had already been in the shop before – the buy button that could not be reached on touch devices, white text on light green, the footer stacked four times, and a search magnifier without a size that blew up to 300 × 150 pixels. I found them because, when measuring, I was not looking for my own changes but for everything.

And one more: my first measuring script was itself wrong. It looked for the background in the parent tree and hit the wrong surface in the footer and on buttons – it reported 1.15:1, the actual value was 9.6:1. Measuring means checking the measurement too.

What of this I built

Design and programming are mine – from the first draft to the Twig templates running in the shop. What I did not touch: the shop's range, prices, product images and texts.

What I built, point by point

Design system

Colours, font sizes, radii, shadows and spacing as one coherent set of values – not as a collection of individual cases. Every colour has been calculated against its background.

Custom Shopware theme

Built as a standalone theme plugin, with Twig templates and SCSS. The most important values and texts can be set in the administration area, so that nobody has to touch code later.

Navigation

Drop-down menus for desktop, a separate menu for mobile and a section column derived from the active category. No fixed IDs and no name list – new categories appear on their own.

Home page

Welcome area, range tiles, trust bar, offers, workshop and rental, location. Built as CMS content so that the shop can change the order itself.

Product tile and buy area

New tile for the grid, new buy area on the product page, real discount values, readable price format, visible buy button.

Rental calculator

Eight price tables condensed into three questions. Plain JavaScript, no library, usable as a CMS block. Runs further down on this page.

Block collection

Six ready-made blocks – benefits, service, products, rental prices, questions and answers, location – that can be placed in any page without programming knowledge. They adapt to their own width, not that of the window.

Dark mode

A second, complete colour system instead of inverted brightness – including the places nobody otherwise thinks of in the dark: forms, tables, overlays, notice bars.

Images and icons

Icon set and illustrations drawn as vector graphics: sharp on every screen, a few kilobytes, in the theme's colour.

Clickable draft in advance

Before anything was changed in the shop, the complete draft existed as a single HTML file to double-click – no installation, no internet. That made it possible to see beforehand what the new shop would look like.

Product migration from the legacy system

Custom console commands that create products, variants, images, manufacturers and categories in Shopware from the database dump of the previous shop – together with the review pages that show beforehand what would come out of it.

Concept for rental reservations

Technical concept for online reservations with a deposit: period, availability, preparation time, status flow – plus a PHP skeleton with an interchangeable connection to the inventory management system, and tests.

Accessibility

Contrasts measured, heading order checked, unnamed buttons named, click targets spaced out, keyboard operation tested.

The rebuild in stages

Show first, then build, then switch on – and every step in a way that it can be reversed on its own. The running shop was never affected at any point; all work was done in a separate test environment.

The stages in detail
21 July 2026

Hands-on drafts

The complete shop as a clickable draft in one file: home page, navigation, category, product page, banner designs. To double-click, no installation – the basis for everything that followed.

  • Drop-down menu and mobile menu with the real category tree
  • Category page with filters for frame height, brand, price, gears
  • Product page with size selection, availability and leasing rate
27 July 2026

Block collection

Six embeddable blocks that the shop can place itself. Each responds to its own width – the same file looks different in the narrow column than across the full page.

29 July 2026

Home page, service pages, calculator

The new home page and eight service pages – workshop, bike leasing, e-bike, ski and snowboard rental, ski service, boot fitting, tennis stringing – with the real prices. Plus the rental calculator.

31 July 2026

First rebuild in the test environment

Colours, fonts and radii in the theme, plus a separate style area that can be switched off again with a tick. A backup of every affected area was taken beforehand.

  • Brand green darkened, because white on the light green only reached 3.66:1
  • Corners from 2 to 14 pixels – the most visible single step
  • Contrast check across all text elements, six violations fixed
31 July 2026

Standalone theme instead of a child theme

A child theme on top of the purchased standard theme does not inherit its settings – announcement bar, trust bar and footer columns would have disappeared along the way. So a standalone theme, in which navigation, home page, product tile and footer actually belong to me.

1 August 2026

Fine-tuning

The pass in which “looks good” becomes “works everywhere”: dark mode measured throughout, click targets spaced out, duplicate menu items removed, empty basket, section column, discount badges from real price data.

2 August 2026

Switched over

The rebuild runs on the real shop. Before the switchover the corners were toned down once more – the buttons in the header are now more angular than in the test system. Since that day, the comparison above shows on the left something that no longer exists on the web.

3 August 2026

The old stock, measured

A complete database dump of the previous shop is still on disk. I read and counted it instead of estimating: 7,410 products, 17,158 images, 88 manufacturers. What of this would really be new and what has long been in the shop is covered further down.

3 August 2026

Test run with 40 products

No import of 6,000 products without a trial. Forty were created, with 204 sizes and 172 images – in the real shop, but with zero stock so that they do not appear in any listing before someone releases them.

In progress

The big migration

The 40 become around 1,400. Three questions need answering first, otherwise the next sync clears everything away again – more on that below as well.

The rebuild itself is complete. The shop has been running with the new theme since 2 August 2026, and what came afterwards – migrating the product stock – is maintenance, not an open item of the build.

6,019 products, 1,400 products, or 138 after all?

Before the Shopware shop there was a Magento shop. It no longer runs, but its database dump is still on disk – together with 660,666 image files. It contains products that were never created in the new shop. The question sounds simple: how many are there? Answering it took more work than the import itself.

How 7,410 rows became 1,400 products
7,410products in the old dump
1,391of which already in the new shop
≈ 1,400products actually being created
3,815sizes attached to them
17,158images in the old stock
88manufacturers

Why the number shrinks three times

  • From 7,410 to 6,019: a character comparison is not enough. Both systems use the EAN as the product number, but write it differently: one with eleven digits, the other with thirteen digits and leading zeros. A blunt text comparison therefore missed 94 duplicates. Only with normalised numbers – digits only, leading zeros removed, padded to thirteen digits – does the matching work.
  • From 6,019 to 1,400: sizes are not products. The old shop listed every shoe size as a separate product; the new one lists them as variants. A direct import would have turned one pair of inline skates into six products where one with six sizes has long existed. The old stock already carries the structure itself, though – 3,815 of the records are already attached to a parent product.
  • From 775 to 138: names are suspects, not proof. 775 of the new products have a name that already exists in a similar form in the shop. Similar is not the same. Instead of deciding automatically, I built a review page that puts both sides next to each other for each row – on the left what the old stock would deliver, on the right the best match in the shop, with both photos. After the image comparison, what remained were 138 rows, which someone really has to look at.

What the test run showed

Forty instead of six thousand

40 products with 204 sizes, created in the real shop, stock zero everywhere. This makes them reachable via their address, but they do not appear in any category or search. As soon as the inventory management system supplies a quantity, they appear by themselves.

Images per colour, not per product

172 images, all 40 products illustrated, no errors. The trick: the old shop attaches the gallery to the parent product and to every size – and only via the sizes can one tell which photo belongs to which colour.

140 brands, no duplicate

Seven of the eight brands in the test run already existed; only one was new. The importer reused them instead of creating “Adidas” a second time – the kind of error that is only noticed months later.

902 categories against 224

One-to-one does not work. The scaffolding levels of the old shop (“Damen”, i.e. women's, and “Sale”) are dropped – that is navigation, not a product group. Whatever matches by name is assigned; the 37 labels without an equivalent end up on a list instead of silently disappearing.

No catch-all category

Deliberately not created. No product would be left without an assignment, and writing a category into the tree by hand – with path, level and counters – is exactly the kind of intervention that silently makes a category tree inconsistent.

Ask first, then import

The catalogue is completely overwritten from outside every day. If products are created only in the shop without the inventory management system knowing them, they disappear again at the next sync. This question comes before the import, not after it.

The error that taught me the most. The importer named the size groups after the names in the legacy system instead of checking whether they already existed. Result: “Größe” next to “Textilgröße”, “Schuhgröße EU” next to “Euro-Größe” – four new groups for values that were already attached to more than 1,600 products.

While merging them, the size selection in the shop was briefly lost. The reason: Shopware keeps a cached copy of the property IDs on every product, and after the intervention it still pointed to deleted values. Only a complete rebuild of the product index – 6 minutes 23 seconds for 16,432 products – put it right. The lesson: Creating via the data layer gets the index for free. Re-linking existing data directly means triggering it yourself.

Checking is the longer part of the work

For a shop that lives off its online store, a wrong number is more expensive than an ugly button. That is why almost every step has its own check behind it – usually as a small page that puts two data states side by side and lets me decide, instead of deciding itself.

All checks
CheckScopeResult
Duplicate check 775 suspected cases, both photos side by side per row Still to decide: 138 – the rest were sizes of the same product
Price comparison 989 products against the manufacturer list 153 discrepancies, of which 9 outliers with a conspicuous factor
Prices per size all variants individually Separate review page, because a product can cost different amounts per size
Categories 902 old against 224 new 37 without an equivalent – as a list to fill in later, not silently discarded
Colours from product names Names without their own colour field 11 suggestions with a confidence rating, each to be confirmed individually
Image comparison 4,000 image references, sampled 4,000 files present – hit rate 100%
Contrasts 2,276 text elements on the home page alone Six violations, all present before the rebuild, all fixed
Dark mode all surfaces and texts measured 42 errors, traced back to nine causes
Data protection Page loaded as a first-time visitor, connections recorded No outgoing connection before consent – as it should be

The table has three columns – on a phone it can be swiped sideways.

Twice the check itself was wrong – and both times that was the real finding.

My first contrast script looked for the background in the parent tree and hit the wrong surface in the footer: it reported 1.15:1, the actual value was 9.6:1.

And with e-mail delivery, I had named two errors in the DNS records as the reason why mail was not arriving. Wrong. Without a sender specified, my test command had inserted a reserved example domain whose mail every recipient discards. With the real sender, everything arrived. Both findings still stand and cost deliverability. The report rates them as important but not urgent.

The rental calculator, working here

Ski, snowboard and e-bike together have eight price tables with more than seventy rows. Anyone who just wants to know what a week of skiing costs for a child does not want to read them. So three questions instead of eight tables – the same block as in the shop, just without the checkout here.

1 Choose equipment

2 For whom?

3 Which equipment?

4 How long?

Price

–

Select equipment on the left.

View rental in the shop

Prices as of July 2026 · the prices in the shop are binding

Show all prices at a glance

No date fields. What people look for is the price, not a calendar day. The period only enters the price as a tier anyway – 4 to 7 or 8 to 14 days. Two tiles instead of two calendars save three inputs on average.

Under the bonnet

The shop runs on Shopware 6. The interface comes from Twig templates and SCSS, the content from the system's CMS. What is shown here is the structure – access credentials, server paths and customer data do not belong on a project page and are not here.

What is under the bonnet

Standalone theme

Not a child theme of the purchased theme, but a theme plugin of its own. Reason: a child theme does not inherit the roughly 1,400 settings fields of the purchased theme – 26 remain, and with the rest, half the page areas disappear.

Twig instead of copies

Only what should change is overridden; the rest still comes from the base theme. That keeps updates to the shop system installable instead of blocking them.

Navigation from the tree

A custom Twig extension builds the header, mobile and section navigation from the category structure. No hard-coded IDs, no maintained name list – new categories appear by themselves.

Content in the CMS

Home page and service pages are CMS content, not code. The shop can change images, order and texts itself, without anyone having to publish anything.

Configurable instead of set in concrete

Colours, announcement text, trust points and the texts of the welcome area are fields in the administration area. Changing them needs no program and no appointment.

Every step reversible

Before every intervention, a backup of the affected area. The additional style area can be switched off with a tick, and the theme values can be restored from the backup.

Separate test environment

All work was done in a separate environment alongside the running shop. Measured: the live shop kept its original values unchanged throughout the entire rebuild.

Container queries

The blocks adapt to their own width instead of that of the window. The same block therefore works in the narrow column just as well as across the full page.

Font from the device

A defined system font stack instead of a web font: available instantly, no request to a third party – and therefore no discussion about fonts from other people's data centres.

Pitfalls, properly noted

Two block names that no longer exist in this version and silently render nothing. An SCSS compiler that breaks on a shorthand notation. Both are documented in the project so that they do not cost anyone a second time.

For context

A short paragraph to make clear what belongs to whom here.

And next: reserving rentals online

A separate follow-up project with its own scope. Concept and skeleton are in place; the business decides on some of the open questions.

How the reservation is meant to work

What it should do

Reserve skis, ski boots and snowboards online for a period. The reservation becomes binding with a deposit, because the shop holds back equipment for it.

What already exists

The technical concept including data model and status flow, plus a working skeleton: availability via period overlap, preparation time after return, deposit calculation, tests.

The interchangeable part

Where the stock comes from is deliberately left open. Whether the inventory management system is behind it, a nightly export or, at first, a list – the rest of the logic notices nothing of it.

What needs clarifying first

Is the deposit refundable on cancellation, and from when not any more? How is it treated for tax purposes? Does the right of withdrawal apply to a rental with a fixed date? These are not programming questions – they need a tax adviser and a look from a lawyer.

Why it comes in steps. Even an enquiry form without payment would be a gain before the winter season and carries no technical risk. Taking everything live at once – shortly before Christmas, the shop's peak period – puts the first stress test right in the middle of that peak.

Contact

Questions about the construction, the decisions behind it or how it transfers to another case.

Arithmetic check Arithmetic task, shown as an image

No Google captcha, no cookies, nothing passed to third parties. Prefer e-mail? info@timobritz.de.