[{"data":1,"prerenderedAt":963},["ShallowReactive",2],{"blog-post-/blogs/jtl-to-shopify-migration":3,"blog-list":666},{"id":4,"title":5,"body":6,"description":647,"extension":648,"meta":649,"navigation":267,"ogImage":661,"path":662,"seo":663,"stem":664,"__hash__":665},"content/blogs/24. jtl-to-shopify-migration.md","Migrating a JTL-Shop to Shopify: What to Import and What to Skip",{"type":7,"value":8,"toc":636},"minimark",[9,13,18,26,47,50,54,70,73,77,80,83,148,151,154,166,169,173,179,506,513,517,524,533,536,548,552,567,575,579,582,586,589,615,618,622,625,632],[10,11,12],"p",{},"This week I moved a small D2C cosmetics shop from JTL-Shop 5 (the storefront of the German JTL ERP ecosystem) to Shopify. It was a one-person project with AI agents doing the grunt work, and it produced a few lessons I would have liked to read first. None of them are about the import button, because there is no import button.",[14,15,17],"h2",{"id":16},"there-is-no-connector-so-plan-what-you-actually-need","There Is No Connector, So Plan What You Actually Need",[10,19,20,21,25],{},"I asked the obvious question first: is there a connector or export that takes JTL-Shop to Shopify? There isn't a one-click one. The JTL storefront only exposes ",[22,23,24],"em",{},"active"," articles, and in this project the ERP was being retired, so Shopify became the leading system. That decision shapes everything else, so I wrote it down before touching data:",[27,28,29,33,41,44],"ul",{},[30,31,32],"li",{},"The ERP goes away. Shopify is the source of truth afterwards.",[30,34,35,36,40],{},"Single articles that are really sizes of one product become ",[37,38,39],"strong",{},"variants",". The old shop had a variant \"parent\" article that was never purchasable; it only carried a display price.",[30,42,43],{},"Categories become collections, with the old tree rebuilt as a menu.",[30,45,46],{},"Reviews, bundles and back-in-stock notifications are only migrated if the old shop actually used them.",[10,48,49],{},"The product import itself runs through Shopify's native product CSV. Collections and blog articles need either Matrixify or the Admin API; redirects go through the native redirect CSV. I generated all of it with scripts from read-only access to the old shop, so the legacy system was never modified.",[14,51,53],{"id":52},"merge-duplicates-before-import-not-after","Merge Duplicates Before Import, Not After",[10,55,56,57,61,62,65,66,69],{},"The old catalog had the classic mess: the same product once as a single article and once as a variant, and handles like ",[58,59,60],"code",{},"…_1"," and ",[58,63,64],{},"…_2"," that were really artifacts of the old URL scheme. Cleaning this inside Shopify afterwards means broken reviews and redirects. I resolved it in the mapping files first and kept a ",[58,67,68],{},"decisions.md"," log of each call.",[10,71,72],{},"That log paid off two days later. Someone asked why a product \"was missing\" in the new shop. It was not: the URL belonged to a non-purchasable variant parent in JTL (it only carried a display price, with three real sizes behind it). The decision was in the log, the old URL had a 301 to the merged product, and the answer took one lookup instead of a debugging session. If you merge products, write down which old URL maps to which new variant.",[14,74,76],{"id":75},"orders-the-encrypted-fields-problem","Orders: The Encrypted Fields Problem",[10,78,79],{},"Order history is where it got interesting. JTL-Shop 5 stores surname, company and street encrypted in the customer and address tables. A raw database dump therefore gives you order numbers, amounts, e-mail, postcode and city, but not the name or street.",[10,81,82],{},"I laid out four options:",[84,85,86,102],"table",{},[87,88,89],"thead",{},[90,91,92,96,99],"tr",{},[93,94,95],"th",{},"Option",[93,97,98],{},"Effort",[93,100,101],{},"Result",[103,104,105,117,127,137],"tbody",{},[90,106,107,111,114],{},[108,109,110],"td",{},"Export from the ERP (its database holds plaintext)",[108,112,113],{},"low",[108,115,116],{},"full data",[90,118,119,122,125],{},[108,120,121],{},"Decrypt with the shop's key via a script",[108,123,124],{},"medium, needs server access",[108,126,116],{},[90,128,129,132,134],{},[108,130,131],{},"Anonymized import",[108,133,113],{},[108,135,136],{},"history and revenue, no addresses",[90,138,139,142,145],{},[108,140,141],{},"No old orders at all",[108,143,144],{},"minimal",[108,146,147],{},"fresh start, old system as archive",[10,149,150],{},"Full data was preferable, but the ERP export was not available, so I went with the anonymized import. What it keeps: order number (as a tag and a custom attribute), date, line items, amounts and taxes, e-mail, postcode, city, country, payment and shipping status, tracking numbers. What it deliberately drops: surname, street, company, phone and customer comments.",[10,152,153],{},"Two details that cost me time:",[27,155,156,163],{},[30,157,158,159,162],{},"Shopify discards a whole address if required fields are empty, taking postcode and city with it. A ",[58,160,161],{},"-"," placeholder in surname and street keeps the rest.",[30,164,165],{},"Shopify does not let you set the fulfillment date on import, so the real shipping date goes into a custom attribute.",[10,167,168],{},"Also: invoices still have to be retained under German bookkeeping rules, so the old system or an archive stays relevant. Shopify does not need to replace it.",[14,170,172],{"id":171},"the-import-script-resumable-throttled-idempotent","The Import Script: Resumable, Throttled, Idempotent",[10,174,175,178],{},[58,176,177],{},"orderCreate"," does not work from an admin browser session, so I created a custom app in the dev dashboard with write access for orders and customers, to be uninstalled afterwards. The script is a plain Node process, not an agent task:",[180,181,186],"pre",{"className":182,"code":183,"language":184,"meta":185,"style":185},"language-ts shiki shiki-themes github-dark","// Sketch: one order per call, append-only log makes restarts safe\nconst done = new Set(readLines('import.log').map((l) => JSON.parse(l).legacyNo))\n\nfor (const order of legacyOrders) {\n  if (done.has(order.number)) continue\n\n  const res = await gql(CREATE_ORDER, {\n    order: {\n      email: order.email,\n      processedAt: order.date,\n      tags: [`legacy-${order.number}`],\n      customAttributes: [{ key: 'legacy_no', value: String(order.number) }],\n      lineItems: order.items,\n      // address with '-' placeholders for dropped fields\n    },\n    options: { sendReceipt: false, sendFulfillmentReceipt: false },\n  })\n\n  if (isThrottled(res)) {\n    await sleep(65_000)\n    continue\n  } // retry same order\n  appendLine('import.log', { legacyNo: order.number, id: res.order.id })\n}\n","ts","",[58,187,188,197,262,269,289,307,312,337,343,349,355,378,396,402,408,414,431,437,442,455,472,478,487,500],{"__ignoreMap":185},[189,190,193],"span",{"class":191,"line":192},"line",1,[189,194,196],{"class":195},"sAwPA","// Sketch: one order per call, append-only log makes restarts safe\n",[189,198,200,204,208,211,214,218,222,225,227,231,234,237,240,244,247,250,253,256,259],{"class":191,"line":199},2,[189,201,203],{"class":202},"snl16","const",[189,205,207],{"class":206},"sDLfK"," done",[189,209,210],{"class":202}," =",[189,212,213],{"class":202}," new",[189,215,217],{"class":216},"svObZ"," Set",[189,219,221],{"class":220},"s95oV","(",[189,223,224],{"class":216},"readLines",[189,226,221],{"class":220},[189,228,230],{"class":229},"sU2Wk","'import.log'",[189,232,233],{"class":220},").",[189,235,236],{"class":216},"map",[189,238,239],{"class":220},"((",[189,241,243],{"class":242},"s9osk","l",[189,245,246],{"class":220},") ",[189,248,249],{"class":202},"=>",[189,251,252],{"class":206}," JSON",[189,254,255],{"class":220},".",[189,257,258],{"class":216},"parse",[189,260,261],{"class":220},"(l).legacyNo))\n",[189,263,265],{"class":191,"line":264},3,[189,266,268],{"emptyLinePlaceholder":267},true,"\n",[189,270,272,275,278,280,283,286],{"class":191,"line":271},4,[189,273,274],{"class":202},"for",[189,276,277],{"class":220}," (",[189,279,203],{"class":202},[189,281,282],{"class":206}," order",[189,284,285],{"class":202}," of",[189,287,288],{"class":220}," legacyOrders) {\n",[189,290,292,295,298,301,304],{"class":191,"line":291},5,[189,293,294],{"class":202},"  if",[189,296,297],{"class":220}," (done.",[189,299,300],{"class":216},"has",[189,302,303],{"class":220},"(order.number)) ",[189,305,306],{"class":202},"continue\n",[189,308,310],{"class":191,"line":309},6,[189,311,268],{"emptyLinePlaceholder":267},[189,313,315,318,321,323,326,329,331,334],{"class":191,"line":314},7,[189,316,317],{"class":202},"  const",[189,319,320],{"class":206}," res",[189,322,210],{"class":202},[189,324,325],{"class":202}," await",[189,327,328],{"class":216}," gql",[189,330,221],{"class":220},[189,332,333],{"class":206},"CREATE_ORDER",[189,335,336],{"class":220},", {\n",[189,338,340],{"class":191,"line":339},8,[189,341,342],{"class":220},"    order: {\n",[189,344,346],{"class":191,"line":345},9,[189,347,348],{"class":220},"      email: order.email,\n",[189,350,352],{"class":191,"line":351},10,[189,353,354],{"class":220},"      processedAt: order.date,\n",[189,356,358,361,364,367,369,372,375],{"class":191,"line":357},11,[189,359,360],{"class":220},"      tags: [",[189,362,363],{"class":229},"`legacy-${",[189,365,366],{"class":220},"order",[189,368,255],{"class":229},[189,370,371],{"class":220},"number",[189,373,374],{"class":229},"}`",[189,376,377],{"class":220},"],\n",[189,379,381,384,387,390,393],{"class":191,"line":380},12,[189,382,383],{"class":220},"      customAttributes: [{ key: ",[189,385,386],{"class":229},"'legacy_no'",[189,388,389],{"class":220},", value: ",[189,391,392],{"class":216},"String",[189,394,395],{"class":220},"(order.number) }],\n",[189,397,399],{"class":191,"line":398},13,[189,400,401],{"class":220},"      lineItems: order.items,\n",[189,403,405],{"class":191,"line":404},14,[189,406,407],{"class":195},"      // address with '-' placeholders for dropped fields\n",[189,409,411],{"class":191,"line":410},15,[189,412,413],{"class":220},"    },\n",[189,415,417,420,423,426,428],{"class":191,"line":416},16,[189,418,419],{"class":220},"    options: { sendReceipt: ",[189,421,422],{"class":206},"false",[189,424,425],{"class":220},", sendFulfillmentReceipt: ",[189,427,422],{"class":206},[189,429,430],{"class":220}," },\n",[189,432,434],{"class":191,"line":433},17,[189,435,436],{"class":220},"  })\n",[189,438,440],{"class":191,"line":439},18,[189,441,268],{"emptyLinePlaceholder":267},[189,443,445,447,449,452],{"class":191,"line":444},19,[189,446,294],{"class":202},[189,448,277],{"class":220},[189,450,451],{"class":216},"isThrottled",[189,453,454],{"class":220},"(res)) {\n",[189,456,458,461,464,466,469],{"class":191,"line":457},20,[189,459,460],{"class":202},"    await",[189,462,463],{"class":216}," sleep",[189,465,221],{"class":220},[189,467,468],{"class":206},"65_000",[189,470,471],{"class":220},")\n",[189,473,475],{"class":191,"line":474},21,[189,476,477],{"class":202},"    continue\n",[189,479,481,484],{"class":191,"line":480},22,[189,482,483],{"class":220},"  } ",[189,485,486],{"class":195},"// retry same order\n",[189,488,490,493,495,497],{"class":191,"line":489},23,[189,491,492],{"class":216},"  appendLine",[189,494,221],{"class":220},[189,496,230],{"class":229},[189,498,499],{"class":220},", { legacyNo: order.number, id: res.order.id })\n",[189,501,503],{"class":191,"line":502},24,[189,504,505],{"class":220},"}\n",[10,507,508,509,512],{},"Reality check from the run: the dev store allows roughly five new orders per minute through the API, so about 1,600 orders take hours, not minutes. The first process died in the evening, most likely because the laptop went to sleep, so I wrapped it in ",[58,510,511],{},"caffeinate"," and later moved it to a launchd job. Because the log is append-only and checked on start, restarting never created duplicates. Six orders with bundle products failed because Shopify does not accept bundle variants as line items; the script now creates those as custom line items with title and SKU.",[14,514,516],{"id":515},"silence-notifications-before-you-import-not-after","Silence Notifications Before You Import, Not After",[10,518,519,520,523],{},"I thought mails were off. I had disabled abandoned-checkout and the other marketing-style automations, and I used ",[58,521,522],{},"sendReceipt: false",". Then the shop owner's inbox filled with \"New order\" mails.",[10,525,526,528,529,532],{},[58,527,522],{}," only suppresses ",[22,530,531],{},"customer"," mail. The staff notification \"New order\" fires for every order created via the API. I stopped the import and switched off that notification. After the import had resumed and the mails kept coming, I switched off all six staff notifications (including the weekly summary) until the import was done. The mails already sent cannot be recalled.",[10,534,535],{},"So, before any bulk order import:",[537,538,539,542,545],"ol",{},[30,540,541],{},"Turn off every staff notification, not just the obvious one.",[30,543,544],{},"Keep customer notification templates off while the store is not live.",[30,546,547],{},"Put \"re-enable New order and Order dispute updates\" on the go-live list. Forget it and you lose real orders after launch.",[14,549,551],{"id":550},"verify-with-scripts-not-with-looks-right","Verify With Scripts, Not With \"Looks Right\"",[10,553,554,555,558,559,562,563,566],{},"A progress counter is not a verification. I set up a separate verify script to run after the import, with three checks against the legacy export: order ",[37,556,557],{},"count",", ",[37,560,561],{},"revenue per year",", and ",[37,564,565],{},"cancel status",". Counting alone misses a half-imported year; revenue per year catches missing or duplicated orders and wrong tax handling; cancel status catches the case where cancelled orders silently become paid ones. I also spot-checked the first 16 imported orders against the legacy totals (amounts, payment status, shipping status, voucher codes, tracking numbers).",[10,568,569,570,255],{},"The same principle applies to redirects. The 560 redirects I had in the end were tested as data: no duplicates, no loops, no chains, every target exists, and every URL from the old sitemap answers with a 301 to a 200. If you want redirect and on-page checks as a repeatable crawl, I wrote about ",[571,572,574],"a",{"href":573},"/blogs/building-an-seo-crawler","building an SEO crawler",[14,576,578],{"id":577},"blog-easy-to-move-easy-to-get-subtly-wrong","Blog: Easy to Move, Easy to Get Subtly Wrong",[10,580,581],{},"Shopify has a native blog with several blogs, tags, featured images, SEO fields and custom handles. The JTL blog (71 articles) has no clean export, so I read it from the database tables and the live pages. Image files had to be re-uploaded to Shopify Files and URLs inside article bodies rewritten. Two traps: the blog handle has to match the old URL prefix, or the redirects point to 404; and the old SEO titles import unchanged, so I rewrote suffixes, lengths and truncated descriptions across all articles afterwards.",[14,583,585],{"id":584},"launch-readiness-check","Launch-Readiness Check",[10,587,588],{},"Before launch I had the products checked for SEO and SEA readiness. The verdict first time round was \"not ready\", and the list is a useful template:",[27,590,591,594,597,600,603,606,609,612],{},[30,592,593],{},"Store was still a development store (test payments only), payment providers incomplete.",[30,595,596],{},"Domain not connected, password protection still on.",[30,598,599],{},"Homepage title and meta description missing, no social image.",[30,601,602],{},"Product meta descriptions cut off mid-word or formulaic, with typos; two products sharing the same title.",[30,604,605],{},"Inconsistent title suffixes across products and collections.",[30,607,608],{},"A utility collection still publicly reachable.",[30,610,611],{},"Cookie consent and tracking pixels (they must not fire before consent).",[30,613,614],{},"Legal texts still mentioning the old payment providers and the old ERP.",[10,616,617],{},"I also removed unprovable health claims from the meta descriptions, which is a risk for ads and under German health-advertising law.",[14,619,621],{"id":620},"what-i-would-do-again","What I Would Do Again",[10,623,624],{},"Decide the system of record first. Merge duplicates before import. Import anonymized history rather than block on data you can't get. Run bulk operations as resumable scripts, not as agent sessions. Write the verify script before the import, and write the notification checklist before the first test order.",[10,626,627,628,255],{},"If you are planning an ERP-shop to Shopify move and want someone to build the mapping, the scripts and the launch checklist, ",[571,629,631],{"href":630},"/contact","get in touch",[633,634,635],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#8B949E}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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);}",{"title":185,"searchDepth":199,"depth":199,"links":637},[638,639,640,641,642,643,644,645,646],{"id":16,"depth":199,"text":17},{"id":52,"depth":199,"text":53},{"id":75,"depth":199,"text":76},{"id":171,"depth":199,"text":172},{"id":515,"depth":199,"text":516},{"id":550,"depth":199,"text":551},{"id":577,"depth":199,"text":578},{"id":584,"depth":199,"text":585},{"id":620,"depth":199,"text":621},"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.","md",{"seoTitle":650,"date":651,"image":652,"imageWidth":653,"imageHeight":654,"alt":655,"tags":656,"author":659,"published":267,"featured":660},"JTL-Shop to Shopify Migration: Plan and Verify","06.10.2026","/blogs-img/jtl-shopify-migration.webp",1536,1024,"Legacy shop with an encrypted database feeding a verified import pipeline into a Shopify storefront",[657,658],"ai","devops","Marco Faul",false,"/blogs-img/jtl-shopify-migration.png","/blogs/jtl-to-shopify-migration",{"title":5,"description":647},"blogs/24. jtl-to-shopify-migration","FgMqAwOMlaMD0v10Ts6ZgvvJHxQElS9MhhZojJPTxpw",[667,678,689,699,709,720,730,739,749,759,769,778,788,797,805,814,823,826,836,846,856,865,876,885,895,906,915,924,933,943,953],{"path":668,"title":669,"description":670,"meta":671},"/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":672,"date":673,"image":674,"imageWidth":653,"imageHeight":654,"alt":669,"tags":675,"published":267,"featured":660},"Shopware 6 Performance Guide","24.10.2025","/blogs-img/shopware-performance.webp",[676,677],"shopware","performance",{"path":679,"title":680,"description":681,"meta":682},"/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":683,"date":684,"image":685,"imageWidth":653,"imageHeight":654,"alt":686,"tags":687,"author":659,"published":267,"featured":660},"Shopware Frontends - Composable Storefronts","06.06.2026","/blogs-img/shopware-frontends.webp","Shopware Frontends composable storefront with Vue and Nuxt",[676,688],"frontend",{"path":690,"title":691,"description":692,"meta":693},"/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":694,"date":695,"image":696,"imageWidth":653,"imageHeight":654,"alt":697,"tags":698,"author":659,"published":267,"featured":660},"What a Headless Shopware Store Really Costs","02.06.2026","/blogs-img/headless-cost.webp","What a headless Shopware store really costs in 2026",[676,688],{"path":700,"title":701,"description":702,"meta":703},"/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":704,"date":705,"image":706,"imageWidth":653,"imageHeight":654,"alt":707,"tags":708,"author":659,"published":267,"featured":660},"Headless vs. Traditional Shopware in 2026","30.05.2026","/blogs-img/headless-vs-traditional.webp","Headless vs traditional Shopware 6 decision framework 2026",[676,688],{"path":710,"title":711,"description":712,"meta":713},"/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":714,"date":715,"image":716,"imageWidth":653,"imageHeight":654,"alt":717,"tags":718,"author":659,"published":267,"featured":267},"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",[719,658],"security",{"path":721,"title":722,"description":723,"meta":724},"/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":725,"date":726,"image":727,"imageWidth":653,"imageHeight":654,"alt":728,"tags":729,"author":659,"published":267,"featured":267},"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",[657,658],{"path":731,"title":732,"description":733,"meta":734},"/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":735,"date":726,"image":736,"imageWidth":653,"imageHeight":654,"alt":737,"tags":738,"author":659,"published":267,"featured":660},"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",[657,658],{"path":740,"title":741,"description":742,"meta":743},"/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":744,"date":745,"image":746,"imageWidth":653,"imageHeight":654,"alt":747,"tags":748,"author":659,"published":267,"featured":660},"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",[719,657],{"path":750,"title":751,"description":752,"meta":753},"/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":754,"date":755,"image":756,"imageWidth":653,"imageHeight":654,"alt":757,"tags":758,"author":659,"published":267,"featured":660},"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",[688,658],{"path":760,"title":761,"description":762,"meta":763},"/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":764,"date":765,"image":766,"imageWidth":653,"imageHeight":654,"alt":767,"tags":768,"author":659,"published":267,"featured":660},"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",[677,658],{"path":770,"title":771,"description":772,"meta":773},"/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":774,"date":765,"image":775,"imageWidth":653,"imageHeight":654,"alt":776,"tags":777,"author":659,"published":267,"featured":660},"Debugging LCP - A Real-World Case Study","/blogs-img/lcp-debugging.webp","Debugging Largest Contentful Paint - a real-world case study on marcofaul.de",[677,688],{"path":779,"title":780,"description":781,"meta":782},"/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":783,"date":784,"image":785,"imageWidth":653,"imageHeight":654,"alt":780,"tags":786,"published":267,"featured":660},"Clean Code by Robert C. Martin - Summary","25.10.2025","/blogs-img/clean-code.webp",[787],"best-practices",{"path":789,"title":790,"description":791,"meta":792},"/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":793,"date":765,"image":794,"imageWidth":653,"imageHeight":654,"alt":795,"tags":796,"author":659,"published":267,"featured":660},"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",[787,658],{"path":573,"title":798,"description":799,"meta":800},"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":801,"date":765,"image":802,"imageWidth":653,"imageHeight":654,"alt":803,"tags":804,"author":659,"published":267,"featured":660},"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",[657,658],{"path":806,"title":807,"description":808,"meta":809},"/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":810,"date":765,"image":811,"imageWidth":653,"imageHeight":654,"alt":812,"tags":813,"author":659,"published":267,"featured":660},"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",[657,658],{"path":815,"title":816,"description":817,"meta":818},"/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":819,"date":765,"image":820,"imageWidth":653,"imageHeight":654,"alt":821,"tags":822,"author":659,"published":267,"featured":267},"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",[676,657],{"path":662,"title":5,"description":647,"meta":824},{"seoTitle":650,"date":651,"image":652,"imageWidth":653,"imageHeight":654,"alt":655,"tags":825,"author":659,"published":267,"featured":660},[657,658],{"path":827,"title":828,"description":829,"meta":830},"/blogs/shopware-configurator-share-links","Shareable Product Configurator in Shopware 6 — State in the URL","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.",{"seoTitle":831,"date":832,"image":833,"imageWidth":653,"imageHeight":654,"alt":834,"tags":835,"author":659,"published":267,"featured":660},"Shopware 6 Configurator: Share Links & PDF Export","02.10.2026","/blogs-img/shopware-configurator.webp","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",[676,688],{"path":837,"title":838,"description":839,"meta":840},"/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":841,"date":842,"image":843,"imageWidth":653,"imageHeight":654,"alt":844,"tags":845,"author":659,"published":267,"featured":660},"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",[787,658],{"path":847,"title":848,"description":849,"meta":850},"/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":851,"date":852,"image":853,"imageWidth":653,"imageHeight":654,"alt":854,"tags":855,"author":659,"published":267,"featured":660},"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",[787,658],{"path":857,"title":858,"description":859,"meta":860},"/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":861,"date":651,"image":862,"imageWidth":653,"imageHeight":654,"alt":863,"tags":864,"author":659,"published":267,"featured":660},"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",[657,658],{"path":866,"title":867,"description":868,"meta":869},"/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":870,"date":871,"image":872,"imageWidth":653,"imageHeight":654,"alt":873,"tags":874,"author":659,"published":267,"featured":660},"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",[875,688],"accessibility",{"path":877,"title":878,"description":879,"meta":880},"/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":881,"date":882,"image":883,"imageWidth":653,"imageHeight":654,"alt":878,"tags":884,"author":659,"published":267,"featured":267},"Shopware 6 Decoration Best Practices","26.10.2025","/blogs-img/shopware-decoration.webp",[676,787],{"path":886,"title":887,"description":888,"meta":889},"/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":890,"date":891,"image":892,"imageWidth":653,"imageHeight":654,"alt":893,"tags":894,"author":659,"published":267,"featured":660},"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",[677,676],{"path":896,"title":897,"description":898,"meta":899},"/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":900,"date":901,"image":902,"imageWidth":653,"imageHeight":654,"alt":903,"tags":904,"author":659,"published":267,"featured":660},"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",[657,905],"seo",{"path":907,"title":908,"description":909,"meta":910},"/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":911,"date":882,"image":912,"imageWidth":653,"imageHeight":654,"alt":913,"tags":914,"published":267,"featured":267},"Headless Shopware 6 with Nuxt - Why It Wins","/blogs-img/shopware-nuxt-headless.webp","Headless Shopware 6 with Nuxt - The future of e-commerce",[676,688],{"path":916,"title":917,"description":918,"meta":919},"/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":920,"date":882,"image":921,"imageWidth":653,"imageHeight":654,"alt":922,"tags":923,"published":267},"Code Quality Tools That Actually Matter","/blogs-img/code-quality-tools.webp","Essential code quality tools for modern development",[787,658],{"path":925,"title":926,"description":927,"meta":928},"/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":929,"date":715,"image":930,"imageWidth":653,"imageHeight":654,"alt":931,"tags":932,"author":659,"published":267,"featured":267},"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",[676,658],{"path":934,"title":935,"description":936,"meta":937},"/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":938,"date":939,"image":940,"imageWidth":653,"imageHeight":654,"alt":941,"tags":942,"author":659,"published":267,"featured":267},"Nuxt 4 Migration Guide","10.06.2026","/blogs-img/nuxt-4-migration.webp","Migrating to Nuxt 4 - new app directory and data fetching",[688,658],{"path":944,"title":945,"description":946,"meta":947},"/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":948,"date":949,"image":950,"imageWidth":653,"imageHeight":654,"alt":951,"tags":952,"author":659,"published":267,"featured":660},"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",[677,688],{"path":954,"title":955,"description":956,"meta":957},"/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":958,"date":959,"image":960,"imageWidth":653,"imageHeight":654,"alt":961,"tags":962,"author":659,"published":267,"featured":267},"AI Product Search in Shopware","09.06.2026","/blogs-img/ai-product-search.webp","AI-powered semantic and vector product search in Shopware 6",[676,657],1793447960680]