When a fan personalises an EV Zug jersey, the store has to sell four things: the shirt itself, the name on the back, the number and any badges. Behind the scenes each of them is a separate product with its own price, and fulfilment needs to see every one.
The fan sees none of that. They bought one jersey. A cart with four lines, three of which look like add-ons, reads like a mistake, and it gets worse in checkout, where every line repeats with its own image and price.
Merging lines without losing them
Shopify's answer is a Cart Transform Function: a small program that runs inside Shopify's cart and can merge several lines into one bundle line. I wrote it in TypeScript, compiled to WebAssembly, so it runs inside Shopify on every cart change.
What the function returns is short. For every personalised jersey it asks Shopify to merge the lines that belong together under one parent product:
{ "operations": [ { "merge": {
"cartLines": [ shirt, name, number, badge ],
"parentVariantId": "gid://shopify/ProductVariant/…",
"title": "Home jersey, personalised"
} } ] }
Simplified. The real function also has to decide which lines belong to which jersey when a fan buys two.
Two rules mattered more than the code. The total never changes: the bundle costs exactly what its parts cost. And every component stays attached to the order, so fulfilment still sees the name, the number and the badges as separate items to produce.
The parts around it
A bundle line is only as good as the jersey preview that sells it. On the product page I centre the number optically, measuring the rendered glyphs on a canvas and recalculating once the club's webfont has loaded, because a 1 and an 8 that are mathematically centred do not look centred.
The function has its own admin page in the store's companion app, and the same store also runs the live auction page and the German and English order emails. In total, 325 of the store's 907 commits are mine.
The result is the kind of feature nobody notices, which is the point: a fan personalises a shirt, sees one jersey in the cart and pays for one jersey.