[{"data":1,"prerenderedAt":1013},["ShallowReactive",2],{"blog-post-/blogs/shopware-configurator-share-links":3,"blog-list":715},{"id":4,"title":5,"body":6,"description":696,"extension":697,"meta":698,"navigation":385,"ogImage":710,"path":711,"seo":712,"stem":713,"__hash__":714},"content/blogs/25. shopware-configurator-share-links.md","Shareable Product Configurator in Shopware 6 — State in the URL",{"type":7,"value":8,"toc":687},"minimark",[9,17,22,25,36,43,190,209,216,220,226,243,254,257,260,264,267,270,308,315,319,322,329,343,565,568,598,604,608,640,644,651,655,675,683],[10,11,12,13],"p",{},"Recently I built a product configurator for a sports-equipment retailer's Shopware 6 storefront: a customer builds a custom ice-hockey stick step by step — colour, length, flex, shaft shape, blade pattern, finish, player name, team logo — and ends up on a summary page they can add to the cart, download as PDF, or send to someone. The concept lived in a Confluence page; the code is a Nuxt storefront in front of Shopware 6. This post is about the decisions that were not in the concept and mattered most: ",[14,15,16],"strong",{},"where the configuration state lives, what you put in it, and what happens when people want to edit what someone else shared.",[18,19,21],"h2",{"id":20},"state-in-the-url-not-in-the-session","State in the URL, not in the session",[10,23,24],{},"The share feature forced the main architectural choice early. A configuration has to survive three things: a page reload, being sent to another person, and ending up in a PDF that links back to it. A server-side session handles the first and fails the other two — the recipient has no session. So the state lives in the URL:",[26,27,32],"pre",{"className":28,"code":30,"language":31},[29],"language-text","/configurator/\u003Cproduct-number>?cfg=\u003Cbase64url(JSON)>\n","text",[33,34,30],"code",{"__ignoreMap":35},"",[10,37,38,39,42],{},"The payload is a flat JSON object whose keys all start with ",[33,40,41],{},"cfg",", base64-encoded. Decoded, one of them looks like this:",[26,44,48],{"className":45,"code":46,"language":47,"meta":35,"style":35},"language-json shiki shiki-themes github-dark","{\n  \"cfgStickColor\": \"Reflecting Gold\",\n  \"cfgStickLength\": \"58\\\"\",\n  \"cfgFlexRating\": \"95\",\n  \"cfgShaftShape\": \"Square\",\n  \"cfgHandedness\": \"Right\",\n  \"cfgBladePattern\": \"#77\",\n  \"cfgShaftFinish\": \"Extra Grip\",\n  \"cfgPlayerName\": \"Jane Doe\",\n  \"cfgTeamLogo\": \"\u003Cmedia-id>\"\n}\n","json",[33,49,50,59,76,95,108,121,134,147,160,173,184],{"__ignoreMap":35},[51,52,55],"span",{"class":53,"line":54},"line",1,[51,56,58],{"class":57},"s95oV","{\n",[51,60,62,66,69,73],{"class":53,"line":61},2,[51,63,65],{"class":64},"sDLfK","  \"cfgStickColor\"",[51,67,68],{"class":57},": ",[51,70,72],{"class":71},"sU2Wk","\"Reflecting Gold\"",[51,74,75],{"class":57},",\n",[51,77,79,82,84,87,90,93],{"class":53,"line":78},3,[51,80,81],{"class":64},"  \"cfgStickLength\"",[51,83,68],{"class":57},[51,85,86],{"class":71},"\"58",[51,88,89],{"class":64},"\\\"",[51,91,92],{"class":71},"\"",[51,94,75],{"class":57},[51,96,98,101,103,106],{"class":53,"line":97},4,[51,99,100],{"class":64},"  \"cfgFlexRating\"",[51,102,68],{"class":57},[51,104,105],{"class":71},"\"95\"",[51,107,75],{"class":57},[51,109,111,114,116,119],{"class":53,"line":110},5,[51,112,113],{"class":64},"  \"cfgShaftShape\"",[51,115,68],{"class":57},[51,117,118],{"class":71},"\"Square\"",[51,120,75],{"class":57},[51,122,124,127,129,132],{"class":53,"line":123},6,[51,125,126],{"class":64},"  \"cfgHandedness\"",[51,128,68],{"class":57},[51,130,131],{"class":71},"\"Right\"",[51,133,75],{"class":57},[51,135,137,140,142,145],{"class":53,"line":136},7,[51,138,139],{"class":64},"  \"cfgBladePattern\"",[51,141,68],{"class":57},[51,143,144],{"class":71},"\"#77\"",[51,146,75],{"class":57},[51,148,150,153,155,158],{"class":53,"line":149},8,[51,151,152],{"class":64},"  \"cfgShaftFinish\"",[51,154,68],{"class":57},[51,156,157],{"class":71},"\"Extra Grip\"",[51,159,75],{"class":57},[51,161,163,166,168,171],{"class":53,"line":162},9,[51,164,165],{"class":64},"  \"cfgPlayerName\"",[51,167,68],{"class":57},[51,169,170],{"class":71},"\"Jane Doe\"",[51,172,75],{"class":57},[51,174,176,179,181],{"class":53,"line":175},10,[51,177,178],{"class":64},"  \"cfgTeamLogo\"",[51,180,68],{"class":57},[51,182,183],{"class":71},"\"\u003Cmedia-id>\"\n",[51,185,187],{"class":53,"line":186},11,[51,188,189],{"class":57},"}\n",[10,191,192,193,196,197,201,202,204,205,208],{},"Why a flat ",[33,194,195],{},"cfg*"," namespace? Because the same keys travel further: they end up as line-item payload in the cart. One naming convention means one place that knows what is configuration and what is not. A review comment later made that explicit — line items ",[198,199,200],"em",{},"without"," a configuration must lose every root-level key starting with ",[33,203,41],{},", not just the known ones, otherwise a crafted payload can smuggle configuration-shaped data into an order. The test now includes a deliberately fake ",[33,206,207],{},"cfgFake"," key for exactly that.",[10,210,211,212,215],{},"Trade-off: the URL is the source of truth for sharing, so it is also untrusted input. Everything decoded from ",[33,213,214],{},"?cfg="," is validated like a request body, not trusted like state.",[18,217,219],{"id":218},"what-to-serialize-and-what-to-reference","What to serialize — and what to reference",[10,221,222,223],{},"The rule I settled on: ",[14,224,225],{},"serialize choices, reference assets.",[227,228,229,233,240],"ul",{},[230,231,232],"li",{},"Choices (colour, length, flex, name) are small strings. They go into the URL as-is.",[230,234,235,236,239],{},"The team logo is an uploaded image. Putting it into the URL as Base64 would blow past any sane URL length and make links unshareable in chat tools. The link carries only the ",[14,237,238],{},"media id",".",[230,241,242],{},"The upload endpoint accepts PNG/JPG/WebP and re-saves every file as PNG with GD. SVG is deliberately rejected: the server-side re-encode can't read it, and user-supplied SVG on your media domain can carry scripts. Supporting it would need a separate converter — a separate ticket, not a quiet extension.",[10,244,245,246,249,250,253],{},"Referencing assets creates a new failure mode: the shared link outlives the asset. If the logo is gone when someone opens the link, the configuration is restored ",[198,247,248],{},"incomplete",". We handle that explicitly — the shared logo is fetched through a dedicated endpoint (",[33,251,252],{},"GET /store-api/fis/configurator/logo/{mediaId}",") that applies the same folder rule as the cart and PDF. A 404 removes the logo and shows a \"logo no longer available\" note; network or server errors keep it, because those are not the logo's fault.",[10,255,256],{},"That dedicated endpoint exists because the first version looked the logo up through the generic media endpoint, which serves any public image. A \"shared logo\" lookup that can fetch arbitrary media is a bug, not a convenience.",[10,258,259],{},"The URL also needs a hard ceiling. The PDF endpoint receives the share URL, so both the Nuxt proxy and the Store API reject anything over 4096 bytes. Without it, a Store API route that embeds a user-supplied URL into a rendered document is an invitation.",[18,261,263],{"id":262},"the-pdf-export","The PDF export",[10,265,266],{},"The PDF is the summary as a document: configuration, product gallery, the team logo, and a link back to the share URL. It is rendered server-side in Shopware with Dompdf, in a Store API route.",[10,268,269],{},"Things that mattered in practice:",[227,271,272,278,288,302],{},[230,273,274,277],{},[14,275,276],{},"Images are inlined as data URIs."," Dompdf never fetches remote files; gallery and logo are read from media storage and embedded.",[230,279,280,283,284,287],{},[14,281,282],{},"Don't decode originals."," Product originals can be around 8000 px. Image dimensions are read ",[198,285,286],{},"before"," decoding, anything over 4096×4096 is skipped, and gallery images use the smallest thumbnail that covers 1200 px. The logo is shrunk to 600 px PNG with transparency kept.",[230,289,290,293,294,297,298,301],{},[14,291,292],{},"Cache with care."," Repeat downloads are common (the customer clicks again, the recipient opens the same link). Finished PDFs are cached in Redis for 10 minutes; the cache key covers everything that can change the output — sales-channel context (language, currency, tax, customer rules, so per-customer prices don't leak), the product and its last-updated timestamp, the validated configuration, the logo, and the share link. The browser gets ",[33,295,296],{},"Cache-Control: private, max-age=300",". ",[14,299,300],{},"No shared caching in the CDN",": the URL doesn't contain the customer context, so a shared cache could serve one customer's price to another.",[230,303,304,307],{},[14,305,306],{},"Rate limiting needs the real IP."," The Nuxt server was calling the Store API itself, so Shopware counted every PDF request against the Nuxt host's IP — effectively one shared bucket for the whole shop. The fix was to let the browser call the Store API directly for the download and drop the proxy route.",[10,309,310,311,314],{},"Two automated review comments on this code were wrong (Dompdf accepting the protocol list as written; a ",[33,312,313],{},"getPath()"," that can't return null in Shopware 6.7). I checked each against the source and replied instead of \"fixing\" it. Review bots are fast, not authoritative.",[18,316,318],{"id":317},"can-i-edit-a-shared-configuration","\"Can I edit a shared configuration?\"",[10,320,321],{},"During stage testing, the client came back with the obvious feature the concept didn't have: open someone's shared link, then change it. A share link is a read-only summary by default, and the summary offered no way back into the steps.",[10,323,324,325,328],{},"The first version was small: an ",[14,326,327],{},"Edit configuration"," link on the summary that returns to the steps with all values kept and jumps to the first open step. As a side effect it fixed a dead end — a shared link whose logo had vanished used to trap the user on the summary; now Edit opens the team-logo step directly.",[10,330,331,332,334,335,338,339,342],{},"My first PR documented a known limitation: while editing, ",[33,333,214],{}," is not rewritten, so a reload restores the ",[198,336,337],{},"shared"," state, while the outgoing share link and PDF always use the ",[198,340,341],{},"current"," selection. That's an inconsistent mental model — the address bar says one thing and the share button another — so I built the rewrite in a follow-up:",[26,344,348],{"className":345,"code":346,"language":347,"meta":35,"style":35},"language-ts shiki shiki-themes github-dark","let timer: ReturnType\u003Ctypeof setTimeout> | undefined\n\nwatch(\n  configuration,\n  (cfg) => {\n    clearTimeout(timer)\n    timer = setTimeout(() => {\n      const url = new URL(window.location.href)\n      if (!url.searchParams.has('cfg')) return\n      url.searchParams.set('cfg', encodeConfiguration(cfg))\n      window.history.replaceState(window.history.state, '', url)\n    }, 300)\n  },\n  { deep: true },\n)\n","ts",[33,349,350,381,387,395,400,417,425,443,463,492,513,530,542,548,560],{"__ignoreMap":35},[51,351,352,356,359,362,366,369,372,375,378],{"class":53,"line":54},[51,353,355],{"class":354},"snl16","let",[51,357,358],{"class":57}," timer",[51,360,361],{"class":354},":",[51,363,365],{"class":364},"svObZ"," ReturnType",[51,367,368],{"class":57},"\u003C",[51,370,371],{"class":354},"typeof",[51,373,374],{"class":57}," setTimeout> ",[51,376,377],{"class":354},"|",[51,379,380],{"class":64}," undefined\n",[51,382,383],{"class":53,"line":61},[51,384,386],{"emptyLinePlaceholder":385},true,"\n",[51,388,389,392],{"class":53,"line":78},[51,390,391],{"class":364},"watch",[51,393,394],{"class":57},"(\n",[51,396,397],{"class":53,"line":97},[51,398,399],{"class":57},"  configuration,\n",[51,401,402,405,408,411,414],{"class":53,"line":110},[51,403,404],{"class":57},"  (",[51,406,41],{"class":407},"s9osk",[51,409,410],{"class":57},") ",[51,412,413],{"class":354},"=>",[51,415,416],{"class":57}," {\n",[51,418,419,422],{"class":53,"line":123},[51,420,421],{"class":364},"    clearTimeout",[51,423,424],{"class":57},"(timer)\n",[51,426,427,430,433,436,439,441],{"class":53,"line":136},[51,428,429],{"class":57},"    timer ",[51,431,432],{"class":354},"=",[51,434,435],{"class":364}," setTimeout",[51,437,438],{"class":57},"(() ",[51,440,413],{"class":354},[51,442,416],{"class":57},[51,444,445,448,451,454,457,460],{"class":53,"line":149},[51,446,447],{"class":354},"      const",[51,449,450],{"class":64}," url",[51,452,453],{"class":354}," =",[51,455,456],{"class":354}," new",[51,458,459],{"class":364}," URL",[51,461,462],{"class":57},"(window.location.href)\n",[51,464,465,468,471,474,477,480,483,486,489],{"class":53,"line":162},[51,466,467],{"class":354},"      if",[51,469,470],{"class":57}," (",[51,472,473],{"class":354},"!",[51,475,476],{"class":57},"url.searchParams.",[51,478,479],{"class":364},"has",[51,481,482],{"class":57},"(",[51,484,485],{"class":71},"'cfg'",[51,487,488],{"class":57},")) ",[51,490,491],{"class":354},"return\n",[51,493,494,497,500,502,504,507,510],{"class":53,"line":175},[51,495,496],{"class":57},"      url.searchParams.",[51,498,499],{"class":364},"set",[51,501,482],{"class":57},[51,503,485],{"class":71},[51,505,506],{"class":57},", ",[51,508,509],{"class":364},"encodeConfiguration",[51,511,512],{"class":57},"(cfg))\n",[51,514,515,518,521,524,527],{"class":53,"line":186},[51,516,517],{"class":57},"      window.history.",[51,519,520],{"class":364},"replaceState",[51,522,523],{"class":57},"(window.history.state, ",[51,525,526],{"class":71},"''",[51,528,529],{"class":57},", url)\n",[51,531,533,536,539],{"class":53,"line":532},12,[51,534,535],{"class":57},"    }, ",[51,537,538],{"class":64},"300",[51,540,541],{"class":57},")\n",[51,543,545],{"class":53,"line":544},13,[51,546,547],{"class":57},"  },\n",[51,549,551,554,557],{"class":53,"line":550},14,[51,552,553],{"class":57},"  { deep: ",[51,555,556],{"class":64},"true",[51,558,559],{"class":57}," },\n",[51,561,563],{"class":53,"line":562},15,[51,564,541],{"class":57},[10,566,567],{},"Three decisions are in those lines:",[569,570,571,580,586],"ol",{},[230,572,573,579],{},[14,574,575,578],{},[33,576,577],{},"history.replaceState",", not the router."," Every router navigation fires a page view in analytics and the ad pixel. Syncing state to the URL must not inflate page views.",[230,581,582,585],{},[14,583,584],{},"300 ms debounce."," Typing a player name must not rewrite the URL per keystroke.",[230,587,588,593,594,597],{},[14,589,590,591,239],{},"Keep other params and only touch pages that already have ",[33,592,214],{}," A tracking parameter like ",[33,595,596],{},"utm_source"," stays, and a normal product page is never rewritten.",[10,599,600,601,603],{},"I verified it in a browser: after editing the name, the address bar followed within about half a second, ",[33,602,596],{}," survived, history length didn't change, and a reload came back with the edited stick. The debounce also leaked a timer between unit tests and made the suite flaky until I fixed the isolation — a reminder that URL sync is global state.",[18,605,607],{"id":606},"smaller-things-that-bit","Smaller things that bit",[227,609,610,628,634],{},[230,611,612,615,616,619,620,623,624,627],{},[14,613,614],{},"Edit showed no options."," After \"Edit\" on desktop, the step content is teleported into the right-hand column, which is created in the ",[198,617,618],{},"same render"," — so the teleport target didn't exist yet and Vue logged \"Failed to locate Teleport target\". Adding ",[33,621,622],{},"defer"," to the ",[33,625,626],{},"\u003CTeleport>"," fixed it. I wrote a test that fails without it.",[230,629,630,633],{},[14,631,632],{},"Tracking."," Events fire on first choice, on finish, and only on a successful add-to-cart of a configured item. They deliberately don't fire on page load, so someone landing on a shared link and going straight to the summary produces no start event.",[230,635,636,639],{},[14,637,638],{},"Product data decides the entry point."," The \"customize this\" teaser on a product page keys off a PIM-imported custom field that holds the product number of the configurable variant. I checked on every environment that it exists and points to a real product — the field arrives with a PIM import prefix, not under the short name from the concept. A related custom field I was asked to verify turned out to carry per-language translated values, so using it to decide player vs. goalie logic would have broken in every non-German locale. Check the data before you write the condition.",[18,641,643],{"id":642},"building-the-landing-page-from-existing-cms-blocks","Building the landing page from existing CMS blocks",[10,645,646,647,650],{},"The entry page was assembled in the Shopware CMS from existing elements and blocks, first locally via the Admin API, then turned into a migration so it can be reproduced on every environment. Two versions taught me the limit of that approach: v1 used product tiles (image and name from product data, but no \"start customizing\" button); v2 used text elements with hand-built cards (the right layout, but images are plain ",[33,648,649],{},"\u003Cimg>"," tags — no responsive sizes, and name and image no longer come from the product). Pick which one you can maintain. If you need a whole-card link, you'll end up making a small frontend change after all.",[18,652,654],{"id":653},"what-id-tell-you-before-you-build-one","What I'd tell you before you build one",[227,656,657,660,666,669,672],{},[230,658,659],{},"Put the state in the URL early; adding it later means rewriting how every step reads its value.",[230,661,662,663,665],{},"Treat ",[33,664,214],{}," as hostile input and cap its size.",[230,667,668],{},"Serialize choices, reference assets, and design what the link does when an asset disappears.",[230,670,671],{},"Edit-after-share is the first thing real users ask for. Decide up front whether the URL follows edits.",[230,673,674],{},"Keep the PDF cache private and key it on everything that can change the output.",[10,676,677,678,239],{},"If you are planning a configurator on Shopware 6 — or have one that grew organically and hurts — ",[679,680,682],"a",{"href":681},"/contact","Get in touch",[684,685,686],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":35,"searchDepth":61,"depth":61,"links":688},[689,690,691,692,693,694,695],{"id":20,"depth":61,"text":21},{"id":218,"depth":61,"text":219},{"id":262,"depth":61,"text":263},{"id":317,"depth":61,"text":318},{"id":606,"depth":61,"text":607},{"id":642,"depth":61,"text":643},{"id":653,"depth":61,"text":654},"How I built a product configurator for a Shopware 6 storefront where the whole state lives in a ?cfg= URL parameter — what to serialize, how the PDF export works, what breaks, and the trade-offs of letting users edit a shared configuration.","md",{"seoTitle":699,"date":700,"image":701,"imageWidth":702,"imageHeight":703,"alt":704,"tags":705,"author":708,"published":385,"featured":709},"Shopware 6 Configurator: Share Links & PDF Export","02.10.2026","/blogs-img/shopware-configurator.webp",1536,1024,"Product configurator card whose state is encoded into a share link that fans out to a phone, a PDF and an email, with an orange loop marking the edit-a-shared-state problem",[706,707],"shopware","frontend","Marco Faul",false,"/blogs-img/shopware-configurator.png","/blogs/shopware-configurator-share-links",{"title":5,"description":696},"blogs/25. shopware-configurator-share-links","xorX0VZePIXgP5r1MkRZu7UgMJ9HKlf8mkuverNNl-Y",[716,726,736,746,756,768,779,788,798,808,818,827,837,846,855,864,873,883,886,896,906,915,926,935,945,956,965,974,983,993,1003],{"path":717,"title":718,"description":719,"meta":720},"/blogs/shopware-6-performance-guide","Shopware 6 Developer Performance Guide - Essential Improvements and Fixes","Performance optimization is crucial for any e-commerce platform. Here are the most impactful performance improvements and fixes for Shopware 6 developers.",{"seoTitle":721,"date":722,"image":723,"imageWidth":702,"imageHeight":703,"alt":718,"tags":724,"published":385,"featured":709},"Shopware 6 Performance Guide","24.10.2025","/blogs-img/shopware-performance.webp",[706,725],"performance",{"path":727,"title":728,"description":729,"meta":730},"/blogs/shopware-frontends-composable-storefront","Shopware Frontends - Building a Composable Storefront with Vue and Nuxt","Shopware Frontends is the official way to build headless storefronts on Shopware 6. Learn the Vue Starter Template, Nuxt layers, composables, and the Store API architecture.",{"seoTitle":731,"date":732,"image":733,"imageWidth":702,"imageHeight":703,"alt":734,"tags":735,"author":708,"published":385,"featured":709},"Shopware Frontends - Composable Storefronts","06.06.2026","/blogs-img/shopware-frontends.webp","Shopware Frontends composable storefront with Vue and Nuxt",[706,707],{"path":737,"title":738,"description":739,"meta":740},"/blogs/headless-shopware-cost","What Does a Headless Shopware Store Really Cost? A Realistic 2026 Breakdown","Thinking about going headless with Shopware? An honest breakdown of what a headless storefront actually costs - build, hosting, maintenance - and when it pays off.",{"seoTitle":741,"date":742,"image":743,"imageWidth":702,"imageHeight":703,"alt":744,"tags":745,"author":708,"published":385,"featured":709},"What a Headless Shopware Store Really Costs","02.06.2026","/blogs-img/headless-cost.webp","What a headless Shopware store really costs in 2026",[706,707],{"path":747,"title":748,"description":749,"meta":750},"/blogs/headless-vs-traditional-shopware-2026","Headless vs. Traditional Shopware - How to Choose in 2026","Headless or the classic Twig storefront? A practical 2026 decision framework for Shopware 6 - performance, cost, flexibility, SEO, and team fit - with a clear recommendation.",{"seoTitle":751,"date":752,"image":753,"imageWidth":702,"imageHeight":703,"alt":754,"tags":755,"author":708,"published":385,"featured":709},"Headless vs. Traditional Shopware in 2026","30.05.2026","/blogs-img/headless-vs-traditional.webp","Headless vs traditional Shopware 6 decision framework 2026",[706,707],{"path":757,"title":758,"description":759,"meta":760},"/blogs/supply-chain-security-2026","The Supply Chain Is the New Attack Surface - npm Worms and Slopsquatting in 2026","A self-replicating npm worm and AI-hallucinated packages are the supply-chain threats hitting JavaScript developers right now. Here's how they work and how I lock my projects down.",{"seoTitle":761,"date":762,"image":763,"imageWidth":702,"imageHeight":703,"alt":764,"tags":765,"author":708,"published":385,"featured":385},"npm Worms and Slopsquatting - Supply Chain 2026","11.06.2026","/blogs-img/supply-chain-security.webp","npm supply chain worms and slopsquatting - 2026 developer security threats",[766,767],"security","devops",{"path":769,"title":770,"description":771,"meta":772},"/blogs/harness-vs-model","The Real Variable in AI Coding Isn't the Model — It's the Harness","Benchmarks show that switching edit formats improved Grok Code Fast by 10x — zero training compute required. As a developer who relies on AI agents daily, the harness deserves far more scrutiny than we give it.",{"seoTitle":773,"date":774,"image":775,"imageWidth":702,"imageHeight":703,"alt":776,"tags":777,"author":708,"published":385,"featured":385},"AI Coding - The Harness Beats the Model","12.06.2026","/blogs-img/harness-vs-model.webp","AI model connected to a codebase through a harness bridge",[778,767],"ai",{"path":780,"title":781,"description":782,"meta":783},"/blogs/oh-my-pi","Oh My Pi — The Open-Source Coding Agent I Actually Use Every Day","Oh My Pi is a fork of Mario Zechner's Pi coding agent with ~1,300 commits of incremental improvements. Here's what makes it different, why harness quality shapes agent reliability more than model choice, and why I run it instead of Claude Code.",{"seoTitle":784,"date":774,"image":785,"imageWidth":702,"imageHeight":703,"alt":786,"tags":787,"author":708,"published":385,"featured":709},"Oh My Pi - My Daily Open-Source Coding Agent","/blogs-img/oh-my-pi.webp","Oh My Pi open-source coding agent harness with flowing code streams",[778,767],{"path":789,"title":790,"description":791,"meta":792},"/blogs/1password-claude-agentic-security","1Password for Claude - AI Agents That Log In Without Ever Seeing Your Password","1Password and Anthropic shipped a zero-exposure integration that lets Claude log into websites and finish real tasks without the model ever touching a credential. Here's how it works, what it's good for, and where the limits are.",{"seoTitle":793,"date":794,"image":795,"imageWidth":702,"imageHeight":703,"alt":796,"tags":797,"author":708,"published":385,"featured":709},"1Password for Claude - Logins Without Secrets","20.07.2026","/blogs-img/1password-claude.webp","1Password and Anthropic zero-exposure credential access for Claude AI agents",[766,778],{"path":799,"title":800,"description":801,"meta":802},"/blogs/nuxt-4-5-road-to-nuxt-5","Nuxt 4.5 - Vite 8, Rspack 2, SSR Streaming, and the Road to Nuxt 5","Nuxt 4.5 is the biggest release in a while - Vite 8, an Rsbuild-powered Rspack 2 builder, experimental SSR streaming, stable error codes, and a pile of groundwork for Nuxt 5. Here's what actually matters, what to test before upgrading, and why the Nuxt 3 clock is about to run out.",{"seoTitle":803,"date":804,"image":805,"imageWidth":702,"imageHeight":703,"alt":806,"tags":807,"author":708,"published":385,"featured":709},"Nuxt 4.5 - Vite 8 and the Road to Nuxt 5","18.07.2026","/blogs-img/nuxt-4-5.webp","Nuxt 4.5 release - Vite 8, Rspack 2, SSR streaming and the road to Nuxt 5",[707,767],{"path":809,"title":810,"description":811,"meta":812},"/blogs/netlify-cloudflare-cache-purge","Purging Cloudflare's Cache Automatically on Every Netlify Deploy","This site sits on Netlify behind Cloudflare, and after every deploy Cloudflare kept serving stale HTML. Here's why the Netlify-CDN-Cache-Control header was a dead end for static files, and the 26-line build plugin that purges Cloudflare's edge cache the moment a deploy goes live — including the CJS/ESM gotcha that broke the first build.",{"seoTitle":813,"date":814,"image":815,"imageWidth":702,"imageHeight":703,"alt":816,"tags":817,"author":708,"published":385,"featured":709},"Auto-Purge Cloudflare Cache on Netlify Deploys","22.07.2026","/blogs-img/netlify-cloudflare-purge.webp","Netlify deploy triggering an automatic Cloudflare edge cache purge",[725,767],{"path":819,"title":820,"description":821,"meta":822},"/blogs/lcp-debugging-case-study","Debugging LCP on a Real Site - A Case Study on marcofaul.de","A first-person walkthrough of fixing LCP on this very site - a JS reveal animation gating first paint, a font preload that held rendering hostage, a 5.8s image CDN cold start, and the measurement noise that almost sent me chasing ghosts.",{"seoTitle":823,"date":814,"image":824,"imageWidth":702,"imageHeight":703,"alt":825,"tags":826,"author":708,"published":385,"featured":709},"Debugging LCP - A Real-World Case Study","/blogs-img/lcp-debugging.webp","Debugging Largest Contentful Paint - a real-world case study on marcofaul.de",[725,707],{"path":828,"title":829,"description":830,"meta":831},"/blogs/clean-code-summary-robert-martin","Summary of Clean Code by Robert C. Martin - Essential Guidelines for Better Software","Code is clean if it can be understood easily by everyone on the team. A comprehensive summary of Robert C. Martin's Clean Code principles and best practices.",{"seoTitle":832,"date":833,"image":834,"imageWidth":702,"imageHeight":703,"alt":829,"tags":835,"published":385,"featured":709},"Clean Code by Robert C. Martin - Summary","25.10.2025","/blogs-img/clean-code.webp",[836],"best-practices",{"path":838,"title":839,"description":840,"meta":841},"/blogs/pg-boss-10-to-12-upgrade","Upgrading pg-boss 10 → 12 When There Is No Migration Path","pg-boss 12 cannot migrate a v10 schema - v11 moved job storage to partitioned tables and the migration chain starts after that cliff. Here's how I upgraded a production job queue anyway - drop the schema, rebuild queue state from my own tables, and the job-ownership guard the rollout taught me to add.",{"seoTitle":842,"date":814,"image":843,"imageWidth":702,"imageHeight":703,"alt":844,"tags":845,"author":708,"published":385,"featured":709},"pg-boss 10 to 12 Upgrade Without a Migration Path","/blogs-img/pg-boss-upgrade.webp","Postgres-backed job queue being rebuilt during a pg-boss major version upgrade",[836,767],{"path":847,"title":848,"description":849,"meta":850},"/blogs/building-an-seo-crawler","I Built My Own SEO Crawler - Architecture Notes from seo-pulse","Instead of paying for Screaming Frog or Ahrefs, I built my own SEO audit tool - a Bun monorepo with a Nuxt 4 UI, a pg-boss worker that crawls and scores sites, and an MCP server so AI agents can trigger crawls and read findings. Here's the real architecture, and the parts that hurt - deploys killing workers mid-crawl, queue schema upgrades, and Docker builds dying without an error.",{"seoTitle":851,"date":814,"image":852,"imageWidth":702,"imageHeight":703,"alt":853,"tags":854,"author":708,"published":385,"featured":709},"Building an SEO Crawler - Architecture Notes","/blogs-img/seo-pulse-crawler.webp","Architecture diagram of a self-hosted SEO crawler with web app, Postgres queue, and worker",[778,767],{"path":856,"title":857,"description":858,"meta":859},"/blogs/claude-code-freelance-workflows","Claude Code in Real Client Work — Workflows, MCP, and Where It Breaks","A year of running Claude Code on actual freelance and agency projects — the workflows that hold up, why MCP is the real game changer for client work, the failure modes nobody puts in demos, and what I promise clients about AI-assisted development.",{"seoTitle":860,"date":814,"image":861,"imageWidth":702,"imageHeight":703,"alt":862,"tags":863,"author":708,"published":385,"featured":709},"Claude Code in Freelance Work - Workflows & Limits","/blogs-img/claude-code-workflows.webp","AI coding agent connected to real project tooling in a client development workflow",[778,767],{"path":865,"title":866,"description":867,"meta":868},"/blogs/shopware-mcp-ai-agents","Shopware 6 Meets AI Agents - What an MCP Server for Your Shop Could Do","MCP gives AI agents a standard way to talk to real systems. Here's what an MCP server over Shopware's Admin API could look like - the tool surface, the architecture, and the honest risks of letting an agent near a live shop.",{"seoTitle":869,"date":814,"image":870,"imageWidth":702,"imageHeight":703,"alt":871,"tags":872,"author":708,"published":385,"featured":385},"Shopware 6 + AI Agents - An MCP Server for Your Shop","/blogs-img/shopware-mcp.webp","AI agent connected to a Shopware 6 shop through an MCP server",[706,778],{"path":874,"title":875,"description":876,"meta":877},"/blogs/jtl-to-shopify-migration","Migrating a JTL-Shop to Shopify: What to Import and What to Skip","There is no one-click connector from JTL-Shop to Shopify. Here is how I planned a real migration - what I imported, what I deliberately did not, how scripts verify the result, how to stop notification mails during an order import, and the launch checklist.",{"seoTitle":878,"date":879,"image":880,"imageWidth":702,"imageHeight":703,"alt":881,"tags":882,"author":708,"published":385,"featured":709},"JTL-Shop to Shopify Migration: Plan and Verify","06.10.2026","/blogs-img/jtl-shopify-migration.webp","Legacy shop with an encrypted database feeding a verified import pipeline into a Shopify storefront",[778,767],{"path":711,"title":5,"description":696,"meta":884},{"seoTitle":699,"date":700,"image":701,"imageWidth":702,"imageHeight":703,"alt":704,"tags":885,"author":708,"published":385,"featured":709},[706,707],{"path":887,"title":888,"description":889,"meta":890},"/blogs/shopify-personalized-products-and-packaging-fees","Personalized Products and Packaging Fees in Shopify","How I modeled printed cups with tiered prices and a logo upload in Shopify, and the options I weighed for passing a per-item packaging licence fee through to customers — with the pitfalls I hit.",{"seoTitle":891,"date":892,"image":893,"imageWidth":702,"imageHeight":703,"alt":894,"tags":895,"author":708,"published":385,"featured":709},"Shopify Personalized Products & Packaging Fees","04.10.2026","/blogs-img/shopify-personalization.webp","Product configurator with size and quantity tiles and a logo upload, next to a cart that separates a regulatory packaging fee line",[836,767],{"path":897,"title":898,"description":899,"meta":900},"/blogs/proving-where-data-stops-integration-debugging","Proving Where Data Stops — How I Debug Multi-System Integrations","A shop field that never updates, a CDN that can't find its images, a sync that \"randomly\" skips entries. A method for multi-system integrations — instrument every hop, read the boundary evidence, hand over proof instead of opinions — with five real cases from the last weeks.",{"seoTitle":901,"date":902,"image":903,"imageWidth":702,"imageHeight":703,"alt":904,"tags":905,"author":708,"published":385,"featured":709},"Integration Debugging - Prove Where Data Stops","05.10.2026","/blogs-img/integration-debugging.webp","Data pipeline across four connected systems with a check at every hop and one orange marker where the data flow breaks",[836,767],{"path":907,"title":908,"description":909,"meta":910},"/blogs/scheduled-ai-agents-lessons","Scheduled AI Agents — What Weeks of Unattended Runs Teach You","Notes from running scheduled, unattended AI agents for a few weeks — a daily upstream-sync job, a research agent that publishes a page, and review-bot tuning. Guards, state files, failing publish steps, and where it breaks.",{"seoTitle":911,"date":879,"image":912,"imageWidth":702,"imageHeight":703,"alt":913,"tags":914,"author":708,"published":385,"featured":709},"Scheduled AI Agents: Lessons From Unattended Runs","/blogs-img/scheduled-agents.webp","Scheduled agent runs flowing through a guard into a published page, with one failing publish step highlighted",[778,767],{"path":916,"title":917,"description":918,"meta":919},"/blogs/bfsg-accessibility-online-shops","BFSG for Online Shops — What an Accessibility Audit Actually Finds","The BFSG has applied to B2C online shops since 28 June 2025. What an accessibility audit on a real headless Shopware storefront turned up, which fixes a developer can ship right away, which ones need design sign-off, and a checklist for your own shop.",{"seoTitle":920,"date":921,"image":922,"imageWidth":702,"imageHeight":703,"alt":923,"tags":924,"author":708,"published":385,"featured":709},"BFSG for Online Shops: Real Audit Findings","14.10.2026","/blogs-img/bfsg-accessibility.webp","Online shop wireframe with keyboard focus moving through header buttons, navigation and a product card, each checked against an accessibility list",[925,707],"accessibility",{"path":927,"title":928,"description":929,"meta":930},"/blogs/shopware-decoration-best-practices","Shopware Decoration Best Practices: Override One Method, Keep Others Untouched","Learn the golden rule of Shopware decorations - how to efficiently override only what you need while letting PHP inheritance handle the rest automatically.",{"seoTitle":931,"date":932,"image":933,"imageWidth":702,"imageHeight":703,"alt":928,"tags":934,"author":708,"published":385,"featured":385},"Shopware 6 Decoration Best Practices","26.10.2025","/blogs-img/shopware-decoration.webp",[706,836],{"path":936,"title":937,"description":938,"meta":939},"/blogs/storefront-requests-531-to-101","531 Requests on a Homepage — Cutting a Headless Storefront Down to 101","A headless Shopware 6 + Nuxt storefront loaded 531 requests on its mobile homepage and scored 56 in Lighthouse. After one batch of changes on production it was 101 requests and 88. How I measured, where the weight came from, what I reverted, and a checklist for your own storefront.",{"seoTitle":940,"date":941,"image":942,"imageWidth":702,"imageHeight":703,"alt":943,"tags":944,"author":708,"published":385,"featured":709},"Headless Storefront Performance: 531 to 101 Requests","29.10.2026","/blogs-img/storefront-requests.webp","A storefront waterfall chart shrinking from a dense wall of requests to a short, tidy list, with a performance gauge moving from orange to green",[725,706],{"path":946,"title":947,"description":948,"meta":949},"/blogs/ai-agent-readiness-website","Making Your Website Readable for AI Agents — What Actually Matters","llms.txt, agents.md, ai-catalog.json, AI-bot rules in robots.txt. I shipped all of it on marcofaul.de, then audited it and found four of my own files broken or missing. What the 2026 data says about these files, what really decides whether AI tools can read your site, and a checklist to verify it.",{"seoTitle":950,"date":951,"image":952,"imageWidth":702,"imageHeight":703,"alt":953,"tags":954,"author":708,"published":385,"featured":709},"AI Agent Readiness: llms.txt, AGENTS.md and What Matters","31.10.2026","/blogs-img/agent-readiness.webp","A website feeding server-rendered HTML, structured data and small machine-readable files to AI crawlers, with one broken file highlighted",[778,955],"seo",{"path":957,"title":958,"description":959,"meta":960},"/blogs/headless-gamechanger","Why Headless Shopware 6 with Nuxt is a Game-Changer for Your Online Store","Going headless with Shopware 6 and Nuxt isn't just a tech trend—it's a competitive advantage. Discover why separating your frontend from backend delivers blazing-fast performance, incredible flexibility, and happier customers.",{"seoTitle":961,"date":932,"image":962,"imageWidth":702,"imageHeight":703,"alt":963,"tags":964,"published":385,"featured":385},"Headless Shopware 6 with Nuxt - Why It Wins","/blogs-img/shopware-nuxt-headless.webp","Headless Shopware 6 with Nuxt - The future of e-commerce",[706,707],{"path":966,"title":967,"description":968,"meta":969},"/blogs/quality-tools-that-matter","Code Quality Tools That Actually Matter - Your PR Review Checklist","Stop shipping bugs and messy code. These essential code quality tools catch issues before they hit production, making your team faster and your code cleaner.",{"seoTitle":970,"date":932,"image":971,"imageWidth":702,"imageHeight":703,"alt":972,"tags":973,"published":385},"Code Quality Tools That Actually Matter","/blogs-img/code-quality-tools.webp","Essential code quality tools for modern development",[836,767],{"path":975,"title":976,"description":977,"meta":978},"/blogs/shopware-67-migration-guide","Shopware 6.6 to 6.7 Migration Guide - Breaking Changes and a Safe Upgrade Path","Upgrading Shopware 6.6 to 6.7? Here are the real breaking changes - Vite, Symfony 7.4, Pinia, delayed cache - and a tested upgrade path that won't break production.",{"seoTitle":979,"date":762,"image":980,"imageWidth":702,"imageHeight":703,"alt":981,"tags":982,"author":708,"published":385,"featured":385},"Shopware 6.6 to 6.7 Migration Guide","/blogs-img/shopware-67-migration.webp","Shopware 6.6 to 6.7 migration guide - breaking changes and upgrade path",[706,767],{"path":984,"title":985,"description":986,"meta":987},"/blogs/nuxt-4-migration-guide","Migrating to Nuxt 4 - The New app/ Directory, Smarter Data Fetching, and a Painless Upgrade","Nuxt 4 is here. Learn the new app/ directory structure, improved useFetch/useAsyncData, the path-alias gotcha, and how to upgrade safely with codemods.",{"seoTitle":988,"date":989,"image":990,"imageWidth":702,"imageHeight":703,"alt":991,"tags":992,"author":708,"published":385,"featured":385},"Nuxt 4 Migration Guide","10.06.2026","/blogs-img/nuxt-4-migration.webp","Migrating to Nuxt 4 - new app directory and data fetching",[707,767],{"path":994,"title":995,"description":996,"meta":997},"/blogs/core-web-vitals-inp-2026","Core Web Vitals 2026 - How to Master INP (Interaction to Next Paint)","INP is the Core Web Vital most sites fail. Learn what INP measures, the 200ms threshold, and concrete JavaScript techniques to make your store feel instant.",{"seoTitle":998,"date":999,"image":1000,"imageWidth":702,"imageHeight":703,"alt":1001,"tags":1002,"author":708,"published":385,"featured":709},"Core Web Vitals 2026 - Mastering INP","04.06.2026","/blogs-img/core-web-vitals-inp.webp","Master INP Interaction to Next Paint Core Web Vitals 2026",[725,707],{"path":1004,"title":1005,"description":1006,"meta":1007},"/blogs/ai-product-search-shopware","AI-Powered Product Search in Shopware - Semantic and Vector Search That Converts","Keyword search misses what shoppers mean. Here's how to add AI semantic/vector search to Shopware 6 with embeddings - the architecture, the tradeoffs, and a practical build.",{"seoTitle":1008,"date":1009,"image":1010,"imageWidth":702,"imageHeight":703,"alt":1011,"tags":1012,"author":708,"published":385,"featured":385},"AI Product Search in Shopware","09.06.2026","/blogs-img/ai-product-search.webp","AI-powered semantic and vector product search in Shopware 6",[706,778],1793447960848]