[{"data":1,"prerenderedAt":10609},["ShallowReactive",2],{"category-frontend":3},[4,714,1412,1953,2373,3095,4824,5586,6029,6356,7243,8266,9642],{"id":5,"title":6,"body":7,"description":700,"extension":701,"meta":702,"navigation":73,"ogImage":704,"path":710,"seo":711,"stem":712,"__hash__":713},"content/blogs/1. shopware-6-performance-guide.md","Shopware 6 Developer Performance Guide - Essential Improvements and Fixes",{"type":8,"value":9,"toc":683},"minimark",[10,15,19,23,31,54,57,99,103,106,157,161,164,211,215,218,266,270,273,324,328,331,351,354,380,384,390,424,428,431,481,485,488,503,507,513,563,567,596,600,626,630,669,673,676,679],[11,12,14],"h3",{"id":13},"introduction","Introduction",[16,17,18],"p",{},"Performance optimization is crucial for any e-commerce platform. Here are the most impactful performance improvements and fixes for Shopware 6 developers. These optimizations can significantly improve your store's loading times and user experience.",[11,20,22],{"id":21},"_1-enable-http-cache","1. Enable HTTP Cache",[16,24,25,26,30],{},"The HTTP cache is your first line of defense for performance. Configure it properly in your ",[27,28,29],"code",{},".env"," file:",[32,33,38],"pre",{"className":34,"code":35,"language":36,"meta":37,"style":37},"language-env shiki shiki-themes dracula","SHOPWARE_HTTP_CACHE_ENABLED=1\nSHOPWARE_HTTP_DEFAULT_TTL=7200\n","env","",[27,39,40,48],{"__ignoreMap":37},[41,42,45],"span",{"class":43,"line":44},"line",1,[41,46,47],{},"SHOPWARE_HTTP_CACHE_ENABLED=1\n",[41,49,51],{"class":43,"line":50},2,[41,52,53],{},"SHOPWARE_HTTP_DEFAULT_TTL=7200\n",[16,55,56],{},"For dynamic content, use cache invalidation strategically:",[32,58,62],{"className":59,"code":60,"language":61,"meta":37,"style":37},"language-php shiki shiki-themes dracula","use Shopware\\Core\\Framework\\Adapter\\Cache\\CacheInvalidator;\n\n$this->cacheInvalidator->invalidate([\n    'product-' . $productId,\n    'navigation'\n]);\n","php",[27,63,64,69,75,81,87,93],{"__ignoreMap":37},[41,65,66],{"class":43,"line":44},[41,67,68],{},"use Shopware\\Core\\Framework\\Adapter\\Cache\\CacheInvalidator;\n",[41,70,71],{"class":43,"line":50},[41,72,74],{"emptyLinePlaceholder":73},true,"\n",[41,76,78],{"class":43,"line":77},3,[41,79,80],{},"$this->cacheInvalidator->invalidate([\n",[41,82,84],{"class":43,"line":83},4,[41,85,86],{},"    'product-' . $productId,\n",[41,88,90],{"class":43,"line":89},5,[41,91,92],{},"    'navigation'\n",[41,94,96],{"class":43,"line":95},6,[41,97,98],{},"]);\n",[11,100,102],{"id":101},"_2-optimize-database-queries","2. Optimize Database Queries",[16,104,105],{},"Avoid N+1 queries by using associations properly:",[32,107,109],{"className":59,"code":108,"language":61,"meta":37,"style":37},"// Bad: Multiple queries\n$products = $this->productRepository->search($criteria, $context);\nforeach ($products as $product) {\n    $manufacturer = $product->getManufacturer(); // Extra query per product\n}\n\n// Good: Single query with associations\n$criteria->addAssociation('manufacturer');\n$products = $this->productRepository->search($criteria, $context);\n",[27,110,111,116,121,126,131,136,140,146,152],{"__ignoreMap":37},[41,112,113],{"class":43,"line":44},[41,114,115],{},"// Bad: Multiple queries\n",[41,117,118],{"class":43,"line":50},[41,119,120],{},"$products = $this->productRepository->search($criteria, $context);\n",[41,122,123],{"class":43,"line":77},[41,124,125],{},"foreach ($products as $product) {\n",[41,127,128],{"class":43,"line":83},[41,129,130],{},"    $manufacturer = $product->getManufacturer(); // Extra query per product\n",[41,132,133],{"class":43,"line":89},[41,134,135],{},"}\n",[41,137,138],{"class":43,"line":95},[41,139,74],{"emptyLinePlaceholder":73},[41,141,143],{"class":43,"line":142},7,[41,144,145],{},"// Good: Single query with associations\n",[41,147,149],{"class":43,"line":148},8,[41,150,151],{},"$criteria->addAssociation('manufacturer');\n",[41,153,155],{"class":43,"line":154},9,[41,156,120],{},[11,158,160],{"id":159},"_3-use-entity-search-for-better-performance","3. Use Entity Search for Better Performance",[16,162,163],{},"Leverage the DAL (Data Abstraction Layer) efficiently:",[32,165,167],{"className":59,"code":166,"language":61,"meta":37,"style":37},"$criteria = new Criteria();\n$criteria->addFilter(new EqualsFilter('active', true));\n$criteria->setLimit(20);\n$criteria->setOffset(0);\n\n// Add only needed associations\n$criteria->addAssociation('cover');\n\n$products = $this->productRepository->search($criteria, $context);\n",[27,168,169,174,179,184,189,193,198,203,207],{"__ignoreMap":37},[41,170,171],{"class":43,"line":44},[41,172,173],{},"$criteria = new Criteria();\n",[41,175,176],{"class":43,"line":50},[41,177,178],{},"$criteria->addFilter(new EqualsFilter('active', true));\n",[41,180,181],{"class":43,"line":77},[41,182,183],{},"$criteria->setLimit(20);\n",[41,185,186],{"class":43,"line":83},[41,187,188],{},"$criteria->setOffset(0);\n",[41,190,191],{"class":43,"line":89},[41,192,74],{"emptyLinePlaceholder":73},[41,194,195],{"class":43,"line":95},[41,196,197],{},"// Add only needed associations\n",[41,199,200],{"class":43,"line":142},[41,201,202],{},"$criteria->addAssociation('cover');\n",[41,204,205],{"class":43,"line":148},[41,206,74],{"emptyLinePlaceholder":73},[41,208,209],{"class":43,"line":154},[41,210,120],{},[11,212,214],{"id":213},"_4-implement-proper-indexing","4. Implement Proper Indexing",[16,216,217],{},"Ensure your custom entities are indexed correctly:",[32,219,221],{"className":59,"code":220,"language":61,"meta":37,"style":37},"use Shopware\\Core\\Framework\\DataAbstractionLayer\\Indexing\\EntityIndexer;\n\nclass CustomEntityIndexer extends EntityIndexer\n{\n    public function update(EntityWrittenContainerEvent $event): void\n    {\n        // Implement efficient indexing logic\n    }\n}\n",[27,222,223,228,232,237,242,247,252,257,262],{"__ignoreMap":37},[41,224,225],{"class":43,"line":44},[41,226,227],{},"use Shopware\\Core\\Framework\\DataAbstractionLayer\\Indexing\\EntityIndexer;\n",[41,229,230],{"class":43,"line":50},[41,231,74],{"emptyLinePlaceholder":73},[41,233,234],{"class":43,"line":77},[41,235,236],{},"class CustomEntityIndexer extends EntityIndexer\n",[41,238,239],{"class":43,"line":83},[41,240,241],{},"{\n",[41,243,244],{"class":43,"line":89},[41,245,246],{},"    public function update(EntityWrittenContainerEvent $event): void\n",[41,248,249],{"class":43,"line":95},[41,250,251],{},"    {\n",[41,253,254],{"class":43,"line":142},[41,255,256],{},"        // Implement efficient indexing logic\n",[41,258,259],{"class":43,"line":148},[41,260,261],{},"    }\n",[41,263,264],{"class":43,"line":154},[41,265,135],{},[11,267,269],{"id":268},"_5-optimize-asset-loading","5. Optimize Asset Loading",[16,271,272],{},"Minimize JavaScript and CSS by configuring the theme properly:",[32,274,278],{"className":275,"code":276,"language":277,"meta":37,"style":37},"language-javascript shiki shiki-themes dracula","// In your theme's webpack configuration\nmodule.exports = {\n  optimization: {\n    minimize: true,\n    splitChunks: {\n      chunks: 'all',\n    },\n  },\n}\n","javascript",[27,279,280,285,290,295,300,305,310,315,320],{"__ignoreMap":37},[41,281,282],{"class":43,"line":44},[41,283,284],{},"// In your theme's webpack configuration\n",[41,286,287],{"class":43,"line":50},[41,288,289],{},"module.exports = {\n",[41,291,292],{"class":43,"line":77},[41,293,294],{},"  optimization: {\n",[41,296,297],{"class":43,"line":83},[41,298,299],{},"    minimize: true,\n",[41,301,302],{"class":43,"line":89},[41,303,304],{},"    splitChunks: {\n",[41,306,307],{"class":43,"line":95},[41,308,309],{},"      chunks: 'all',\n",[41,311,312],{"class":43,"line":142},[41,313,314],{},"    },\n",[41,316,317],{"class":43,"line":148},[41,318,319],{},"  },\n",[41,321,322],{"class":43,"line":154},[41,323,135],{},[11,325,327],{"id":326},"_6-use-message-queue-for-heavy-operations","6. Use Message Queue for Heavy Operations",[16,329,330],{},"Offload time-consuming tasks to the message queue:",[32,332,334],{"className":59,"code":333,"language":61,"meta":37,"style":37},"$this->messageBus->dispatch(\n    new ProductExportMessage($exportId)\n);\n",[27,335,336,341,346],{"__ignoreMap":37},[41,337,338],{"class":43,"line":44},[41,339,340],{},"$this->messageBus->dispatch(\n",[41,342,343],{"class":43,"line":50},[41,344,345],{},"    new ProductExportMessage($exportId)\n",[41,347,348],{"class":43,"line":77},[41,349,350],{},");\n",[16,352,353],{},"Configure workers in your deployment:",[32,355,359],{"className":356,"code":357,"language":358,"meta":37,"style":37},"language-bash shiki shiki-themes dracula","php bin/console messenger:consume async --time-limit=3600\n","bash",[27,360,361],{"__ignoreMap":37},[41,362,363,366,370,373,376],{"class":43,"line":44},[41,364,61],{"class":365},"sAOxA",[41,367,369],{"class":368},"s-mGx"," bin/console",[41,371,372],{"class":368}," messenger:consume",[41,374,375],{"class":368}," async",[41,377,379],{"class":378},"sIQBb"," --time-limit=3600\n",[11,381,383],{"id":382},"_7-enable-redis-for-cache-and-sessions","7. Enable Redis for Cache and Sessions",[16,385,386,387,389],{},"Configure Redis in ",[27,388,29],{},":",[32,391,393],{"className":34,"code":392,"language":36,"meta":37,"style":37},"REDIS_CACHE_HOST=127.0.0.1\nREDIS_CACHE_PORT=6379\nAPP_CACHE_ADAPTER=cache.adapter.redis\n\nSESSION_HANDLER=redis\nSESSION_HANDLER_ID=redis://127.0.0.1:6379\n",[27,394,395,400,405,410,414,419],{"__ignoreMap":37},[41,396,397],{"class":43,"line":44},[41,398,399],{},"REDIS_CACHE_HOST=127.0.0.1\n",[41,401,402],{"class":43,"line":50},[41,403,404],{},"REDIS_CACHE_PORT=6379\n",[41,406,407],{"class":43,"line":77},[41,408,409],{},"APP_CACHE_ADAPTER=cache.adapter.redis\n",[41,411,412],{"class":43,"line":83},[41,413,74],{"emptyLinePlaceholder":73},[41,415,416],{"class":43,"line":89},[41,417,418],{},"SESSION_HANDLER=redis\n",[41,420,421],{"class":43,"line":95},[41,422,423],{},"SESSION_HANDLER_ID=redis://127.0.0.1:6379\n",[11,425,427],{"id":426},"_8-database-connection-pooling","8. Database Connection Pooling",[16,429,430],{},"Use persistent connections for better database performance:",[32,432,436],{"className":433,"code":434,"language":435,"meta":37,"style":37},"language-yaml shiki shiki-themes dracula","# config/packages/doctrine.yaml\ndoctrine:\n  dbal:\n    options:\n      !php/const PDO::ATTR_PERSISTENT: true\n","yaml",[27,437,438,444,454,461,468],{"__ignoreMap":37},[41,439,440],{"class":43,"line":44},[41,441,443],{"class":442},"shSDL","# config/packages/doctrine.yaml\n",[41,445,446,450],{"class":43,"line":50},[41,447,449],{"class":448},"sLL85","doctrine",[41,451,453],{"class":452},"s0Tla",":\n",[41,455,456,459],{"class":43,"line":77},[41,457,458],{"class":448},"  dbal",[41,460,453],{"class":452},[41,462,463,466],{"class":43,"line":83},[41,464,465],{"class":448},"    options",[41,467,453],{"class":452},[41,469,470,473,476,478],{"class":43,"line":89},[41,471,472],{"class":452},"      !php/const",[41,474,475],{"class":448}," PDO::ATTR_PERSISTENT",[41,477,389],{"class":452},[41,479,480],{"class":378}," true\n",[11,482,484],{"id":483},"_9-profiler-management","9. Profiler Management",[16,486,487],{},"Disable the profiler in production:",[32,489,491],{"className":34,"code":490,"language":36,"meta":37,"style":37},"APP_ENV=prod\nSHOPWARE_PROFILER_ENABLED=0\n",[27,492,493,498],{"__ignoreMap":37},[41,494,495],{"class":43,"line":44},[41,496,497],{},"APP_ENV=prod\n",[41,499,500],{"class":43,"line":50},[41,501,502],{},"SHOPWARE_PROFILER_ENABLED=0\n",[11,504,506],{"id":505},"_10-cdn-integration","10. CDN Integration",[16,508,509,510,389],{},"Serve static assets through a CDN by configuring your ",[27,511,512],{},"shopware.yaml",[32,514,516],{"className":433,"code":515,"language":435,"meta":37,"style":37},"shopware:\n  cdn:\n    url: 'https://cdn.your-domain.com'\n    strategy: 'physical_filename'\n",[27,517,518,525,532,549],{"__ignoreMap":37},[41,519,520,523],{"class":43,"line":44},[41,521,522],{"class":448},"shopware",[41,524,453],{"class":452},[41,526,527,530],{"class":43,"line":50},[41,528,529],{"class":448},"  cdn",[41,531,453],{"class":452},[41,533,534,537,539,543,546],{"class":43,"line":77},[41,535,536],{"class":448},"    url",[41,538,389],{"class":452},[41,540,542],{"class":541},"seVfx"," '",[41,544,545],{"class":368},"https://cdn.your-domain.com",[41,547,548],{"class":541},"'\n",[41,550,551,554,556,558,561],{"class":43,"line":83},[41,552,553],{"class":448},"    strategy",[41,555,389],{"class":452},[41,557,542],{"class":541},[41,559,560],{"class":368},"physical_filename",[41,562,548],{"class":541},[11,564,566],{"id":565},"key-performance-metrics-to-monitor","Key Performance Metrics to Monitor",[568,569,570,578,584,590],"ul",{},[571,572,573,577],"li",{},[574,575,576],"strong",{},"Time to First Byte (TTFB)",": Should be under 200ms",[571,579,580,583],{},[574,581,582],{},"Page Load Time",": Target under 2 seconds",[571,585,586,589],{},[574,587,588],{},"Database Query Time",": Keep under 50ms per request",[571,591,592,595],{},[574,593,594],{},"Cache Hit Ratio",": Aim for 80%+",[11,597,599],{"id":598},"quick-wins-checklist","Quick Wins Checklist",[568,601,602,605,608,611,614,617,620,623],{},[571,603,604],{},"✓ Enable HTTP cache",[571,606,607],{},"✓ Configure Redis",[571,609,610],{},"✓ Optimize images (WebP format)",[571,612,613],{},"✓ Enable lazy loading for images",[571,615,616],{},"✓ Minify CSS and JavaScript",[571,618,619],{},"✓ Use proper indexing",[571,621,622],{},"✓ Implement message queues",[571,624,625],{},"✓ Monitor slow queries",[11,627,629],{"id":628},"useful-reference-links","Useful Reference Links",[568,631,632,641,648,655,662],{},[571,633,634],{},[635,636,640],"a",{"href":637,"rel":638},"https://docs.shopware.com/en/shopware-6-en/hosting/performance/performance-tweaks",[639],"nofollow","Official Shopware 6 Performance Guide",[571,642,643],{},[635,644,647],{"href":645,"rel":646},"https://docs.shopware.com/en/shopware-6-en/hosting/performance/caches",[639],"Shopware 6 Cache Documentation",[571,649,650],{},[635,651,654],{"href":652,"rel":653},"https://docs.shopware.com/en/shopware-6-en/concepts-and-architecture/data-abstraction-layer",[639],"DAL Best Practices",[571,656,657],{},[635,658,661],{"href":659,"rel":660},"https://docs.shopware.com/en/shopware-6-en/concepts-and-architecture/message-queue",[639],"Message Queue Documentation",[571,663,664],{},[635,665,668],{"href":666,"rel":667},"https://docs.shopware.com/en/shopware-6-en/hosting/performance/caches#redis",[639],"Redis Configuration Guide",[11,670,672],{"id":671},"conclusion","Conclusion",[16,674,675],{},"Performance optimization is an ongoing process. Start with the quick wins like enabling HTTP cache and Redis, then gradually implement more advanced optimizations. Always measure before and after to validate your improvements.",[16,677,678],{},"Remember: Premature optimization is the root of all evil, but ignoring performance is the root of all user frustration.",[680,681,682],"style",{},"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 .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}",{"title":37,"searchDepth":50,"depth":50,"links":684},[685,686,687,688,689,690,691,692,693,694,695,696,697,698,699],{"id":13,"depth":77,"text":14},{"id":21,"depth":77,"text":22},{"id":101,"depth":77,"text":102},{"id":159,"depth":77,"text":160},{"id":213,"depth":77,"text":214},{"id":268,"depth":77,"text":269},{"id":326,"depth":77,"text":327},{"id":382,"depth":77,"text":383},{"id":426,"depth":77,"text":427},{"id":483,"depth":77,"text":484},{"id":505,"depth":77,"text":506},{"id":565,"depth":77,"text":566},{"id":598,"depth":77,"text":599},{"id":628,"depth":77,"text":629},{"id":671,"depth":77,"text":672},"Performance optimization is crucial for any e-commerce platform. Here are the most impactful performance improvements and fixes for Shopware 6 developers.","md",{"date":703,"image":704,"alt":6,"tags":705,"published":73,"featured":709},"24.10.2025","/blogs-img/shopware-performance.png",[522,706,707,708],"performance","optimization","e-commerce",false,"/blogs/shopware-6-performance-guide",{"title":6,"description":700},"blogs/1. shopware-6-performance-guide","_U7degVsiQLjYD9_goMFzG9EBhSCL81XfbCmsQH7fo8",{"id":715,"title":716,"body":717,"description":1398,"extension":701,"meta":1399,"navigation":73,"ogImage":1401,"path":1408,"seo":1409,"stem":1410,"__hash__":1411},"content/blogs/10. shopware-frontends-composable-storefront.md","Shopware Frontends - Building a Composable Storefront with Vue and Nuxt",{"type":8,"value":718,"toc":1382},[719,722,727,730,740,746,754,758,761,767,774,780,797,803,806,810,813,817,828,842,845,879,884,899,910,914,917,1003,1018,1024,1028,1031,1081,1084,1088,1091,1266,1271,1279,1283,1286,1291,1308,1313,1324,1327,1331,1334,1341,1345,1379],[16,720,721],{},"When a client first asked me to build them a headless Shopware storefront, I almost reached for a from-scratch Nuxt app. I'm glad I didn't. I spent about half a day exploring what Shopware Frontends actually offered before writing a single component — and what I found was a collection of Vue/Nuxt packages that handles the API client, composables, CMS rendering, and TypeScript types out of the box. That half day saved me weeks. This post walks through the architecture, the packages, the right starting point, and when this approach is actually worth the complexity.",[723,724,726],"h2",{"id":725},"the-architecture-store-api-nuxt-cleanly-separated","The Architecture: Store API + Nuxt, Cleanly Separated",[16,728,729],{},"The mental model that clicked for me immediately: Shopware 6 owns the data, Nuxt owns the user interface.",[16,731,732,735,736,739],{},[574,733,734],{},"Shopware 6"," exposes everything through the ",[574,737,738],{},"Store API"," — products, categories, carts, orders, customer accounts, CMS content, search. It doesn't care what renders it. Every product detail page, every filter request, every checkout step is a Store API call.",[16,741,742,745],{},[574,743,744],{},"Your Nuxt frontend"," handles everything the browser touches: rendering, routing, state management, server-side rendering (SSR), and caching. Deployed independently, scaled independently. A traffic spike on the storefront doesn't put load on your ERP or Shopware admin — the backend just serves API responses.",[16,747,748,749,753],{},"I've found this separation to be exactly as clean in practice as it sounds on paper. On a recent project, we needed to move the frontend to a different CDN region without touching the Shopware instance at all — that kind of independent scaling is only possible when the two layers are genuinely decoupled. It's the same separation I described in ",[635,750,752],{"href":751},"/blogs/headless-gamechanger","Why Headless Shopware 6 with Nuxt is a Game-Changer",". Shopware Frontends is the official, opinionated way to implement it.",[723,755,757],{"id":756},"the-packages","The Packages",[16,759,760],{},"Shopware Frontends is not a monolith — it's a set of scoped packages you compose. Here's what each one actually does:",[11,762,764],{"id":763},"shopwareapi-client",[27,765,766],{},"@shopware/api-client",[16,768,769,770,773],{},"A standalone TypeScript HTTP client for the Store API. The thing I appreciate most: it's ",[574,771,772],{},"framework-independent"," — you can use it in a React project, a Svelte app, a plain Node.js script, or any environment that can run JavaScript. It ships with generated TypeScript types from the Store API OpenAPI schema, so you get autocompletion and type safety on every response without maintaining your own type definitions. I've wasted enough hours on hand-rolled API types to know this alone is worth pulling the package in.",[11,775,777],{"id":776},"shopwarecomposables",[27,778,779],{},"@shopware/composables",[16,781,782,783,786,787,786,790,786,793,796],{},"Vue 3 composables that wrap the API client with reactive state. ",[27,784,785],{},"useAddToCart",", ",[27,788,789],{},"useProductSearch",[27,791,792],{},"useCustomerOrders",[27,794,795],{},"useCheckout"," — the business-logic layer your components consume. Built on the Composition API; no Options API in sight. My rule of thumb: if the composable exists, use it before you reach for a direct API call.",[11,798,800],{"id":799},"shopwarecms-base-layer",[27,801,802],{},"@shopware/cms-base-layer",[16,804,805],{},"A Nuxt layer that provides default Vue components for rendering Shopware's CMS (Shopping Experiences / Layouts). Maps CMS block types to Vue components so your category pages and landing pages render CMS-managed content without you manually writing a component per block type. You override only the blocks you need to customise. I've found this particularly useful when clients need marketing control over page layouts — you hand them CMS control without giving up your component structure.",[11,807,809],{"id":808},"the-nuxt-layer-module","The Nuxt Layer / Module",[16,811,812],{},"Registers composables, sets up the API client with your Store API URL and access key, and wires everything into the Nuxt context. This is what makes the composables available globally without manual imports.",[723,814,816],{"id":815},"start-here-the-vue-starter-template-not-the-other-one","Start Here: The Vue Starter Template (Not the Other One)",[16,818,819,820,823,824,827],{},"There are two official starting points. This is where I'd save you a mistake I made myself: ",[574,821,822],{},"do NOT start on the Demo Store Template"," — I did once, thinking it was the more feature-complete option. It's not. Shopware classifies it as ",[574,825,826],{},"not production-ready",". It exists to demonstrate capabilities and explore the packages. Use it to poke around, not to ship.",[16,829,830,833,834,837,838,841],{},[574,831,832],{},"Vue Starter Template"," — this is the one you want. Since late 2025 it's based on ",[574,835,836],{},"Nuxt 4"," with the ",[27,839,840],{},"app/"," directory structure, uses Tailwind CSS for styling, and is explicitly positioned as a production-ready foundation. It ships as a fully functional storefront you refine, not a blank canvas you fill.",[16,843,844],{},"Bootstrapping is quick:",[32,846,848],{"className":356,"code":847,"language":358,"meta":37,"style":37},"npx tiged shopware/frontends/examples/vue-starter my-storefront\ncd my-storefront\nnpm install\n",[27,849,850,864,871],{"__ignoreMap":37},[41,851,852,855,858,861],{"class":43,"line":44},[41,853,854],{"class":365},"npx",[41,856,857],{"class":368}," tiged",[41,859,860],{"class":368}," shopware/frontends/examples/vue-starter",[41,862,863],{"class":368}," my-storefront\n",[41,865,866,869],{"class":43,"line":50},[41,867,868],{"class":448},"cd",[41,870,863],{"class":368},[41,872,873,876],{"class":43,"line":77},[41,874,875],{"class":365},"npm",[41,877,878],{"class":368}," install\n",[16,880,881,882,389],{},"Then add your ",[27,883,29],{},[32,885,887],{"className":34,"code":886,"language":36,"meta":37,"style":37},"NUXT_PUBLIC_SHOPWARE_ENDPOINT=https://your-shop.example.com\nNUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN=your-store-api-access-token\n",[27,888,889,894],{"__ignoreMap":37},[41,890,891],{"class":43,"line":44},[41,892,893],{},"NUXT_PUBLIC_SHOPWARE_ENDPOINT=https://your-shop.example.com\n",[41,895,896],{"class":43,"line":50},[41,897,898],{},"NUXT_PUBLIC_SHOPWARE_ACCESS_TOKEN=your-store-api-access-token\n",[16,900,901,902,904,905,909],{},"The template already runs on Nuxt 4. If you haven't worked with the ",[27,903,840],{}," directory layout yet, ",[635,906,908],{"href":907},"/blogs/nuxt-4-migration-guide","the Nuxt 4 migration guide"," covers the structural changes and what moved where.",[723,911,913],{"id":912},"nuxt-layers-the-multi-brand-multi-storefront-model","Nuxt Layers: The Multi-Brand / Multi-Storefront Model",[16,915,916],{},"The layers model clicked for me when I was looking at a two-brand project and dreading the idea of maintaining two separate repos with duplicated checkout logic. The extends chain is what solved it:",[32,918,922],{"className":919,"code":920,"language":921,"meta":37,"style":37},"language-ts shiki shiki-themes dracula","// nuxt.config.ts — your project\nexport default defineNuxtConfig({\n  extends: [\n    './vue-starter-template',        // your cloned/installed base\n    '@shopware/composables/nuxt-layer',\n    '@shopware/cms-base-layer',\n  ],\n})\n","ts",[27,923,924,929,944,954,971,983,993,998],{"__ignoreMap":37},[41,925,926],{"class":43,"line":44},[41,927,928],{"class":442},"// nuxt.config.ts — your project\n",[41,930,931,934,937,940],{"class":43,"line":50},[41,932,933],{"class":452},"export",[41,935,936],{"class":452}," default",[41,938,939],{"class":365}," defineNuxtConfig",[41,941,943],{"class":942},"sCdxs","({\n",[41,945,946,949,951],{"class":43,"line":77},[41,947,948],{"class":942},"  extends",[41,950,389],{"class":452},[41,952,953],{"class":942}," [\n",[41,955,956,959,962,965,968],{"class":43,"line":83},[41,957,958],{"class":541},"    '",[41,960,961],{"class":368},"./vue-starter-template",[41,963,964],{"class":541},"'",[41,966,967],{"class":942},",        ",[41,969,970],{"class":442},"// your cloned/installed base\n",[41,972,973,975,978,980],{"class":43,"line":89},[41,974,958],{"class":541},[41,976,977],{"class":368},"@shopware/composables/nuxt-layer",[41,979,964],{"class":541},[41,981,982],{"class":942},",\n",[41,984,985,987,989,991],{"class":43,"line":95},[41,986,958],{"class":541},[41,988,802],{"class":368},[41,990,964],{"class":541},[41,992,982],{"class":942},[41,994,995],{"class":43,"line":142},[41,996,997],{"class":942},"  ],\n",[41,999,1000],{"class":43,"line":148},[41,1001,1002],{"class":942},"})\n",[16,1004,1005,1006,1009,1010,1013,1014,1017],{},"Child layers ",[574,1007,1008],{},"inherit"," everything from parent layers and selectively ",[574,1011,1012],{},"override"," only what they need. A custom component in your project replaces the same-named component from the template layer. A custom CMS block component shadows the default from ",[27,1015,1016],{},"cms-base-layer",".",[16,1019,1020,1021,1023],{},"The concrete payoff for multi-brand setups: you maintain one ",[27,1022,779],{}," layer with core business logic and two project layers for Brand A and Brand B. Both brands share checkout logic, wishlist, account pages — the hard parts — and only diverge in design and brand-specific flows. No copy-pasting composable logic between repos. I've seen teams burn a lot of time keeping two codebases in sync; this architecture makes that problem largely disappear.",[723,1025,1027],{"id":1026},"what-you-get-out-of-the-box","What You Get Out of the Box",[16,1029,1030],{},"This is where I always pause for a moment when onboarding someone to Frontends, because the list is genuinely substantial. A fresh Vue Starter Template ships with working implementations of:",[568,1032,1033,1039,1045,1051,1057,1063,1069,1075],{},[571,1034,1035,1038],{},[574,1036,1037],{},"PDP"," — product detail page with variant selection and add-to-cart",[571,1040,1041,1044],{},[574,1042,1043],{},"PLP"," — product listing page with faceted filters and sorting",[571,1046,1047,1050],{},[574,1048,1049],{},"Search"," — live search with results page",[571,1052,1053,1056],{},[574,1054,1055],{},"Checkout"," — cart → shipping/payment selection → order confirmation",[571,1058,1059,1062],{},[574,1060,1061],{},"Account"," — registration, login, profile, addresses, order history, password/email change",[571,1064,1065,1068],{},[574,1066,1067],{},"Wishlist"," — add/remove, persistent across sessions",[571,1070,1071,1074],{},[574,1072,1073],{},"Newsletter"," — subscription and confirmation flow",[571,1076,1077,1080],{},[574,1078,1079],{},"Layout"," — header, footer, navigation (mega-menu capable), side menu, account dropdown, modals",[16,1082,1083],{},"You're not assembling plumbing from scratch. The job is customising these pages to match your design system and extending them with your business requirements. Every time I've skipped appreciating how much comes pre-wired, I've ended up rebuilding something that was already there.",[723,1085,1087],{"id":1086},"a-composable-in-practice","A Composable in Practice",[16,1089,1090],{},"Here's the shape of a typical usage — adding a product to cart from a PDP component:",[32,1092,1096],{"className":1093,"code":1094,"language":1095,"meta":37,"style":37},"language-vue shiki shiki-themes dracula","\u003Cscript setup lang=\"ts\">\nconst { product } = defineProps\u003C{ product: Product }>()\n\nconst { addToCart, isLoading } = useAddToCart(product)\nconst { count } = useCart()\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cbutton :disabled=\"isLoading\" @click=\"addToCart()\">\n    Add to cart ({{ count }})\n  \u003C/button>\n\u003C/template>\n","vue",[27,1097,1098,1126,1151,1155,1170,1185,1194,1198,1207,1241,1247,1257],{"__ignoreMap":37},[41,1099,1100,1103,1106,1110,1113,1116,1119,1121,1123],{"class":43,"line":44},[41,1101,1102],{"class":942},"\u003C",[41,1104,1105],{"class":452},"script",[41,1107,1109],{"class":1108},"sY_PY"," setup",[41,1111,1112],{"class":1108}," lang",[41,1114,1115],{"class":452},"=",[41,1117,1118],{"class":541},"\"",[41,1120,921],{"class":368},[41,1122,1118],{"class":541},[41,1124,1125],{"class":942},">\n",[41,1127,1128,1131,1134,1136,1139,1142,1144,1148],{"class":43,"line":50},[41,1129,1130],{"class":452},"const",[41,1132,1133],{"class":942}," { product } ",[41,1135,1115],{"class":452},[41,1137,1138],{"class":365}," defineProps",[41,1140,1141],{"class":942},"\u003C{ product",[41,1143,389],{"class":452},[41,1145,1147],{"class":1146},"sGEwX"," Product",[41,1149,1150],{"class":942}," }>()\n",[41,1152,1153],{"class":43,"line":77},[41,1154,74],{"emptyLinePlaceholder":73},[41,1156,1157,1159,1162,1164,1167],{"class":43,"line":83},[41,1158,1130],{"class":452},[41,1160,1161],{"class":942}," { addToCart, isLoading } ",[41,1163,1115],{"class":452},[41,1165,1166],{"class":365}," useAddToCart",[41,1168,1169],{"class":942},"(product)\n",[41,1171,1172,1174,1177,1179,1182],{"class":43,"line":89},[41,1173,1130],{"class":452},[41,1175,1176],{"class":942}," { count } ",[41,1178,1115],{"class":452},[41,1180,1181],{"class":365}," useCart",[41,1183,1184],{"class":942},"()\n",[41,1186,1187,1190,1192],{"class":43,"line":95},[41,1188,1189],{"class":942},"\u003C/",[41,1191,1105],{"class":452},[41,1193,1125],{"class":942},[41,1195,1196],{"class":43,"line":142},[41,1197,74],{"emptyLinePlaceholder":73},[41,1199,1200,1202,1205],{"class":43,"line":148},[41,1201,1102],{"class":942},[41,1203,1204],{"class":452},"template",[41,1206,1125],{"class":942},[41,1208,1209,1212,1215,1218,1220,1222,1225,1227,1230,1232,1234,1237,1239],{"class":43,"line":154},[41,1210,1211],{"class":942},"  \u003C",[41,1213,1214],{"class":452},"button",[41,1216,1217],{"class":1108}," :disabled",[41,1219,1115],{"class":452},[41,1221,1118],{"class":541},[41,1223,1224],{"class":368},"isLoading",[41,1226,1118],{"class":541},[41,1228,1229],{"class":1108}," @click",[41,1231,1115],{"class":452},[41,1233,1118],{"class":541},[41,1235,1236],{"class":368},"addToCart()",[41,1238,1118],{"class":541},[41,1240,1125],{"class":942},[41,1242,1244],{"class":43,"line":1243},10,[41,1245,1246],{"class":942},"    Add to cart ({{ count }})\n",[41,1248,1250,1253,1255],{"class":43,"line":1249},11,[41,1251,1252],{"class":942},"  \u003C/",[41,1254,1214],{"class":452},[41,1256,1125],{"class":942},[41,1258,1260,1262,1264],{"class":43,"line":1259},12,[41,1261,1189],{"class":942},[41,1263,1204],{"class":452},[41,1265,1125],{"class":942},[16,1267,1268,1270],{},[27,1269,785],{}," manages the API call, loading state, and cart refresh. Your component stays declarative — no manual fetch calls, no manual state wiring. This is the pattern across the whole surface: the composable owns the side effects, the component owns the template. I've found this split makes components dramatically easier to test and reason about in isolation.",[16,1272,1273,1274,1278],{},"For features beyond the storefront baseline — like AI-assisted product discovery — composables are the natural extension point. An ",[635,1275,1277],{"href":1276},"/blogs/ai-product-search-shopware","AI-powered product search integration"," slots cleanly into the same pattern: a composable wrapping the search logic, a component consuming reactive results.",[723,1280,1282],{"id":1281},"when-frontends-is-the-right-call","When Frontends Is the Right Call",[16,1284,1285],{},"I've been asked enough times \"is this overkill for our project?\" that I've developed a fairly clear heuristic.",[16,1287,1288],{},[574,1289,1290],{},"Use Shopware Frontends when:",[568,1292,1293,1296,1299,1302,1305],{},[571,1294,1295],{},"You need a custom design that the default Shopware storefront template system can't deliver",[571,1297,1298],{},"You want a reactive UI for product configurators, live filters, or real-time personalisation — things the classic JS plugin system handles awkwardly",[571,1300,1301],{},"You need to scale the frontend horizontally and independently from the Shopware backend",[571,1303,1304],{},"You're running multiple brands or storefronts that should share core logic",[571,1306,1307],{},"You want TypeScript types across your entire API layer without maintaining them yourself",[16,1309,1310],{},[574,1311,1312],{},"Stick with the default storefront when:",[568,1314,1315,1318,1321],{},[571,1316,1317],{},"The project is a standard catalogue with no unusual UI requirements",[571,1319,1320],{},"The team has no Vue/Nuxt experience and the timeline is tight",[571,1322,1323],{},"The budget doesn't support the additional infrastructure complexity of a decoupled frontend",[16,1325,1326],{},"Headless is a multiplier — it amplifies velocity for teams that know Vue, and amplifies friction for teams that don't. I've seen both sides of that equation. Be honest about which situation you're in before you commit.",[723,1328,1330],{"id":1329},"my-takeaway","My Takeaway",[16,1332,1333],{},"Shopware Frontends gives you the official, typed, composable-first path to a headless Shopware storefront. The architecture is clean: Store API for data, Nuxt for rendering, composables for business logic, Nuxt layers for sharing and extending. The Vue Starter Template means you're not starting from zero — you inherit a complete storefront and customise from there.",[16,1335,1336,1337,1017],{},"If you're evaluating a headless Shopware project and want a technical audit, a second opinion on architecture, or hands-on implementation help — that's exactly the kind of work I do as a freelance Shopware + Nuxt developer. ",[635,1338,1340],{"href":1339},"/contact","Get in touch",[723,1342,1344],{"id":1343},"useful-references","Useful References",[568,1346,1347,1355,1363,1371],{},[571,1348,1349,1354],{},[635,1350,1353],{"href":1351,"rel":1352},"https://frontends.shopware.com/",[639],"Shopware Frontends Documentation"," — official docs, guides, and API reference",[571,1356,1357,1362],{},[635,1358,1361],{"href":1359,"rel":1360},"https://developer.shopware.com/docs/concepts/api/store-api.html",[639],"Shopware Store API Reference"," — the API your frontend consumes",[571,1364,1365,1370],{},[635,1366,1369],{"href":1367,"rel":1368},"https://nuxt.com/docs/getting-started/layers",[639],"Nuxt Layers Documentation"," — how the extends mechanism works",[571,1372,1373,1378],{},[635,1374,1377],{"href":1375,"rel":1376},"https://github.com/shopware/frontends",[639],"Shopware Composable Frontends GitHub"," — source, examples, and the Vue Starter Template",[680,1380,1381],{},"html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}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 .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .sY_PY, html code.shiki .sY_PY{--shiki-default:#50FA7B;--shiki-default-font-style:italic}html pre.shiki code .sGEwX, html code.shiki .sGEwX{--shiki-default:#FFB86C;--shiki-default-font-style:italic}",{"title":37,"searchDepth":50,"depth":50,"links":1383},[1384,1385,1391,1392,1393,1394,1395,1396,1397],{"id":725,"depth":50,"text":726},{"id":756,"depth":50,"text":757,"children":1386},[1387,1388,1389,1390],{"id":763,"depth":77,"text":766},{"id":776,"depth":77,"text":779},{"id":799,"depth":77,"text":802},{"id":808,"depth":77,"text":809},{"id":815,"depth":50,"text":816},{"id":912,"depth":50,"text":913},{"id":1026,"depth":50,"text":1027},{"id":1086,"depth":50,"text":1087},{"id":1281,"depth":50,"text":1282},{"id":1329,"depth":50,"text":1330},{"id":1343,"depth":50,"text":1344},"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.",{"date":1400,"image":1401,"alt":1402,"tags":1403,"author":1407,"published":73,"featured":709},"06.06.2026","/blogs-img/shopware-frontends.png","Shopware Frontends composable storefront with Vue and Nuxt",[522,1404,1095,1405,1406],"nuxt","headless","composable-frontends","Marco Faul","/blogs/shopware-frontends-composable-storefront",{"title":716,"description":1398},"blogs/10. shopware-frontends-composable-storefront","NHoUbe4YrppS43r22FtDaEj3oF96CVODfN-RBsDGtWA",{"id":1413,"title":1414,"body":1415,"description":1941,"extension":701,"meta":1942,"navigation":73,"ogImage":1944,"path":1949,"seo":1950,"stem":1951,"__hash__":1952},"content/blogs/11. headless-shopware-cost.md","What Does a Headless Shopware Store Really Cost? A Realistic 2026 Breakdown",{"type":8,"value":1416,"toc":1925},[1417,1425,1436,1440,1444,1447,1454,1457,1461,1468,1471,1475,1481,1497,1500,1504,1507,1526,1537,1541,1544,1550,1564,1567,1571,1578,1581,1585,1730,1736,1740,1743,1781,1788,1792,1795,1831,1835,1838,1874,1878,1881,1884,1890,1894],[16,1418,1419,1420,1424],{},"Every few months a founder or product lead asks me some version of: ",[1421,1422,1423],"em",{},"should we go headless?"," And every time, my honest answer is: it depends — but not in the hand-wavy way. There's a concrete framework I run through with clients, and I want to share it here because I've seen both extremes go wrong. Teams that went headless when they didn't need to, burning budget and time. Teams that stayed traditional when headless would have unlocked real business value. This is the breakdown I give them.",[16,1426,1427,1428,1431,1432,1435],{},"The short version: headless ",[1421,1429,1430],{},"is"," more expensive — upfront and ongoing. The question worth asking isn't \"is headless cheaper?\" It never is. The real question is whether the performance, UX control, flexibility, and omnichannel reach pay back for ",[1421,1433,1434],{},"your"," specific business. Let me walk through where the money actually goes.",[723,1437,1439],{"id":1438},"where-the-money-goes-in-a-headless-build","Where the Money Goes in a Headless Build",[11,1441,1443],{"id":1442},"_1-build-development-the-biggest-delta","1. Build / Development (the biggest delta)",[16,1445,1446],{},"A traditional Shopware setup means working within Shopware's conventions — Twig templates, SCSS overrides, maybe custom JavaScript on top of the existing storefront. The framework hands you a lot for free.",[16,1448,1449,1450,1453],{},"Going headless means building a ",[574,1451,1452],{},"complete custom frontend"," — typically Nuxt/Vue — on top of the Shopware Store API. I mean everything: routing, cart logic, checkout flow, authentication, product detail pages, category listings, search integration. Every screen that used to be \"tweak the theme\" becomes \"build from scratch.\"",[16,1455,1456],{},"That's a fundamentally different scope, and I want to be clear-eyed about it. More frontend engineering hours, more integration surface, more QA. This is where most of the cost delta lives, and it's where I've seen the most sticker shock when clients compare initial estimates between the two approaches.",[11,1458,1460],{"id":1459},"_2-design-ux","2. Design & UX",[16,1462,1463,1464,1467],{},"Here's the thing: most projects I see go headless ",[1421,1465,1466],{},"because"," the team wants differentiated design. That's a perfectly valid reason — it's often the right one. But it means the design budget goes up alongside development. You're not adapting templates anymore; you're designing a bespoke experience from a blank canvas.",[16,1469,1470],{},"I always push back a little here though. If the team was going to commission fully custom design anyway, the marginal cost from headless is smaller than it looks. But if the brief is \"a solid Shopware shop with our logo and colors,\" headless adds design cost you simply didn't need to pay.",[11,1472,1474],{"id":1473},"_3-hosting-infrastructure","3. Hosting & Infrastructure",[16,1476,1477,1478,389],{},"A traditional Shopware setup is one thing to host and operate. Headless is ",[574,1479,1480],{},"two",[568,1482,1483,1490],{},[571,1484,1485,1486,1489],{},"The ",[574,1487,1488],{},"Shopware backend"," (PHP, MariaDB, Elasticsearch/OpenSearch) — your existing infrastructure.",[571,1491,1492,1493,1496],{},"A ",[574,1494,1495],{},"separately deployed frontend"," (Node.js process, or deployed to an edge/CDN platform like Netlify, Vercel, or Cloudflare Pages).",[16,1498,1499],{},"Add a CDN layer on top for static assets and ISR (Incremental Static Regeneration) and you've got more moving parts. The upside is real — each piece scales independently. The downside is also real: two things to monitor, two things to secure, two things to keep running. I've been paged at odd hours for frontend deployments on headless projects when the backend was perfectly healthy. That's just the nature of the architecture.",[11,1501,1503],{"id":1502},"_4-third-party-saas-the-recurring-bill-that-sneaks-up-on-you","4. Third-Party SaaS (the recurring bill that sneaks up on you)",[16,1505,1506],{},"Headless projects frequently add a stack of services that weren't needed before:",[568,1508,1509,1514,1520],{},[571,1510,1511,1513],{},[574,1512,1049],{},": a dedicated search provider (semantic/AI search, faceting at scale).",[571,1515,1516,1519],{},[574,1517,1518],{},"Headless CMS",": for rich content pages, editorial workflows, landing pages — tools like Storyblok.",[571,1521,1522,1525],{},[574,1523,1524],{},"Payments, reviews, loyalty",": sometimes swapped out for more flexible headless-compatible providers.",[16,1527,1528,1529,1532,1533,1536],{},"These are ",[574,1530,1531],{},"recurring monthly costs"," that compound. I've worked on projects where the SaaS layer added meaningful overhead every single month — budget them as operational overhead from day one, not one-time build costs. If you're curious how AI product search fits into this picture, ",[635,1534,1535],{"href":1276},"AI-powered product search in Shopware"," is worth reading alongside this post.",[11,1538,1540],{"id":1539},"_5-maintenance-the-cost-everyone-forgets","5. Maintenance (the cost everyone forgets)",[16,1542,1543],{},"I'll be direct here because this is where I've seen the most pain: maintenance is the cost everyone underestimates, and it's the one that comes back to bite you 18 months after launch.",[16,1545,1546,1547,389],{},"Traditional Shopware maintenance means keeping Shopware updated and your theme compatible. Headless means keeping ",[574,1548,1549],{},"two codebases healthy",[568,1551,1552,1558],{},[571,1553,1554,1557],{},[574,1555,1556],{},"Shopware core updates"," — major version bumps (e.g. 6.6 → 6.7) mean verifying Store API compatibility and updating any custom API consumers.",[571,1559,1560,1563],{},[574,1561,1562],{},"Frontend framework updates"," — Nuxt major versions are not trivial migrations. Your custom frontend will need periodic investment just to stay on supported versions.",[16,1565,1566],{},"I've inherited too many abandoned headless frontends — projects where the initial agency shipped something genuinely impressive and then the client lost track of maintenance. A year later, the codebase is two major Nuxt versions behind, nobody on the team wants to touch it, and the \"modern architecture\" has become a liability. Maintenance isn't a one-time effort. Build it into your monthly cost model before you commit to the approach.",[11,1568,1570],{"id":1569},"_6-team-skills","6. Team & Skills",[16,1572,1573,1574,1577],{},"Shopware theming requires Shopware-specific knowledge — Twig, the plugin system, the admin. Headless requires ",[574,1575,1576],{},"Vue/Nuxt expertise"," on top of (or instead of) that. You either hire for it, upskill for it, or work with an agency or freelancer that has it.",[16,1579,1580],{},"If you're already running a Vue/Nuxt team internally, the cost delta here is smaller. If you're starting from a Shopware-only skillset, factor in the skills gap honestly — it affects both the initial build and every future sprint.",[723,1582,1584],{"id":1583},"traditional-vs-headless-a-relative-comparison","Traditional vs. Headless: A Relative Comparison",[1586,1587,1588,1604],"table",{},[1589,1590,1591],"thead",{},[1592,1593,1594,1598,1601],"tr",{},[1595,1596,1597],"th",{},"Area",[1595,1599,1600],{},"Traditional Shopware",[1595,1602,1603],{},"Headless Shopware",[1605,1606,1607,1621,1634,1647,1659,1672,1683,1696,1708,1719],"tbody",{},[1592,1608,1609,1615,1618],{},[1610,1611,1612],"td",{},[574,1613,1614],{},"Initial build cost",[1610,1616,1617],{},"Low–Medium",[1610,1619,1620],{},"High",[1592,1622,1623,1628,1631],{},[1610,1624,1625],{},[574,1626,1627],{},"Time to first launch",[1610,1629,1630],{},"Faster",[1610,1632,1633],{},"Slower",[1592,1635,1636,1641,1644],{},[1610,1637,1638],{},[574,1639,1640],{},"Hosting complexity",[1610,1642,1643],{},"Low",[1610,1645,1646],{},"Medium–High",[1592,1648,1649,1654,1656],{},[1610,1650,1651],{},[574,1652,1653],{},"Hosting cost",[1610,1655,1643],{},[1610,1657,1658],{},"Medium",[1592,1660,1661,1666,1669],{},[1610,1662,1663],{},[574,1664,1665],{},"Third-party SaaS",[1610,1667,1668],{},"Optional / Low",[1610,1670,1671],{},"Often Medium–High",[1592,1673,1674,1679,1681],{},[1610,1675,1676],{},[574,1677,1678],{},"Ongoing maintenance",[1610,1680,1658],{},[1610,1682,1620],{},[1592,1684,1685,1690,1693],{},[1610,1686,1687],{},[574,1688,1689],{},"Team skills required",[1610,1691,1692],{},"Shopware/PHP",[1610,1694,1695],{},"+ Vue/Nuxt/API",[1592,1697,1698,1703,1705],{},[1610,1699,1700],{},[574,1701,1702],{},"Design flexibility",[1610,1704,1658],{},[1610,1706,1707],{},"Very High",[1592,1709,1710,1715,1717],{},[1610,1711,1712],{},[574,1713,1714],{},"Scalability ceiling",[1610,1716,1658],{},[1610,1718,1620],{},[1592,1720,1721,1726,1728],{},[1610,1722,1723],{},[574,1724,1725],{},"Omnichannel readiness",[1610,1727,1643],{},[1610,1729,1620],{},[1731,1732,1733],"blockquote",{},[16,1734,1735],{},"Relative labels only — absolute costs depend heavily on project scope, team rates, and hosting choices.",[723,1737,1739],{"id":1738},"when-headless-actually-pays-off","When Headless Actually Pays Off",[16,1741,1742],{},"In my experience, headless earns its cost premium when:",[568,1744,1745,1751,1757,1763,1769,1775],{},[571,1746,1747,1750],{},[574,1748,1749],{},"Traffic is high and performance-sensitive"," — every millisecond matters at scale; a custom Nuxt frontend with edge delivery will outperform a traditional storefront under load.",[571,1752,1753,1756],{},[574,1754,1755],{},"You need differentiated UX"," — your brand story, configurators, animations, or checkout flow genuinely can't be expressed in stock Shopware templates.",[571,1758,1759,1762],{},[574,1760,1761],{},"Content is core to your brand"," — you need editorial control, landing page flexibility, and a proper CMS layer sitting alongside commerce.",[571,1764,1765,1768],{},[574,1766,1767],{},"Omnichannel is the goal"," — one Shopware backend serving web, mobile app, kiosk, or B2B portals simultaneously.",[571,1770,1771,1774],{},[574,1772,1773],{},"Multi-brand or multi-store architecture"," — a single backend, multiple frontend deployments sharing a component library is genuinely cheaper than maintaining N separate traditional storefronts.",[571,1776,1777,1780],{},[574,1778,1779],{},"You're hitting Twig theme limits"," — deeply custom logic crammed into templates becomes unmaintainable fast; a proper frontend separation is the right architectural fix.",[16,1782,1783,1784,1017],{},"For the broader architectural decision, see the ",[635,1785,1787],{"href":1786},"/blogs/headless-vs-traditional-shopware-2026","headless vs. traditional Shopware comparison",[723,1789,1791],{"id":1790},"when-headless-does-not-pay-off","When Headless Does NOT Pay Off",[16,1793,1794],{},"I always push back when a client proposes headless and the brief is essentially a standard catalog shop. Skip headless if:",[568,1796,1797,1803,1809,1815,1821],{},[571,1798,1799,1802],{},[574,1800,1801],{},"Small or standard catalog"," — if the default Shopware storefront covers your use case, the overhead isn't justified.",[571,1804,1805,1808],{},[574,1806,1807],{},"Tight budget or fast time-to-market"," — the build is longer and you'll burn runway before you ship.",[571,1810,1811,1814],{},[574,1812,1813],{},"Standard shop UX is fine"," — the Shopware 6.7 default storefront is now Vite-based with a modern DX; it's a genuinely good product that I'd recommend without hesitation for many projects.",[571,1816,1817,1820],{},[574,1818,1819],{},"Small team, no frontend specialists"," — you'll end up maintaining a codebase no one on your team fully understands, and that's a ticking clock.",[571,1822,1823,1826,1827,1830],{},[574,1824,1825],{},"You just want Shopware working well"," — sometimes the honest answer is ",[635,1828,1829],{"href":710},"performance optimization"," and careful Shopware configuration, not a full architecture change.",[723,1832,1834],{"id":1833},"how-to-keep-headless-costs-down","How to Keep Headless Costs Down",[16,1836,1837],{},"If headless is genuinely the right call, there are concrete ways to manage the spend:",[568,1839,1840,1850,1856,1862,1868],{},[571,1841,1842,1845,1846,1849],{},[574,1843,1844],{},"Start with Shopware Frontends instead of from scratch."," The official ",[635,1847,1848],{"href":1408},"Shopware Frontends composable storefront"," is a Vue/Nuxt starter with Shopware Store API integration pre-built. It's not a complete product, but it eliminates weeks of boilerplate — I reach for it as the starting point on headless builds.",[571,1851,1852,1855],{},[574,1853,1854],{},"Use managed hosting for the frontend."," Platforms like Netlify, Vercel, or Cloudflare Pages handle CDN, edge, and scaling without a devops team. Cost is predictable and lower than self-managed infrastructure.",[571,1857,1858,1861],{},[574,1859,1860],{},"Budget for upgrades explicitly, in writing, before launch."," The most expensive scenario I've seen is an unmaintained headless frontend that nobody wants to touch after 18 months. A maintenance retainer agreed upfront avoids that outcome.",[571,1863,1864,1867],{},[574,1865,1866],{},"Reuse your component library across brands."," If multi-brand is your reason for going headless, design the frontend architecture for reuse from day one — shared primitives, brand-level theme tokens, separate deployments.",[571,1869,1870,1873],{},[574,1871,1872],{},"Don't over-engineer the CMS layer."," Not every headless project needs Storyblok. Sometimes a simpler approach (Nuxt Content for editorial pages, Shopware CMS blocks for commerce pages) is enough and costs considerably less to operate.",[723,1875,1877],{"id":1876},"the-bottom-line","The Bottom Line",[16,1879,1880],{},"Headless Shopware is a legitimate architectural choice with real advantages — and real costs. The build is larger, the maintenance is ongoing, and the skills requirement is higher. None of that is a reason not to do it if the business case is solid.",[16,1882,1883],{},"The test I apply with clients: can you articulate a specific business outcome — conversion, brand differentiation, channel expansion, scale — that the headless architecture enables and that you genuinely can't achieve with a well-configured traditional storefront? If yes, the investment is defensible. If the answer is \"it's just more modern,\" I'd encourage you to revisit.",[16,1885,1886,1887,1889],{},"If you're working through that evaluation, I build both — traditional and headless Shopware projects. ",[635,1888,1340],{"href":1339}," and we can work through the tradeoffs specific to your shop.",[723,1891,1893],{"id":1892},"further-reading","Further Reading",[568,1895,1896,1902,1909,1917],{},[571,1897,1898,1901],{},[635,1899,1353],{"href":1351,"rel":1900},[639]," — official starter for headless Shopware builds",[571,1903,1904,1908],{},[635,1905,1361],{"href":1906,"rel":1907},"https://developer.shopware.com/docs/guides/integrations-api/store-api/",[639]," — the API layer a headless frontend consumes",[571,1910,1911,1916],{},[635,1912,1915],{"href":1913,"rel":1914},"https://developer.shopware.com/",[639],"Shopware 6.7 Release Notes"," — includes the new Vite-based default storefront",[571,1918,1919,1924],{},[635,1920,1923],{"href":1921,"rel":1922},"https://nuxt.com/",[639],"Nuxt Official Docs"," — the frontend framework most commonly paired with headless Shopware",{"title":37,"searchDepth":50,"depth":50,"links":1926},[1927,1935,1936,1937,1938,1939,1940],{"id":1438,"depth":50,"text":1439,"children":1928},[1929,1930,1931,1932,1933,1934],{"id":1442,"depth":77,"text":1443},{"id":1459,"depth":77,"text":1460},{"id":1473,"depth":77,"text":1474},{"id":1502,"depth":77,"text":1503},{"id":1539,"depth":77,"text":1540},{"id":1569,"depth":77,"text":1570},{"id":1583,"depth":50,"text":1584},{"id":1738,"depth":50,"text":1739},{"id":1790,"depth":50,"text":1791},{"id":1833,"depth":50,"text":1834},{"id":1876,"depth":50,"text":1877},{"id":1892,"depth":50,"text":1893},"Thinking about going headless with Shopware? An honest breakdown of what a headless storefront actually costs - build, hosting, maintenance - and when it pays off.",{"date":1943,"image":1944,"alt":1945,"tags":1946,"author":1407,"published":73,"featured":709},"02.06.2026","/blogs-img/headless-cost.png","What a headless Shopware store really costs in 2026",[522,1405,708,1947,1948],"cost","business","/blogs/headless-shopware-cost",{"title":1414,"description":1941},"blogs/11. headless-shopware-cost","HW7sjq-YylLB0Uu1nsKkocxK3KLm05aplXb9G0YpKlY",{"id":1954,"title":1955,"body":1956,"description":2362,"extension":701,"meta":2363,"navigation":73,"ogImage":2365,"path":1786,"seo":2370,"stem":2371,"__hash__":2372},"content/blogs/12. headless-vs-traditional-shopware-2026.md","Headless vs. Traditional Shopware - How to Choose in 2026",{"type":8,"value":1957,"toc":2348},[1958,1961,1965,1972,1980,1986,1990,2000,2007,2012,2016,2166,2170,2176,2179,2196,2203,2207,2210,2216,2222,2225,2229,2233,2253,2257,2274,2278,2281,2288,2299,2302,2305,2309,2312,2319,2321],[16,1959,1960],{},"This is the single most common architecture question I get — and too many teams answer it with hype instead of their own constraints. Every Shopware 6 project hits that fork early on: do you build on the built-in Twig storefront, or go headless with a separate Vue/Nuxt frontend? Both are legitimate production choices in 2026. Both can fail you if you pick the wrong one. This post is my concrete framework for making that call — a side-by-side comparison, the gotchas nobody warns you about, and my honest recommendation.",[723,1962,1964],{"id":1963},"what-traditional-means-in-2026","What \"Traditional\" Means in 2026",[16,1966,1967,1968,1971],{},"The Shopware 6 default storefront is server-rendered HTML generated by ",[574,1969,1970],{},"Twig templates",", styled via a theme system, and hydrated with Shopware's own storefront JavaScript. You get a fully functional shop out of the box, a mature plugin ecosystem, and a single application to deploy and maintain.",[16,1973,1974,1975,1979],{},"The part I want to call out for 2026: ",[635,1976,1978],{"href":1977},"/blogs/shopware-67-migration-guide","Shopware 6.7 overhauled the storefront build tooling, migrating to Vite",". I've worked with the old tooling enough to say this matters — the DX improvement is real, and build performance is noticeably better. The traditional path is no longer stuck with legacy tooling. For many teams, the gap between \"modern\" and \"headless\" just got narrower, and that changes the calculus more than most people realize.",[16,1981,1982,1985],{},[574,1983,1984],{},"In short:"," one codebase, fastest launch, plugin ecosystem works out of the box, SSR by default.",[723,1987,1989],{"id":1988},"what-headless-means-in-2026","What \"Headless\" Means in 2026",[16,1991,1992,1993,1995,1996,1999],{},"Going headless means the Shopware backend only serves your frontend through the ",[574,1994,738],{},". Your storefront — typically built with Nuxt and ",[635,1997,1998],{"href":1408},"Shopware Frontends"," — is a completely separate application. Shopware handles catalog, cart, checkout, and order management. Your Nuxt app handles every pixel the customer sees.",[16,2001,2002,2003,2006],{},"This is the architecture ",[635,2004,2005],{"href":751},"I described in depth in the headless game-changer post",": maximum flexibility, independently deployable frontends, and the ability to serve the same backend to a web app, mobile app, or any other channel simultaneously. I find it genuinely exciting — when the use case calls for it.",[16,2008,2009,2011],{},[574,2010,1984],{}," two codebases, higher build cost, higher performance ceiling, full design freedom.",[723,2013,2015],{"id":2014},"side-by-side-comparison","Side-by-Side Comparison",[1586,2017,2018,2031],{},[1589,2019,2020],{},[1592,2021,2022,2025,2028],{},[1595,2023,2024],{},"Dimension",[1595,2026,2027],{},"Traditional (Twig)",[1595,2029,2030],{},"Headless (Nuxt + Store API)",[1605,2032,2033,2046,2059,2072,2083,2095,2112,2125,2141,2153],{},[1592,2034,2035,2040,2043],{},[1610,2036,2037],{},[574,2038,2039],{},"Performance ceiling",[1610,2041,2042],{},"Medium — SSR is fast, but Shopware's storefront JS adds weight",[1610,2044,2045],{},"High — full control over every asset and rendering strategy",[1592,2047,2048,2053,2056],{},[1610,2049,2050],{},[574,2051,2052],{},"UX / customization",[1610,2054,2055],{},"Medium — theme system with limits",[1610,2057,2058],{},"High — arbitrary component architecture, any design",[1592,2060,2061,2066,2069],{},[1610,2062,2063],{},[574,2064,2065],{},"Time to market",[1610,2067,2068],{},"Fast — works out of the box",[1610,2070,2071],{},"Slow — custom frontend from scratch or Shopware Frontends scaffold",[1592,2073,2074,2079,2081],{},[1610,2075,2076],{},[574,2077,2078],{},"Build cost",[1610,2080,1617],{},[1610,2082,1646],{},[1592,2084,2085,2089,2092],{},[1610,2086,2087],{},[574,2088,1678],{},[1610,2090,2091],{},"Low — one codebase, one deployment",[1610,2093,2094],{},"Medium–High — two codebases, two dependency chains",[1592,2096,2097,2102,2105],{},[1610,2098,2099],{},[574,2100,2101],{},"SEO control",[1610,2103,2104],{},"High — SSR by default",[1610,2106,2107,2108,2111],{},"High — ",[1421,2109,2110],{},"if"," SSR/SSG is implemented correctly (Nuxt does this well)",[1592,2113,2114,2119,2122],{},[1610,2115,2116],{},[574,2117,2118],{},"Omnichannel / multi-brand",[1610,2120,2121],{},"Low — storefront is tightly coupled",[1610,2123,2124],{},"High — same API feeds any number of frontends",[1592,2126,2127,2132,2135],{},[1610,2128,2129],{},[574,2130,2131],{},"Plugin ecosystem",[1610,2133,2134],{},"High — storefront + admin plugins both work",[1610,2136,2137,2138],{},"Partial — admin plugins work; storefront plugins do ",[574,2139,2140],{},"not",[1592,2142,2143,2147,2150],{},[1610,2144,2145],{},[574,2146,1689],{},[1610,2148,2149],{},"PHP + Twig + some JS",[1610,2151,2152],{},"Vue/Nuxt + TypeScript, plus Shopware Store API knowledge",[1592,2154,2155,2160,2163],{},[1610,2156,2157],{},[574,2158,2159],{},"Hosting / ops complexity",[1610,2161,2162],{},"Low — single app",[1610,2164,2165],{},"Higher — Nuxt app needs separate hosting (Node, edge, or static CDN)",[723,2167,2169],{"id":2168},"the-plugin-gotcha-nobody-warns-you-about","The Plugin Gotcha Nobody Warns You About",[16,2171,2172,2173,1017],{},"I'll be honest — this is the one that surprises clients most. It's caught more than one team off-guard mid-migration: ",[574,2174,2175],{},"not all plugins survive the switch to headless",[16,2177,2178],{},"Shopware plugins have two layers:",[568,2180,2181,2187],{},[571,2182,2183,2186],{},[574,2184,2185],{},"Admin/backend extensions"," — these run entirely within the Shopware admin app. They are unaffected by your frontend choice. Payment providers, ERP connectors, and admin dashboards all continue to work.",[571,2188,2189,2192,2193,1017],{},[574,2190,2191],{},"Storefront plugins"," — these extend the Twig templates or inject storefront JavaScript. In a headless setup, your Twig storefront simply does not exist. Any behavior those plugins provided must be ",[574,2194,2195],{},"re-implemented in your Nuxt app",[16,2197,2198,2199,2202],{},"Before committing to headless, audit your plugin list. For every storefront-layer plugin you depend on, budget time to replicate that functionality in Vue. On a plugin-heavy store, this cost can surprise you significantly — make sure it's part of the ",[635,2200,2201],{"href":1949},"total headless cost calculation",". I've seen teams blow past their initial budget estimates precisely because they counted plugins too optimistically.",[723,2204,2206],{"id":2205},"seo-busting-the-myth","SEO: Busting the Myth",[16,2208,2209],{},"I've heard both sides of this one — \"headless is better for SEO,\" \"headless will kill your rankings.\" My experience: neither is universally true.",[16,2211,2212,2215],{},[574,2213,2214],{},"Traditional"," is SSR by default — crawlers get complete HTML immediately. Done.",[16,2217,2218,2221],{},[574,2219,2220],{},"Headless"," is only as good as its rendering strategy. A Nuxt app with proper SSR or SSG behaves identically for crawlers — and Nuxt makes this easy. The risk is when teams skip SSR and ship a client-rendered SPA. A misconfigured headless setup will hurt SEO; a properly configured one is indistinguishable from traditional.",[16,2223,2224],{},"My honest answer: SEO is an execution question, not an architecture question. Both can rank well. Traditional is lower risk if your team has no Nuxt experience. I've seen headless setups with excellent SEO and traditional shops with avoidable crawlability problems — the architecture was never the deciding factor.",[723,2226,2228],{"id":2227},"decision-checklist","Decision Checklist",[11,2230,2232],{"id":2231},"choose-headless-if-you-tick-3-or-more-of-these","Choose headless if you tick 3 or more of these:",[568,2234,2235,2238,2241,2244,2247,2250],{},[571,2236,2237],{},"You need top-tier Core Web Vitals / performance and have tight CWV targets",[571,2239,2240],{},"You want a fully custom UX that the Twig theme system cannot deliver",[571,2242,2243],{},"Your store is content-heavy or CMS-driven, requiring flexible page composition",[571,2245,2246],{},"You need omnichannel — web, mobile app, or multiple brands from one backend",[571,2248,2249],{},"You have (or will hire) developers with Vue/Nuxt skills",[571,2251,2252],{},"You have the budget to build and maintain two codebases long-term",[11,2254,2256],{"id":2255},"choose-traditional-if","Choose traditional if:",[568,2258,2259,2262,2265,2268,2271],{},[571,2260,2261],{},"Standard e-commerce UX is acceptable for your brand",[571,2263,2264],{},"You have a smaller budget or a lean team",[571,2266,2267],{},"Fast time to market is the priority",[571,2269,2270],{},"You rely heavily on third-party storefront plugins",[571,2272,2273],{},"You are selling on a single web channel with no multi-frontend plans",[723,2275,2277],{"id":2276},"my-recommendation","My Recommendation",[16,2279,2280],{},"Nine times out of ten I steer people to the default storefront — and here's why.",[16,2282,2283,2284,2287],{},"For the ",[574,2285,2286],{},"majority of Shopware projects",", the modernized default storefront is the right choice. It ships faster, costs less to maintain, and — especially with the Vite build system in 6.7 — it produces a capable, performant storefront without reinventing the wheel.",[16,2289,2290,2291,2294,2295,2298],{},"Go headless when you have a ",[574,2292,2293],{},"concrete, unavoidable reason",": a performance target the Twig storefront can't reach, a design brief that breaks every template constraint, or a confirmed need for multiple frontends. And only when you also have the ",[574,2296,2297],{},"budget and team"," to sustain a second codebase.",[16,2300,2301],{},"The stores that regret going headless almost always did it for one reason: everyone else was doing it. That is not a requirement. Don't architect for hype.",[16,2303,2304],{},"If the concrete reasons are there — if after the checklist above you're ticking 3+ boxes — then headless with Nuxt and Shopware Frontends is genuinely excellent. It's just not the default right answer.",[723,2306,2308],{"id":2307},"wrapping-up","Wrapping Up",[16,2310,2311],{},"The Shopware 6 stack is mature enough in 2026 that both paths are solid. Traditional gives you speed, ecosystem, and low ops overhead. Headless gives you flexibility, performance ceiling, and omnichannel capability — at real cost.",[16,2313,2314,2315,2318],{},"Run through the checklist honestly. If you're unsure which architecture fits your project, or you need someone to audit your current setup and plugin dependencies before making the call, that's exactly the kind of engagement I help with. ",[635,2316,2317],{"href":1339},"Reach out"," and we can make the decision together before the first line of code is written.",[723,2320,1344],{"id":1343},[568,2322,2323,2329,2334,2341],{},[571,2324,2325],{},[635,2326,2328],{"href":1359,"rel":2327},[639],"Shopware Store API documentation",[571,2330,2331],{},[635,2332,1998],{"href":1351,"rel":2333},[639],[571,2335,2336],{},[635,2337,2340],{"href":2338,"rel":2339},"https://nuxt.com/docs",[639],"Nuxt documentation",[571,2342,2343],{},[635,2344,2347],{"href":2345,"rel":2346},"https://web.dev/explore/vitals",[639],"Core Web Vitals — web.dev",{"title":37,"searchDepth":50,"depth":50,"links":2349},[2350,2351,2352,2353,2354,2355,2359,2360,2361],{"id":1963,"depth":50,"text":1964},{"id":1988,"depth":50,"text":1989},{"id":2014,"depth":50,"text":2015},{"id":2168,"depth":50,"text":2169},{"id":2205,"depth":50,"text":2206},{"id":2227,"depth":50,"text":2228,"children":2356},[2357,2358],{"id":2231,"depth":77,"text":2232},{"id":2255,"depth":77,"text":2256},{"id":2276,"depth":50,"text":2277},{"id":2307,"depth":50,"text":2308},{"id":1343,"depth":50,"text":1344},"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.",{"date":2364,"image":2365,"alt":2366,"tags":2367,"author":1407,"published":73,"featured":709},"30.05.2026","/blogs-img/headless-vs-traditional.png","Headless vs traditional Shopware 6 decision framework 2026",[522,1405,2368,708,2369],"architecture","decision",{"title":1955,"description":2362},"blogs/12. headless-vs-traditional-shopware-2026","InsrGd_4nCMw5nXm9MPxbjZtZD-eiMzSRwSxZT8mEk8",{"id":2374,"title":2375,"body":2376,"description":3081,"extension":701,"meta":3082,"navigation":73,"ogImage":3084,"path":3091,"seo":3092,"stem":3093,"__hash__":3094},"content/blogs/13. supply-chain-security-2026.md","The Supply Chain Is the New Attack Surface - npm Worms and Slopsquatting in 2026",{"type":8,"value":2377,"toc":3065},[2378,2384,2387,2391,2394,2400,2404,2407,2410,2429,2436,2439,2445,2449,2455,2481,2484,2491,2495,2498,2502,2505,2549,2570,2650,2654,2663,2717,2720,2724,2727,2753,2786,2790,2803,2810,2814,2828,2831,2918,2925,2929,2932,2939,2943,2946,3005,3009,3015,3018,3024,3026,3062],[16,2379,2380,2381,1017],{},"A while back I was doing a routine code review before a deploy — the kind where you skim the lockfile diff and look for anything obviously out of place. There it was: a package I'd never heard of, added as a transitive dependency, with a publish timestamp from two days earlier. New package, zero prior versions, no GitHub link in the manifest. My gut said something was off. I didn't install it; I rolled back the upstream dependency instead and spent the next hour reading about how supply-chain attacks actually propagate. That afternoon changed how I think about every ",[27,2382,2383],{},"npm install",[16,2385,2386],{},"The supply chain has always been a weak spot. But 2025–2026 brought two threats that are qualitatively different from the typosquats and protestware of previous years. One is a self-replicating worm that turns compromised developer accounts into its own distribution network. The other is a class of attack that didn't even exist before LLMs — and it gets worse every time someone adds AI to their dev workflow.",[723,2388,2390],{"id":2389},"why-the-supply-chain-why-now","Why the Supply Chain, Why Now",[16,2392,2393],{},"Traditional malware requires a human attacker to manually publish a package and hope someone installs it. What changed is automation: attackers now write code that spreads itself, and AI assistants generate install commands for packages that were never real to begin with.",[16,2395,2396,2397,2399],{},"Both threats target the same choke point: the moment you run ",[27,2398,2383],{},". By then it's already too late if you haven't done your homework up front.",[723,2401,2403],{"id":2402},"threat-1-the-self-replicating-worm-shai-hulud","Threat 1: The Self-Replicating Worm (Shai-Hulud)",[16,2405,2406],{},"In September 2025 a worm called Shai-Hulud hit the npm ecosystem. CISA issued a public alert on September 23, 2025. The mechanism is elegant in the worst way: steal an npm publish token from a compromised developer machine or CI environment, enumerate every package that token has write access to, inject malicious code into those packages, and republish. The initial wave compromised over 500 packages — not because the attacker manually touched each one, but because each compromised account's tokens unlocked the next set of packages.",[16,2408,2409],{},"Once inside a developer's environment it scans for secrets: GitHub tokens, AWS/GCP/Azure credentials, npm tokens themselves. Everything it finds gets exfiltrated to attacker-controlled GitHub repositories. The worm part is that stolen publish tokens from victim A become the vector for attacking everyone who depends on packages victim A maintains. No human attacker needed per hop. I find this genuinely unsettling — it's the supply chain attacking itself.",[16,2411,2412,2413,2416,2417,2420,2421,2424,2425,2428],{},"Version 2.0 arrived in November 2025 and I'd call it the more dangerous one. It moved execution from ",[27,2414,2415],{},"postinstall"," to ",[27,2418,2419],{},"npm PREinstall"," — meaning it runs before anything else touches your project. It dropped a heavily obfuscated payload (look for ",[27,2422,2423],{},"setup_bun.js"," morphed into ",[27,2426,2427],{},"bun_environment.js"," in affected versions), added a data-wiping capability, and ultimately affected tens of thousands of GitHub repos — around 25,000+ malicious repositories across ~350 users.",[16,2430,2431,2432,2435],{},"Then came what I'd consider the most significant escalation: Mini Shai-Hulud, released May 11, 2026. Over 170 npm packages and 2 PyPI packages across 404 malicious versions — the first supply-chain attack to simultaneously span npm ",[1421,2433,2434],{},"and"," PyPI in a single coordinated operation. The threat actor \"TeamPCP\" exploited a pull-request workflow misconfiguration in TanStack's GitHub Actions CI to inject it. By mid-May they'd released the worm's source code publicly, which predictably spawned clones.",[16,2437,2438],{},"By June 2026, variants named Miasma and Hades were active. As of June 5, at least 57 npm packages and 300+ malicious versions were tied to Miasma alone. Even Red Hat-maintained npm packages with ~80,000 downloads per week were hit. At this point this isn't a niche threat — it's hitting mainstream, well-maintained packages.",[16,2440,2441,2444],{},[574,2442,2443],{},"The core lesson:"," the worm spreads on stolen publish tokens. That's the choke point you can actually control.",[723,2446,2448],{"id":2447},"threat-2-slopsquatting","Threat 2: Slopsquatting",[16,2450,2451,2452,2454],{},"\"Slopsquatting\" was coined by security researcher Seth Larson. \"Slop\" = sloppy AI output. The mechanism: you ask an AI coding assistant to help with something, the model references a package that doesn't exist, an attacker has already registered that exact name on the public registry, and you — or an automated agent — runs ",[27,2453,2383],{}," and executes malicious code.",[16,2456,2457,2458,2461,2462,2465,2466,2469,2470,2473,2474,2465,2477,2480],{},"I've caught this myself. You're vibe-coding, the model suggests ",[27,2459,2460],{},"react-codeshift"," (a January 2026 hallucination — it mashed together ",[27,2463,2464],{},"jscodeshift"," and ",[27,2467,2468],{},"react-codemod","), and if you don't check, you're just running whatever was registered under that name. A confirmed malicious slopsquat called ",[27,2471,2472],{},"unused-imports"," ran post-install scripts to steal credentials and API keys. Attackers are systematically registering plausible-sounding names like ",[27,2475,2476],{},"aws-helper-sdk",[27,2478,2479],{},"fastapi-middleware"," specifically to catch this.",[16,2482,2483],{},"A USENIX Security 2025 study analyzed 576,000 AI-generated code samples across 16 LLMs. About 20% referenced Python or JavaScript packages that don't exist. More concerning: 43% of hallucinated package names recurred consistently across similar prompts, and 58% reappeared within 10 runs of the same query. Open-source models hallucinated package names at ~21.7% on average; commercial models at ~5.2%; some CodeLlama configurations exceeded 33%. GPT-4 Turbo was the lowest at 3.59% — still far from zero.",[16,2485,2486,2487,2490],{},"The autonomous AI coding agent angle makes this categorically worse. When an agent resolves and installs dependencies programmatically with no human glancing at the names — which is exactly what I'm seeing in the more advanced AI-assisted dev setups I build for clients, like the ",[635,2488,2489],{"href":1276},"AI-driven product search pipelines I wrote about"," — there's no moment of \"wait, is this real?\" Slopsquatting is now considered a top-three supply-chain threat against AI-driven development.",[723,2492,2494],{"id":2493},"how-i-lock-my-projects-down","How I Lock My Projects Down",[16,2496,2497],{},"This is the playbook I've settled on. I use most of these on every project I set up now; a few are CI-specific.",[11,2499,2501],{"id":2500},"disable-install-scripts-by-default","Disable install scripts by default",[16,2503,2504],{},"The worm's main execution vector is lifecycle scripts. My first line of defense:",[32,2506,2508],{"className":356,"code":2507,"language":358,"meta":37,"style":37},"# Set globally on your machine\nnpm config set ignore-scripts true\n\n# In CI — always\nnpm ci --ignore-scripts\n",[27,2509,2510,2515,2530,2534,2539],{"__ignoreMap":37},[41,2511,2512],{"class":43,"line":44},[41,2513,2514],{"class":442},"# Set globally on your machine\n",[41,2516,2517,2519,2522,2525,2528],{"class":43,"line":50},[41,2518,875],{"class":365},[41,2520,2521],{"class":368}," config",[41,2523,2524],{"class":368}," set",[41,2526,2527],{"class":368}," ignore-scripts",[41,2529,480],{"class":378},[41,2531,2532],{"class":43,"line":77},[41,2533,74],{"emptyLinePlaceholder":73},[41,2535,2536],{"class":43,"line":83},[41,2537,2538],{"class":442},"# In CI — always\n",[41,2540,2541,2543,2546],{"class":43,"line":89},[41,2542,875],{"class":365},[41,2544,2545],{"class":368}," ci",[41,2547,2548],{"class":378}," --ignore-scripts\n",[16,2550,2551,2552,2555,2556,2559,2560,786,2563,786,2566,2569],{},"If you use pnpm, you get this by default — lifecycle scripts are blocked unless a package is explicitly listed in ",[27,2553,2554],{},"onlyBuiltDependencies"," in your ",[27,2557,2558],{},"package.json",". That allowlist discipline is the right mental model even if you're on npm. I now maintain a short list of packages I know genuinely need build scripts (",[27,2561,2562],{},"esbuild",[27,2564,2565],{},"sharp",[27,2567,2568],{},"@swc/core",") and everything else gets none.",[32,2571,2575],{"className":2572,"code":2573,"language":2574,"meta":37,"style":37},"language-json shiki shiki-themes dracula","// pnpm in package.json\n{\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\"esbuild\", \"sharp\", \"@swc/core\"]\n  }\n}\n","json",[27,2576,2577,2582,2586,2602,2641,2646],{"__ignoreMap":37},[41,2578,2579],{"class":43,"line":44},[41,2580,2581],{"class":442},"// pnpm in package.json\n",[41,2583,2584],{"class":43,"line":50},[41,2585,241],{"class":942},[41,2587,2588,2592,2595,2597,2599],{"class":43,"line":77},[41,2589,2591],{"class":2590},"sY8FZ","  \"",[41,2593,2594],{"class":448},"pnpm",[41,2596,1118],{"class":2590},[41,2598,389],{"class":452},[41,2600,2601],{"class":942}," {\n",[41,2603,2604,2607,2609,2611,2613,2616,2618,2620,2622,2624,2626,2628,2630,2632,2634,2636,2638],{"class":43,"line":83},[41,2605,2606],{"class":2590},"    \"",[41,2608,2554],{"class":448},[41,2610,1118],{"class":2590},[41,2612,389],{"class":452},[41,2614,2615],{"class":942}," [",[41,2617,1118],{"class":541},[41,2619,2562],{"class":368},[41,2621,1118],{"class":541},[41,2623,786],{"class":942},[41,2625,1118],{"class":541},[41,2627,2565],{"class":368},[41,2629,1118],{"class":541},[41,2631,786],{"class":942},[41,2633,1118],{"class":541},[41,2635,2568],{"class":368},[41,2637,1118],{"class":541},[41,2639,2640],{"class":942},"]\n",[41,2642,2643],{"class":43,"line":89},[41,2644,2645],{"class":942},"  }\n",[41,2647,2648],{"class":43,"line":95},[41,2649,135],{"class":942},[11,2651,2653],{"id":2652},"pin-everything-and-add-an-update-cooldown","Pin everything and add an update cooldown",[16,2655,2656,2658,2659,2662],{},[27,2657,2383],{}," in CI is an antipattern. ",[27,2660,2661],{},"npm ci"," from a committed lockfile is the only acceptable option. I also never auto-merge dependency updates the same day they appear — Shai-Hulud releases get yanked within days once reported, but only if you haven't already pulled the malicious version. Renovate's stability window config:",[32,2664,2666],{"className":2572,"code":2665,"language":2574,"meta":37,"style":37},"// renovate.json\n{\n  \"stabilityDays\": 3,\n  \"prCreation\": \"not-pending\"\n}\n",[27,2667,2668,2673,2677,2693,2713],{"__ignoreMap":37},[41,2669,2670],{"class":43,"line":44},[41,2671,2672],{"class":442},"// renovate.json\n",[41,2674,2675],{"class":43,"line":50},[41,2676,241],{"class":942},[41,2678,2679,2681,2684,2686,2688,2691],{"class":43,"line":77},[41,2680,2591],{"class":2590},[41,2682,2683],{"class":448},"stabilityDays",[41,2685,1118],{"class":2590},[41,2687,389],{"class":452},[41,2689,2690],{"class":378}," 3",[41,2692,982],{"class":942},[41,2694,2695,2697,2700,2702,2704,2707,2710],{"class":43,"line":83},[41,2696,2591],{"class":2590},[41,2698,2699],{"class":448},"prCreation",[41,2701,1118],{"class":2590},[41,2703,389],{"class":452},[41,2705,2706],{"class":541}," \"",[41,2708,2709],{"class":368},"not-pending",[41,2711,2712],{"class":541},"\"\n",[41,2714,2715],{"class":43,"line":89},[41,2716,135],{"class":942},[16,2718,2719],{},"Three days of \"cooldown\" before a version is even considered. Small friction, meaningful safety net.",[11,2721,2723],{"id":2722},"lock-down-your-publish-tokens","Lock down your publish tokens",[16,2725,2726],{},"The worm spreads on stolen tokens. My hygiene:",[568,2728,2729,2740,2747,2750],{},[571,2730,2731,2732,2735,2736,2739],{},"Use granular, scoped npm tokens with least-privilege (",[27,2733,2734],{},"automation"," type for CI, not the ",[27,2737,2738],{},"publish"," token from your personal account)",[571,2741,2742,2743,2746],{},"Require 2FA for publishing — both on your account and via ",[27,2744,2745],{},"npm access"," settings on the package",[571,2748,2749],{},"Prefer trusted publishing / OIDC-based tokens over long-lived static tokens wherever the registry supports it",[571,2751,2752],{},"Rotate tokens after any suspected compromise, obviously, but also on a schedule",[32,2754,2756],{"className":356,"code":2755,"language":358,"meta":37,"style":37},"# Create a least-privilege automation token (CI only, no 2FA bypass for publishing)\nnpm token create --type=automation --cidr-whitelist=\u003Cyour-ci-ip-range>\n",[27,2757,2758,2763],{"__ignoreMap":37},[41,2759,2760],{"class":43,"line":44},[41,2761,2762],{"class":442},"# Create a least-privilege automation token (CI only, no 2FA bypass for publishing)\n",[41,2764,2765,2767,2770,2773,2776,2779,2781,2784],{"class":43,"line":50},[41,2766,875],{"class":365},[41,2768,2769],{"class":368}," token",[41,2771,2772],{"class":368}," create",[41,2774,2775],{"class":378}," --type=automation",[41,2777,2778],{"class":378}," --cidr-whitelist=",[41,2780,1102],{"class":452},[41,2782,2783],{"class":378},"your-ci-ip-range",[41,2785,1125],{"class":452},[11,2787,2789],{"id":2788},"scan-and-maintain-an-sbom","Scan and maintain an SBOM",[16,2791,2792,2793,2798,2799,2802],{},"I run ",[635,2794,2797],{"href":2795,"rel":2796},"https://socket.dev",[639],"socket.dev"," on the projects where I control the CI. It catches behavioral signals — packages that started doing unusual things in a new version — not just known CVEs. ",[27,2800,2801],{},"npm audit"," is table stakes but it's backward-looking; Socket is more proactive. I also maintain a CycloneDX SBOM for larger client projects so that when something like Miasma gets flagged, I can check my exposure in minutes rather than hours.",[16,2804,1485,2805,2809],{},[635,2806,2808],{"href":2807},"/blogs/quality-tools-that-matter","quality tools I already have in my standard CI setup"," make this relatively painless to add — it's another scan step, not a new workflow.",[11,2811,2813],{"id":2812},"harden-your-ci-pipeline","Harden your CI pipeline",[16,2815,2816,2817,2820,2821,2823,2824,2827],{},"Mini Shai-Hulud spread via a misconfigured GitHub Actions ",[27,2818,2819],{},"pull_request_target"," workflow. This is a well-known footgun: ",[27,2822,2819],{}," runs in the context of the ",[1421,2825,2826],{},"base"," branch and has access to secrets, even when triggered by a fork PR. I've audited every workflow I maintain for this.",[16,2829,2830],{},"My CI baseline now requires:",[32,2832,2834],{"className":433,"code":2833,"language":435,"meta":37,"style":37},"# Pin every action to a commit SHA, not a tag\n- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2\n\n# Explicit least-privilege permissions block at the job level\npermissions:\n  contents: read\n  pull-requests: read\n\n# Never expose secrets to fork PRs\non:\n  pull_request:  # NOT pull_request_target unless you've thought it through\n",[27,2835,2836,2841,2857,2861,2866,2873,2883,2892,2896,2901,2908],{"__ignoreMap":37},[41,2837,2838],{"class":43,"line":44},[41,2839,2840],{"class":442},"# Pin every action to a commit SHA, not a tag\n",[41,2842,2843,2846,2849,2851,2854],{"class":43,"line":50},[41,2844,2845],{"class":452},"-",[41,2847,2848],{"class":448}," uses",[41,2850,389],{"class":452},[41,2852,2853],{"class":368}," actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",[41,2855,2856],{"class":442}," # v4.2.2\n",[41,2858,2859],{"class":43,"line":77},[41,2860,74],{"emptyLinePlaceholder":73},[41,2862,2863],{"class":43,"line":83},[41,2864,2865],{"class":442},"# Explicit least-privilege permissions block at the job level\n",[41,2867,2868,2871],{"class":43,"line":89},[41,2869,2870],{"class":448},"permissions",[41,2872,453],{"class":452},[41,2874,2875,2878,2880],{"class":43,"line":95},[41,2876,2877],{"class":448},"  contents",[41,2879,389],{"class":452},[41,2881,2882],{"class":368}," read\n",[41,2884,2885,2888,2890],{"class":43,"line":142},[41,2886,2887],{"class":448},"  pull-requests",[41,2889,389],{"class":452},[41,2891,2882],{"class":368},[41,2893,2894],{"class":43,"line":148},[41,2895,74],{"emptyLinePlaceholder":73},[41,2897,2898],{"class":43,"line":154},[41,2899,2900],{"class":442},"# Never expose secrets to fork PRs\n",[41,2902,2903,2906],{"class":43,"line":1243},[41,2904,2905],{"class":378},"on",[41,2907,453],{"class":452},[41,2909,2910,2913,2915],{"class":43,"line":1249},[41,2911,2912],{"class":448},"  pull_request",[41,2914,389],{"class":452},[41,2916,2917],{"class":442},"  # NOT pull_request_target unless you've thought it through\n",[16,2919,2920,2921,2924],{},"If you want a deeper dive on the Node/npm stack context these workflows sit in, my ",[635,2922,2923],{"href":907},"Nuxt 4 migration post"," covers the toolchain setup I use as a baseline.",[11,2926,2928],{"id":2927},"vet-every-ai-suggested-dependency","Vet every AI-suggested dependency",[16,2930,2931],{},"My rule: any package an AI assistant recommends gets the same ten-second check before I add it. Does it actually exist on the registry? Who published it, and when? What are its weekly downloads? If a model suggests something with zero downloads and a registration date from last week, that's a hard no until proven otherwise. I treat AI-suggested package names as unverified user input — because that's exactly what they are.",[16,2933,2934,2935,2938],{},"For any autonomous agent flow I build, I add an explicit allowlist gate: the agent can ",[1421,2936,2937],{},"suggest"," packages but cannot install them without a human-verified allowlist approval. No exceptions.",[723,2940,2942],{"id":2941},"my-pre-merge-checklist","My Pre-Merge Checklist",[16,2944,2945],{},"Before anything ships:",[568,2947,2950,2963,2969,2975,2981,2991,2997],{"className":2948},[2949],"contains-task-list",[571,2951,2954,2958,2959,2962],{"className":2952},[2953],"task-list-item",[2955,2956],"input",{"disabled":73,"type":2957},"checkbox"," ",[27,2960,2961],{},"npm ci --ignore-scripts"," passes clean in CI",[571,2964,2966,2968],{"className":2965},[2953],[2955,2967],{"disabled":73,"type":2957}," Lockfile diff reviewed — no packages I didn't explicitly add",[571,2970,2972,2974],{"className":2971},[2953],[2955,2973],{"disabled":73,"type":2957}," New dependencies checked: age, download count, publisher reputation",[571,2976,2978,2980],{"className":2977},[2953],[2955,2979],{"disabled":73,"type":2957}," No AI-suggested package names added without manual registry verification",[571,2982,2984,2986,2987,2990],{"className":2983},[2953],[2955,2985],{"disabled":73,"type":2957}," GitHub Actions using SHA-pinned actions + explicit ",[27,2988,2989],{},"permissions:"," blocks",[571,2992,2994,2996],{"className":2993},[2953],[2955,2995],{"disabled":73,"type":2957}," No long-lived npm tokens committed or exposed in CI logs",[571,2998,3000,2958,3002,3004],{"className":2999},[2953],[2955,3001],{"disabled":73,"type":2957},[27,3003,2801],{}," / socket.dev scan green (or exceptions explicitly acknowledged)",[723,3006,3008],{"id":3007},"closing-thoughts","Closing Thoughts",[16,3010,3011,3012,3014],{},"I'll be honest — supply-chain security felt like background noise to me a few years ago. CVE scanners, dependency updates, standard stuff. What changed my perspective was realizing these attacks don't require you to do anything wrong. You can run ",[27,3013,2383],{}," on a package you've used for years, and if its maintainer's token was stolen last Tuesday, you're now compromised.",[16,3016,3017],{},"The defenses aren't complicated. Most of them are config changes and workflow habits, not new tools. What they require is treating the supply chain with the same skepticism you'd apply to any external input — because that's all it is.",[16,3019,3020,3021,1017],{},"If you want to talk through hardening your CI pipeline or auditing your current npm setup, ",[635,3022,3023],{"href":1339},"I'm available for consulting work",[723,3025,1344],{"id":1343},[568,3027,3028,3035,3042,3049,3056],{},[571,3029,3030],{},[635,3031,3034],{"href":3032,"rel":3033},"https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem",[639],"CISA Alert AA25-266A — Widespread Supply Chain Compromise Impacting npm Ecosystem",[571,3036,3037],{},[635,3038,3041],{"href":3039,"rel":3040},"https://www.microsoft.com/en-us/security/blog/",[639],"Microsoft Security Blog — Shai-Hulud 2.0 Guidance",[571,3043,3044],{},[635,3045,3048],{"href":3046,"rel":3047},"https://sethmlarson.dev",[639],"Seth Larson — Slopsquatting research and OWASP Top 10 for LLM Applications",[571,3050,3051],{},[635,3052,3055],{"href":3053,"rel":3054},"https://docs.npmjs.com/about-access-tokens",[639],"npm Docs — Tokens and 2FA for Publishing",[571,3057,3058],{},[635,3059,3061],{"href":2795,"rel":3060},[639],"Socket.dev — Supply Chain Security Scanner",[680,3063,3064],{},"html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}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 .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sY8FZ, html code.shiki .sY8FZ{--shiki-default:#8BE9FE}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}",{"title":37,"searchDepth":50,"depth":50,"links":3066},[3067,3068,3069,3070,3078,3079,3080],{"id":2389,"depth":50,"text":2390},{"id":2402,"depth":50,"text":2403},{"id":2447,"depth":50,"text":2448},{"id":2493,"depth":50,"text":2494,"children":3071},[3072,3073,3074,3075,3076,3077],{"id":2500,"depth":77,"text":2501},{"id":2652,"depth":77,"text":2653},{"id":2722,"depth":77,"text":2723},{"id":2788,"depth":77,"text":2789},{"id":2812,"depth":77,"text":2813},{"id":2927,"depth":77,"text":2928},{"id":2941,"depth":50,"text":2942},{"id":3007,"depth":50,"text":3008},{"id":1343,"depth":50,"text":1344},"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.",{"date":3083,"image":3084,"alt":3085,"tags":3086,"author":1407,"published":73,"featured":73},"11.06.2026","/blogs-img/supply-chain-security.png","npm supply chain worms and slopsquatting - 2026 developer security threats",[3087,3088,875,3089,3090],"security","supply-chain","devops","ai","/blogs/supply-chain-security-2026",{"title":2375,"description":3081},"blogs/13. supply-chain-security-2026","mcXX_ZPRElD9zoioyyLFI1J1hO0MdYROodVX5rfWPVs",{"id":3096,"title":3097,"body":3098,"description":4811,"extension":701,"meta":4812,"navigation":73,"ogImage":4814,"path":4820,"seo":4821,"stem":4822,"__hash__":4823},"content/blogs/2. clean-code-summary-robert-martin.md","Summary of Clean Code by Robert C. Martin - Essential Guidelines for Better Software",{"type":8,"value":3099,"toc":4796},[3100,3102,3105,3108,3112,3139,3143,3148,3271,3276,3498,3503,3517,3521,3556,3560,3689,3694,3714,3718,4111,4116,4136,4140,4261,4266,4283,4287,4330,4334,4490,4495,4514,4518,4689,4694,4711,4715,4753,4757,4762,4782,4784,4787,4790,4793],[11,3101,14],{"id":13},[16,3103,3104],{},"Code is clean if it can be understood easily – by everyone on the team. Clean code can be read and enhanced by a developer other than its original author. With understandability comes readability, changeability, extensibility and maintainability.",[16,3106,3107],{},"Robert C. Martin's \"Clean Code\" is a fundamental guide for writing maintainable, readable, and professional code. Here's a comprehensive summary of the key principles and practices.",[11,3109,3111],{"id":3110},"general-rules","General Rules",[3113,3114,3115,3121,3127,3133],"ol",{},[571,3116,3117,3120],{},[574,3118,3119],{},"Follow standard conventions"," - Consistency is key across your codebase",[571,3122,3123,3126],{},[574,3124,3125],{},"Keep it simple stupid (KISS)"," - Simpler is always better. Reduce complexity as much as possible",[571,3128,3129,3132],{},[574,3130,3131],{},"Boy scout rule"," - Leave the campground cleaner than you found it",[571,3134,3135,3138],{},[574,3136,3137],{},"Always find root cause"," - Always look for the root cause of a problem",[11,3140,3142],{"id":3141},"design-rules","Design Rules",[16,3144,3145],{},[574,3146,3147],{},"Keep configurable data at high levels",[32,3149,3151],{"className":275,"code":3150,"language":277,"meta":37,"style":37},"// Good: Configuration at the top level\nconst CONFIG = {\n  API_URL: 'https://api.example.com',\n  TIMEOUT: 5000,\n}\n\nclass ApiService {\n  constructor(config = CONFIG) {\n    this.apiUrl = config.API_URL\n    this.timeout = config.TIMEOUT\n  }\n}\n",[27,3152,3153,3158,3169,3185,3197,3201,3205,3215,3232,3249,3263,3267],{"__ignoreMap":37},[41,3154,3155],{"class":43,"line":44},[41,3156,3157],{"class":442},"// Good: Configuration at the top level\n",[41,3159,3160,3162,3165,3167],{"class":43,"line":50},[41,3161,1130],{"class":452},[41,3163,3164],{"class":942}," CONFIG ",[41,3166,1115],{"class":452},[41,3168,2601],{"class":942},[41,3170,3171,3174,3176,3178,3181,3183],{"class":43,"line":77},[41,3172,3173],{"class":942},"  API_URL",[41,3175,389],{"class":452},[41,3177,542],{"class":541},[41,3179,3180],{"class":368},"https://api.example.com",[41,3182,964],{"class":541},[41,3184,982],{"class":942},[41,3186,3187,3190,3192,3195],{"class":43,"line":83},[41,3188,3189],{"class":942},"  TIMEOUT",[41,3191,389],{"class":452},[41,3193,3194],{"class":378}," 5000",[41,3196,982],{"class":942},[41,3198,3199],{"class":43,"line":89},[41,3200,135],{"class":942},[41,3202,3203],{"class":43,"line":95},[41,3204,74],{"emptyLinePlaceholder":73},[41,3206,3207,3210,3213],{"class":43,"line":142},[41,3208,3209],{"class":452},"class",[41,3211,3212],{"class":448}," ApiService",[41,3214,2601],{"class":942},[41,3216,3217,3220,3223,3226,3229],{"class":43,"line":148},[41,3218,3219],{"class":452},"  constructor",[41,3221,3222],{"class":942},"(",[41,3224,3225],{"class":1146},"config",[41,3227,3228],{"class":452}," =",[41,3230,3231],{"class":942}," CONFIG) {\n",[41,3233,3234,3238,3241,3243,3246],{"class":43,"line":154},[41,3235,3237],{"class":3236},"sqerP","    this",[41,3239,3240],{"class":942},".apiUrl ",[41,3242,1115],{"class":452},[41,3244,3245],{"class":942}," config.",[41,3247,3248],{"class":378},"API_URL\n",[41,3250,3251,3253,3256,3258,3260],{"class":43,"line":1243},[41,3252,3237],{"class":3236},[41,3254,3255],{"class":942},".timeout ",[41,3257,1115],{"class":452},[41,3259,3245],{"class":942},[41,3261,3262],{"class":378},"TIMEOUT\n",[41,3264,3265],{"class":43,"line":1249},[41,3266,2645],{"class":942},[41,3268,3269],{"class":43,"line":1259},[41,3270,135],{"class":942},[16,3272,3273],{},[574,3274,3275],{},"Prefer polymorphism to if/else or switch/case",[32,3277,3279],{"className":275,"code":3278,"language":277,"meta":37,"style":37},"// Bad: Multiple if/else statements\nfunction calculateArea(shape) {\n  if (shape.type === 'circle') {\n    return Math.PI * shape.radius * shape.radius\n  } else if (shape.type === 'rectangle') {\n    return shape.width * shape.height\n  }\n}\n\n// Good: Polymorphism\nclass Circle {\n  calculateArea() {\n    return Math.PI * this.radius * this.radius\n  }\n}\n\nclass Rectangle {\n  calculateArea() {\n    return this.width * this.height\n  }\n}\n",[27,3280,3281,3286,3302,3322,3345,3369,3381,3385,3389,3393,3398,3407,3415,3439,3444,3449,3454,3464,3471,3488,3493],{"__ignoreMap":37},[41,3282,3283],{"class":43,"line":44},[41,3284,3285],{"class":442},"// Bad: Multiple if/else statements\n",[41,3287,3288,3291,3294,3296,3299],{"class":43,"line":50},[41,3289,3290],{"class":452},"function",[41,3292,3293],{"class":365}," calculateArea",[41,3295,3222],{"class":942},[41,3297,3298],{"class":1146},"shape",[41,3300,3301],{"class":942},") {\n",[41,3303,3304,3307,3310,3313,3315,3318,3320],{"class":43,"line":77},[41,3305,3306],{"class":452},"  if",[41,3308,3309],{"class":942}," (shape.type ",[41,3311,3312],{"class":452},"===",[41,3314,542],{"class":541},[41,3316,3317],{"class":368},"circle",[41,3319,964],{"class":541},[41,3321,3301],{"class":942},[41,3323,3324,3327,3330,3333,3336,3339,3342],{"class":43,"line":83},[41,3325,3326],{"class":452},"    return",[41,3328,3329],{"class":942}," Math.",[41,3331,3332],{"class":378},"PI",[41,3334,3335],{"class":452}," *",[41,3337,3338],{"class":942}," shape.radius ",[41,3340,3341],{"class":452},"*",[41,3343,3344],{"class":942}," shape.radius\n",[41,3346,3347,3350,3353,3356,3358,3360,3362,3365,3367],{"class":43,"line":89},[41,3348,3349],{"class":942},"  } ",[41,3351,3352],{"class":452},"else",[41,3354,3355],{"class":452}," if",[41,3357,3309],{"class":942},[41,3359,3312],{"class":452},[41,3361,542],{"class":541},[41,3363,3364],{"class":368},"rectangle",[41,3366,964],{"class":541},[41,3368,3301],{"class":942},[41,3370,3371,3373,3376,3378],{"class":43,"line":95},[41,3372,3326],{"class":452},[41,3374,3375],{"class":942}," shape.width ",[41,3377,3341],{"class":452},[41,3379,3380],{"class":942}," shape.height\n",[41,3382,3383],{"class":43,"line":142},[41,3384,2645],{"class":942},[41,3386,3387],{"class":43,"line":148},[41,3388,135],{"class":942},[41,3390,3391],{"class":43,"line":154},[41,3392,74],{"emptyLinePlaceholder":73},[41,3394,3395],{"class":43,"line":1243},[41,3396,3397],{"class":442},"// Good: Polymorphism\n",[41,3399,3400,3402,3405],{"class":43,"line":1249},[41,3401,3209],{"class":452},[41,3403,3404],{"class":448}," Circle",[41,3406,2601],{"class":942},[41,3408,3409,3412],{"class":43,"line":1259},[41,3410,3411],{"class":365},"  calculateArea",[41,3413,3414],{"class":942},"() {\n",[41,3416,3418,3420,3422,3424,3426,3429,3432,3434,3436],{"class":43,"line":3417},13,[41,3419,3326],{"class":452},[41,3421,3329],{"class":942},[41,3423,3332],{"class":378},[41,3425,3335],{"class":452},[41,3427,3428],{"class":3236}," this",[41,3430,3431],{"class":942},".radius ",[41,3433,3341],{"class":452},[41,3435,3428],{"class":3236},[41,3437,3438],{"class":942},".radius\n",[41,3440,3442],{"class":43,"line":3441},14,[41,3443,2645],{"class":942},[41,3445,3447],{"class":43,"line":3446},15,[41,3448,135],{"class":942},[41,3450,3452],{"class":43,"line":3451},16,[41,3453,74],{"emptyLinePlaceholder":73},[41,3455,3457,3459,3462],{"class":43,"line":3456},17,[41,3458,3209],{"class":452},[41,3460,3461],{"class":448}," Rectangle",[41,3463,2601],{"class":942},[41,3465,3467,3469],{"class":43,"line":3466},18,[41,3468,3411],{"class":365},[41,3470,3414],{"class":942},[41,3472,3474,3476,3478,3481,3483,3485],{"class":43,"line":3473},19,[41,3475,3326],{"class":452},[41,3477,3428],{"class":3236},[41,3479,3480],{"class":942},".width ",[41,3482,3341],{"class":452},[41,3484,3428],{"class":3236},[41,3486,3487],{"class":942},".height\n",[41,3489,3491],{"class":43,"line":3490},20,[41,3492,2645],{"class":942},[41,3494,3496],{"class":43,"line":3495},21,[41,3497,135],{"class":942},[16,3499,3500],{},[574,3501,3502],{},"Additional Design Principles:",[568,3504,3505,3508,3511,3514],{},[571,3506,3507],{},"Separate multi-threading code",[571,3509,3510],{},"Prevent over-configurability",[571,3512,3513],{},"Use dependency injection",[571,3515,3516],{},"Follow Law of Demeter - A class should know only its direct dependencies",[11,3518,3520],{"id":3519},"understandability-tips","Understandability Tips",[568,3522,3523,3529,3534,3540,3545,3551],{},[571,3524,3525,3528],{},[574,3526,3527],{},"Be consistent"," - If you do something a certain way, do all similar things in the same way",[571,3530,3531],{},[574,3532,3533],{},"Use explanatory variables",[571,3535,3536,3539],{},[574,3537,3538],{},"Encapsulate boundary conditions"," - Put boundary processing in one place",[571,3541,3542],{},[574,3543,3544],{},"Prefer dedicated value objects to primitive types",[571,3546,3547,3550],{},[574,3548,3549],{},"Avoid logical dependency"," - Don't write methods that depend on something else in the same class",[571,3552,3553],{},[574,3554,3555],{},"Avoid negative conditionals",[11,3557,3559],{"id":3558},"naming-rules","Naming Rules",[32,3561,3563],{"className":275,"code":3562,"language":277,"meta":37,"style":37},"// Bad naming\nlet d // elapsed time in days\nlet users = []\n\nfunction calc(u) {\n  return u.age * 365\n}\n\n// Good naming\nlet elapsedTimeInDays\nlet registeredUsers = []\n\nfunction calculateDaysFromAge(user) {\n  return user.age * DAYS_PER_YEAR\n}\n",[27,3564,3565,3570,3581,3593,3597,3611,3624,3628,3632,3637,3644,3655,3659,3673,3685],{"__ignoreMap":37},[41,3566,3567],{"class":43,"line":44},[41,3568,3569],{"class":442},"// Bad naming\n",[41,3571,3572,3575,3578],{"class":43,"line":50},[41,3573,3574],{"class":452},"let",[41,3576,3577],{"class":942}," d ",[41,3579,3580],{"class":442},"// elapsed time in days\n",[41,3582,3583,3585,3588,3590],{"class":43,"line":77},[41,3584,3574],{"class":452},[41,3586,3587],{"class":942}," users ",[41,3589,1115],{"class":452},[41,3591,3592],{"class":942}," []\n",[41,3594,3595],{"class":43,"line":83},[41,3596,74],{"emptyLinePlaceholder":73},[41,3598,3599,3601,3604,3606,3609],{"class":43,"line":89},[41,3600,3290],{"class":452},[41,3602,3603],{"class":365}," calc",[41,3605,3222],{"class":942},[41,3607,3608],{"class":1146},"u",[41,3610,3301],{"class":942},[41,3612,3613,3616,3619,3621],{"class":43,"line":95},[41,3614,3615],{"class":452},"  return",[41,3617,3618],{"class":942}," u.age ",[41,3620,3341],{"class":452},[41,3622,3623],{"class":378}," 365\n",[41,3625,3626],{"class":43,"line":142},[41,3627,135],{"class":942},[41,3629,3630],{"class":43,"line":148},[41,3631,74],{"emptyLinePlaceholder":73},[41,3633,3634],{"class":43,"line":154},[41,3635,3636],{"class":442},"// Good naming\n",[41,3638,3639,3641],{"class":43,"line":1243},[41,3640,3574],{"class":452},[41,3642,3643],{"class":942}," elapsedTimeInDays\n",[41,3645,3646,3648,3651,3653],{"class":43,"line":1249},[41,3647,3574],{"class":452},[41,3649,3650],{"class":942}," registeredUsers ",[41,3652,1115],{"class":452},[41,3654,3592],{"class":942},[41,3656,3657],{"class":43,"line":1259},[41,3658,74],{"emptyLinePlaceholder":73},[41,3660,3661,3663,3666,3668,3671],{"class":43,"line":3417},[41,3662,3290],{"class":452},[41,3664,3665],{"class":365}," calculateDaysFromAge",[41,3667,3222],{"class":942},[41,3669,3670],{"class":1146},"user",[41,3672,3301],{"class":942},[41,3674,3675,3677,3680,3682],{"class":43,"line":3441},[41,3676,3615],{"class":452},[41,3678,3679],{"class":942}," user.age ",[41,3681,3341],{"class":452},[41,3683,3684],{"class":942}," DAYS_PER_YEAR\n",[41,3686,3687],{"class":43,"line":3446},[41,3688,135],{"class":942},[16,3690,3691],{},[574,3692,3693],{},"Key naming principles:",[568,3695,3696,3699,3702,3705,3708,3711],{},[571,3697,3698],{},"Choose descriptive and unambiguous names",[571,3700,3701],{},"Make meaningful distinctions",[571,3703,3704],{},"Use pronounceable names",[571,3706,3707],{},"Use searchable names",[571,3709,3710],{},"Replace magic numbers with named constants",[571,3712,3713],{},"Avoid encodings and prefixes",[11,3715,3717],{"id":3716},"function-rules","Function Rules",[32,3719,3721],{"className":275,"code":3720,"language":277,"meta":37,"style":37},"// Bad: Large function doing multiple things\nfunction processUserData(users) {\n  // Validate users\n  users.forEach((user) => {\n    if (!user.email || !user.name) {\n      throw new Error('Invalid user')\n    }\n  })\n\n  // Calculate statistics\n  let totalAge = 0\n  users.forEach((user) => (totalAge += user.age))\n\n  // Send emails\n  users.forEach((user) => {\n    sendEmail(user.email, 'Welcome!')\n  })\n\n  return { averageAge: totalAge / users.length }\n}\n\n// Good: Small functions doing one thing\nfunction validateUsers(users) {\n  users.forEach(validateUser)\n}\n\nfunction calculateAverageAge(users) {\n  const totalAge = users.reduce((sum, user) => sum + user.age, 0)\n  return totalAge / users.length\n}\n\nfunction sendWelcomeEmails(users) {\n  users.forEach((user) => sendEmail(user.email, 'Welcome!'))\n}\n",[27,3722,3723,3728,3742,3747,3768,3791,3815,3819,3824,3828,3833,3846,3869,3873,3878,3894,3911,3915,3919,3936,3940,3944,3950,3964,3974,3979,3984,3998,4041,4053,4058,4063,4077,4106],{"__ignoreMap":37},[41,3724,3725],{"class":43,"line":44},[41,3726,3727],{"class":442},"// Bad: Large function doing multiple things\n",[41,3729,3730,3732,3735,3737,3740],{"class":43,"line":50},[41,3731,3290],{"class":452},[41,3733,3734],{"class":365}," processUserData",[41,3736,3222],{"class":942},[41,3738,3739],{"class":1146},"users",[41,3741,3301],{"class":942},[41,3743,3744],{"class":43,"line":77},[41,3745,3746],{"class":442},"  // Validate users\n",[41,3748,3749,3752,3755,3758,3760,3763,3766],{"class":43,"line":83},[41,3750,3751],{"class":942},"  users.",[41,3753,3754],{"class":365},"forEach",[41,3756,3757],{"class":942},"((",[41,3759,3670],{"class":1146},[41,3761,3762],{"class":942},") ",[41,3764,3765],{"class":452},"=>",[41,3767,2601],{"class":942},[41,3769,3770,3773,3776,3779,3782,3785,3788],{"class":43,"line":89},[41,3771,3772],{"class":452},"    if",[41,3774,3775],{"class":942}," (",[41,3777,3778],{"class":452},"!",[41,3780,3781],{"class":942},"user.email ",[41,3783,3784],{"class":452},"||",[41,3786,3787],{"class":452}," !",[41,3789,3790],{"class":942},"user.name) {\n",[41,3792,3793,3796,3800,3803,3805,3807,3810,3812],{"class":43,"line":95},[41,3794,3795],{"class":452},"      throw",[41,3797,3799],{"class":3798},"sviCg"," new",[41,3801,3802],{"class":365}," Error",[41,3804,3222],{"class":942},[41,3806,964],{"class":541},[41,3808,3809],{"class":368},"Invalid user",[41,3811,964],{"class":541},[41,3813,3814],{"class":942},")\n",[41,3816,3817],{"class":43,"line":142},[41,3818,261],{"class":942},[41,3820,3821],{"class":43,"line":148},[41,3822,3823],{"class":942},"  })\n",[41,3825,3826],{"class":43,"line":154},[41,3827,74],{"emptyLinePlaceholder":73},[41,3829,3830],{"class":43,"line":1243},[41,3831,3832],{"class":442},"  // Calculate statistics\n",[41,3834,3835,3838,3841,3843],{"class":43,"line":1249},[41,3836,3837],{"class":452},"  let",[41,3839,3840],{"class":942}," totalAge ",[41,3842,1115],{"class":452},[41,3844,3845],{"class":378}," 0\n",[41,3847,3848,3850,3852,3854,3856,3858,3860,3863,3866],{"class":43,"line":1259},[41,3849,3751],{"class":942},[41,3851,3754],{"class":365},[41,3853,3757],{"class":942},[41,3855,3670],{"class":1146},[41,3857,3762],{"class":942},[41,3859,3765],{"class":452},[41,3861,3862],{"class":942}," (totalAge ",[41,3864,3865],{"class":452},"+=",[41,3867,3868],{"class":942}," user.age))\n",[41,3870,3871],{"class":43,"line":3417},[41,3872,74],{"emptyLinePlaceholder":73},[41,3874,3875],{"class":43,"line":3441},[41,3876,3877],{"class":442},"  // Send emails\n",[41,3879,3880,3882,3884,3886,3888,3890,3892],{"class":43,"line":3446},[41,3881,3751],{"class":942},[41,3883,3754],{"class":365},[41,3885,3757],{"class":942},[41,3887,3670],{"class":1146},[41,3889,3762],{"class":942},[41,3891,3765],{"class":452},[41,3893,2601],{"class":942},[41,3895,3896,3899,3902,3904,3907,3909],{"class":43,"line":3451},[41,3897,3898],{"class":365},"    sendEmail",[41,3900,3901],{"class":942},"(user.email, ",[41,3903,964],{"class":541},[41,3905,3906],{"class":368},"Welcome!",[41,3908,964],{"class":541},[41,3910,3814],{"class":942},[41,3912,3913],{"class":43,"line":3456},[41,3914,3823],{"class":942},[41,3916,3917],{"class":43,"line":3466},[41,3918,74],{"emptyLinePlaceholder":73},[41,3920,3921,3923,3926,3928,3930,3933],{"class":43,"line":3473},[41,3922,3615],{"class":452},[41,3924,3925],{"class":942}," { averageAge",[41,3927,389],{"class":452},[41,3929,3840],{"class":942},[41,3931,3932],{"class":452},"/",[41,3934,3935],{"class":942}," users.length }\n",[41,3937,3938],{"class":43,"line":3490},[41,3939,135],{"class":942},[41,3941,3942],{"class":43,"line":3495},[41,3943,74],{"emptyLinePlaceholder":73},[41,3945,3947],{"class":43,"line":3946},22,[41,3948,3949],{"class":442},"// Good: Small functions doing one thing\n",[41,3951,3953,3955,3958,3960,3962],{"class":43,"line":3952},23,[41,3954,3290],{"class":452},[41,3956,3957],{"class":365}," validateUsers",[41,3959,3222],{"class":942},[41,3961,3739],{"class":1146},[41,3963,3301],{"class":942},[41,3965,3967,3969,3971],{"class":43,"line":3966},24,[41,3968,3751],{"class":942},[41,3970,3754],{"class":365},[41,3972,3973],{"class":942},"(validateUser)\n",[41,3975,3977],{"class":43,"line":3976},25,[41,3978,135],{"class":942},[41,3980,3982],{"class":43,"line":3981},26,[41,3983,74],{"emptyLinePlaceholder":73},[41,3985,3987,3989,3992,3994,3996],{"class":43,"line":3986},27,[41,3988,3290],{"class":452},[41,3990,3991],{"class":365}," calculateAverageAge",[41,3993,3222],{"class":942},[41,3995,3739],{"class":1146},[41,3997,3301],{"class":942},[41,3999,4001,4004,4006,4008,4011,4014,4016,4019,4021,4023,4025,4027,4030,4033,4036,4039],{"class":43,"line":4000},28,[41,4002,4003],{"class":452},"  const",[41,4005,3840],{"class":942},[41,4007,1115],{"class":452},[41,4009,4010],{"class":942}," users.",[41,4012,4013],{"class":365},"reduce",[41,4015,3757],{"class":942},[41,4017,4018],{"class":1146},"sum",[41,4020,786],{"class":942},[41,4022,3670],{"class":1146},[41,4024,3762],{"class":942},[41,4026,3765],{"class":452},[41,4028,4029],{"class":942}," sum ",[41,4031,4032],{"class":452},"+",[41,4034,4035],{"class":942}," user.age, ",[41,4037,4038],{"class":378},"0",[41,4040,3814],{"class":942},[41,4042,4044,4046,4048,4050],{"class":43,"line":4043},29,[41,4045,3615],{"class":452},[41,4047,3840],{"class":942},[41,4049,3932],{"class":452},[41,4051,4052],{"class":942}," users.length\n",[41,4054,4056],{"class":43,"line":4055},30,[41,4057,135],{"class":942},[41,4059,4061],{"class":43,"line":4060},31,[41,4062,74],{"emptyLinePlaceholder":73},[41,4064,4066,4068,4071,4073,4075],{"class":43,"line":4065},32,[41,4067,3290],{"class":452},[41,4069,4070],{"class":365}," sendWelcomeEmails",[41,4072,3222],{"class":942},[41,4074,3739],{"class":1146},[41,4076,3301],{"class":942},[41,4078,4080,4082,4084,4086,4088,4090,4092,4095,4097,4099,4101,4103],{"class":43,"line":4079},33,[41,4081,3751],{"class":942},[41,4083,3754],{"class":365},[41,4085,3757],{"class":942},[41,4087,3670],{"class":1146},[41,4089,3762],{"class":942},[41,4091,3765],{"class":452},[41,4093,4094],{"class":365}," sendEmail",[41,4096,3901],{"class":942},[41,4098,964],{"class":541},[41,4100,3906],{"class":368},[41,4102,964],{"class":541},[41,4104,4105],{"class":942},"))\n",[41,4107,4109],{"class":43,"line":4108},34,[41,4110,135],{"class":942},[16,4112,4113],{},[574,4114,4115],{},"Function principles:",[568,4117,4118,4121,4124,4127,4130,4133],{},[571,4119,4120],{},"Keep functions small",[571,4122,4123],{},"Do one thing",[571,4125,4126],{},"Use descriptive names",[571,4128,4129],{},"Prefer fewer arguments",[571,4131,4132],{},"Have no side effects",[571,4134,4135],{},"Don't use flag arguments",[11,4137,4139],{"id":4138},"comments-rules","Comments Rules",[32,4141,4143],{"className":275,"code":4142,"language":277,"meta":37,"style":37},"// Bad: Redundant comment\nlet age = 25 // Set age to 25\n\n// Bad: Commented out code\n// function oldFunction() {\n//   return 'deprecated';\n// }\n\n// Good: Explanation of intent\n// Using binary search for O(log n) performance\nfunction findUser(users, targetId) {\n  // Implementation here\n}\n\n// Good: Warning of consequences\n// Don't run this in production - it will delete all data\nfunction resetDatabase() {\n  // Implementation here\n}\n",[27,4144,4145,4150,4165,4169,4174,4179,4184,4189,4193,4198,4203,4221,4226,4230,4234,4239,4244,4253,4257],{"__ignoreMap":37},[41,4146,4147],{"class":43,"line":44},[41,4148,4149],{"class":442},"// Bad: Redundant comment\n",[41,4151,4152,4154,4157,4159,4162],{"class":43,"line":50},[41,4153,3574],{"class":452},[41,4155,4156],{"class":942}," age ",[41,4158,1115],{"class":452},[41,4160,4161],{"class":378}," 25",[41,4163,4164],{"class":442}," // Set age to 25\n",[41,4166,4167],{"class":43,"line":77},[41,4168,74],{"emptyLinePlaceholder":73},[41,4170,4171],{"class":43,"line":83},[41,4172,4173],{"class":442},"// Bad: Commented out code\n",[41,4175,4176],{"class":43,"line":89},[41,4177,4178],{"class":442},"// function oldFunction() {\n",[41,4180,4181],{"class":43,"line":95},[41,4182,4183],{"class":442},"//   return 'deprecated';\n",[41,4185,4186],{"class":43,"line":142},[41,4187,4188],{"class":442},"// }\n",[41,4190,4191],{"class":43,"line":148},[41,4192,74],{"emptyLinePlaceholder":73},[41,4194,4195],{"class":43,"line":154},[41,4196,4197],{"class":442},"// Good: Explanation of intent\n",[41,4199,4200],{"class":43,"line":1243},[41,4201,4202],{"class":442},"// Using binary search for O(log n) performance\n",[41,4204,4205,4207,4210,4212,4214,4216,4219],{"class":43,"line":1249},[41,4206,3290],{"class":452},[41,4208,4209],{"class":365}," findUser",[41,4211,3222],{"class":942},[41,4213,3739],{"class":1146},[41,4215,786],{"class":942},[41,4217,4218],{"class":1146},"targetId",[41,4220,3301],{"class":942},[41,4222,4223],{"class":43,"line":1259},[41,4224,4225],{"class":442},"  // Implementation here\n",[41,4227,4228],{"class":43,"line":3417},[41,4229,135],{"class":942},[41,4231,4232],{"class":43,"line":3441},[41,4233,74],{"emptyLinePlaceholder":73},[41,4235,4236],{"class":43,"line":3446},[41,4237,4238],{"class":442},"// Good: Warning of consequences\n",[41,4240,4241],{"class":43,"line":3451},[41,4242,4243],{"class":442},"// Don't run this in production - it will delete all data\n",[41,4245,4246,4248,4251],{"class":43,"line":3456},[41,4247,3290],{"class":452},[41,4249,4250],{"class":365}," resetDatabase",[41,4252,3414],{"class":942},[41,4254,4255],{"class":43,"line":3466},[41,4256,4225],{"class":442},[41,4258,4259],{"class":43,"line":3473},[41,4260,135],{"class":942},[16,4262,4263],{},[574,4264,4265],{},"Comment guidelines:",[568,4267,4268,4271,4274,4277,4280],{},[571,4269,4270],{},"Always try to explain yourself in code first",[571,4272,4273],{},"Don't be redundant",[571,4275,4276],{},"Don't add obvious noise",[571,4278,4279],{},"Don't comment out code - just remove it",[571,4281,4282],{},"Use comments for explanation of intent and warnings",[11,4284,4286],{"id":4285},"source-code-structure","Source Code Structure",[568,4288,4289,4295,4300,4305,4310,4315,4320,4325],{},[571,4290,4291,4294],{},[574,4292,4293],{},"Separate concepts vertically"," - Use blank lines to separate different concepts",[571,4296,4297],{},[574,4298,4299],{},"Related code should appear vertically dense",[571,4301,4302],{},[574,4303,4304],{},"Declare variables close to their usage",[571,4306,4307],{},[574,4308,4309],{},"Dependent functions should be close",[571,4311,4312],{},[574,4313,4314],{},"Similar functions should be close",[571,4316,4317],{},[574,4318,4319],{},"Place functions in downward direction",[571,4321,4322],{},[574,4323,4324],{},"Keep lines short",[571,4326,4327],{},[574,4328,4329],{},"Use whitespace to associate related things",[11,4331,4333],{"id":4332},"objects-and-data-structures","Objects and Data Structures",[32,4335,4337],{"className":275,"code":4336,"language":277,"meta":37,"style":37},"// Good: Hide internal structure\nclass BankAccount {\n  #balance = 0\n\n  deposit(amount) {\n    this.#balance += amount\n  }\n\n  getBalance() {\n    return this.#balance\n  }\n}\n\n// Good: Prefer data structures for simple data\nconst userPreferences = {\n  theme: 'dark',\n  language: 'en',\n  notifications: true,\n}\n",[27,4338,4339,4344,4353,4362,4366,4378,4390,4394,4398,4405,4414,4418,4422,4426,4431,4442,4458,4474,4486],{"__ignoreMap":37},[41,4340,4341],{"class":43,"line":44},[41,4342,4343],{"class":442},"// Good: Hide internal structure\n",[41,4345,4346,4348,4351],{"class":43,"line":50},[41,4347,3209],{"class":452},[41,4349,4350],{"class":448}," BankAccount",[41,4352,2601],{"class":942},[41,4354,4355,4358,4360],{"class":43,"line":77},[41,4356,4357],{"class":942},"  #balance ",[41,4359,1115],{"class":452},[41,4361,3845],{"class":378},[41,4363,4364],{"class":43,"line":83},[41,4365,74],{"emptyLinePlaceholder":73},[41,4367,4368,4371,4373,4376],{"class":43,"line":89},[41,4369,4370],{"class":365},"  deposit",[41,4372,3222],{"class":942},[41,4374,4375],{"class":1146},"amount",[41,4377,3301],{"class":942},[41,4379,4380,4382,4385,4387],{"class":43,"line":95},[41,4381,3237],{"class":3236},[41,4383,4384],{"class":942},".#balance ",[41,4386,3865],{"class":452},[41,4388,4389],{"class":942}," amount\n",[41,4391,4392],{"class":43,"line":142},[41,4393,2645],{"class":942},[41,4395,4396],{"class":43,"line":148},[41,4397,74],{"emptyLinePlaceholder":73},[41,4399,4400,4403],{"class":43,"line":154},[41,4401,4402],{"class":365},"  getBalance",[41,4404,3414],{"class":942},[41,4406,4407,4409,4411],{"class":43,"line":1243},[41,4408,3326],{"class":452},[41,4410,3428],{"class":3236},[41,4412,4413],{"class":942},".#balance\n",[41,4415,4416],{"class":43,"line":1249},[41,4417,2645],{"class":942},[41,4419,4420],{"class":43,"line":1259},[41,4421,135],{"class":942},[41,4423,4424],{"class":43,"line":3417},[41,4425,74],{"emptyLinePlaceholder":73},[41,4427,4428],{"class":43,"line":3441},[41,4429,4430],{"class":442},"// Good: Prefer data structures for simple data\n",[41,4432,4433,4435,4438,4440],{"class":43,"line":3446},[41,4434,1130],{"class":452},[41,4436,4437],{"class":942}," userPreferences ",[41,4439,1115],{"class":452},[41,4441,2601],{"class":942},[41,4443,4444,4447,4449,4451,4454,4456],{"class":43,"line":3451},[41,4445,4446],{"class":942},"  theme",[41,4448,389],{"class":452},[41,4450,542],{"class":541},[41,4452,4453],{"class":368},"dark",[41,4455,964],{"class":541},[41,4457,982],{"class":942},[41,4459,4460,4463,4465,4467,4470,4472],{"class":43,"line":3456},[41,4461,4462],{"class":942},"  language",[41,4464,389],{"class":452},[41,4466,542],{"class":541},[41,4468,4469],{"class":368},"en",[41,4471,964],{"class":541},[41,4473,982],{"class":942},[41,4475,4476,4479,4481,4484],{"class":43,"line":3466},[41,4477,4478],{"class":942},"  notifications",[41,4480,389],{"class":452},[41,4482,4483],{"class":378}," true",[41,4485,982],{"class":942},[41,4487,4488],{"class":43,"line":3473},[41,4489,135],{"class":942},[16,4491,4492],{},[574,4493,4494],{},"Object principles:",[568,4496,4497,4500,4503,4506,4509,4511],{},[571,4498,4499],{},"Hide internal structure",[571,4501,4502],{},"Prefer data structures for simple data",[571,4504,4505],{},"Avoid hybrid structures (half object, half data)",[571,4507,4508],{},"Keep objects small",[571,4510,4123],{},[571,4512,4513],{},"Prefer non-static methods to static methods",[11,4515,4517],{"id":4516},"test-guidelines","Test Guidelines",[32,4519,4521],{"className":275,"code":4520,"language":277,"meta":37,"style":37},"// Good: One assert per test\ndescribe('Calculator', () => {\n  it('should add two numbers correctly', () => {\n    const result = calculator.add(2, 3)\n    expect(result).toBe(5)\n  })\n\n  it('should handle negative numbers', () => {\n    const result = calculator.add(-2, 3)\n    expect(result).toBe(1)\n  })\n})\n",[27,4522,4523,4528,4549,4569,4597,4615,4619,4623,4642,4666,4681,4685],{"__ignoreMap":37},[41,4524,4525],{"class":43,"line":44},[41,4526,4527],{"class":442},"// Good: One assert per test\n",[41,4529,4530,4533,4535,4537,4540,4542,4545,4547],{"class":43,"line":50},[41,4531,4532],{"class":365},"describe",[41,4534,3222],{"class":942},[41,4536,964],{"class":541},[41,4538,4539],{"class":368},"Calculator",[41,4541,964],{"class":541},[41,4543,4544],{"class":942},", () ",[41,4546,3765],{"class":452},[41,4548,2601],{"class":942},[41,4550,4551,4554,4556,4558,4561,4563,4565,4567],{"class":43,"line":77},[41,4552,4553],{"class":365},"  it",[41,4555,3222],{"class":942},[41,4557,964],{"class":541},[41,4559,4560],{"class":368},"should add two numbers correctly",[41,4562,964],{"class":541},[41,4564,4544],{"class":942},[41,4566,3765],{"class":452},[41,4568,2601],{"class":942},[41,4570,4571,4574,4577,4579,4582,4585,4587,4590,4592,4595],{"class":43,"line":83},[41,4572,4573],{"class":452},"    const",[41,4575,4576],{"class":942}," result ",[41,4578,1115],{"class":452},[41,4580,4581],{"class":942}," calculator.",[41,4583,4584],{"class":365},"add",[41,4586,3222],{"class":942},[41,4588,4589],{"class":378},"2",[41,4591,786],{"class":942},[41,4593,4594],{"class":378},"3",[41,4596,3814],{"class":942},[41,4598,4599,4602,4605,4608,4610,4613],{"class":43,"line":89},[41,4600,4601],{"class":365},"    expect",[41,4603,4604],{"class":942},"(result).",[41,4606,4607],{"class":365},"toBe",[41,4609,3222],{"class":942},[41,4611,4612],{"class":378},"5",[41,4614,3814],{"class":942},[41,4616,4617],{"class":43,"line":95},[41,4618,3823],{"class":942},[41,4620,4621],{"class":43,"line":142},[41,4622,74],{"emptyLinePlaceholder":73},[41,4624,4625,4627,4629,4631,4634,4636,4638,4640],{"class":43,"line":148},[41,4626,4553],{"class":365},[41,4628,3222],{"class":942},[41,4630,964],{"class":541},[41,4632,4633],{"class":368},"should handle negative numbers",[41,4635,964],{"class":541},[41,4637,4544],{"class":942},[41,4639,3765],{"class":452},[41,4641,2601],{"class":942},[41,4643,4644,4646,4648,4650,4652,4654,4656,4658,4660,4662,4664],{"class":43,"line":154},[41,4645,4573],{"class":452},[41,4647,4576],{"class":942},[41,4649,1115],{"class":452},[41,4651,4581],{"class":942},[41,4653,4584],{"class":365},[41,4655,3222],{"class":942},[41,4657,2845],{"class":452},[41,4659,4589],{"class":378},[41,4661,786],{"class":942},[41,4663,4594],{"class":378},[41,4665,3814],{"class":942},[41,4667,4668,4670,4672,4674,4676,4679],{"class":43,"line":1243},[41,4669,4601],{"class":365},[41,4671,4604],{"class":942},[41,4673,4607],{"class":365},[41,4675,3222],{"class":942},[41,4677,4678],{"class":378},"1",[41,4680,3814],{"class":942},[41,4682,4683],{"class":43,"line":1249},[41,4684,3823],{"class":942},[41,4686,4687],{"class":43,"line":1259},[41,4688,1002],{"class":942},[16,4690,4691],{},[574,4692,4693],{},"Test principles:",[568,4695,4696,4699,4702,4705,4708],{},[571,4697,4698],{},"One assert per test",[571,4700,4701],{},"Tests should be readable",[571,4703,4704],{},"Tests should be fast",[571,4706,4707],{},"Tests should be independent",[571,4709,4710],{},"Tests should be repeatable",[11,4712,4714],{"id":4713},"code-smells-to-avoid","Code Smells to Avoid",[3113,4716,4717,4723,4729,4735,4741,4747],{},[571,4718,4719,4722],{},[574,4720,4721],{},"Rigidity"," - Software difficult to change; small changes cause cascading effects",[571,4724,4725,4728],{},[574,4726,4727],{},"Fragility"," - Software breaks in many places due to single change",[571,4730,4731,4734],{},[574,4732,4733],{},"Immobility"," - Cannot reuse parts of code in other projects",[571,4736,4737,4740],{},[574,4738,4739],{},"Needless Complexity"," - Over-engineering solutions",[571,4742,4743,4746],{},[574,4744,4745],{},"Needless Repetition"," - DRY principle violations",[571,4748,4749,4752],{},[574,4750,4751],{},"Opacity"," - Code that's hard to understand",[11,4754,4756],{"id":4755},"quick-reference-checklist","Quick Reference Checklist",[16,4758,4759],{},[574,4760,4761],{},"Before committing code, ask yourself:",[568,4763,4764,4767,4770,4773,4776,4779],{},[571,4765,4766],{},"✓ Can another developer easily understand this?",[571,4768,4769],{},"✓ Are function and variable names descriptive?",[571,4771,4772],{},"✓ Are functions small and focused?",[571,4774,4775],{},"✓ Is the code properly structured?",[571,4777,4778],{},"✓ Are there unnecessary comments?",[571,4780,4781],{},"✓ Is the code as simple as possible?",[11,4783,672],{"id":671},[16,4785,4786],{},"Clean code is not written by following a set of rules. Clean code is written by programmers who care about their craft and take the time to make their code readable and maintainable.",[16,4788,4789],{},"Remember: \"Any fool can write code that a computer can understand. Good programmers write code that humans can understand.\" - Martin Fowler",[16,4791,4792],{},"The investment in clean code pays dividends in reduced maintenance costs, fewer bugs, and improved team productivity. Start applying these principles today, and your future self (and teammates) will thank you.",[680,4794,4795],{},"html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .sGEwX, html code.shiki .sGEwX{--shiki-default:#FFB86C;--shiki-default-font-style:italic}html pre.shiki code .sqerP, html code.shiki .sqerP{--shiki-default:#BD93F9;--shiki-default-font-style:italic}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 .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .sviCg, html code.shiki .sviCg{--shiki-default:#FF79C6;--shiki-default-font-weight:bold}",{"title":37,"searchDepth":50,"depth":50,"links":4797},[4798,4799,4800,4801,4802,4803,4804,4805,4806,4807,4808,4809,4810],{"id":13,"depth":77,"text":14},{"id":3110,"depth":77,"text":3111},{"id":3141,"depth":77,"text":3142},{"id":3519,"depth":77,"text":3520},{"id":3558,"depth":77,"text":3559},{"id":3716,"depth":77,"text":3717},{"id":4138,"depth":77,"text":4139},{"id":4285,"depth":77,"text":4286},{"id":4332,"depth":77,"text":4333},{"id":4516,"depth":77,"text":4517},{"id":4713,"depth":77,"text":4714},{"id":4755,"depth":77,"text":4756},{"id":671,"depth":77,"text":672},"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.",{"date":4813,"image":4814,"alt":3097,"tags":4815,"published":73,"featured":709},"25.10.2025","/blogs-img/clean-code.png",[4816,4817,4818,4819],"clean-code","best-practices","software-development","programming","/blogs/clean-code-summary-robert-martin",{"title":3097,"description":4811},"blogs/2. clean-code-summary-robert-martin","zXub80-zAwRsrVewfsbF8O-vdWsOgDBBOhvu6qj7YZk",{"id":4825,"title":4826,"body":4827,"description":5574,"extension":701,"meta":5575,"navigation":73,"ogImage":5581,"path":5582,"seo":5583,"stem":5584,"__hash__":5585},"content/blogs/3. shopware-decoration-best-practices.md","Shopware Decoration Best Practices: Override One Method, Keep Others Untouched",{"type":8,"value":4828,"toc":5559},[4829,4836,4840,4872,4876,4954,4958,5096,5100,5104,5184,5188,5249,5253,5257,5321,5325,5379,5382,5483,5488,5515,5519,5524,5532,5537,5542,5556],[16,4830,4831,4832,4835],{},"Die ",[574,4833,4834],{},"Goldene Regel"," für Shopware 6 Decorations: Überschreibe nur die Methode, die du ändern musst. PHP's Vererbung handhabt automatisch alle anderen.",[723,4837,4839],{"id":4838},"die-grundregeln","Die Grundregeln",[568,4841,4842,4849,4855,4862,4869],{},[571,4843,4844,4845,4848],{},"✅ Überschreibe ",[574,4846,4847],{},"NUR"," die Methode, die du ändern musst",[571,4850,4851,4852],{},"✅ Implementiere immer ",[27,4853,4854],{},"getDecorated()",[571,4856,4857,4858,4861],{},"✅ Decorated Service ist ",[574,4859,4860],{},"ERSTES"," Constructor-Argument",[571,4863,4864,4865,4868],{},"✅ Nutze ",[27,4866,4867],{},"$this->getDecorated()->method()"," für die Kette",[571,4870,4871],{},"✅ Lass Parent-Klasse alle anderen Methoden automatisch handhaben",[723,4873,4875],{"id":4874},"falsch-alles-überschreiben","❌ Falsch - Alles überschreiben",[32,4877,4879],{"className":59,"code":4878,"language":61,"meta":37,"style":37},"class ProductListingDecorator extends AbstractProductListingRoute\n{\n    // ❌ Unnötig: Nur Delegation\n    public function getName(): string\n    {\n        return $this->getDecorated()->getName();\n    }\n\n    // ✅ Das brauchen wir wirklich\n    public function load(string $categoryId, Request $request, SalesChannelContext $context, Criteria $criteria): ProductListingResult\n    {\n        $result = $this->getDecorated()->load($categoryId, $request, $context, $criteria);\n        $this->customService->enhance($result);\n        return $result;\n    }\n}\n",[27,4880,4881,4886,4890,4895,4900,4904,4909,4913,4917,4922,4927,4931,4936,4941,4946,4950],{"__ignoreMap":37},[41,4882,4883],{"class":43,"line":44},[41,4884,4885],{},"class ProductListingDecorator extends AbstractProductListingRoute\n",[41,4887,4888],{"class":43,"line":50},[41,4889,241],{},[41,4891,4892],{"class":43,"line":77},[41,4893,4894],{},"    // ❌ Unnötig: Nur Delegation\n",[41,4896,4897],{"class":43,"line":83},[41,4898,4899],{},"    public function getName(): string\n",[41,4901,4902],{"class":43,"line":89},[41,4903,251],{},[41,4905,4906],{"class":43,"line":95},[41,4907,4908],{},"        return $this->getDecorated()->getName();\n",[41,4910,4911],{"class":43,"line":142},[41,4912,261],{},[41,4914,4915],{"class":43,"line":148},[41,4916,74],{"emptyLinePlaceholder":73},[41,4918,4919],{"class":43,"line":154},[41,4920,4921],{},"    // ✅ Das brauchen wir wirklich\n",[41,4923,4924],{"class":43,"line":1243},[41,4925,4926],{},"    public function load(string $categoryId, Request $request, SalesChannelContext $context, Criteria $criteria): ProductListingResult\n",[41,4928,4929],{"class":43,"line":1249},[41,4930,251],{},[41,4932,4933],{"class":43,"line":1259},[41,4934,4935],{},"        $result = $this->getDecorated()->load($categoryId, $request, $context, $criteria);\n",[41,4937,4938],{"class":43,"line":3417},[41,4939,4940],{},"        $this->customService->enhance($result);\n",[41,4942,4943],{"class":43,"line":3441},[41,4944,4945],{},"        return $result;\n",[41,4947,4948],{"class":43,"line":3446},[41,4949,261],{},[41,4951,4952],{"class":43,"line":3451},[41,4953,135],{},[723,4955,4957],{"id":4956},"richtig-nur-das-nötige","✅ Richtig - Nur das Nötige",[32,4959,4961],{"className":59,"code":4960,"language":61,"meta":37,"style":37},"class ProductListingDecorator extends AbstractProductListingRoute\n{\n    private AbstractProductListingRoute $decorated;\n    private MyCustomService $customService;\n\n    public function __construct(AbstractProductListingRoute $decorated, MyCustomService $customService)\n    {\n        $this->decorated = $decorated;\n        $this->customService = $customService;\n    }\n\n    public function getDecorated(): AbstractProductListingRoute\n    {\n        return $this->decorated;\n    }\n\n    // Nur die eine Methode überschreiben\n    public function load(string $categoryId, Request $request, SalesChannelContext $context, Criteria $criteria): ProductListingResult\n    {\n        $result = $this->getDecorated()->load($categoryId, $request, $context, $criteria);\n        \n        foreach ($result->getProducts() as $product) {\n            $this->customService->enhance($product);\n        }\n        \n        return $result;\n    }\n\n    // getName(), getCriteria() etc. automatisch durch Parent-Klasse\n}\n",[27,4962,4963,4967,4971,4976,4981,4985,4990,4994,4999,5004,5008,5012,5017,5021,5026,5030,5034,5039,5043,5047,5051,5056,5061,5066,5071,5075,5079,5083,5087,5092],{"__ignoreMap":37},[41,4964,4965],{"class":43,"line":44},[41,4966,4885],{},[41,4968,4969],{"class":43,"line":50},[41,4970,241],{},[41,4972,4973],{"class":43,"line":77},[41,4974,4975],{},"    private AbstractProductListingRoute $decorated;\n",[41,4977,4978],{"class":43,"line":83},[41,4979,4980],{},"    private MyCustomService $customService;\n",[41,4982,4983],{"class":43,"line":89},[41,4984,74],{"emptyLinePlaceholder":73},[41,4986,4987],{"class":43,"line":95},[41,4988,4989],{},"    public function __construct(AbstractProductListingRoute $decorated, MyCustomService $customService)\n",[41,4991,4992],{"class":43,"line":142},[41,4993,251],{},[41,4995,4996],{"class":43,"line":148},[41,4997,4998],{},"        $this->decorated = $decorated;\n",[41,5000,5001],{"class":43,"line":154},[41,5002,5003],{},"        $this->customService = $customService;\n",[41,5005,5006],{"class":43,"line":1243},[41,5007,261],{},[41,5009,5010],{"class":43,"line":1249},[41,5011,74],{"emptyLinePlaceholder":73},[41,5013,5014],{"class":43,"line":1259},[41,5015,5016],{},"    public function getDecorated(): AbstractProductListingRoute\n",[41,5018,5019],{"class":43,"line":3417},[41,5020,251],{},[41,5022,5023],{"class":43,"line":3441},[41,5024,5025],{},"        return $this->decorated;\n",[41,5027,5028],{"class":43,"line":3446},[41,5029,261],{},[41,5031,5032],{"class":43,"line":3451},[41,5033,74],{"emptyLinePlaceholder":73},[41,5035,5036],{"class":43,"line":3456},[41,5037,5038],{},"    // Nur die eine Methode überschreiben\n",[41,5040,5041],{"class":43,"line":3466},[41,5042,4926],{},[41,5044,5045],{"class":43,"line":3473},[41,5046,251],{},[41,5048,5049],{"class":43,"line":3490},[41,5050,4935],{},[41,5052,5053],{"class":43,"line":3495},[41,5054,5055],{},"        \n",[41,5057,5058],{"class":43,"line":3946},[41,5059,5060],{},"        foreach ($result->getProducts() as $product) {\n",[41,5062,5063],{"class":43,"line":3952},[41,5064,5065],{},"            $this->customService->enhance($product);\n",[41,5067,5068],{"class":43,"line":3966},[41,5069,5070],{},"        }\n",[41,5072,5073],{"class":43,"line":3976},[41,5074,5055],{},[41,5076,5077],{"class":43,"line":3981},[41,5078,4945],{},[41,5080,5081],{"class":43,"line":3986},[41,5082,261],{},[41,5084,5085],{"class":43,"line":4000},[41,5086,74],{"emptyLinePlaceholder":73},[41,5088,5089],{"class":43,"line":4043},[41,5090,5091],{},"    // getName(), getCriteria() etc. automatisch durch Parent-Klasse\n",[41,5093,5094],{"class":43,"line":4055},[41,5095,135],{},[723,5097,5099],{"id":5098},"constructor-patterns","Constructor-Patterns",[11,5101,5103],{"id":5102},"einfach-nur-decorated-service","Einfach (nur Decorated Service)",[32,5105,5107],{"className":59,"code":5106,"language":61,"meta":37,"style":37},"class SimpleCartDecorator extends AbstractCartPersister\n{\n    private AbstractCartPersister $decorated;\n\n    public function __construct(AbstractCartPersister $decorated)\n    {\n        $this->decorated = $decorated;\n    }\n\n    public function save(Cart $cart, SalesChannelContext $context): void\n    {\n        if ($cart->getLineItems()->count() > 100) {\n            throw new \\RuntimeException('Zu viele Items im Warenkorb');\n        }\n        $this->getDecorated()->save($cart, $context);\n    }\n}\n",[27,5108,5109,5114,5118,5123,5127,5132,5136,5140,5144,5148,5153,5157,5162,5167,5171,5176,5180],{"__ignoreMap":37},[41,5110,5111],{"class":43,"line":44},[41,5112,5113],{},"class SimpleCartDecorator extends AbstractCartPersister\n",[41,5115,5116],{"class":43,"line":50},[41,5117,241],{},[41,5119,5120],{"class":43,"line":77},[41,5121,5122],{},"    private AbstractCartPersister $decorated;\n",[41,5124,5125],{"class":43,"line":83},[41,5126,74],{"emptyLinePlaceholder":73},[41,5128,5129],{"class":43,"line":89},[41,5130,5131],{},"    public function __construct(AbstractCartPersister $decorated)\n",[41,5133,5134],{"class":43,"line":95},[41,5135,251],{},[41,5137,5138],{"class":43,"line":142},[41,5139,4998],{},[41,5141,5142],{"class":43,"line":148},[41,5143,261],{},[41,5145,5146],{"class":43,"line":154},[41,5147,74],{"emptyLinePlaceholder":73},[41,5149,5150],{"class":43,"line":1243},[41,5151,5152],{},"    public function save(Cart $cart, SalesChannelContext $context): void\n",[41,5154,5155],{"class":43,"line":1249},[41,5156,251],{},[41,5158,5159],{"class":43,"line":1259},[41,5160,5161],{},"        if ($cart->getLineItems()->count() > 100) {\n",[41,5163,5164],{"class":43,"line":3417},[41,5165,5166],{},"            throw new \\RuntimeException('Zu viele Items im Warenkorb');\n",[41,5168,5169],{"class":43,"line":3441},[41,5170,5070],{},[41,5172,5173],{"class":43,"line":3446},[41,5174,5175],{},"        $this->getDecorated()->save($cart, $context);\n",[41,5177,5178],{"class":43,"line":3451},[41,5179,261],{},[41,5181,5182],{"class":43,"line":3456},[41,5183,135],{},[11,5185,5187],{"id":5186},"mit-dependencies","Mit Dependencies",[32,5189,5191],{"className":59,"code":5190,"language":61,"meta":37,"style":37},"class AdvancedCartDecorator extends AbstractCartPersister\n{\n    public function __construct(\n        AbstractCartPersister $decorated,  // Erstes Argument!\n        LoggerInterface $logger,\n        SystemConfigService $systemConfig\n    ) {\n        $this->decorated = $decorated;\n        $this->logger = $logger;\n        $this->systemConfig = $systemConfig;\n    }\n}\n",[27,5192,5193,5198,5202,5207,5212,5217,5222,5227,5231,5236,5241,5245],{"__ignoreMap":37},[41,5194,5195],{"class":43,"line":44},[41,5196,5197],{},"class AdvancedCartDecorator extends AbstractCartPersister\n",[41,5199,5200],{"class":43,"line":50},[41,5201,241],{},[41,5203,5204],{"class":43,"line":77},[41,5205,5206],{},"    public function __construct(\n",[41,5208,5209],{"class":43,"line":83},[41,5210,5211],{},"        AbstractCartPersister $decorated,  // Erstes Argument!\n",[41,5213,5214],{"class":43,"line":89},[41,5215,5216],{},"        LoggerInterface $logger,\n",[41,5218,5219],{"class":43,"line":95},[41,5220,5221],{},"        SystemConfigService $systemConfig\n",[41,5223,5224],{"class":43,"line":142},[41,5225,5226],{},"    ) {\n",[41,5228,5229],{"class":43,"line":148},[41,5230,4998],{},[41,5232,5233],{"class":43,"line":154},[41,5234,5235],{},"        $this->logger = $logger;\n",[41,5237,5238],{"class":43,"line":1243},[41,5239,5240],{},"        $this->systemConfig = $systemConfig;\n",[41,5242,5243],{"class":43,"line":1249},[41,5244,261],{},[41,5246,5247],{"class":43,"line":1259},[41,5248,135],{},[723,5250,5252],{"id":5251},"häufige-fehler","Häufige Fehler",[11,5254,5256],{"id":5255},"decorated-service-nicht-aufrufen","❌ Decorated Service nicht aufrufen",[32,5258,5260],{"className":59,"code":5259,"language":61,"meta":37,"style":37},"// FALSCH: Original-Logik geht verloren\npublic function load(string $id): ProductEntity\n{\n    return new ProductEntity(); // Decorated nie aufgerufen!\n}\n\n// RICHTIG: Immer decorated aufrufen\npublic function load(string $id): ProductEntity\n{\n    $product = $this->getDecorated()->load($id);\n    $this->enhance($product);\n    return $product;\n}\n",[27,5261,5262,5267,5272,5276,5281,5285,5289,5294,5298,5302,5307,5312,5317],{"__ignoreMap":37},[41,5263,5264],{"class":43,"line":44},[41,5265,5266],{},"// FALSCH: Original-Logik geht verloren\n",[41,5268,5269],{"class":43,"line":50},[41,5270,5271],{},"public function load(string $id): ProductEntity\n",[41,5273,5274],{"class":43,"line":77},[41,5275,241],{},[41,5277,5278],{"class":43,"line":83},[41,5279,5280],{},"    return new ProductEntity(); // Decorated nie aufgerufen!\n",[41,5282,5283],{"class":43,"line":89},[41,5284,135],{},[41,5286,5287],{"class":43,"line":95},[41,5288,74],{"emptyLinePlaceholder":73},[41,5290,5291],{"class":43,"line":142},[41,5292,5293],{},"// RICHTIG: Immer decorated aufrufen\n",[41,5295,5296],{"class":43,"line":148},[41,5297,5271],{},[41,5299,5300],{"class":43,"line":154},[41,5301,241],{},[41,5303,5304],{"class":43,"line":1243},[41,5305,5306],{},"    $product = $this->getDecorated()->load($id);\n",[41,5308,5309],{"class":43,"line":1249},[41,5310,5311],{},"    $this->enhance($product);\n",[41,5313,5314],{"class":43,"line":1259},[41,5315,5316],{},"    return $product;\n",[41,5318,5319],{"class":43,"line":3417},[41,5320,135],{},[11,5322,5324],{"id":5323},"getdecorated-vergessen","❌ getDecorated() vergessen",[32,5326,5328],{"className":59,"code":5327,"language":61,"meta":37,"style":37},"// FALSCH: Decoration-Chain kaputt\npublic function doSomething(): void\n{\n    $this->decorated->doSomething(); // Direkter Aufruf\n}\n\n// RICHTIG: Über getDecorated()\npublic function doSomething(): void\n{\n    $this->getDecorated()->doSomething();\n}\n",[27,5329,5330,5335,5340,5344,5349,5353,5357,5362,5366,5370,5375],{"__ignoreMap":37},[41,5331,5332],{"class":43,"line":44},[41,5333,5334],{},"// FALSCH: Decoration-Chain kaputt\n",[41,5336,5337],{"class":43,"line":50},[41,5338,5339],{},"public function doSomething(): void\n",[41,5341,5342],{"class":43,"line":77},[41,5343,241],{},[41,5345,5346],{"class":43,"line":83},[41,5347,5348],{},"    $this->decorated->doSomething(); // Direkter Aufruf\n",[41,5350,5351],{"class":43,"line":89},[41,5352,135],{},[41,5354,5355],{"class":43,"line":95},[41,5356,74],{"emptyLinePlaceholder":73},[41,5358,5359],{"class":43,"line":142},[41,5360,5361],{},"// RICHTIG: Über getDecorated()\n",[41,5363,5364],{"class":43,"line":148},[41,5365,5339],{},[41,5367,5368],{"class":43,"line":154},[41,5369,241],{},[41,5371,5372],{"class":43,"line":1243},[41,5373,5374],{},"    $this->getDecorated()->doSomething();\n",[41,5376,5377],{"class":43,"line":1249},[41,5378,135],{},[723,5380,5381],{"id":1204},"Template",[32,5383,5385],{"className":59,"code":5384,"language":61,"meta":37,"style":37},"class MyDecorator extends AbstractOriginalService\n{\n    private AbstractOriginalService $decorated;\n\n    public function __construct(AbstractOriginalService $decorated)\n    {\n        $this->decorated = $decorated;\n    }\n\n    public function getDecorated(): AbstractOriginalService\n    {\n        return $this->decorated;\n    }\n\n    // Nur überschreiben was nötig ist\n    public function theMethodToChange(): ReturnType\n    {\n        $result = $this->getDecorated()->theMethodToChange();\n        // Deine Logik\n        return $result;\n    }\n}\n",[27,5386,5387,5392,5396,5401,5405,5410,5414,5418,5422,5426,5431,5435,5439,5443,5447,5452,5457,5461,5466,5471,5475,5479],{"__ignoreMap":37},[41,5388,5389],{"class":43,"line":44},[41,5390,5391],{},"class MyDecorator extends AbstractOriginalService\n",[41,5393,5394],{"class":43,"line":50},[41,5395,241],{},[41,5397,5398],{"class":43,"line":77},[41,5399,5400],{},"    private AbstractOriginalService $decorated;\n",[41,5402,5403],{"class":43,"line":83},[41,5404,74],{"emptyLinePlaceholder":73},[41,5406,5407],{"class":43,"line":89},[41,5408,5409],{},"    public function __construct(AbstractOriginalService $decorated)\n",[41,5411,5412],{"class":43,"line":95},[41,5413,251],{},[41,5415,5416],{"class":43,"line":142},[41,5417,4998],{},[41,5419,5420],{"class":43,"line":148},[41,5421,261],{},[41,5423,5424],{"class":43,"line":154},[41,5425,74],{"emptyLinePlaceholder":73},[41,5427,5428],{"class":43,"line":1243},[41,5429,5430],{},"    public function getDecorated(): AbstractOriginalService\n",[41,5432,5433],{"class":43,"line":1249},[41,5434,251],{},[41,5436,5437],{"class":43,"line":1259},[41,5438,5025],{},[41,5440,5441],{"class":43,"line":3417},[41,5442,261],{},[41,5444,5445],{"class":43,"line":3441},[41,5446,74],{"emptyLinePlaceholder":73},[41,5448,5449],{"class":43,"line":3446},[41,5450,5451],{},"    // Nur überschreiben was nötig ist\n",[41,5453,5454],{"class":43,"line":3451},[41,5455,5456],{},"    public function theMethodToChange(): ReturnType\n",[41,5458,5459],{"class":43,"line":3456},[41,5460,251],{},[41,5462,5463],{"class":43,"line":3466},[41,5464,5465],{},"        $result = $this->getDecorated()->theMethodToChange();\n",[41,5467,5468],{"class":43,"line":3473},[41,5469,5470],{},"        // Deine Logik\n",[41,5472,5473],{"class":43,"line":3490},[41,5474,4945],{},[41,5476,5477],{"class":43,"line":3495},[41,5478,261],{},[41,5480,5481],{"class":43,"line":3946},[41,5482,135],{},[16,5484,5485],{},[574,5486,5487],{},"services.xml:",[32,5489,5493],{"className":5490,"code":5491,"language":5492,"meta":37,"style":37},"language-xml shiki shiki-themes dracula","\u003Cservice id=\"MyPlugin\\Service\\MyDecorator\" decorates=\"Original\\Service\">\n    \u003Cargument type=\"service\" id=\"MyPlugin\\Service\\MyDecorator.inner\"/>\n    \u003C!-- Weitere Dependencies -->\n\u003C/service>\n","xml",[27,5494,5495,5500,5505,5510],{"__ignoreMap":37},[41,5496,5497],{"class":43,"line":44},[41,5498,5499],{},"\u003Cservice id=\"MyPlugin\\Service\\MyDecorator\" decorates=\"Original\\Service\">\n",[41,5501,5502],{"class":43,"line":50},[41,5503,5504],{},"    \u003Cargument type=\"service\" id=\"MyPlugin\\Service\\MyDecorator.inner\"/>\n",[41,5506,5507],{"class":43,"line":77},[41,5508,5509],{},"    \u003C!-- Weitere Dependencies -->\n",[41,5511,5512],{"class":43,"line":83},[41,5513,5514],{},"\u003C/service>\n",[723,5516,5518],{"id":5517},"warum-funktioniert-das","Warum funktioniert das?",[16,5520,5521],{},[574,5522,5523],{},"PHP Vererbungskette:",[32,5525,5530],{"className":5526,"code":5528,"language":5529},[5527],"language-text","Dein Decorator → Abstract Class → Decorated Service\n","text",[27,5531,5528],{"__ignoreMap":37},[16,5533,5534,5535,1017],{},"Wenn eine nicht-überschriebene Methode aufgerufen wird, delegiert die Abstract Class automatisch an ",[27,5536,4854],{},[16,5538,5539],{},[574,5540,5541],{},"Vorteile:",[568,5543,5544,5547,5550,5553],{},[571,5545,5546],{},"Weniger Code = weniger Bugs",[571,5548,5549],{},"Automatische Kompatibilität bei Shopware-Updates",[571,5551,5552],{},"Bessere Performance durch weniger Methodenaufrufe",[571,5554,5555],{},"Einfachere Wartung",[680,5557,5558],{},"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":37,"searchDepth":50,"depth":50,"links":5560},[5561,5562,5563,5564,5568,5572,5573],{"id":4838,"depth":50,"text":4839},{"id":4874,"depth":50,"text":4875},{"id":4956,"depth":50,"text":4957},{"id":5098,"depth":50,"text":5099,"children":5565},[5566,5567],{"id":5102,"depth":77,"text":5103},{"id":5186,"depth":77,"text":5187},{"id":5251,"depth":50,"text":5252,"children":5569},[5570,5571],{"id":5255,"depth":77,"text":5256},{"id":5323,"depth":77,"text":5324},{"id":1204,"depth":50,"text":5381},{"id":5517,"depth":50,"text":5518},"Learn the golden rule of Shopware decorations - how to efficiently override only what you need while letting PHP inheritance handle the rest automatically.",{"date":5576,"image":5577,"alt":4826,"ogimage":5577,"tags":5578,"author":1407,"published":73,"featured":73},"26.10.2025","/blogs-img/shopware-decoration.png",[5579,5580,61,4817,706],"shopware6","decorations",null,"/blogs/shopware-decoration-best-practices",{"title":4826,"description":5574},"blogs/3. shopware-decoration-best-practices","uhlsw0WKmscBM49JfMqJ70cHBj_D3wL7lZTdH7SLvu4",{"id":5587,"title":5588,"body":5589,"description":6020,"extension":701,"meta":6021,"navigation":73,"ogImage":6022,"path":751,"seo":6026,"stem":6027,"__hash__":6028},"content/blogs/4. headless-gamechanger.md","Why Headless Shopware 6 with Nuxt is a Game-Changer for Your Online Store",{"type":8,"value":5590,"toc":5999},[5591,5595,5598,5602,5605,5611,5614,5618,5621,5625,5651,5662,5666,5669,5695,5699,5702,5706,5709,5735,5738,5742,5745,5762,5765,5769,5773,5799,5803,5806,5810,5813,5817,5820,5846,5857,5861,5864,5900,5904,5907,5924,5927,5929,5936,5947,5950,5953,5956,5960,5991,5994],[5592,5593,5588],"h1",{"id":5594},"why-headless-shopware-6-with-nuxt-is-a-game-changer-for-your-online-store",[16,5596,5597],{},"Imagine your online store loading in under a second. Every time. On every device. Your customers gliding through pages like butter, adding products to their cart without a hint of lag. That's not a dream—that's the power of going headless.",[723,5599,5601],{"id":5600},"what-is-headless-commerce-anyway","What is Headless Commerce, Anyway?",[16,5603,5604],{},"Think of traditional e-commerce like a smartphone you can't customize. Everything's bundled together—the screen, the software, the apps. You get what you get.",[16,5606,5607,5610],{},[574,5608,5609],{},"Headless commerce"," is different. It's like separating your phone's screen from its brain. The backend (Shopware 6) handles all the heavy lifting—product data, inventory, orders, customer management. The frontend (Nuxt) focuses purely on creating a stunning, lightning-fast experience for your customers.",[16,5612,5613],{},"The best part? They talk to each other through APIs, so you get the best of both worlds.",[723,5615,5617],{"id":5616},"the-wow-factor-performance-that-sells","The \"WOW\" Factor: Performance That Sells",[16,5619,5620],{},"Let's talk numbers, because they're impressive:",[11,5622,5624],{"id":5623},"speed-that-actually-matters","⚡ Speed That Actually Matters",[568,5626,5627,5633,5639,5645],{},[571,5628,5629,5632],{},[574,5630,5631],{},"Pages load 3-5x faster"," than traditional Shopware storefronts",[571,5634,5635,5638],{},[574,5636,5637],{},"Under 1 second"," page load times become the norm, not the exception",[571,5640,5641,5644],{},[574,5642,5643],{},"40-60% reduction"," in bounce rates—people actually stick around",[571,5646,5647,5650],{},[574,5648,5649],{},"20-30% increase"," in conversion rates—speed literally makes you money",[16,5652,5653,5654,5657,5658,5661],{},"Here's the thing: Amazon found that every ",[574,5655,5656],{},"100ms of delay"," costs them ",[574,5659,5660],{},"1% in sales",". When your store loads instantly, people buy more. It's that simple.",[11,5663,5665],{"id":5664},"perfect-google-scores","📊 Perfect Google Scores",[16,5667,5668],{},"Headless setups with Nuxt routinely achieve:",[568,5670,5671,5677,5683,5689],{},[571,5672,5673,5676],{},[574,5674,5675],{},"90-100 Lighthouse performance scores"," (traditional stores struggle to hit 50)",[571,5678,5679,5682],{},[574,5680,5681],{},"Core Web Vitals"," passed with flying colors",[571,5684,5685,5688],{},[574,5686,5687],{},"Top Google rankings"," because speed is a ranking factor",[571,5690,5691,5694],{},[574,5692,5693],{},"Better SEO"," out of the box",[723,5696,5698],{"id":5697},"why-designers-and-marketers-love-it","Why Designers and Marketers Love It",[16,5700,5701],{},"Remember those times your designer created something amazing, but your developer said, \"Sorry, the platform can't do that\"? Those days are over.",[11,5703,5705],{"id":5704},"unlimited-creative-freedom","🎨 Unlimited Creative Freedom",[16,5707,5708],{},"With headless, your frontend is just... web. Modern web. Which means:",[568,5710,5711,5717,5723,5729],{},[571,5712,5713,5716],{},[574,5714,5715],{},"Any design is possible","—no template constraints",[571,5718,5719,5722],{},[574,5720,5721],{},"Interactive experiences"," like 3D product viewers, AR try-ons, custom configurators",[571,5724,5725,5728],{},[574,5726,5727],{},"Unique animations"," and transitions that make your brand memorable",[571,5730,5731,5734],{},[574,5732,5733],{},"Mobile experiences"," that feel like native apps",[16,5736,5737],{},"Want a homepage that looks like nothing else on the internet? Done. Need a checkout flow that matches your exact customer journey? Easy.",[11,5739,5741],{"id":5740},"one-backend-infinite-frontends","📱 One Backend, Infinite Frontends",[16,5743,5744],{},"Your Shopware backend powers everything:",[568,5746,5747,5750,5753,5756,5759],{},[571,5748,5749],{},"Your website",[571,5751,5752],{},"Your mobile app",[571,5754,5755],{},"Your smartwatch app",[571,5757,5758],{},"Your voice assistant shopping experience",[571,5760,5761],{},"Your IoT fridge ordering (okay, maybe not yet, but you get the idea!)",[16,5763,5764],{},"Build once, deploy everywhere. Your product catalog stays in sync automatically.",[723,5766,5768],{"id":5767},"the-business-case-is-crystal-clear","The Business Case is Crystal Clear",[11,5770,5772],{"id":5771},"better-roi","💰 Better ROI",[568,5774,5775,5781,5787,5793],{},[571,5776,5777,5780],{},[574,5778,5779],{},"Lower bounce rates"," = more customers stay",[571,5782,5783,5786],{},[574,5784,5785],{},"Faster pages"," = higher conversions",[571,5788,5789,5792],{},[574,5790,5791],{},"Better mobile experience"," = capture the 70% of shoppers on phones",[571,5794,5795,5798],{},[574,5796,5797],{},"Future-proof"," = no massive platform migrations every few years",[11,5800,5802],{"id":5801},"faster-time-to-market","🚀 Faster Time to Market",[16,5804,5805],{},"Launch new experiences in days, not months. Your marketing team can create landing pages without waiting for development cycles. A/B test different designs without touching the backend.",[11,5807,5809],{"id":5808},"️-reduced-risk","🛡️ Reduced Risk",[16,5811,5812],{},"Your frontend crashes? Your backend keeps humming. Need to update your design? Zero risk to your order processing. It's like having a safety net for your entire business.",[723,5814,5816],{"id":5815},"real-world-success-stories","Real-World Success Stories",[16,5818,5819],{},"Companies switching to headless Shopware + Nuxt report:",[568,5821,5822,5828,5834,5840],{},[571,5823,5824,5827],{},[574,5825,5826],{},"60% faster"," time-to-interactive compared to their old setup",[571,5829,5830,5833],{},[574,5831,5832],{},"35% increase"," in mobile conversions within 3 months",[571,5835,5836,5839],{},[574,5837,5838],{},"50% reduction"," in infrastructure costs (yes, it's actually cheaper!)",[571,5841,5842,5845],{},[574,5843,5844],{},"90% fewer"," customer complaints about slow pages",[16,5847,5848,5849,5852,5853,5856],{},"One fashion retailer saw their average page load time drop from ",[574,5850,5851],{},"4.2 seconds to 0.8 seconds",". Their mobile sales jumped by ",[574,5854,5855],{},"43%"," in the first quarter.",[723,5858,5860],{"id":5859},"the-competitive-edge","The Competitive Edge",[16,5862,5863],{},"While your competitors struggle with slow, templated storefronts, you'll have:",[16,5865,5866,5867,5870,5871,5874,5875,5878,5879,5881,5882,5885,5886,5888,5889,5892,5893,5895,5896,5899],{},"✨ ",[574,5868,5869],{},"Instant page loads"," that feel magical",[5872,5873],"br",{},"\n🎯 ",[574,5876,5877],{},"Personalized experiences"," that adapt in real-time",[5872,5880],{},"\n🌍 ",[574,5883,5884],{},"Global reach"," with edge caching and CDN delivery",[5872,5887],{},"\n📈 ",[574,5890,5891],{},"Analytics and testing"," that don't slow down your site",[5872,5894],{},"\n🔄 ",[574,5897,5898],{},"Easy updates"," without site downtime",[723,5901,5903],{"id":5902},"is-headless-right-for-you","Is Headless Right for You?",[16,5905,5906],{},"Headless makes sense if you:",[568,5908,5909,5912,5915,5918,5921],{},[571,5910,5911],{},"Want to stand out from cookie-cutter competitors",[571,5913,5914],{},"Care about performance and user experience",[571,5916,5917],{},"Need flexibility for future growth",[571,5919,5920],{},"Want to delight customers instead of just serving them",[571,5922,5923],{},"Believe your online store should feel as fast as scrolling Instagram",[16,5925,5926],{},"Basically, if you're serious about e-commerce, headless is the way forward.",[723,5928,1877],{"id":1876},[16,5930,5931,5932,5935],{},"Traditional e-commerce platforms were built for a different era. Headless architecture with Shopware 6 and Nuxt is built for ",[574,5933,5934],{},"today's customers"," who expect:",[568,5937,5938,5941,5944],{},[571,5939,5940],{},"Netflix-level smoothness",[571,5942,5943],{},"Instagram-level design",[571,5945,5946],{},"Amazon-level speed",[16,5948,5949],{},"And here's the beautiful part: it's not science fiction anymore. It's available right now, proven, and working for stores around the world.",[16,5951,5952],{},"The question isn't \"Why go headless?\"",[16,5954,5955],{},"It's \"Why haven't you gone headless yet?\"",[723,5957,5959],{"id":5958},"ready-to-go-headless","Ready to Go Headless?",[568,5961,5962,5968,5975,5983],{},[571,5963,5964,5967],{},[635,5965,1353],{"href":1351,"rel":5966},[639]," - Everything you need to get started",[571,5969,5970,5974],{},[635,5971,5973],{"href":1921,"rel":5972},[639],"Nuxt 3 Official Site"," - The modern Vue framework powering it all",[571,5976,5977,5982],{},[635,5978,5981],{"href":5979,"rel":5980},"https://shopware.stoplight.io/docs/store-api",[639],"Shopware 6 Store API"," - Connect anything to your shop",[571,5984,5985,5990],{},[635,5986,5989],{"href":5987,"rel":5988},"https://www.shopware.com/en/solutions/headless-commerce/",[639],"Headless Commerce Guide"," - Deep dive into the concept",[5992,5993],"hr",{},[16,5995,5996],{},[1421,5997,5998],{},"The future of e-commerce is headless. The future is now.",{"title":37,"searchDepth":50,"depth":50,"links":6000},[6001,6002,6006,6010,6015,6016,6017,6018,6019],{"id":5600,"depth":50,"text":5601},{"id":5616,"depth":50,"text":5617,"children":6003},[6004,6005],{"id":5623,"depth":77,"text":5624},{"id":5664,"depth":77,"text":5665},{"id":5697,"depth":50,"text":5698,"children":6007},[6008,6009],{"id":5704,"depth":77,"text":5705},{"id":5740,"depth":77,"text":5741},{"id":5767,"depth":50,"text":5768,"children":6011},[6012,6013,6014],{"id":5771,"depth":77,"text":5772},{"id":5801,"depth":77,"text":5802},{"id":5808,"depth":77,"text":5809},{"id":5815,"depth":50,"text":5816},{"id":5859,"depth":50,"text":5860},{"id":5902,"depth":50,"text":5903},{"id":1876,"depth":50,"text":1877},{"id":5958,"depth":50,"text":5959},"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.",{"date":5576,"image":6022,"alt":6023,"tags":6024,"published":73,"featured":73},"/blogs-img/shopware-nuxt-headless.png","Headless Shopware 6 with Nuxt - The future of e-commerce",[522,1404,1405,6025,708,706],"cms",{"title":5588,"description":6020},"blogs/4. headless-gamechanger","sx6S7U1V5MmoiqiXogkgTv6w2cOzxNooQae8koOsG9Q",{"id":6030,"title":6031,"body":6032,"description":6346,"extension":701,"meta":6347,"navigation":73,"ogImage":6348,"path":2807,"seo":6353,"stem":6354,"__hash__":6355},"content/blogs/5. quality-tools-that-matter.md","Code Quality Tools That Actually Matter - Your PR Review Checklist",{"type":8,"value":6033,"toc":6331},[6034,6038,6044,6048,6051,6055,6081,6085,6088,6092,6096,6102,6108,6114,6118,6123,6128,6133,6137,6140,6145,6149,6156,6159,6173,6177,6180,6210,6213,6215,6218,6244,6247,6251,6324,6326],[5592,6035,6037],{"id":6036},"code-quality-tools-that-actually-matter","Code Quality Tools That Actually Matter",[16,6039,6040,6041,1017],{},"Ever had that sinking feeling when a bug slips into production? Or spent hours in code review arguing about formatting? The solution is simpler than you think: ",[574,6042,6043],{},"automate your quality checks",[723,6045,6047],{"id":6046},"why-code-quality-tools-are-non-negotiable","Why Code Quality Tools Are Non-Negotiable",[16,6049,6050],{},"Quality tools aren't optional anymore. They're the difference between shipping with confidence and crossing your fingers. Here's why smart teams rely on them:",[11,6052,6054],{"id":6053},"the-hard-numbers","The Hard Numbers",[568,6056,6057,6063,6069,6075],{},[571,6058,6059,6062],{},[574,6060,6061],{},"60% fewer bugs"," reach production when using automated quality checks",[571,6064,6065,6068],{},[574,6066,6067],{},"50% faster code reviews"," when style is automated",[571,6070,6071,6074],{},[574,6072,6073],{},"30% less time"," spent fixing preventable issues",[571,6076,6077,6080],{},[574,6078,6079],{},"90% of code quality issues"," can be caught automatically before human review",[11,6082,6084],{"id":6083},"the-real-world-impact","The Real-World Impact",[16,6086,6087],{},"Quality tools transform how teams work. They catch problems in seconds that would take hours to debug in production. They eliminate endless debates about code style. They make large refactorings safe instead of scary.",[723,6089,6091],{"id":6090},"the-not-so-known-quality-toolkit","The not-so-known Quality Toolkit",[11,6093,6095],{"id":6094},"phpunuhi-translation-guardian","PHPUnuhi - Translation Guardian",[16,6097,6098,6101],{},[574,6099,6100],{},"Purpose:"," Validates translation files for completeness and consistency.",[16,6103,6104,6107],{},[574,6105,6106],{},"Why it matters:"," Catches missing translations before users see \"translation.key.not.found\" in production. Essential for international applications.",[16,6109,6110,6113],{},[574,6111,6112],{},"Key benefit:"," Professional multi-language support.",[11,6115,6117],{"id":6116},"composer-validate-dependency-defender","Composer Validate - Dependency Defender",[16,6119,6120,6122],{},[574,6121,6100],{}," Checks package configuration for issues.",[16,6124,6125,6127],{},[574,6126,6106],{}," Prevents dependency conflicts and validates package structure before they cause problems.",[16,6129,6130,6132],{},[574,6131,6112],{}," Stable, reliable dependency management.",[11,6134,6136],{"id":6135},"deptrac","Deptrac",[16,6138,6139],{},"Enforces architectural boundaries and prevents layer violations. Stops controllers from calling databases directly or services from depending on presentation logic.",[16,6141,6142,6144],{},[574,6143,6112],{}," Maintains clean architecture automatically.",[723,6146,6148],{"id":6147},"the-automation-advantage","The Automation Advantage",[16,6150,6151,6152,6155],{},"The real power comes from automation. Run quality checks automatically on every commit, every PR, every deploy. Make them part of your CI/CD pipeline. ",[574,6153,6154],{},"If it doesn't pass quality checks, it doesn't get merged."," No exceptions.",[16,6157,6158],{},"Teams with automated quality gates report:",[568,6160,6161,6164,6167,6170],{},[571,6162,6163],{},"Faster onboarding for new developers",[571,6165,6166],{},"More confidence when refactoring legacy code",[571,6168,6169],{},"Fewer production incidents",[571,6171,6172],{},"Better sleep at night",[723,6174,6176],{"id":6175},"getting-started-the-smart-approach","Getting Started: The Smart Approach",[16,6178,6179],{},"Don't try to implement everything at once. Start with the essentials:",[16,6181,6182,6185,6186,6188,6191,6192,6194,6197,6198,6200,6203,6204,6206,6209],{},[574,6183,6184],{},"Phase 1:"," Code formatting (PHP CS Fixer)",[5872,6187],{},[574,6189,6190],{},"Phase 2:"," Testing (PHPUnit or improve existing tests)",[5872,6193],{},[574,6195,6196],{},"Phase 3:"," Static analysis (PHPStan, start at lower levels)",[5872,6199],{},[574,6201,6202],{},"Phase 4:"," Automated refactoring (Rector)",[5872,6205],{},[574,6207,6208],{},"Phase 5:"," Specialized tools based on your needs",[16,6211,6212],{},"Each tool builds on the previous one. Each adds another layer of protection against bugs and technical debt.",[723,6214,1877],{"id":1876},[16,6216,6217],{},"Code quality tools aren't about being perfectionist nerds (okay, maybe a little). They're about:",[568,6219,6220,6226,6232,6238],{},[571,6221,6222,6225],{},[574,6223,6224],{},"Shipping faster"," by catching issues early",[571,6227,6228,6231],{},[574,6229,6230],{},"Sleeping better"," knowing bugs are caught automatically",[571,6233,6234,6237],{},[574,6235,6236],{},"Focusing reviews"," on architecture and logic, not style",[571,6239,6240,6243],{},[574,6241,6242],{},"Maintaining sanity"," on legacy codebases",[16,6245,6246],{},"Your future self will thank you. Your teammates will thank you. Your users (who don't see bugs) will thank you.",[723,6248,6250],{"id":6249},"essential-resources","Essential Resources",[568,6252,6253,6261,6269,6277,6285,6293,6301,6309,6316],{},[571,6254,6255,6260],{},[635,6256,6259],{"href":6257,"rel":6258},"https://github.com/PHP-CS-Fixer/PHP-CS-Fixer",[639],"PHP CS Fixer"," - Code style fixer",[571,6262,6263,6268],{},[635,6264,6267],{"href":6265,"rel":6266},"https://phpstan.org/",[639],"PHPStan"," - Static analysis tool",[571,6270,6271,6276],{},[635,6272,6275],{"href":6273,"rel":6274},"https://github.com/rectorphp/rector",[639],"Rector"," - Automated refactoring",[571,6278,6279,6284],{},[635,6280,6283],{"href":6281,"rel":6282},"https://phpunit.de/",[639],"PHPUnit"," - Testing framework",[571,6286,6287,6292],{},[635,6288,6291],{"href":6289,"rel":6290},"https://phpmd.org/",[639],"PHPMD"," - Mess detector",[571,6294,6295,6300],{},[635,6296,6299],{"href":6297,"rel":6298},"https://psalm.dev/",[639],"Psalm"," - Static analysis alternative",[571,6302,6303,6308],{},[635,6304,6307],{"href":6305,"rel":6306},"https://infection.github.io/",[639],"Infection"," - Mutation testing",[571,6310,6311,6315],{},[635,6312,6136],{"href":6313,"rel":6314},"https://github.com/qossmic/deptrac",[639]," - Architecture enforcement",[571,6317,6318,6323],{},[635,6319,6322],{"href":6320,"rel":6321},"https://www.phpmetrics.org/",[639],"PHPMetrics"," - Code metrics and reports",[5992,6325],{},[16,6327,6328],{},[1421,6329,6330],{},"Quality is not an act, it is a habit. - Aristotle (who definitely would have used PHPStan)",{"title":37,"searchDepth":50,"depth":50,"links":6332},[6333,6337,6342,6343,6344,6345],{"id":6046,"depth":50,"text":6047,"children":6334},[6335,6336],{"id":6053,"depth":77,"text":6054},{"id":6083,"depth":77,"text":6084},{"id":6090,"depth":50,"text":6091,"children":6338},[6339,6340,6341],{"id":6094,"depth":77,"text":6095},{"id":6116,"depth":77,"text":6117},{"id":6135,"depth":77,"text":6136},{"id":6147,"depth":50,"text":6148},{"id":6175,"depth":50,"text":6176},{"id":1876,"depth":50,"text":1877},{"id":6249,"depth":50,"text":6250},"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.",{"date":5576,"image":6348,"alt":6349,"tags":6350,"published":73},"/blogs-img/code-quality-tools.png","Essential code quality tools for modern development",[6351,61,6352,4817,2734],"code-quality","tools",{"title":6031,"description":6346},"blogs/5. quality-tools-that-matter","9ZVw-XGb97-aA41F48_u2zGedUBuJAEXQGt8xZha7DU",{"id":6357,"title":6358,"body":6359,"description":7233,"extension":701,"meta":7234,"navigation":73,"ogImage":7235,"path":1977,"seo":7240,"stem":7241,"__hash__":7242},"content/blogs/6. shopware-67-migration-guide.md","Shopware 6.6 to 6.7 Migration Guide - Breaking Changes and a Safe Upgrade Path",{"type":8,"value":6360,"toc":7214},[6361,6368,6371,6375,6382,6385,6396,6399,6403,6409,6425,6436,6440,6454,6461,6464,6478,6485,6492,6503,6525,6531,6537,6542,6551,6556,6599,6604,6623,6630,6634,6645,6664,6667,6671,6682,6744,6751,6776,6780,6794,6802,6813,6839,6851,6858,6879,6884,6909,6914,6943,6949,6953,6960,6971,6978,7013,7016,7023,7027,7030,7129,7136,7151,7155,7158,7164,7166,7211],[16,6362,6363,6364,6367],{},"A while back I was midway through upgrading a client's Shopware store to 6.7 when the admin simply refused to load — no error page, no useful console output, just a blank screen. Two hours later I'd traced it to a plugin that had published a \"6.7 compatible\" release but hadn't actually rebuilt its assets against Vite. The vendor had bumped the constraint in ",[27,6365,6366],{},"composer.json"," and called it done. That afternoon taught me more about 6.7's breaking changes than any changelog ever could.",[16,6369,6370],{},"Shopware 6.7 landed in June 2025 and ships meaningful architectural jumps: Vite everywhere, Symfony 7.4, Vuex out / Pinia in, and a redesigned cache invalidation model. None of these are insurmountable, but each one can silently kill a plugin or break a theme if you upgrade blind. This guide walks you through every breaking area, with concrete before/after code, so you can ship 6.7 without a production incident.",[723,6372,6374],{"id":6373},"should-you-upgrade-right-now","Should You Upgrade Right Now?",[16,6376,6377,6378,6381],{},"My honest take: probably not immediately. Shopware 6.6 is an ",[574,6379,6380],{},"LTS release"," — it receives security patches for roughly two years from its GA date. There is no urgency. The correct move is a deliberate, staged upgrade, not a panic migration chasing the newest minor.",[16,6383,6384],{},"Upgrade when:",[568,6386,6387,6390,6393],{},[571,6388,6389],{},"Your third-party plugins already have 6.7-compatible releases",[571,6391,6392],{},"You have a staging environment that mirrors production",[571,6394,6395],{},"You have capacity to QA checkout, custom storefront areas, and admin workflows end-to-end",[16,6397,6398],{},"If you are on 6.6 and none of your plugins have published 6.7 builds yet, wait. Plugin incompatibility is the single biggest cause of upgrade outages — I've seen it enough times to make it my first check, every single time.",[723,6400,6402],{"id":6401},"step-0-run-the-upgrade-check-before-touching-anything","Step 0: Run the Upgrade Check Before Touching Anything",[16,6404,6405,6406,6408],{},"My rule before any major Shopware upgrade: don't touch ",[27,6407,6366],{}," until you've run the official compatibility scanner:",[32,6410,6412],{"className":356,"code":6411,"language":358,"meta":37,"style":37},"shopware-cli project upgrade-check\n",[27,6413,6414],{"__ignoreMap":37},[41,6415,6416,6419,6422],{"class":43,"line":44},[41,6417,6418],{"class":365},"shopware-cli",[41,6420,6421],{"class":368}," project",[41,6423,6424],{"class":368}," upgrade-check\n",[16,6426,6427,6428,6431,6432,6435],{},"This scans your install against the 6.7 compatibility matrix and prints exactly what breaks — deprecated usages, incompatible plugin versions, missing ",[27,6429,6430],{},"technicalName"," entries, and more. ",[574,6433,6434],{},"Treat its output as your checklist."," Everything below maps to findings this tool surfaces. I can't count the number of times this single command has saved me from a nasty surprise on staging.",[723,6437,6439],{"id":6438},"vite-replaces-webpack-and-plugins-are-always-what-bite-first","Vite Replaces Webpack — and Plugins Are Always What Bite First",[16,6441,6442,6443,2465,6446,6449,6450,6453],{},"The most impactful frontend change: both the ",[574,6444,6445],{},"admin",[574,6447,6448],{},"storefront"," build pipelines switched from Webpack to ",[574,6451,6452],{},"Vite",". The two pipelines are not backwards compatible. A plugin compiled against 6.6's Webpack setup will not load in 6.7.",[16,6455,6456,6457,6460],{},"This is the part that burned me in the story above. Any plugin shipping admin Vue components or custom Storefront JS ",[574,6458,6459],{},"must publish a separate release"," targeting 6.7. If the plugin vendor hasn't done this yet, the plugin is either broken or blocked from loading entirely. A bumped version constraint without a rebuilt asset bundle is not a compatible release — verify the changelog carefully.",[16,6462,6463],{},"What this means for your project:",[568,6465,6466,6472,6475],{},[571,6467,6468,6469,6471],{},"Audit every installed plugin in ",[27,6470,6366],{},". Check the vendor's changelog for a \"6.7 / Vite\" release.",[571,6473,6474],{},"If you maintain internal plugins, update their build config to Vite and publish a new version before upgrading the shop.",[571,6476,6477],{},"Custom themes extending the default Storefront may need their Webpack-specific configs removed.",[16,6479,6480,6481,6484],{},"For safe, upgrade-resilient theme overrides, see the ",[635,6482,6483],{"href":5582},"Shopware decoration best practices"," — patterns like template extensions and SCSS variable overrides tend to survive major version jumps better than deep JS patches.",[723,6486,6488,6489],{"id":6487},"symfony-74-type-declarations-and-the-death-of-requestget","Symfony 7.4 — Type Declarations and the Death of ",[27,6490,6491],{},"Request::get()",[16,6493,6494,6495,6498,6499,6502],{},"All Symfony packages are updated to ",[574,6496,6497],{},"v7.4",", and Doctrine DBAL moves to ",[574,6500,6501],{},"v4",". Symfony 7 tightened method signatures and removed APIs that were deprecated for years. In my experience, the most common plugin break is a service that extends or implements a Symfony class whose method signature changed.",[16,6504,6505,6524],{},[574,6506,6507,6508,6511,6512,6515,6516,6519,6520,6523],{},"Check your ",[27,6509,6510],{},"phpstan.neon"," / ",[27,6513,6514],{},"phpcs"," baseline for any ",[27,6517,6518],{},"extends"," or ",[27,6521,6522],{},"implements"," on Symfony core classes."," The upgrade-check tool flags these; resolve them before bumping the composer constraint.",[11,6526,6528,6530],{"id":6527},"requestget-is-gone-use-explicit-bags",[27,6529,6491],{}," is Gone — Use Explicit Bags",[16,6532,6533,6534,6536],{},"This one catches people off guard. Symfony deprecated the magic ",[27,6535,6491],{}," helper, which searched attributes → query → request bags in sequence. In 6.7 you must be explicit:",[16,6538,6539],{},[574,6540,6541],{},"Before (6.6):",[32,6543,6545],{"className":59,"code":6544,"language":61,"meta":37,"style":37},"$foo = $request->get('foo');\n",[27,6546,6547],{"__ignoreMap":37},[41,6548,6549],{"class":43,"line":44},[41,6550,6544],{},[16,6552,6553],{},[574,6554,6555],{},"After (6.7) — explicit bag:",[32,6557,6559],{"className":59,"code":6558,"language":61,"meta":37,"style":37},"// Query string (?foo=bar)\n$foo = $request->query->get('foo');\n\n// POST body\n$foo = $request->request->get('foo');\n\n// Route attribute\n$foo = $request->attributes->get('foo');\n",[27,6560,6561,6566,6571,6575,6580,6585,6589,6594],{"__ignoreMap":37},[41,6562,6563],{"class":43,"line":44},[41,6564,6565],{},"// Query string (?foo=bar)\n",[41,6567,6568],{"class":43,"line":50},[41,6569,6570],{},"$foo = $request->query->get('foo');\n",[41,6572,6573],{"class":43,"line":77},[41,6574,74],{"emptyLinePlaceholder":73},[41,6576,6577],{"class":43,"line":83},[41,6578,6579],{},"// POST body\n",[41,6581,6582],{"class":43,"line":89},[41,6583,6584],{},"$foo = $request->request->get('foo');\n",[41,6586,6587],{"class":43,"line":95},[41,6588,74],{"emptyLinePlaceholder":73},[41,6590,6591],{"class":43,"line":142},[41,6592,6593],{},"// Route attribute\n",[41,6595,6596],{"class":43,"line":148},[41,6597,6598],{},"$foo = $request->attributes->get('foo');\n",[16,6600,6601],{},[574,6602,6603],{},"After (6.7) — when the source is genuinely unknown (rare):",[32,6605,6607],{"className":59,"code":6606,"language":61,"meta":37,"style":37},"use Shopware\\Core\\Framework\\Routing\\RequestParamHelper;\n\n$foo = RequestParamHelper::get($request, 'foo');\n",[27,6608,6609,6614,6618],{"__ignoreMap":37},[41,6610,6611],{"class":43,"line":44},[41,6612,6613],{},"use Shopware\\Core\\Framework\\Routing\\RequestParamHelper;\n",[41,6615,6616],{"class":43,"line":50},[41,6617,74],{"emptyLinePlaceholder":73},[41,6619,6620],{"class":43,"line":77},[41,6621,6622],{},"$foo = RequestParamHelper::get($request, 'foo');\n",[16,6624,6625,6626,6629],{},"Prefer explicit bags wherever possible. ",[27,6627,6628],{},"RequestParamHelper"," is a compatibility bridge for legacy code paths, not a blanket replacement. I've found that forcing yourself to be explicit here actually surfaces assumptions in the original code that were never intentional.",[11,6631,6633],{"id":6632},"redis-requirement","Redis Requirement",[16,6635,6636,6637,6644],{},"If your setup uses Redis: ",[574,6638,6639,6640,6643],{},"the ",[27,6641,6642],{},"php-redis"," extension must be v6.1 or higher"," — this is a hard Symfony 7.4 constraint. Check before upgrade:",[32,6646,6648],{"className":356,"code":6647,"language":358,"meta":37,"style":37},"php -r \"echo phpversion('redis');\"\n",[27,6649,6650],{"__ignoreMap":37},[41,6651,6652,6654,6657,6659,6662],{"class":43,"line":44},[41,6653,61],{"class":365},[41,6655,6656],{"class":378}," -r",[41,6658,2706],{"class":541},[41,6660,6661],{"class":368},"echo phpversion('redis');",[41,6663,2712],{"class":541},[16,6665,6666],{},"Anything below 6.1 needs an extension update first. I keep this check in my pre-upgrade notes now after catching it too late once on a client environment.",[723,6668,6670],{"id":6669},"admin-js-vuex-out-pinia-in","Admin JS — Vuex Out, Pinia In",[16,6672,6673,6674,6677,6678,6681],{},"The Shopware admin moved ",[574,6675,6676],{},"fully out of Vue 3 compatibility mode"," and migrated state management from Vuex to ",[574,6679,6680],{},"Pinia",". The old Vuex helper utilities still exist but are renamed to avoid collision:",[1586,6683,6684,6694],{},[1589,6685,6686],{},[1592,6687,6688,6691],{},[1595,6689,6690],{},"6.6 helper",[1595,6692,6693],{},"6.7 replacement",[1605,6695,6696,6708,6720,6732],{},[1592,6697,6698,6703],{},[1610,6699,6700],{},[27,6701,6702],{},"mapState",[1610,6704,6705],{},[27,6706,6707],{},"mapVuexState",[1592,6709,6710,6715],{},[1610,6711,6712],{},[27,6713,6714],{},"mapMutations",[1610,6716,6717],{},[27,6718,6719],{},"mapVuexMutations",[1592,6721,6722,6727],{},[1610,6723,6724],{},[27,6725,6726],{},"mapGetters",[1610,6728,6729],{},[27,6730,6731],{},"mapVuexGetters",[1592,6733,6734,6739],{},[1610,6735,6736],{},[27,6737,6738],{},"mapActions",[1610,6740,6741],{},[27,6742,6743],{},"mapVuexActions",[16,6745,6746,6747,6750],{},"Rename every import and usage in your admin components. A project-wide search for ",[27,6748,6749],{},"mapState\\|mapMutations\\|mapGetters\\|mapActions"," catches them all. Honestly this rename is mechanical and quick — don't let it intimidate you.",[16,6752,6753,6756,6757,6760,6761,6764,6765,6768,6769,6772,6773,1017],{},[574,6754,6755],{},"vue-i18n"," was also updated to v10, which removes the ",[27,6758,6759],{},"tc"," function. The ",[27,6762,6763],{},"$tc"," shorthand on Vue components still works (it internally calls ",[27,6766,6767],{},"t","), but any direct ",[27,6770,6771],{},"tc(...)"," calls in non-component JS need to be replaced with ",[27,6774,6775],{},"t(...)",[723,6777,6779],{"id":6778},"storefront-html-semantic-elements-and-pagelet-loaders","Storefront HTML — Semantic Elements and Pagelet Loaders",[16,6781,6782,6783,6786,6787,3932,6790,6793],{},"Several previously ",[27,6784,6785],{},"\u003Cdiv>","-based list areas are now proper ",[27,6788,6789],{},"\u003Cul>",[27,6791,6792],{},"\u003Cli>"," elements:",[568,6795,6796,6799],{},[571,6797,6798],{},"Account order overview",[571,6800,6801],{},"Cart line-items",[16,6803,6804,6805,6808,6809,6812],{},"If your custom theme targets these with element selectors (e.g. ",[27,6806,6807],{},".cart-item-list > div","), update to ",[27,6810,6811],{},"> li",". It's a quick find-and-replace but I've seen it slip through code review and make it to staging, where a customer notices the broken styling before anyone else does.",[16,6814,6815,6816,6828,6829,6831,6832,2465,6835,6838],{},"More importantly: ",[574,6817,6818,786,6821,6824,6825],{},[27,6819,6820],{},"header",[27,6822,6823],{},"footer",", payment methods, and shipping methods are no longer loaded by ",[27,6826,6827],{},"GenericPageLoader",". If you have a custom page type that relied on ",[27,6830,6827],{}," pulling these in automatically, you must now extend ",[27,6833,6834],{},"HeaderPageletLoader",[27,6836,6837],{},"FooterPageletLoader"," explicitly.",[16,6840,6841,6842,6845,6846,3932,6848,6850],{},"Additionally, ",[27,6843,6844],{},"ErrorTemplateStruct"," had its ",[27,6847,6820],{},[27,6849,6823],{}," properties, getters, and setters removed. Custom error pages extending this struct need to fetch header/footer independently.",[723,6852,6854,6855,6857],{"id":6853},"payment-shipping-technicalname-is-now-non-nullable","Payment & Shipping — ",[27,6856,6430],{}," Is Now Non-Nullable",[16,6859,6860,6861,2465,6864,6867,6868,6871,6872,6875,6876,6878],{},"Both ",[27,6862,6863],{},"payment_method",[27,6865,6866],{},"shipping_method"," tables have their ",[27,6869,6870],{},"technical_name"," column made ",[574,6873,6874],{},"non-nullable",". Any payment or shipping method created without a ",[27,6877,6430],{}," in the API will now fail validation.",[16,6880,6881],{},[574,6882,6883],{},"Before (worked in 6.6, fails in 6.7):",[32,6885,6887],{"className":59,"code":6886,"language":61,"meta":37,"style":37},"$paymentMethod = [\n    'name' => 'My Custom Payment',\n    'handlerIdentifier' => MyPaymentHandler::class,\n];\n",[27,6888,6889,6894,6899,6904],{"__ignoreMap":37},[41,6890,6891],{"class":43,"line":44},[41,6892,6893],{},"$paymentMethod = [\n",[41,6895,6896],{"class":43,"line":50},[41,6897,6898],{},"    'name' => 'My Custom Payment',\n",[41,6900,6901],{"class":43,"line":77},[41,6902,6903],{},"    'handlerIdentifier' => MyPaymentHandler::class,\n",[41,6905,6906],{"class":43,"line":83},[41,6907,6908],{},"];\n",[16,6910,6911],{},[574,6912,6913],{},"After (6.7):",[32,6915,6917],{"className":59,"code":6916,"language":61,"meta":37,"style":37},"$paymentMethod = [\n    'name' => 'My Custom Payment',\n    'technicalName' => 'payment_my_custom', // required, lowercase snake_case\n    'handlerIdentifier' => MyPaymentHandler::class,\n];\n",[27,6918,6919,6923,6927,6935,6939],{"__ignoreMap":37},[41,6920,6921],{"class":43,"line":44},[41,6922,6893],{},[41,6924,6925],{"class":43,"line":50},[41,6926,6898],{},[41,6928,6929,6932],{"class":43,"line":77},[41,6930,6931],{},"    'technicalName' => 'payment_my_custom',",[41,6933,6934],{}," // required, lowercase snake_case\n",[41,6936,6937],{"class":43,"line":83},[41,6938,6903],{},[41,6940,6941],{"class":43,"line":89},[41,6942,6908],{},[16,6944,6945,6946,6948],{},"Run the upgrade-check tool — it will flag payment/shipping entries in your database that are missing this value so you can backfill before upgrading. Don't skip this step; a missing ",[27,6947,6430],{}," will surface at the worst possible moment otherwise.",[723,6950,6952],{"id":6951},"delayed-cache-invalidation-and-the-esi-gotcha","Delayed Cache Invalidation and the ESI Gotcha",[16,6954,6955,6956,6959],{},"Cache invalidation in 6.7 is ",[574,6957,6958],{},"delayed by default",": the cache is no longer purged immediately on data change, but at regular intervals. This is a deliberate performance trade-off — it reduces invalidation storms at the cost of slight staleness windows.",[16,6961,6962,6963,6966,6967,6970],{},"More critically: ",[574,6964,6965],{},"ESI (Edge Side Includes) must be explicitly enabled"," at the HTTP layer, or your header and footer will not render. This one cost me an afternoon once — everything looked fine in a shallow smoke test, but navigating deeper into the storefront revealed that the header was just... gone. ESI is how Shopware assembles cached page fragments, and 6.7 requires the layer in front of PHP (Symfony HttpCache, Nginx, or Varnish) to support and process ",[27,6968,6969],{},"\u003Cesi:include>"," tags.",[16,6972,6973,6974,6977],{},"If you are running Symfony's built-in HttpCache (the default for many deployments), ensure ",[27,6975,6976],{},"esi: true"," is set in your framework config:",[32,6979,6981],{"className":433,"code":6980,"language":435,"meta":37,"style":37},"# config/packages/framework.yaml\nframework:\n    esi: true\n    fragments: true\n",[27,6982,6983,6988,6995,7004],{"__ignoreMap":37},[41,6984,6985],{"class":43,"line":44},[41,6986,6987],{"class":442},"# config/packages/framework.yaml\n",[41,6989,6990,6993],{"class":43,"line":50},[41,6991,6992],{"class":448},"framework",[41,6994,453],{"class":452},[41,6996,6997,7000,7002],{"class":43,"line":77},[41,6998,6999],{"class":448},"    esi",[41,7001,389],{"class":452},[41,7003,480],{"class":378},[41,7005,7006,7009,7011],{"class":43,"line":83},[41,7007,7008],{"class":448},"    fragments",[41,7010,389],{"class":452},[41,7012,480],{"class":378},[16,7014,7015],{},"For Nginx / Varnish setups, consult your reverse-proxy documentation for ESI processing configuration. If this is skipped, page rendering works but header/footer fragments are missing — an infuriatingly subtle bug that won't show up until you actually click around the storefront.",[16,7017,7018,7019,7022],{},"For a deeper look at how Shopware's HTTP cache layer works and how to tune it, the ",[635,7020,7021],{"href":710},"Shopware 6 performance guide"," covers cache configuration in detail.",[723,7024,7026],{"id":7025},"my-upgrade-checklist","My Upgrade Checklist",[16,7028,7029],{},"Run through this in order on a staging branch before touching production:",[568,7031,7033,7042,7048,7054,7063,7075,7084,7098,7108,7117,7123],{"className":7032},[2949],[571,7034,7036,2958,7038,7041],{"className":7035},[2953],[2955,7037],{"disabled":73,"type":2957},[27,7039,7040],{},"shopware-cli project upgrade-check"," — resolve every reported item",[571,7043,7045,7047],{"className":7044},[2953],[2955,7046],{"disabled":73,"type":2957}," Verify all plugins have 6.7-compatible (Vite) releases; update or disable the rest",[571,7049,7051,7053],{"className":7050},[2953],[2955,7052],{"disabled":73,"type":2957}," Update internal plugins' build config to Vite",[571,7055,7057,7059,7060,7062],{"className":7056},[2953],[2955,7058],{"disabled":73,"type":2957}," Rename Vuex helper imports → ",[27,7061,6707],{}," etc. in admin components",[571,7064,7066,7068,7069,7072,7073],{"className":7065},[2953],[2955,7067],{"disabled":73,"type":2957}," Replace ",[27,7070,7071],{},"$request->get()"," with explicit bags or ",[27,7074,6628],{},[571,7076,7078,7080,7081,7083],{"className":7077},[2953],[2955,7079],{"disabled":73,"type":2957}," Check ",[27,7082,6642],{}," version ≥ 6.1 if Redis is in use",[571,7085,7087,7089,7090,7093,7094,7097],{"className":7086},[2953],[2955,7088],{"disabled":73,"type":2957}," Audit theme CSS for ",[27,7091,7092],{},"div"," → ",[27,7095,7096],{},"ul/li"," element selector breakage",[571,7099,7101,7103,7104,3932,7106],{"className":7100},[2953],[2955,7102],{"disabled":73,"type":2957}," Update custom page types to extend ",[27,7105,6834],{},[27,7107,6837],{},[571,7109,7111,7113,7114,7116],{"className":7110},[2953],[2955,7112],{"disabled":73,"type":2957}," Backfill ",[27,7115,6430],{}," on all payment and shipping methods",[571,7118,7120,7122],{"className":7119},[2953],[2955,7121],{"disabled":73,"type":2957}," Enable ESI in Symfony/Nginx/Varnish config",[571,7124,7126,7128],{"className":7125},[2953],[2955,7127],{"disabled":73,"type":2957}," Full QA: checkout flow, account pages, cart, admin order management",[16,7130,7131,7132,7135],{},"For build-time catches, a solid CI pipeline with PHP static analysis pays dividends here — see ",[635,7133,7134],{"href":2807},"quality tools that matter"," for tool recommendations that surface these classes of error before they hit staging.",[16,7137,7138,7139,7144,7145,7150],{},"The authoritative list of every breaking change is in ",[574,7140,7141],{},[27,7142,7143],{},"UPGRADE-6.7.md"," in the ",[635,7146,7149],{"href":7147,"rel":7148},"https://github.com/shopware/shopware",[639],"shopware/shopware GitHub repo",". If anything in this guide contradicts that file, the file wins.",[723,7152,7154],{"id":7153},"worth-it-absolutely-just-dont-rush-it","Worth It? Absolutely — Just Don't Rush It",[16,7156,7157],{},"Shopware 6.7 is a genuinely better platform — Vite is faster, Pinia is cleaner, and the delayed cache model scales better under load. I've done this migration on a few stores now and every time the result is noticeably snappier admin builds and a less tangled state management story. The upgrade is not particularly hard if you approach it methodically: run the compatibility scanner, fix plugins first, work through the backend changes, then validate the storefront and cache layer.",[16,7159,7160,7161,7163],{},"If you have a complex plugin ecosystem or a heavily customized Shopware installation and want someone to plan and execute the migration without a production outage, that's exactly the kind of work I take on. ",[635,7162,2317],{"href":1339}," and we can scope it together.",[723,7165,1344],{"id":1343},[568,7167,7168,7176,7183,7191,7203],{},[571,7169,7170,7175],{},[635,7171,7174],{"href":7172,"rel":7173},"https://github.com/shopware/shopware/blob/trunk/UPGRADE-6.7.md",[639],"Shopware 6.7 UPGRADE notes (GitHub)"," — the canonical breaking-change list",[571,7177,7178,7182],{},[635,7179,7181],{"href":1913,"rel":7180},[639],"Shopware developer docs"," — plugin development and API reference",[571,7184,7185,7190],{},[635,7186,7189],{"href":7187,"rel":7188},"https://symfony.com/doc/7.0/setup/upgrade_major.html",[639],"Symfony 7.0 upgrade guide"," — covers the Symfony-side breaking changes in depth",[571,7192,7193,7198,7199,7202],{},[635,7194,7197],{"href":7195,"rel":7196},"https://sw-cli.fos.gg/",[639],"Shopware CLI docs"," — ",[27,7200,7201],{},"project upgrade-check"," and other migration utilities",[571,7204,7205,7210],{},[635,7206,7209],{"href":7207,"rel":7208},"https://vitejs.dev/guide/migration",[639],"Vite migration guide"," — Vite API changes relevant to plugin build configs",[680,7212,7213],{},"html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}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 .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}",{"title":37,"searchDepth":50,"depth":50,"links":7215},[7216,7217,7218,7219,7225,7226,7227,7229,7230,7231,7232],{"id":6373,"depth":50,"text":6374},{"id":6401,"depth":50,"text":6402},{"id":6438,"depth":50,"text":6439},{"id":6487,"depth":50,"text":7220,"children":7221},"Symfony 7.4 — Type Declarations and the Death of Request::get()",[7222,7224],{"id":6527,"depth":77,"text":7223},"Request::get() is Gone — Use Explicit Bags",{"id":6632,"depth":77,"text":6633},{"id":6669,"depth":50,"text":6670},{"id":6778,"depth":50,"text":6779},{"id":6853,"depth":50,"text":7228},"Payment & Shipping — technicalName Is Now Non-Nullable",{"id":6951,"depth":50,"text":6952},{"id":7025,"depth":50,"text":7026},{"id":7153,"depth":50,"text":7154},{"id":1343,"depth":50,"text":1344},"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.",{"date":3083,"image":7235,"alt":7236,"tags":7237,"author":1407,"published":73,"featured":73},"/blogs-img/shopware-67-migration.png","Shopware 6.6 to 6.7 migration guide - breaking changes and upgrade path",[522,5579,7238,7239,708],"migration","symfony",{"title":6358,"description":7233},"blogs/6. shopware-67-migration-guide","ysGAbFt2-x-Kkuw55S3KFrV8FagUHqLEGIjsLlKjkjw",{"id":7244,"title":7245,"body":7246,"description":8255,"extension":701,"meta":8256,"navigation":73,"ogImage":8258,"path":907,"seo":8263,"stem":8264,"__hash__":8265},"content/blogs/7. nuxt-4-migration-guide.md","Migrating to Nuxt 4 - The New app/ Directory, Smarter Data Fetching, and a Painless Upgrade",{"type":8,"value":7247,"toc":8238},[7248,7251,7255,7293,7300,7314,7320,7327,7347,7352,7424,7433,7440,7443,7464,7477,7485,7577,7586,7596,7605,7608,7650,7654,7661,7665,7674,7678,7681,7874,7892,7896,7902,7907,7928,7936,7940,7957,7980,7994,7998,8001,8064,8078,8082,8156,8162,8170,8174,8180,8187,8189,8235],[16,7249,7250],{},"I'll be honest — I put off this migration for longer than I should have. \"It'll probably break something\" is the lie I kept telling myself. When I finally sat down and did it, the whole thing took a morning. Nuxt 4 shipped in July 2025 with a clear mandate: make the upgrade painless. Most breaking changes were previewed behind a compatibility flag for over a year, every sharp edge has a config escape hatch, and a codemod handles the mechanical parts. This guide covers everything that actually bit me — and what didn't.",[723,7252,7254],{"id":7253},"what-changed-the-short-version","What Changed (The Short Version)",[568,7256,7257,7265,7277,7287],{},[571,7258,7259,7264],{},[574,7260,7261,7263],{},[27,7262,840],{}," directory"," — application code moves into an isolated subtree; Vite's watcher scope shrinks",[571,7266,7267,7276],{},[574,7268,7269,7270,6511,7273],{},"Smarter ",[27,7271,7272],{},"useAsyncData",[27,7274,7275],{},"useFetch"," — shared keys deduplicate requests, reactive keys auto-refetch, data cleans up on unmount",[571,7278,7279,7282,7283,7286],{},[574,7280,7281],{},"TypeScript multi-project"," — separate TS projects for app / server / shared / builder code; one root ",[27,7284,7285],{},"tsconfig.json"," is all you need",[571,7288,7289,7292],{},[574,7290,7291],{},"Compatibility-first"," — the classic Nuxt 3 layout still works; nothing breaks on a bare version bump",[723,7294,7296,7297,7299],{"id":7295},"the-new-app-directory-why-i-actually-like-it","The New ",[27,7298,840],{}," Directory — Why I Actually Like It",[16,7301,7302,7303,7305,7306,7309,7310,7313],{},"In Nuxt 4, your application code lives inside ",[27,7304,840],{},". The root is reserved for configuration, server code, and content. No more sifting through ",[27,7307,7308],{},"node_modules"," entries and ",[27,7311,7312],{},"server/"," routes just to find a component.",[32,7315,7318],{"className":7316,"code":7317,"language":5529},[5527],"project/\n├── app/                   ← all application code lives here\n│   ├── app.vue\n│   ├── app.config.ts\n│   ├── error.vue\n│   ├── components/\n│   ├── composables/\n│   ├── layouts/\n│   ├── middleware/\n│   ├── pages/\n│   ├── plugins/\n│   └── utils/\n├── content/\n├── public/\n├── server/\n├── shared/\n├── nuxt.config.ts\n└── tsconfig.json\n",[27,7319,7317],{"__ignoreMap":37},[16,7321,7322,7323,7326],{},"This is exactly the layout this blog runs on — and ",[635,7324,7325],{"href":751},"the headless Nuxt + Shopware stack"," I use for storefront work follows the same pattern — so I can tell you from experience it holds up in production.",[16,7328,7329,7332,7333,786,7335,7338,7339,7341,7342,7346],{},[574,7330,7331],{},"Why it's faster:"," Vite watches a smaller directory tree. With ",[27,7334,7308],{},[27,7336,7337],{},".git",", and ",[27,7340,7312],{}," out of scope, HMR is noticeably snappier on large projects. If you care about development-loop performance the same way you care about ",[635,7343,7345],{"href":7344},"/blogs/core-web-vitals-inp-2026","Core Web Vitals in production",", this is worth the migration.",[16,7348,7349],{},[574,7350,7351],{},"Moving your files:",[32,7353,7355],{"className":356,"code":7354,"language":358,"meta":37,"style":37},"mkdir -p app\nmv components composables layouts middleware pages plugins utils app/\nmv app.vue app.config.ts error.vue app/ 2>/dev/null || true\n",[27,7356,7357,7368,7397],{"__ignoreMap":37},[41,7358,7359,7362,7365],{"class":43,"line":44},[41,7360,7361],{"class":365},"mkdir",[41,7363,7364],{"class":378}," -p",[41,7366,7367],{"class":368}," app\n",[41,7369,7370,7373,7376,7379,7382,7385,7388,7391,7394],{"class":43,"line":50},[41,7371,7372],{"class":365},"mv",[41,7374,7375],{"class":368}," components",[41,7377,7378],{"class":368}," composables",[41,7380,7381],{"class":368}," layouts",[41,7383,7384],{"class":368}," middleware",[41,7386,7387],{"class":368}," pages",[41,7389,7390],{"class":368}," plugins",[41,7392,7393],{"class":368}," utils",[41,7395,7396],{"class":368}," app/\n",[41,7398,7399,7401,7404,7407,7410,7413,7416,7419,7422],{"class":43,"line":77},[41,7400,7372],{"class":365},[41,7402,7403],{"class":368}," app.vue",[41,7405,7406],{"class":368}," app.config.ts",[41,7408,7409],{"class":368}," error.vue",[41,7411,7412],{"class":368}," app/",[41,7414,7415],{"class":452}," 2>",[41,7417,7418],{"class":368},"/dev/null",[41,7420,7421],{"class":452}," ||",[41,7423,480],{"class":448},[1731,7425,7426],{},[16,7427,7428,7429,7432],{},"This migration is ",[574,7430,7431],{},"optional",". Nuxt 4 detects the classic Nuxt 3 layout and keeps working. You choose when — or whether — to move.",[723,7434,1485,7436,7439],{"id":7435},"the-alias-gotcha-this-is-the-one-that-got-me",[27,7437,7438],{},"~/"," Alias Gotcha — This Is the One That Got Me",[16,7441,7442],{},"Read this before you do anything else. I wish someone had told me.",[16,7444,7445,2958,7448,2465,7450,7453,7454,7456,7461,7462,1017],{},[574,7446,7447],{},"Before Nuxt 4:",[27,7449,7438],{},[27,7451,7452],{},"@/"," resolved to the project root.",[5872,7455],{},[574,7457,7458,7459,389],{},"After Nuxt 4 with ",[27,7460,840],{}," both resolve to ",[27,7463,840],{},[16,7465,7466,7467,7469,7470,7472,7473,7476],{},"Any file outside ",[27,7468,840],{}," — shared types, root-level helpers, config constants — that you import via ",[27,7471,7438],{}," will throw a \"module not found\" error at build time. I had a handful of shared type files sitting at the project root, all imported with ",[27,7474,7475],{},"~/types/...",", and they all blew up the moment I moved to the new structure. Easy to fix once you know, infuriating to diagnose if you don't.",[16,7478,7479],{},[574,7480,7481,7482,389],{},"Fix option 1 — add a root alias in ",[27,7483,7484],{},"nuxt.config.ts",[32,7486,7488],{"className":919,"code":7487,"language":921,"meta":37,"style":37},"import { fileURLToPath } from 'node:url'\n\nexport default defineNuxtConfig({\n  alias: {\n    '~root': fileURLToPath(new URL('./', import.meta.url)),\n  },\n})\n",[27,7489,7490,7508,7512,7522,7531,7569,7573],{"__ignoreMap":37},[41,7491,7492,7495,7498,7501,7503,7506],{"class":43,"line":44},[41,7493,7494],{"class":452},"import",[41,7496,7497],{"class":942}," { fileURLToPath } ",[41,7499,7500],{"class":452},"from",[41,7502,542],{"class":541},[41,7504,7505],{"class":368},"node:url",[41,7507,548],{"class":541},[41,7509,7510],{"class":43,"line":50},[41,7511,74],{"emptyLinePlaceholder":73},[41,7513,7514,7516,7518,7520],{"class":43,"line":77},[41,7515,933],{"class":452},[41,7517,936],{"class":452},[41,7519,939],{"class":365},[41,7521,943],{"class":942},[41,7523,7524,7527,7529],{"class":43,"line":83},[41,7525,7526],{"class":942},"  alias",[41,7528,389],{"class":452},[41,7530,2601],{"class":942},[41,7532,7533,7535,7538,7540,7542,7545,7547,7550,7553,7555,7557,7560,7562,7564,7566],{"class":43,"line":89},[41,7534,958],{"class":541},[41,7536,7537],{"class":368},"~root",[41,7539,964],{"class":541},[41,7541,389],{"class":452},[41,7543,7544],{"class":365}," fileURLToPath",[41,7546,3222],{"class":942},[41,7548,7549],{"class":3798},"new",[41,7551,7552],{"class":365}," URL",[41,7554,3222],{"class":942},[41,7556,964],{"class":541},[41,7558,7559],{"class":368},"./",[41,7561,964],{"class":541},[41,7563,786],{"class":942},[41,7565,7494],{"class":452},[41,7567,7568],{"class":942},".meta.url)),\n",[41,7570,7571],{"class":43,"line":95},[41,7572,319],{"class":942},[41,7574,7575],{"class":43,"line":142},[41,7576,1002],{"class":942},[16,7578,7579,7580,7093,7583,1017],{},"Then update affected imports: ",[27,7581,7582],{},"~/types/product",[27,7584,7585],{},"~root/types/product",[16,7587,7588],{},[574,7589,7590,7591,6519,7593,389],{},"Fix option 2 — move shared code into ",[27,7592,840],{},[27,7594,7595],{},"shared/",[16,7597,7598,7599,7601,7602,7604],{},"Anything used only by the app belongs in ",[27,7600,840],{},". Anything shared between app and server belongs in ",[27,7603,7595],{}," — Nuxt auto-imports from there and the alias change doesn't affect it.",[16,7606,7607],{},"Run a quick search before migrating:",[32,7609,7611],{"className":356,"code":7610,"language":358,"meta":37,"style":37},"# find imports that cross the app/ boundary\ngrep -r 'from ~/' app/ | grep -v 'from ~/app/'\n",[27,7612,7613,7618],{"__ignoreMap":37},[41,7614,7615],{"class":43,"line":44},[41,7616,7617],{"class":442},"# find imports that cross the app/ boundary\n",[41,7619,7620,7623,7625,7627,7630,7632,7634,7637,7640,7643,7645,7648],{"class":43,"line":50},[41,7621,7622],{"class":365},"grep",[41,7624,6656],{"class":378},[41,7626,542],{"class":541},[41,7628,7629],{"class":368},"from ~/",[41,7631,964],{"class":541},[41,7633,7412],{"class":368},[41,7635,7636],{"class":452}," |",[41,7638,7639],{"class":365}," grep",[41,7641,7642],{"class":378}," -v",[41,7644,542],{"class":541},[41,7646,7647],{"class":368},"from ~/app/",[41,7649,548],{"class":541},[723,7651,7653],{"id":7652},"data-fetching-upgrades-three-things-worth-knowing","Data Fetching Upgrades — Three Things Worth Knowing",[16,7655,7656,2465,7658,7660],{},[27,7657,7272],{},[27,7659,7275],{}," got three meaningful improvements. In my experience, the shared-key deduplication alone pays back the migration cost on any app with nested layouts.",[11,7662,7664],{"id":7663},"shared-keys-no-more-duplicate-requests","Shared Keys — no more duplicate requests",[16,7666,7667,7668,837,7670,7673],{},"Components that call ",[27,7669,7272],{},[574,7671,7672],{},"same key"," now share the result. Previously, two components mounted on the same page each triggered their own request even for identical data. I've seen this cause noticeable flicker in product listing pages — now it just works.",[11,7675,7677],{"id":7676},"reactive-keys-automatic-refetch","Reactive Keys — automatic refetch",[16,7679,7680],{},"Pass a function (or computed) as the key and the composable refetches whenever it changes:",[32,7682,7684],{"className":1093,"code":7683,"language":1095,"meta":37,"style":37},"\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\nconst { data: product } = await useAsyncData(\n  // reactive key — changes when the route param changes\n  () => `product-${route.params.slug}`,\n  () => $fetch(`/api/products/${route.params.slug}`)\n)\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"product\">\n    \u003Ch1>{{ product.name }}\u003C/h1>\n  \u003C/div>\n\u003C/template>\n",[27,7685,7686,7706,7720,7724,7747,7752,7776,7800,7804,7812,7816,7824,7844,7858,7866],{"__ignoreMap":37},[41,7687,7688,7690,7692,7694,7696,7698,7700,7702,7704],{"class":43,"line":44},[41,7689,1102],{"class":942},[41,7691,1105],{"class":452},[41,7693,1109],{"class":1108},[41,7695,1112],{"class":1108},[41,7697,1115],{"class":452},[41,7699,1118],{"class":541},[41,7701,921],{"class":368},[41,7703,1118],{"class":541},[41,7705,1125],{"class":942},[41,7707,7708,7710,7713,7715,7718],{"class":43,"line":50},[41,7709,1130],{"class":452},[41,7711,7712],{"class":942}," route ",[41,7714,1115],{"class":452},[41,7716,7717],{"class":365}," useRoute",[41,7719,1184],{"class":942},[41,7721,7722],{"class":43,"line":77},[41,7723,74],{"emptyLinePlaceholder":73},[41,7725,7726,7728,7731,7733,7736,7738,7741,7744],{"class":43,"line":83},[41,7727,1130],{"class":452},[41,7729,7730],{"class":942}," { data",[41,7732,389],{"class":452},[41,7734,7735],{"class":942}," product } ",[41,7737,1115],{"class":452},[41,7739,7740],{"class":452}," await",[41,7742,7743],{"class":365}," useAsyncData",[41,7745,7746],{"class":942},"(\n",[41,7748,7749],{"class":43,"line":89},[41,7750,7751],{"class":442},"  // reactive key — changes when the route param changes\n",[41,7753,7754,7757,7759,7762,7765,7768,7771,7774],{"class":43,"line":95},[41,7755,7756],{"class":942},"  () ",[41,7758,3765],{"class":452},[41,7760,7761],{"class":368}," `product-",[41,7763,7764],{"class":452},"${",[41,7766,7767],{"class":942},"route.params.slug",[41,7769,7770],{"class":452},"}",[41,7772,7773],{"class":368},"`",[41,7775,982],{"class":942},[41,7777,7778,7780,7782,7785,7787,7790,7792,7794,7796,7798],{"class":43,"line":142},[41,7779,7756],{"class":942},[41,7781,3765],{"class":452},[41,7783,7784],{"class":365}," $fetch",[41,7786,3222],{"class":942},[41,7788,7789],{"class":368},"`/api/products/",[41,7791,7764],{"class":452},[41,7793,7767],{"class":942},[41,7795,7770],{"class":452},[41,7797,7773],{"class":368},[41,7799,3814],{"class":942},[41,7801,7802],{"class":43,"line":148},[41,7803,3814],{"class":942},[41,7805,7806,7808,7810],{"class":43,"line":154},[41,7807,1189],{"class":942},[41,7809,1105],{"class":452},[41,7811,1125],{"class":942},[41,7813,7814],{"class":43,"line":1243},[41,7815,74],{"emptyLinePlaceholder":73},[41,7817,7818,7820,7822],{"class":43,"line":1249},[41,7819,1102],{"class":942},[41,7821,1204],{"class":452},[41,7823,1125],{"class":942},[41,7825,7826,7828,7830,7833,7835,7837,7840,7842],{"class":43,"line":1259},[41,7827,1211],{"class":942},[41,7829,7092],{"class":452},[41,7831,7832],{"class":1108}," v-if",[41,7834,1115],{"class":452},[41,7836,1118],{"class":541},[41,7838,7839],{"class":368},"product",[41,7841,1118],{"class":541},[41,7843,1125],{"class":942},[41,7845,7846,7849,7851,7854,7856],{"class":43,"line":3417},[41,7847,7848],{"class":942},"    \u003C",[41,7850,5592],{"class":452},[41,7852,7853],{"class":942},">{{ product.name }}\u003C/",[41,7855,5592],{"class":452},[41,7857,1125],{"class":942},[41,7859,7860,7862,7864],{"class":43,"line":3441},[41,7861,1252],{"class":942},[41,7863,7092],{"class":452},[41,7865,1125],{"class":942},[41,7867,7868,7870,7872],{"class":43,"line":3446},[41,7869,1189],{"class":942},[41,7871,1204],{"class":452},[41,7873,1125],{"class":942},[16,7875,7876,7877,2416,7880,7883,7884,7887,7888,7891],{},"When the user navigates from ",[27,7878,7879],{},"/products/shirt",[27,7881,7882],{},"/products/jacket",", the key changes, the old data is cleaned up, and a fresh fetch fires — without any manual ",[27,7885,7886],{},"watch",". My rule of thumb used to be \"add a watcher and call ",[27,7889,7890],{},"refresh()","\"; now I just reach for a reactive key.",[11,7893,7895],{"id":7894},"automatic-cleanup","Automatic cleanup",[16,7897,7898,7899,7901],{},"Data registered via ",[27,7900,7272],{}," is now cleared when the owning component unmounts. In long-lived SPAs this was a subtle memory leak; Nuxt 4 handles it transparently. I didn't realise how often I was leaking state until I wasn't anymore.",[16,7903,7904],{},[574,7905,7906],{},"Quick mental model:",[568,7908,7909,7915,7920],{},[571,7910,7911,7914],{},[27,7912,7913],{},"$fetch"," — raw request, runs every time, no caching",[571,7916,7917,7919],{},[27,7918,7275],{}," — smart wrapper; fetches once on server, rehydrates on client",[571,7921,7922,7924,7925,7927],{},[27,7923,7272],{}," — full control; reactive key, manual ",[27,7926,7890],{},", custom transforms",[16,7929,7930,7932,7933,7935],{},[635,7931,1998],{"href":1408}," leans heavily on ",[27,7934,7275],{}," + reactive keys for its catalog pages — the Nuxt 4 upgrade makes that pattern cheaper out of the box.",[723,7937,7939],{"id":7938},"typescript-one-config-four-projects","TypeScript — One Config, Four Projects",[16,7941,7942,7943,7946,7947,786,7950,786,7953,7956],{},"Nuxt 4 sets up ",[574,7944,7945],{},"separate TypeScript projects"," for each environment: ",[27,7948,7949],{},"app",[27,7951,7952],{},"server",[27,7954,7955],{},"shared",", and the Nuxt builder itself. This gives you:",[568,7958,7959,7966,7973],{},[571,7960,7961,7962,7965],{},"Accurate autocompletion — server-only globals like ",[27,7963,7964],{},"H3Event"," don't bleed into component files",[571,7967,7968,7969,7972],{},"Correct inference — no more false \"cannot find name ",[27,7970,7971],{},"useNuxtApp","\" in server routes",[571,7974,7975,7976,7979],{},"Faster ",[27,7977,7978],{},"tsc"," — each project is smaller and checked in isolation",[16,7981,7982,7983,7985,7986,7989,7990,7993],{},"You only maintain one ",[27,7984,7285],{}," at the project root. Nuxt generates the sub-configs in ",[27,7987,7988],{},".nuxt/"," during ",[27,7991,7992],{},"nuxi prepare",". Nothing to hand-edit. I was slightly worried this would make IDE setup more complex — it didn't. If anything, autocomplete in VS Code got more accurate.",[723,7995,7997],{"id":7996},"removed-apis-check-these-before-upgrading","Removed APIs — Check These Before Upgrading",[16,7999,8000],{},"A few things disappeared in Nuxt 4:",[1586,8002,8003,8013],{},[1589,8004,8005],{},[1592,8006,8007,8010],{},[1595,8008,8009],{},"Removed",[1595,8011,8012],{},"What to do",[1605,8014,8015,8032,8049],{},[1592,8016,8017,8022],{},[1610,8018,8019],{},[27,8020,8021],{},"generate.exclude",[1610,8023,8024,8025,8028,8029,8031],{},"Use ",[27,8026,8027],{},"routeRules"," in ",[27,8030,7484],{}," to control pre-rendering per route",[1592,8033,8034,8039],{},[1610,8035,8036],{},[27,8037,8038],{},"generate.routes",[1610,8040,8041,8042,8044,8045,8048],{},"Same — ",[27,8043,8027],{}," or a ",[27,8046,8047],{},"nitro.prerender.routes"," array",[1592,8050,8051,8057],{},[1610,8052,8053,8054],{},"Nuxt 2 compatibility in ",[27,8055,8056],{},"@nuxt/kit",[1610,8058,8059,8060,8063],{},"Module authors: drop legacy ",[27,8061,8062],{},"defineNuxtModule"," patterns that targeted Nuxt 2",[16,8065,8066,8067,8070,8071,8073,8074,8077],{},"If you maintain a custom Nuxt module, run ",[27,8068,8069],{},"npx nuxi module check"," to surface compatibility issues before upgrading the consuming app. I hit the ",[27,8072,8056],{}," one on a client project with a homegrown module — ",[27,8075,8076],{},"nuxi module check"," flagged it immediately.",[723,8079,8081],{"id":8080},"how-i-actually-did-the-upgrade-5-steps","How I Actually Did the Upgrade — 5 Steps",[3113,8083,8084,8093,8114,8128,8145],{},[571,8085,8086,7198,8089,8092],{},[574,8087,8088],{},"Bump the package",[27,8090,8091],{},"bun add nuxt@latest"," (or npm/pnpm equivalent)",[571,8094,8095,8098,8099],{},[574,8096,8097],{},"Run the codemod"," — automates the majority of mechanical changes:\n",[32,8100,8102],{"className":356,"code":8101,"language":358,"meta":37,"style":37},"npx codemod@latest nuxt/4/migration-recipe\n",[27,8103,8104],{"__ignoreMap":37},[41,8105,8106,8108,8111],{"class":43,"line":44},[41,8107,854],{"class":365},[41,8109,8110],{"class":368}," codemod@latest",[41,8112,8113],{"class":368}," nuxt/4/migration-recipe\n",[571,8115,8116,8122,8123,3932,8125,8127],{},[574,8117,8118,8119,8121],{},"Move ",[27,8120,840],{}," code"," — run the ",[27,8124,7361],{},[27,8126,7372],{}," commands from the section above, or skip and stay on the Nuxt 3 layout",[571,8129,8130,8133,8134,8136,8137,8139,8140,8142,8143],{},[574,8131,8132],{},"Fix alias imports"," — audit ",[27,8135,7438],{}," references outside ",[27,8138,840],{},"; apply the ",[27,8141,7537],{}," alias or move files to ",[27,8144,7595],{},[571,8146,8147,8150,8151,786,8153,8155],{},[574,8148,8149],{},"Check removed APIs"," — search for ",[27,8152,8021],{},[27,8154,8038],{},", and any Nuxt 2 module patterns; replace per the table above",[16,8157,8158,8159,8161],{},"Honestly, most projects are done after step 2. The ",[27,8160,840],{}," move and alias fixes are only relevant if you opt into the new directory structure — and given that the alias thing is what got me, I'd say audit your imports first regardless.",[1731,8163,8164],{},[16,8165,8166,8169],{},[574,8167,8168],{},"Support window:"," Nuxt 3 receives support for at least 6 months after Nuxt 4's release date (July 15, 2025). Plan your migration before January 2026; anything after that is on borrowed time.",[723,8171,8173],{"id":8172},"worth-doing","Worth Doing",[16,8175,8176,8177,8179],{},"Nuxt 4 is a well-engineered major version bump — the compatibility flag approach meant most teams hit zero surprises on release day, and that matched my experience. The ",[27,8178,840],{}," directory structure is the biggest conceptual shift, but it's genuinely useful: faster HMR, cleaner project root, and an obvious home for application code. The data fetching improvements pay dividends on any project with nested routes or shared components that hit the same endpoints.",[16,8181,8182,8183,8186],{},"If you're building or migrating a headless Shopware storefront with Nuxt, this is worth doing now rather than carrying the Nuxt 3 layout indefinitely. I help teams navigate exactly these migrations — feel free to ",[635,8184,8185],{"href":1339},"reach out"," if you want a second pair of eyes on your upgrade.",[723,8188,1344],{"id":1343},[568,8190,8191,8199,8213,8224],{},[571,8192,8193,8198],{},[635,8194,8197],{"href":8195,"rel":8196},"https://nuxt.com/docs/getting-started/upgrade",[639],"Nuxt 4 Migration Guide"," — official upgrade docs and compatibility flags",[571,8200,8201,7198,8206,786,8208,786,8210,8212],{},[635,8202,8205],{"href":8203,"rel":8204},"https://nuxt.com/docs/getting-started/data-fetching",[639],"Nuxt Data Fetching Docs",[27,8207,7275],{},[27,8209,7272],{},[27,8211,7913],{}," explained",[571,8214,8215,8220,8221,8223],{},[635,8216,8219],{"href":8217,"rel":8218},"https://nuxt.com/docs/guide/directory-structure/app",[639],"Nuxt Directory Structure"," — the new ",[27,8222,840],{}," directory reference",[571,8225,8226,8122,8231,8234],{},[635,8227,8230],{"href":8228,"rel":8229},"https://codemod.com/",[639],"codemod CLI",[27,8232,8233],{},"nuxt/4/migration-recipe"," from here",[680,8236,8237],{},"html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sLL85, html code.shiki .sLL85{--shiki-default:#8BE9FD}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 .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .sviCg, html code.shiki .sviCg{--shiki-default:#FF79C6;--shiki-default-font-weight:bold}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sY_PY, html code.shiki .sY_PY{--shiki-default:#50FA7B;--shiki-default-font-style:italic}",{"title":37,"searchDepth":50,"depth":50,"links":8239},[8240,8241,8243,8245,8250,8251,8252,8253,8254],{"id":7253,"depth":50,"text":7254},{"id":7295,"depth":50,"text":8242},"The New app/ Directory — Why I Actually Like It",{"id":7435,"depth":50,"text":8244},"The ~/ Alias Gotcha — This Is the One That Got Me",{"id":7652,"depth":50,"text":7653,"children":8246},[8247,8248,8249],{"id":7663,"depth":77,"text":7664},{"id":7676,"depth":77,"text":7677},{"id":7894,"depth":77,"text":7895},{"id":7938,"depth":50,"text":7939},{"id":7996,"depth":50,"text":7997},{"id":8080,"depth":50,"text":8081},{"id":8172,"depth":50,"text":8173},{"id":1343,"depth":50,"text":1344},"Nuxt 4 is here. Learn the new app/ directory structure, improved useFetch/useAsyncData, the path-alias gotcha, and how to upgrade safely with codemods.",{"date":8257,"image":8258,"alt":8259,"tags":8260,"author":1407,"published":73,"featured":73},"10.06.2026","/blogs-img/nuxt-4-migration.png","Migrating to Nuxt 4 - new app directory and data fetching",[1404,8261,1095,7238,8262],"nuxt4","frontend",{"title":7245,"description":8255},"blogs/7. nuxt-4-migration-guide","Ijuj8qIIT7mu1QdfapdlsHPbCqKEcl-3h5zYIAbhr2M",{"id":8267,"title":8268,"body":8269,"description":9630,"extension":701,"meta":9631,"navigation":73,"ogImage":9633,"path":7344,"seo":9639,"stem":9640,"__hash__":9641},"content/blogs/8. core-web-vitals-inp-2026.md","Core Web Vitals 2026 - How to Master INP (Interaction to Next Paint)",{"type":8,"value":8270,"toc":9614},[8271,8274,8277,8281,8288,8308,8314,8317,8321,8365,8372,8375,8379,8386,8389,8392,8398,8402,8408,8437,8440,8603,8610,8616,8623,8627,8631,8638,8720,8730,8811,8817,8821,8827,8895,8898,8901,8905,8908,9036,9039,9043,9046,9164,9167,9207,9210,9214,9221,9474,9479,9483,9486,9489,9492,9549,9556,9558,9561,9564,9567,9572,9574,9611],[16,8272,8273],{},"A client pinged me in a panic because Search Console had flagged INP red across their entire product catalog — overnight, no deployment, nothing changed on their end. That's when I really dug into what INP actually demands, and honestly, I've been auditing for it on every project since.",[16,8275,8276],{},"INP — Interaction to Next Paint — is the Core Web Vital with the lowest pass rate. Roughly 43% of websites still fail its 200ms threshold, which makes it the most commonly missed signal in Google's ranking algorithm. Unlike its predecessor FID, which was easy to game, INP demands real JavaScript architecture work. Here's what it measures and how to fix it.",[723,8278,8280],{"id":8279},"what-inp-actually-measures","What INP Actually Measures",[16,8282,8283,8284,8287],{},"INP captures the ",[574,8285,8286],{},"full interaction lifecycle"," from the moment a user clicks, taps, or presses a key to the moment the browser finishes painting the resulting visual change. That lifecycle has three distinct phases:",[568,8289,8290,8296,8302],{},[571,8291,8292,8295],{},[574,8293,8294],{},"Input delay"," — time from user gesture to when the event handler starts (blocked by other tasks on the main thread)",[571,8297,8298,8301],{},[574,8299,8300],{},"Processing time"," — time the event handler itself takes to run",[571,8303,8304,8307],{},[574,8305,8306],{},"Presentation delay"," — time from handler completion to the next frame being painted",[16,8309,8310,8313],{},[574,8311,8312],{},"FID only measured input delay"," and only for the very first interaction. INP measures all three phases for every interaction across the entire page visit, then reports the worst-case interaction at the 75th percentile of your real users.",[16,8315,8316],{},"I've found that the presentation delay phase catches the most people off guard. You profile your handler, it looks fast, but the browser still takes 400ms to repaint. That's usually a layout thrash or a style recalculation triggered by the handler — something FID would have completely missed.",[723,8318,8320],{"id":8319},"thresholds","Thresholds",[1586,8322,8323,8333],{},[1589,8324,8325],{},[1592,8326,8327,8330],{},[1595,8328,8329],{},"Score",[1595,8331,8332],{},"INP",[1605,8334,8335,8345,8355],{},[1592,8336,8337,8342],{},[1610,8338,8339],{},[574,8340,8341],{},"Good",[1610,8343,8344],{},"≤ 200ms",[1592,8346,8347,8352],{},[1610,8348,8349],{},[574,8350,8351],{},"Needs Improvement",[1610,8353,8354],{},"200 – 500ms",[1592,8356,8357,8362],{},[1610,8358,8359],{},[574,8360,8361],{},"Poor",[1610,8363,8364],{},"> 500ms",[16,8366,8367,8368,8371],{},"\"Good\" means at least ",[574,8369,8370],{},"75% of user interactions"," complete within 200ms. A single sluggish filter dropdown can push your entire site into the red.",[16,8373,8374],{},"My rule of thumb: if a filter or accordion takes a beat to respond on a mid-range Android phone, you're already over 200ms for a big chunk of your users. Don't benchmark on your M3 MacBook.",[723,8376,8378],{"id":8377},"why-so-many-sites-fail-it","Why So Many Sites Fail It",[16,8380,8381,8382,8385],{},"FID was generous — it only cared about one interaction and ignored processing time entirely. Most sites passed with minimal effort. INP changed the contract entirely. Now every accordion, form submission, dropdown, and filter application is scored. ",[574,8383,8384],{},"Third-party scripts"," (analytics, chat widgets, ads, social embeds) compete for the same main thread, extending your input delay without you writing a single line of bad code.",[16,8387,8388],{},"I'll be honest — third-party scripts are always the silent killer. Every time I see a poor INP score on an otherwise clean codebase, the first thing I check is the waterfall of third-party tags loading on page. It's almost never the product code.",[16,8390,8391],{},"The business stakes are real: Google's Web Vitals team found improving INP from 500ms to 200ms correlates with roughly a 22% improvement in user engagement metrics — and it's an official ranking signal. Sites that coast on their old FID pass are now actively losing ground.",[16,8393,8394,8395,8397],{},"If you're working on a Shopware store, this compounds the server-side tuning covered in the ",[635,8396,7021],{"href":710}," — fast TTFB doesn't help if the main thread is jammed after load.",[723,8399,8401],{"id":8400},"how-to-measure-inp","How to Measure INP",[16,8403,8404,8407],{},[574,8405,8406],{},"Field data"," (real users) is the only authoritative source for INP because it depends on actual interaction patterns:",[568,8409,8410,8416,8422,8428],{},[571,8411,8412,8415],{},[574,8413,8414],{},"PageSpeed Insights"," — real-user CrUX data for any URL, plus lab diagnostics",[571,8417,8418,8421],{},[574,8419,8420],{},"Google Search Console"," — Core Web Vitals report with 28-day rolling field data",[571,8423,8424,8427],{},[574,8425,8426],{},"Chrome UX Report (CrUX)"," — raw dataset via BigQuery or API",[571,8429,8430,8436],{},[574,8431,8432,8435],{},[27,8433,8434],{},"web-vitals"," JS library"," — instrument your own users in production",[16,8438,8439],{},"Add this to your Nuxt/TS app to capture INP in real-time:",[32,8441,8443],{"className":919,"code":8442,"language":921,"meta":37,"style":37},"import { onINP } from 'web-vitals'\n\nonINP(({ value, rating, attribution }) => {\n  console.log(`INP: ${value}ms (${rating})`)\n  // Send to your analytics endpoint\n  navigator.sendBeacon('/api/vitals', JSON.stringify({\n    metric: 'INP',\n    value,\n    rating,\n    element: attribution.interactionTargetElement?.tagName,\n  }))\n})\n",[27,8444,8445,8460,8464,8492,8525,8530,8559,8574,8579,8584,8594,8599],{"__ignoreMap":37},[41,8446,8447,8449,8452,8454,8456,8458],{"class":43,"line":44},[41,8448,7494],{"class":452},[41,8450,8451],{"class":942}," { onINP } ",[41,8453,7500],{"class":452},[41,8455,542],{"class":541},[41,8457,8434],{"class":368},[41,8459,548],{"class":541},[41,8461,8462],{"class":43,"line":50},[41,8463,74],{"emptyLinePlaceholder":73},[41,8465,8466,8469,8472,8475,8477,8480,8482,8485,8488,8490],{"class":43,"line":77},[41,8467,8468],{"class":365},"onINP",[41,8470,8471],{"class":942},"(({ ",[41,8473,8474],{"class":1146},"value",[41,8476,786],{"class":942},[41,8478,8479],{"class":1146},"rating",[41,8481,786],{"class":942},[41,8483,8484],{"class":1146},"attribution",[41,8486,8487],{"class":942}," }) ",[41,8489,3765],{"class":452},[41,8491,2601],{"class":942},[41,8493,8494,8497,8500,8502,8505,8507,8509,8511,8514,8516,8518,8520,8523],{"class":43,"line":83},[41,8495,8496],{"class":942},"  console.",[41,8498,8499],{"class":365},"log",[41,8501,3222],{"class":942},[41,8503,8504],{"class":368},"`INP: ",[41,8506,7764],{"class":452},[41,8508,8474],{"class":942},[41,8510,7770],{"class":452},[41,8512,8513],{"class":368},"ms (",[41,8515,7764],{"class":452},[41,8517,8479],{"class":942},[41,8519,7770],{"class":452},[41,8521,8522],{"class":368},")`",[41,8524,3814],{"class":942},[41,8526,8527],{"class":43,"line":89},[41,8528,8529],{"class":442},"  // Send to your analytics endpoint\n",[41,8531,8532,8535,8538,8540,8542,8545,8547,8549,8552,8554,8557],{"class":43,"line":95},[41,8533,8534],{"class":942},"  navigator.",[41,8536,8537],{"class":365},"sendBeacon",[41,8539,3222],{"class":942},[41,8541,964],{"class":541},[41,8543,8544],{"class":368},"/api/vitals",[41,8546,964],{"class":541},[41,8548,786],{"class":942},[41,8550,8551],{"class":378},"JSON",[41,8553,1017],{"class":942},[41,8555,8556],{"class":365},"stringify",[41,8558,943],{"class":942},[41,8560,8561,8564,8566,8568,8570,8572],{"class":43,"line":142},[41,8562,8563],{"class":942},"    metric",[41,8565,389],{"class":452},[41,8567,542],{"class":541},[41,8569,8332],{"class":368},[41,8571,964],{"class":541},[41,8573,982],{"class":942},[41,8575,8576],{"class":43,"line":148},[41,8577,8578],{"class":942},"    value,\n",[41,8580,8581],{"class":43,"line":154},[41,8582,8583],{"class":942},"    rating,\n",[41,8585,8586,8589,8591],{"class":43,"line":1243},[41,8587,8588],{"class":942},"    element",[41,8590,389],{"class":452},[41,8592,8593],{"class":942}," attribution.interactionTargetElement?.tagName,\n",[41,8595,8596],{"class":43,"line":1249},[41,8597,8598],{"class":942},"  }))\n",[41,8600,8601],{"class":43,"line":1259},[41,8602,1002],{"class":942},[16,8604,8605,8606,8609],{},"That ",[27,8607,8608],{},"attribution.interactionTargetElement"," is gold — it tells you exactly which DOM element triggered the slow interaction. I've caught filter checkboxes, autocomplete inputs, and quantity steppers this way. Far faster than guessing.",[16,8611,8612,8615],{},[574,8613,8614],{},"Lab tools"," (Lighthouse, Chrome DevTools Performance panel) can't measure INP directly — there's no scripted interaction to record. Use them to spot long tasks and trace event handler costs, then validate in the field.",[16,8617,8618,8619,8622],{},"After deploying fixes, ",[574,8620,8621],{},"allow 2–4 weeks"," before Search Console reflects the improvement — it runs on a 28-day rolling window. I always warn clients upfront: don't panic when the graph doesn't move the day after a deploy.",[723,8624,8626],{"id":8625},"optimization-playbook","Optimization Playbook",[11,8628,8630],{"id":8629},"yield-long-tasks","Yield Long Tasks",[16,8632,8633,8634,8637],{},"Any task running longer than ",[574,8635,8636],{},"50ms"," blocks the main thread. Break it up and yield between chunks so the browser can process pending input events:",[32,8639,8641],{"className":919,"code":8640,"language":921,"meta":37,"style":37},"async function processLargeList(items: Item[]) {\n  for (const item of items) {\n    processItem(item)\n    // Yield to the main thread after each item\n    await scheduler.yield()\n  }\n}\n",[27,8642,8643,8668,8686,8694,8699,8712,8716],{"__ignoreMap":37},[41,8644,8645,8648,8651,8654,8656,8659,8661,8665],{"class":43,"line":44},[41,8646,8647],{"class":452},"async",[41,8649,8650],{"class":452}," function",[41,8652,8653],{"class":365}," processLargeList",[41,8655,3222],{"class":942},[41,8657,8658],{"class":1146},"items",[41,8660,389],{"class":452},[41,8662,8664],{"class":8663},"sCp4m"," Item",[41,8666,8667],{"class":942},"[]) {\n",[41,8669,8670,8673,8675,8677,8680,8683],{"class":43,"line":50},[41,8671,8672],{"class":452},"  for",[41,8674,3775],{"class":942},[41,8676,1130],{"class":452},[41,8678,8679],{"class":942}," item ",[41,8681,8682],{"class":452},"of",[41,8684,8685],{"class":942}," items) {\n",[41,8687,8688,8691],{"class":43,"line":77},[41,8689,8690],{"class":365},"    processItem",[41,8692,8693],{"class":942},"(item)\n",[41,8695,8696],{"class":43,"line":83},[41,8697,8698],{"class":442},"    // Yield to the main thread after each item\n",[41,8700,8701,8704,8707,8710],{"class":43,"line":89},[41,8702,8703],{"class":452},"    await",[41,8705,8706],{"class":942}," scheduler.",[41,8708,8709],{"class":365},"yield",[41,8711,1184],{"class":942},[41,8713,8714],{"class":43,"line":95},[41,8715,2645],{"class":942},[41,8717,8718],{"class":43,"line":142},[41,8719,135],{"class":942},[16,8721,8722,8725,8726,8729],{},[27,8723,8724],{},"scheduler.yield()"," is the cleanest API. For broader browser support, use a ",[27,8727,8728],{},"setTimeout(0)"," fallback:",[32,8731,8733],{"className":919,"code":8732,"language":921,"meta":37,"style":37},"const yieldToMain = () =>\n  'scheduler' in window\n    ? scheduler.yield()\n    : new Promise\u003Cvoid>(resolve => setTimeout(resolve, 0))\n",[27,8734,8735,8750,8766,8777],{"__ignoreMap":37},[41,8736,8737,8739,8742,8744,8747],{"class":43,"line":44},[41,8738,1130],{"class":452},[41,8740,8741],{"class":365}," yieldToMain",[41,8743,3228],{"class":452},[41,8745,8746],{"class":942}," () ",[41,8748,8749],{"class":452},"=>\n",[41,8751,8752,8755,8758,8760,8763],{"class":43,"line":50},[41,8753,8754],{"class":541},"  '",[41,8756,8757],{"class":368},"scheduler",[41,8759,964],{"class":541},[41,8761,8762],{"class":452}," in",[41,8764,8765],{"class":942}," window\n",[41,8767,8768,8771,8773,8775],{"class":43,"line":77},[41,8769,8770],{"class":452},"    ?",[41,8772,8706],{"class":942},[41,8774,8709],{"class":365},[41,8776,1184],{"class":942},[41,8778,8779,8782,8784,8787,8789,8792,8795,8798,8801,8804,8807,8809],{"class":43,"line":83},[41,8780,8781],{"class":452},"    :",[41,8783,3799],{"class":3798},[41,8785,8786],{"class":8663}," Promise",[41,8788,1102],{"class":942},[41,8790,8791],{"class":8663},"void",[41,8793,8794],{"class":942},">(",[41,8796,8797],{"class":1146},"resolve",[41,8799,8800],{"class":452}," =>",[41,8802,8803],{"class":365}," setTimeout",[41,8805,8806],{"class":942},"(resolve, ",[41,8808,4038],{"class":378},[41,8810,4105],{"class":942},[16,8812,8813,8814,8816],{},"In my experience, adding a single ",[27,8815,8724],{}," call inside a cart recalculation loop dropped a client's checkout INP from over 600ms to under 180ms. Sometimes one line is all it takes — the work was already there, it just wasn't letting the browser breathe.",[11,8818,8820],{"id":8819},"code-split-heavy-bundles","Code-Split Heavy Bundles",[16,8822,8823,8824,389],{},"Large JS payloads inflate processing time. Ship only what's needed for the current interaction using dynamic ",[27,8825,8826],{},"import()",[32,8828,8830],{"className":919,"code":8829,"language":921,"meta":37,"style":37},"button.addEventListener('click', async () => {\n  const { openModal } = await import('./modal')\n  openModal()\n})\n",[27,8831,8832,8859,8884,8891],{"__ignoreMap":37},[41,8833,8834,8837,8840,8842,8844,8847,8849,8851,8853,8855,8857],{"class":43,"line":44},[41,8835,8836],{"class":942},"button.",[41,8838,8839],{"class":365},"addEventListener",[41,8841,3222],{"class":942},[41,8843,964],{"class":541},[41,8845,8846],{"class":368},"click",[41,8848,964],{"class":541},[41,8850,786],{"class":942},[41,8852,8647],{"class":452},[41,8854,8746],{"class":942},[41,8856,3765],{"class":452},[41,8858,2601],{"class":942},[41,8860,8861,8863,8866,8868,8870,8873,8875,8877,8880,8882],{"class":43,"line":50},[41,8862,4003],{"class":452},[41,8864,8865],{"class":942}," { openModal } ",[41,8867,1115],{"class":452},[41,8869,7740],{"class":452},[41,8871,8872],{"class":452}," import",[41,8874,3222],{"class":942},[41,8876,964],{"class":541},[41,8878,8879],{"class":368},"./modal",[41,8881,964],{"class":541},[41,8883,3814],{"class":942},[41,8885,8886,8889],{"class":43,"line":77},[41,8887,8888],{"class":365},"  openModal",[41,8890,1184],{"class":942},[41,8892,8893],{"class":43,"line":83},[41,8894,1002],{"class":942},[16,8896,8897],{},"This defers parsing and execution of the modal module until the user actually triggers it, keeping the initial bundle lean.",[16,8899,8900],{},"I reach for this pattern every time I see a heavy modal or multi-step form that only a fraction of users will open. No reason to pay that parse cost upfront.",[11,8902,8904],{"id":8903},"offload-work-to-web-workers-and-requestidlecallback","Offload Work to Web Workers and requestIdleCallback",[16,8906,8907],{},"CPU-heavy work (search indexing, data transformation, complex calculations) should never block the main thread:",[32,8909,8911],{"className":919,"code":8910,"language":921,"meta":37,"style":37},"// Heavy computation in a worker\nconst worker = new Worker(new URL('./search.worker.ts', import.meta.url))\nworker.postMessage({ query: inputValue })\nworker.onmessage = ({ data }) => renderResults(data.results)\n\n// Non-critical work during browser idle time\nrequestIdleCallback(() => {\n  prefetchNextPageData()\n}, { timeout: 2000 })\n",[27,8912,8913,8918,8954,8970,8995,8999,9004,9016,9023],{"__ignoreMap":37},[41,8914,8915],{"class":43,"line":44},[41,8916,8917],{"class":442},"// Heavy computation in a worker\n",[41,8919,8920,8922,8925,8927,8929,8932,8934,8936,8938,8940,8942,8945,8947,8949,8951],{"class":43,"line":50},[41,8921,1130],{"class":452},[41,8923,8924],{"class":942}," worker ",[41,8926,1115],{"class":452},[41,8928,3799],{"class":3798},[41,8930,8931],{"class":365}," Worker",[41,8933,3222],{"class":942},[41,8935,7549],{"class":3798},[41,8937,7552],{"class":365},[41,8939,3222],{"class":942},[41,8941,964],{"class":541},[41,8943,8944],{"class":368},"./search.worker.ts",[41,8946,964],{"class":541},[41,8948,786],{"class":942},[41,8950,7494],{"class":452},[41,8952,8953],{"class":942},".meta.url))\n",[41,8955,8956,8959,8962,8965,8967],{"class":43,"line":77},[41,8957,8958],{"class":942},"worker.",[41,8960,8961],{"class":365},"postMessage",[41,8963,8964],{"class":942},"({ query",[41,8966,389],{"class":452},[41,8968,8969],{"class":942}," inputValue })\n",[41,8971,8972,8974,8977,8979,8982,8985,8987,8989,8992],{"class":43,"line":83},[41,8973,8958],{"class":942},[41,8975,8976],{"class":365},"onmessage",[41,8978,3228],{"class":452},[41,8980,8981],{"class":942}," ({ ",[41,8983,8984],{"class":1146},"data",[41,8986,8487],{"class":942},[41,8988,3765],{"class":452},[41,8990,8991],{"class":365}," renderResults",[41,8993,8994],{"class":942},"(data.results)\n",[41,8996,8997],{"class":43,"line":89},[41,8998,74],{"emptyLinePlaceholder":73},[41,9000,9001],{"class":43,"line":95},[41,9002,9003],{"class":442},"// Non-critical work during browser idle time\n",[41,9005,9006,9009,9012,9014],{"class":43,"line":142},[41,9007,9008],{"class":365},"requestIdleCallback",[41,9010,9011],{"class":942},"(() ",[41,9013,3765],{"class":452},[41,9015,2601],{"class":942},[41,9017,9018,9021],{"class":43,"line":148},[41,9019,9020],{"class":365},"  prefetchNextPageData",[41,9022,1184],{"class":942},[41,9024,9025,9028,9030,9033],{"class":43,"line":154},[41,9026,9027],{"class":942},"}, { timeout",[41,9029,389],{"class":452},[41,9031,9032],{"class":378}," 2000",[41,9034,9035],{"class":942}," })\n",[16,9037,9038],{},"Web Workers are one of those things I avoided for years because the setup felt fiddly. Once I started using them for faceted search indexes, I wondered why I'd waited so long. The main thread is just cleaner.",[11,9040,9042],{"id":9041},"defer-and-facade-third-party-scripts","Defer and Facade Third-Party Scripts",[16,9044,9045],{},"Third-party scripts are the silent INP killers. Don't load them until they're actually needed:",[32,9047,9049],{"className":919,"code":9048,"language":921,"meta":37,"style":37},"// Facade: show a static placeholder, load the real widget on interaction\nchatButton.addEventListener('click', async () => {\n  const script = document.createElement('script')\n  script.src = 'https://cdn.chatwidget.example/widget.js'\n  document.head.appendChild(script)\n  script.onload = () => window.ChatWidget.open()\n}, { once: true })\n",[27,9050,9051,9056,9081,9106,9120,9131,9153],{"__ignoreMap":37},[41,9052,9053],{"class":43,"line":44},[41,9054,9055],{"class":442},"// Facade: show a static placeholder, load the real widget on interaction\n",[41,9057,9058,9061,9063,9065,9067,9069,9071,9073,9075,9077,9079],{"class":43,"line":50},[41,9059,9060],{"class":942},"chatButton.",[41,9062,8839],{"class":365},[41,9064,3222],{"class":942},[41,9066,964],{"class":541},[41,9068,8846],{"class":368},[41,9070,964],{"class":541},[41,9072,786],{"class":942},[41,9074,8647],{"class":452},[41,9076,8746],{"class":942},[41,9078,3765],{"class":452},[41,9080,2601],{"class":942},[41,9082,9083,9085,9088,9090,9093,9096,9098,9100,9102,9104],{"class":43,"line":77},[41,9084,4003],{"class":452},[41,9086,9087],{"class":942}," script ",[41,9089,1115],{"class":452},[41,9091,9092],{"class":942}," document.",[41,9094,9095],{"class":365},"createElement",[41,9097,3222],{"class":942},[41,9099,964],{"class":541},[41,9101,1105],{"class":368},[41,9103,964],{"class":541},[41,9105,3814],{"class":942},[41,9107,9108,9111,9113,9115,9118],{"class":43,"line":83},[41,9109,9110],{"class":942},"  script.src ",[41,9112,1115],{"class":452},[41,9114,542],{"class":541},[41,9116,9117],{"class":368},"https://cdn.chatwidget.example/widget.js",[41,9119,548],{"class":541},[41,9121,9122,9125,9128],{"class":43,"line":89},[41,9123,9124],{"class":942},"  document.head.",[41,9126,9127],{"class":365},"appendChild",[41,9129,9130],{"class":942},"(script)\n",[41,9132,9133,9136,9139,9141,9143,9145,9148,9151],{"class":43,"line":95},[41,9134,9135],{"class":942},"  script.",[41,9137,9138],{"class":365},"onload",[41,9140,3228],{"class":452},[41,9142,8746],{"class":942},[41,9144,3765],{"class":452},[41,9146,9147],{"class":942}," window.ChatWidget.",[41,9149,9150],{"class":365},"open",[41,9152,1184],{"class":942},[41,9154,9155,9158,9160,9162],{"class":43,"line":142},[41,9156,9157],{"class":942},"}, { once",[41,9159,389],{"class":452},[41,9161,4483],{"class":378},[41,9163,9035],{"class":942},[16,9165,9166],{},"For analytics and ad scripts, load them after the page is interactive:",[32,9168,9172],{"className":9169,"code":9170,"language":9171,"meta":37,"style":37},"language-html shiki shiki-themes dracula","\u003C!-- Defer non-critical scripts entirely -->\n\u003Cscript src=\"https://analytics.example/tracker.js\" defer>\u003C/script>\n","html",[27,9173,9174,9179],{"__ignoreMap":37},[41,9175,9176],{"class":43,"line":44},[41,9177,9178],{"class":442},"\u003C!-- Defer non-critical scripts entirely -->\n",[41,9180,9181,9183,9185,9188,9190,9192,9195,9197,9200,9203,9205],{"class":43,"line":50},[41,9182,1102],{"class":942},[41,9184,1105],{"class":452},[41,9186,9187],{"class":1108}," src",[41,9189,1115],{"class":452},[41,9191,1118],{"class":541},[41,9193,9194],{"class":368},"https://analytics.example/tracker.js",[41,9196,1118],{"class":541},[41,9198,9199],{"class":1108}," defer",[41,9201,9202],{"class":942},">\u003C/",[41,9204,1105],{"class":452},[41,9206,1125],{"class":942},[16,9208,9209],{},"The facade pattern feels slightly hacky the first time you implement it, but it's the right call. Users who never click the chat button never pay for that widget's JavaScript at all.",[11,9211,9213],{"id":9212},"debounce-handlers-and-use-passive-listeners","Debounce Handlers and Use Passive Listeners",[16,9215,9216,9217,9220],{},"Expensive event handlers (search-as-you-type, scroll-triggered updates) must be debounced. Scroll and touch listeners should be ",[27,9218,9219],{},"passive"," to avoid blocking gesture handling:",[32,9222,9224],{"className":919,"code":9223,"language":921,"meta":37,"style":37},"const debounce = \u003CT extends unknown[]>(fn: (...args: T) => void, delay: number) => {\n  let timer: ReturnType\u003Ctypeof setTimeout>\n  return (...args: T) => {\n    clearTimeout(timer)\n    timer = setTimeout(() => fn(...args), delay)\n  }\n}\n\nconst handleSearch = debounce(async (query: string) => {\n  const results = await fetchSearchResults(query)\n  renderResults(results)\n}, 200)\n\n// Passive listener — tells the browser this handler won't call preventDefault()\ndocument.addEventListener('touchstart', onTouch, { passive: true })\n",[27,9225,9226,9291,9311,9331,9339,9362,9366,9370,9374,9405,9422,9430,9440,9444,9449],{"__ignoreMap":37},[41,9227,9228,9230,9233,9235,9238,9241,9244,9247,9250,9253,9255,9257,9260,9263,9265,9268,9270,9272,9275,9277,9280,9282,9285,9287,9289],{"class":43,"line":44},[41,9229,1130],{"class":452},[41,9231,9232],{"class":365}," debounce",[41,9234,3228],{"class":452},[41,9236,9237],{"class":942}," \u003C",[41,9239,9240],{"class":1146},"T",[41,9242,9243],{"class":452}," extends",[41,9245,9246],{"class":8663}," unknown",[41,9248,9249],{"class":942},"[]>(",[41,9251,9252],{"class":365},"fn",[41,9254,389],{"class":452},[41,9256,3775],{"class":942},[41,9258,9259],{"class":452},"...",[41,9261,9262],{"class":1146},"args",[41,9264,389],{"class":452},[41,9266,9267],{"class":8663}," T",[41,9269,3762],{"class":942},[41,9271,3765],{"class":452},[41,9273,9274],{"class":8663}," void",[41,9276,786],{"class":942},[41,9278,9279],{"class":1146},"delay",[41,9281,389],{"class":452},[41,9283,9284],{"class":8663}," number",[41,9286,3762],{"class":942},[41,9288,3765],{"class":452},[41,9290,2601],{"class":942},[41,9292,9293,9295,9298,9300,9303,9305,9308],{"class":43,"line":50},[41,9294,3837],{"class":452},[41,9296,9297],{"class":942}," timer",[41,9299,389],{"class":452},[41,9301,9302],{"class":8663}," ReturnType",[41,9304,1102],{"class":942},[41,9306,9307],{"class":452},"typeof",[41,9309,9310],{"class":942}," setTimeout>\n",[41,9312,9313,9315,9317,9319,9321,9323,9325,9327,9329],{"class":43,"line":77},[41,9314,3615],{"class":452},[41,9316,3775],{"class":942},[41,9318,9259],{"class":452},[41,9320,9262],{"class":1146},[41,9322,389],{"class":452},[41,9324,9267],{"class":8663},[41,9326,3762],{"class":942},[41,9328,3765],{"class":452},[41,9330,2601],{"class":942},[41,9332,9333,9336],{"class":43,"line":83},[41,9334,9335],{"class":365},"    clearTimeout",[41,9337,9338],{"class":942},"(timer)\n",[41,9340,9341,9344,9346,9348,9350,9352,9355,9357,9359],{"class":43,"line":89},[41,9342,9343],{"class":942},"    timer ",[41,9345,1115],{"class":452},[41,9347,8803],{"class":365},[41,9349,9011],{"class":942},[41,9351,3765],{"class":452},[41,9353,9354],{"class":365}," fn",[41,9356,3222],{"class":942},[41,9358,9259],{"class":452},[41,9360,9361],{"class":942},"args), delay)\n",[41,9363,9364],{"class":43,"line":95},[41,9365,2645],{"class":942},[41,9367,9368],{"class":43,"line":142},[41,9369,135],{"class":942},[41,9371,9372],{"class":43,"line":148},[41,9373,74],{"emptyLinePlaceholder":73},[41,9375,9376,9378,9381,9383,9385,9387,9389,9391,9394,9396,9399,9401,9403],{"class":43,"line":154},[41,9377,1130],{"class":452},[41,9379,9380],{"class":942}," handleSearch ",[41,9382,1115],{"class":452},[41,9384,9232],{"class":365},[41,9386,3222],{"class":942},[41,9388,8647],{"class":452},[41,9390,3775],{"class":942},[41,9392,9393],{"class":1146},"query",[41,9395,389],{"class":452},[41,9397,9398],{"class":8663}," string",[41,9400,3762],{"class":942},[41,9402,3765],{"class":452},[41,9404,2601],{"class":942},[41,9406,9407,9409,9412,9414,9416,9419],{"class":43,"line":1243},[41,9408,4003],{"class":452},[41,9410,9411],{"class":942}," results ",[41,9413,1115],{"class":452},[41,9415,7740],{"class":452},[41,9417,9418],{"class":365}," fetchSearchResults",[41,9420,9421],{"class":942},"(query)\n",[41,9423,9424,9427],{"class":43,"line":1249},[41,9425,9426],{"class":365},"  renderResults",[41,9428,9429],{"class":942},"(results)\n",[41,9431,9432,9435,9438],{"class":43,"line":1259},[41,9433,9434],{"class":942},"}, ",[41,9436,9437],{"class":378},"200",[41,9439,3814],{"class":942},[41,9441,9442],{"class":43,"line":3417},[41,9443,74],{"emptyLinePlaceholder":73},[41,9445,9446],{"class":43,"line":3441},[41,9447,9448],{"class":442},"// Passive listener — tells the browser this handler won't call preventDefault()\n",[41,9450,9451,9454,9456,9458,9460,9463,9465,9468,9470,9472],{"class":43,"line":3446},[41,9452,9453],{"class":942},"document.",[41,9455,8839],{"class":365},[41,9457,3222],{"class":942},[41,9459,964],{"class":541},[41,9461,9462],{"class":368},"touchstart",[41,9464,964],{"class":541},[41,9466,9467],{"class":942},", onTouch, { passive",[41,9469,389],{"class":452},[41,9471,4483],{"class":378},[41,9473,9035],{"class":942},[16,9475,1485,9476,9478],{},[27,9477,9219],{}," flag is one of those small wins that costs nothing to add. Every time I skip it and someone later profiles a scroll-heavy page, it shows up. I just add it by default now.",[723,9480,9482],{"id":9481},"for-vuenuxt-stores-specifically","For Vue/Nuxt Stores Specifically",[16,9484,9485],{},"Nuxt's SSR → hydration cycle is a prime INP risk window. During hydration, Vue is replaying event bindings and component setup on top of server-rendered HTML. Any user interaction that lands during this window will experience inflated input delay.",[16,9487,9488],{},"What burned me once was a heavy product listing component that eagerly set up several watchers during hydration — users on slower connections who interacted before hydration finished would get 700ms+ responses. It looked fine in Lighthouse. It only surfaced in field data.",[16,9490,9491],{},"Key tactics:",[568,9493,9494,9512,9527,9540],{},[571,9495,9496,9499,9500,9503,9504,9507,9508,9511],{},[574,9497,9498],{},"Lazy-load heavy components"," with ",[27,9501,9502],{},"\u003CLazyMyComponent />"," (Nuxt's auto-import prefix) or ",[27,9505,9506],{},"defineAsyncComponent",". The ",[635,9509,9510],{"href":907},"Nuxt 4 migration guide"," covers the updated lazy hydration APIs worth adopting.",[571,9513,9514,9517,9518,8044,9521,9523,9524,1017],{},[574,9515,9516],{},"Defer non-critical composable setup"," — move analytics initialization, A/B test evaluation, and prefetch logic into ",[27,9519,9520],{},"onNuxtReady",[27,9522,9008],{}," call inside ",[27,9525,9526],{},"onMounted",[571,9528,9529,9535,9536,9539],{},[574,9530,9531,9532,9534],{},"Avoid large synchronous ",[27,9533,7886],{}," handlers"," on reactive state tied to UI interactions — split them into ",[27,9537,9538],{},"watchEffect"," with a yield, or offload to a worker.",[571,9541,9542,9545,9546,9548],{},[574,9543,9544],{},"Check your hydration payload size"," — oversized ",[27,9547,7272],{}," payloads inflate the JS parse budget and delay when the page becomes interactive.",[16,9550,9551,9552,9555],{},"Going ",[635,9553,9554],{"href":751},"headless with Shopware + Nuxt"," already removes a large chunk of server-rendered PHP overhead from the equation; the remaining INP budget is entirely yours to control on the frontend.",[723,9557,672],{"id":671},[16,9559,9560],{},"INP is the performance metric that separates sites built for real users from sites built to pass a one-time audit. The fixes aren't glamorous — yield the main thread, split bundles, defer third parties — but they're concrete and the payoff is measurable. Many sites see a 30–50% INP improvement in under eight hours of focused work.",[16,9562,9563],{},"I've gone through this process enough times now to know the pattern: field data reveals the worst offender, DevTools traces it, one of the techniques above fixes it. Repeat until you're green. It's not magic, it's just methodical.",[16,9565,9566],{},"If your store is running JavaScript-heavy interactions and hasn't been audited for long tasks, start with PageSpeed Insights field data today. Find the worst-offending interaction, trace it in DevTools, and apply the yield pattern. The ranking and engagement gains follow from there.",[16,9568,9569,9570,1017],{},"Need a targeted performance audit or a headless frontend that starts with good INP by design? ",[635,9571,1340],{"href":1339},[723,9573,1344],{"id":1343},[568,9575,9576,9583,9590,9597,9604],{},[571,9577,9578],{},[635,9579,9582],{"href":9580,"rel":9581},"https://web.dev/articles/inp",[639],"Interaction to Next Paint (INP) — web.dev",[571,9584,9585],{},[635,9586,9589],{"href":9587,"rel":9588},"https://web.dev/articles/optimize-inp",[639],"Optimize INP — web.dev",[571,9591,9592],{},[635,9593,9596],{"href":9594,"rel":9595},"https://developers.google.com/search/docs/appearance/core-web-vitals",[639],"Core Web Vitals — Google Search Central",[571,9598,9599],{},[635,9600,9603],{"href":9601,"rel":9602},"https://web.dev/articles/vitals-measurement-getting-started",[639],"web-vitals JS library — GitHub/web.dev",[571,9605,9606],{},[635,9607,9610],{"href":9608,"rel":9609},"https://web.dev/articles/optimize-long-tasks",[639],"Yield to the main thread — web.dev",[680,9612,9613],{},"html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .sGEwX, html code.shiki .sGEwX{--shiki-default:#FFB86C;--shiki-default-font-style:italic}html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}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 .sCp4m, html code.shiki .sCp4m{--shiki-default:#8BE9FD;--shiki-default-font-style:italic}html pre.shiki code .sviCg, html code.shiki .sviCg{--shiki-default:#FF79C6;--shiki-default-font-weight:bold}html pre.shiki code .sY_PY, html code.shiki .sY_PY{--shiki-default:#50FA7B;--shiki-default-font-style:italic}",{"title":37,"searchDepth":50,"depth":50,"links":9615},[9616,9617,9618,9619,9620,9627,9628,9629],{"id":8279,"depth":50,"text":8280},{"id":8319,"depth":50,"text":8320},{"id":8377,"depth":50,"text":8378},{"id":8400,"depth":50,"text":8401},{"id":8625,"depth":50,"text":8626,"children":9621},[9622,9623,9624,9625,9626],{"id":8629,"depth":77,"text":8630},{"id":8819,"depth":77,"text":8820},{"id":8903,"depth":77,"text":8904},{"id":9041,"depth":77,"text":9042},{"id":9212,"depth":77,"text":9213},{"id":9481,"depth":50,"text":9482},{"id":671,"depth":50,"text":672},{"id":1343,"depth":50,"text":1344},"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.",{"date":9632,"image":9633,"alt":9634,"tags":9635,"author":1407,"published":73,"featured":709},"04.06.2026","/blogs-img/core-web-vitals-inp.png","Master INP Interaction to Next Paint Core Web Vitals 2026",[706,9636,9637,9638,277],"core-web-vitals","inp","seo",{"title":8268,"description":9630},"blogs/8. core-web-vitals-inp-2026","DHlMdkjBOqKDgjvXb-8TtnJ76DURFkhpYfRfmRNWo5M",{"id":9643,"title":9644,"body":9645,"description":10599,"extension":701,"meta":10600,"navigation":73,"ogImage":10602,"path":1276,"seo":10606,"stem":10607,"__hash__":10608},"content/blogs/9. ai-product-search-shopware.md","AI-Powered Product Search in Shopware - Semantic and Vector Search That Converts",{"type":8,"value":9646,"toc":10582},[9647,9650,9654,9657,9660,9663,9667,9678,9681,9684,9695,9698,9702,9705,9714,9719,9723,9726,9730,9733,9751,9762,9766,9769,9775,9779,9834,9837,9841,9844,10171,10185,10189,10438,10448,10452,10455,10458,10468,10474,10477,10481,10487,10497,10503,10509,10513,10520,10527,10529,10532,10535,10541,10543,10579],[16,9648,9649],{},"The query that finally convinced me default search was costing a client real sales was something like \"warm jacket for winter hiking\". Zero results. The store had plenty of insulated parkas and mountain softshells — but none of them used those exact words. The customer bounced. I watched it happen in a session recording and that was that. Shopware's built-in product search is a token matcher: it finds products whose text contains the exact words in the query. That works fine for \"Nike Air Max 90\". It fails badly for anything a human would actually say. Semantic search understands intent instead of tokens, and layering it onto an existing Shopware setup is more approachable than it sounds.",[723,9651,9653],{"id":9652},"why-keyword-search-loses-the-sale","Why Keyword Search Loses the Sale",[16,9655,9656],{},"Full-text and BM25 search work by matching terms. The query must share vocabulary with the indexed document. When a customer describes their need in natural language — or speaks in a different register than your product copy — results fall apart.",[16,9658,9659],{},"Edge cases pile up faster than you'd expect: synonyms, plurals, abbreviations, brand names in inconsistent capitalisations, and non-English queries in a multilingual store. I've found that every store I've audited for search quality has a long tail of zero-results queries that nobody ever cleaned up, because keyword search makes the fix feel impossible — you'd have to add synonyms and aliases forever.",[16,9661,9662],{},"The practical consequence is a search results page that looks empty or irrelevant, which your analytics surface as a high zero-results rate or low click-through from search. Both are direct revenue signals.",[723,9664,9666],{"id":9665},"how-semantic-search-works-embeddings-in-plain-terms","How Semantic Search Works: Embeddings in Plain Terms",[16,9668,9669,9670,9673,9674,9677],{},"An ",[574,9671,9672],{},"embedding model"," takes any piece of text and outputs a fixed-length vector of floating-point numbers — say, 768 or 1536 dimensions. The model is trained such that texts with similar ",[1421,9675,9676],{},"meaning"," end up close to each other in that high-dimensional space, regardless of exact word overlap.",[16,9679,9680],{},"Concretely: \"insulated mountain parka\" and \"warm jacket for winter hiking\" land near each other. \"Running shoes\" and \"casual sneakers\" are moderately close. \"Running shoes\" and \"ceramic mixing bowl\" are far apart.",[16,9682,9683],{},"The search operation then becomes:",[3113,9685,9686,9689,9692],{},[571,9687,9688],{},"Embed the user's query → get a query vector.",[571,9690,9691],{},"Find the stored product vectors with the highest cosine similarity to the query vector.",[571,9693,9694],{},"Return those products, ranked by similarity score.",[16,9696,9697],{},"That's the entire core idea. Everything else — hybrid scoring, re-ranking, index freshness — is engineering around it. I like explaining it this way because it demystifies the \"AI\" part: at query time you're doing nearest-neighbour lookup, not running a language model on every request.",[723,9699,9701],{"id":9700},"pure-vector-vs-hybrid-search-and-why-i-always-start-with-hybrid","Pure Vector vs. Hybrid Search — and Why I Always Start with Hybrid",[16,9703,9704],{},"Pure vector search is great for intent-driven queries but can mis-rank for exact lookups. If a customer types a specific SKU or brand name, a vector search might surface conceptually related products rather than the exact match. For e-commerce, that's a real problem. I learned this the hard way on a build where we shipped pure vector and immediately had complaints from customers who typed exact product codes and got something adjacent instead.",[16,9706,9707,9710,9711,9713],{},[574,9708,9709],{},"Hybrid search"," combines a keyword/BM25 score with a vector similarity score and merges the ranked lists — commonly with Reciprocal Rank Fusion (RRF) or a weighted linear combination. You get exact-match precision from keyword search ",[1421,9712,2434],{}," intent-understanding from the vector branch. In practice, hybrid consistently outperforms pure vector for retail catalogs.",[1731,9715,9716],{},[16,9717,9718],{},"My rule of thumb: always start with hybrid, never pure vector. You can tune keyword vs. vector weight per category or query type once you have real traffic data. Shipping pure vector first is a trap.",[723,9720,9722],{"id":9721},"architecture-in-a-shopware-world","Architecture in a Shopware World",[16,9724,9725],{},"Shopware runs on MySQL by default. MySQL has no native vector index. Your vector store lives outside Shopware — the question is how to bridge the two.",[11,9727,9729],{"id":9728},"option-a-search-microservice-middleware","Option A: Search Microservice / Middleware",[16,9731,9732],{},"A small standalone service (Node/Bun, Python, or Go) does three things:",[568,9734,9735,9741],{},[571,9736,9737,9740],{},[574,9738,9739],{},"Indexing",": subscribes to Shopware product-write events (or runs a scheduled re-index), fetches product data from the Store API or Admin API, embeds it, and upserts vectors into the vector store.",[571,9742,9743,9746,9747,9750],{},[574,9744,9745],{},"Query",": exposes a ",[27,9748,9749],{},"/search"," endpoint your storefront calls. It embeds the query, queries the vector store, optionally merges with a keyword pass, and returns ranked product IDs. The storefront then hydrates full product data from Shopware's Store API by ID.",[16,9752,9753,9754,9757,9758,9761],{},"For a ",[635,9755,9756],{"href":751},"headless Shopware + Nuxt setup",", this fits naturally: the Nuxt frontend calls your search service and the Store API independently. If you're using ",[635,9759,9760],{"href":1408},"Shopware Frontends as your composable storefront",", you can intercept the search composable to route through your service.",[11,9763,9765],{"id":9764},"option-b-shopware-plugin","Option B: Shopware Plugin",[16,9767,9768],{},"A PHP plugin that decorates the product search route handler. The plugin embeds the query (calling out to an embedding API), queries the vector store, and merges the result with Shopware's native search. Keeps everything in one deployable unit but mixes PHP with an AI I/O path, which complicates latency management.",[16,9770,9771,9774],{},[574,9772,9773],{},"My recommendation",": the microservice approach is easier to scale, iterate on, and debug independently of Shopware. I've used both. The plugin approach sounds appealing until you need to tune embedding models or swap vector stores — at that point you're refactoring core Shopware plumbing. Go with the plugin only if you need tight integration with Shopware's admin configuration UI.",[11,9776,9778],{"id":9777},"vector-store-options","Vector Store Options",[1586,9780,9781,9791],{},[1589,9782,9783],{},[1592,9784,9785,9788],{},[1595,9786,9787],{},"Store",[1595,9789,9790],{},"Notes",[1605,9792,9793,9804,9814,9824],{},[1592,9794,9795,9801],{},[1610,9796,9797,9800],{},[574,9798,9799],{},"pgvector"," (Postgres extension)",[1610,9802,9803],{},"Simple if you already run Postgres elsewhere; good enough for catalogs up to hundreds of thousands of products",[1592,9805,9806,9811],{},[1610,9807,9808],{},[574,9809,9810],{},"OpenSearch / Elasticsearch kNN",[1610,9812,9813],{},"Strong hybrid story; many teams already run this for search",[1592,9815,9816,9821],{},[1610,9817,9818],{},[574,9819,9820],{},"Qdrant / Weaviate / Milvus",[1610,9822,9823],{},"Purpose-built; excellent filter+vector combination; more operational overhead",[1592,9825,9826,9831],{},[1610,9827,9828],{},[574,9829,9830],{},"Pinecone",[1610,9832,9833],{},"Managed; zero ops; per-query cost",[16,9835,9836],{},"For most Shopware stores, pgvector or OpenSearch is the least-new-infrastructure choice. I've reached for pgvector when a client already ran a Postgres instance for something else — it's a single extension install and you're done.",[723,9838,9840],{"id":9839},"building-it-indexing-pipeline","Building It: Indexing Pipeline",[16,9842,9843],{},"The indexing step fetches products, builds a text representation, embeds it, and upserts into the vector store. Here's illustrative TypeScript:",[32,9845,9847],{"className":919,"code":9846,"language":921,"meta":37,"style":37},"// indexing.ts — run on schedule or triggered by product-written events\n\nasync function indexProducts(shopwareProducts: ShopwareProduct[]) {\n  const vectors: VectorRecord[] = []\n\n  for (const product of shopwareProducts) {\n    // Concatenate the fields that describe the product semantically\n    const text = [\n      product.name,\n      product.description,\n      product.manufacturer?.name,\n      product.categories?.map((c) => c.name).join(' '),\n      product.properties?.map((p) => `${p.group} ${p.name}`).join(' '),\n    ]\n      .filter(Boolean)\n      .join(' | ')\n\n    const embedding = await embedText(text) // calls your chosen embedding API/model\n\n    vectors.push({\n      id: product.id,\n      vector: embedding,\n      payload: { name: product.name, price: product.calculatedPrice?.gross },\n    })\n  }\n\n  await vectorStore.upsert(vectors)\n}\n",[27,9848,9849,9854,9858,9879,9898,9902,9918,9923,9934,9939,9944,9949,9981,10029,10034,10045,10062,10066,10086,10090,10100,10110,10120,10140,10145,10149,10153,10167],{"__ignoreMap":37},[41,9850,9851],{"class":43,"line":44},[41,9852,9853],{"class":442},"// indexing.ts — run on schedule or triggered by product-written events\n",[41,9855,9856],{"class":43,"line":50},[41,9857,74],{"emptyLinePlaceholder":73},[41,9859,9860,9862,9864,9867,9869,9872,9874,9877],{"class":43,"line":77},[41,9861,8647],{"class":452},[41,9863,8650],{"class":452},[41,9865,9866],{"class":365}," indexProducts",[41,9868,3222],{"class":942},[41,9870,9871],{"class":1146},"shopwareProducts",[41,9873,389],{"class":452},[41,9875,9876],{"class":8663}," ShopwareProduct",[41,9878,8667],{"class":942},[41,9880,9881,9883,9886,9888,9891,9894,9896],{"class":43,"line":83},[41,9882,4003],{"class":452},[41,9884,9885],{"class":942}," vectors",[41,9887,389],{"class":452},[41,9889,9890],{"class":8663}," VectorRecord",[41,9892,9893],{"class":942},"[] ",[41,9895,1115],{"class":452},[41,9897,3592],{"class":942},[41,9899,9900],{"class":43,"line":89},[41,9901,74],{"emptyLinePlaceholder":73},[41,9903,9904,9906,9908,9910,9913,9915],{"class":43,"line":95},[41,9905,8672],{"class":452},[41,9907,3775],{"class":942},[41,9909,1130],{"class":452},[41,9911,9912],{"class":942}," product ",[41,9914,8682],{"class":452},[41,9916,9917],{"class":942}," shopwareProducts) {\n",[41,9919,9920],{"class":43,"line":142},[41,9921,9922],{"class":442},"    // Concatenate the fields that describe the product semantically\n",[41,9924,9925,9927,9930,9932],{"class":43,"line":148},[41,9926,4573],{"class":452},[41,9928,9929],{"class":942}," text ",[41,9931,1115],{"class":452},[41,9933,953],{"class":942},[41,9935,9936],{"class":43,"line":154},[41,9937,9938],{"class":942},"      product.name,\n",[41,9940,9941],{"class":43,"line":1243},[41,9942,9943],{"class":942},"      product.description,\n",[41,9945,9946],{"class":43,"line":1249},[41,9947,9948],{"class":942},"      product.manufacturer?.name,\n",[41,9950,9951,9954,9957,9959,9962,9964,9966,9969,9972,9974,9976,9978],{"class":43,"line":1259},[41,9952,9953],{"class":942},"      product.categories?.",[41,9955,9956],{"class":365},"map",[41,9958,3757],{"class":942},[41,9960,9961],{"class":1146},"c",[41,9963,3762],{"class":942},[41,9965,3765],{"class":452},[41,9967,9968],{"class":942}," c.name).",[41,9970,9971],{"class":365},"join",[41,9973,3222],{"class":942},[41,9975,964],{"class":541},[41,9977,542],{"class":541},[41,9979,9980],{"class":942},"),\n",[41,9982,9983,9986,9988,9990,9992,9994,9996,9999,10001,10004,10006,10009,10012,10014,10016,10019,10021,10023,10025,10027],{"class":43,"line":3417},[41,9984,9985],{"class":942},"      product.properties?.",[41,9987,9956],{"class":365},[41,9989,3757],{"class":942},[41,9991,16],{"class":1146},[41,9993,3762],{"class":942},[41,9995,3765],{"class":452},[41,9997,9998],{"class":368}," `",[41,10000,7764],{"class":452},[41,10002,10003],{"class":942},"p.group",[41,10005,7770],{"class":452},[41,10007,10008],{"class":452}," ${",[41,10010,10011],{"class":942},"p.name",[41,10013,7770],{"class":452},[41,10015,7773],{"class":368},[41,10017,10018],{"class":942},").",[41,10020,9971],{"class":365},[41,10022,3222],{"class":942},[41,10024,964],{"class":541},[41,10026,542],{"class":541},[41,10028,9980],{"class":942},[41,10030,10031],{"class":43,"line":3441},[41,10032,10033],{"class":942},"    ]\n",[41,10035,10036,10039,10042],{"class":43,"line":3446},[41,10037,10038],{"class":942},"      .",[41,10040,10041],{"class":365},"filter",[41,10043,10044],{"class":942},"(Boolean)\n",[41,10046,10047,10049,10051,10053,10055,10058,10060],{"class":43,"line":3451},[41,10048,10038],{"class":942},[41,10050,9971],{"class":365},[41,10052,3222],{"class":942},[41,10054,964],{"class":541},[41,10056,10057],{"class":368}," | ",[41,10059,964],{"class":541},[41,10061,3814],{"class":942},[41,10063,10064],{"class":43,"line":3456},[41,10065,74],{"emptyLinePlaceholder":73},[41,10067,10068,10070,10073,10075,10077,10080,10083],{"class":43,"line":3466},[41,10069,4573],{"class":452},[41,10071,10072],{"class":942}," embedding ",[41,10074,1115],{"class":452},[41,10076,7740],{"class":452},[41,10078,10079],{"class":365}," embedText",[41,10081,10082],{"class":942},"(text) ",[41,10084,10085],{"class":442},"// calls your chosen embedding API/model\n",[41,10087,10088],{"class":43,"line":3473},[41,10089,74],{"emptyLinePlaceholder":73},[41,10091,10092,10095,10098],{"class":43,"line":3490},[41,10093,10094],{"class":942},"    vectors.",[41,10096,10097],{"class":365},"push",[41,10099,943],{"class":942},[41,10101,10102,10105,10107],{"class":43,"line":3495},[41,10103,10104],{"class":942},"      id",[41,10106,389],{"class":452},[41,10108,10109],{"class":942}," product.id,\n",[41,10111,10112,10115,10117],{"class":43,"line":3946},[41,10113,10114],{"class":942},"      vector",[41,10116,389],{"class":452},[41,10118,10119],{"class":942}," embedding,\n",[41,10121,10122,10125,10127,10130,10132,10135,10137],{"class":43,"line":3952},[41,10123,10124],{"class":942},"      payload",[41,10126,389],{"class":452},[41,10128,10129],{"class":942}," { name",[41,10131,389],{"class":452},[41,10133,10134],{"class":942}," product.name, price",[41,10136,389],{"class":452},[41,10138,10139],{"class":942}," product.calculatedPrice?.gross },\n",[41,10141,10142],{"class":43,"line":3966},[41,10143,10144],{"class":942},"    })\n",[41,10146,10147],{"class":43,"line":3976},[41,10148,2645],{"class":942},[41,10150,10151],{"class":43,"line":3981},[41,10152,74],{"emptyLinePlaceholder":73},[41,10154,10155,10158,10161,10164],{"class":43,"line":3986},[41,10156,10157],{"class":452},"  await",[41,10159,10160],{"class":942}," vectorStore.",[41,10162,10163],{"class":365},"upsert",[41,10165,10166],{"class":942},"(vectors)\n",[41,10168,10169],{"class":43,"line":4000},[41,10170,135],{"class":942},[16,10172,10173,10176,10177,10180,10181,10184],{},[27,10174,10175],{},"embedText()"," is a thin wrapper around whichever embedding backend you chose — OpenAI's ",[27,10178,10179],{},"text-embedding-3-small",", a self-hosted ",[27,10182,10183],{},"bge-m3"," via a local HTTP API, or a sentence-transformers endpoint. The wrapper is the only place that changes when you swap models. I've found that making this seam explicit from the start saves a painful refactor later.",[723,10186,10188],{"id":10187},"building-it-query-path","Building It: Query Path",[32,10190,10192],{"className":919,"code":10191,"language":921,"meta":37,"style":37},"// search.ts — called by your storefront or API route\n\nasync function semanticSearch(query: string, limit = 20): Promise\u003CSearchResult[]> {\n  // 1. Embed the user query with the same model used at index time\n  const queryVector = await embedText(query)\n\n  // 2. Vector search — returns product IDs + similarity scores\n  const vectorHits = await vectorStore.search(queryVector, { limit: limit * 2 })\n\n  // 3. Optional: keyword pass against Shopware's own search for exact matches\n  const keywordHits = await shopwareSearchByKeyword(query, { limit: limit * 2 })\n\n  // 4. Merge with Reciprocal Rank Fusion\n  const merged = reciprocalRankFusion([vectorHits, keywordHits], { limit })\n\n  // 5. Hydrate full product data from Store API by ranked IDs\n  const productIds = merged.map((h) => h.id)\n  const products = await fetchProductsByIds(productIds) // Store API POST /store-api/product\n\n  return rankPreserving(products, productIds)\n}\n",[27,10193,10194,10199,10203,10245,10250,10265,10269,10274,10305,10309,10314,10341,10345,10350,10365,10369,10374,10400,10420,10424,10434],{"__ignoreMap":37},[41,10195,10196],{"class":43,"line":44},[41,10197,10198],{"class":442},"// search.ts — called by your storefront or API route\n",[41,10200,10201],{"class":43,"line":50},[41,10202,74],{"emptyLinePlaceholder":73},[41,10204,10205,10207,10209,10212,10214,10216,10218,10220,10222,10225,10227,10230,10233,10235,10237,10239,10242],{"class":43,"line":77},[41,10206,8647],{"class":452},[41,10208,8650],{"class":452},[41,10210,10211],{"class":365}," semanticSearch",[41,10213,3222],{"class":942},[41,10215,9393],{"class":1146},[41,10217,389],{"class":452},[41,10219,9398],{"class":8663},[41,10221,786],{"class":942},[41,10223,10224],{"class":1146},"limit",[41,10226,3228],{"class":452},[41,10228,10229],{"class":378}," 20",[41,10231,10232],{"class":942},")",[41,10234,389],{"class":452},[41,10236,8786],{"class":8663},[41,10238,1102],{"class":942},[41,10240,10241],{"class":1146},"SearchResult",[41,10243,10244],{"class":942},"[]> {\n",[41,10246,10247],{"class":43,"line":83},[41,10248,10249],{"class":442},"  // 1. Embed the user query with the same model used at index time\n",[41,10251,10252,10254,10257,10259,10261,10263],{"class":43,"line":89},[41,10253,4003],{"class":452},[41,10255,10256],{"class":942}," queryVector ",[41,10258,1115],{"class":452},[41,10260,7740],{"class":452},[41,10262,10079],{"class":365},[41,10264,9421],{"class":942},[41,10266,10267],{"class":43,"line":95},[41,10268,74],{"emptyLinePlaceholder":73},[41,10270,10271],{"class":43,"line":142},[41,10272,10273],{"class":442},"  // 2. Vector search — returns product IDs + similarity scores\n",[41,10275,10276,10278,10281,10283,10285,10287,10290,10293,10295,10298,10300,10303],{"class":43,"line":148},[41,10277,4003],{"class":452},[41,10279,10280],{"class":942}," vectorHits ",[41,10282,1115],{"class":452},[41,10284,7740],{"class":452},[41,10286,10160],{"class":942},[41,10288,10289],{"class":365},"search",[41,10291,10292],{"class":942},"(queryVector, { limit",[41,10294,389],{"class":452},[41,10296,10297],{"class":942}," limit ",[41,10299,3341],{"class":452},[41,10301,10302],{"class":378}," 2",[41,10304,9035],{"class":942},[41,10306,10307],{"class":43,"line":154},[41,10308,74],{"emptyLinePlaceholder":73},[41,10310,10311],{"class":43,"line":1243},[41,10312,10313],{"class":442},"  // 3. Optional: keyword pass against Shopware's own search for exact matches\n",[41,10315,10316,10318,10321,10323,10325,10328,10331,10333,10335,10337,10339],{"class":43,"line":1249},[41,10317,4003],{"class":452},[41,10319,10320],{"class":942}," keywordHits ",[41,10322,1115],{"class":452},[41,10324,7740],{"class":452},[41,10326,10327],{"class":365}," shopwareSearchByKeyword",[41,10329,10330],{"class":942},"(query, { limit",[41,10332,389],{"class":452},[41,10334,10297],{"class":942},[41,10336,3341],{"class":452},[41,10338,10302],{"class":378},[41,10340,9035],{"class":942},[41,10342,10343],{"class":43,"line":1259},[41,10344,74],{"emptyLinePlaceholder":73},[41,10346,10347],{"class":43,"line":3417},[41,10348,10349],{"class":442},"  // 4. Merge with Reciprocal Rank Fusion\n",[41,10351,10352,10354,10357,10359,10362],{"class":43,"line":3441},[41,10353,4003],{"class":452},[41,10355,10356],{"class":942}," merged ",[41,10358,1115],{"class":452},[41,10360,10361],{"class":365}," reciprocalRankFusion",[41,10363,10364],{"class":942},"([vectorHits, keywordHits], { limit })\n",[41,10366,10367],{"class":43,"line":3446},[41,10368,74],{"emptyLinePlaceholder":73},[41,10370,10371],{"class":43,"line":3451},[41,10372,10373],{"class":442},"  // 5. Hydrate full product data from Store API by ranked IDs\n",[41,10375,10376,10378,10381,10383,10386,10388,10390,10393,10395,10397],{"class":43,"line":3456},[41,10377,4003],{"class":452},[41,10379,10380],{"class":942}," productIds ",[41,10382,1115],{"class":452},[41,10384,10385],{"class":942}," merged.",[41,10387,9956],{"class":365},[41,10389,3757],{"class":942},[41,10391,10392],{"class":1146},"h",[41,10394,3762],{"class":942},[41,10396,3765],{"class":452},[41,10398,10399],{"class":942}," h.id)\n",[41,10401,10402,10404,10407,10409,10411,10414,10417],{"class":43,"line":3466},[41,10403,4003],{"class":452},[41,10405,10406],{"class":942}," products ",[41,10408,1115],{"class":452},[41,10410,7740],{"class":452},[41,10412,10413],{"class":365}," fetchProductsByIds",[41,10415,10416],{"class":942},"(productIds) ",[41,10418,10419],{"class":442},"// Store API POST /store-api/product\n",[41,10421,10422],{"class":43,"line":3473},[41,10423,74],{"emptyLinePlaceholder":73},[41,10425,10426,10428,10431],{"class":43,"line":3490},[41,10427,3615],{"class":452},[41,10429,10430],{"class":365}," rankPreserving",[41,10432,10433],{"class":942},"(products, productIds)\n",[41,10435,10436],{"class":43,"line":3495},[41,10437,135],{"class":942},[16,10439,10440,10443,10444,10447],{},[27,10441,10442],{},"fetchProductsByIds"," hits the Store API's product endpoint. Importantly, ",[574,10445,10446],{},"Shopware's pricing, stock, and visibility rules apply at hydration time"," — you never bypass them by going through your own search layer. That's one of the things I appreciate about this architecture: the trust boundary stays clear.",[723,10449,10451],{"id":10450},"keeping-the-index-fresh-where-projects-quietly-rot","Keeping the Index Fresh — Where Projects Quietly Rot",[16,10453,10454],{},"This is the part that gets underestimated on almost every build I've seen. The indexing pipeline gets built, the demo looks great, and then six months later someone notices that a product relaunched under a new name is returning stale results. Index freshness is where the project either holds up or quietly falls apart.",[16,10456,10457],{},"Two complementary mechanisms:",[16,10459,10460,10463,10464,10467],{},[574,10461,10462],{},"Shopware message queue / event subscribers",": Write a subscriber for ",[27,10465,10466],{},"EntityWrittenEvent"," on the product entity. On write, push the product ID onto a queue. A worker processes the queue by re-embedding and upserting only changed products.",[16,10469,10470,10473],{},[574,10471,10472],{},"Scheduled full re-index",": Run a nightly or weekly full re-index to catch anything that slipped through — category renames, property group changes, manufacturer updates that ripple across products.",[16,10475,10476],{},"Keep the re-index job idempotent: upsert by product ID, don't delete-and-recreate unless you change the embedding model (which requires a full catalog re-embed). Every time I've skipped the scheduled full re-index \"to save costs\", I've regretted it within a quarter.",[723,10478,10480],{"id":10479},"cost-latency-and-multilingual-caveats","Cost, Latency, and Multilingual Caveats",[16,10482,10483,10486],{},[574,10484,10485],{},"Embedding cost",": Every product edit triggers a re-embed API call if you're using a hosted model. For a large catalog with frequent price updates, batch product fields carefully — don't re-embed on price-only changes if price isn't part of your embedding text. Cache embeddings; they're deterministic for the same text.",[16,10488,10489,10492,10493,10496],{},[574,10490,10491],{},"Query latency",": An embedding API call adds round-trip time. Mitigate by keeping the embedding model close (self-hosted in the same region) or by caching query embeddings for popular search terms. The ",[635,10494,10495],{"href":710},"Shopware performance guide"," covers caching strategies that apply equally here.",[16,10498,10499,10502],{},[574,10500,10501],{},"Self-hosted vs. hosted models",": Hosted APIs (OpenAI, Cohere, etc.) are operationally easy and produce high-quality embeddings, but every query and every product re-index sends data to a third party. Self-hosted models (sentence-transformers, BGE, E5 family) keep data in your infrastructure and have no per-call cost after initial deployment, but require you to manage GPU/CPU resources and model versioning. I've done both — for data-sensitive clients, self-hosted is non-negotiable regardless of the overhead.",[16,10504,10505,10508],{},[574,10506,10507],{},"Multilingual stores",": Embeddings are language-sensitive. A model trained primarily on English will produce poor semantic alignment across German, French, or Dutch product text. Use a multilingual model (mBERT, multilingual-e5, LaBSE, paraphrase-multilingual) or maintain per-language indexes. Don't use a single English-only model for a multilingual catalog and expect reasonable results. I've seen this bite teams who benchmarked on English only and shipped to a German storefront — the semantic quality is genuinely bad in ways that aren't obvious until real users start complaining.",[723,10510,10512],{"id":10511},"whats-next-rag-and-conversational-discovery","What's Next: RAG and Conversational Discovery",[16,10514,10515,10516,10519],{},"Once you have product embeddings and a retrieval layer, you have the building blocks for ",[574,10517,10518],{},"RAG-style product Q&A",": a customer asks \"What's a good gift for a trail runner who already has shoes?\", a language model retrieves semantically relevant products from your index and generates a contextual recommendation. This is a natural next step once your vector pipeline is stable — I wouldn't jump straight to it, but it's a worthwhile north star to keep in mind when you're making architecture decisions early on.",[16,10521,10522,10523,10526],{},"Faceted filters still compose cleanly with vector search — filter by category/brand/price ",[1421,10524,10525],{},"before"," the nearest-neighbour pass to keep the candidate set relevant and bounded.",[723,10528,672],{"id":671},[16,10530,10531],{},"Keyword search is a liability when your customers shop by intent rather than vocabulary. The technical path to fixing it — embeddings, a vector store, hybrid merging — is straightforward to layer onto Shopware without replacing what works. The architecture is a microservice that subscribes to product changes, keeps a vector index, and answers queries your storefront already knows how to call.",[16,10533,10534],{},"The main engineering discipline is keeping the index fresh and choosing the right embedding model for your catalog's language mix. Measure success in conversion rate and click-through from search results, not just subjective \"relevance\" — those are the numbers that justify the infrastructure cost.",[16,10536,10537,10538,1017],{},"If you're running a headless Shopware store and want to implement semantic search, or want a second opinion on your architecture before committing to a vector DB, ",[635,10539,10540],{"href":1339},"I'm happy to talk it through",[723,10542,1344],{"id":1343},[568,10544,10545,10551,10558,10565,10572],{},[571,10546,10547],{},[635,10548,10550],{"href":1359,"rel":10549},[639],"Shopware Store API — Product Listings",[571,10552,10553],{},[635,10554,10557],{"href":10555,"rel":10556},"https://developer.shopware.com/docs/concepts/api/admin-api.html",[639],"Shopware Admin API — Product Entity",[571,10559,10560],{},[635,10561,10564],{"href":10562,"rel":10563},"https://developer.shopware.com/docs/guides/hosting/infrastructure/message-queue.html",[639],"Shopware Message Queue Documentation",[571,10566,10567],{},[635,10568,10571],{"href":10569,"rel":10570},"https://github.com/pgvector/pgvector",[639],"pgvector — open-source vector similarity for Postgres",[571,10573,10574],{},[635,10575,10578],{"href":10576,"rel":10577},"https://opensearch.org/docs/latest/search-plugins/knn/index/",[639],"OpenSearch kNN Search Documentation",[680,10580,10581],{},"html pre.shiki code .shSDL, html code.shiki .shSDL{--shiki-default:#6272A4}html pre.shiki code .s0Tla, html code.shiki .s0Tla{--shiki-default:#FF79C6}html pre.shiki code .sAOxA, html code.shiki .sAOxA{--shiki-default:#50FA7B}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sGEwX, html code.shiki .sGEwX{--shiki-default:#FFB86C;--shiki-default-font-style:italic}html pre.shiki code .sCp4m, html code.shiki .sCp4m{--shiki-default:#8BE9FD;--shiki-default-font-style:italic}html pre.shiki code .seVfx, html code.shiki .seVfx{--shiki-default:#E9F284}html pre.shiki code .s-mGx, html code.shiki .s-mGx{--shiki-default:#F1FA8C}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 .sIQBb, html code.shiki .sIQBb{--shiki-default:#BD93F9}",{"title":37,"searchDepth":50,"depth":50,"links":10583},[10584,10585,10586,10587,10592,10593,10594,10595,10596,10597,10598],{"id":9652,"depth":50,"text":9653},{"id":9665,"depth":50,"text":9666},{"id":9700,"depth":50,"text":9701},{"id":9721,"depth":50,"text":9722,"children":10588},[10589,10590,10591],{"id":9728,"depth":77,"text":9729},{"id":9764,"depth":77,"text":9765},{"id":9777,"depth":77,"text":9778},{"id":9839,"depth":50,"text":9840},{"id":10187,"depth":50,"text":10188},{"id":10450,"depth":50,"text":10451},{"id":10479,"depth":50,"text":10480},{"id":10511,"depth":50,"text":10512},{"id":671,"depth":50,"text":672},{"id":1343,"depth":50,"text":1344},"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.",{"date":10601,"image":10602,"alt":10603,"tags":10604,"author":1407,"published":73,"featured":73},"09.06.2026","/blogs-img/ai-product-search.png","AI-powered semantic and vector product search in Shopware 6",[522,3090,10289,708,10605],"machine-learning",{"title":9644,"description":10599},"blogs/9. ai-product-search-shopware","xLoeaGZX5DK5sF2xKDqSRRig0leukqdfgLsN21hynM0",1781205300418]