[{"data":1,"prerenderedAt":1022},["ShallowReactive",2],{"blog-\u002Fblog\u002Fheadless-ecommerce-nuxt-payload":3},{"id":4,"title":5,"body":6,"date":1009,"description":1010,"extension":1011,"icon":1012,"meta":1013,"navigation":92,"path":1014,"readingTime":180,"seo":1015,"stem":1016,"tags":1017,"__hash__":1021},"content\u002Fblog\u002Fheadless-ecommerce-nuxt-payload.md","Headless e-commerce with Nuxt and Payload CMS — what works, what doesn't",{"type":7,"value":8,"toc":1002},"minimark",[9,19,22,27,30,33,491,494,498,501,504,852,855,859,862,960,968,971,975,978,985,988,992,998],[10,11,12,13,18],"p",{},"I recently wrapped up the development of ",[14,15,17],"a",{"href":16},"\u002Fprojects\u002Fsweet-layer","Sweet Layer",", a bespoke headless e-commerce platform designed to handle complex product customisation and high-volume checkout traffic. The stack choice was deliberate: Nuxt 4 for the storefront architecture and Payload CMS for the administrative data engine. While the final build is incredibly fast, getting a headless e-commerce system to perform seamlessly requires navigating a lot of structural friction that the platform documentation conveniently glosses over.",[10,20,21],{},"Going headless means you inherit full responsibility for state synchronisation, inventory race conditions, and session management across distinct network boundaries. If you don't map out your content schemas and caching layers correctly from day one, you end up with a brittle architecture that is harder to maintain than a monolithic legacy setup.",[23,24,26],"h2",{"id":25},"why-payload-won-the-backend-selection","Why Payload won the backend selection",[10,28,29],{},"Before settling on Payload CMS for the backend engine, I carefully evaluated both Strapi and Directus. Strapi is highly popular, but its auto-generated REST and GraphQL APIs often feel restrictive when you need to inject custom Express or Fastify middleware directly into the core application lifecycle. Directus is an excellent database wrapper, but its content localisation workflows didn't feel as intuitive for the non-technical team managing day-to-day operations.",[10,31,32],{},"Payload won the selection because it is entirely code-first, written in TypeScript, and embeds natively into a clean MongoDB or PostgreSQL instance without abstracting away the database layer.",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-typescript shiki shiki-themes tokyo-night","\u002F\u002F A snippet of the localised product schema in Payload\nimport { CollectionConfig } from 'payload\u002Ftypes';\n\nexport const Products: CollectionConfig = {\n  slug: 'products',\n  admin: { useAsTitle: 'title' },\n  fields: [\n    { name: 'title', type: 'text', required: true, localized: true },\n    { name: 'sku', type: 'text', required: true, unique: true },\n    {\n      name: 'variants',\n      type: 'array',\n      fields: [\n        { name: 'name', type: 'text', required: true },\n        { name: 'price', type: 'number', required: true },\n        { name: 'stock', type: 'number', required: true, min: 0 }\n      ]\n    }\n  ]\n};\n","typescript","",[41,42,43,52,87,94,121,140,167,178,234,283,289,306,323,333,375,416,465,471,477,483],"code",{"__ignoreMap":39},[44,45,48],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sbD-w","\u002F\u002F A snippet of the localised product schema in Payload\n",[44,53,55,59,63,67,70,73,77,81,84],{"class":46,"line":54},2,[44,56,58],{"class":57},"su0L4","import",[44,60,62],{"class":61},"sgJMe"," { ",[44,64,66],{"class":65},"sySf4","CollectionConfig",[44,68,69],{"class":61}," }",[44,71,72],{"class":57}," from",[44,74,76],{"class":75},"sAklC"," '",[44,78,80],{"class":79},"sPY7s","payload\u002Ftypes",[44,82,83],{"class":75},"'",[44,85,86],{"class":75},";\n",[44,88,90],{"class":46,"line":89},3,[44,91,93],{"emptyLinePlaceholder":92},true,"\n",[44,95,97,100,104,108,111,115,118],{"class":46,"line":96},4,[44,98,99],{"class":57},"export",[44,101,103],{"class":102},"sN7LL"," const",[44,105,107],{"class":106},"sd1Qi"," Products",[44,109,110],{"class":75},":",[44,112,114],{"class":113},"sE3pS"," CollectionConfig",[44,116,117],{"class":75}," =",[44,119,120],{"class":61}," {\n",[44,122,124,128,130,132,135,137],{"class":46,"line":123},5,[44,125,127],{"class":126},"syYvs","  slug",[44,129,110],{"class":75},[44,131,76],{"class":75},[44,133,134],{"class":79},"products",[44,136,83],{"class":75},[44,138,139],{"class":75},",\n",[44,141,143,146,148,151,154,156,158,161,163,165],{"class":46,"line":142},6,[44,144,145],{"class":126},"  admin",[44,147,110],{"class":75},[44,149,150],{"class":61}," {",[44,152,153],{"class":126}," useAsTitle",[44,155,110],{"class":75},[44,157,76],{"class":75},[44,159,160],{"class":79},"title",[44,162,83],{"class":75},[44,164,69],{"class":61},[44,166,139],{"class":75},[44,168,170,173,175],{"class":46,"line":169},7,[44,171,172],{"class":126},"  fields",[44,174,110],{"class":75},[44,176,177],{"class":61}," [\n",[44,179,181,184,187,189,191,193,195,198,201,203,205,208,210,212,215,217,221,223,226,228,230,232],{"class":46,"line":180},8,[44,182,183],{"class":61},"    {",[44,185,186],{"class":126}," name",[44,188,110],{"class":75},[44,190,76],{"class":75},[44,192,160],{"class":79},[44,194,83],{"class":75},[44,196,197],{"class":75},",",[44,199,200],{"class":126}," type",[44,202,110],{"class":75},[44,204,76],{"class":75},[44,206,207],{"class":79},"text",[44,209,83],{"class":75},[44,211,197],{"class":75},[44,213,214],{"class":126}," required",[44,216,110],{"class":75},[44,218,220],{"class":219},"sOJ5S"," true",[44,222,197],{"class":75},[44,224,225],{"class":126}," localized",[44,227,110],{"class":75},[44,229,220],{"class":219},[44,231,69],{"class":61},[44,233,139],{"class":75},[44,235,237,239,241,243,245,248,250,252,254,256,258,260,262,264,266,268,270,272,275,277,279,281],{"class":46,"line":236},9,[44,238,183],{"class":61},[44,240,186],{"class":126},[44,242,110],{"class":75},[44,244,76],{"class":75},[44,246,247],{"class":79},"sku",[44,249,83],{"class":75},[44,251,197],{"class":75},[44,253,200],{"class":126},[44,255,110],{"class":75},[44,257,76],{"class":75},[44,259,207],{"class":79},[44,261,83],{"class":75},[44,263,197],{"class":75},[44,265,214],{"class":126},[44,267,110],{"class":75},[44,269,220],{"class":219},[44,271,197],{"class":75},[44,273,274],{"class":126}," unique",[44,276,110],{"class":75},[44,278,220],{"class":219},[44,280,69],{"class":61},[44,282,139],{"class":75},[44,284,286],{"class":46,"line":285},10,[44,287,288],{"class":61},"    {\n",[44,290,292,295,297,299,302,304],{"class":46,"line":291},11,[44,293,294],{"class":126},"      name",[44,296,110],{"class":75},[44,298,76],{"class":75},[44,300,301],{"class":79},"variants",[44,303,83],{"class":75},[44,305,139],{"class":75},[44,307,309,312,314,316,319,321],{"class":46,"line":308},12,[44,310,311],{"class":126},"      type",[44,313,110],{"class":75},[44,315,76],{"class":75},[44,317,318],{"class":79},"array",[44,320,83],{"class":75},[44,322,139],{"class":75},[44,324,326,329,331],{"class":46,"line":325},13,[44,327,328],{"class":126},"      fields",[44,330,110],{"class":75},[44,332,177],{"class":61},[44,334,336,339,342,344,346,349,351,353,355,357,359,361,363,365,367,369,371,373],{"class":46,"line":335},14,[44,337,338],{"class":61},"        {",[44,340,186],{"class":341},"sJ0GE",[44,343,110],{"class":75},[44,345,76],{"class":75},[44,347,348],{"class":79},"name",[44,350,83],{"class":75},[44,352,197],{"class":75},[44,354,200],{"class":341},[44,356,110],{"class":75},[44,358,76],{"class":75},[44,360,207],{"class":79},[44,362,83],{"class":75},[44,364,197],{"class":75},[44,366,214],{"class":341},[44,368,110],{"class":75},[44,370,220],{"class":219},[44,372,69],{"class":61},[44,374,139],{"class":75},[44,376,378,380,382,384,386,389,391,393,395,397,399,402,404,406,408,410,412,414],{"class":46,"line":377},15,[44,379,338],{"class":61},[44,381,186],{"class":341},[44,383,110],{"class":75},[44,385,76],{"class":75},[44,387,388],{"class":79},"price",[44,390,83],{"class":75},[44,392,197],{"class":75},[44,394,200],{"class":341},[44,396,110],{"class":75},[44,398,76],{"class":75},[44,400,401],{"class":79},"number",[44,403,83],{"class":75},[44,405,197],{"class":75},[44,407,214],{"class":341},[44,409,110],{"class":75},[44,411,220],{"class":219},[44,413,69],{"class":61},[44,415,139],{"class":75},[44,417,419,421,423,425,427,430,432,434,436,438,440,442,444,446,448,450,452,454,457,459,462],{"class":46,"line":418},16,[44,420,338],{"class":61},[44,422,186],{"class":341},[44,424,110],{"class":75},[44,426,76],{"class":75},[44,428,429],{"class":79},"stock",[44,431,83],{"class":75},[44,433,197],{"class":75},[44,435,200],{"class":341},[44,437,110],{"class":75},[44,439,76],{"class":75},[44,441,401],{"class":79},[44,443,83],{"class":75},[44,445,197],{"class":75},[44,447,214],{"class":341},[44,449,110],{"class":75},[44,451,220],{"class":219},[44,453,197],{"class":75},[44,455,456],{"class":341}," min",[44,458,110],{"class":75},[44,460,461],{"class":219}," 0",[44,463,464],{"class":61}," }\n",[44,466,468],{"class":46,"line":467},17,[44,469,470],{"class":61},"      ]\n",[44,472,474],{"class":46,"line":473},18,[44,475,476],{"class":61},"    }\n",[44,478,480],{"class":46,"line":479},19,[44,481,482],{"class":61},"  ]\n",[44,484,486,489],{"class":46,"line":485},20,[44,487,488],{"class":61},"}",[44,490,86],{"class":75},[10,492,493],{},"Defining the database schema directly in TypeScript files meant I could export the exact type definitions from the backend codebase straight into the Nuxt frontend repository. This removed an entire class of serialisation bugs where the frontend application might expect a snake_case property that the headless API emitted in camelCase.",[23,495,497],{"id":496},"mapping-nested-product-variants-in-vue","Mapping nested product variants in Vue",[10,499,500],{},"Handling product variants in a headless setup gets messy quickly. For Sweet Layer, items aren't just simple standalone units; they feature complex tier combinations where selecting a specific size alters the available colour options and dynamically updates the inventory threshold.",[10,502,503],{},"In Nuxt, handling this level of interactive state requires a tight combination of computed properties and reactive design. Trying to track every permutation inside a single massive template block leads to unmaintainable code. Instead, I isolated the variant selection logic into a dedicated Vue composable.",[34,505,507],{"className":36,"code":506,"language":38,"meta":39,"style":39},"\u002F\u002F Composable for tracking reactive variant selections\nimport { ref, computed } from 'vue';\n\nexport function useVariantSelector(variants: ProductVariant[]) {\n  const selectedSize = ref\u003Cstring | null>(null);\n  const selectedColour = ref\u003Cstring | null>(null);\n\n  const activeVariant = computed(() => {\n    return variants.find(v =>\n      v.attributes.size === selectedSize.value &&\n      v.attributes.colour === selectedColour.value\n    ) || null;\n  });\n\n  const isAvailable = computed(() => {\n    return activeVariant.value ? activeVariant.value.stock > 0 : false;\n  });\n\n  return { selectedSize, selectedColour, activeVariant, isAvailable };\n}\n",[41,508,509,514,541,545,572,610,639,643,662,685,713,735,747,754,758,775,811,817,821,847],{"__ignoreMap":39},[44,510,511],{"class":46,"line":47},[44,512,513],{"class":50},"\u002F\u002F Composable for tracking reactive variant selections\n",[44,515,516,518,520,523,525,528,530,532,534,537,539],{"class":46,"line":54},[44,517,58],{"class":57},[44,519,62],{"class":61},[44,521,522],{"class":65},"ref",[44,524,197],{"class":75},[44,526,527],{"class":65}," computed",[44,529,69],{"class":61},[44,531,72],{"class":57},[44,533,76],{"class":75},[44,535,536],{"class":79},"vue",[44,538,83],{"class":75},[44,540,86],{"class":75},[44,542,543],{"class":46,"line":89},[44,544,93],{"emptyLinePlaceholder":92},[44,546,547,549,552,556,559,562,564,567,570],{"class":46,"line":96},[44,548,99],{"class":57},[44,550,551],{"class":106}," function",[44,553,555],{"class":554},"s3R4Z"," useVariantSelector",[44,557,558],{"class":61},"(",[44,560,301],{"class":561},"sT800",[44,563,110],{"class":75},[44,565,566],{"class":113}," ProductVariant",[44,568,569],{"class":61},"[])",[44,571,120],{"class":61},[44,573,574,577,580,582,585,588,591,594,597,600,602,605,608],{"class":46,"line":123},[44,575,576],{"class":102},"  const",[44,578,579],{"class":106}," selectedSize",[44,581,117],{"class":75},[44,583,584],{"class":554}," ref",[44,586,587],{"class":75},"\u003C",[44,589,590],{"class":65},"string",[44,592,593],{"class":75}," |",[44,595,596],{"class":65}," null",[44,598,599],{"class":75},">",[44,601,558],{"class":61},[44,603,604],{"class":219},"null",[44,606,607],{"class":61},")",[44,609,86],{"class":75},[44,611,612,614,617,619,621,623,625,627,629,631,633,635,637],{"class":46,"line":142},[44,613,576],{"class":102},[44,615,616],{"class":106}," selectedColour",[44,618,117],{"class":75},[44,620,584],{"class":554},[44,622,587],{"class":75},[44,624,590],{"class":65},[44,626,593],{"class":75},[44,628,596],{"class":65},[44,630,599],{"class":75},[44,632,558],{"class":61},[44,634,604],{"class":219},[44,636,607],{"class":61},[44,638,86],{"class":75},[44,640,641],{"class":46,"line":169},[44,642,93],{"emptyLinePlaceholder":92},[44,644,645,647,650,652,654,657,660],{"class":46,"line":180},[44,646,576],{"class":102},[44,648,649],{"class":106}," activeVariant",[44,651,117],{"class":75},[44,653,527],{"class":554},[44,655,656],{"class":61},"(() ",[44,658,659],{"class":106},"=>",[44,661,120],{"class":61},[44,663,664,668,671,674,677,679,682],{"class":46,"line":236},[44,665,667],{"class":666},"sEsAJ","    return",[44,669,670],{"class":113}," variants",[44,672,673],{"class":75},".",[44,675,676],{"class":554},"find",[44,678,558],{"class":61},[44,680,681],{"class":561},"v",[44,683,684],{"class":106}," =>\n",[44,686,687,690,692,695,697,700,703,705,707,710],{"class":46,"line":285},[44,688,689],{"class":113},"      v",[44,691,673],{"class":75},[44,693,694],{"class":113},"attributes",[44,696,673],{"class":75},[44,698,699],{"class":57},"size",[44,701,702],{"class":106}," ===",[44,704,579],{"class":113},[44,706,673],{"class":75},[44,708,709],{"class":57},"value",[44,711,712],{"class":106}," &&\n",[44,714,715,717,719,721,723,726,728,730,732],{"class":46,"line":291},[44,716,689],{"class":113},[44,718,673],{"class":75},[44,720,694],{"class":113},[44,722,673],{"class":75},[44,724,725],{"class":57},"colour",[44,727,702],{"class":106},[44,729,616],{"class":113},[44,731,673],{"class":75},[44,733,734],{"class":57},"value\n",[44,736,737,740,743,745],{"class":46,"line":308},[44,738,739],{"class":61},"    ) ",[44,741,742],{"class":106},"||",[44,744,596],{"class":219},[44,746,86],{"class":75},[44,748,749,752],{"class":46,"line":325},[44,750,751],{"class":61},"  })",[44,753,86],{"class":75},[44,755,756],{"class":46,"line":335},[44,757,93],{"emptyLinePlaceholder":92},[44,759,760,762,765,767,769,771,773],{"class":46,"line":377},[44,761,576],{"class":102},[44,763,764],{"class":106}," isAvailable",[44,766,117],{"class":75},[44,768,527],{"class":554},[44,770,656],{"class":61},[44,772,659],{"class":106},[44,774,120],{"class":61},[44,776,777,779,781,783,785,788,790,792,794,796,798,801,803,806,809],{"class":46,"line":418},[44,778,667],{"class":666},[44,780,649],{"class":113},[44,782,673],{"class":75},[44,784,709],{"class":57},[44,786,787],{"class":106}," ?",[44,789,649],{"class":113},[44,791,673],{"class":75},[44,793,709],{"class":113},[44,795,673],{"class":75},[44,797,429],{"class":57},[44,799,800],{"class":106}," >",[44,802,461],{"class":219},[44,804,805],{"class":106}," :",[44,807,808],{"class":219}," false",[44,810,86],{"class":75},[44,812,813,815],{"class":46,"line":467},[44,814,751],{"class":61},[44,816,86],{"class":75},[44,818,819],{"class":46,"line":473},[44,820,93],{"emptyLinePlaceholder":92},[44,822,823,826,828,831,833,835,837,839,841,843,845],{"class":46,"line":479},[44,824,825],{"class":666},"  return",[44,827,62],{"class":61},[44,829,830],{"class":113},"selectedSize",[44,832,197],{"class":75},[44,834,616],{"class":113},[44,836,197],{"class":75},[44,838,649],{"class":113},[44,840,197],{"class":75},[44,842,764],{"class":113},[44,844,69],{"class":61},[44,846,86],{"class":75},[44,848,849],{"class":46,"line":485},[44,850,851],{"class":61},"}\n",[10,853,854],{},"By decoupling this business logic from the presentation layer, the UI components remained completely clean. The storefront updates prices instantly, disables out-of-stock selection elements safely, and updates the global cart basket without forcing deep component re-renders.",[23,856,858],{"id":857},"isr-caching-and-the-stale-data-problem","ISR caching and the stale-data problem",[10,860,861],{},"To ensure the storefront loads instantly, I utilised Incremental Static Regeneration (ISR) within Nuxt's underlying Nitro engine. This approach pre-renders product detail pages on demand and caches them at the edge, serving static HTML files to users while silently refreshing the underlying data layout in the background every few minutes.",[34,863,865],{"className":36,"code":864,"language":38,"meta":39,"style":39},"\u002F\u002F nuxt.config.ts configuration for Nitro ISR routing\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002Fproducts\u002F**': { isr: 600 }, \u002F\u002F Cache product pages for 10 minutes\n    '\u002Fcheckout\u002F**': { ssr: false } \u002F\u002F Completely disable SSR for checkout flows\n  }\n});\n",[41,866,867,872,885,894,923,948,953],{"__ignoreMap":39},[44,868,869],{"class":46,"line":47},[44,870,871],{"class":50},"\u002F\u002F nuxt.config.ts configuration for Nitro ISR routing\n",[44,873,874,876,879,882],{"class":46,"line":54},[44,875,99],{"class":57},[44,877,878],{"class":57}," default",[44,880,881],{"class":554}," defineNuxtConfig",[44,883,884],{"class":61},"({\n",[44,886,887,890,892],{"class":46,"line":89},[44,888,889],{"class":126},"  routeRules",[44,891,110],{"class":75},[44,893,120],{"class":61},[44,895,896,899,902,904,906,908,911,913,916,918,920],{"class":46,"line":96},[44,897,898],{"class":75},"    '",[44,900,901],{"class":79},"\u002Fproducts\u002F**",[44,903,83],{"class":75},[44,905,110],{"class":75},[44,907,150],{"class":61},[44,909,910],{"class":341}," isr",[44,912,110],{"class":75},[44,914,915],{"class":219}," 600",[44,917,69],{"class":61},[44,919,197],{"class":75},[44,921,922],{"class":50}," \u002F\u002F Cache product pages for 10 minutes\n",[44,924,925,927,930,932,934,936,939,941,943,945],{"class":46,"line":123},[44,926,898],{"class":75},[44,928,929],{"class":79},"\u002Fcheckout\u002F**",[44,931,83],{"class":75},[44,933,110],{"class":75},[44,935,150],{"class":61},[44,937,938],{"class":341}," ssr",[44,940,110],{"class":75},[44,942,808],{"class":219},[44,944,69],{"class":61},[44,946,947],{"class":50}," \u002F\u002F Completely disable SSR for checkout flows\n",[44,949,950],{"class":46,"line":142},[44,951,952],{"class":61},"  }\n",[44,954,955,958],{"class":46,"line":169},[44,956,957],{"class":61},"})",[44,959,86],{"class":75},[10,961,962,963,967],{},"While ISR works incredibly well for static content layouts like the ",[14,964,966],{"href":965},"\u002Fprojects\u002Fastro-blog","Astro Blog",", it introduces a critical hazard for e-commerce: inventory presentation. If a high-demand item sells out entirely, an ISR-cached page might still show the product as \"In Stock\" to visitors for up to ten minutes.",[10,969,970],{},"To circumvent this stale-data issue, I configured the static page layout to render the baseline product description and images directly from the edge cache, but forced the specific stock availability tags and purchase buttons to mount as client-only elements. The moment the page hydrates in the user's browser, a lightweight fetch request hits the live database to confirm the current inventory numbers before the user can even attempt to click the purchase trigger.",[23,972,974],{"id":973},"trade-offs-and-the-reality-of-stripe-integrations","Trade-offs and the reality of Stripe integrations",[10,976,977],{},"The single biggest headache in this headless architecture was managing the checkout lifecycle with Stripe. In a monolithic ecosystem like WordPress with WooCommerce, plugins handle the state sync between your basket, taxes, and the payment gateway transparently behind the scenes. In a custom headless architecture, you have to build that bridge by hand using secure webhook handlers.",[10,979,980,981,984],{},"I chose to handle the payment verification via a custom API endpoint embedded directly inside the Payload CMS application instance. When a user clicks checkout, Nuxt creates a draft order record via a secure API call and redirects the browser session to a hosted Stripe Checkout page. Once the transaction completes, Stripe fires a ",[41,982,983],{},"checkout.session.completed"," event to the Payload webhook.",[10,986,987],{},"The friction appears when a user closes their browser tab after confirming the payment on Stripe but before the webhook arrives at the backend server. If your database state relies entirely on synchronous redirect responses, you will drop orders. The architecture must treat the Stripe webhook as the absolute, immutable source of truth. The order is only marked as paid and inventory officially deducted when the cryptographic webhook verification passes, regardless of what the frontend UI claims.",[23,989,991],{"id":990},"closing-take","Closing take",[10,993,994,995,673],{},"Building a custom headless storefront with Nuxt and Payload CMS gives you total design freedom, unmatched Lighthouse scores, and an administration interface that content teams genuinely enjoy using. However, you shouldn't pick a headless stack simply because it sounds modern; the added architectural complexity of state reconciliation, webhook handling, and inventory verification is only worth it if your project genuinely demands specialised product customisation. If you want to see how this architecture performs in the wild, check out the final implementation details on the ",[14,996,997],{"href":16},"Sweet Layer project page",[999,1000,1001],"style",{},"html pre.shiki code .sbD-w, html code.shiki .sbD-w{--shiki-default:#51597D;--shiki-default-font-style:italic}html pre.shiki code .su0L4, html code.shiki .su0L4{--shiki-default:#7DCFFF}html pre.shiki code .sgJMe, html code.shiki .sgJMe{--shiki-default:#9ABDF5}html pre.shiki code .sySf4, html code.shiki .sySf4{--shiki-default:#0DB9D7}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sPY7s, html code.shiki .sPY7s{--shiki-default:#9ECE6A}html pre.shiki code .sN7LL, html code.shiki .sN7LL{--shiki-default:#9D7CD8;--shiki-default-font-style:italic}html pre.shiki code .sd1Qi, html code.shiki .sd1Qi{--shiki-default:#BB9AF7}html pre.shiki code .sE3pS, html code.shiki .sE3pS{--shiki-default:#C0CAF5}html pre.shiki code .syYvs, html code.shiki .syYvs{--shiki-default:#73DACA}html pre.shiki code .sOJ5S, html code.shiki .sOJ5S{--shiki-default:#FF9E64}html pre.shiki code .sJ0GE, html code.shiki .sJ0GE{--shiki-default:#41A6B5}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 .s3R4Z, html code.shiki .s3R4Z{--shiki-default:#7AA2F7}html pre.shiki code .sT800, html code.shiki .sT800{--shiki-default:#E0AF68}html pre.shiki code .sEsAJ, html code.shiki .sEsAJ{--shiki-default:#BB9AF7;--shiki-default-font-style:italic}",{"title":39,"searchDepth":54,"depth":54,"links":1003},[1004,1005,1006,1007,1008],{"id":25,"depth":54,"text":26},{"id":496,"depth":54,"text":497},{"id":857,"depth":54,"text":858},{"id":973,"depth":54,"text":974},{"id":990,"depth":54,"text":991},"2026-04-28","An honest look at building a high-performance headless e-commerce store using Nuxt 4 and Payload CMS, from complex variant schemas to Stripe friction.","md","🛒",{},"\u002Fblog\u002Fheadless-ecommerce-nuxt-payload",{"title":5,"description":1010},"blog\u002Fheadless-ecommerce-nuxt-payload",[1018,1019,1020],"Nuxt","Vue","Headless CMS","ySnoFG0Si9k8QVGmv5Xkn4LBQaRwie6tpue0wb-OvIw",1779864409476]