[{"data":1,"prerenderedAt":7195},["ShallowReactive",2],{"blog-post-nuxt-3-performance-optimization-guide":3,"blog-posts":608,"content-query-O06HNnunrt":6741},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":11,"readingTime":12,"tags":13,"featured":16,"body":17,"_type":602,"_id":603,"_source":604,"_file":605,"_stem":606,"_extension":607},"\u002Fblog\u002Fnuxt-3-performance-optimization-guide","blog",false,"","Nuxt 3 Performance Optimization: A Production Playbook for Core Web Vitals","A production playbook for optimizing Nuxt 3 applications. SSR strategies, image optimization, code splitting, and Core Web Vitals tuning that holds up in CI.","2024-08-22","2026-07-05",14,[14,15],"Nuxt","Performance",true,{"type":18,"children":19,"toc":585},"root",[20,29,43,57,63,70,75,88,93,128,134,139,150,160,178,184,189,200,205,214,227,236,246,252,257,266,271,277,282,291,302,313,318,324,329,338,343,361,366,372,377,386,391,397,402,465,477,483,488,497,502,520,526,580],{"type":21,"tag":22,"props":23,"children":25},"element","h2",{"id":24},"starting-point-a-slow-production-app",[26],{"type":27,"value":28},"text","Starting Point: A Slow Production App",{"type":21,"tag":30,"props":31,"children":32},"p",{},[33,35,41],{"type":27,"value":34},"The app this playbook comes from was slow in the way most B2B apps get slow — gradually, then noticeably. Largest Contentful Paint on key routes was ",{"type":21,"tag":36,"props":37,"children":38},"strong",{},[39],{"type":27,"value":40},"4.2s",{"type":27,"value":42}," on throttled mobile. By Google's standards, anything over 2.5s for LCP is failing, and every user felt it.",{"type":21,"tag":30,"props":44,"children":45},{},[46,48,55],{"type":27,"value":47},"The full program behind this post — what was tried, what was rejected, and how the results are enforced — is written up in the ",{"type":21,"tag":49,"props":50,"children":52},"a",{"href":51},"\u002Fcase-studies\u002Fsaas-performance-program",[53],{"type":27,"value":54},"SaaS performance case study",{"type":27,"value":56},". This post is the generalized playbook.",{"type":21,"tag":22,"props":58,"children":60},{"id":59},"the-optimization-playbook",[61],{"type":27,"value":62},"The Optimization Playbook",{"type":21,"tag":64,"props":65,"children":67},"h3",{"id":66},"_1-analyze-with-lighthouse-bundle-analysis",[68],{"type":27,"value":69},"1. Analyze with Lighthouse & Bundle Analysis",{"type":21,"tag":30,"props":71,"children":72},{},[73],{"type":27,"value":74},"Before optimizing, we measured everything:",{"type":21,"tag":76,"props":77,"children":82},"pre",{"className":78,"code":80,"language":81,"meta":7},[79],"language-bash","npm run build --analyze\n# Generated visual bundle report\n\n# Lighthouse CI integration\nnpm install -D @lhci\u002Fcli@0.9.x\n","bash",[83],{"type":21,"tag":84,"props":85,"children":86},"code",{"__ignoreMap":7},[87],{"type":27,"value":80},{"type":21,"tag":30,"props":89,"children":90},{},[91],{"type":27,"value":92},"A typical analysis of an unoptimized app reveals something like this (illustrative example — run your own):",{"type":21,"tag":94,"props":95,"children":96},"ul",{},[97,108,118],{"type":21,"tag":98,"props":99,"children":100},"li",{},[101,106],{"type":21,"tag":36,"props":102,"children":103},{},[104],{"type":27,"value":105},"Main bundle",{"type":27,"value":107},": a few hundred KB gzipped of code the first paint doesn't need",{"type":21,"tag":98,"props":109,"children":110},{},[111,116],{"type":21,"tag":36,"props":112,"children":113},{},[114],{"type":27,"value":115},"Vendor bundle",{"type":27,"value":117},": duplicate versions of the same utility library, bundled separately",{"type":21,"tag":98,"props":119,"children":120},{},[121,126],{"type":21,"tag":36,"props":122,"children":123},{},[124],{"type":27,"value":125},"Images",{"type":27,"value":127},": the largest single line item — no WebP, no lazy loading, full-resolution on mobile",{"type":21,"tag":64,"props":129,"children":131},{"id":130},"_2-code-splitting-strategy",[132],{"type":27,"value":133},"2. Code Splitting Strategy",{"type":21,"tag":30,"props":135,"children":136},{},[137],{"type":27,"value":138},"Nuxt 3 has better defaults than Nuxt 2, but we needed aggressive splitting:",{"type":21,"tag":76,"props":140,"children":145},{"className":141,"code":143,"language":144,"meta":7},[142],"language-typescript","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: {\n          \u002F\u002F Separate heavy dependencies\n          'date-fns': ['date-fns'],\n          'lodash': ['lodash-es'],\n          'charts': ['chart.js', 'vue-chartjs'],\n          \u002F\u002F Keep vendor chunks under 200KB each\n        }\n      }\n    }\n  },\n  nitro: {\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002Fsitemap.xml', '\u002Frss.xml'],\n      \u002F\u002F Pre-render critical routes at build time\n      ignore: ['\u002Fadmin', '\u002Fpreview']\n    }\n  }\n})\n","typescript",[146],{"type":21,"tag":84,"props":147,"children":148},{"__ignoreMap":7},[149],{"type":27,"value":143},{"type":21,"tag":30,"props":151,"children":152},{},[153,158],{"type":21,"tag":36,"props":154,"children":155},{},[156],{"type":27,"value":157},"What to expect",{"type":27,"value":159}," (illustrative example — your numbers will differ):",{"type":21,"tag":94,"props":161,"children":162},{},[163,168,173],{"type":21,"tag":98,"props":164,"children":165},{},[166],{"type":27,"value":167},"The main bundle drops to whatever the first route genuinely needs",{"type":21,"tag":98,"props":169,"children":170},{},[171],{"type":27,"value":172},"Each lazy route loads a small chunk on demand",{"type":21,"tag":98,"props":174,"children":175},{},[176],{"type":27,"value":177},"Routes render without waiting for full app initialization",{"type":21,"tag":64,"props":179,"children":181},{"id":180},"_3-image-optimization",[182],{"type":27,"value":183},"3. Image Optimization",{"type":21,"tag":30,"props":185,"children":186},{},[187],{"type":27,"value":188},"This single change had the biggest impact:",{"type":21,"tag":76,"props":190,"children":195},{"className":191,"code":193,"language":194,"meta":7},[192],"language-vue","\u003Cscript setup lang=\"ts\">\nimport { useImage } from '#app'\n\nconst imageUrl = 'hero.jpg'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ❌ Old way: no optimization -->\n  \u003Cimg src=\"\u002Fimages\u002Fhero.jpg\" alt=\"Hero\" \u002F>\n  \n  \u003C!-- ✅ New way: Nuxt Image component -->\n  \u003CNuxtImg\n    src=\"\u002Fimages\u002Fhero.jpg\"\n    alt=\"Hero\"\n    sizes=\"xs:100vw sm:100vw md:600px lg:800px\"\n    format=\"webp,jpg\"\n    quality=\"80\"\n    loading=\"lazy\"\n  \u002F>\n\u003C\u002Ftemplate>\n","vue",[196],{"type":21,"tag":84,"props":197,"children":198},{"__ignoreMap":7},[199],{"type":27,"value":193},{"type":21,"tag":30,"props":201,"children":202},{},[203],{"type":27,"value":204},"Install the Nuxt Image module:",{"type":21,"tag":76,"props":206,"children":209},{"className":207,"code":208,"language":81,"meta":7},[79],"npm install @nuxt\u002Fimage\n",[210],{"type":21,"tag":84,"props":211,"children":212},{"__ignoreMap":7},[213],{"type":27,"value":208},{"type":21,"tag":30,"props":215,"children":216},{},[217,219,225],{"type":27,"value":218},"Configure in ",{"type":21,"tag":84,"props":220,"children":222},{"className":221},[],[223],{"type":27,"value":224},"nuxt.config.ts",{"type":27,"value":226},":",{"type":21,"tag":76,"props":228,"children":231},{"className":229,"code":230,"language":144,"meta":7},[142],"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fimage'],\n  image: {\n    provider: 'ipx', \u002F\u002F or 'cloudinary', 'imgix', etc.\n    ipx: {\n      maxAge: 60 * 60 * 24 * 365, \u002F\u002F 1 year cache\n    }\n  }\n})\n",[232],{"type":21,"tag":84,"props":233,"children":234},{"__ignoreMap":7},[235],{"type":27,"value":230},{"type":21,"tag":30,"props":237,"children":238},{},[239,244],{"type":21,"tag":36,"props":240,"children":241},{},[242],{"type":27,"value":243},"Why this matters most",{"type":27,"value":245},": on image-heavy routes, WebP conversion plus lazy loading is routinely the single largest LCP win — images are usually the largest contentful paint element. Measure your image bytes before and after; the reduction is typically dramatic.",{"type":21,"tag":64,"props":247,"children":249},{"id":248},"_4-server-side-rendering-ssr-optimization",[250],{"type":27,"value":251},"4. Server-Side Rendering (SSR) Optimization",{"type":21,"tag":30,"props":253,"children":254},{},[255],{"type":27,"value":256},"Nuxt 3's SSR is better, but configuration matters:",{"type":21,"tag":76,"props":258,"children":261},{"className":259,"code":260,"language":144,"meta":7},[142],"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  nitro: {\n    \u002F\u002F Compress responses\n    gzip: { enabled: true },\n    brotli: { enabled: true },\n    \n    \u002F\u002F Cache aggressively\n    headers: {\n      'Cache-Control': 'public, max-age=3600, s-maxage=86400'\n    },\n    \n    \u002F\u002F Pre-render static routes\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002Fblog', '\u002Fprojects', '\u002F'],\n      ignore: ['\u002Fadmin', '\u002Fapi\u002Finternal']\n    },\n    \n    \u002F\u002F Enable Redis caching for API responses\n    storage: {\n      redis: {\n        host: process.env.REDIS_HOST,\n        port: 6379\n      }\n    }\n  }\n})\n",[262],{"type":21,"tag":84,"props":263,"children":264},{"__ignoreMap":7},[265],{"type":27,"value":260},{"type":21,"tag":30,"props":267,"children":268},{},[269],{"type":27,"value":270},"The result: time-to-first-byte drops sharply once cacheable responses stop hitting the origin (illustrative — TTFB depends heavily on your hosting).",{"type":21,"tag":64,"props":272,"children":274},{"id":273},"_5-font-loading-optimization",[275],{"type":27,"value":276},"5. Font Loading Optimization",{"type":21,"tag":30,"props":278,"children":279},{},[280],{"type":27,"value":281},"Fonts are invisible but heavy. We optimized aggressively:",{"type":21,"tag":76,"props":283,"children":286},{"className":284,"code":285,"language":144,"meta":7},[142],"\u002F\u002F app.vue\nimport { useState } from '#app'\n\nconst fontLoadingComplete = useState('fontLoadingComplete', () => false)\n\nuseHead({\n  link: [\n    \u002F\u002F Preconnect to font providers\n    {\n      rel: 'preconnect',\n      href: 'https:\u002F\u002Ffonts.googleapis.com'\n    },\n    {\n      rel: 'preconnect',\n      href: 'https:\u002F\u002Ffonts.gstatic.com',\n      crossorigin: ''\n    },\n    \u002F\u002F Load only necessary weights\n    {\n      rel: 'stylesheet',\n      href: 'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap'\n    }\n  ]\n})\n",[287],{"type":21,"tag":84,"props":288,"children":289},{"__ignoreMap":7},[290],{"type":27,"value":285},{"type":21,"tag":30,"props":292,"children":293},{},[294,296,301],{"type":27,"value":295},"But the real win came from ",{"type":21,"tag":36,"props":297,"children":298},{},[299],{"type":27,"value":300},"font-display: swap",{"type":27,"value":226},{"type":21,"tag":76,"props":303,"children":308},{"className":304,"code":306,"language":307,"meta":7},[305],"language-css","@import url('https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap');\n","css",[309],{"type":21,"tag":84,"props":310,"children":311},{"__ignoreMap":7},[312],{"type":27,"value":306},{"type":21,"tag":30,"props":314,"children":315},{},[316],{"type":27,"value":317},"This tells the browser: \"Show text immediately with fallback, swap in custom font when ready.\" No invisible text delays.",{"type":21,"tag":64,"props":319,"children":321},{"id":320},"_6-hydration-optimization",[322],{"type":27,"value":323},"6. Hydration Optimization",{"type":21,"tag":30,"props":325,"children":326},{},[327],{"type":27,"value":328},"This is a Nuxt 3 secret weapon. Partial hydration means not every component needs JavaScript:",{"type":21,"tag":76,"props":330,"children":333},{"className":331,"code":332,"language":194,"meta":7},[192],"\u003C!-- This component renders as static HTML on server -->\n\u003C!-- JavaScript only loads when needed -->\n\u003CClientOnly fallback=\"\u003Cdiv class='skeleton'>\u003C\u002Fdiv>\">\n  \u003CExpensiveInteractiveComponent \u002F>\n\u003C\u002FClientOnly>\n",[334],{"type":21,"tag":84,"props":335,"children":336},{"__ignoreMap":7},[337],{"type":27,"value":332},{"type":21,"tag":30,"props":339,"children":340},{},[341],{"type":27,"value":342},"Use it for:",{"type":21,"tag":94,"props":344,"children":345},{},[346,351,356],{"type":21,"tag":98,"props":347,"children":348},{},[349],{"type":27,"value":350},"Hero sections (no JS needed)",{"type":21,"tag":98,"props":352,"children":353},{},[354],{"type":27,"value":355},"Blog content (pure markup)",{"type":21,"tag":98,"props":357,"children":358},{},[359],{"type":27,"value":360},"Static sidebars",{"type":21,"tag":30,"props":362,"children":363},{},[364],{"type":27,"value":365},"Static sections that skip hydration are JavaScript the browser never has to parse.",{"type":21,"tag":64,"props":367,"children":369},{"id":368},"_7-caching-strategy",[370],{"type":27,"value":371},"7. Caching Strategy",{"type":21,"tag":30,"props":373,"children":374},{},[375],{"type":27,"value":376},"Implemented a tiered caching approach:",{"type":21,"tag":76,"props":378,"children":381},{"className":379,"code":380,"language":144,"meta":7},[142],"\u002F\u002F server\u002Fapi\u002Fposts.get.ts\nexport default cachedEventHandler(\n  async (event) => {\n    return await db.posts.findAll()\n  },\n  {\n    maxAge: 60 * 10, \u002F\u002F 10 minute cache\n    sMaxAge: 60 * 60 * 24, \u002F\u002F 1 day for CDN\n    staleMaxAge: 60 * 60 * 24 * 7, \u002F\u002F 1 week stale\n    name: 'api_posts',\n    getKey: () => 'api_posts',\n    vary: ['accept-encoding'] \u002F\u002F Cache based on compression\n  }\n)\n",[382],{"type":21,"tag":84,"props":383,"children":384},{"__ignoreMap":7},[385],{"type":27,"value":380},{"type":21,"tag":30,"props":387,"children":388},{},[389],{"type":27,"value":390},"With a tiered cache, most repeat API reads never reach the database.",{"type":21,"tag":22,"props":392,"children":394},{"id":393},"the-final-results",[395],{"type":27,"value":396},"The Final Results",{"type":21,"tag":30,"props":398,"children":399},{},[400],{"type":27,"value":401},"On the production program this playbook comes from, the headline result was:",{"type":21,"tag":403,"props":404,"children":405},"table",{},[406,435],{"type":21,"tag":407,"props":408,"children":409},"thead",{},[410],{"type":21,"tag":411,"props":412,"children":413},"tr",{},[414,420,425,430],{"type":21,"tag":415,"props":416,"children":417},"th",{},[418],{"type":27,"value":419},"Metric",{"type":21,"tag":415,"props":421,"children":422},{},[423],{"type":27,"value":424},"Before",{"type":21,"tag":415,"props":426,"children":427},{},[428],{"type":27,"value":429},"After",{"type":21,"tag":415,"props":431,"children":432},{},[433],{"type":27,"value":434},"Improvement",{"type":21,"tag":436,"props":437,"children":438},"tbody",{},[439],{"type":21,"tag":411,"props":440,"children":441},{},[442,448,452,457],{"type":21,"tag":443,"props":444,"children":445},"td",{},[446],{"type":27,"value":447},"Largest Contentful Paint",{"type":21,"tag":443,"props":449,"children":450},{},[451],{"type":27,"value":40},{"type":21,"tag":443,"props":453,"children":454},{},[455],{"type":27,"value":456},"2.5s",{"type":21,"tag":443,"props":458,"children":459},{},[460],{"type":21,"tag":36,"props":461,"children":462},{},[463],{"type":27,"value":464},"−40%",{"type":21,"tag":30,"props":466,"children":467},{},[468,470,475],{"type":27,"value":469},"The other vitals moved in the same direction; the LCP number is the one measured carefully enough to publish. The ",{"type":21,"tag":49,"props":471,"children":472},{"href":51},[473],{"type":27,"value":474},"case study",{"type":27,"value":476}," covers how it was measured and enforced.",{"type":21,"tag":22,"props":478,"children":480},{"id":479},"performance-budget-going-forward",[481],{"type":27,"value":482},"Performance Budget Going Forward",{"type":21,"tag":30,"props":484,"children":485},{},[486],{"type":27,"value":487},"We set strict budgets to prevent regressions:",{"type":21,"tag":76,"props":489,"children":492},{"className":490,"code":491,"language":81,"meta":7},[79],"# package.json\n\"build-analyze\": \"nuxi build && npm run bundle-check\",\n\n# .github\u002Fworkflows\u002Fperformance.yml\n- name: Bundle Size Check\n  run: npm run build-analyze\n  # Fails if bundles exceed thresholds\n",[493],{"type":21,"tag":84,"props":494,"children":495},{"__ignoreMap":7},[496],{"type":27,"value":491},{"type":21,"tag":30,"props":498,"children":499},{},[500],{"type":27,"value":501},"Thresholds:",{"type":21,"tag":94,"props":503,"children":504},{},[505,510,515],{"type":21,"tag":98,"props":506,"children":507},{},[508],{"type":27,"value":509},"Main bundle: \u003C 200KB gzipped",{"type":21,"tag":98,"props":511,"children":512},{},[513],{"type":27,"value":514},"Per route: \u003C 80KB gzipped",{"type":21,"tag":98,"props":516,"children":517},{},[518],{"type":27,"value":519},"Images: \u003C 1MB total per page",{"type":21,"tag":22,"props":521,"children":523},{"id":522},"key-takeaways",[524],{"type":27,"value":525},"Key Takeaways",{"type":21,"tag":527,"props":528,"children":529},"ol",{},[530,540,550,560,570],{"type":21,"tag":98,"props":531,"children":532},{},[533,538],{"type":21,"tag":36,"props":534,"children":535},{},[536],{"type":27,"value":537},"Measure first",{"type":27,"value":539}," — every optimization should be data-driven",{"type":21,"tag":98,"props":541,"children":542},{},[543,548],{"type":21,"tag":36,"props":544,"children":545},{},[546],{"type":27,"value":547},"Images are the lowest-hanging fruit",{"type":27,"value":549}," — image handling routinely delivers the largest share of the wins",{"type":21,"tag":98,"props":551,"children":552},{},[553,558],{"type":21,"tag":36,"props":554,"children":555},{},[556],{"type":27,"value":557},"HTTP caching > code optimization",{"type":27,"value":559}," — smart caching beats micro-optimizations",{"type":21,"tag":98,"props":561,"children":562},{},[563,568],{"type":21,"tag":36,"props":564,"children":565},{},[566],{"type":27,"value":567},"Nuxt 3's primitives are powerful",{"type":27,"value":569}," — when used correctly, you don't need complex workarounds",{"type":21,"tag":98,"props":571,"children":572},{},[573,578],{"type":21,"tag":36,"props":574,"children":575},{},[576],{"type":27,"value":577},"Monitor in production",{"type":27,"value":579}," — real-world metrics differ from labs. Use tools like Sentry, Vercel Analytics",{"type":21,"tag":30,"props":581,"children":582},{},[583],{"type":27,"value":584},"If you're on Nuxt 2, the move to Nuxt 3 alone usually buys a meaningful improvement before you optimize anything — better defaults, better tree-shaking, better hydration. The playbook above is how you claim the rest.",{"title":7,"searchDepth":586,"depth":586,"links":587},2,[588,589,599,600,601],{"id":24,"depth":586,"text":28},{"id":59,"depth":586,"text":62,"children":590},[591,593,594,595,596,597,598],{"id":66,"depth":592,"text":69},3,{"id":130,"depth":592,"text":133},{"id":180,"depth":592,"text":183},{"id":248,"depth":592,"text":251},{"id":273,"depth":592,"text":276},{"id":320,"depth":592,"text":323},{"id":368,"depth":592,"text":371},{"id":393,"depth":586,"text":396},{"id":479,"depth":586,"text":482},{"id":522,"depth":586,"text":525},"markdown","content:blog:nuxt-3-performance-optimization-guide.md","content","blog\u002Fnuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide","md",[609,1151,1605,2150,2955,3626,4057,4916,5379,6113],{"_path":610,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":611,"description":612,"date":613,"updated":11,"readingTime":614,"tags":615,"featured":16,"body":618,"_type":602,"_id":1148,"_source":604,"_file":1149,"_stem":1150,"_extension":607},"\u002Fblog\u002Fvue-2-to-vue-3-migration-at-scale","Vue 2 to Vue 3 Migration at Scale: Lessons from a Production Migration","A guide to migrating large Vue 2 codebases to Vue 3 without downtime. Learn the strategies, tooling, and patterns that worked across dozens of components and the full test suite.","2024-09-15",12,[616,617,15],"Vue","Architecture",{"type":18,"children":619,"toc":1136},[620,626,631,636,686,691,697,703,722,731,736,754,760,765,798,805,814,819,868,874,887,893,904,909,915,924,929,935,944,949,955,960,969,974,979,985,990,998,1023,1029,1082,1088,1131],{"type":21,"tag":22,"props":621,"children":623},{"id":622},"the-challenge-why-we-needed-to-migrate",[624],{"type":27,"value":625},"The Challenge: Why We Needed to Migrate",{"type":21,"tag":30,"props":627,"children":628},{},[629],{"type":27,"value":630},"We faced a critical decision when Vue 3 became stable in 2021. Our Vue 2 codebase had grown large across multiple teams.",{"type":21,"tag":30,"props":632,"children":633},{},[634],{"type":27,"value":635},"The pain points were real:",{"type":21,"tag":94,"props":637,"children":638},{},[639,649,659,668],{"type":21,"tag":98,"props":640,"children":641},{},[642,647],{"type":21,"tag":36,"props":643,"children":644},{},[645],{"type":27,"value":646},"Type Safety",{"type":27,"value":648},": Vue 2 + TypeScript was never fully integrated. The IDE caught some issues; the rest surfaced in QA",{"type":21,"tag":98,"props":650,"children":651},{},[652,657],{"type":21,"tag":36,"props":653,"children":654},{},[655],{"type":27,"value":656},"Composition Reuse",{"type":27,"value":658},": Mixins led to naming collisions and unclear data flow. Multiple teams independently \"solved\" the same problems",{"type":21,"tag":98,"props":660,"children":661},{},[662,666],{"type":21,"tag":36,"props":663,"children":664},{},[665],{"type":27,"value":15},{"type":27,"value":667},": Bundle size kept growing. Tree-shaking didn't work well with Vue 2's runtime structure",{"type":21,"tag":98,"props":669,"children":670},{},[671,676,678,684],{"type":21,"tag":36,"props":672,"children":673},{},[674],{"type":27,"value":675},"Developer Experience",{"type":27,"value":677},": New team members struggled with implicit ",{"type":21,"tag":84,"props":679,"children":681},{"className":680},[],[682],{"type":27,"value":683},"this",{"type":27,"value":685}," context and composition patterns",{"type":21,"tag":30,"props":687,"children":688},{},[689],{"type":27,"value":690},"The business case was compelling: fewer bugs in production, faster onboarding, a smaller bundle.",{"type":21,"tag":22,"props":692,"children":694},{"id":693},"our-migration-strategy",[695],{"type":27,"value":696},"Our Migration Strategy",{"type":21,"tag":64,"props":698,"children":700},{"id":699},"phase-1-set-up-build-tooling-1-week",[701],{"type":27,"value":702},"Phase 1: Set Up Build Tooling (1 week)",{"type":21,"tag":30,"props":704,"children":705},{},[706,708,713,715,720],{"type":27,"value":707},"We chose ",{"type":21,"tag":36,"props":709,"children":710},{},[711],{"type":27,"value":712},"Vite",{"type":27,"value":714}," + ",{"type":21,"tag":36,"props":716,"children":717},{},[718],{"type":27,"value":719},"@vitejs\u002Fplugin-vue",{"type":27,"value":721}," instead of webpack. This decision paid dividends:",{"type":21,"tag":76,"props":723,"children":726},{"className":724,"code":725,"language":144,"meta":7},[142],"\u002F\u002F vite.config.ts\nexport default {\n  plugins: [vue(), visualizer()],\n  build: {\n    rollupOptions: {\n      output: { manualChunks: customChunks }\n    }\n  }\n}\n",[727],{"type":21,"tag":84,"props":728,"children":729},{"__ignoreMap":7},[730],{"type":27,"value":725},{"type":21,"tag":30,"props":732,"children":733},{},[734],{"type":27,"value":735},"Bundle analysis immediately showed why the tooling move pays for itself:",{"type":21,"tag":94,"props":737,"children":738},{},[739,744,749],{"type":21,"tag":98,"props":740,"children":741},{},[742],{"type":27,"value":743},"Vite's default code splitting cut the main bundle substantially before we touched a component",{"type":21,"tag":98,"props":745,"children":746},{},[747],{"type":27,"value":748},"Build times dropped from tens of seconds under webpack to single digits under Vite",{"type":21,"tag":98,"props":750,"children":751},{},[752],{"type":27,"value":753},"Hot Module Replacement went from a coffee-sip wait to effectively instant",{"type":21,"tag":64,"props":755,"children":757},{"id":756},"phase-2-incremental-component-migration-6-weeks",[758],{"type":27,"value":759},"Phase 2: Incremental Component Migration (6 weeks)",{"type":21,"tag":30,"props":761,"children":762},{},[763],{"type":27,"value":764},"We didn't rewrite everything. Instead, we:",{"type":21,"tag":527,"props":766,"children":767},{},[768,778,788],{"type":21,"tag":98,"props":769,"children":770},{},[771,776],{"type":21,"tag":36,"props":772,"children":773},{},[774],{"type":27,"value":775},"Identified high-ROI components",{"type":27,"value":777}," — components used across multiple products, or frequently modified",{"type":21,"tag":98,"props":779,"children":780},{},[781,786],{"type":21,"tag":36,"props":782,"children":783},{},[784],{"type":27,"value":785},"Created a co-existence layer",{"type":27,"value":787}," — Vue 2 and Vue 3 components lived side-by-side",{"type":21,"tag":98,"props":789,"children":790},{},[791,796],{"type":21,"tag":36,"props":792,"children":793},{},[794],{"type":27,"value":795},"Migrated by priority",{"type":27,"value":797}," — a steady per-engineer cadence, not a big-bang rewrite",{"type":21,"tag":799,"props":800,"children":802},"h4",{"id":801},"key-pattern-the-composition-api-advantage",[803],{"type":27,"value":804},"Key Pattern: The Composition API Advantage",{"type":21,"tag":76,"props":806,"children":809},{"className":807,"code":808,"language":144,"meta":7},[142],"\u002F\u002F Old Vue 2 (mixin-based)\nexport const userMixin = {\n  data() {\n    return { user: null, loading: false }\n  },\n  methods: {\n    async fetchUser(id) {\n      this.loading = true\n      this.user = await api.getUser(id)\n      this.loading = false\n    }\n  },\n  computed: {\n    isAuthor() {\n      return this.user?.id === this.currentUserId\n    }\n  }\n}\n\n\u002F\u002F New Vue 3 (Composition API)\nexport const useUser = (userId: Ref\u003Cstring>) => {\n  const user = ref\u003CUser | null>(null)\n  const loading = ref(false)\n  \n  const fetchUser = async (id: string) => {\n    loading.value = true\n    user.value = await api.getUser(id)\n    loading.value = false\n  }\n  \n  const isAuthor = computed(() => user.value?.id === userId.value)\n  \n  watchEffect(() => {\n    if (userId.value) fetchUser(userId.value)\n  })\n  \n  return { user, loading, isAuthor, fetchUser }\n}\n",[810],{"type":21,"tag":84,"props":811,"children":812},{"__ignoreMap":7},[813],{"type":27,"value":808},{"type":21,"tag":30,"props":815,"children":816},{},[817],{"type":27,"value":818},"The Composition API version:",{"type":21,"tag":94,"props":820,"children":821},{},[822,832,842,852],{"type":21,"tag":98,"props":823,"children":824},{},[825,830],{"type":21,"tag":36,"props":826,"children":827},{},[828],{"type":27,"value":829},"Eliminates naming collisions",{"type":27,"value":831}," — composables are just functions, no implicit merging",{"type":21,"tag":98,"props":833,"children":834},{},[835,840],{"type":21,"tag":36,"props":836,"children":837},{},[838],{"type":27,"value":839},"Makes data flow explicit",{"type":27,"value":841}," — you see exactly which data is used",{"type":21,"tag":98,"props":843,"children":844},{},[845,850],{"type":21,"tag":36,"props":846,"children":847},{},[848],{"type":27,"value":849},"Enables better tree-shaking",{"type":27,"value":851}," — unused composables are easier to identify",{"type":21,"tag":98,"props":853,"children":854},{},[855,860,862],{"type":21,"tag":36,"props":856,"children":857},{},[858],{"type":27,"value":859},"Improves TypeScript",{"type":27,"value":861}," — full type inference without ",{"type":21,"tag":84,"props":863,"children":865},{"className":864},[],[866],{"type":27,"value":867},"as any",{"type":21,"tag":64,"props":869,"children":871},{"id":870},"phase-3-handle-the-breaking-changes",[872],{"type":27,"value":873},"Phase 3: Handle the Breaking Changes",{"type":21,"tag":30,"props":875,"children":876},{},[877,879,885],{"type":27,"value":878},"Vue 3's breaking changes were documented, but the ",{"type":21,"tag":880,"props":881,"children":882},"em",{},[883],{"type":27,"value":884},"scale",{"type":27,"value":886}," matters. Here's what actually cost us the most time:",{"type":21,"tag":799,"props":888,"children":890},{"id":889},"_1-event-handling-3-days-spent",[891],{"type":27,"value":892},"1. Event Handling (3 days spent)",{"type":21,"tag":76,"props":894,"children":899},{"className":895,"code":897,"language":898,"meta":7},[896],"language-javascript","\u002F\u002F Vue 2 - event modifiers worked universally\n\u003Cinput @keyup.enter=\"submit\" \u002F>\n\u003Cdiv @click.self=\"close\" \u002F>\n\n\u002F\u002F Vue 3 - custom components don't auto-inherit modifiers\n\u003CCustomInput @keyup.enter=\"submit\" \u002F> \u002F\u002F ❌ Won't work\n\n\u002F\u002F Solution: Explicitly bind in component\n\u003Cinput @keyup.enter=\"$emit('keyup-enter')\" \u002F>\n","javascript",[900],{"type":21,"tag":84,"props":901,"children":902},{"__ignoreMap":7},[903],{"type":27,"value":897},{"type":21,"tag":30,"props":905,"children":906},{},[907],{"type":27,"value":908},"We had instances of this all over the codebase. Automated search + manual review took longer than the code changes themselves.",{"type":21,"tag":799,"props":910,"children":912},{"id":911},"_2-async-components",[913],{"type":27,"value":914},"2. Async Components",{"type":21,"tag":76,"props":916,"children":919},{"className":917,"code":918,"language":898,"meta":7},[896],"\u002F\u002F Vue 2\nconst AsyncComponent = () => import('.\u002FAsyncComponent.vue')\n\n\u002F\u002F Vue 3 - requires `defineAsyncComponent`\nimport { defineAsyncComponent } from 'vue'\nconst AsyncComponent = defineAsyncComponent(\n  () => import('.\u002FAsyncComponent.vue')\n)\n",[920],{"type":21,"tag":84,"props":921,"children":922},{"__ignoreMap":7},[923],{"type":27,"value":918},{"type":21,"tag":30,"props":925,"children":926},{},[927],{"type":27,"value":928},"Dozens more locations to update. Pro tip: codemods exist for this, but they're not perfect. Invest time in understanding edge cases.",{"type":21,"tag":799,"props":930,"children":932},{"id":931},"_3-filters-computed-properties",[933],{"type":27,"value":934},"3. Filters → Computed Properties",{"type":21,"tag":76,"props":936,"children":939},{"className":937,"code":938,"language":898,"meta":7},[896],"\u002F\u002F Vue 2\n{{ message | capitalize }}\n\u002F\u002F Defined as: Vue.filter('capitalize', ...)\n\n\u002F\u002F Vue 3 - must be explicit\n{{ capitalizedMessage }}\n\u002F\u002F const capitalizedMessage = computed(() => capitalize(message.value))\n",[940],{"type":21,"tag":84,"props":941,"children":942},{"__ignoreMap":7},[943],{"type":27,"value":938},{"type":21,"tag":30,"props":945,"children":946},{},[947],{"type":27,"value":948},"Dozens of filter usages. We created a utility module to centralize custom filters as functions.",{"type":21,"tag":64,"props":950,"children":952},{"id":951},"phase-4-test-migration-parallel-4-weeks",[953],{"type":27,"value":954},"Phase 4: Test Migration (Parallel, 4 weeks)",{"type":21,"tag":30,"props":956,"children":957},{},[958],{"type":27,"value":959},"Our Jest test suite had to be updated to work with Vue 3. The good news: tests were actually easier to write.",{"type":21,"tag":76,"props":961,"children":964},{"className":962,"code":963,"language":144,"meta":7},[142],"\u002F\u002F Vue 2 test with vue-test-utils\nconst wrapper = mount(MyComponent, {\n  mocks: { $t: (key) => key },\n  propsData: { user: testUser }\n})\nexpect(wrapper.vm.formattedName).toBe('John Doe')\n\n\u002F\u002F Vue 3 test - cleaner syntax\nconst { vm } = mount(MyComponent, {\n  props: { user: testUser },\n  global: { mocks: { $t: (key) => key } }\n})\nexpect(vm.formattedName).toBe('John Doe')\n",[965],{"type":21,"tag":84,"props":966,"children":967},{"__ignoreMap":7},[968],{"type":27,"value":963},{"type":21,"tag":30,"props":970,"children":971},{},[972],{"type":27,"value":973},"We migrated the full test suite.",{"type":21,"tag":30,"props":975,"children":976},{},[977],{"type":27,"value":978},"It took weeks, not days — but many tests revealed brittleness in the original code. This was actually valuable.",{"type":21,"tag":22,"props":980,"children":982},{"id":981},"the-results",[983],{"type":27,"value":984},"The Results",{"type":21,"tag":30,"props":986,"children":987},{},[988],{"type":27,"value":989},"The migration shipped incrementally, with zero downtime, and the codebase came out the other side smaller, faster to build, and easier to type-check.",{"type":21,"tag":30,"props":991,"children":992},{},[993],{"type":21,"tag":36,"props":994,"children":995},{},[996],{"type":27,"value":997},"What held up qualitatively:",{"type":21,"tag":94,"props":999,"children":1000},{},[1001,1006,1018],{"type":21,"tag":98,"props":1002,"children":1003},{},[1004],{"type":27,"value":1005},"Tree-shaking finally worked, so dead code stopped shipping",{"type":21,"tag":98,"props":1007,"children":1008},{},[1009,1011,1016],{"type":27,"value":1010},"Full TypeScript inference replaced ",{"type":21,"tag":84,"props":1012,"children":1014},{"className":1013},[],[1015],{"type":27,"value":867},{"type":27,"value":1017}," scattered through the codebase",{"type":21,"tag":98,"props":1019,"children":1020},{},[1021],{"type":27,"value":1022},"Vite's dev server made the edit-refresh loop feel free again",{"type":21,"tag":22,"props":1024,"children":1026},{"id":1025},"lessons-for-your-codebase",[1027],{"type":27,"value":1028},"Lessons for Your Codebase",{"type":21,"tag":527,"props":1030,"children":1031},{},[1032,1042,1052,1062,1072],{"type":21,"tag":98,"props":1033,"children":1034},{},[1035,1040],{"type":21,"tag":36,"props":1036,"children":1037},{},[1038],{"type":27,"value":1039},"Tooling matters as much as code",{"type":27,"value":1041}," — Vite's superior DX made the whole migration feel faster",{"type":21,"tag":98,"props":1043,"children":1044},{},[1045,1050],{"type":21,"tag":36,"props":1046,"children":1047},{},[1048],{"type":27,"value":1049},"Go incremental",{"type":27,"value":1051}," — don't try a big bang rewrite. Parallel migration kept the team shipping",{"type":21,"tag":98,"props":1053,"children":1054},{},[1055,1060],{"type":21,"tag":36,"props":1056,"children":1057},{},[1058],{"type":27,"value":1059},"Type safety is an enabler",{"type":27,"value":1061}," — we spent 20% on migration, but gained 80% in error prevention",{"type":21,"tag":98,"props":1063,"children":1064},{},[1065,1070],{"type":21,"tag":36,"props":1066,"children":1067},{},[1068],{"type":27,"value":1069},"Test coverage reveals truth",{"type":27,"value":1071}," — broken tests weren't a migration problem; they revealed logic bugs",{"type":21,"tag":98,"props":1073,"children":1074},{},[1075,1080],{"type":21,"tag":36,"props":1076,"children":1077},{},[1078],{"type":27,"value":1079},"Plan for edge cases",{"type":27,"value":1081}," — our 10% \"final polish\" took 20% of the time (event handling, filters, etc.)",{"type":21,"tag":22,"props":1083,"children":1085},{"id":1084},"what-wed-do-differently",[1086],{"type":27,"value":1087},"What We'd Do Differently",{"type":21,"tag":94,"props":1089,"children":1090},{},[1091,1101,1111,1121],{"type":21,"tag":98,"props":1092,"children":1093},{},[1094,1099],{"type":21,"tag":36,"props":1095,"children":1096},{},[1097],{"type":27,"value":1098},"Start with Vite first",{"type":27,"value":1100}," — migrate tooling before components (the performance boost motivated the team)",{"type":21,"tag":98,"props":1102,"children":1103},{},[1104,1109],{"type":21,"tag":36,"props":1105,"children":1106},{},[1107],{"type":27,"value":1108},"Create better codemods",{"type":27,"value":1110}," — we wrote several after the fact that would have saved days",{"type":21,"tag":98,"props":1112,"children":1113},{},[1114,1119],{"type":21,"tag":36,"props":1115,"children":1116},{},[1117],{"type":27,"value":1118},"Allocate 15-20% for unknowns",{"type":27,"value":1120}," — we estimated 6 weeks; it took 8. That's normal for large migrations",{"type":21,"tag":98,"props":1122,"children":1123},{},[1124,1129],{"type":21,"tag":36,"props":1125,"children":1126},{},[1127],{"type":27,"value":1128},"Train junior engineers",{"type":27,"value":1130}," — force 2-3 people to migrate components solo. They become experts quickly",{"type":21,"tag":30,"props":1132,"children":1133},{},[1134],{"type":27,"value":1135},"The Vue 3 migration wasn't just about staying current. It fundamentally improved how we ship features, onboard engineers, and debug in production. If you're on the fence, the ROI is worth it.",{"title":7,"searchDepth":586,"depth":586,"links":1137},[1138,1139,1145,1146,1147],{"id":622,"depth":586,"text":625},{"id":693,"depth":586,"text":696,"children":1140},[1141,1142,1143,1144],{"id":699,"depth":592,"text":702},{"id":756,"depth":592,"text":759},{"id":870,"depth":592,"text":873},{"id":951,"depth":592,"text":954},{"id":981,"depth":586,"text":984},{"id":1025,"depth":586,"text":1028},{"id":1084,"depth":586,"text":1087},"content:blog:vue-2-to-vue-3-migration-at-scale.md","blog\u002Fvue-2-to-vue-3-migration-at-scale.md","blog\u002Fvue-2-to-vue-3-migration-at-scale",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":11,"readingTime":12,"tags":1152,"featured":16,"body":1153,"_type":602,"_id":603,"_source":604,"_file":605,"_stem":606,"_extension":607},[14,15],{"type":18,"children":1154,"toc":1590},[1155,1159,1168,1177,1181,1185,1189,1197,1201,1228,1232,1236,1244,1252,1267,1271,1275,1283,1287,1295,1305,1313,1321,1325,1329,1337,1341,1345,1349,1357,1366,1374,1378,1382,1386,1394,1398,1413,1417,1421,1425,1433,1437,1441,1445,1495,1504,1508,1512,1520,1524,1539,1543,1586],{"type":21,"tag":22,"props":1156,"children":1157},{"id":24},[1158],{"type":27,"value":28},{"type":21,"tag":30,"props":1160,"children":1161},{},[1162,1163,1167],{"type":27,"value":34},{"type":21,"tag":36,"props":1164,"children":1165},{},[1166],{"type":27,"value":40},{"type":27,"value":42},{"type":21,"tag":30,"props":1169,"children":1170},{},[1171,1172,1176],{"type":27,"value":47},{"type":21,"tag":49,"props":1173,"children":1174},{"href":51},[1175],{"type":27,"value":54},{"type":27,"value":56},{"type":21,"tag":22,"props":1178,"children":1179},{"id":59},[1180],{"type":27,"value":62},{"type":21,"tag":64,"props":1182,"children":1183},{"id":66},[1184],{"type":27,"value":69},{"type":21,"tag":30,"props":1186,"children":1187},{},[1188],{"type":27,"value":74},{"type":21,"tag":76,"props":1190,"children":1192},{"className":1191,"code":80,"language":81,"meta":7},[79],[1193],{"type":21,"tag":84,"props":1194,"children":1195},{"__ignoreMap":7},[1196],{"type":27,"value":80},{"type":21,"tag":30,"props":1198,"children":1199},{},[1200],{"type":27,"value":92},{"type":21,"tag":94,"props":1202,"children":1203},{},[1204,1212,1220],{"type":21,"tag":98,"props":1205,"children":1206},{},[1207,1211],{"type":21,"tag":36,"props":1208,"children":1209},{},[1210],{"type":27,"value":105},{"type":27,"value":107},{"type":21,"tag":98,"props":1213,"children":1214},{},[1215,1219],{"type":21,"tag":36,"props":1216,"children":1217},{},[1218],{"type":27,"value":115},{"type":27,"value":117},{"type":21,"tag":98,"props":1221,"children":1222},{},[1223,1227],{"type":21,"tag":36,"props":1224,"children":1225},{},[1226],{"type":27,"value":125},{"type":27,"value":127},{"type":21,"tag":64,"props":1229,"children":1230},{"id":130},[1231],{"type":27,"value":133},{"type":21,"tag":30,"props":1233,"children":1234},{},[1235],{"type":27,"value":138},{"type":21,"tag":76,"props":1237,"children":1239},{"className":1238,"code":143,"language":144,"meta":7},[142],[1240],{"type":21,"tag":84,"props":1241,"children":1242},{"__ignoreMap":7},[1243],{"type":27,"value":143},{"type":21,"tag":30,"props":1245,"children":1246},{},[1247,1251],{"type":21,"tag":36,"props":1248,"children":1249},{},[1250],{"type":27,"value":157},{"type":27,"value":159},{"type":21,"tag":94,"props":1253,"children":1254},{},[1255,1259,1263],{"type":21,"tag":98,"props":1256,"children":1257},{},[1258],{"type":27,"value":167},{"type":21,"tag":98,"props":1260,"children":1261},{},[1262],{"type":27,"value":172},{"type":21,"tag":98,"props":1264,"children":1265},{},[1266],{"type":27,"value":177},{"type":21,"tag":64,"props":1268,"children":1269},{"id":180},[1270],{"type":27,"value":183},{"type":21,"tag":30,"props":1272,"children":1273},{},[1274],{"type":27,"value":188},{"type":21,"tag":76,"props":1276,"children":1278},{"className":1277,"code":193,"language":194,"meta":7},[192],[1279],{"type":21,"tag":84,"props":1280,"children":1281},{"__ignoreMap":7},[1282],{"type":27,"value":193},{"type":21,"tag":30,"props":1284,"children":1285},{},[1286],{"type":27,"value":204},{"type":21,"tag":76,"props":1288,"children":1290},{"className":1289,"code":208,"language":81,"meta":7},[79],[1291],{"type":21,"tag":84,"props":1292,"children":1293},{"__ignoreMap":7},[1294],{"type":27,"value":208},{"type":21,"tag":30,"props":1296,"children":1297},{},[1298,1299,1304],{"type":27,"value":218},{"type":21,"tag":84,"props":1300,"children":1302},{"className":1301},[],[1303],{"type":27,"value":224},{"type":27,"value":226},{"type":21,"tag":76,"props":1306,"children":1308},{"className":1307,"code":230,"language":144,"meta":7},[142],[1309],{"type":21,"tag":84,"props":1310,"children":1311},{"__ignoreMap":7},[1312],{"type":27,"value":230},{"type":21,"tag":30,"props":1314,"children":1315},{},[1316,1320],{"type":21,"tag":36,"props":1317,"children":1318},{},[1319],{"type":27,"value":243},{"type":27,"value":245},{"type":21,"tag":64,"props":1322,"children":1323},{"id":248},[1324],{"type":27,"value":251},{"type":21,"tag":30,"props":1326,"children":1327},{},[1328],{"type":27,"value":256},{"type":21,"tag":76,"props":1330,"children":1332},{"className":1331,"code":260,"language":144,"meta":7},[142],[1333],{"type":21,"tag":84,"props":1334,"children":1335},{"__ignoreMap":7},[1336],{"type":27,"value":260},{"type":21,"tag":30,"props":1338,"children":1339},{},[1340],{"type":27,"value":270},{"type":21,"tag":64,"props":1342,"children":1343},{"id":273},[1344],{"type":27,"value":276},{"type":21,"tag":30,"props":1346,"children":1347},{},[1348],{"type":27,"value":281},{"type":21,"tag":76,"props":1350,"children":1352},{"className":1351,"code":285,"language":144,"meta":7},[142],[1353],{"type":21,"tag":84,"props":1354,"children":1355},{"__ignoreMap":7},[1356],{"type":27,"value":285},{"type":21,"tag":30,"props":1358,"children":1359},{},[1360,1361,1365],{"type":27,"value":295},{"type":21,"tag":36,"props":1362,"children":1363},{},[1364],{"type":27,"value":300},{"type":27,"value":226},{"type":21,"tag":76,"props":1367,"children":1369},{"className":1368,"code":306,"language":307,"meta":7},[305],[1370],{"type":21,"tag":84,"props":1371,"children":1372},{"__ignoreMap":7},[1373],{"type":27,"value":306},{"type":21,"tag":30,"props":1375,"children":1376},{},[1377],{"type":27,"value":317},{"type":21,"tag":64,"props":1379,"children":1380},{"id":320},[1381],{"type":27,"value":323},{"type":21,"tag":30,"props":1383,"children":1384},{},[1385],{"type":27,"value":328},{"type":21,"tag":76,"props":1387,"children":1389},{"className":1388,"code":332,"language":194,"meta":7},[192],[1390],{"type":21,"tag":84,"props":1391,"children":1392},{"__ignoreMap":7},[1393],{"type":27,"value":332},{"type":21,"tag":30,"props":1395,"children":1396},{},[1397],{"type":27,"value":342},{"type":21,"tag":94,"props":1399,"children":1400},{},[1401,1405,1409],{"type":21,"tag":98,"props":1402,"children":1403},{},[1404],{"type":27,"value":350},{"type":21,"tag":98,"props":1406,"children":1407},{},[1408],{"type":27,"value":355},{"type":21,"tag":98,"props":1410,"children":1411},{},[1412],{"type":27,"value":360},{"type":21,"tag":30,"props":1414,"children":1415},{},[1416],{"type":27,"value":365},{"type":21,"tag":64,"props":1418,"children":1419},{"id":368},[1420],{"type":27,"value":371},{"type":21,"tag":30,"props":1422,"children":1423},{},[1424],{"type":27,"value":376},{"type":21,"tag":76,"props":1426,"children":1428},{"className":1427,"code":380,"language":144,"meta":7},[142],[1429],{"type":21,"tag":84,"props":1430,"children":1431},{"__ignoreMap":7},[1432],{"type":27,"value":380},{"type":21,"tag":30,"props":1434,"children":1435},{},[1436],{"type":27,"value":390},{"type":21,"tag":22,"props":1438,"children":1439},{"id":393},[1440],{"type":27,"value":396},{"type":21,"tag":30,"props":1442,"children":1443},{},[1444],{"type":27,"value":401},{"type":21,"tag":403,"props":1446,"children":1447},{},[1448,1470],{"type":21,"tag":407,"props":1449,"children":1450},{},[1451],{"type":21,"tag":411,"props":1452,"children":1453},{},[1454,1458,1462,1466],{"type":21,"tag":415,"props":1455,"children":1456},{},[1457],{"type":27,"value":419},{"type":21,"tag":415,"props":1459,"children":1460},{},[1461],{"type":27,"value":424},{"type":21,"tag":415,"props":1463,"children":1464},{},[1465],{"type":27,"value":429},{"type":21,"tag":415,"props":1467,"children":1468},{},[1469],{"type":27,"value":434},{"type":21,"tag":436,"props":1471,"children":1472},{},[1473],{"type":21,"tag":411,"props":1474,"children":1475},{},[1476,1480,1484,1488],{"type":21,"tag":443,"props":1477,"children":1478},{},[1479],{"type":27,"value":447},{"type":21,"tag":443,"props":1481,"children":1482},{},[1483],{"type":27,"value":40},{"type":21,"tag":443,"props":1485,"children":1486},{},[1487],{"type":27,"value":456},{"type":21,"tag":443,"props":1489,"children":1490},{},[1491],{"type":21,"tag":36,"props":1492,"children":1493},{},[1494],{"type":27,"value":464},{"type":21,"tag":30,"props":1496,"children":1497},{},[1498,1499,1503],{"type":27,"value":469},{"type":21,"tag":49,"props":1500,"children":1501},{"href":51},[1502],{"type":27,"value":474},{"type":27,"value":476},{"type":21,"tag":22,"props":1505,"children":1506},{"id":479},[1507],{"type":27,"value":482},{"type":21,"tag":30,"props":1509,"children":1510},{},[1511],{"type":27,"value":487},{"type":21,"tag":76,"props":1513,"children":1515},{"className":1514,"code":491,"language":81,"meta":7},[79],[1516],{"type":21,"tag":84,"props":1517,"children":1518},{"__ignoreMap":7},[1519],{"type":27,"value":491},{"type":21,"tag":30,"props":1521,"children":1522},{},[1523],{"type":27,"value":501},{"type":21,"tag":94,"props":1525,"children":1526},{},[1527,1531,1535],{"type":21,"tag":98,"props":1528,"children":1529},{},[1530],{"type":27,"value":509},{"type":21,"tag":98,"props":1532,"children":1533},{},[1534],{"type":27,"value":514},{"type":21,"tag":98,"props":1536,"children":1537},{},[1538],{"type":27,"value":519},{"type":21,"tag":22,"props":1540,"children":1541},{"id":522},[1542],{"type":27,"value":525},{"type":21,"tag":527,"props":1544,"children":1545},{},[1546,1554,1562,1570,1578],{"type":21,"tag":98,"props":1547,"children":1548},{},[1549,1553],{"type":21,"tag":36,"props":1550,"children":1551},{},[1552],{"type":27,"value":537},{"type":27,"value":539},{"type":21,"tag":98,"props":1555,"children":1556},{},[1557,1561],{"type":21,"tag":36,"props":1558,"children":1559},{},[1560],{"type":27,"value":547},{"type":27,"value":549},{"type":21,"tag":98,"props":1563,"children":1564},{},[1565,1569],{"type":21,"tag":36,"props":1566,"children":1567},{},[1568],{"type":27,"value":557},{"type":27,"value":559},{"type":21,"tag":98,"props":1571,"children":1572},{},[1573,1577],{"type":21,"tag":36,"props":1574,"children":1575},{},[1576],{"type":27,"value":567},{"type":27,"value":569},{"type":21,"tag":98,"props":1579,"children":1580},{},[1581,1585],{"type":21,"tag":36,"props":1582,"children":1583},{},[1584],{"type":27,"value":577},{"type":27,"value":579},{"type":21,"tag":30,"props":1587,"children":1588},{},[1589],{"type":27,"value":584},{"title":7,"searchDepth":586,"depth":586,"links":1591},[1592,1593,1602,1603,1604],{"id":24,"depth":586,"text":28},{"id":59,"depth":586,"text":62,"children":1594},[1595,1596,1597,1598,1599,1600,1601],{"id":66,"depth":592,"text":69},{"id":130,"depth":592,"text":133},{"id":180,"depth":592,"text":183},{"id":248,"depth":592,"text":251},{"id":273,"depth":592,"text":276},{"id":320,"depth":592,"text":323},{"id":368,"depth":592,"text":371},{"id":393,"depth":586,"text":396},{"id":479,"depth":586,"text":482},{"id":522,"depth":586,"text":525},{"_path":1606,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1607,"description":1608,"date":1609,"updated":11,"readingTime":1610,"tags":1611,"featured":6,"body":1612,"_type":602,"_id":2147,"_source":604,"_file":2148,"_stem":2149,"_extension":607},"\u002Fblog\u002Fbuilding-scalable-design-systems-vue","Building Scalable Design Systems in Vue: Tokens, APIs, and Versioning That Scale","How to architect a design system that scales across teams. Learn component APIs, token management, versioning strategies, and Storybook workflows that actually work.","2024-07-30",13,[616,617],{"type":18,"children":1613,"toc":2133},[1614,1620,1625,1630,1653,1666,1672,1678,1683,1691,1696,1719,1725,1730,1739,1744,1787,1793,1798,1807,1812,1835,1841,1846,1852,1861,1867,1876,1882,1887,1896,1902,1907,1916,1921,1954,1959,1970,1976,1988,2041,2047,2059,2065,2128],{"type":21,"tag":22,"props":1615,"children":1617},{"id":1616},"the-problem-with-ad-hoc-components",[1618],{"type":27,"value":1619},"The Problem with Ad-Hoc Components",{"type":21,"tag":30,"props":1621,"children":1622},{},[1623],{"type":27,"value":1624},"We started with a few buttons and modals in a shared folder. A few years later, components were spread across repositories, with competing ways to handle form validation, competing naming conventions, and constant debates about \"is this a Button or a PrimaryButton?\"",{"type":21,"tag":30,"props":1626,"children":1627},{},[1628],{"type":27,"value":1629},"Inconsistency was costing us:",{"type":21,"tag":94,"props":1631,"children":1632},{},[1633,1638,1643,1648],{"type":21,"tag":98,"props":1634,"children":1635},{},[1636],{"type":27,"value":1637},"New team members took weeks to learn existing patterns",{"type":21,"tag":98,"props":1639,"children":1640},{},[1641],{"type":27,"value":1642},"Components were re-implemented multiple times across products",{"type":21,"tag":98,"props":1644,"children":1645},{},[1646],{"type":27,"value":1647},"No single source of truth for design decisions",{"type":21,"tag":98,"props":1649,"children":1650},{},[1651],{"type":27,"value":1652},"Design-to-dev handoff took meetings just to clarify prop names",{"type":21,"tag":30,"props":1654,"children":1655},{},[1656,1658,1664],{"type":27,"value":1657},"We needed a design system. The production version of this story — with the monorepo layout, versioning policy, and adoption mechanics — is in the ",{"type":21,"tag":49,"props":1659,"children":1661},{"href":1660},"\u002Fcase-studies\u002Fmonorepo-design-system",[1662],{"type":27,"value":1663},"monorepo design system case study",{"type":27,"value":1665},".",{"type":21,"tag":22,"props":1667,"children":1669},{"id":1668},"architecture-the-foundation",[1670],{"type":27,"value":1671},"Architecture: The Foundation",{"type":21,"tag":64,"props":1673,"children":1675},{"id":1674},"_1-monorepo-structure",[1676],{"type":27,"value":1677},"1. Monorepo Structure",{"type":21,"tag":30,"props":1679,"children":1680},{},[1681],{"type":27,"value":1682},"We used pnpm workspaces:",{"type":21,"tag":76,"props":1684,"children":1686},{"code":1685},"packages\u002F\n├── @acme\u002Fdesign-system\u002F\n│   ├── src\u002F\n│   │   ├── components\u002F\n│   │   │   ├── Button\u002F\n│   │   │   │   ├── Button.vue\n│   │   │   │   ├── Button.test.ts\n│   │   │   │   ├── Button.stories.ts\n│   │   │   │   └── index.ts\n│   │   │   ├── Input\u002F\n│   │   │   └── ...\n│   │   ├── styles\u002F\n│   │   │   ├── tokens.css\n│   │   │   ├── base.css\n│   │   │   └── utilities.css\n│   │   └── index.ts\n│   ├── package.json\n│   └── tsconfig.json\n├── @acme\u002Ficons\u002F\n├── @acme\u002Fdocs\u002F\n└── @acme\u002Fbrand\u002F\n",[1687],{"type":21,"tag":84,"props":1688,"children":1689},{"__ignoreMap":7},[1690],{"type":27,"value":1685},{"type":21,"tag":30,"props":1692,"children":1693},{},[1694],{"type":27,"value":1695},"Benefits:",{"type":21,"tag":94,"props":1697,"children":1698},{},[1699,1704,1709,1714],{"type":21,"tag":98,"props":1700,"children":1701},{},[1702],{"type":27,"value":1703},"Monorepo enforces consistency",{"type":21,"tag":98,"props":1705,"children":1706},{},[1707],{"type":27,"value":1708},"Shared tooling configuration",{"type":21,"tag":98,"props":1710,"children":1711},{},[1712],{"type":27,"value":1713},"Easy to test components in isolation",{"type":21,"tag":98,"props":1715,"children":1716},{},[1717],{"type":27,"value":1718},"Clear dependency management",{"type":21,"tag":64,"props":1720,"children":1722},{"id":1721},"_2-component-api-design",[1723],{"type":27,"value":1724},"2. Component API Design",{"type":21,"tag":30,"props":1726,"children":1727},{},[1728],{"type":27,"value":1729},"We established strict rules for component props:",{"type":21,"tag":76,"props":1731,"children":1734},{"code":1732,"language":144,"meta":7,"className":1733},"\u002F\u002F ❌ Bad: Too many variants\ninterface ButtonProps {\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n  variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'info'\n  disabled?: boolean\n  loading?: boolean\n  fullWidth?: boolean\n}\n\n\u002F\u002F ✅ Good: Composition over props\ninterface ButtonProps {\n  variant?: 'primary' | 'secondary' | 'danger'\n  size?: 'sm' | 'md' | 'lg'\n  disabled?: boolean\n  loading?: boolean\n}\n\ninterface ButtonGroupProps {\n  orientation?: 'horizontal' | 'vertical'\n}\n\n\u002F\u002F Compose complex UIs from simple components\n",[142],[1735],{"type":21,"tag":84,"props":1736,"children":1737},{"__ignoreMap":7},[1738],{"type":27,"value":1732},{"type":21,"tag":30,"props":1740,"children":1741},{},[1742],{"type":27,"value":1743},"Guidelines:",{"type":21,"tag":527,"props":1745,"children":1746},{},[1747,1757,1767,1777],{"type":21,"tag":98,"props":1748,"children":1749},{},[1750,1755],{"type":21,"tag":36,"props":1751,"children":1752},{},[1753],{"type":27,"value":1754},"Keep props count \u003C 8",{"type":27,"value":1756}," — if you need more, you're probably missing a abstraction",{"type":21,"tag":98,"props":1758,"children":1759},{},[1760,1765],{"type":21,"tag":36,"props":1761,"children":1762},{},[1763],{"type":27,"value":1764},"Use CSS variables for styling",{"type":27,"value":1766}," — don't expose every style property",{"type":21,"tag":98,"props":1768,"children":1769},{},[1770,1775],{"type":21,"tag":36,"props":1771,"children":1772},{},[1773],{"type":27,"value":1774},"Provide sensible defaults",{"type":27,"value":1776}," — most components should work with zero props",{"type":21,"tag":98,"props":1778,"children":1779},{},[1780,1785],{"type":21,"tag":36,"props":1781,"children":1782},{},[1783],{"type":27,"value":1784},"Document the \"why\"",{"type":27,"value":1786}," — every prop should have a reason",{"type":21,"tag":64,"props":1788,"children":1790},{"id":1789},"_3-design-tokens",[1791],{"type":27,"value":1792},"3. Design Tokens",{"type":21,"tag":30,"props":1794,"children":1795},{},[1796],{"type":27,"value":1797},"Tokens are the single source of truth:",{"type":21,"tag":76,"props":1799,"children":1802},{"code":1800,"language":144,"meta":7,"className":1801},"\u002F\u002F src\u002Fstyles\u002Ftokens.ts\nexport const tokens = {\n  colors: {\n    primary: '#0070F3',\n    secondary: '#666',\n    danger: '#E81E3D',\n    success: '#21BA45',\n  },\n  spacing: {\n    xs: '0.25rem',\n    sm: '0.5rem',\n    md: '1rem',\n    lg: '1.5rem',\n    xl: '2rem',\n  },\n  typography: {\n    body: { size: '1rem', weight: 400, lineHeight: 1.5 },\n    heading: { size: '1.5rem', weight: 600, lineHeight: 1.2 },\n  },\n  radius: {\n    sm: '0.25rem',\n    md: '0.5rem',\n    lg: '1rem',\n  },\n  shadow: {\n    sm: '0 1px 2px rgba(0,0,0,0.05)',\n    md: '0 4px 6px rgba(0,0,0,0.1)',\n    lg: '0 10px 15px rgba(0,0,0,0.2)',\n  },\n}\n\n\u002F\u002F Generate CSS variables\nconst generateCSSVariables = (tokens: any, prefix = '') => {\n  let css = ':root {\\n'\n  Object.entries(tokens).forEach(([key, value]) => {\n    if (typeof value === 'object') {\n      css += generateCSSVariables(value, `${prefix}${key}-`)\n    } else {\n      css += `  --${prefix}${key}: ${value};\\n`\n    }\n  })\n  css += '}\\n'\n  return css\n}\n",[142],[1803],{"type":21,"tag":84,"props":1804,"children":1805},{"__ignoreMap":7},[1806],{"type":27,"value":1800},{"type":21,"tag":30,"props":1808,"children":1809},{},[1810],{"type":27,"value":1811},"This single file becomes the source of truth for:",{"type":21,"tag":94,"props":1813,"children":1814},{},[1815,1820,1825,1830],{"type":21,"tag":98,"props":1816,"children":1817},{},[1818],{"type":27,"value":1819},"Figma design tokens",{"type":21,"tag":98,"props":1821,"children":1822},{},[1823],{"type":27,"value":1824},"CSS variables",{"type":21,"tag":98,"props":1826,"children":1827},{},[1828],{"type":27,"value":1829},"Type definitions",{"type":21,"tag":98,"props":1831,"children":1832},{},[1833],{"type":27,"value":1834},"Storybook theming",{"type":21,"tag":64,"props":1836,"children":1838},{"id":1837},"_4-component-patterns",[1839],{"type":27,"value":1840},"4. Component Patterns",{"type":21,"tag":30,"props":1842,"children":1843},{},[1844],{"type":27,"value":1845},"We established patterns for common scenarios:",{"type":21,"tag":799,"props":1847,"children":1849},{"id":1848},"simple-component",[1850],{"type":27,"value":1851},"Simple Component",{"type":21,"tag":76,"props":1853,"children":1856},{"code":1854,"language":194,"meta":7,"className":1855},"\u003Cscript setup lang=\"ts\">\ninterface Props {\n  variant?: 'primary' | 'secondary'\n  size?: 'sm' | 'md' | 'lg'\n  disabled?: boolean\n}\n\nwithDefaults(defineProps\u003CProps>(), {\n  variant: 'primary',\n  size: 'md',\n})\n\ndefineEmits\u003C{\n  click: [event: MouseEvent]\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton\n    :class=\"[\n      'btn',\n      `btn-${variant}`,\n      `btn-${size}`,\n      { 'is-disabled': disabled }\n    ]\"\n    :disabled=\"disabled\"\n  >\n    \u003Cslot \u002F>\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.btn {\n  --py: var(--spacing-md);\n  --px: var(--spacing-lg);\n  padding: var(--py) var(--px);\n  background: var(--color-primary);\n  border-radius: var(--radius-md);\n  transition: all 0.2s ease;\n}\n\n.btn:hover:not(.is-disabled) {\n  background: var(--color-primary-dark);\n}\n\u003C\u002Fstyle>\n",[192],[1857],{"type":21,"tag":84,"props":1858,"children":1859},{"__ignoreMap":7},[1860],{"type":27,"value":1854},{"type":21,"tag":799,"props":1862,"children":1864},{"id":1863},"compound-component",[1865],{"type":27,"value":1866},"Compound Component",{"type":21,"tag":76,"props":1868,"children":1871},{"code":1869,"language":194,"meta":7,"className":1870},"\u003C!-- FieldGroup.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface Props {\n  label?: string\n  error?: string\n  hint?: string\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {})\nconst id = `field-${Math.random().toString(36).slice(2, 9)}`\n\nprovide('fieldId', id)\nprovide('fieldError', props.error)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cfieldset class=\"field-group\">\n    \u003Clabel v-if=\"label\" :for=\"id\" class=\"field-label\">\n      {{ label }}\n    \u003C\u002Flabel>\n    \u003Cslot \u002F>\n    \u003Cp v-if=\"error\" :id=\"`${id}-error`\" class=\"field-error\">{{ error }}\u003C\u002Fp>\n    \u003Cp v-if=\"hint && !error\" :id=\"`${id}-hint`\" class=\"field-hint\">{{ hint }}\u003C\u002Fp>\n  \u003C\u002Ffieldset>\n\u003C\u002Ftemplate>\n\n\u003C!-- Usage -->\n\u003CFieldGroup label=\"Email\" error=\"Invalid email\">\n  \u003Cinput type=\"email\" required \u002F>\n\u003C\u002FFieldGroup>\n",[192],[1872],{"type":21,"tag":84,"props":1873,"children":1874},{"__ignoreMap":7},[1875],{"type":27,"value":1869},{"type":21,"tag":22,"props":1877,"children":1879},{"id":1878},"documentation-storybook",[1880],{"type":27,"value":1881},"Documentation & Storybook",{"type":21,"tag":30,"props":1883,"children":1884},{},[1885],{"type":27,"value":1886},"Storybook is critical for design system success:",{"type":21,"tag":76,"props":1888,"children":1891},{"code":1889,"language":144,"meta":7,"className":1890},"\u002F\u002F Button.stories.ts\nimport type { Meta, StoryObj } from '@storybook\u002Fvue3'\nimport Button from '.\u002FButton.vue'\n\nconst meta: Meta\u003Ctypeof Button> = {\n  component: Button,\n  tags: ['autodocs'],\n  argTypes: {\n    variant: {\n      options: ['primary', 'secondary', 'danger'],\n      control: { type: 'select' }\n    },\n    size: {\n      options: ['sm', 'md', 'lg'],\n      control: { type: 'select' }\n    }\n  }\n}\n\nexport default meta\n\nexport const Primary: StoryObj = {\n  args: {\n    variant: 'primary',\n    size: 'md'\n  },\n  render: (args) => ({\n    components: { Button },\n    setup() { return { args } },\n    template: '\u003CButton v-bind=\"args\">Click me\u003C\u002FButton>'\n  })\n}\n\nexport const Disabled: StoryObj = {\n  args: { disabled: true }\n}\n\nexport const All: StoryObj = {\n  render: () => ({\n    template: `\n      \u003Cdiv class=\"space-y-4\">\n        \u003CButton variant=\"primary\">Primary\u003C\u002FButton>\n        \u003CButton variant=\"secondary\">Secondary\u003C\u002FButton>\n        \u003CButton variant=\"danger\">Danger\u003C\u002FButton>\n      \u003C\u002Fdiv>\n    `\n  })\n}\n",[142],[1892],{"type":21,"tag":84,"props":1893,"children":1894},{"__ignoreMap":7},[1895],{"type":27,"value":1889},{"type":21,"tag":22,"props":1897,"children":1899},{"id":1898},"versioning-releases",[1900],{"type":27,"value":1901},"Versioning & Releases",{"type":21,"tag":30,"props":1903,"children":1904},{},[1905],{"type":27,"value":1906},"Design systems are libraries. Treat them like it:",{"type":21,"tag":76,"props":1908,"children":1911},{"code":1909,"language":81,"meta":7,"className":1910},"# semantic-release configuration\nnpm install -D semantic-release @semantic-release\u002Fcommit-analyzer @semantic-release\u002Frelease-notes-generator\n\n# Automatically bump versions based on commits\n# BREAKING CHANGE: → major\n# feat: → minor\n# fix: → patch\n",[79],[1912],{"type":21,"tag":84,"props":1913,"children":1914},{"__ignoreMap":7},[1915],{"type":27,"value":1909},{"type":21,"tag":30,"props":1917,"children":1918},{},[1919],{"type":27,"value":1920},"We published:",{"type":21,"tag":94,"props":1922,"children":1923},{},[1924,1934,1944],{"type":21,"tag":98,"props":1925,"children":1926},{},[1927,1932],{"type":21,"tag":36,"props":1928,"children":1929},{},[1930],{"type":27,"value":1931},"v1.0.0",{"type":27,"value":1933}," — the core component set\n",{"type":21,"tag":98,"props":1935,"children":1936},{},[1937,1942],{"type":21,"tag":36,"props":1938,"children":1939},{},[1940],{"type":27,"value":1941},"v1.2.3",{"type":27,"value":1943}," — new tokens, backward compatible",{"type":21,"tag":98,"props":1945,"children":1946},{},[1947,1952],{"type":21,"tag":36,"props":1948,"children":1949},{},[1950],{"type":27,"value":1951},"v2.0.0",{"type":27,"value":1953}," — redesign with breaking changes",{"type":21,"tag":30,"props":1955,"children":1956},{},[1957],{"type":27,"value":1958},"Each version went to npm registry. Teams pinned versions:",{"type":21,"tag":76,"props":1960,"children":1965},{"code":1961,"language":1962,"meta":7,"className":1963},"{\n  \"dependencies\": {\n    \"@acme\u002Fdesign-system\": \"^1.2.0\"\n  }\n}\n","json",[1964],"language-json",[1966],{"type":21,"tag":84,"props":1967,"children":1968},{"__ignoreMap":7},[1969],{"type":27,"value":1961},{"type":21,"tag":22,"props":1971,"children":1973},{"id":1972},"adoption-strategy",[1974],{"type":27,"value":1975},"Adoption Strategy",{"type":21,"tag":30,"props":1977,"children":1978},{},[1979,1981,1986],{"type":27,"value":1980},"Getting teams to ",{"type":21,"tag":880,"props":1982,"children":1983},{},[1984],{"type":27,"value":1985},"use",{"type":27,"value":1987}," the system was as important as building it:",{"type":21,"tag":527,"props":1989,"children":1990},{},[1991,2001,2011,2021,2031],{"type":21,"tag":98,"props":1992,"children":1993},{},[1994,1999],{"type":21,"tag":36,"props":1995,"children":1996},{},[1997],{"type":27,"value":1998},"Dogfood internally",{"type":27,"value":2000}," — our flagship product used v1.0 before public release",{"type":21,"tag":98,"props":2002,"children":2003},{},[2004,2009],{"type":21,"tag":36,"props":2005,"children":2006},{},[2007],{"type":27,"value":2008},"Create migration guides",{"type":27,"value":2010}," — \"How to replace your Button with DesignSystemButton\"",{"type":21,"tag":98,"props":2012,"children":2013},{},[2014,2019],{"type":21,"tag":36,"props":2015,"children":2016},{},[2017],{"type":27,"value":2018},"Celebrate wins",{"type":27,"value":2020}," — \"we deleted three duplicate Button implementations this sprint\" beats any adoption mandate",{"type":21,"tag":98,"props":2022,"children":2023},{},[2024,2029],{"type":21,"tag":36,"props":2025,"children":2026},{},[2027],{"type":27,"value":2028},"Make it easier to use than not",{"type":27,"value":2030}," — publish to npm, show install in docs",{"type":21,"tag":98,"props":2032,"children":2033},{},[2034,2039],{"type":21,"tag":36,"props":2035,"children":2036},{},[2037],{"type":27,"value":2038},"Maintain it publicly",{"type":27,"value":2040}," — commit to performance, accessibility, TypeScript support",{"type":21,"tag":22,"props":2042,"children":2044},{"id":2043},"adoption-metrics",[2045],{"type":27,"value":2046},"Adoption Metrics",{"type":21,"tag":30,"props":2048,"children":2049},{},[2050,2052,2057],{"type":27,"value":2051},"The measured outcome on the production system this post draws from: ",{"type":21,"tag":36,"props":2053,"children":2054},{},[2055],{"type":27,"value":2056},"20% faster feature delivery",{"type":27,"value":2058}," on projects consuming the library — teams assemble features from documented components instead of rebuilding primitives.",{"type":21,"tag":22,"props":2060,"children":2062},{"id":2061},"key-lessons",[2063],{"type":27,"value":2064},"Key Lessons",{"type":21,"tag":527,"props":2066,"children":2067},{},[2068,2078,2088,2098,2108,2118],{"type":21,"tag":98,"props":2069,"children":2070},{},[2071,2076],{"type":21,"tag":36,"props":2072,"children":2073},{},[2074],{"type":27,"value":2075},"Tokens are the foundation",{"type":27,"value":2077}," — invest heavily in design tokens before components",{"type":21,"tag":98,"props":2079,"children":2080},{},[2081,2086],{"type":21,"tag":36,"props":2082,"children":2083},{},[2084],{"type":27,"value":2085},"API design is everything",{"type":27,"value":2087}," — a good API is adopted naturally; a bad one requires enforcement",{"type":21,"tag":98,"props":2089,"children":2090},{},[2091,2096],{"type":21,"tag":36,"props":2092,"children":2093},{},[2094],{"type":27,"value":2095},"Storybook isn't optional",{"type":27,"value":2097}," — it's how designers and developers communicate",{"type":21,"tag":98,"props":2099,"children":2100},{},[2101,2106],{"type":21,"tag":36,"props":2102,"children":2103},{},[2104],{"type":27,"value":2105},"Version properly",{"type":27,"value":2107}," — teams need confidence that upgrades won't break things",{"type":21,"tag":98,"props":2109,"children":2110},{},[2111,2116],{"type":21,"tag":36,"props":2112,"children":2113},{},[2114],{"type":27,"value":2115},"One person owns it",{"type":27,"value":2117}," — design systems need a maintainer, not a committee",{"type":21,"tag":98,"props":2119,"children":2120},{},[2121,2126],{"type":21,"tag":36,"props":2122,"children":2123},{},[2124],{"type":27,"value":2125},"Measure success by adoption",{"type":27,"value":2127}," — a perfect system nobody uses is useless",{"type":21,"tag":30,"props":2129,"children":2130},{},[2131],{"type":27,"value":2132},"The design system went from idea to adoption in 4 months with 2 full-time engineers. The payoff: 30% faster feature development, 50% fewer component bugs, and consistent user experience across products.",{"title":7,"searchDepth":586,"depth":586,"links":2134},[2135,2136,2142,2143,2144,2145,2146],{"id":1616,"depth":586,"text":1619},{"id":1668,"depth":586,"text":1671,"children":2137},[2138,2139,2140,2141],{"id":1674,"depth":592,"text":1677},{"id":1721,"depth":592,"text":1724},{"id":1789,"depth":592,"text":1792},{"id":1837,"depth":592,"text":1840},{"id":1878,"depth":586,"text":1881},{"id":1898,"depth":586,"text":1901},{"id":1972,"depth":586,"text":1975},{"id":2043,"depth":586,"text":2046},{"id":2061,"depth":586,"text":2064},"content:blog:building-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue",{"_path":2151,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2152,"description":2153,"date":2154,"updated":11,"readingTime":2155,"tags":2156,"featured":6,"body":2157,"_type":602,"_id":2952,"_source":604,"_file":2953,"_stem":2954,"_extension":607},"\u002Fblog\u002Ffrontend-architecture-for-large-saas","Frontend Architecture for Large SaaS: Monorepos, Micro-Frontends & Module Federation","Learn how to architect frontend systems for enterprise SaaS. Covers monorepo strategies, micro-frontend patterns, Module Federation, state management at scale, and team autonomy.","2024-07-15",15,[617,15],{"type":18,"children":2158,"toc":2930},[2159,2165,2170,2223,2228,2234,2239,2247,2253,2306,2312,2355,2360,2366,2371,2379,2384,2407,2413,2419,2428,2434,2443,2448,2471,2477,2482,2535,2541,2546,2555,2559,2602,2608,2613,2622,2628,2634,2642,2647,2656,2662,2671,2677,2682,2690,2696,2701,2709,2714,2720,2725,2776,2782,2787,2811,2816,2822,2827,2925],{"type":21,"tag":22,"props":2160,"children":2162},{"id":2161},"the-scaling-problem",[2163],{"type":27,"value":2164},"The Scaling Problem",{"type":21,"tag":30,"props":2166,"children":2167},{},[2168],{"type":27,"value":2169},"Our SaaS application started as a single Vue.js codebase. Three years later, 40 engineers across 8 teams were stepping on each other's toes:",{"type":21,"tag":94,"props":2171,"children":2172},{},[2173,2183,2193,2203,2213],{"type":21,"tag":98,"props":2174,"children":2175},{},[2176,2181],{"type":21,"tag":36,"props":2177,"children":2178},{},[2179],{"type":27,"value":2180},"Git conflicts",{"type":27,"value":2182}," — 20+ per day from different teams modifying the same files",{"type":21,"tag":98,"props":2184,"children":2185},{},[2186,2191],{"type":21,"tag":36,"props":2187,"children":2188},{},[2189],{"type":27,"value":2190},"Deployment bottlenecks",{"type":27,"value":2192}," — one bug in team A's feature blocked team B's release",{"type":21,"tag":98,"props":2194,"children":2195},{},[2196,2201],{"type":21,"tag":36,"props":2197,"children":2198},{},[2199],{"type":27,"value":2200},"Code ownership unclear",{"type":27,"value":2202}," — who owns the form validation layer? Three people claim they do",{"type":21,"tag":98,"props":2204,"children":2205},{},[2206,2211],{"type":21,"tag":36,"props":2207,"children":2208},{},[2209],{"type":27,"value":2210},"Bundle bloat",{"type":27,"value":2212}," — features for team A's customers were loading for team B's customers",{"type":21,"tag":98,"props":2214,"children":2215},{},[2216,2221],{"type":21,"tag":36,"props":2217,"children":2218},{},[2219],{"type":27,"value":2220},"Development friction",{"type":27,"value":2222}," — new team members took 4 weeks to understand the codebase",{"type":21,"tag":30,"props":2224,"children":2225},{},[2226],{"type":27,"value":2227},"Something had to change.",{"type":21,"tag":22,"props":2229,"children":2231},{"id":2230},"option-1-monorepo-what-we-chose",[2232],{"type":27,"value":2233},"Option 1: Monorepo (What We Chose)",{"type":21,"tag":30,"props":2235,"children":2236},{},[2237],{"type":27,"value":2238},"We chose a monorepo structure with pnpm workspaces:",{"type":21,"tag":76,"props":2240,"children":2242},{"code":2241},"apps\u002F\n├── dashboard\u002F          # Main SaaS dashboard\n├── admin-panel\u002F        # Admin-only features\n├── billing\u002F            # Standalone billing app\n└── docs\u002F               # Internal & external docs\n\npackages\u002F\n├── design-system\u002F      # Shared components\n├── api-client\u002F         # Shared API layer\n├── shared-utils\u002F       # Common utilities\n├── shared-state\u002F       # Pinia stores\n└── icons\u002F              # SVG icons library\n",[2243],{"type":21,"tag":84,"props":2244,"children":2245},{"__ignoreMap":7},[2246],{"type":27,"value":2241},{"type":21,"tag":64,"props":2248,"children":2250},{"id":2249},"monorepo-benefits",[2251],{"type":27,"value":2252},"Monorepo Benefits",{"type":21,"tag":94,"props":2254,"children":2255},{},[2256,2266,2276,2286,2296],{"type":21,"tag":98,"props":2257,"children":2258},{},[2259,2264],{"type":21,"tag":36,"props":2260,"children":2261},{},[2262],{"type":27,"value":2263},"Single source of truth",{"type":27,"value":2265}," for shared code",{"type":21,"tag":98,"props":2267,"children":2268},{},[2269,2274],{"type":21,"tag":36,"props":2270,"children":2271},{},[2272],{"type":27,"value":2273},"Atomic commits",{"type":27,"value":2275}," — can update multiple packages together",{"type":21,"tag":98,"props":2277,"children":2278},{},[2279,2284],{"type":21,"tag":36,"props":2280,"children":2281},{},[2282],{"type":27,"value":2283},"Shared CI\u002FCD",{"type":27,"value":2285}," — one build pipeline for all apps",{"type":21,"tag":98,"props":2287,"children":2288},{},[2289,2294],{"type":21,"tag":36,"props":2290,"children":2291},{},[2292],{"type":27,"value":2293},"Consistent tooling",{"type":27,"value":2295}," — eslint, prettier, typescript config",{"type":21,"tag":98,"props":2297,"children":2298},{},[2299,2304],{"type":21,"tag":36,"props":2300,"children":2301},{},[2302],{"type":27,"value":2303},"Easy refactoring",{"type":27,"value":2305}," — move code between apps without npm publish",{"type":21,"tag":64,"props":2307,"children":2309},{"id":2308},"monorepo-drawbacks",[2310],{"type":27,"value":2311},"Monorepo Drawbacks",{"type":21,"tag":94,"props":2313,"children":2314},{},[2315,2325,2335,2345],{"type":21,"tag":98,"props":2316,"children":2317},{},[2318,2323],{"type":21,"tag":36,"props":2319,"children":2320},{},[2321],{"type":27,"value":2322},"Build complexity",{"type":27,"value":2324}," — must rebuild only changed packages",{"type":21,"tag":98,"props":2326,"children":2327},{},[2328,2333],{"type":21,"tag":36,"props":2329,"children":2330},{},[2331],{"type":27,"value":2332},"Dependency management",{"type":27,"value":2334}," — circular dependencies possible",{"type":21,"tag":98,"props":2336,"children":2337},{},[2338,2343],{"type":21,"tag":36,"props":2339,"children":2340},{},[2341],{"type":27,"value":2342},"Harder to open-source",{"type":27,"value":2344}," — internal code in same repo",{"type":21,"tag":98,"props":2346,"children":2347},{},[2348,2353],{"type":21,"tag":36,"props":2349,"children":2350},{},[2351],{"type":27,"value":2352},"Steeper learning curve",{"type":27,"value":2354}," — new engineers need to understand workspace structure",{"type":21,"tag":30,"props":2356,"children":2357},{},[2358],{"type":27,"value":2359},"For us, benefits outweighed drawbacks.",{"type":21,"tag":22,"props":2361,"children":2363},{"id":2362},"option-2-micro-frontends-the-alternative",[2364],{"type":27,"value":2365},"Option 2: Micro-Frontends (The Alternative)",{"type":21,"tag":30,"props":2367,"children":2368},{},[2369],{"type":27,"value":2370},"For teams that want complete independence, micro-frontends are powerful:",{"type":21,"tag":76,"props":2372,"children":2374},{"code":2373},"main-app\u002F\n├── shell\u002F              # Shell\u002Fhost\n├── packages\u002F\n│   ├── dashboard-mfe\u002F  # Micro-frontend (team A)\n│   ├── billing-mfe\u002F    # Micro-frontend (team B)\n│   └── admin-mfe\u002F      # Micro-frontend (team C)\n",[2375],{"type":21,"tag":84,"props":2376,"children":2377},{"__ignoreMap":7},[2378],{"type":27,"value":2373},{"type":21,"tag":30,"props":2380,"children":2381},{},[2382],{"type":27,"value":2383},"Each micro-frontend:",{"type":21,"tag":94,"props":2385,"children":2386},{},[2387,2392,2397,2402],{"type":21,"tag":98,"props":2388,"children":2389},{},[2390],{"type":27,"value":2391},"Has its own git repo",{"type":21,"tag":98,"props":2393,"children":2394},{},[2395],{"type":27,"value":2396},"Deploys independently",{"type":21,"tag":98,"props":2398,"children":2399},{},[2400],{"type":27,"value":2401},"Ships its own bundle",{"type":21,"tag":98,"props":2403,"children":2404},{},[2405],{"type":27,"value":2406},"Uses shared design tokens + api client",{"type":21,"tag":64,"props":2408,"children":2410},{"id":2409},"micro-frontend-patterns",[2411],{"type":27,"value":2412},"Micro-Frontend Patterns",{"type":21,"tag":799,"props":2414,"children":2416},{"id":2415},"_1-module-federation-webpack-5-vite",[2417],{"type":27,"value":2418},"1. Module Federation (Webpack 5 \u002F Vite)",{"type":21,"tag":76,"props":2420,"children":2423},{"code":2421,"language":144,"meta":7,"className":2422},"\u002F\u002F host\u002Fvite.config.ts\nimport federation from '@originjs\u002Fvite-plugin-federation'\n\nexport default defineConfig({\n  plugins: [\n    federation({\n      name: 'host',\n      remotes: {\n        dashboard: 'http:\u002F\u002Flocalhost:5001\u002Fassets\u002FremoteEntry.js',\n        billing: 'http:\u002F\u002Flocalhost:5002\u002Fassets\u002FremoteEntry.js',\n        admin: 'http:\u002F\u002Flocalhost:5003\u002Fassets\u002FremoteEntry.js',\n      },\n      shared: ['vue', 'pinia', '@acme\u002Fapi-client']\n    })\n  ]\n})\n\n\u002F\u002F remote\u002Fvite.config.ts\nexport default defineConfig({\n  plugins: [\n    federation({\n      name: 'dashboard',\n      filename: 'remoteEntry.js',\n      exposes: {\n        '.\u002FApp': '.\u002Fsrc\u002FApp.vue',\n        '.\u002Fstore': '.\u002Fsrc\u002Fstore\u002Findex.ts'\n      },\n      shared: ['vue', 'pinia', '@acme\u002Fapi-client']\n    })\n  ]\n})\n",[142],[2424],{"type":21,"tag":84,"props":2425,"children":2426},{"__ignoreMap":7},[2427],{"type":27,"value":2421},{"type":21,"tag":799,"props":2429,"children":2431},{"id":2430},"_2-url-based-routing",[2432],{"type":27,"value":2433},"2. URL-based Routing",{"type":21,"tag":76,"props":2435,"children":2438},{"code":2436,"language":194,"meta":7,"className":2437},"\u003C!-- host\u002FApp.vue -->\n\u003Cscript setup>\nconst microFrontends = {\n  dashboard: { url: 'http:\u002F\u002Fdashboard.micro.local', path: '\u002Fdashboard' },\n  billing: { url: 'http:\u002F\u002Fbilling.micro.local', path: '\u002Fbilling' },\n  admin: { url: 'http:\u002F\u002Fadmin.micro.local', path: '\u002Fadmin' }\n}\n\nconst loadMicroFrontend = async (name) => {\n  const mfe = microFrontends[name]\n  const container = await import(mfe.url)\n  return container.App\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Crouter-view \u002F>\n\u003C\u002Ftemplate>\n",[192],[2439],{"type":21,"tag":84,"props":2440,"children":2441},{"__ignoreMap":7},[2442],{"type":27,"value":2436},{"type":21,"tag":30,"props":2444,"children":2445},{},[2446],{"type":27,"value":2447},"Each micro-frontend is a complete app that can:",{"type":21,"tag":94,"props":2449,"children":2450},{},[2451,2456,2461,2466],{"type":21,"tag":98,"props":2452,"children":2453},{},[2454],{"type":27,"value":2455},"Deploy independently",{"type":21,"tag":98,"props":2457,"children":2458},{},[2459],{"type":27,"value":2460},"Use different versions of dependencies (if needed)",{"type":21,"tag":98,"props":2462,"children":2463},{},[2464],{"type":27,"value":2465},"Be owned by a separate team",{"type":21,"tag":98,"props":2467,"children":2468},{},[2469],{"type":27,"value":2470},"Scale independently",{"type":21,"tag":22,"props":2472,"children":2474},{"id":2473},"our-monorepo-decision",[2475],{"type":27,"value":2476},"Our Monorepo Decision",{"type":21,"tag":30,"props":2478,"children":2479},{},[2480],{"type":27,"value":2481},"We chose monorepo because:",{"type":21,"tag":527,"props":2483,"children":2484},{},[2485,2495,2505,2515,2525],{"type":21,"tag":98,"props":2486,"children":2487},{},[2488,2493],{"type":21,"tag":36,"props":2489,"children":2490},{},[2491],{"type":27,"value":2492},"Team maturity",{"type":27,"value":2494}," — all teams understood git and npm workspaces",{"type":21,"tag":98,"props":2496,"children":2497},{},[2498,2503],{"type":21,"tag":36,"props":2499,"children":2500},{},[2501],{"type":27,"value":2502},"Shared design system",{"type":27,"value":2504}," — critical for consistent UX",{"type":21,"tag":98,"props":2506,"children":2507},{},[2508,2513],{"type":21,"tag":36,"props":2509,"children":2510},{},[2511],{"type":27,"value":2512},"Single deployment",{"type":27,"value":2514}," — easier for our ops team",{"type":21,"tag":98,"props":2516,"children":2517},{},[2518,2523],{"type":21,"tag":36,"props":2519,"children":2520},{},[2521],{"type":27,"value":2522},"Developer experience",{"type":27,"value":2524}," — IDE support better in monorepo",{"type":21,"tag":98,"props":2526,"children":2527},{},[2528,2533],{"type":21,"tag":36,"props":2529,"children":2530},{},[2531],{"type":27,"value":2532},"Build optimization",{"type":27,"value":2534}," — turborepo handles incremental builds well",{"type":21,"tag":22,"props":2536,"children":2538},{"id":2537},"implementation-turborepo",[2539],{"type":27,"value":2540},"Implementation: Turborepo",{"type":21,"tag":30,"props":2542,"children":2543},{},[2544],{"type":27,"value":2545},"We use Turborepo for build orchestration:",{"type":21,"tag":76,"props":2547,"children":2550},{"code":2548,"language":1962,"meta":7,"className":2549},"{\n  \"$schema\": \"https:\u002F\u002Fturbo.build\u002Fjson-schema.json\",\n  \"pipeline\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist\u002F**\"],\n      \"cache\": true\n    },\n    \"test\": {\n      \"dependsOn\": [\"build\"],\n      \"outputs\": [],\n      \"cache\": true\n    },\n    \"lint\": {\n      \"outputs\": []\n    },\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    }\n  },\n  \"globalEnv\": [\"NODE_ENV\", \"VERCEL_ENV\"],\n  \"globalDependencies\": [\"tsconfig.json\", \".eslintrc.js\"]\n}\n",[1964],[2551],{"type":21,"tag":84,"props":2552,"children":2553},{"__ignoreMap":7},[2554],{"type":27,"value":2548},{"type":21,"tag":30,"props":2556,"children":2557},{},[2558],{"type":27,"value":1695},{"type":21,"tag":94,"props":2560,"children":2561},{},[2562,2572,2582,2592],{"type":21,"tag":98,"props":2563,"children":2564},{},[2565,2570],{"type":21,"tag":36,"props":2566,"children":2567},{},[2568],{"type":27,"value":2569},"Parallel execution",{"type":27,"value":2571}," — builds run in parallel when possible",{"type":21,"tag":98,"props":2573,"children":2574},{},[2575,2580],{"type":21,"tag":36,"props":2576,"children":2577},{},[2578],{"type":27,"value":2579},"Task pipelines",{"type":27,"value":2581}," — test only runs after build",{"type":21,"tag":98,"props":2583,"children":2584},{},[2585,2590],{"type":21,"tag":36,"props":2586,"children":2587},{},[2588],{"type":27,"value":2589},"Caching",{"type":27,"value":2591}," — skips tasks that haven't changed",{"type":21,"tag":98,"props":2593,"children":2594},{},[2595,2600],{"type":21,"tag":36,"props":2596,"children":2597},{},[2598],{"type":27,"value":2599},"CI\u002FCD integration",{"type":27,"value":2601}," — auto-detects changed packages",{"type":21,"tag":22,"props":2603,"children":2605},{"id":2604},"state-management-at-scale",[2606],{"type":27,"value":2607},"State Management at Scale",{"type":21,"tag":30,"props":2609,"children":2610},{},[2611],{"type":27,"value":2612},"Pinia (Vue's official state management) handles monorepo scales well:",{"type":21,"tag":76,"props":2614,"children":2617},{"code":2615,"language":144,"meta":7,"className":2616},"\u002F\u002F packages\u002Fshared-state\u002Fsrc\u002Fstores\u002Fauth.ts\nimport { defineStore } from 'pinia'\n\nexport const useAuthStore = defineStore('auth', () => {\n  const user = ref(null)\n  const isAuthenticated = computed(() => !!user.value)\n  \n  const login = async (credentials) => {\n    const response = await api.login(credentials)\n    user.value = response.user\n    localStorage.setItem('token', response.token)\n  }\n  \n  const logout = () => {\n    user.value = null\n    localStorage.removeItem('token')\n  }\n  \n  return { user, isAuthenticated, login, logout }\n})\n\n\u002F\u002F Usage in any app\nimport { useAuthStore } from '@acme\u002Fshared-state'\n\nconst authStore = useAuthStore()\nauthStore.login(credentials)\n",[142],[2618],{"type":21,"tag":84,"props":2619,"children":2620},{"__ignoreMap":7},[2621],{"type":27,"value":2615},{"type":21,"tag":22,"props":2623,"children":2625},{"id":2624},"preventing-common-pitfalls",[2626],{"type":27,"value":2627},"Preventing Common Pitfalls",{"type":21,"tag":64,"props":2629,"children":2631},{"id":2630},"_1-circular-dependencies",[2632],{"type":27,"value":2633},"1. Circular Dependencies",{"type":21,"tag":76,"props":2635,"children":2637},{"code":2636},"Bad: dashboard → api-client → shared-utils → dashboard\n",[2638],{"type":21,"tag":84,"props":2639,"children":2640},{"__ignoreMap":7},[2641],{"type":27,"value":2636},{"type":21,"tag":30,"props":2643,"children":2644},{},[2645],{"type":27,"value":2646},"Solution: Enforce dependency graph with eslint:",{"type":21,"tag":76,"props":2648,"children":2651},{"code":2649,"language":898,"meta":7,"className":2650},"\u002F\u002F .eslintrc.js\n{\n  \"rules\": {\n    \"import\u002Fno-cycle\": \"error\"\n  }\n}\n",[896],[2652],{"type":21,"tag":84,"props":2653,"children":2654},{"__ignoreMap":7},[2655],{"type":27,"value":2649},{"type":21,"tag":64,"props":2657,"children":2659},{"id":2658},"_2-shared-dependency-versions",[2660],{"type":27,"value":2661},"2. Shared Dependency Versions",{"type":21,"tag":76,"props":2663,"children":2666},{"code":2664,"language":144,"meta":7,"className":2665},"\u002F\u002F Root tsconfig.json\n{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"resolvePackageJsonExports\": true\n  },\n  \"extends\": \".\u002Ftsconfig.base.json\"\n}\n\n\u002F\u002F pnpm-workspace.yaml\npackages:\n  - 'packages\u002F*'\n  - 'apps\u002F*'\n\n# Lockfile ensures all packages use same versions\n",[142],[2667],{"type":21,"tag":84,"props":2668,"children":2669},{"__ignoreMap":7},[2670],{"type":27,"value":2664},{"type":21,"tag":64,"props":2672,"children":2674},{"id":2673},"_3-code-ownership",[2675],{"type":27,"value":2676},"3. Code Ownership",{"type":21,"tag":30,"props":2678,"children":2679},{},[2680],{"type":27,"value":2681},"Create a CODEOWNERS file:",{"type":21,"tag":76,"props":2683,"children":2685},{"code":2684},"# CODEOWNERS\npackages\u002Fdesign-system\u002F  @design-team\npackages\u002Fapi-client\u002F     @backend-team, @frontend-team\napps\u002Fdashboard\u002F          @dashboard-team\napps\u002Fadmin\u002F              @admin-team\n",[2686],{"type":21,"tag":84,"props":2687,"children":2688},{"__ignoreMap":7},[2689],{"type":27,"value":2684},{"type":21,"tag":22,"props":2691,"children":2693},{"id":2692},"performance-impact",[2694],{"type":27,"value":2695},"Performance Impact",{"type":21,"tag":30,"props":2697,"children":2698},{},[2699],{"type":27,"value":2700},"Monorepo with proper caching:",{"type":21,"tag":76,"props":2702,"children":2704},{"code":2703},"Before: npm run build (all apps)\nReal:  45 seconds\nUser:  78 seconds (with npm overhead)\n\nAfter: turbo build (only changed apps)\nReal:  12 seconds (cached)\nUser:  3 seconds (with caching)\n",[2705],{"type":21,"tag":84,"props":2706,"children":2707},{"__ignoreMap":7},[2708],{"type":27,"value":2703},{"type":21,"tag":30,"props":2710,"children":2711},{},[2712],{"type":27,"value":2713},"Cached builds like this reclaim real hours of CI time every week — measure your own before\u002Fafter; the delta is usually the easiest infrastructure win of the quarter.",{"type":21,"tag":22,"props":2715,"children":2717},{"id":2716},"team-autonomy-in-monorepo",[2718],{"type":27,"value":2719},"Team Autonomy in Monorepo",{"type":21,"tag":30,"props":2721,"children":2722},{},[2723],{"type":27,"value":2724},"To prevent chaos:",{"type":21,"tag":527,"props":2726,"children":2727},{},[2728,2738,2748,2766],{"type":21,"tag":98,"props":2729,"children":2730},{},[2731,2736],{"type":21,"tag":36,"props":2732,"children":2733},{},[2734],{"type":27,"value":2735},"Clear package boundaries",{"type":27,"value":2737}," — dashboard team doesn't touch billing code",{"type":21,"tag":98,"props":2739,"children":2740},{},[2741,2746],{"type":21,"tag":36,"props":2742,"children":2743},{},[2744],{"type":27,"value":2745},"Owned packages",{"type":27,"value":2747}," — each package has a code owner",{"type":21,"tag":98,"props":2749,"children":2750},{},[2751,2756,2758,2764],{"type":21,"tag":36,"props":2752,"children":2753},{},[2754],{"type":27,"value":2755},"Version contracts",{"type":27,"value":2757}," — ",{"type":21,"tag":84,"props":2759,"children":2761},{"className":2760},[],[2762],{"type":27,"value":2763},"@acme\u002Fapi-client",{"type":27,"value":2765}," v2.0.0 is stable, don't break it",{"type":21,"tag":98,"props":2767,"children":2768},{},[2769,2774],{"type":21,"tag":36,"props":2770,"children":2771},{},[2772],{"type":27,"value":2773},"Shared conventions",{"type":27,"value":2775}," — naming, folder structure, testing patterns",{"type":21,"tag":22,"props":2777,"children":2779},{"id":2778},"when-monorepo-isnt-enough",[2780],{"type":27,"value":2781},"When Monorepo Isn't Enough",{"type":21,"tag":30,"props":2783,"children":2784},{},[2785],{"type":27,"value":2786},"For teams that need complete independence, consider micro-frontends:",{"type":21,"tag":30,"props":2788,"children":2789},{},[2790,2795,2797,2802,2804,2809],{"type":21,"tag":36,"props":2791,"children":2792},{},[2793],{"type":27,"value":2794},"Monorepo",{"type":27,"value":2796}," → Single deployment, shared everything\n",{"type":21,"tag":36,"props":2798,"children":2799},{},[2800],{"type":27,"value":2801},"Micro-Frontends",{"type":27,"value":2803}," → Independent deployments, isolated teams\n",{"type":21,"tag":36,"props":2805,"children":2806},{},[2807],{"type":27,"value":2808},"Hybrid",{"type":27,"value":2810}," → Monorepo for shared packages, micro-frontends for apps",{"type":21,"tag":30,"props":2812,"children":2813},{},[2814],{"type":27,"value":2815},"We started with monorepo. If we grew to 200 engineers across 30 teams, we'd likely move to micro-frontends with Module Federation.",{"type":21,"tag":22,"props":2817,"children":2819},{"id":2818},"final-metrics",[2820],{"type":27,"value":2821},"Final Metrics",{"type":21,"tag":30,"props":2823,"children":2824},{},[2825],{"type":27,"value":2826},"The movement a monorepo migration typically produces (illustrative example — not measured claims from one project):",{"type":21,"tag":403,"props":2828,"children":2829},{},[2830,2845],{"type":21,"tag":407,"props":2831,"children":2832},{},[2833],{"type":21,"tag":411,"props":2834,"children":2835},{},[2836,2840],{"type":21,"tag":415,"props":2837,"children":2838},{},[2839],{"type":27,"value":419},{"type":21,"tag":415,"props":2841,"children":2842},{},[2843],{"type":27,"value":2844},"Direction",{"type":21,"tag":436,"props":2846,"children":2847},{},[2848,2861,2874,2886,2899,2912],{"type":21,"tag":411,"props":2849,"children":2850},{},[2851,2856],{"type":21,"tag":443,"props":2852,"children":2853},{},[2854],{"type":27,"value":2855},"Build time",{"type":21,"tag":443,"props":2857,"children":2858},{},[2859],{"type":27,"value":2860},"Sharply down with caching",{"type":21,"tag":411,"props":2862,"children":2863},{},[2864,2869],{"type":21,"tag":443,"props":2865,"children":2866},{},[2867],{"type":27,"value":2868},"CI\u002FCD time",{"type":21,"tag":443,"props":2870,"children":2871},{},[2872],{"type":27,"value":2873},"Down, especially on unchanged packages",{"type":21,"tag":411,"props":2875,"children":2876},{},[2877,2881],{"type":21,"tag":443,"props":2878,"children":2879},{},[2880],{"type":27,"value":2180},{"type":21,"tag":443,"props":2882,"children":2883},{},[2884],{"type":27,"value":2885},"Down — one repo, one integration point",{"type":21,"tag":411,"props":2887,"children":2888},{},[2889,2894],{"type":21,"tag":443,"props":2890,"children":2891},{},[2892],{"type":27,"value":2893},"Deployment failures",{"type":21,"tag":443,"props":2895,"children":2896},{},[2897],{"type":27,"value":2898},"Down — shared tooling, fewer bespoke pipelines",{"type":21,"tag":411,"props":2900,"children":2901},{},[2902,2907],{"type":21,"tag":443,"props":2903,"children":2904},{},[2905],{"type":27,"value":2906},"Code duplication",{"type":21,"tag":443,"props":2908,"children":2909},{},[2910],{"type":27,"value":2911},"Down — importing beats re-implementing",{"type":21,"tag":411,"props":2913,"children":2914},{},[2915,2920],{"type":21,"tag":443,"props":2916,"children":2917},{},[2918],{"type":27,"value":2919},"Onboarding time",{"type":21,"tag":443,"props":2921,"children":2922},{},[2923],{"type":27,"value":2924},"Down — one setup, one set of conventions",{"type":21,"tag":30,"props":2926,"children":2927},{},[2928],{"type":27,"value":2929},"The architecture change pays off through clear ownership, faster development, and fewer bugs — if you enforce the boundaries.",{"title":7,"searchDepth":586,"depth":586,"links":2931},[2932,2933,2937,2940,2941,2942,2943,2948,2949,2950,2951],{"id":2161,"depth":586,"text":2164},{"id":2230,"depth":586,"text":2233,"children":2934},[2935,2936],{"id":2249,"depth":592,"text":2252},{"id":2308,"depth":592,"text":2311},{"id":2362,"depth":586,"text":2365,"children":2938},[2939],{"id":2409,"depth":592,"text":2412},{"id":2473,"depth":586,"text":2476},{"id":2537,"depth":586,"text":2540},{"id":2604,"depth":586,"text":2607},{"id":2624,"depth":586,"text":2627,"children":2944},[2945,2946,2947],{"id":2630,"depth":592,"text":2633},{"id":2658,"depth":592,"text":2661},{"id":2673,"depth":592,"text":2676},{"id":2692,"depth":586,"text":2695},{"id":2716,"depth":586,"text":2719},{"id":2778,"depth":586,"text":2781},{"id":2818,"depth":586,"text":2821},"content:blog:frontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas",{"_path":2956,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2957,"description":2958,"date":2959,"readingTime":2960,"tags":2961,"featured":6,"body":2962,"_type":602,"_id":3623,"_source":604,"_file":3624,"_stem":3625,"_extension":607},"\u002Fblog\u002Fcore-web-vitals-nuxt-guide","Core Web Vitals in Nuxt: Mastering LCP, FID, and CLS","Complete guide to measuring and optimizing Core Web Vitals in Nuxt applications. Learn how to achieve green scores on Google PageSpeed Insights with real strategies.","2024-06-28",11,[15,14],{"type":18,"children":2963,"toc":3597},[2964,2970,2975,3052,3058,3064,3072,3077,3083,3088,3097,3102,3111,3116,3125,3131,3136,3142,3147,3156,3161,3179,3185,3194,3200,3205,3214,3220,3225,3234,3240,3245,3251,3260,3266,3275,3281,3290,3296,3301,3349,3355,3364,3370,3379,3385,3394,3400,3405,3501,3507,3512,3521,3526,3535,3539,3592],{"type":21,"tag":22,"props":2965,"children":2967},{"id":2966},"understanding-core-web-vitals",[2968],{"type":27,"value":2969},"Understanding Core Web Vitals",{"type":21,"tag":30,"props":2971,"children":2972},{},[2973],{"type":27,"value":2974},"Google's Core Web Vitals measure three aspects of user experience:",{"type":21,"tag":527,"props":2976,"children":2977},{},[2978,3001,3029],{"type":21,"tag":98,"props":2979,"children":2980},{},[2981,2986,2988],{"type":21,"tag":36,"props":2982,"children":2983},{},[2984],{"type":27,"value":2985},"LCP (Largest Contentful Paint)",{"type":27,"value":2987}," — How fast does content appear?",{"type":21,"tag":94,"props":2989,"children":2990},{},[2991,2996],{"type":21,"tag":98,"props":2992,"children":2993},{},[2994],{"type":27,"value":2995},"Target: \u003C 2.5s",{"type":21,"tag":98,"props":2997,"children":2998},{},[2999],{"type":27,"value":3000},"Measures: When largest element (image, heading, video) becomes visible",{"type":21,"tag":98,"props":3002,"children":3003},{},[3004,3009,3011],{"type":21,"tag":36,"props":3005,"children":3006},{},[3007],{"type":27,"value":3008},"FID (First Input Delay)",{"type":27,"value":3010}," — How responsive is the page?",{"type":21,"tag":94,"props":3012,"children":3013},{},[3014,3019,3024],{"type":21,"tag":98,"props":3015,"children":3016},{},[3017],{"type":27,"value":3018},"Target: \u003C 100ms",{"type":21,"tag":98,"props":3020,"children":3021},{},[3022],{"type":27,"value":3023},"Measures: Delay from user input to browser processing",{"type":21,"tag":98,"props":3025,"children":3026},{},[3027],{"type":27,"value":3028},"Being replaced by INP in 2024",{"type":21,"tag":98,"props":3030,"children":3031},{},[3032,3037,3039],{"type":21,"tag":36,"props":3033,"children":3034},{},[3035],{"type":27,"value":3036},"CLS (Cumulative Layout Shift)",{"type":27,"value":3038}," — How stable is the layout?",{"type":21,"tag":94,"props":3040,"children":3041},{},[3042,3047],{"type":21,"tag":98,"props":3043,"children":3044},{},[3045],{"type":27,"value":3046},"Target: \u003C 0.1",{"type":21,"tag":98,"props":3048,"children":3049},{},[3050],{"type":27,"value":3051},"Measures: Unexpected visual shifts during page load",{"type":21,"tag":22,"props":3053,"children":3055},{"id":3054},"measuring-web-vitals",[3056],{"type":27,"value":3057},"Measuring Web Vitals",{"type":21,"tag":64,"props":3059,"children":3061},{"id":3060},"_1-google-pagespeed-insights",[3062],{"type":27,"value":3063},"1. Google PageSpeed Insights",{"type":21,"tag":76,"props":3065,"children":3067},{"code":3066},"https:\u002F\u002Fpagespeed.web.dev\u002F?url=yoursite.com\n",[3068],{"type":21,"tag":84,"props":3069,"children":3070},{"__ignoreMap":7},[3071],{"type":27,"value":3066},{"type":21,"tag":30,"props":3073,"children":3074},{},[3075],{"type":27,"value":3076},"Shows real-world data from actual users. This is what matters most.",{"type":21,"tag":64,"props":3078,"children":3080},{"id":3079},"_2-local-testing",[3081],{"type":27,"value":3082},"2. Local Testing",{"type":21,"tag":30,"props":3084,"children":3085},{},[3086],{"type":27,"value":3087},"Install web-vitals library:",{"type":21,"tag":76,"props":3089,"children":3092},{"code":3090,"language":81,"meta":7,"className":3091},"npm install web-vitals\n",[79],[3093],{"type":21,"tag":84,"props":3094,"children":3095},{"__ignoreMap":7},[3096],{"type":27,"value":3090},{"type":21,"tag":30,"props":3098,"children":3099},{},[3100],{"type":27,"value":3101},"Track metrics in Nuxt:",{"type":21,"tag":76,"props":3103,"children":3106},{"code":3104,"language":144,"meta":7,"className":3105},"\u002F\u002F plugins\u002Fweb-vitals.ts\nimport { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals'\n\nexport default defineNuxtPlugin(() => {\n  getCLS(console.log)\n  getFCP(console.log)\n  getFID(console.log)\n  getLCP(console.log)\n  getTTFB(console.log)\n})\n",[142],[3107],{"type":21,"tag":84,"props":3108,"children":3109},{"__ignoreMap":7},[3110],{"type":27,"value":3104},{"type":21,"tag":30,"props":3112,"children":3113},{},[3114],{"type":27,"value":3115},"Send to analytics:",{"type":21,"tag":76,"props":3117,"children":3120},{"code":3118,"language":144,"meta":7,"className":3119},"import { getCLS, getLCP, getFID } from 'web-vitals'\n\nconst vitals = {\n  cls: null,\n  lcp: null,\n  fid: null\n}\n\ngetCLS((metric) => {\n  vitals.cls = metric.value\n  sendToAnalytics('CLS', metric.value)\n})\n\ngetLCP((metric) => {\n  vitals.lcp = metric.value\n  sendToAnalytics('LCP', metric.value)\n})\n\ngetFID((metric) => {\n  vitals.fid = metric.value\n  sendToAnalytics('FID', metric.value)\n})\n",[142],[3121],{"type":21,"tag":84,"props":3122,"children":3123},{"__ignoreMap":7},[3124],{"type":27,"value":3118},{"type":21,"tag":22,"props":3126,"children":3128},{"id":3127},"optimizing-lcp",[3129],{"type":27,"value":3130},"Optimizing LCP",{"type":21,"tag":30,"props":3132,"children":3133},{},[3134],{"type":27,"value":3135},"LCP is usually the main blocker. Focus here first.",{"type":21,"tag":64,"props":3137,"children":3139},{"id":3138},"strategy-1-optimize-largest-element",[3140],{"type":27,"value":3141},"Strategy 1: Optimize Largest Element",{"type":21,"tag":30,"props":3143,"children":3144},{},[3145],{"type":27,"value":3146},"Identify what's causing slow LCP:",{"type":21,"tag":76,"props":3148,"children":3151},{"code":3149,"language":144,"meta":7,"className":3150},"\u002F\u002F Measure element rendering\nconst observer = new PerformanceObserver((entryList) => {\n  for (const entry of entryList.getEntries()) {\n    if (entry.entryType === 'largest-contentful-paint') {\n      console.log('LCP Element:', entry.element)\n      console.log('LCP Size:', entry.size)\n      console.log('LCP Time:', entry.renderTime || entry.loadTime)\n    }\n  }\n})\n\nobserver.observe({ entryTypes: ['largest-contentful-paint'] })\n",[142],[3152],{"type":21,"tag":84,"props":3153,"children":3154},{"__ignoreMap":7},[3155],{"type":27,"value":3149},{"type":21,"tag":30,"props":3157,"children":3158},{},[3159],{"type":27,"value":3160},"Usually the largest element is:",{"type":21,"tag":94,"props":3162,"children":3163},{},[3164,3169,3174],{"type":21,"tag":98,"props":3165,"children":3166},{},[3167],{"type":27,"value":3168},"Hero image",{"type":21,"tag":98,"props":3170,"children":3171},{},[3172],{"type":27,"value":3173},"Main heading",{"type":21,"tag":98,"props":3175,"children":3176},{},[3177],{"type":27,"value":3178},"Large text block",{"type":21,"tag":64,"props":3180,"children":3182},{"id":3181},"strategy-2-preload-critical-resources",[3183],{"type":27,"value":3184},"Strategy 2: Preload Critical Resources",{"type":21,"tag":76,"props":3186,"children":3189},{"code":3187,"language":144,"meta":7,"className":3188},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        \u002F\u002F Preload hero image\n        {\n          rel: 'preload',\n          as: 'image',\n          href: '\u002Fhero.webp',\n          imagesrcset: '\u002Fhero-mobile.webp 600w, \u002Fhero-desktop.webp 1200w',\n          imagesizes: '(max-width: 600px) 600px, 1200px'\n        },\n        \u002F\u002F Preload critical fonts\n        {\n          rel: 'preload',\n          as: 'font',\n          href: '\u002Ffonts\u002Finter.woff2',\n          type: 'font\u002Fwoff2',\n          crossorigin: ''\n        },\n        \u002F\u002F DNS prefetch for external APIs\n        {\n          rel: 'dns-prefetch',\n          href: 'https:\u002F\u002Fapi.example.com'\n        },\n        \u002F\u002F Preconnect to critical domains\n        {\n          rel: 'preconnect',\n          href: 'https:\u002F\u002Fapi.example.com'\n        }\n      ]\n    }\n  }\n})\n",[142],[3190],{"type":21,"tag":84,"props":3191,"children":3192},{"__ignoreMap":7},[3193],{"type":27,"value":3187},{"type":21,"tag":64,"props":3195,"children":3197},{"id":3196},"strategy-3-server-side-rendering",[3198],{"type":27,"value":3199},"Strategy 3: Server-Side Rendering",{"type":21,"tag":30,"props":3201,"children":3202},{},[3203],{"type":27,"value":3204},"SSR ensures content is in HTML, not dependent on JavaScript:",{"type":21,"tag":76,"props":3206,"children":3209},{"code":3207,"language":144,"meta":7,"className":3208},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    \u002F\u002F Cache static pages at build time\n    '\u002F**': { cache: { maxAge: 60 * 10 } },\n    '\u002Fapi\u002F**': { noCache: true },\n    '\u002Fadmin\u002F**': { ssr: false } \u002F\u002F Client-side only for admin\n  }\n})\n",[142],[3210],{"type":21,"tag":84,"props":3211,"children":3212},{"__ignoreMap":7},[3213],{"type":27,"value":3207},{"type":21,"tag":64,"props":3215,"children":3217},{"id":3216},"strategy-4-image-optimization",[3218],{"type":27,"value":3219},"Strategy 4: Image Optimization",{"type":21,"tag":30,"props":3221,"children":3222},{},[3223],{"type":27,"value":3224},"Hero images often block LCP. Optimize aggressively:",{"type":21,"tag":76,"props":3226,"children":3229},{"code":3227,"language":194,"meta":7,"className":3228},"\u003Cscript setup>\nimport { useImage } from '#app'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"hero\">\n    \u003C!-- Use NuxtImg for automatic optimization -->\n    \u003CNuxtImg\n      src=\"\u002Fhero.jpg\"\n      alt=\"Hero\"\n      sizes=\"xs:100vw md:800px\"\n      quality=\"80\"\n      format=\"webp,jpg\"\n      loading=\"eager\"\n      fetchpriority=\"high\"\n    \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.hero {\n  \u002F* Prevent layout shift while image loads -->\n  aspect-ratio: 16 \u002F 9;\n}\n\u003C\u002Fstyle>\n",[192],[3230],{"type":21,"tag":84,"props":3231,"children":3232},{"__ignoreMap":7},[3233],{"type":27,"value":3227},{"type":21,"tag":22,"props":3235,"children":3237},{"id":3236},"optimizing-fid-and-inp",[3238],{"type":27,"value":3239},"Optimizing FID (and INP)",{"type":21,"tag":30,"props":3241,"children":3242},{},[3243],{"type":27,"value":3244},"FID measures JavaScript execution blocking input. Reduce with:",{"type":21,"tag":64,"props":3246,"children":3248},{"id":3247},"_1-code-splitting",[3249],{"type":27,"value":3250},"1. Code Splitting",{"type":21,"tag":76,"props":3252,"children":3255},{"code":3253,"language":144,"meta":7,"className":3254},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: {\n          'vendor': ['lodash-es', 'date-fns'],\n          'charts': ['chart.js']\n        }\n      }\n    }\n  }\n})\n",[142],[3256],{"type":21,"tag":84,"props":3257,"children":3258},{"__ignoreMap":7},[3259],{"type":27,"value":3253},{"type":21,"tag":64,"props":3261,"children":3263},{"id":3262},"_2-web-workers-for-heavy-tasks",[3264],{"type":27,"value":3265},"2. Web Workers for Heavy Tasks",{"type":21,"tag":76,"props":3267,"children":3270},{"code":3268,"language":144,"meta":7,"className":3269},"\u002F\u002F composables\u002FuseExpensiveCalculation.ts\nexport const useExpensiveCalculation = () => {\n  const result = ref(null)\n  const processing = ref(false)\n\n  const calculate = async (data: any) => {\n    processing.value = true\n    \n    \u002F\u002F Offload to worker\n    const worker = new Worker(new URL('@\u002Fworkers\u002Fcalculation.ts', import.meta.url), {\n      type: 'module'\n    })\n    \n    worker.postMessage(data)\n    \n    await new Promise\u003Cvoid>((resolve) => {\n      worker.onmessage = (e) => {\n        result.value = e.data\n        processing.value = false\n        worker.terminate()\n        resolve()\n      }\n    })\n  }\n\n  return { result, processing, calculate }\n}\n",[142],[3271],{"type":21,"tag":84,"props":3272,"children":3273},{"__ignoreMap":7},[3274],{"type":27,"value":3268},{"type":21,"tag":64,"props":3276,"children":3278},{"id":3277},"_3-defer-non-critical-javascript",[3279],{"type":27,"value":3280},"3. Defer Non-Critical JavaScript",{"type":21,"tag":76,"props":3282,"children":3285},{"code":3283,"language":194,"meta":7,"className":3284},"\u003Cscript setup>\n\u002F\u002F Track analytics only after page is interactive\nonMounted(() => {\n  if (window.requestIdleCallback) {\n    requestIdleCallback(() => {\n      \u002F\u002F Load analytics after browser is idle\n      window.gtag?.('event', 'page_view')\n    })\n  } else {\n    setTimeout(() => {\n      window.gtag?.('event', 'page_view')\n    }, 2000)\n  }\n})\n\u003C\u002Fscript>\n",[192],[3286],{"type":21,"tag":84,"props":3287,"children":3288},{"__ignoreMap":7},[3289],{"type":27,"value":3283},{"type":21,"tag":22,"props":3291,"children":3293},{"id":3292},"optimizing-cls",[3294],{"type":27,"value":3295},"Optimizing CLS",{"type":21,"tag":30,"props":3297,"children":3298},{},[3299],{"type":27,"value":3300},"CLS is layout instability. Causes:",{"type":21,"tag":527,"props":3302,"children":3303},{},[3304,3314,3324,3339],{"type":21,"tag":98,"props":3305,"children":3306},{},[3307,3312],{"type":21,"tag":36,"props":3308,"children":3309},{},[3310],{"type":27,"value":3311},"Images without dimensions",{"type":27,"value":3313}," — add width\u002Fheight",{"type":21,"tag":98,"props":3315,"children":3316},{},[3317,3322],{"type":21,"tag":36,"props":3318,"children":3319},{},[3320],{"type":27,"value":3321},"Ads loading late",{"type":27,"value":3323}," — reserve space",{"type":21,"tag":98,"props":3325,"children":3326},{},[3327,3332,3334],{"type":21,"tag":36,"props":3328,"children":3329},{},[3330],{"type":27,"value":3331},"Fonts loading",{"type":27,"value":3333}," — use ",{"type":21,"tag":84,"props":3335,"children":3337},{"className":3336},[],[3338],{"type":27,"value":300},{"type":21,"tag":98,"props":3340,"children":3341},{},[3342,3347],{"type":21,"tag":36,"props":3343,"children":3344},{},[3345],{"type":27,"value":3346},"Dynamic content",{"type":27,"value":3348}," — skeleton screens",{"type":21,"tag":64,"props":3350,"children":3352},{"id":3351},"fix-1-image-dimensions",[3353],{"type":27,"value":3354},"Fix 1: Image Dimensions",{"type":21,"tag":76,"props":3356,"children":3359},{"code":3357,"language":194,"meta":7,"className":3358},"\u003C!-- ❌ Causes CLS -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\n\u003C!-- ✅ No CLS -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" width=\"800\" height=\"600\" \u002F>\n\n\u003C!-- Or use aspect-ratio -->\n\u003Cdiv class=\"image-container\">\n  \u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\u003C\u002Fdiv>\n\n\u003Cstyle>\n.image-container {\n  aspect-ratio: 800 \u002F 600;\n}\n\u003C\u002Fstyle>\n",[192],[3360],{"type":21,"tag":84,"props":3361,"children":3362},{"__ignoreMap":7},[3363],{"type":27,"value":3357},{"type":21,"tag":64,"props":3365,"children":3367},{"id":3366},"fix-2-font-loading",[3368],{"type":27,"value":3369},"Fix 2: Font Loading",{"type":21,"tag":76,"props":3371,"children":3374},{"code":3372,"language":144,"meta":7,"className":3373},"\u002F\u002F Use font-display: swap to show text immediately\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        {\n          rel: 'stylesheet',\n          href: 'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap'\n        }\n      ]\n    }\n  }\n})\n",[142],[3375],{"type":21,"tag":84,"props":3376,"children":3377},{"__ignoreMap":7},[3378],{"type":27,"value":3372},{"type":21,"tag":64,"props":3380,"children":3382},{"id":3381},"fix-3-skeleton-screens",[3383],{"type":27,"value":3384},"Fix 3: Skeleton Screens",{"type":21,"tag":76,"props":3386,"children":3389},{"code":3387,"language":194,"meta":7,"className":3388},"\u003Cscript setup>\nconst post = ref(null)\nconst loading = ref(true)\n\nonMounted(async () => {\n  post.value = await fetchPost()\n  loading.value = false\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle v-if=\"loading\" class=\"post-skeleton\">\n    \u003Cdiv class=\"skeleton-title\" \u002F>\n    \u003Cdiv class=\"skeleton-text\" \u002F>\n    \u003Cdiv class=\"skeleton-text\" \u002F>\n  \u003C\u002Farticle>\n  \n  \u003Carticle v-else>\n    \u003Ch1>{{ post.title }}\u003C\u002Fh1>\n    \u003Cp>{{ post.content }}\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.skeleton-title {\n  width: 80%;\n  height: 2rem;\n  background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);\n  background-size: 200% 100%;\n  animation: loading 1.5s infinite;\n  border-radius: 0.25rem;\n  margin-bottom: 1rem;\n}\n\n@keyframes loading {\n  0% { background-position: 200% 0; }\n  100% { background-position: -200% 0; }\n}\n\u003C\u002Fstyle>\n",[192],[3390],{"type":21,"tag":84,"props":3391,"children":3392},{"__ignoreMap":7},[3393],{"type":27,"value":3387},{"type":21,"tag":22,"props":3395,"children":3397},{"id":3396},"real-world-results",[3398],{"type":27,"value":3399},"Real-World Results",{"type":21,"tag":30,"props":3401,"children":3402},{},[3403],{"type":27,"value":3404},"After implementing these optimizations:",{"type":21,"tag":403,"props":3406,"children":3407},{},[3408,3426],{"type":21,"tag":407,"props":3409,"children":3410},{},[3411],{"type":21,"tag":411,"props":3412,"children":3413},{},[3414,3418,3422],{"type":21,"tag":415,"props":3415,"children":3416},{},[3417],{"type":27,"value":419},{"type":21,"tag":415,"props":3419,"children":3420},{},[3421],{"type":27,"value":424},{"type":21,"tag":415,"props":3423,"children":3424},{},[3425],{"type":27,"value":429},{"type":21,"tag":436,"props":3427,"children":3428},{},[3429,3447,3465,3483],{"type":21,"tag":411,"props":3430,"children":3431},{},[3432,3437,3442],{"type":21,"tag":443,"props":3433,"children":3434},{},[3435],{"type":27,"value":3436},"LCP",{"type":21,"tag":443,"props":3438,"children":3439},{},[3440],{"type":27,"value":3441},"3.8s",{"type":21,"tag":443,"props":3443,"children":3444},{},[3445],{"type":27,"value":3446},"1.6s",{"type":21,"tag":411,"props":3448,"children":3449},{},[3450,3455,3460],{"type":21,"tag":443,"props":3451,"children":3452},{},[3453],{"type":27,"value":3454},"FID",{"type":21,"tag":443,"props":3456,"children":3457},{},[3458],{"type":27,"value":3459},"180ms",{"type":21,"tag":443,"props":3461,"children":3462},{},[3463],{"type":27,"value":3464},"45ms",{"type":21,"tag":411,"props":3466,"children":3467},{},[3468,3473,3478],{"type":21,"tag":443,"props":3469,"children":3470},{},[3471],{"type":27,"value":3472},"CLS",{"type":21,"tag":443,"props":3474,"children":3475},{},[3476],{"type":27,"value":3477},"0.22",{"type":21,"tag":443,"props":3479,"children":3480},{},[3481],{"type":27,"value":3482},"0.04",{"type":21,"tag":411,"props":3484,"children":3485},{},[3486,3491,3496],{"type":21,"tag":443,"props":3487,"children":3488},{},[3489],{"type":27,"value":3490},"PageSpeed Score",{"type":21,"tag":443,"props":3492,"children":3493},{},[3494],{"type":27,"value":3495},"42",{"type":21,"tag":443,"props":3497,"children":3498},{},[3499],{"type":27,"value":3500},"92",{"type":21,"tag":22,"props":3502,"children":3504},{"id":3503},"continuous-monitoring",[3505],{"type":27,"value":3506},"Continuous Monitoring",{"type":21,"tag":30,"props":3508,"children":3509},{},[3510],{"type":27,"value":3511},"Set up alerts for regressions:",{"type":21,"tag":76,"props":3513,"children":3516},{"code":3514,"language":144,"meta":7,"className":3515},"\u002F\u002F Lighthouse CI\nnpm install -D @lhci\u002Fcli\n\n# lhciconfig.json\n{\n  \"ci\": {\n    \"collect\": {\n      \"url\": [\"https:\u002F\u002Fyoursite.com\"],\n      \"numberOfRuns\": 3\n    },\n    \"assert\": {\n      \"preset\": \"lighthouse:recommended\",\n      \"assertions\": {\n        \"largest-contentful-paint\": [\"error\", { \"maxNumericValue\": 2500 }],\n        \"first-input-delay\": [\"error\", { \"maxNumericValue\": 100 }],\n        \"cumulative-layout-shift\": [\"error\", { \"maxNumericValue\": 0.1 }]\n      }\n    }\n  }\n}\n",[142],[3517],{"type":21,"tag":84,"props":3518,"children":3519},{"__ignoreMap":7},[3520],{"type":27,"value":3514},{"type":21,"tag":30,"props":3522,"children":3523},{},[3524],{"type":27,"value":3525},"Run in CI:",{"type":21,"tag":76,"props":3527,"children":3530},{"code":3528,"language":81,"meta":7,"className":3529},"# .github\u002Fworkflows\u002Flighthouse.yml\n- name: Run Lighthouse CI\n  run: lhci autorun\n",[79],[3531],{"type":21,"tag":84,"props":3532,"children":3533},{"__ignoreMap":7},[3534],{"type":27,"value":3528},{"type":21,"tag":22,"props":3536,"children":3537},{"id":522},[3538],{"type":27,"value":525},{"type":21,"tag":527,"props":3540,"children":3541},{},[3542,3552,3562,3572,3582],{"type":21,"tag":98,"props":3543,"children":3544},{},[3545,3550],{"type":21,"tag":36,"props":3546,"children":3547},{},[3548],{"type":27,"value":3549},"Measure real user data",{"type":27,"value":3551}," — lab data differs from production",{"type":21,"tag":98,"props":3553,"children":3554},{},[3555,3560],{"type":21,"tag":36,"props":3556,"children":3557},{},[3558],{"type":27,"value":3559},"LCP is usually the bottleneck",{"type":27,"value":3561}," — focus optimization efforts there",{"type":21,"tag":98,"props":3563,"children":3564},{},[3565,3570],{"type":21,"tag":36,"props":3566,"children":3567},{},[3568],{"type":27,"value":3569},"Images matter most",{"type":27,"value":3571}," — proper optimization yields biggest gains",{"type":21,"tag":98,"props":3573,"children":3574},{},[3575,3580],{"type":21,"tag":36,"props":3576,"children":3577},{},[3578],{"type":27,"value":3579},"Preload critical resources",{"type":27,"value":3581}," — hero images and fonts",{"type":21,"tag":98,"props":3583,"children":3584},{},[3585,3590],{"type":21,"tag":36,"props":3586,"children":3587},{},[3588],{"type":27,"value":3589},"Monitor continuously",{"type":27,"value":3591}," — regressions happen. Catch them early",{"type":21,"tag":30,"props":3593,"children":3594},{},[3595],{"type":27,"value":3596},"Core Web Vitals are no longer just SEO metrics—they're user experience fundamentals. Invest in getting them right.",{"title":7,"searchDepth":586,"depth":586,"links":3598},[3599,3600,3604,3610,3615,3620,3621,3622],{"id":2966,"depth":586,"text":2969},{"id":3054,"depth":586,"text":3057,"children":3601},[3602,3603],{"id":3060,"depth":592,"text":3063},{"id":3079,"depth":592,"text":3082},{"id":3127,"depth":586,"text":3130,"children":3605},[3606,3607,3608,3609],{"id":3138,"depth":592,"text":3141},{"id":3181,"depth":592,"text":3184},{"id":3196,"depth":592,"text":3199},{"id":3216,"depth":592,"text":3219},{"id":3236,"depth":586,"text":3239,"children":3611},[3612,3613,3614],{"id":3247,"depth":592,"text":3250},{"id":3262,"depth":592,"text":3265},{"id":3277,"depth":592,"text":3280},{"id":3292,"depth":586,"text":3295,"children":3616},[3617,3618,3619],{"id":3351,"depth":592,"text":3354},{"id":3366,"depth":592,"text":3369},{"id":3381,"depth":592,"text":3384},{"id":3396,"depth":586,"text":3399},{"id":3503,"depth":586,"text":3506},{"id":522,"depth":586,"text":525},"content:blog:core-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide",{"_path":3627,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3628,"description":3629,"date":3630,"updated":11,"readingTime":614,"tags":3631,"featured":6,"body":3634,"_type":602,"_id":4054,"_source":604,"_file":4055,"_stem":4056,"_extension":607},"\u002Fblog\u002Fadvanced-frontend-ci-cd-workflows","Advanced Frontend CI\u002FCD Workflows: From Code to Production in 5 Minutes","Build automated CI\u002FCD pipelines for frontend apps. Learn GitHub Actions, parallel testing, performance monitoring, semantic versioning, and production deployments.","2024-06-10",[3632,3633],"CI\u002FCD","DevOps",{"type":18,"children":3635,"toc":4038},[3636,3642,3647,3652,3665,3670,3675,3728,3734,3742,3748,3759,3765,3771,3776,3785,3790,3796,3805,3818,3824,3833,3838,3844,3853,3858,3886,3891,3897,3906,3911,3917,3926,3932,3937,3948,3954,3959,3964,3969,3974,3980,4033],{"type":21,"tag":22,"props":3637,"children":3639},{"id":3638},"the-state-of-frontend-cicd",[3640],{"type":27,"value":3641},"The State of Frontend CI\u002FCD",{"type":21,"tag":30,"props":3643,"children":3644},{},[3645],{"type":27,"value":3646},"The \"5 minutes\" in the title is the target this guide builds toward — a design goal for the pipeline, not a production stat I'm claiming. My own verified result is more modest and stated at the end.",{"type":21,"tag":30,"props":3648,"children":3649},{},[3650],{"type":27,"value":3651},"Many teams still use basic CI pipelines:",{"type":21,"tag":527,"props":3653,"children":3654},{},[3655,3660],{"type":21,"tag":98,"props":3656,"children":3657},{},[3658],{"type":27,"value":3659},"Run tests",{"type":21,"tag":98,"props":3661,"children":3662},{},[3663],{"type":27,"value":3664},"If pass, deploy",{"type":21,"tag":30,"props":3666,"children":3667},{},[3668],{"type":27,"value":3669},"Result: slow feedback, missed bugs, brittle deployments.",{"type":21,"tag":30,"props":3671,"children":3672},{},[3673],{"type":27,"value":3674},"Modern CI\u002FCD is sophisticated:",{"type":21,"tag":527,"props":3676,"children":3677},{},[3678,3688,3698,3708,3718],{"type":21,"tag":98,"props":3679,"children":3680},{},[3681,3686],{"type":21,"tag":36,"props":3682,"children":3683},{},[3684],{"type":27,"value":3685},"Parallel testing",{"type":27,"value":3687}," — unit, integration, e2e at the same time",{"type":21,"tag":98,"props":3689,"children":3690},{},[3691,3696],{"type":21,"tag":36,"props":3692,"children":3693},{},[3694],{"type":27,"value":3695},"Performance monitoring",{"type":27,"value":3697}," — reject PRs that slow down the app",{"type":21,"tag":98,"props":3699,"children":3700},{},[3701,3706],{"type":21,"tag":36,"props":3702,"children":3703},{},[3704],{"type":27,"value":3705},"Visual regression testing",{"type":27,"value":3707}," — catch unintended design changes",{"type":21,"tag":98,"props":3709,"children":3710},{},[3711,3716],{"type":21,"tag":36,"props":3712,"children":3713},{},[3714],{"type":27,"value":3715},"Semantic versioning",{"type":27,"value":3717}," — automate version bumps and releases",{"type":21,"tag":98,"props":3719,"children":3720},{},[3721,3726],{"type":21,"tag":36,"props":3722,"children":3723},{},[3724],{"type":27,"value":3725},"Multi-stage deployments",{"type":27,"value":3727}," — preview → staging → production",{"type":21,"tag":22,"props":3729,"children":3731},{"id":3730},"the-full-pipeline",[3732],{"type":27,"value":3733},"The Full Pipeline",{"type":21,"tag":76,"props":3735,"children":3737},{"code":3736},"Code Push\n  ↓\n├─→ Lint & Format\n├─→ Unit Tests (parallel)\n├─→ Integration Tests (parallel)\n├─→ Build Artifact\n├─→ Visual Regression Tests\n├─→ Lighthouse Performance Check\n├─→ Security Scanning\n├─→ Deploy Preview (for PRs)\n└─→ Deploy Production (on main)\n",[3738],{"type":21,"tag":84,"props":3739,"children":3740},{"__ignoreMap":7},[3741],{"type":27,"value":3736},{"type":21,"tag":22,"props":3743,"children":3745},{"id":3744},"github-actions-workflow",[3746],{"type":27,"value":3747},"GitHub Actions Workflow",{"type":21,"tag":76,"props":3749,"children":3754},{"code":3750,"language":3751,"meta":7,"className":3752},"name: Frontend CI\u002FCD\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n\nenv:\n  NODE_VERSION: '18'\n  PNPM_VERSION: '8'\n\njobs:\n  # Setup\n  setup:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      \n      - uses: pnpm\u002Faction-setup@v2\n        with:\n          version: ${{ env.PNPM_VERSION }}\n      \n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      \n      - run: pnpm install --frozen-lockfile\n  \n  # Lint\n  lint:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm lint\n  \n  # Unit Tests (Parallel)\n  test-unit:\n    needs: setup\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        shard: [1, 2, 3, 4]\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm test:unit -- --shard=${{ matrix.shard }}\u002F4\n  \n  # Integration Tests\n  test-integration:\n    needs: setup\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:15\n        env:\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm test:integration\n        env:\n          DATABASE_URL: postgresql:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest\n  \n  # Build\n  build:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm build\n      - uses: actions\u002Fupload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n  \n  # Visual Regression Tests\n  visual-regression:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - run: pnpm dlx playwright install --with-deps\n      - run: pnpm test:visual\n      \n      - uses: actions\u002Fupload-artifact@v3\n        if: failure()\n        with:\n          name: visual-regression-report\n          path: test-results\u002F\n  \n  # Performance Check\n  lighthouse:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - uses: treosh\u002Flighthouse-ci-action@v9\n        with:\n          uploadArtifacts: true\n          configPath: '.\u002Flighthouserc.json'\n  \n  # Security Scanning\n  security:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      \n      # npm audit\n      - run: npm audit --audit-level=moderate\n      \n      # Snyk scanning\n      - uses: snyk\u002Factions\u002Fsetup@master\n      - run: snyk test --severity-threshold=high\n        env:\n          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n      \n      # OWASP dependency check\n      - uses: dependency-check\u002FDependency-Check_Action@main\n        with:\n          project: 'Frontend'\n          path: '.'\n          format: 'SARIF'\n          args: >\n            --enableExperimental\n            --enableRetired\n  \n  # Deploy Preview (on PR)\n  deploy-preview:\n    needs: [lint, test-unit, build, lighthouse]\n    if: github.event_name == 'pull_request'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - uses: netlify\u002Factions\u002Fcli@master\n        with:\n          args: deploy --dir=dist\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}\n      \n      - uses: actions\u002Fgithub-script@v6\n        with:\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: `Preview deployed: [View](https:\u002F\u002Fdeploy-preview-${{ github.event.number }}.netlify.app)`\n            })\n  \n  # Deploy Production (on main)\n  deploy-production:\n    needs: [lint, test-unit, build, lighthouse, security]\n    if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n        with:\n          fetch-depth: 0\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      # Semantic release\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: cycjimmy\u002Fsemantic-release-action@v3\n        id: semantic\n        with:\n          branches: |\n            [\n              '+([0-9])?(.{+([0-9]),x}).x',\n              'main',\n              'develop',\n              {\n                name: 'next',\n                prerelease: true\n              }\n            ]\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n      \n      # Deploy to Vercel\n      - uses: amondnet\u002Fvercel-action@v20\n        if: steps.semantic.outputs.new_release_published == 'true'\n        with:\n          vercel-token: ${{ secrets.VERCEL_TOKEN }}\n          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}\n          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}\n          working-directory: .\u002Fdist\n      \n      # Notify Slack\n      - uses: slackapi\u002Fslack-github-action@v1.24.0\n        with:\n          payload: |\n            {\n              \"text\": \"✅ Production deployment successful\",\n              \"blocks\": [\n                {\n                  \"type\": \"section\",\n                  \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": \"*Production Deployed*\\n*Version:* ${{ steps.semantic.outputs.new_release_version }}\\n*Commit:* ${{ github.sha }}\"\n                  }\n                }\n              ]\n            }\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\n","yaml",[3753],"language-yaml",[3755],{"type":21,"tag":84,"props":3756,"children":3757},{"__ignoreMap":7},[3758],{"type":27,"value":3750},{"type":21,"tag":22,"props":3760,"children":3762},{"id":3761},"key-features-explained",[3763],{"type":27,"value":3764},"Key Features Explained",{"type":21,"tag":64,"props":3766,"children":3768},{"id":3767},"_1-parallel-job-execution",[3769],{"type":27,"value":3770},"1. Parallel Job Execution",{"type":21,"tag":30,"props":3772,"children":3773},{},[3774],{"type":27,"value":3775},"Tests run simultaneously instead of sequentially:",{"type":21,"tag":76,"props":3777,"children":3780},{"code":3778,"language":3751,"meta":7,"className":3779},"strategy:\n  matrix:\n    shard: [1, 2, 3, 4]\nrun: pnpm test -- --shard=${{ matrix.shard }}\u002F4\n",[3753],[3781],{"type":21,"tag":84,"props":3782,"children":3783},{"__ignoreMap":7},[3784],{"type":27,"value":3778},{"type":21,"tag":30,"props":3786,"children":3787},{},[3788],{"type":27,"value":3789},"Reduces CI time from 12 minutes → 4 minutes.",{"type":21,"tag":64,"props":3791,"children":3793},{"id":3792},"_2-dependency-caching",[3794],{"type":27,"value":3795},"2. Dependency Caching",{"type":21,"tag":76,"props":3797,"children":3800},{"code":3798,"language":3751,"meta":7,"className":3799},"- uses: actions\u002Fsetup-node@v3\n  with:\n    node-version: ${{ env.NODE_VERSION }}\n    cache: 'pnpm'\n",[3753],[3801],{"type":21,"tag":84,"props":3802,"children":3803},{"__ignoreMap":7},[3804],{"type":27,"value":3798},{"type":21,"tag":30,"props":3806,"children":3807},{},[3808,3810,3816],{"type":27,"value":3809},"Skips ",{"type":21,"tag":84,"props":3811,"children":3813},{"className":3812},[],[3814],{"type":27,"value":3815},"pnpm install",{"type":27,"value":3817}," if dependencies haven't changed. Saves 1-2 minutes per run.",{"type":21,"tag":64,"props":3819,"children":3821},{"id":3820},"_3-conditional-deployment",[3822],{"type":27,"value":3823},"3. Conditional Deployment",{"type":21,"tag":76,"props":3825,"children":3828},{"code":3826,"language":3751,"meta":7,"className":3827},"if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n",[3753],[3829],{"type":21,"tag":84,"props":3830,"children":3831},{"__ignoreMap":7},[3832],{"type":27,"value":3826},{"type":21,"tag":30,"props":3834,"children":3835},{},[3836],{"type":27,"value":3837},"Only deploy to production on main branch pushes. PRs get preview deployments.",{"type":21,"tag":64,"props":3839,"children":3841},{"id":3840},"_4-semantic-versioning",[3842],{"type":27,"value":3843},"4. Semantic Versioning",{"type":21,"tag":76,"props":3845,"children":3848},{"code":3846,"language":3751,"meta":7,"className":3847},"- uses: cycjimmy\u002Fsemantic-release-action@v3\n",[3753],[3849],{"type":21,"tag":84,"props":3850,"children":3851},{"__ignoreMap":7},[3852],{"type":27,"value":3846},{"type":21,"tag":30,"props":3854,"children":3855},{},[3856],{"type":27,"value":3857},"Automatically:",{"type":21,"tag":94,"props":3859,"children":3860},{},[3861,3866,3871,3876,3881],{"type":21,"tag":98,"props":3862,"children":3863},{},[3864],{"type":27,"value":3865},"Analyzes commits (feat → minor, fix → patch)",{"type":21,"tag":98,"props":3867,"children":3868},{},[3869],{"type":27,"value":3870},"Bumps version in package.json",{"type":21,"tag":98,"props":3872,"children":3873},{},[3874],{"type":27,"value":3875},"Creates GitHub release",{"type":21,"tag":98,"props":3877,"children":3878},{},[3879],{"type":27,"value":3880},"Publishes to npm",{"type":21,"tag":98,"props":3882,"children":3883},{},[3884],{"type":27,"value":3885},"Generates changelog",{"type":21,"tag":30,"props":3887,"children":3888},{},[3889],{"type":27,"value":3890},"One less manual step.",{"type":21,"tag":64,"props":3892,"children":3894},{"id":3893},"_5-performance-monitoring",[3895],{"type":27,"value":3896},"5. Performance Monitoring",{"type":21,"tag":76,"props":3898,"children":3901},{"code":3899,"language":3751,"meta":7,"className":3900},"- uses: treosh\u002Flighthouse-ci-action@v9\n",[3753],[3902],{"type":21,"tag":84,"props":3903,"children":3904},{"__ignoreMap":7},[3905],{"type":27,"value":3899},{"type":21,"tag":30,"props":3907,"children":3908},{},[3909],{"type":27,"value":3910},"Blocks deployment if performance degrades. Ensures Lighthouse score stays above threshold.",{"type":21,"tag":22,"props":3912,"children":3914},{"id":3913},"caching-strategy",[3915],{"type":27,"value":3916},"Caching Strategy",{"type":21,"tag":76,"props":3918,"children":3921},{"code":3919,"language":3751,"meta":7,"className":3920},"# .github\u002Fworkflows\u002Fci.yml\ncache-strategy:\n  - cache: node_modules\n    key: ${{ runner.os }}-pnpm-${{ hashFiles('**\u002Fpnpm-lock.yaml') }}\n    restore-keys: ${{ runner.os }}-pnpm-\n  \n  - cache: .next\n    key: ${{ runner.os }}-next-${{ hashFiles('**\u002Fpackage-lock.json') }}\n    restore-keys: ${{ runner.os }}-next-\n  \n  - cache: .turbo\n    key: ${{ runner.os }}-turbo-${{ hashFiles('**\u002Fpnpm-lock.yaml') }}\n    restore-keys: ${{ runner.os }}-turbo-\n",[3753],[3922],{"type":21,"tag":84,"props":3923,"children":3924},{"__ignoreMap":7},[3925],{"type":27,"value":3919},{"type":21,"tag":22,"props":3927,"children":3929},{"id":3928},"results",[3930],{"type":27,"value":3931},"Results",{"type":21,"tag":30,"props":3933,"children":3934},{},[3935],{"type":27,"value":3936},"What a pipeline like this typically changes (illustrative, not measured claims): CI time drops from tens of minutes to single digits, deploys go from a weekly event to a routine one, and security scanning stops depending on someone remembering to run it.",{"type":21,"tag":30,"props":3938,"children":3939},{},[3940,3942,3947],{"type":27,"value":3941},"My verified production result from building this kind of pipeline at Ordant: ",{"type":21,"tag":36,"props":3943,"children":3944},{},[3945],{"type":27,"value":3946},"merge-to-deploy time down 40%, with 99.9% uptime",{"type":27,"value":1665},{"type":21,"tag":22,"props":3949,"children":3951},{"id":3950},"cost-optimization",[3952],{"type":27,"value":3953},"Cost Optimization",{"type":21,"tag":30,"props":3955,"children":3956},{},[3957],{"type":27,"value":3958},"Worked example at GitHub Actions list pricing ($0.008\u002Fminute per job):",{"type":21,"tag":30,"props":3960,"children":3961},{},[3962],{"type":27,"value":3963},"A 5-minute pipeline with 6 parallel jobs = 30 job-minutes = $0.24\u002Frun.",{"type":21,"tag":30,"props":3965,"children":3966},{},[3967],{"type":27,"value":3968},"At 30 runs\u002Fday, that's $7.20\u002Fday ≈ $216\u002Fmonth.",{"type":21,"tag":30,"props":3970,"children":3971},{},[3972],{"type":27,"value":3973},"Worth it for the bugs prevented and deployment speed gained.",{"type":21,"tag":22,"props":3975,"children":3977},{"id":3976},"pro-tips",[3978],{"type":27,"value":3979},"Pro Tips",{"type":21,"tag":527,"props":3981,"children":3982},{},[3983,3993,4003,4013,4023],{"type":21,"tag":98,"props":3984,"children":3985},{},[3986,3991],{"type":21,"tag":36,"props":3987,"children":3988},{},[3989],{"type":27,"value":3990},"Cache aggressively",{"type":27,"value":3992}," — saves 1-3 minutes per run",{"type":21,"tag":98,"props":3994,"children":3995},{},[3996,4001],{"type":21,"tag":36,"props":3997,"children":3998},{},[3999],{"type":27,"value":4000},"Run tests in parallel",{"type":27,"value":4002}," — hardware is cheaper than time",{"type":21,"tag":98,"props":4004,"children":4005},{},[4006,4011],{"type":21,"tag":36,"props":4007,"children":4008},{},[4009],{"type":27,"value":4010},"Fail fast",{"type":27,"value":4012}," — lint before tests, tests before build",{"type":21,"tag":98,"props":4014,"children":4015},{},[4016,4021],{"type":21,"tag":36,"props":4017,"children":4018},{},[4019],{"type":27,"value":4020},"Monitor performance",{"type":27,"value":4022}," — performance regression is a deployment blocker",{"type":21,"tag":98,"props":4024,"children":4025},{},[4026,4031],{"type":21,"tag":36,"props":4027,"children":4028},{},[4029],{"type":27,"value":4030},"Automate versioning",{"type":27,"value":4032}," — semantic-release removes guesswork",{"type":21,"tag":30,"props":4034,"children":4035},{},[4036],{"type":27,"value":4037},"Good CI\u002FCD is a force multiplier. Invest in it early.",{"title":7,"searchDepth":586,"depth":586,"links":4039},[4040,4041,4042,4043,4050,4051,4052,4053],{"id":3638,"depth":586,"text":3641},{"id":3730,"depth":586,"text":3733},{"id":3744,"depth":586,"text":3747},{"id":3761,"depth":586,"text":3764,"children":4044},[4045,4046,4047,4048,4049],{"id":3767,"depth":592,"text":3770},{"id":3792,"depth":592,"text":3795},{"id":3820,"depth":592,"text":3823},{"id":3840,"depth":592,"text":3843},{"id":3893,"depth":592,"text":3896},{"id":3913,"depth":586,"text":3916},{"id":3928,"depth":586,"text":3931},{"id":3950,"depth":586,"text":3953},{"id":3976,"depth":586,"text":3979},"content:blog:advanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows",{"_path":4058,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":4059,"description":4060,"date":4061,"readingTime":1610,"tags":4062,"featured":6,"body":4064,"_type":602,"_id":4913,"_source":604,"_file":4914,"_stem":4915,"_extension":607},"\u002Fblog\u002Faccessibility-engineering-in-vue","Accessibility Engineering in Vue: Building for Everyone (WCAG 2.1 AA)","Comprehensive guide to building accessible Vue applications. Learn ARIA, keyboard navigation, semantic HTML, color contrast, screen reader support, and automated testing.","2024-05-20",[4063,616],"Accessibility",{"type":18,"children":4065,"toc":4900},[4066,4072,4077,4082,4125,4130,4136,4141,4174,4185,4191,4196,4205,4210,4325,4331,4336,4345,4350,4430,4436,4441,4450,4456,4461,4470,4475,4493,4499,4504,4513,4518,4570,4576,4585,4590,4619,4625,4630,4639,4648,4653,4662,4668,4673,4861,4867,4872,4895],{"type":21,"tag":22,"props":4067,"children":4069},{"id":4068},"why-accessibility-matters",[4070],{"type":27,"value":4071},"Why Accessibility Matters",{"type":21,"tag":30,"props":4073,"children":4074},{},[4075],{"type":27,"value":4076},"15% of the global population has disabilities. That's 1 billion people.",{"type":21,"tag":30,"props":4078,"children":4079},{},[4080],{"type":27,"value":4081},"For businesses:",{"type":21,"tag":94,"props":4083,"children":4084},{},[4085,4095,4105,4115],{"type":21,"tag":98,"props":4086,"children":4087},{},[4088,4093],{"type":21,"tag":36,"props":4089,"children":4090},{},[4091],{"type":27,"value":4092},"Legal",{"type":27,"value":4094},": WCAG compliance required in many jurisdictions (US, UK, EU)",{"type":21,"tag":98,"props":4096,"children":4097},{},[4098,4103],{"type":21,"tag":36,"props":4099,"children":4100},{},[4101],{"type":27,"value":4102},"Market",{"type":27,"value":4104},": Accessible sites reach 1 billion+ users",{"type":21,"tag":98,"props":4106,"children":4107},{},[4108,4113],{"type":21,"tag":36,"props":4109,"children":4110},{},[4111],{"type":27,"value":4112},"SEO",{"type":27,"value":4114},": Google rewards accessible sites",{"type":21,"tag":98,"props":4116,"children":4117},{},[4118,4123],{"type":21,"tag":36,"props":4119,"children":4120},{},[4121],{"type":27,"value":4122},"UX",{"type":27,"value":4124},": Good accessibility is good UX for everyone (captions help in noisy environments)",{"type":21,"tag":30,"props":4126,"children":4127},{},[4128],{"type":27,"value":4129},"Accessibility isn't a feature. It's a requirement.",{"type":21,"tag":22,"props":4131,"children":4133},{"id":4132},"web-content-accessibility-guidelines-wcag-21",[4134],{"type":27,"value":4135},"Web Content Accessibility Guidelines (WCAG) 2.1",{"type":21,"tag":30,"props":4137,"children":4138},{},[4139],{"type":27,"value":4140},"The standard has three levels:",{"type":21,"tag":94,"props":4142,"children":4143},{},[4144,4154,4164],{"type":21,"tag":98,"props":4145,"children":4146},{},[4147,4152],{"type":21,"tag":36,"props":4148,"children":4149},{},[4150],{"type":27,"value":4151},"A",{"type":27,"value":4153},": Minimum compliance",{"type":21,"tag":98,"props":4155,"children":4156},{},[4157,4162],{"type":21,"tag":36,"props":4158,"children":4159},{},[4160],{"type":27,"value":4161},"AA",{"type":27,"value":4163},": Recommended (what most sites aim for)",{"type":21,"tag":98,"props":4165,"children":4166},{},[4167,4172],{"type":21,"tag":36,"props":4168,"children":4169},{},[4170],{"type":27,"value":4171},"AAA",{"type":27,"value":4173},": Enhanced (nice to have)",{"type":21,"tag":30,"props":4175,"children":4176},{},[4177,4179,4183],{"type":27,"value":4178},"We'll focus on ",{"type":21,"tag":36,"props":4180,"children":4181},{},[4182],{"type":27,"value":4161},{"type":27,"value":4184},", the legal standard in most places.",{"type":21,"tag":22,"props":4186,"children":4188},{"id":4187},"semantic-html",[4189],{"type":27,"value":4190},"Semantic HTML",{"type":21,"tag":30,"props":4192,"children":4193},{},[4194],{"type":27,"value":4195},"The foundation of accessibility:",{"type":21,"tag":76,"props":4197,"children":4200},{"className":4198,"code":4199,"language":194,"meta":7},[192],"\u003C!-- ❌ Bad: Non-semantic HTML -->\n\u003Cdiv class=\"header\">\n  \u003Cdiv class=\"heading\">My Blog\u003C\u002Fdiv>\n  \u003Cdiv class=\"navigation\">\n    \u003Cdiv class=\"nav-item\">\u003Ca href=\"\u002F\">Home\u003C\u002Fa>\u003C\u002Fdiv>\n    \u003Cdiv class=\"nav-item\">\u003Ca href=\"\u002Fblog\">Blog\u003C\u002Fa>\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n\u003Cdiv class=\"main\">\n  \u003Cdiv class=\"article\">\n    \u003Cdiv class=\"title\">Post Title\u003C\u002Fdiv>\n    \u003Cdiv class=\"content\">Post content...\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n\u003C!-- ✅ Good: Semantic HTML -->\n\u003Cheader>\n  \u003Ch1>My Blog\u003C\u002Fh1>\n  \u003Cnav>\n    \u003Cul>\n      \u003Cli>\u003Ca href=\"\u002F\">Home\u003C\u002Fa>\u003C\u002Fli>\n      \u003Cli>\u003Ca href=\"\u002Fblog\">Blog\u003C\u002Fa>\u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fnav>\n\u003C\u002Fheader>\n\n\u003Cmain>\n  \u003Carticle>\n    \u003Ch2>Post Title\u003C\u002Fh2>\n    \u003Cp>Post content...\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Fmain>\n",[4201],{"type":21,"tag":84,"props":4202,"children":4203},{"__ignoreMap":7},[4204],{"type":27,"value":4199},{"type":21,"tag":30,"props":4206,"children":4207},{},[4208],{"type":27,"value":4209},"Semantic elements:",{"type":21,"tag":94,"props":4211,"children":4212},{},[4213,4258,4277,4288,4299],{"type":21,"tag":98,"props":4214,"children":4215},{},[4216,4222,4224,4230,4231,4237,4238,4244,4245,4251,4252],{"type":21,"tag":84,"props":4217,"children":4219},{"className":4218},[],[4220],{"type":27,"value":4221},"\u003Cheader>",{"type":27,"value":4223},", ",{"type":21,"tag":84,"props":4225,"children":4227},{"className":4226},[],[4228],{"type":27,"value":4229},"\u003Cnav>",{"type":27,"value":4223},{"type":21,"tag":84,"props":4232,"children":4234},{"className":4233},[],[4235],{"type":27,"value":4236},"\u003Cmain>",{"type":27,"value":4223},{"type":21,"tag":84,"props":4239,"children":4241},{"className":4240},[],[4242],{"type":27,"value":4243},"\u003Carticle>",{"type":27,"value":4223},{"type":21,"tag":84,"props":4246,"children":4248},{"className":4247},[],[4249],{"type":27,"value":4250},"\u003Csection>",{"type":27,"value":4223},{"type":21,"tag":84,"props":4253,"children":4255},{"className":4254},[],[4256],{"type":27,"value":4257},"\u003Cfooter>",{"type":21,"tag":98,"props":4259,"children":4260},{},[4261,4267,4269,4275],{"type":21,"tag":84,"props":4262,"children":4264},{"className":4263},[],[4265],{"type":27,"value":4266},"\u003Ch1>",{"type":27,"value":4268}," - ",{"type":21,"tag":84,"props":4270,"children":4272},{"className":4271},[],[4273],{"type":27,"value":4274},"\u003Ch6>",{"type":27,"value":4276}," for headings (never skip levels)",{"type":21,"tag":98,"props":4278,"children":4279},{},[4280,4286],{"type":21,"tag":84,"props":4281,"children":4283},{"className":4282},[],[4284],{"type":27,"value":4285},"\u003Cbutton>",{"type":27,"value":4287}," for interactive elements",{"type":21,"tag":98,"props":4289,"children":4290},{},[4291,4297],{"type":21,"tag":84,"props":4292,"children":4294},{"className":4293},[],[4295],{"type":27,"value":4296},"\u003Clabel>",{"type":27,"value":4298}," for form inputs",{"type":21,"tag":98,"props":4300,"children":4301},{},[4302,4308,4310,4316,4317,4323],{"type":21,"tag":84,"props":4303,"children":4305},{"className":4304},[],[4306],{"type":27,"value":4307},"\u003Ctable>",{"type":27,"value":4309}," for tabular data (with ",{"type":21,"tag":84,"props":4311,"children":4313},{"className":4312},[],[4314],{"type":27,"value":4315},"\u003Cthead>",{"type":27,"value":4223},{"type":21,"tag":84,"props":4318,"children":4320},{"className":4319},[],[4321],{"type":27,"value":4322},"\u003Ctbody>",{"type":27,"value":4324},")",{"type":21,"tag":22,"props":4326,"children":4328},{"id":4327},"aria-accessible-rich-internet-applications",[4329],{"type":27,"value":4330},"ARIA (Accessible Rich Internet Applications)",{"type":21,"tag":30,"props":4332,"children":4333},{},[4334],{"type":27,"value":4335},"Use ARIA when HTML alone can't describe the UI:",{"type":21,"tag":76,"props":4337,"children":4340},{"className":4338,"code":4339,"language":194,"meta":7},[192],"\u003Cscript setup lang=\"ts\">\nconst isOpen = ref(false)\nconst buttonRef = ref\u003CHTMLButtonElement>()\nconst menuRef = ref\u003CHTMLUListElement>()\n\nconst toggleMenu = () => {\n  isOpen.value = !isOpen.value\n  if (isOpen.value) {\n    menuRef.value?.focus()\n  }\n}\n\nconst closeMenu = () => {\n  isOpen.value = false\n  buttonRef.value?.focus()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ARIA roles, states, and properties -->\n  \u003Cdiv class=\"dropdown\">\n    \u003Cbutton\n      ref=\"buttonRef\"\n      :aria-expanded=\"isOpen\"\n      aria-haspopup=\"menu\"\n      @click=\"toggleMenu\"\n    >\n      Menu\n    \u003C\u002Fbutton>\n\n    \u003Cul\n      v-if=\"isOpen\"\n      ref=\"menuRef\"\n      role=\"menu\"\n      class=\"dropdown-menu\"\n      @keydown.esc=\"closeMenu\"\n    >\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#home\" @click=\"closeMenu\">Home\u003C\u002Fa>\n      \u003C\u002Fli>\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#about\" @click=\"closeMenu\">About\u003C\u002Fa>\n      \u003C\u002Fli>\n      \u003Cli role=\"separator\" aria-label=\"Section divider\" \u002F>\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#contact\" @click=\"closeMenu\">Contact\u003C\u002Fa>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[4341],{"type":21,"tag":84,"props":4342,"children":4343},{"__ignoreMap":7},[4344],{"type":27,"value":4339},{"type":21,"tag":30,"props":4346,"children":4347},{},[4348],{"type":27,"value":4349},"Key ARIA attributes:",{"type":21,"tag":94,"props":4351,"children":4352},{},[4353,4364,4375,4386,4397,4408,4419],{"type":21,"tag":98,"props":4354,"children":4355},{},[4356,4362],{"type":21,"tag":84,"props":4357,"children":4359},{"className":4358},[],[4360],{"type":27,"value":4361},"aria-label",{"type":27,"value":4363}," — Label for screen readers",{"type":21,"tag":98,"props":4365,"children":4366},{},[4367,4373],{"type":21,"tag":84,"props":4368,"children":4370},{"className":4369},[],[4371],{"type":27,"value":4372},"aria-labelledby",{"type":27,"value":4374}," — Reference element that labels this one",{"type":21,"tag":98,"props":4376,"children":4377},{},[4378,4384],{"type":21,"tag":84,"props":4379,"children":4381},{"className":4380},[],[4382],{"type":27,"value":4383},"aria-describedby",{"type":27,"value":4385}," — Reference element that describes this one",{"type":21,"tag":98,"props":4387,"children":4388},{},[4389,4395],{"type":21,"tag":84,"props":4390,"children":4392},{"className":4391},[],[4393],{"type":27,"value":4394},"aria-expanded",{"type":27,"value":4396}," — Whether collapsible element is open",{"type":21,"tag":98,"props":4398,"children":4399},{},[4400,4406],{"type":21,"tag":84,"props":4401,"children":4403},{"className":4402},[],[4404],{"type":27,"value":4405},"aria-hidden",{"type":27,"value":4407}," — Hide from screen readers (for decorative elements)",{"type":21,"tag":98,"props":4409,"children":4410},{},[4411,4417],{"type":21,"tag":84,"props":4412,"children":4414},{"className":4413},[],[4415],{"type":27,"value":4416},"aria-live",{"type":27,"value":4418}," — Announce changes dynamically",{"type":21,"tag":98,"props":4420,"children":4421},{},[4422,4428],{"type":21,"tag":84,"props":4423,"children":4425},{"className":4424},[],[4426],{"type":27,"value":4427},"role",{"type":27,"value":4429}," — Define element purpose when semantic HTML won't work",{"type":21,"tag":22,"props":4431,"children":4433},{"id":4432},"keyboard-navigation",[4434],{"type":27,"value":4435},"Keyboard Navigation",{"type":21,"tag":30,"props":4437,"children":4438},{},[4439],{"type":27,"value":4440},"Many users navigate by keyboard only. Every interactive element must be keyboard accessible:",{"type":21,"tag":76,"props":4442,"children":4445},{"className":4443,"code":4444,"language":194,"meta":7},[192],"\u003Cscript setup lang=\"ts\">\n\u002F\u002F Trap focus in modal\nconst trapFocus = (event: KeyboardEvent) => {\n  if (event.key !== 'Tab') return\n\n  const focusableElements = document.querySelectorAll(\n    'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n  )\n  \n  const firstElement = focusableElements[0] as HTMLElement\n  const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement\n\n  if (event.shiftKey && document.activeElement === firstElement) {\n    event.preventDefault()\n    lastElement.focus()\n  } else if (!event.shiftKey && document.activeElement === lastElement) {\n    event.preventDefault()\n    firstElement.focus()\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- Make elements focusable -->\n  \u003Cdiv\n    role=\"dialog\"\n    @keydown=\"trapFocus\"\n    @keydown.escape=\"closeModal\"\n  >\n    \u003Ch2 id=\"modal-title\">Confirm Action\u003C\u002Fh2>\n    \n    \u003C!-- Visible focus indicator -->\n    \u003Cbutton @click=\"confirm\" class=\"focus-visible:ring-2\">\n      Confirm\n    \u003C\u002Fbutton>\n    \n    \u003Cbutton @click=\"cancel\" class=\"focus-visible:ring-2\">\n      Cancel\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n\u002F* Always show focus indicator -->\n:focus-visible {\n  outline: 2px solid var(--accent);\n  outline-offset: 2px;\n}\n\n\u002F* Don't hide focus on keyboard users -->\nbutton:focus-visible {\n  outline: inherit;\n}\n\n\u002F* But hide for mouse users if desired *\u002F\nbutton:focus:not(:focus-visible) {\n  outline: none;\n}\n\u003C\u002Fstyle>\n",[4446],{"type":21,"tag":84,"props":4447,"children":4448},{"__ignoreMap":7},[4449],{"type":27,"value":4444},{"type":21,"tag":22,"props":4451,"children":4453},{"id":4452},"color-contrast",[4454],{"type":27,"value":4455},"Color Contrast",{"type":21,"tag":30,"props":4457,"children":4458},{},[4459],{"type":27,"value":4460},"Text must have sufficient contrast ratio:",{"type":21,"tag":76,"props":4462,"children":4465},{"className":4463,"code":4464,"language":194,"meta":7},[192],"\u003Cstyle scoped>\n\u002F* WCAG AA requires:\n   - Normal text: 4.5:1\n   - Large text: 3:1\n*\u002F\n\n.text-primary {\n  color: #000;\n  background: #fff;\n  \u002F* 21:1 contrast ✅ *\u002F\n}\n\n.text-secondary {\n  color: #555;\n  background: #fff;\n  \u002F* 8.59:1 contrast ✅ *\u002F\n}\n\n.text-tertiary {\n  color: #999;\n  background: #fff;\n  \u002F* 3.74:1 contrast ❌ *\u002F\n}\n\n\u002F* Test with: https:\u002F\u002Fwebaim.org\u002Fresources\u002Fcontrastchecker\u002F *\u002F\n\u003C\u002Fstyle>\n",[4466],{"type":21,"tag":84,"props":4467,"children":4468},{"__ignoreMap":7},[4469],{"type":27,"value":4464},{"type":21,"tag":30,"props":4471,"children":4472},{},[4473],{"type":27,"value":4474},"Use tools to check:",{"type":21,"tag":94,"props":4476,"children":4477},{},[4478,4483,4488],{"type":21,"tag":98,"props":4479,"children":4480},{},[4481],{"type":27,"value":4482},"WebAIM Contrast Checker",{"type":21,"tag":98,"props":4484,"children":4485},{},[4486],{"type":27,"value":4487},"Accessible Colors",{"type":21,"tag":98,"props":4489,"children":4490},{},[4491],{"type":27,"value":4492},"Chrome DevTools (automatic detection)",{"type":21,"tag":22,"props":4494,"children":4496},{"id":4495},"form-accessibility",[4497],{"type":27,"value":4498},"Form Accessibility",{"type":21,"tag":30,"props":4500,"children":4501},{},[4502],{"type":27,"value":4503},"Forms are a common accessibility fail point:",{"type":21,"tag":76,"props":4505,"children":4508},{"className":4506,"code":4507,"language":194,"meta":7},[192],"\u003Ctemplate>\n  \u003Cform @submit.prevent=\"submit\">\n    \u003C!-- ✅ Properly labeled input -->\n    \u003Cdiv class=\"form-group\">\n      \u003Clabel for=\"email\">Email Address\u003C\u002Flabel>\n      \u003Cinput\n        id=\"email\"\n        type=\"email\"\n        required\n        aria-describedby=\"email-hint\"\n      \u002F>\n      \u003Csmall id=\"email-hint\">We'll never share your email\u003C\u002Fsmall>\n    \u003C\u002Fdiv>\n\n    \u003C!-- ✅ Error messages linked to input -->\n    \u003Cdiv class=\"form-group\">\n      \u003Clabel for=\"password\">Password\u003C\u002Flabel>\n      \u003Cinput\n        id=\"password\"\n        type=\"password\"\n        required\n        aria-invalid=\"password.invalid\"\n        :aria-describedby=\"password.invalid ? 'password-error' : undefined\"\n      \u002F>\n      \u003Cdiv v-if=\"password.invalid\" id=\"password-error\" role=\"alert\">\n        Password must be at least 8 characters\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003C!-- ✅ Radio buttons with fieldset -->\n    \u003Cfieldset>\n      \u003Clegend>Select your role\u003C\u002Flegend>\n      \u003Cdiv>\n        \u003Cinput id=\"admin\" type=\"radio\" name=\"role\" value=\"admin\" \u002F>\n        \u003Clabel for=\"admin\">Administrator\u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv>\n        \u003Cinput id=\"user\" type=\"radio\" name=\"role\" value=\"user\" \u002F>\n        \u003Clabel for=\"user\">User\u003C\u002Flabel>\n      \u003C\u002Fdiv>\n    \u003C\u002Ffieldset>\n\n    \u003Cbutton type=\"submit\">Submit\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[4509],{"type":21,"tag":84,"props":4510,"children":4511},{"__ignoreMap":7},[4512],{"type":27,"value":4507},{"type":21,"tag":30,"props":4514,"children":4515},{},[4516],{"type":27,"value":4517},"Key form patterns:",{"type":21,"tag":94,"props":4519,"children":4520},{},[4521,4531,4542,4552,4557],{"type":21,"tag":98,"props":4522,"children":4523},{},[4524,4526],{"type":27,"value":4525},"Every input has a ",{"type":21,"tag":84,"props":4527,"children":4529},{"className":4528},[],[4530],{"type":27,"value":4296},{"type":21,"tag":98,"props":4532,"children":4533},{},[4534,4536],{"type":27,"value":4535},"Labels are connected with ",{"type":21,"tag":84,"props":4537,"children":4539},{"className":4538},[],[4540],{"type":27,"value":4541},"for=\"id\"",{"type":21,"tag":98,"props":4543,"children":4544},{},[4545,4547],{"type":27,"value":4546},"Error messages use ",{"type":21,"tag":84,"props":4548,"children":4550},{"className":4549},[],[4551],{"type":27,"value":4383},{"type":21,"tag":98,"props":4553,"children":4554},{},[4555],{"type":27,"value":4556},"Fieldset groups related inputs",{"type":21,"tag":98,"props":4558,"children":4559},{},[4560,4562,4568],{"type":27,"value":4561},"Use ",{"type":21,"tag":84,"props":4563,"children":4565},{"className":4564},[],[4566],{"type":27,"value":4567},"aria-invalid",{"type":27,"value":4569}," for validation states",{"type":21,"tag":22,"props":4571,"children":4573},{"id":4572},"images-media",[4574],{"type":27,"value":4575},"Images & Media",{"type":21,"tag":76,"props":4577,"children":4580},{"className":4578,"code":4579,"language":194,"meta":7},[192],"\u003Ctemplate>\n  \u003C!-- ✅ Descriptive alt text -->\n  \u003Cimg\n    src=\"hero.jpg\"\n    alt=\"Sunset over mountains with golden light reflecting on water\"\n  \u002F>\n\n  \u003C!-- ❌ Vague alt text -->\n  \u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\n  \u003C!-- Icons with labels -->\n  \u003Cbutton aria-label=\"Close menu\">\n    \u003Csvg>\u003C!-- close icon -->\u003C\u002Fsvg>\n  \u003C\u002Fbutton>\n\n  \u003C!-- Videos with captions -->\n  \u003Cvideo controls>\n    \u003Csource src=\"video.mp4\" type=\"video\u002Fmp4\" \u002F>\n    \u003Ctrack kind=\"captions\" src=\"captions.vtt\" srclang=\"en\" \u002F>\n  \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n",[4581],{"type":21,"tag":84,"props":4582,"children":4583},{"__ignoreMap":7},[4584],{"type":27,"value":4579},{"type":21,"tag":30,"props":4586,"children":4587},{},[4588],{"type":27,"value":4589},"Alt text guidelines:",{"type":21,"tag":94,"props":4591,"children":4592},{},[4593,4598,4609,4614],{"type":21,"tag":98,"props":4594,"children":4595},{},[4596],{"type":27,"value":4597},"Describe the content, not \"image of\"",{"type":21,"tag":98,"props":4599,"children":4600},{},[4601,4603],{"type":27,"value":4602},"Decorative images get ",{"type":21,"tag":84,"props":4604,"children":4606},{"className":4605},[],[4607],{"type":27,"value":4608},"alt=\"\"",{"type":21,"tag":98,"props":4610,"children":4611},{},[4612],{"type":27,"value":4613},"Icons need aria-label if no visible text",{"type":21,"tag":98,"props":4615,"children":4616},{},[4617],{"type":27,"value":4618},"Keep under 125 characters",{"type":21,"tag":22,"props":4620,"children":4622},{"id":4621},"automated-testing",[4623],{"type":27,"value":4624},"Automated Testing",{"type":21,"tag":30,"props":4626,"children":4627},{},[4628],{"type":27,"value":4629},"Use axe for automated accessibility testing:",{"type":21,"tag":76,"props":4631,"children":4634},{"className":4632,"code":4633,"language":81,"meta":7},[79],"npm install -D @axe-core\u002Fplaywright jest-axe\n",[4635],{"type":21,"tag":84,"props":4636,"children":4637},{"__ignoreMap":7},[4638],{"type":27,"value":4633},{"type":21,"tag":76,"props":4640,"children":4643},{"className":4641,"code":4642,"language":144,"meta":7},[142],"\u002F\u002F __tests__\u002Faccessibility.spec.ts\nimport { test, expect } from '@playwright\u002Ftest'\nimport { injectAxe, checkA11y } from 'axe-playwright'\n\ntest('homepage is accessible', async ({ page }) => {\n  await page.goto('http:\u002F\u002Flocalhost:3000')\n  await injectAxe(page)\n  await checkA11y(page)\n})\n",[4644],{"type":21,"tag":84,"props":4645,"children":4646},{"__ignoreMap":7},[4647],{"type":27,"value":4642},{"type":21,"tag":30,"props":4649,"children":4650},{},[4651],{"type":27,"value":4652},"Jest testing:",{"type":21,"tag":76,"props":4654,"children":4657},{"className":4655,"code":4656,"language":144,"meta":7},[142],"import { render } from '@vue\u002Ftest-utils'\nimport { axe, toHaveNoViolations } from 'jest-axe'\nimport Button from '.\u002FButton.vue'\n\nexpect.extend(toHaveNoViolations)\n\nit('should not have accessibility violations', async () => {\n  const { container } = render(Button)\n  const results = await axe(container)\n  expect(results).toHaveNoViolations()\n})\n",[4658],{"type":21,"tag":84,"props":4659,"children":4660},{"__ignoreMap":7},[4661],{"type":27,"value":4656},{"type":21,"tag":22,"props":4663,"children":4665},{"id":4664},"accessibility-checklist",[4666],{"type":27,"value":4667},"Accessibility Checklist",{"type":21,"tag":30,"props":4669,"children":4670},{},[4671],{"type":27,"value":4672},"Before shipping:",{"type":21,"tag":94,"props":4674,"children":4677},{"className":4675},[4676],"contains-task-list",[4678,4696,4711,4726,4741,4756,4771,4786,4801,4816,4831,4846],{"type":21,"tag":98,"props":4679,"children":4682},{"className":4680},[4681],"task-list-item",[4683,4688,4690,4694],{"type":21,"tag":4684,"props":4685,"children":4687},"input",{"disabled":16,"type":4686},"checkbox",[],{"type":27,"value":4689}," ",{"type":21,"tag":36,"props":4691,"children":4692},{},[4693],{"type":27,"value":4190},{"type":27,"value":4695}," — use proper elements, not divs",{"type":21,"tag":98,"props":4697,"children":4699},{"className":4698},[4681],[4700,4703,4704,4709],{"type":21,"tag":4684,"props":4701,"children":4702},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4705,"children":4706},{},[4707],{"type":27,"value":4708},"Color contrast",{"type":27,"value":4710}," — 4.5:1 for normal text",{"type":21,"tag":98,"props":4712,"children":4714},{"className":4713},[4681],[4715,4718,4719,4724],{"type":21,"tag":4684,"props":4716,"children":4717},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4720,"children":4721},{},[4722],{"type":27,"value":4723},"Keyboard navigation",{"type":27,"value":4725}," — every interactive element accessible",{"type":21,"tag":98,"props":4727,"children":4729},{"className":4728},[4681],[4730,4733,4734,4739],{"type":21,"tag":4684,"props":4731,"children":4732},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4735,"children":4736},{},[4737],{"type":27,"value":4738},"Focus visible",{"type":27,"value":4740}," — clear focus indicator visible",{"type":21,"tag":98,"props":4742,"children":4744},{"className":4743},[4681],[4745,4748,4749,4754],{"type":21,"tag":4684,"props":4746,"children":4747},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4750,"children":4751},{},[4752],{"type":27,"value":4753},"Forms labeled",{"type":27,"value":4755}," — all inputs have labels",{"type":21,"tag":98,"props":4757,"children":4759},{"className":4758},[4681],[4760,4763,4764,4769],{"type":21,"tag":4684,"props":4761,"children":4762},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4765,"children":4766},{},[4767],{"type":27,"value":4768},"Images have alt text",{"type":27,"value":4770}," — descriptive, not vague",{"type":21,"tag":98,"props":4772,"children":4774},{"className":4773},[4681],[4775,4778,4779,4784],{"type":21,"tag":4684,"props":4776,"children":4777},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4780,"children":4781},{},[4782],{"type":27,"value":4783},"Headings logical",{"type":27,"value":4785}," — h1 → h2 → h3 (no skipping)",{"type":21,"tag":98,"props":4787,"children":4789},{"className":4788},[4681],[4790,4793,4794,4799],{"type":21,"tag":4684,"props":4791,"children":4792},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4795,"children":4796},{},[4797],{"type":27,"value":4798},"ARIA appropriate",{"type":27,"value":4800}," — only when necessary",{"type":21,"tag":98,"props":4802,"children":4804},{"className":4803},[4681],[4805,4808,4809,4814],{"type":21,"tag":4684,"props":4806,"children":4807},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4810,"children":4811},{},[4812],{"type":27,"value":4813},"No flash",{"type":27,"value":4815}," — no content flashing > 3 times\u002Fsecond",{"type":21,"tag":98,"props":4817,"children":4819},{"className":4818},[4681],[4820,4823,4824,4829],{"type":21,"tag":4684,"props":4821,"children":4822},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4825,"children":4826},{},[4827],{"type":27,"value":4828},"Responsive text",{"type":27,"value":4830}," — readable at 200% zoom",{"type":21,"tag":98,"props":4832,"children":4834},{"className":4833},[4681],[4835,4838,4839,4844],{"type":21,"tag":4684,"props":4836,"children":4837},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4840,"children":4841},{},[4842],{"type":27,"value":4843},"Mobile accessible",{"type":27,"value":4845}," — touch targets 48px minimum",{"type":21,"tag":98,"props":4847,"children":4849},{"className":4848},[4681],[4850,4853,4854,4859],{"type":21,"tag":4684,"props":4851,"children":4852},{"disabled":16,"type":4686},[],{"type":27,"value":4689},{"type":21,"tag":36,"props":4855,"children":4856},{},[4857],{"type":27,"value":4858},"Automated tests pass",{"type":27,"value":4860}," — axe, jest-axe",{"type":21,"tag":22,"props":4862,"children":4864},{"id":4863},"real-impact",[4865],{"type":27,"value":4866},"Real Impact",{"type":21,"tag":30,"props":4868,"children":4869},{},[4870],{"type":27,"value":4871},"After implementing accessibility:",{"type":21,"tag":94,"props":4873,"children":4874},{},[4875,4880,4885,4890],{"type":21,"tag":98,"props":4876,"children":4877},{},[4878],{"type":27,"value":4879},"Accessibility score: 40 → 92\u002F100 (Lighthouse)",{"type":21,"tag":98,"props":4881,"children":4882},{},[4883],{"type":27,"value":4884},"Users with assistive tech: +8% traffic",{"type":21,"tag":98,"props":4886,"children":4887},{},[4888],{"type":27,"value":4889},"SEO improvement: +15% in search ranking",{"type":21,"tag":98,"props":4891,"children":4892},{},[4893],{"type":27,"value":4894},"General UX improvement: better for everyone",{"type":21,"tag":30,"props":4896,"children":4897},{},[4898],{"type":27,"value":4899},"Accessibility isn't a nice-to-have. It's engineering excellence.",{"title":7,"searchDepth":586,"depth":586,"links":4901},[4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912],{"id":4068,"depth":586,"text":4071},{"id":4132,"depth":586,"text":4135},{"id":4187,"depth":586,"text":4190},{"id":4327,"depth":586,"text":4330},{"id":4432,"depth":586,"text":4435},{"id":4452,"depth":586,"text":4455},{"id":4495,"depth":586,"text":4498},{"id":4572,"depth":586,"text":4575},{"id":4621,"depth":586,"text":4624},{"id":4664,"depth":586,"text":4667},{"id":4863,"depth":586,"text":4866},"content:blog:accessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue",{"_path":4917,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":4918,"description":4919,"date":4920,"readingTime":2960,"tags":4921,"featured":6,"body":4922,"_type":602,"_id":5376,"_source":604,"_file":5377,"_stem":5378,"_extension":607},"\u002Fblog\u002Fhow-to-structure-enterprise-vue-applications","How to Structure Enterprise Vue Applications: Scalable Folder Architecture","Architecture guide for large Vue applications. Learn folder structure, dependency injection, composables patterns, naming conventions, and scaling to 100K+ lines of code.","2024-05-05",[617,616],{"type":18,"children":4923,"toc":5364},[4924,4930,4935,4945,4953,4958,4968,4976,4981,4991,4999,5004,5010,5022,5030,5034,5091,5097,5102,5111,5116,5125,5130,5139,5145,5150,5159,5164,5173,5179,5184,5193,5197,5206,5212,5217,5226,5231,5237,5246,5252,5257,5265,5271,5276,5284,5290,5359],{"type":21,"tag":22,"props":4925,"children":4927},{"id":4926},"the-folder-structure-problem",[4928],{"type":27,"value":4929},"The Folder Structure Problem",{"type":21,"tag":30,"props":4931,"children":4932},{},[4933],{"type":27,"value":4934},"I've seen three patterns:",{"type":21,"tag":30,"props":4936,"children":4937},{},[4938,4943],{"type":21,"tag":36,"props":4939,"children":4940},{},[4941],{"type":27,"value":4942},"Chaos Model",{"type":27,"value":4944}," (what startups do):",{"type":21,"tag":76,"props":4946,"children":4948},{"code":4947},"src\u002F\n├── components\u002F\n│   ├── Button.vue\n│   ├── Modal.vue\n│   ├── UserCard.vue\n│   └── SomeRandomComponent.vue\n├── views\u002F\n│   ├── Home.vue\n│   └── Dashboard.vue\n└── utils.js\n",[4949],{"type":21,"tag":84,"props":4950,"children":4951},{"__ignoreMap":7},[4952],{"type":27,"value":4947},{"type":21,"tag":30,"props":4954,"children":4955},{},[4956],{"type":27,"value":4957},"After 6 months, nobody remembers where anything is.",{"type":21,"tag":30,"props":4959,"children":4960},{},[4961,4966],{"type":21,"tag":36,"props":4962,"children":4963},{},[4964],{"type":27,"value":4965},"Rails Model",{"type":27,"value":4967}," (what some teams copy):",{"type":21,"tag":76,"props":4969,"children":4971},{"code":4970},"src\u002F\n├── views\u002F\n├── components\u002F\n├── composables\u002F\n├── stores\u002F\n├── utils\u002F\n└── services\u002F\n",[4972],{"type":21,"tag":84,"props":4973,"children":4974},{"__ignoreMap":7},[4975],{"type":27,"value":4970},{"type":21,"tag":30,"props":4977,"children":4978},{},[4979],{"type":27,"value":4980},"Better, but still unclear what belongs where.",{"type":21,"tag":30,"props":4982,"children":4983},{},[4984,4989],{"type":21,"tag":36,"props":4985,"children":4986},{},[4987],{"type":27,"value":4988},"Domain-Driven Model",{"type":27,"value":4990}," (what scales):",{"type":21,"tag":76,"props":4992,"children":4994},{"code":4993},"src\u002F\n├── domains\u002F\n│   ├── users\u002F\n│   │   ├── components\u002F\n│   │   ├── composables\u002F\n│   │   ├── stores\u002F\n│   │   ├── types\u002F\n│   │   └── api.ts\n│   ├── billing\u002F\n│   └── products\u002F\n├── shared\u002F\n│   ├── components\u002F\n│   ├── composables\u002F\n│   ├── types\u002F\n│   └── utils\u002F\n└── app.vue\n",[4995],{"type":21,"tag":84,"props":4996,"children":4997},{"__ignoreMap":7},[4998],{"type":27,"value":4993},{"type":21,"tag":30,"props":5000,"children":5001},{},[5002],{"type":27,"value":5003},"We'll focus on the domain-driven model.",{"type":21,"tag":22,"props":5005,"children":5007},{"id":5006},"the-domain-driven-structure",[5008],{"type":27,"value":5009},"The Domain-Driven Structure",{"type":21,"tag":30,"props":5011,"children":5012},{},[5013,5015,5020],{"type":27,"value":5014},"Organize by ",{"type":21,"tag":36,"props":5016,"children":5017},{},[5018],{"type":27,"value":5019},"business domain",{"type":27,"value":5021},", not technical layer:",{"type":21,"tag":76,"props":5023,"children":5025},{"code":5024},"src\u002F\n├── domains\u002F\n│   │\n│   ├── users\u002F\n│   │   ├── api.ts              # API calls for users\n│   │   ├── types.ts            # User types (User, CreateUserDTO)\n│   │   ├── composables\u002F\n│   │   │   ├── useUser.ts      # Single user fetch\u002Fupdate\n│   │   │   ├── useUserList.ts  # List with pagination\n│   │   │   └── useAuth.ts      # Authentication\n│   │   ├── stores\u002F\n│   │   │   └── userStore.ts    # Pinia store\n│   │   ├── components\u002F\n│   │   │   ├── UserCard.vue\n│   │   │   ├── UserForm.vue\n│   │   │   └── UserAvatar.vue\n│   │   ├── pages\u002F\n│   │   │   ├── UserDetail.vue\n│   │   │   └── UserList.vue\n│   │   └── index.ts            # Public API\n│   │\n│   ├── billing\u002F\n│   │   ├── api.ts\n│   │   ├── types.ts            # Invoice, Subscription types\n│   │   ├── composables\u002F\n│   │   ├── stores\u002F\n│   │   ├── components\u002F\n│   │   ├── pages\u002F\n│   │   └── index.ts\n│   │\n│   └── products\u002F\n│       ├── api.ts\n│       ├── types.ts\n│       ├── composables\u002F\n│       ├── stores\u002F\n│       ├── components\u002F\n│       ├── pages\u002F\n│       └── index.ts\n│\n├── shared\u002F\n│   ├── components\u002F             # Reusable UI (Button, Modal, etc.)\n│   │   ├── Button.vue\n│   │   ├── Modal.vue\n│   │   └── Pagination.vue\n│   ├── composables\u002F            # Shared logic (useLocalStorage, etc.)\n│   │   ├── useFetch.ts\n│   │   ├── useLocalStorage.ts\n│   │   └── useDebounce.ts\n│   ├── types\u002F\n│   │   ├── index.ts\n│   │   └── api.ts\n│   ├── utils\u002F\n│   │   ├── formatters.ts\n│   │   ├── validators.ts\n│   │   └── helpers.ts\n│   ├── stores\u002F\n│   │   └── appStore.ts         # Global app state\n│   ├── middleware\u002F\n│   │   ├── auth.ts\n│   │   └── logging.ts\n│   └── index.ts                # Barrel export\n│\n├── router\u002F\n│   ├── index.ts\n│   ├── guards.ts\n│   └── routes.ts\n│\n├── styles\u002F\n│   ├── main.css\n│   ├── variables.css\n│   └── utilities.css\n│\n├── plugins\u002F\n│   └── index.ts\n│\n└── app.vue\n",[5026],{"type":21,"tag":84,"props":5027,"children":5028},{"__ignoreMap":7},[5029],{"type":27,"value":5024},{"type":21,"tag":30,"props":5031,"children":5032},{},[5033],{"type":27,"value":1695},{"type":21,"tag":94,"props":5035,"children":5036},{},[5037,5047,5071,5081],{"type":21,"tag":98,"props":5038,"children":5039},{},[5040,5045],{"type":21,"tag":36,"props":5041,"children":5042},{},[5043],{"type":27,"value":5044},"Feature cohesion",{"type":27,"value":5046}," — all code for \"users\" is in one place",{"type":21,"tag":98,"props":5048,"children":5049},{},[5050,5055,5057,5063,5065],{"type":21,"tag":36,"props":5051,"children":5052},{},[5053],{"type":27,"value":5054},"Independent shipping",{"type":27,"value":5056}," — one team owns ",{"type":21,"tag":84,"props":5058,"children":5060},{"className":5059},[],[5061],{"type":27,"value":5062},"domains\u002Fusers",{"type":27,"value":5064},", another owns ",{"type":21,"tag":84,"props":5066,"children":5068},{"className":5067},[],[5069],{"type":27,"value":5070},"domains\u002Fbilling",{"type":21,"tag":98,"props":5072,"children":5073},{},[5074,5079],{"type":21,"tag":36,"props":5075,"children":5076},{},[5077],{"type":27,"value":5078},"Clear dependencies",{"type":27,"value":5080}," — easy to see what each domain needs",{"type":21,"tag":98,"props":5082,"children":5083},{},[5084,5089],{"type":21,"tag":36,"props":5085,"children":5086},{},[5087],{"type":27,"value":5088},"Scaling",{"type":27,"value":5090}," — add new domains without touching existing ones",{"type":21,"tag":22,"props":5092,"children":5094},{"id":5093},"the-barrel-export-pattern",[5095],{"type":27,"value":5096},"The Barrel Export Pattern",{"type":21,"tag":30,"props":5098,"children":5099},{},[5100],{"type":27,"value":5101},"Each domain exports a public API:",{"type":21,"tag":76,"props":5103,"children":5106},{"code":5104,"language":144,"meta":7,"className":5105},"\u002F\u002F domains\u002Fusers\u002Findex.ts\nexport * from '.\u002Ftypes'\nexport { default as useUser } from '.\u002Fcomposables\u002FuseUser'\nexport { default as useUserList } from '.\u002Fcomposables\u002FuseUserList'\nexport { default as UserCard } from '.\u002Fcomponents\u002FUserCard.vue'\nexport { default as UserForm } from '.\u002Fcomponents\u002FUserForm.vue'\nexport { useUserStore } from '.\u002Fstores\u002FuserStore'\nexport * as userApi from '.\u002Fapi'\n",[142],[5107],{"type":21,"tag":84,"props":5108,"children":5109},{"__ignoreMap":7},[5110],{"type":27,"value":5104},{"type":21,"tag":30,"props":5112,"children":5113},{},[5114],{"type":27,"value":5115},"Usage:",{"type":21,"tag":76,"props":5117,"children":5120},{"code":5118,"language":144,"meta":7,"className":5119},"\u002F\u002F Importing from domain is clean\nimport { useUser, UserCard, userApi } from '@\u002Fdomains\u002Fusers'\n\n\u002F\u002F Instead of scattered imports\nimport useUser from '@\u002Fdomains\u002Fusers\u002Fcomposables\u002FuseUser'\nimport UserCard from '@\u002Fdomains\u002Fusers\u002Fcomponents\u002FUserCard.vue'\nimport * as userApi from '@\u002Fdomains\u002Fusers\u002Fapi'\n",[142],[5121],{"type":21,"tag":84,"props":5122,"children":5123},{"__ignoreMap":7},[5124],{"type":27,"value":5118},{"type":21,"tag":30,"props":5126,"children":5127},{},[5128],{"type":27,"value":5129},"Enforce this with eslint:",{"type":21,"tag":76,"props":5131,"children":5134},{"code":5132,"language":898,"meta":7,"className":5133},"\u002F\u002F .eslintrc.js\n{\n  rules: {\n    'import\u002Fno-restricted-paths': [\n      'error',\n      {\n        zones: [\n          {\n            target: '.\u002Fsrc\u002Fdomains\u002Fusers',\n            from: '.\u002Fsrc\u002Fdomains\u002Fbilling',\n            message: 'Users domain cannot import from Billing domain'\n          }\n        ]\n      }\n    ]\n  }\n}\n",[896],[5135],{"type":21,"tag":84,"props":5136,"children":5137},{"__ignoreMap":7},[5138],{"type":27,"value":5132},{"type":21,"tag":22,"props":5140,"children":5142},{"id":5141},"composables-the-shared-logic",[5143],{"type":27,"value":5144},"Composables: The Shared Logic",{"type":21,"tag":30,"props":5146,"children":5147},{},[5148],{"type":27,"value":5149},"Composables are where reusable logic lives:",{"type":21,"tag":76,"props":5151,"children":5154},{"code":5152,"language":144,"meta":7,"className":5153},"\u002F\u002F domains\u002Fusers\u002Fcomposables\u002FuseUser.ts\nimport { ref, computed } from 'vue'\nimport type { User } from '..\u002Ftypes'\nimport * as userApi from '..\u002Fapi'\n\nexport const useUser = (userId: string) => {\n  const user = ref\u003CUser | null>(null)\n  const loading = ref(false)\n  const error = ref\u003Cstring | null>(null)\n\n  const fetchUser = async () => {\n    loading.value = true\n    try {\n      user.value = await userApi.getUser(userId)\n      error.value = null\n    } catch (err) {\n      error.value = (err as Error).message\n      user.value = null\n    } finally {\n      loading.value = false\n    }\n  }\n\n  const updateUser = async (updates: Partial\u003CUser>) => {\n    try {\n      user.value = await userApi.updateUser(userId, updates)\n      error.value = null\n    } catch (err) {\n      error.value = (err as Error).message\n    }\n  }\n\n  onMounted(() => fetchUser())\n\n  return {\n    user: readonly(user),\n    loading: readonly(loading),\n    error: readonly(error),\n    fetchUser,\n    updateUser\n  }\n}\n",[142],[5155],{"type":21,"tag":84,"props":5156,"children":5157},{"__ignoreMap":7},[5158],{"type":27,"value":5152},{"type":21,"tag":30,"props":5160,"children":5161},{},[5162],{"type":27,"value":5163},"Use it:",{"type":21,"tag":76,"props":5165,"children":5168},{"code":5166,"language":194,"meta":7,"className":5167},"\u003Cscript setup lang=\"ts\">\nimport { useUser } from '@\u002Fdomains\u002Fusers'\n\nconst route = useRoute()\nconst { user, loading, error } = useUser(route.params.id)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"loading\">Loading...\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"error\">{{ error }}\u003C\u002Fdiv>\n  \u003Cdiv v-else>{{ user?.name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[192],[5169],{"type":21,"tag":84,"props":5170,"children":5171},{"__ignoreMap":7},[5172],{"type":27,"value":5166},{"type":21,"tag":22,"props":5174,"children":5176},{"id":5175},"dependency-injection",[5177],{"type":27,"value":5178},"Dependency Injection",{"type":21,"tag":30,"props":5180,"children":5181},{},[5182],{"type":27,"value":5183},"For large apps, inject dependencies instead of hardcoding:",{"type":21,"tag":76,"props":5185,"children":5188},{"code":5186,"language":144,"meta":7,"className":5187},"\u002F\u002F shared\u002Fservices\u002Fapi.ts\nclass ApiService {\n  private baseUrl: string\n\n  constructor(baseUrl: string) {\n    this.baseUrl = baseUrl\n  }\n\n  async get(path: string) {\n    return fetch(`${this.baseUrl}${path}`).then(r => r.json())\n  }\n}\n\n\u002F\u002F main.ts\nimport { createApp } from 'vue'\nimport ApiService from '.\u002Fshared\u002Fservices\u002Fapi'\n\nconst app = createApp(App)\n\napp.provide('api', new ApiService(import.meta.env.VITE_API_URL))\n\napp.mount('#app')\n",[142],[5189],{"type":21,"tag":84,"props":5190,"children":5191},{"__ignoreMap":7},[5192],{"type":27,"value":5186},{"type":21,"tag":30,"props":5194,"children":5195},{},[5196],{"type":27,"value":5163},{"type":21,"tag":76,"props":5198,"children":5201},{"code":5199,"language":144,"meta":7,"className":5200},"\u002F\u002F domains\u002Fusers\u002Fapi.ts\nimport { inject } from 'vue'\n\nexport const useUserApi = () => {\n  const api = inject\u003CApiService>('api')!\n  \n  return {\n    getUser: (id: string) => api.get(`\u002Fusers\u002F${id}`),\n    listUsers: () => api.get('\u002Fusers'),\n    createUser: (data: any) => api.post('\u002Fusers', data)\n  }\n}\n",[142],[5202],{"type":21,"tag":84,"props":5203,"children":5204},{"__ignoreMap":7},[5205],{"type":27,"value":5199},{"type":21,"tag":22,"props":5207,"children":5209},{"id":5208},"types-organization",[5210],{"type":27,"value":5211},"Types Organization",{"type":21,"tag":30,"props":5213,"children":5214},{},[5215],{"type":27,"value":5216},"Centralize types:",{"type":21,"tag":76,"props":5218,"children":5221},{"code":5219,"language":144,"meta":7,"className":5220},"\u002F\u002F domains\u002Fusers\u002Ftypes.ts\nexport interface User {\n  id: string\n  name: string\n  email: string\n  avatar?: string\n  role: 'user' | 'admin'\n  createdAt: string\n  updatedAt: string\n}\n\nexport interface CreateUserDTO {\n  name: string\n  email: string\n  password: string\n  role?: 'user' | 'admin'\n}\n\nexport interface UpdateUserDTO {\n  name?: string\n  email?: string\n  avatar?: string\n}\n\nexport interface UserListResponse {\n  data: User[]\n  total: number\n  page: number\n  pageSize: number\n}\n",[142],[5222],{"type":21,"tag":84,"props":5223,"children":5224},{"__ignoreMap":7},[5225],{"type":27,"value":5219},{"type":21,"tag":30,"props":5227,"children":5228},{},[5229],{"type":27,"value":5230},"Never use inline interfaces.",{"type":21,"tag":22,"props":5232,"children":5234},{"id":5233},"naming-conventions",[5235],{"type":27,"value":5236},"Naming Conventions",{"type":21,"tag":76,"props":5238,"children":5241},{"code":5239,"language":144,"meta":7,"className":5240},"\u002F\u002F Components: PascalCase\nUserCard.vue\nUserForm.vue\nCreateUserModal.vue\n\n\u002F\u002F Composables: useCamelCase\nuseUser.ts\nuseUserList.ts\nuseUserForm.ts\nuseDebounce.ts\n\n\u002F\u002F Stores: Store suffix\nuserStore.ts\nauthStore.ts\nappStore.ts\n\n\u002F\u002F API files: lowercase\napi.ts (or users-api.ts)\n\n\u002F\u002F Types: PascalCase\nUser.ts\nCreateUserDTO.ts\n\n\u002F\u002F Utils: camelCase, descriptive\nformatDate.ts\nvalidateEmail.ts\ncalculateDiscount.ts\n",[142],[5242],{"type":21,"tag":84,"props":5243,"children":5244},{"__ignoreMap":7},[5245],{"type":27,"value":5239},{"type":21,"tag":22,"props":5247,"children":5249},{"id":5248},"growing-the-structure",[5250],{"type":27,"value":5251},"Growing the Structure",{"type":21,"tag":30,"props":5253,"children":5254},{},[5255],{"type":27,"value":5256},"As you add more code, follow these patterns:",{"type":21,"tag":76,"props":5258,"children":5260},{"code":5259},"domains\u002Fusers\u002F (20 KB)\n  ├── api.ts\n  ├── types.ts\n  ├── composables\u002F (3 files)\n  ├── components\u002F (5 files)\n  └── pages\u002F (2 files)\n\n# When it gets too big (50+ KB), break it down:\n\ndomains\u002F\n├── users\u002F\n│   ├── list\u002F            # User listing feature\n│   ├── detail\u002F          # User detail\u002Fedit feature\n│   ├── auth\u002F            # Auth-specific logic\n│   └── shared\u002F          # Shared user types\u002Fapi\n",[5261],{"type":21,"tag":84,"props":5262,"children":5263},{"__ignoreMap":7},[5264],{"type":27,"value":5259},{"type":21,"tag":22,"props":5266,"children":5268},{"id":5267},"scaling-to-100k-lines",[5269],{"type":27,"value":5270},"Scaling to 100K+ Lines",{"type":21,"tag":30,"props":5272,"children":5273},{},[5274],{"type":27,"value":5275},"At scale, add:",{"type":21,"tag":76,"props":5277,"children":5279},{"code":5278},"src\u002F\n├── domains\u002F\n├── features\u002F            # Cross-domain workflows\n│   ├── onboarding\u002F      # Onboarding flow uses Users + Billing\n│   ├── migration\u002F       # Data migration uses multiple domains\n│   └── reporting\u002F\n├── shared\u002F\n├── router\u002F\n└── layouts\u002F             # Layout components\n",[5280],{"type":21,"tag":84,"props":5281,"children":5282},{"__ignoreMap":7},[5283],{"type":27,"value":5278},{"type":21,"tag":22,"props":5285,"children":5287},{"id":5286},"key-rules",[5288],{"type":27,"value":5289},"Key Rules",{"type":21,"tag":527,"props":5291,"children":5292},{},[5293,5303,5313,5323,5333,5349],{"type":21,"tag":98,"props":5294,"children":5295},{},[5296,5301],{"type":21,"tag":36,"props":5297,"children":5298},{},[5299],{"type":27,"value":5300},"Keep domains independent",{"type":27,"value":5302}," — Users domain can't import from Billing",{"type":21,"tag":98,"props":5304,"children":5305},{},[5306,5311],{"type":21,"tag":36,"props":5307,"children":5308},{},[5309],{"type":27,"value":5310},"Shared is truly shared",{"type":27,"value":5312}," — only reusable across domains",{"type":21,"tag":98,"props":5314,"children":5315},{},[5316,5321],{"type":21,"tag":36,"props":5317,"children":5318},{},[5319],{"type":27,"value":5320},"Use barrel exports",{"type":27,"value":5322}," — clean public APIs",{"type":21,"tag":98,"props":5324,"children":5325},{},[5326,5331],{"type":21,"tag":36,"props":5327,"children":5328},{},[5329],{"type":27,"value":5330},"Type everything",{"type":27,"value":5332}," — centralize types by domain",{"type":21,"tag":98,"props":5334,"children":5335},{},[5336,5341,5343],{"type":21,"tag":36,"props":5337,"children":5338},{},[5339],{"type":27,"value":5340},"One entry point",{"type":27,"value":5342}," — each domain exports from ",{"type":21,"tag":84,"props":5344,"children":5346},{"className":5345},[],[5347],{"type":27,"value":5348},"index.ts",{"type":21,"tag":98,"props":5350,"children":5351},{},[5352,5357],{"type":21,"tag":36,"props":5353,"children":5354},{},[5355],{"type":27,"value":5356},"Enforce with linting",{"type":27,"value":5358}," — catch violations early",{"type":21,"tag":30,"props":5360,"children":5361},{},[5362],{"type":27,"value":5363},"This structure scales from 10 engineers to 100. Your codebase will thank you.",{"title":7,"searchDepth":586,"depth":586,"links":5365},[5366,5367,5368,5369,5370,5371,5372,5373,5374,5375],{"id":4926,"depth":586,"text":4929},{"id":5006,"depth":586,"text":5009},{"id":5093,"depth":586,"text":5096},{"id":5141,"depth":586,"text":5144},{"id":5175,"depth":586,"text":5178},{"id":5208,"depth":586,"text":5211},{"id":5233,"depth":586,"text":5236},{"id":5248,"depth":586,"text":5251},{"id":5267,"depth":586,"text":5270},{"id":5286,"depth":586,"text":5289},"content:blog:how-to-structure-enterprise-vue-applications.md","blog\u002Fhow-to-structure-enterprise-vue-applications.md","blog\u002Fhow-to-structure-enterprise-vue-applications",{"_path":5380,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":5381,"description":5382,"date":5383,"updated":11,"readingTime":5384,"tags":5385,"featured":6,"body":5386,"_type":602,"_id":6110,"_source":604,"_file":6111,"_stem":6112,"_extension":607},"\u002Fblog\u002Freducing-bundle-size-in-large-vue-apps","Reducing Bundle Size in Large Vue Apps: A Strategic Guide","Strategic guide to bundle optimization. Learn tree-shaking, dynamic imports, dependency analysis, code splitting, and continuous monitoring.","2024-04-18",10,[15],{"type":18,"children":5387,"toc":6088},[5388,5394,5406,5418,5449,5454,5460,5465,5474,5479,5497,5502,5558,5564,5570,5579,5585,5594,5600,5609,5615,5624,5629,5647,5657,5663,5668,5677,5683,5692,5698,5707,5720,5725,5743,5749,5754,5763,5772,5777,5786,5791,5797,5803,5812,5818,5827,5833,5843,5938,5950,5968,5974,6027,6031,6083],{"type":21,"tag":22,"props":5389,"children":5391},{"id":5390},"the-bundle-problem",[5392],{"type":27,"value":5393},"The Bundle Problem",{"type":21,"tag":30,"props":5395,"children":5396},{},[5397,5399,5404],{"type":27,"value":5398},"Vue apps grow organically, and nobody watches the bundle until it hurts. Working on a clinical data platform, I shipped a bundle reduction of ",{"type":21,"tag":36,"props":5400,"children":5401},{},[5402],{"type":27,"value":5403},"35%",{"type":27,"value":5405}," using the approach in this guide — the strategy below is the repeatable part.",{"type":21,"tag":30,"props":5407,"children":5408},{},[5409,5411,5416],{"type":27,"value":5410},"To make the mechanics concrete, this guide walks through a representative large Vue app. ",{"type":21,"tag":36,"props":5412,"children":5413},{},[5414],{"type":27,"value":5415},"All specific sizes below are an illustrative example",{"type":27,"value":5417},", not measurements from one project:",{"type":21,"tag":94,"props":5419,"children":5420},{},[5421,5430,5439],{"type":21,"tag":98,"props":5422,"children":5423},{},[5424,5428],{"type":21,"tag":36,"props":5425,"children":5426},{},[5427],{"type":27,"value":105},{"type":27,"value":5429},": 850 KB (gzipped: 280 KB)",{"type":21,"tag":98,"props":5431,"children":5432},{},[5433,5437],{"type":21,"tag":36,"props":5434,"children":5435},{},[5436],{"type":27,"value":115},{"type":27,"value":5438},": 450 KB (gzipped: 150 KB)",{"type":21,"tag":98,"props":5440,"children":5441},{},[5442,5447],{"type":21,"tag":36,"props":5443,"children":5444},{},[5445],{"type":27,"value":5446},"Total",{"type":27,"value":5448},": 1.2 MB gzipped",{"type":21,"tag":30,"props":5450,"children":5451},{},[5452],{"type":27,"value":5453},"At 3G speeds (1 Mbps), that's 10+ seconds just for JavaScript.",{"type":21,"tag":22,"props":5455,"children":5457},{"id":5456},"step-1-analyze-the-damage",[5458],{"type":27,"value":5459},"Step 1: Analyze the Damage",{"type":21,"tag":30,"props":5461,"children":5462},{},[5463],{"type":27,"value":5464},"First, visualize where bytes are going:",{"type":21,"tag":76,"props":5466,"children":5469},{"className":5467,"code":5468,"language":81,"meta":7},[79],"npm install -D rollup-plugin-visualizer\n\n# vite.config.ts\nimport { visualizer } from 'rollup-plugin-visualizer'\n\nexport default {\n  plugins: [vue(), visualizer()]\n}\n\nnpm run build\n# Opens dist\u002Fstats.html — interactive bundle visualization\n",[5470],{"type":21,"tag":84,"props":5471,"children":5472},{"__ignoreMap":7},[5473],{"type":27,"value":5468},{"type":21,"tag":30,"props":5475,"children":5476},{},[5477],{"type":27,"value":5478},"You'll see:",{"type":21,"tag":94,"props":5480,"children":5481},{},[5482,5487,5492],{"type":21,"tag":98,"props":5483,"children":5484},{},[5485],{"type":27,"value":5486},"What packages are large",{"type":21,"tag":98,"props":5488,"children":5489},{},[5490],{"type":27,"value":5491},"What's imported but unused",{"type":21,"tag":98,"props":5493,"children":5494},{},[5495],{"type":27,"value":5496},"Duplicate dependencies at different versions",{"type":21,"tag":30,"props":5498,"children":5499},{},[5500],{"type":27,"value":5501},"A typical analysis reveals (illustrative example):",{"type":21,"tag":94,"props":5503,"children":5504},{},[5505,5516,5534,5545],{"type":21,"tag":98,"props":5506,"children":5507},{},[5508,5514],{"type":21,"tag":84,"props":5509,"children":5511},{"className":5510},[],[5512],{"type":27,"value":5513},"lodash-es",{"type":27,"value":5515},": 71 KB (for 3 functions actually used)",{"type":21,"tag":98,"props":5517,"children":5518},{},[5519,5525,5527,5533],{"type":21,"tag":84,"props":5520,"children":5522},{"className":5521},[],[5523],{"type":27,"value":5524},"moment",{"type":27,"value":5526},": 65 KB (replaced with ",{"type":21,"tag":84,"props":5528,"children":5530},{"className":5529},[],[5531],{"type":27,"value":5532},"date-fns",{"type":27,"value":4324},{"type":21,"tag":98,"props":5535,"children":5536},{},[5537,5543],{"type":21,"tag":84,"props":5538,"children":5540},{"className":5539},[],[5541],{"type":27,"value":5542},"chart.js",{"type":27,"value":5544},": 120 KB (only used on one page)",{"type":21,"tag":98,"props":5546,"children":5547},{},[5548,5550,5556],{"type":27,"value":5549},"Duplicate ",{"type":21,"tag":84,"props":5551,"children":5553},{"className":5552},[],[5554],{"type":27,"value":5555},"axios",{"type":27,"value":5557},": two versions imported by different packages",{"type":21,"tag":22,"props":5559,"children":5561},{"id":5560},"step-2-tree-shake-aggressively",[5562],{"type":27,"value":5563},"Step 2: Tree-Shake Aggressively",{"type":21,"tag":64,"props":5565,"children":5567},{"id":5566},"remove-unused-packages",[5568],{"type":27,"value":5569},"Remove Unused Packages",{"type":21,"tag":76,"props":5571,"children":5574},{"className":5572,"code":5573,"language":81,"meta":7},[79],"# Find unused dependencies\nnpm install -g depcheck\ndepcheck\n\n# Removes 50+ KB of dead code\nnpm uninstall unused-package\n",[5575],{"type":21,"tag":84,"props":5576,"children":5577},{"__ignoreMap":7},[5578],{"type":27,"value":5573},{"type":21,"tag":64,"props":5580,"children":5582},{"id":5581},"replace-heavy-packages",[5583],{"type":27,"value":5584},"Replace Heavy Packages",{"type":21,"tag":76,"props":5586,"children":5589},{"className":5587,"code":5588,"language":144,"meta":7},[142],"\u002F\u002F ❌ lodash-es (71 KB)\nimport { debounce, throttle, cloneDeep } from 'lodash-es'\n\n\u002F\u002F ✅ Use native JavaScript + small libraries\n\u002F\u002F Debounce (use native setTimeout)\nconst debounce = (fn: Function, delay: number) => {\n  let timeout: NodeJS.Timeout\n  return (...args: any[]) => {\n    clearTimeout(timeout)\n    timeout = setTimeout(() => fn(...args), delay)\n  }\n}\n\n\u002F\u002F Or use a lighter library\nimport { debounce } from 'radash' \u002F\u002F 8 KB vs 71 KB\n",[5590],{"type":21,"tag":84,"props":5591,"children":5592},{"__ignoreMap":7},[5593],{"type":27,"value":5588},{"type":21,"tag":64,"props":5595,"children":5597},{"id":5596},"dependency-consolidation",[5598],{"type":27,"value":5599},"Dependency Consolidation",{"type":21,"tag":76,"props":5601,"children":5604},{"className":5602,"code":5603,"language":144,"meta":7},[142],"\u002F\u002F ❌ Multiple date libraries\nimport { format } from 'date-fns'\nimport moment from 'moment' \u002F\u002F Someone else's code\nimport dayjs from 'dayjs' \u002F\u002F Yet another\n\n\u002F\u002F ✅ Choose one\nimport { format } from 'date-fns'\n\u002F\u002F date-fns: 30 KB (tree-shakeable) vs moment: 65 KB (not tree-shakeable)\n",[5605],{"type":21,"tag":84,"props":5606,"children":5607},{"__ignoreMap":7},[5608],{"type":27,"value":5603},{"type":21,"tag":64,"props":5610,"children":5612},{"id":5611},"build-config-for-tree-shaking",[5613],{"type":27,"value":5614},"Build Config for Tree-Shaking",{"type":21,"tag":76,"props":5616,"children":5619},{"className":5617,"code":5618,"language":144,"meta":7},[142],"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  build: {\n    minify: 'terser',\n    terserOptions: {\n      compress: {\n        drop_console: true,\n        drop_debugger: true\n      }\n    },\n    rollupOptions: {\n      output: {\n        \u002F\u002F Manual code splitting\n        manualChunks: {\n          \u002F\u002F Separate large dependencies\n          'charts': ['chart.js', 'vue-chartjs'],\n          'markdown': ['markdown-it', 'highlight.js'],\n          'utils': ['lodash', 'date-fns'],\n          'vendor': ['vue', 'vue-router', 'pinia']\n        }\n      }\n    }\n  }\n})\n",[5620],{"type":21,"tag":84,"props":5621,"children":5622},{"__ignoreMap":7},[5623],{"type":27,"value":5618},{"type":21,"tag":30,"props":5625,"children":5626},{},[5627],{"type":27,"value":5628},"Results for the illustrative example app:",{"type":21,"tag":94,"props":5630,"children":5631},{},[5632,5637,5642],{"type":21,"tag":98,"props":5633,"children":5634},{},[5635],{"type":27,"value":5636},"Removed unused: -150 KB",{"type":21,"tag":98,"props":5638,"children":5639},{},[5640],{"type":27,"value":5641},"Replaced heavy packages: -120 KB",{"type":21,"tag":98,"props":5643,"children":5644},{},[5645],{"type":27,"value":5646},"Tree-shaking optimized: -40 KB",{"type":21,"tag":30,"props":5648,"children":5649},{},[5650,5655],{"type":21,"tag":36,"props":5651,"children":5652},{},[5653],{"type":27,"value":5654},"Total: -310 KB (27% reduction)",{"type":27,"value":5656}," — again, illustrative; the ratio between the three buckets is what generalizes.",{"type":21,"tag":22,"props":5658,"children":5660},{"id":5659},"step-3-smart-code-splitting",[5661],{"type":27,"value":5662},"Step 3: Smart Code Splitting",{"type":21,"tag":30,"props":5664,"children":5665},{},[5666],{"type":27,"value":5667},"Load code only when needed:",{"type":21,"tag":76,"props":5669,"children":5672},{"className":5670,"code":5671,"language":144,"meta":7},[142],"\u002F\u002F ❌ All routes imported upfront\nimport Home from '@\u002Fpages\u002FHome.vue'\nimport Dashboard from '@\u002Fpages\u002FDashboard.vue'\nimport Settings from '@\u002Fpages\u002FSettings.vue'\nimport Admin from '@\u002Fpages\u002FAdmin.vue'\n\nconst routes = [\n  { path: '\u002F', component: Home },\n  { path: '\u002Fdashboard', component: Dashboard },\n  { path: '\u002Fsettings', component: Settings },\n  { path: '\u002Fadmin', component: Admin }\n]\n\n\u002F\u002F ✅ Dynamic imports for routes\nconst routes = [\n  { path: '\u002F', component: () => import('@\u002Fpages\u002FHome.vue') },\n  { path: '\u002Fdashboard', component: () => import('@\u002Fpages\u002FDashboard.vue') },\n  { path: '\u002Fsettings', component: () => import('@\u002Fpages\u002FSettings.vue') },\n  { path: '\u002Fadmin', component: () => import('@\u002Fpages\u002FAdmin.vue') }\n]\n",[5673],{"type":21,"tag":84,"props":5674,"children":5675},{"__ignoreMap":7},[5676],{"type":27,"value":5671},{"type":21,"tag":64,"props":5678,"children":5680},{"id":5679},"split-heavy-features",[5681],{"type":27,"value":5682},"Split Heavy Features",{"type":21,"tag":76,"props":5684,"children":5687},{"className":5685,"code":5686,"language":144,"meta":7},[142],"\u002F\u002F ❌ Import editor everywhere (even if not used)\nimport RichEditor from '@\u002Fcomponents\u002FRichEditor.vue'\n\n\u002F\u002F ✅ Async only when needed\nconst RichEditor = defineAsyncComponent(\n  () => import('@\u002Fcomponents\u002FRichEditor.vue')\n)\n",[5688],{"type":21,"tag":84,"props":5689,"children":5690},{"__ignoreMap":7},[5691],{"type":27,"value":5686},{"type":21,"tag":64,"props":5693,"children":5695},{"id":5694},"webpack-magic-comment",[5696],{"type":27,"value":5697},"Webpack Magic Comment",{"type":21,"tag":76,"props":5699,"children":5702},{"className":5700,"code":5701,"language":144,"meta":7},[142],"\u002F\u002F Name the chunk for better debugging\nconst Dashboard = () =>\n  import(\n    \u002F* webpackChunkName: \"dashboard\" *\u002F\n    '@\u002Fpages\u002FDashboard.vue'\n  )\n",[5703],{"type":21,"tag":84,"props":5704,"children":5705},{"__ignoreMap":7},[5706],{"type":27,"value":5701},{"type":21,"tag":30,"props":5708,"children":5709},{},[5710,5712,5718],{"type":27,"value":5711},"Generates: ",{"type":21,"tag":84,"props":5713,"children":5715},{"className":5714},[],[5716],{"type":27,"value":5717},"dist\u002Fdashboard.abc123.js",{"type":27,"value":5719}," (clear what it contains)",{"type":21,"tag":30,"props":5721,"children":5722},{},[5723],{"type":27,"value":5724},"Results of code splitting (illustrative example):",{"type":21,"tag":94,"props":5726,"children":5727},{},[5728,5733,5738],{"type":21,"tag":98,"props":5729,"children":5730},{},[5731],{"type":27,"value":5732},"The main bundle shrinks to what the first route actually needs",{"type":21,"tag":98,"props":5734,"children":5735},{},[5736],{"type":27,"value":5737},"Lazy routes load in small on-demand chunks",{"type":21,"tag":98,"props":5739,"children":5740},{},[5741],{"type":27,"value":5742},"First page load stops paying for pages the user never visits",{"type":21,"tag":22,"props":5744,"children":5746},{"id":5745},"step-4-monitor-continuously",[5747],{"type":27,"value":5748},"Step 4: Monitor Continuously",{"type":21,"tag":30,"props":5750,"children":5751},{},[5752],{"type":27,"value":5753},"Prevent regressions:",{"type":21,"tag":76,"props":5755,"children":5758},{"className":5756,"code":5757,"language":81,"meta":7},[79],"npm install -D bundlesize\n",[5759],{"type":21,"tag":84,"props":5760,"children":5761},{"__ignoreMap":7},[5762],{"type":27,"value":5757},{"type":21,"tag":76,"props":5764,"children":5767},{"className":5765,"code":5766,"language":1962,"meta":7},[1964],"\u002F\u002F .bundlesize.json\n{\n  \"files\": [\n    {\n      \"path\": \".\u002Fdist\u002Findex.*.js\",\n      \"maxSize\": \"250kb\"\n    },\n    {\n      \"path\": \".\u002Fdist\u002F*.js\",\n      \"maxSize\": \"100kb\"\n    }\n  ]\n}\n",[5768],{"type":21,"tag":84,"props":5769,"children":5770},{"__ignoreMap":7},[5771],{"type":27,"value":5766},{"type":21,"tag":30,"props":5773,"children":5774},{},[5775],{"type":27,"value":5776},"GitHub Action:",{"type":21,"tag":76,"props":5778,"children":5781},{"className":5779,"code":5780,"language":3751,"meta":7},[3753],"# .github\u002Fworkflows\u002Fbundle-size.yml\nname: Bundle Size Check\n\non: [pull_request]\n\njobs:\n  bundle:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - run: npm ci\n      - run: npm run build\n      - uses: bundlesize\u002Faction@v1\n        with:\n          files: .\u002Fdist\u002F**\u002F*.js\n          builtFile: dist\n",[5782],{"type":21,"tag":84,"props":5783,"children":5784},{"__ignoreMap":7},[5785],{"type":27,"value":5780},{"type":21,"tag":30,"props":5787,"children":5788},{},[5789],{"type":27,"value":5790},"Fails PR if bundles exceed limits.",{"type":21,"tag":22,"props":5792,"children":5794},{"id":5793},"step-5-optimize-imports",[5795],{"type":27,"value":5796},"Step 5: Optimize Imports",{"type":21,"tag":64,"props":5798,"children":5800},{"id":5799},"named-vs-default-exports",[5801],{"type":27,"value":5802},"Named vs Default Exports",{"type":21,"tag":76,"props":5804,"children":5807},{"className":5805,"code":5806,"language":144,"meta":7},[142],"\u002F\u002F ❌ Default exports (can't tree-shake)\nexport default { formatDate: ... }\nimport * as dateUtils from 'lib'\n\n\u002F\u002F ✅ Named exports (tree-shakeable)\nexport { formatDate, formatTime }\nimport { formatDate } from 'lib'\n",[5808],{"type":21,"tag":84,"props":5809,"children":5810},{"__ignoreMap":7},[5811],{"type":27,"value":5806},{"type":21,"tag":64,"props":5813,"children":5815},{"id":5814},"avoid-wildcard-imports",[5816],{"type":27,"value":5817},"Avoid Wildcard Imports",{"type":21,"tag":76,"props":5819,"children":5822},{"className":5820,"code":5821,"language":144,"meta":7},[142],"\u002F\u002F ❌ Imports everything\nimport * as utils from '@\u002Futils'\nutils.formatDate()\n\n\u002F\u002F ✅ Import only what's needed\nimport { formatDate } from '@\u002Futils'\nformatDate()\n",[5823],{"type":21,"tag":84,"props":5824,"children":5825},{"__ignoreMap":7},[5826],{"type":27,"value":5821},{"type":21,"tag":22,"props":5828,"children":5830},{"id":5829},"results-ongoing-monitoring",[5831],{"type":27,"value":5832},"Results & Ongoing Monitoring",{"type":21,"tag":30,"props":5834,"children":5835},{},[5836,5841],{"type":21,"tag":36,"props":5837,"children":5838},{},[5839],{"type":27,"value":5840},"Before & after for the illustrative example app",{"type":27,"value":5842}," (your ratios will vary; the direction won't):",{"type":21,"tag":403,"props":5844,"children":5845},{},[5846,5868],{"type":21,"tag":407,"props":5847,"children":5848},{},[5849],{"type":21,"tag":411,"props":5850,"children":5851},{},[5852,5856,5860,5864],{"type":21,"tag":415,"props":5853,"children":5854},{},[5855],{"type":27,"value":419},{"type":21,"tag":415,"props":5857,"children":5858},{},[5859],{"type":27,"value":424},{"type":21,"tag":415,"props":5861,"children":5862},{},[5863],{"type":27,"value":429},{"type":21,"tag":415,"props":5865,"children":5866},{},[5867],{"type":27,"value":434},{"type":21,"tag":436,"props":5869,"children":5870},{},[5871,5894,5916],{"type":21,"tag":411,"props":5872,"children":5873},{},[5874,5879,5884,5889],{"type":21,"tag":443,"props":5875,"children":5876},{},[5877],{"type":27,"value":5878},"Main Bundle",{"type":21,"tag":443,"props":5880,"children":5881},{},[5882],{"type":27,"value":5883},"850 KB",{"type":21,"tag":443,"props":5885,"children":5886},{},[5887],{"type":27,"value":5888},"~300 KB",{"type":21,"tag":443,"props":5890,"children":5891},{},[5892],{"type":27,"value":5893},"large",{"type":21,"tag":411,"props":5895,"children":5896},{},[5897,5902,5907,5912],{"type":21,"tag":443,"props":5898,"children":5899},{},[5900],{"type":27,"value":5901},"Vendor Bundle",{"type":21,"tag":443,"props":5903,"children":5904},{},[5905],{"type":27,"value":5906},"450 KB",{"type":21,"tag":443,"props":5908,"children":5909},{},[5910],{"type":27,"value":5911},"~180 KB",{"type":21,"tag":443,"props":5913,"children":5914},{},[5915],{"type":27,"value":5893},{"type":21,"tag":411,"props":5917,"children":5918},{},[5919,5924,5929,5934],{"type":21,"tag":443,"props":5920,"children":5921},{},[5922],{"type":27,"value":5923},"First Paint (3G)",{"type":21,"tag":443,"props":5925,"children":5926},{},[5927],{"type":27,"value":5928},"~8s",{"type":21,"tag":443,"props":5930,"children":5931},{},[5932],{"type":27,"value":5933},"~2s",{"type":21,"tag":443,"props":5935,"children":5936},{},[5937],{"type":27,"value":5893},{"type":21,"tag":30,"props":5939,"children":5940},{},[5941,5943,5948],{"type":27,"value":5942},"In my own production use of this strategy, the verified outcome was a ",{"type":21,"tag":36,"props":5944,"children":5945},{},[5946],{"type":27,"value":5947},"35% bundle reduction",{"type":27,"value":5949},". Beyond raw size:",{"type":21,"tag":94,"props":5951,"children":5952},{},[5953,5958,5963],{"type":21,"tag":98,"props":5954,"children":5955},{},[5956],{"type":27,"value":5957},"Faster for all users, especially mobile",{"type":21,"tag":98,"props":5959,"children":5960},{},[5961],{"type":27,"value":5962},"Better SEO (Core Web Vitals improvement)",{"type":21,"tag":98,"props":5964,"children":5965},{},[5966],{"type":27,"value":5967},"Less battery drain on mobile",{"type":21,"tag":22,"props":5969,"children":5971},{"id":5970},"ongoing-strategy",[5972],{"type":27,"value":5973},"Ongoing Strategy",{"type":21,"tag":527,"props":5975,"children":5976},{},[5977,5987,5997,6007,6017],{"type":21,"tag":98,"props":5978,"children":5979},{},[5980,5985],{"type":21,"tag":36,"props":5981,"children":5982},{},[5983],{"type":27,"value":5984},"Weekly monitoring",{"type":27,"value":5986}," — bundlesize checks every PR",{"type":21,"tag":98,"props":5988,"children":5989},{},[5990,5995],{"type":21,"tag":36,"props":5991,"children":5992},{},[5993],{"type":27,"value":5994},"Quarterly audits",{"type":27,"value":5996}," — analyze with visualizer",{"type":21,"tag":98,"props":5998,"children":5999},{},[6000,6005],{"type":21,"tag":36,"props":6001,"children":6002},{},[6003],{"type":27,"value":6004},"Department rotation",{"type":27,"value":6006}," — one engineer owns bundle each month",{"type":21,"tag":98,"props":6008,"children":6009},{},[6010,6015],{"type":21,"tag":36,"props":6011,"children":6012},{},[6013],{"type":27,"value":6014},"Team education",{"type":27,"value":6016}," — awareness of bundle impact",{"type":21,"tag":98,"props":6018,"children":6019},{},[6020,6025],{"type":21,"tag":36,"props":6021,"children":6022},{},[6023],{"type":27,"value":6024},"Dependency reviews",{"type":27,"value":6026}," — approve new packages by size",{"type":21,"tag":22,"props":6028,"children":6029},{"id":3976},[6030],{"type":27,"value":3979},{"type":21,"tag":527,"props":6032,"children":6033},{},[6034,6044,6054,6064,6074],{"type":21,"tag":98,"props":6035,"children":6036},{},[6037,6042],{"type":21,"tag":36,"props":6038,"children":6039},{},[6040],{"type":27,"value":6041},"Lazy load routes by default",{"type":27,"value":6043}," — all routes should be dynamic",{"type":21,"tag":98,"props":6045,"children":6046},{},[6047,6052],{"type":21,"tag":36,"props":6048,"children":6049},{},[6050],{"type":27,"value":6051},"Split by feature",{"type":27,"value":6053}," — heavy features in separate chunks",{"type":21,"tag":98,"props":6055,"children":6056},{},[6057,6062],{"type":21,"tag":36,"props":6058,"children":6059},{},[6060],{"type":27,"value":6061},"Use es2015 module syntax",{"type":27,"value":6063}," — enables tree-shaking",{"type":21,"tag":98,"props":6065,"children":6066},{},[6067,6072],{"type":21,"tag":36,"props":6068,"children":6069},{},[6070],{"type":27,"value":6071},"Compress with Brotli",{"type":27,"value":6073}," — 15-20% smaller than gzip",{"type":21,"tag":98,"props":6075,"children":6076},{},[6077,6081],{"type":21,"tag":36,"props":6078,"children":6079},{},[6080],{"type":27,"value":577},{"type":27,"value":6082}," — real user metrics matter most",{"type":21,"tag":30,"props":6084,"children":6085},{},[6086],{"type":27,"value":6087},"Bundle size affects every user, every time. It's worth optimizing.",{"title":7,"searchDepth":586,"depth":586,"links":6089},[6090,6091,6092,6098,6102,6103,6107,6108,6109],{"id":5390,"depth":586,"text":5393},{"id":5456,"depth":586,"text":5459},{"id":5560,"depth":586,"text":5563,"children":6093},[6094,6095,6096,6097],{"id":5566,"depth":592,"text":5569},{"id":5581,"depth":592,"text":5584},{"id":5596,"depth":592,"text":5599},{"id":5611,"depth":592,"text":5614},{"id":5659,"depth":586,"text":5662,"children":6099},[6100,6101],{"id":5679,"depth":592,"text":5682},{"id":5694,"depth":592,"text":5697},{"id":5745,"depth":586,"text":5748},{"id":5793,"depth":586,"text":5796,"children":6104},[6105,6106],{"id":5799,"depth":592,"text":5802},{"id":5814,"depth":592,"text":5817},{"id":5829,"depth":586,"text":5832},{"id":5970,"depth":586,"text":5973},{"id":3976,"depth":586,"text":3979},"content:blog:reducing-bundle-size-in-large-vue-apps.md","blog\u002Freducing-bundle-size-in-large-vue-apps.md","blog\u002Freducing-bundle-size-in-large-vue-apps",{"_path":6114,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":6115,"description":6116,"date":6117,"readingTime":5384,"tags":6118,"featured":6,"body":6121,"_type":602,"_id":6738,"_source":604,"_file":6739,"_stem":6740,"_extension":607},"\u002Fblog\u002Fremote-frontend-engineering-best-practices","Remote Frontend Engineering Best Practices: Async-First, Documented, Autonomous","Guide to being an effective remote frontend engineer. Covers async communication, documentation, code review culture, timezone management, and team autonomy principles.","2024-04-01",[6119,6120],"Remote","Culture",{"type":18,"children":6122,"toc":6713},[6123,6129,6134,6139,6161,6171,6177,6183,6188,6196,6201,6209,6214,6267,6273,6278,6286,6291,6297,6302,6307,6317,6322,6328,6333,6339,6348,6353,6359,6368,6374,6383,6389,6398,6404,6422,6428,6433,6441,6446,6451,6456,6461,6469,6474,6480,6485,6491,6496,6504,6510,6518,6524,6533,6539,6547,6553,6558,6611,6617,6622,6664,6670,6680,6685,6708],{"type":21,"tag":22,"props":6124,"children":6126},{"id":6125},"the-remote-frontend-engineer",[6127],{"type":27,"value":6128},"The Remote Frontend Engineer",{"type":21,"tag":30,"props":6130,"children":6131},{},[6132],{"type":27,"value":6133},"Remote work is now standard. Being a remote engineer requires a different mindset than co-located work.",{"type":21,"tag":30,"props":6135,"children":6136},{},[6137],{"type":27,"value":6138},"The difference:",{"type":21,"tag":94,"props":6140,"children":6141},{},[6142,6152],{"type":21,"tag":98,"props":6143,"children":6144},{},[6145,6150],{"type":21,"tag":36,"props":6146,"children":6147},{},[6148],{"type":27,"value":6149},"Co-located",{"type":27,"value":6151},": \"I'll ask Bob on Slack\"",{"type":21,"tag":98,"props":6153,"children":6154},{},[6155,6159],{"type":21,"tag":36,"props":6156,"children":6157},{},[6158],{"type":27,"value":6119},{"type":27,"value":6160},": \"Bob is asleep; I'll find the answer in docs\"",{"type":21,"tag":30,"props":6162,"children":6163},{},[6164,6166],{"type":27,"value":6165},"The principle: ",{"type":21,"tag":36,"props":6167,"children":6168},{},[6169],{"type":27,"value":6170},"Assume your teammates are asleep, in a meeting, or handling an emergency.",{"type":21,"tag":22,"props":6172,"children":6174},{"id":6173},"async-first-communication",[6175],{"type":27,"value":6176},"Async-First Communication",{"type":21,"tag":64,"props":6178,"children":6180},{"id":6179},"write-instead-of-chat",[6181],{"type":27,"value":6182},"Write Instead of Chat",{"type":21,"tag":30,"props":6184,"children":6185},{},[6186],{"type":27,"value":6187},"Bad:",{"type":21,"tag":76,"props":6189,"children":6191},{"code":6190},"You: hey are you around?\nYou: quick question about the Button component\nYou: the disabled state styling?\nYou: is it supposed to have opacity or a different color?\n(Bob sees messages 4 hours later, types a 2-word answer)\n",[6192],{"type":21,"tag":84,"props":6193,"children":6194},{"__ignoreMap":7},[6195],{"type":27,"value":6190},{"type":21,"tag":30,"props":6197,"children":6198},{},[6199],{"type":27,"value":6200},"Good:",{"type":21,"tag":76,"props":6202,"children":6204},{"code":6203},"Slack message in #frontend-design-system:\n\"Question about Button.vue disabled state:\n- Current: opacity: 0.5\n- Proposal: color: var(--color-disabled)\n- Context: new accessibility requirement\n- Decision needed by EOD\nReference: #PR-1234\"\n(Bob reads full context, gives detailed answer with reasoning)\n",[6205],{"type":21,"tag":84,"props":6206,"children":6207},{"__ignoreMap":7},[6208],{"type":27,"value":6203},{"type":21,"tag":30,"props":6210,"children":6211},{},[6212],{"type":27,"value":6213},"Include:",{"type":21,"tag":94,"props":6215,"children":6216},{},[6217,6227,6237,6247,6257],{"type":21,"tag":98,"props":6218,"children":6219},{},[6220,6225],{"type":21,"tag":36,"props":6221,"children":6222},{},[6223],{"type":27,"value":6224},"Context",{"type":27,"value":6226}," — what you're trying to do",{"type":21,"tag":98,"props":6228,"children":6229},{},[6230,6235],{"type":21,"tag":36,"props":6231,"children":6232},{},[6233],{"type":27,"value":6234},"What you've tried",{"type":27,"value":6236}," — don't ask before researching",{"type":21,"tag":98,"props":6238,"children":6239},{},[6240,6245],{"type":21,"tag":36,"props":6241,"children":6242},{},[6243],{"type":27,"value":6244},"Specific question",{"type":27,"value":6246}," — not \"does this work?\"",{"type":21,"tag":98,"props":6248,"children":6249},{},[6250,6255],{"type":21,"tag":36,"props":6251,"children":6252},{},[6253],{"type":27,"value":6254},"Deadline",{"type":27,"value":6256}," — when you need the answer",{"type":21,"tag":98,"props":6258,"children":6259},{},[6260,6265],{"type":21,"tag":36,"props":6261,"children":6262},{},[6263],{"type":27,"value":6264},"Links",{"type":27,"value":6266}," — PR\u002Fissue references",{"type":21,"tag":64,"props":6268,"children":6270},{"id":6269},"use-threads",[6271],{"type":27,"value":6272},"Use Threads",{"type":21,"tag":30,"props":6274,"children":6275},{},[6276],{"type":27,"value":6277},"Slack threading keeps conversations organized:",{"type":21,"tag":76,"props":6279,"children":6281},{"code":6280},"Message in #frontend channel:\n\"Deploying new design tokens to production EOD\"\n\nReplies (threaded, not cluttering main channel):\n- Person A: \"Looks good, tested locally\"\n- Person B: \"Breaking changes documented?\"\n- Person A: \"Yes, migration guide in PR\"\n",[6282],{"type":21,"tag":84,"props":6283,"children":6284},{"__ignoreMap":7},[6285],{"type":27,"value":6280},{"type":21,"tag":30,"props":6287,"children":6288},{},[6289],{"type":27,"value":6290},"Main channel stays clean. Interested people can see the discussion.",{"type":21,"tag":64,"props":6292,"children":6294},{"id":6293},"status-updates-not-standups",[6295],{"type":27,"value":6296},"Status Updates (Not Standups)",{"type":21,"tag":30,"props":6298,"children":6299},{},[6300],{"type":27,"value":6301},"Bad: Synchronous standup at 9am (3am for India team)",{"type":21,"tag":30,"props":6303,"children":6304},{},[6305],{"type":27,"value":6306},"Good: Async status in shared doc",{"type":21,"tag":76,"props":6308,"children":6312},{"code":6309,"language":602,"meta":7,"className":6310},"## Frontend Team Status — Week of May 15\n\n### Alice (Timeline: US Pacific)\n- **Done**: Button component refactor, 12 tests passing\n- **Doing**: Design token migration (ETA: Wed)\n- **Blocked**: Need approval from Design on color palette changes\n- **Time**: 9am-6pm PT, can overlap Tue\u002FWed\u002FThu with Europe\n\n### Bob (Timeline: Europe CET)\n- **Done**: Accessibility audit of forms, 8 WCAG violations fixed\n- **Doing**: Modal component docs\n- **Blocked**: Waiting for Alice's Button refactor to merge\n- **Time**: 9am-5pm CET, overlaps 12pm-5pm PT with Alice\n\n### Charlie (Timeline: India IST)\n- **Done**: Setup new Vue 3 project, 4 components created\n- **Doing**: Migration docs\n- **Blocked**: Need styling guidelines from Design\n- **Time**: 10am-6pm IST, overlaps Tue\u002FWed with Bob\n",[6311],"language-markdown",[6313],{"type":21,"tag":84,"props":6314,"children":6315},{"__ignoreMap":7},[6316],{"type":27,"value":6309},{"type":21,"tag":30,"props":6318,"children":6319},{},[6320],{"type":27,"value":6321},"This works for everyone, any timezone.",{"type":21,"tag":22,"props":6323,"children":6325},{"id":6324},"documentation-is-law",[6326],{"type":27,"value":6327},"Documentation Is Law",{"type":21,"tag":30,"props":6329,"children":6330},{},[6331],{"type":27,"value":6332},"Remote engineers live in documentation.",{"type":21,"tag":64,"props":6334,"children":6336},{"id":6335},"architecture-decisions",[6337],{"type":27,"value":6338},"Architecture Decisions",{"type":21,"tag":76,"props":6340,"children":6343},{"code":6341,"language":602,"meta":7,"className":6342},"## ADR-12: Design System Versioning\n\n### Context\nWe have 5 products using our design system.\nDifferent teams want to update components at different rates.\nShared state causes delays.\n\n### Decision\nUse semantic versioning with 30-day support windows.\n- Major version: Breaking changes (1 month notice)\n- Minor version: New features (backcompat)\n- Patch version: Bug fixes (auto-deploy)\n\n### Consequences\n- Positive: Teams can update independently\n- Positive: Clear upgrade path\n- Negative: Need to backport critical bugs for 30 days\n- Negative: More coordination needed\n\n### Approved by\n- Design lead (2024-03-15)\n- Frontend lead (2024-03-15)\n",[6311],[6344],{"type":21,"tag":84,"props":6345,"children":6346},{"__ignoreMap":7},[6347],{"type":27,"value":6341},{"type":21,"tag":30,"props":6349,"children":6350},{},[6351],{"type":27,"value":6352},"Store in shared repo, searchable, link from PRs.",{"type":21,"tag":64,"props":6354,"children":6356},{"id":6355},"component-documentation",[6357],{"type":27,"value":6358},"Component Documentation",{"type":21,"tag":76,"props":6360,"children":6363},{"code":6361,"language":144,"meta":7,"className":6362},"\u002F\u002F Button.vue\n\u002F**\n * Universal button component\n * \n * @component\n * @example\n * \u003CButton variant=\"primary\" size=\"lg\">Click me\u003C\u002FButton>\n * \n * @prop {string} variant - \"primary\" | \"secondary\" | \"danger\"\n * @prop {string} size - \"sm\" | \"md\" | \"lg\"\n * @prop {boolean} disabled - Disable the button\n * @prop {boolean} loading - Show loading state\n * \n * @emits click - Emitted when clicked\n * \n * @accessibility\n * - Keyboard accessible (Tab to focus, Enter\u002FSpace to activate)\n * - Aria-label for icon-only buttons\n * - Focus indicator visible (outline: 2px solid)\n * - Disabled state properly conveyed to screen readers\n *\u002F\n",[142],[6364],{"type":21,"tag":84,"props":6365,"children":6366},{"__ignoreMap":7},[6367],{"type":27,"value":6361},{"type":21,"tag":64,"props":6369,"children":6371},{"id":6370},"api-documentation",[6372],{"type":27,"value":6373},"API Documentation",{"type":21,"tag":76,"props":6375,"children":6378},{"code":6376,"language":602,"meta":7,"className":6377},"## POST \u002Fapi\u002Fusers\n\nCreate a new user.\n\n### Request\n```json\n{\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"role\": \"user\" \u002F\u002F optional, defaults to \"user\"\n}\n",[6311],[6379],{"type":21,"tag":84,"props":6380,"children":6381},{"__ignoreMap":7},[6382],{"type":27,"value":6376},{"type":21,"tag":64,"props":6384,"children":6386},{"id":6385},"response",[6387],{"type":27,"value":6388},"Response",{"type":21,"tag":76,"props":6390,"children":6393},{"code":6391,"language":1962,"meta":7,"className":6392},"{\n  \"id\": \"user_123\",\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"role\": \"user\",\n  \"createdAt\": \"2024-04-01T10:00:00Z\"\n}\n",[1964],[6394],{"type":21,"tag":84,"props":6395,"children":6396},{"__ignoreMap":7},[6397],{"type":27,"value":6391},{"type":21,"tag":64,"props":6399,"children":6401},{"id":6400},"errors",[6402],{"type":27,"value":6403},"Errors",{"type":21,"tag":94,"props":6405,"children":6406},{},[6407,6412,6417],{"type":21,"tag":98,"props":6408,"children":6409},{},[6410],{"type":27,"value":6411},"400: Invalid email format",{"type":21,"tag":98,"props":6413,"children":6414},{},[6415],{"type":27,"value":6416},"409: Email already exists",{"type":21,"tag":98,"props":6418,"children":6419},{},[6420],{"type":27,"value":6421},"422: Missing required fields",{"type":21,"tag":64,"props":6423,"children":6425},{"id":6424},"rate-limit",[6426],{"type":27,"value":6427},"Rate Limit",{"type":21,"tag":30,"props":6429,"children":6430},{},[6431],{"type":27,"value":6432},"100 requests per minute per API key",{"type":21,"tag":76,"props":6434,"children":6436},{"code":6435},"\n## Code Review Culture\n\nRemote code reviews must be thorough and asynchronous.\n\n### Good Code Review Comment\n\n",[6437],{"type":21,"tag":84,"props":6438,"children":6439},{"__ignoreMap":7},[6440],{"type":27,"value":6435},{"type":21,"tag":30,"props":6442,"children":6443},{},[6444],{"type":27,"value":6445},"\u002F\u002F ❌ Vague\n\"This looks off\"",{"type":21,"tag":30,"props":6447,"children":6448},{},[6449],{"type":27,"value":6450},"\u002F\u002F ✅ Specific\n\"Line 45: The loading state here doesn't clear after success.\nIf the request succeeds but then fails, isLoading is true forever.\nSuggest wrapping the entire try\u002Ffinally block:",{"type":21,"tag":30,"props":6452,"children":6453},{},[6454],{"type":27,"value":6455},"try {\nisLoading.value = true\nresponse = await api.create(data)\n\u002F\u002F handle success\n} finally {\nisLoading.value = false  \u002F\u002F Always runs\n}",{"type":21,"tag":30,"props":6457,"children":6458},{},[6459],{"type":27,"value":6460},"See similar pattern in useUser.ts line 32\"",{"type":21,"tag":76,"props":6462,"children":6464},{"code":6463},"\nInclude:\n- Line number\n- What's wrong\n- Why it's wrong\n- Suggested fix\n- Reference to similar code\n\n### Review Checklist\n\n```markdown\n## Code Review Checklist\n\n- [ ] Does the code match the PR description?\n- [ ] Is there test coverage?\n- [ ] Are there console.logs or debugging code left?\n- [ ] Does this follow our naming conventions?\n- [ ] Is the TypeScript correct (no `any`)?\n- [ ] Are error cases handled?\n- [ ] Does this need documentation?\n- [ ] Is accessibility considered?\n- [ ] Will this cause performance regressions?\n- [ ] Does this have security implications?\n",[6465],{"type":21,"tag":84,"props":6466,"children":6467},{"__ignoreMap":7},[6468],{"type":27,"value":6463},{"type":21,"tag":30,"props":6470,"children":6471},{},[6472],{"type":27,"value":6473},"Use it for every PR.",{"type":21,"tag":22,"props":6475,"children":6477},{"id":6476},"timezone-management",[6478],{"type":27,"value":6479},"Timezone Management",{"type":21,"tag":30,"props":6481,"children":6482},{},[6483],{"type":27,"value":6484},"Working across timezones requires structure:",{"type":21,"tag":64,"props":6486,"children":6488},{"id":6487},"core-collaboration-hours",[6489],{"type":27,"value":6490},"Core Collaboration Hours",{"type":21,"tag":30,"props":6492,"children":6493},{},[6494],{"type":27,"value":6495},"Define overlapping times when synchronous work happens:",{"type":21,"tag":76,"props":6497,"children":6499},{"code":6498},"Team spans: IST (UTC+5:30), CET (UTC+1), PST (UTC-8)\n\nOverlap windows:\n- IST + CET: 10am-1pm IST = 4:30am-7:30am CET ❌ (too early)\n- CET + PST: 12pm-5pm CET = 3am-8am PST ❌ (too early)\n- IST + PST: No overlap ❌\n\nSolution: Async-first, with scheduled 1-on-1s\n\nMonday: IST + CET overlap (2-hour sync)\nWednesday: CET + PST overlap (2-hour sync)\nFriday: Full team async, no meetings\n",[6500],{"type":21,"tag":84,"props":6501,"children":6502},{"__ignoreMap":7},[6503],{"type":27,"value":6498},{"type":21,"tag":64,"props":6505,"children":6507},{"id":6506},"meeting-best-practices",[6508],{"type":27,"value":6509},"Meeting Best Practices",{"type":21,"tag":76,"props":6511,"children":6513},{"code":6512},"❌ 9am PT (5pm CET, 1:30am IST)\n  - Impossible for India team\n\n✅ 3pm PT (12am CET, 10:30pm IST)\n  - Morning for EU, evening for India, working hours for US\n\n❌ Monthly recurring at same time\n  - Someone is always inconvenienced\n\n✅ Rotate meeting times monthly\n  - Different teams take the inconvenient slot each month\n  - Everyone shares the burden\n",[6514],{"type":21,"tag":84,"props":6515,"children":6516},{"__ignoreMap":7},[6517],{"type":27,"value":6512},{"type":21,"tag":64,"props":6519,"children":6521},{"id":6520},"communication-norms",[6522],{"type":27,"value":6523},"Communication Norms",{"type":21,"tag":76,"props":6525,"children":6528},{"code":6526,"language":602,"meta":7,"className":6527},"## Communication SLA\n\n### Chat (Slack)\n- Team questions: 24-hour response\n- Feature requests: 48-hour response\n- Blocking issues: 2-hour response if during working hours\n\n### PRs\n- Approval: 24 hours\n- Blocking feedback: 48 hours\n\n### Urgent\n- Use @channel sparingly (maybe once\u002Fmonth)\n- Use if production is down or major deadline at risk\n- Include: What's broken, who's addressing it, ETA\n",[6311],[6529],{"type":21,"tag":84,"props":6530,"children":6531},{"__ignoreMap":7},[6532],{"type":27,"value":6526},{"type":21,"tag":22,"props":6534,"children":6536},{"id":6535},"tools-that-work-remote",[6537],{"type":27,"value":6538},"Tools That Work Remote",{"type":21,"tag":76,"props":6540,"children":6542},{"code":6541},"✅ Async Decision Making\n- GitHub issues for RFCs (Requests for Comments)\n- Decision deadline in issue\n- Link from PRs\n\n✅ Asynchronous Testing\n- CI\u002FCD that runs before merge\n- Shared staging environment (no manual testing)\n- Performance benchmarks automated\n\n✅ Knowledge Sharing\n- Video recordings of tech talks (async-watchable)\n- Loom videos for complex explanations\n- Recorded demos of features\n\n✅ Code Ownership\n- CODEOWNERS file in repo\n- Auto-assigned reviewers\n- Clear escalation path\n",[6543],{"type":21,"tag":84,"props":6544,"children":6545},{"__ignoreMap":7},[6546],{"type":27,"value":6541},{"type":21,"tag":22,"props":6548,"children":6550},{"id":6549},"remote-interview-tips-meta",[6551],{"type":27,"value":6552},"Remote Interview Tips (Meta)",{"type":21,"tag":30,"props":6554,"children":6555},{},[6556],{"type":27,"value":6557},"You're hiring a remote engineer. Watch for:",{"type":21,"tag":527,"props":6559,"children":6560},{},[6561,6571,6581,6591,6601],{"type":21,"tag":98,"props":6562,"children":6563},{},[6564,6569],{"type":21,"tag":36,"props":6565,"children":6566},{},[6567],{"type":27,"value":6568},"Written communication",{"type":27,"value":6570}," — can they explain clearly?",{"type":21,"tag":98,"props":6572,"children":6573},{},[6574,6579],{"type":21,"tag":36,"props":6575,"children":6576},{},[6577],{"type":27,"value":6578},"Self-motivation",{"type":27,"value":6580}," — do they ask good questions during prep?",{"type":21,"tag":98,"props":6582,"children":6583},{},[6584,6589],{"type":21,"tag":36,"props":6585,"children":6586},{},[6587],{"type":27,"value":6588},"Timezone awareness",{"type":27,"value":6590}," — do they mention working across timezones?",{"type":21,"tag":98,"props":6592,"children":6593},{},[6594,6599],{"type":21,"tag":36,"props":6595,"children":6596},{},[6597],{"type":27,"value":6598},"Documentation review",{"type":27,"value":6600}," — did they read our docs before interview?",{"type":21,"tag":98,"props":6602,"children":6603},{},[6604,6609],{"type":21,"tag":36,"props":6605,"children":6606},{},[6607],{"type":27,"value":6608},"Async examples",{"type":27,"value":6610}," — how have they worked async before?",{"type":21,"tag":22,"props":6612,"children":6614},{"id":6613},"results-of-good-remote-practices",[6615],{"type":27,"value":6616},"Results of Good Remote Practices",{"type":21,"tag":30,"props":6618,"children":6619},{},[6620],{"type":27,"value":6621},"After adopting these practices:",{"type":21,"tag":94,"props":6623,"children":6624},{},[6625,6635,6644,6654],{"type":21,"tag":98,"props":6626,"children":6627},{},[6628,6633],{"type":21,"tag":36,"props":6629,"children":6630},{},[6631],{"type":27,"value":6632},"Decision speed",{"type":27,"value":6634},": 2 days → 24 hours (async docs > meetings)",{"type":21,"tag":98,"props":6636,"children":6637},{},[6638,6642],{"type":21,"tag":36,"props":6639,"children":6640},{},[6641],{"type":27,"value":2919},{"type":27,"value":6643},": 4 weeks → 1.5 weeks (docs are source of truth)",{"type":21,"tag":98,"props":6645,"children":6646},{},[6647,6652],{"type":21,"tag":36,"props":6648,"children":6649},{},[6650],{"type":27,"value":6651},"Bug reports",{"type":27,"value":6653},": 4\u002Fweek → 1\u002Fweek (context is clear)",{"type":21,"tag":98,"props":6655,"children":6656},{},[6657,6662],{"type":21,"tag":36,"props":6658,"children":6659},{},[6660],{"type":27,"value":6661},"Developer satisfaction",{"type":27,"value":6663},": 7\u002F10 → 9\u002F10 (no sync meeting fatigue)",{"type":21,"tag":22,"props":6665,"children":6667},{"id":6666},"the-secret",[6668],{"type":27,"value":6669},"The Secret",{"type":21,"tag":30,"props":6671,"children":6672},{},[6673,6675],{"type":27,"value":6674},"Remote work isn't about Zoom calls. It's about ",{"type":21,"tag":36,"props":6676,"children":6677},{},[6678],{"type":27,"value":6679},"clear documentation, trust, and autonomy.",{"type":21,"tag":30,"props":6681,"children":6682},{},[6683],{"type":27,"value":6684},"Engineers who can work async:",{"type":21,"tag":94,"props":6686,"children":6687},{},[6688,6693,6698,6703],{"type":21,"tag":98,"props":6689,"children":6690},{},[6691],{"type":27,"value":6692},"Ship faster",{"type":21,"tag":98,"props":6694,"children":6695},{},[6696],{"type":27,"value":6697},"Need less supervision",{"type":21,"tag":98,"props":6699,"children":6700},{},[6701],{"type":27,"value":6702},"Solve problems independently",{"type":21,"tag":98,"props":6704,"children":6705},{},[6706],{"type":27,"value":6707},"Get better sleep (no midnight calls)",{"type":21,"tag":30,"props":6709,"children":6710},{},[6711],{"type":27,"value":6712},"The best remote engineers aren't chatty. They're clear writers.",{"title":7,"searchDepth":586,"depth":586,"links":6714},[6715,6716,6721,6729,6734,6735,6736,6737],{"id":6125,"depth":586,"text":6128},{"id":6173,"depth":586,"text":6176,"children":6717},[6718,6719,6720],{"id":6179,"depth":592,"text":6182},{"id":6269,"depth":592,"text":6272},{"id":6293,"depth":592,"text":6296},{"id":6324,"depth":586,"text":6327,"children":6722},[6723,6724,6725,6726,6727,6728],{"id":6335,"depth":592,"text":6338},{"id":6355,"depth":592,"text":6358},{"id":6370,"depth":592,"text":6373},{"id":6385,"depth":592,"text":6388},{"id":6400,"depth":592,"text":6403},{"id":6424,"depth":592,"text":6427},{"id":6476,"depth":586,"text":6479,"children":6730},[6731,6732,6733],{"id":6487,"depth":592,"text":6490},{"id":6506,"depth":592,"text":6509},{"id":6520,"depth":592,"text":6523},{"id":6535,"depth":586,"text":6538},{"id":6549,"depth":586,"text":6552},{"id":6613,"depth":586,"text":6616},{"id":6666,"depth":586,"text":6669},"content:blog:remote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":11,"readingTime":12,"tags":6742,"featured":16,"body":6743,"_type":602,"_id":603,"_source":604,"_file":605,"_stem":606,"_extension":607},[14,15],{"type":18,"children":6744,"toc":7180},[6745,6749,6758,6767,6771,6775,6779,6787,6791,6818,6822,6826,6834,6842,6857,6861,6865,6873,6877,6885,6895,6903,6911,6915,6919,6927,6931,6935,6939,6947,6956,6964,6968,6972,6976,6984,6988,7003,7007,7011,7015,7023,7027,7031,7035,7085,7094,7098,7102,7110,7114,7129,7133,7176],{"type":21,"tag":22,"props":6746,"children":6747},{"id":24},[6748],{"type":27,"value":28},{"type":21,"tag":30,"props":6750,"children":6751},{},[6752,6753,6757],{"type":27,"value":34},{"type":21,"tag":36,"props":6754,"children":6755},{},[6756],{"type":27,"value":40},{"type":27,"value":42},{"type":21,"tag":30,"props":6759,"children":6760},{},[6761,6762,6766],{"type":27,"value":47},{"type":21,"tag":49,"props":6763,"children":6764},{"href":51},[6765],{"type":27,"value":54},{"type":27,"value":56},{"type":21,"tag":22,"props":6768,"children":6769},{"id":59},[6770],{"type":27,"value":62},{"type":21,"tag":64,"props":6772,"children":6773},{"id":66},[6774],{"type":27,"value":69},{"type":21,"tag":30,"props":6776,"children":6777},{},[6778],{"type":27,"value":74},{"type":21,"tag":76,"props":6780,"children":6782},{"className":6781,"code":80,"language":81,"meta":7},[79],[6783],{"type":21,"tag":84,"props":6784,"children":6785},{"__ignoreMap":7},[6786],{"type":27,"value":80},{"type":21,"tag":30,"props":6788,"children":6789},{},[6790],{"type":27,"value":92},{"type":21,"tag":94,"props":6792,"children":6793},{},[6794,6802,6810],{"type":21,"tag":98,"props":6795,"children":6796},{},[6797,6801],{"type":21,"tag":36,"props":6798,"children":6799},{},[6800],{"type":27,"value":105},{"type":27,"value":107},{"type":21,"tag":98,"props":6803,"children":6804},{},[6805,6809],{"type":21,"tag":36,"props":6806,"children":6807},{},[6808],{"type":27,"value":115},{"type":27,"value":117},{"type":21,"tag":98,"props":6811,"children":6812},{},[6813,6817],{"type":21,"tag":36,"props":6814,"children":6815},{},[6816],{"type":27,"value":125},{"type":27,"value":127},{"type":21,"tag":64,"props":6819,"children":6820},{"id":130},[6821],{"type":27,"value":133},{"type":21,"tag":30,"props":6823,"children":6824},{},[6825],{"type":27,"value":138},{"type":21,"tag":76,"props":6827,"children":6829},{"className":6828,"code":143,"language":144,"meta":7},[142],[6830],{"type":21,"tag":84,"props":6831,"children":6832},{"__ignoreMap":7},[6833],{"type":27,"value":143},{"type":21,"tag":30,"props":6835,"children":6836},{},[6837,6841],{"type":21,"tag":36,"props":6838,"children":6839},{},[6840],{"type":27,"value":157},{"type":27,"value":159},{"type":21,"tag":94,"props":6843,"children":6844},{},[6845,6849,6853],{"type":21,"tag":98,"props":6846,"children":6847},{},[6848],{"type":27,"value":167},{"type":21,"tag":98,"props":6850,"children":6851},{},[6852],{"type":27,"value":172},{"type":21,"tag":98,"props":6854,"children":6855},{},[6856],{"type":27,"value":177},{"type":21,"tag":64,"props":6858,"children":6859},{"id":180},[6860],{"type":27,"value":183},{"type":21,"tag":30,"props":6862,"children":6863},{},[6864],{"type":27,"value":188},{"type":21,"tag":76,"props":6866,"children":6868},{"className":6867,"code":193,"language":194,"meta":7},[192],[6869],{"type":21,"tag":84,"props":6870,"children":6871},{"__ignoreMap":7},[6872],{"type":27,"value":193},{"type":21,"tag":30,"props":6874,"children":6875},{},[6876],{"type":27,"value":204},{"type":21,"tag":76,"props":6878,"children":6880},{"className":6879,"code":208,"language":81,"meta":7},[79],[6881],{"type":21,"tag":84,"props":6882,"children":6883},{"__ignoreMap":7},[6884],{"type":27,"value":208},{"type":21,"tag":30,"props":6886,"children":6887},{},[6888,6889,6894],{"type":27,"value":218},{"type":21,"tag":84,"props":6890,"children":6892},{"className":6891},[],[6893],{"type":27,"value":224},{"type":27,"value":226},{"type":21,"tag":76,"props":6896,"children":6898},{"className":6897,"code":230,"language":144,"meta":7},[142],[6899],{"type":21,"tag":84,"props":6900,"children":6901},{"__ignoreMap":7},[6902],{"type":27,"value":230},{"type":21,"tag":30,"props":6904,"children":6905},{},[6906,6910],{"type":21,"tag":36,"props":6907,"children":6908},{},[6909],{"type":27,"value":243},{"type":27,"value":245},{"type":21,"tag":64,"props":6912,"children":6913},{"id":248},[6914],{"type":27,"value":251},{"type":21,"tag":30,"props":6916,"children":6917},{},[6918],{"type":27,"value":256},{"type":21,"tag":76,"props":6920,"children":6922},{"className":6921,"code":260,"language":144,"meta":7},[142],[6923],{"type":21,"tag":84,"props":6924,"children":6925},{"__ignoreMap":7},[6926],{"type":27,"value":260},{"type":21,"tag":30,"props":6928,"children":6929},{},[6930],{"type":27,"value":270},{"type":21,"tag":64,"props":6932,"children":6933},{"id":273},[6934],{"type":27,"value":276},{"type":21,"tag":30,"props":6936,"children":6937},{},[6938],{"type":27,"value":281},{"type":21,"tag":76,"props":6940,"children":6942},{"className":6941,"code":285,"language":144,"meta":7},[142],[6943],{"type":21,"tag":84,"props":6944,"children":6945},{"__ignoreMap":7},[6946],{"type":27,"value":285},{"type":21,"tag":30,"props":6948,"children":6949},{},[6950,6951,6955],{"type":27,"value":295},{"type":21,"tag":36,"props":6952,"children":6953},{},[6954],{"type":27,"value":300},{"type":27,"value":226},{"type":21,"tag":76,"props":6957,"children":6959},{"className":6958,"code":306,"language":307,"meta":7},[305],[6960],{"type":21,"tag":84,"props":6961,"children":6962},{"__ignoreMap":7},[6963],{"type":27,"value":306},{"type":21,"tag":30,"props":6965,"children":6966},{},[6967],{"type":27,"value":317},{"type":21,"tag":64,"props":6969,"children":6970},{"id":320},[6971],{"type":27,"value":323},{"type":21,"tag":30,"props":6973,"children":6974},{},[6975],{"type":27,"value":328},{"type":21,"tag":76,"props":6977,"children":6979},{"className":6978,"code":332,"language":194,"meta":7},[192],[6980],{"type":21,"tag":84,"props":6981,"children":6982},{"__ignoreMap":7},[6983],{"type":27,"value":332},{"type":21,"tag":30,"props":6985,"children":6986},{},[6987],{"type":27,"value":342},{"type":21,"tag":94,"props":6989,"children":6990},{},[6991,6995,6999],{"type":21,"tag":98,"props":6992,"children":6993},{},[6994],{"type":27,"value":350},{"type":21,"tag":98,"props":6996,"children":6997},{},[6998],{"type":27,"value":355},{"type":21,"tag":98,"props":7000,"children":7001},{},[7002],{"type":27,"value":360},{"type":21,"tag":30,"props":7004,"children":7005},{},[7006],{"type":27,"value":365},{"type":21,"tag":64,"props":7008,"children":7009},{"id":368},[7010],{"type":27,"value":371},{"type":21,"tag":30,"props":7012,"children":7013},{},[7014],{"type":27,"value":376},{"type":21,"tag":76,"props":7016,"children":7018},{"className":7017,"code":380,"language":144,"meta":7},[142],[7019],{"type":21,"tag":84,"props":7020,"children":7021},{"__ignoreMap":7},[7022],{"type":27,"value":380},{"type":21,"tag":30,"props":7024,"children":7025},{},[7026],{"type":27,"value":390},{"type":21,"tag":22,"props":7028,"children":7029},{"id":393},[7030],{"type":27,"value":396},{"type":21,"tag":30,"props":7032,"children":7033},{},[7034],{"type":27,"value":401},{"type":21,"tag":403,"props":7036,"children":7037},{},[7038,7060],{"type":21,"tag":407,"props":7039,"children":7040},{},[7041],{"type":21,"tag":411,"props":7042,"children":7043},{},[7044,7048,7052,7056],{"type":21,"tag":415,"props":7045,"children":7046},{},[7047],{"type":27,"value":419},{"type":21,"tag":415,"props":7049,"children":7050},{},[7051],{"type":27,"value":424},{"type":21,"tag":415,"props":7053,"children":7054},{},[7055],{"type":27,"value":429},{"type":21,"tag":415,"props":7057,"children":7058},{},[7059],{"type":27,"value":434},{"type":21,"tag":436,"props":7061,"children":7062},{},[7063],{"type":21,"tag":411,"props":7064,"children":7065},{},[7066,7070,7074,7078],{"type":21,"tag":443,"props":7067,"children":7068},{},[7069],{"type":27,"value":447},{"type":21,"tag":443,"props":7071,"children":7072},{},[7073],{"type":27,"value":40},{"type":21,"tag":443,"props":7075,"children":7076},{},[7077],{"type":27,"value":456},{"type":21,"tag":443,"props":7079,"children":7080},{},[7081],{"type":21,"tag":36,"props":7082,"children":7083},{},[7084],{"type":27,"value":464},{"type":21,"tag":30,"props":7086,"children":7087},{},[7088,7089,7093],{"type":27,"value":469},{"type":21,"tag":49,"props":7090,"children":7091},{"href":51},[7092],{"type":27,"value":474},{"type":27,"value":476},{"type":21,"tag":22,"props":7095,"children":7096},{"id":479},[7097],{"type":27,"value":482},{"type":21,"tag":30,"props":7099,"children":7100},{},[7101],{"type":27,"value":487},{"type":21,"tag":76,"props":7103,"children":7105},{"className":7104,"code":491,"language":81,"meta":7},[79],[7106],{"type":21,"tag":84,"props":7107,"children":7108},{"__ignoreMap":7},[7109],{"type":27,"value":491},{"type":21,"tag":30,"props":7111,"children":7112},{},[7113],{"type":27,"value":501},{"type":21,"tag":94,"props":7115,"children":7116},{},[7117,7121,7125],{"type":21,"tag":98,"props":7118,"children":7119},{},[7120],{"type":27,"value":509},{"type":21,"tag":98,"props":7122,"children":7123},{},[7124],{"type":27,"value":514},{"type":21,"tag":98,"props":7126,"children":7127},{},[7128],{"type":27,"value":519},{"type":21,"tag":22,"props":7130,"children":7131},{"id":522},[7132],{"type":27,"value":525},{"type":21,"tag":527,"props":7134,"children":7135},{},[7136,7144,7152,7160,7168],{"type":21,"tag":98,"props":7137,"children":7138},{},[7139,7143],{"type":21,"tag":36,"props":7140,"children":7141},{},[7142],{"type":27,"value":537},{"type":27,"value":539},{"type":21,"tag":98,"props":7145,"children":7146},{},[7147,7151],{"type":21,"tag":36,"props":7148,"children":7149},{},[7150],{"type":27,"value":547},{"type":27,"value":549},{"type":21,"tag":98,"props":7153,"children":7154},{},[7155,7159],{"type":21,"tag":36,"props":7156,"children":7157},{},[7158],{"type":27,"value":557},{"type":27,"value":559},{"type":21,"tag":98,"props":7161,"children":7162},{},[7163,7167],{"type":21,"tag":36,"props":7164,"children":7165},{},[7166],{"type":27,"value":567},{"type":27,"value":569},{"type":21,"tag":98,"props":7169,"children":7170},{},[7171,7175],{"type":21,"tag":36,"props":7172,"children":7173},{},[7174],{"type":27,"value":577},{"type":27,"value":579},{"type":21,"tag":30,"props":7177,"children":7178},{},[7179],{"type":27,"value":584},{"title":7,"searchDepth":586,"depth":586,"links":7181},[7182,7183,7192,7193,7194],{"id":24,"depth":586,"text":28},{"id":59,"depth":586,"text":62,"children":7184},[7185,7186,7187,7188,7189,7190,7191],{"id":66,"depth":592,"text":69},{"id":130,"depth":592,"text":133},{"id":180,"depth":592,"text":183},{"id":248,"depth":592,"text":251},{"id":273,"depth":592,"text":276},{"id":320,"depth":592,"text":323},{"id":368,"depth":592,"text":371},{"id":393,"depth":586,"text":396},{"id":479,"depth":586,"text":482},{"id":522,"depth":586,"text":525},1783235260544]