[{"data":1,"prerenderedAt":6287},["ShallowReactive",2],{"blog-posts":3},[4,889,1326,1873,2558,3365,3828,4390,5124,5752],{"_path":5,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":9,"description":10,"date":11,"readingTime":12,"tags":13,"featured":7,"body":16,"_type":883,"_id":884,"_source":885,"_file":886,"_stem":887,"_extension":888},"\u002Fblog\u002Faccessibility-engineering-in-vue","blog",false,"","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",13,[14,15],"Accessibility","Vue",{"type":17,"children":18,"toc":869},"root",[19,28,34,39,85,90,96,101,134,145,151,156,169,174,289,295,300,309,314,394,400,405,414,420,425,434,439,457,463,468,477,482,534,540,549,554,583,589,594,605,616,621,630,636,641,830,836,841,864],{"type":20,"tag":21,"props":22,"children":24},"element","h2",{"id":23},"why-accessibility-matters",[25],{"type":26,"value":27},"text","Why Accessibility Matters",{"type":20,"tag":29,"props":30,"children":31},"p",{},[32],{"type":26,"value":33},"15% of the global population has disabilities. That's 1 billion people.",{"type":20,"tag":29,"props":35,"children":36},{},[37],{"type":26,"value":38},"For businesses:",{"type":20,"tag":40,"props":41,"children":42},"ul",{},[43,55,65,75],{"type":20,"tag":44,"props":45,"children":46},"li",{},[47,53],{"type":20,"tag":48,"props":49,"children":50},"strong",{},[51],{"type":26,"value":52},"Legal",{"type":26,"value":54},": WCAG compliance required in many jurisdictions (US, UK, EU)",{"type":20,"tag":44,"props":56,"children":57},{},[58,63],{"type":20,"tag":48,"props":59,"children":60},{},[61],{"type":26,"value":62},"Market",{"type":26,"value":64},": Accessible sites reach 1 billion+ users",{"type":20,"tag":44,"props":66,"children":67},{},[68,73],{"type":20,"tag":48,"props":69,"children":70},{},[71],{"type":26,"value":72},"SEO",{"type":26,"value":74},": Google rewards accessible sites",{"type":20,"tag":44,"props":76,"children":77},{},[78,83],{"type":20,"tag":48,"props":79,"children":80},{},[81],{"type":26,"value":82},"UX",{"type":26,"value":84},": Good accessibility is good UX for everyone (captions help in noisy environments)",{"type":20,"tag":29,"props":86,"children":87},{},[88],{"type":26,"value":89},"Accessibility isn't a feature. It's a requirement.",{"type":20,"tag":21,"props":91,"children":93},{"id":92},"web-content-accessibility-guidelines-wcag-21",[94],{"type":26,"value":95},"Web Content Accessibility Guidelines (WCAG) 2.1",{"type":20,"tag":29,"props":97,"children":98},{},[99],{"type":26,"value":100},"The standard has three levels:",{"type":20,"tag":40,"props":102,"children":103},{},[104,114,124],{"type":20,"tag":44,"props":105,"children":106},{},[107,112],{"type":20,"tag":48,"props":108,"children":109},{},[110],{"type":26,"value":111},"A",{"type":26,"value":113},": Minimum compliance",{"type":20,"tag":44,"props":115,"children":116},{},[117,122],{"type":20,"tag":48,"props":118,"children":119},{},[120],{"type":26,"value":121},"AA",{"type":26,"value":123},": Recommended (what most sites aim for)",{"type":20,"tag":44,"props":125,"children":126},{},[127,132],{"type":20,"tag":48,"props":128,"children":129},{},[130],{"type":26,"value":131},"AAA",{"type":26,"value":133},": Enhanced (nice to have)",{"type":20,"tag":29,"props":135,"children":136},{},[137,139,143],{"type":26,"value":138},"We'll focus on ",{"type":20,"tag":48,"props":140,"children":141},{},[142],{"type":26,"value":121},{"type":26,"value":144},", the legal standard in most places.",{"type":20,"tag":21,"props":146,"children":148},{"id":147},"semantic-html",[149],{"type":26,"value":150},"Semantic HTML",{"type":20,"tag":29,"props":152,"children":153},{},[154],{"type":26,"value":155},"The foundation of accessibility:",{"type":20,"tag":157,"props":158,"children":163},"pre",{"className":159,"code":161,"language":162,"meta":8},[160],"language-vue","\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","vue",[164],{"type":20,"tag":165,"props":166,"children":167},"code",{"__ignoreMap":8},[168],{"type":26,"value":161},{"type":20,"tag":29,"props":170,"children":171},{},[172],{"type":26,"value":173},"Semantic elements:",{"type":20,"tag":40,"props":175,"children":176},{},[177,222,241,252,263],{"type":20,"tag":44,"props":178,"children":179},{},[180,186,188,194,195,201,202,208,209,215,216],{"type":20,"tag":165,"props":181,"children":183},{"className":182},[],[184],{"type":26,"value":185},"\u003Cheader>",{"type":26,"value":187},", ",{"type":20,"tag":165,"props":189,"children":191},{"className":190},[],[192],{"type":26,"value":193},"\u003Cnav>",{"type":26,"value":187},{"type":20,"tag":165,"props":196,"children":198},{"className":197},[],[199],{"type":26,"value":200},"\u003Cmain>",{"type":26,"value":187},{"type":20,"tag":165,"props":203,"children":205},{"className":204},[],[206],{"type":26,"value":207},"\u003Carticle>",{"type":26,"value":187},{"type":20,"tag":165,"props":210,"children":212},{"className":211},[],[213],{"type":26,"value":214},"\u003Csection>",{"type":26,"value":187},{"type":20,"tag":165,"props":217,"children":219},{"className":218},[],[220],{"type":26,"value":221},"\u003Cfooter>",{"type":20,"tag":44,"props":223,"children":224},{},[225,231,233,239],{"type":20,"tag":165,"props":226,"children":228},{"className":227},[],[229],{"type":26,"value":230},"\u003Ch1>",{"type":26,"value":232}," - ",{"type":20,"tag":165,"props":234,"children":236},{"className":235},[],[237],{"type":26,"value":238},"\u003Ch6>",{"type":26,"value":240}," for headings (never skip levels)",{"type":20,"tag":44,"props":242,"children":243},{},[244,250],{"type":20,"tag":165,"props":245,"children":247},{"className":246},[],[248],{"type":26,"value":249},"\u003Cbutton>",{"type":26,"value":251}," for interactive elements",{"type":20,"tag":44,"props":253,"children":254},{},[255,261],{"type":20,"tag":165,"props":256,"children":258},{"className":257},[],[259],{"type":26,"value":260},"\u003Clabel>",{"type":26,"value":262}," for form inputs",{"type":20,"tag":44,"props":264,"children":265},{},[266,272,274,280,281,287],{"type":20,"tag":165,"props":267,"children":269},{"className":268},[],[270],{"type":26,"value":271},"\u003Ctable>",{"type":26,"value":273}," for tabular data (with ",{"type":20,"tag":165,"props":275,"children":277},{"className":276},[],[278],{"type":26,"value":279},"\u003Cthead>",{"type":26,"value":187},{"type":20,"tag":165,"props":282,"children":284},{"className":283},[],[285],{"type":26,"value":286},"\u003Ctbody>",{"type":26,"value":288},")",{"type":20,"tag":21,"props":290,"children":292},{"id":291},"aria-accessible-rich-internet-applications",[293],{"type":26,"value":294},"ARIA (Accessible Rich Internet Applications)",{"type":20,"tag":29,"props":296,"children":297},{},[298],{"type":26,"value":299},"Use ARIA when HTML alone can't describe the UI:",{"type":20,"tag":157,"props":301,"children":304},{"className":302,"code":303,"language":162,"meta":8},[160],"\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",[305],{"type":20,"tag":165,"props":306,"children":307},{"__ignoreMap":8},[308],{"type":26,"value":303},{"type":20,"tag":29,"props":310,"children":311},{},[312],{"type":26,"value":313},"Key ARIA attributes:",{"type":20,"tag":40,"props":315,"children":316},{},[317,328,339,350,361,372,383],{"type":20,"tag":44,"props":318,"children":319},{},[320,326],{"type":20,"tag":165,"props":321,"children":323},{"className":322},[],[324],{"type":26,"value":325},"aria-label",{"type":26,"value":327}," — Label for screen readers",{"type":20,"tag":44,"props":329,"children":330},{},[331,337],{"type":20,"tag":165,"props":332,"children":334},{"className":333},[],[335],{"type":26,"value":336},"aria-labelledby",{"type":26,"value":338}," — Reference element that labels this one",{"type":20,"tag":44,"props":340,"children":341},{},[342,348],{"type":20,"tag":165,"props":343,"children":345},{"className":344},[],[346],{"type":26,"value":347},"aria-describedby",{"type":26,"value":349}," — Reference element that describes this one",{"type":20,"tag":44,"props":351,"children":352},{},[353,359],{"type":20,"tag":165,"props":354,"children":356},{"className":355},[],[357],{"type":26,"value":358},"aria-expanded",{"type":26,"value":360}," — Whether collapsible element is open",{"type":20,"tag":44,"props":362,"children":363},{},[364,370],{"type":20,"tag":165,"props":365,"children":367},{"className":366},[],[368],{"type":26,"value":369},"aria-hidden",{"type":26,"value":371}," — Hide from screen readers (for decorative elements)",{"type":20,"tag":44,"props":373,"children":374},{},[375,381],{"type":20,"tag":165,"props":376,"children":378},{"className":377},[],[379],{"type":26,"value":380},"aria-live",{"type":26,"value":382}," — Announce changes dynamically",{"type":20,"tag":44,"props":384,"children":385},{},[386,392],{"type":20,"tag":165,"props":387,"children":389},{"className":388},[],[390],{"type":26,"value":391},"role",{"type":26,"value":393}," — Define element purpose when semantic HTML won't work",{"type":20,"tag":21,"props":395,"children":397},{"id":396},"keyboard-navigation",[398],{"type":26,"value":399},"Keyboard Navigation",{"type":20,"tag":29,"props":401,"children":402},{},[403],{"type":26,"value":404},"Many users navigate by keyboard only. Every interactive element must be keyboard accessible:",{"type":20,"tag":157,"props":406,"children":409},{"className":407,"code":408,"language":162,"meta":8},[160],"\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",[410],{"type":20,"tag":165,"props":411,"children":412},{"__ignoreMap":8},[413],{"type":26,"value":408},{"type":20,"tag":21,"props":415,"children":417},{"id":416},"color-contrast",[418],{"type":26,"value":419},"Color Contrast",{"type":20,"tag":29,"props":421,"children":422},{},[423],{"type":26,"value":424},"Text must have sufficient contrast ratio:",{"type":20,"tag":157,"props":426,"children":429},{"className":427,"code":428,"language":162,"meta":8},[160],"\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",[430],{"type":20,"tag":165,"props":431,"children":432},{"__ignoreMap":8},[433],{"type":26,"value":428},{"type":20,"tag":29,"props":435,"children":436},{},[437],{"type":26,"value":438},"Use tools to check:",{"type":20,"tag":40,"props":440,"children":441},{},[442,447,452],{"type":20,"tag":44,"props":443,"children":444},{},[445],{"type":26,"value":446},"WebAIM Contrast Checker",{"type":20,"tag":44,"props":448,"children":449},{},[450],{"type":26,"value":451},"Accessible Colors",{"type":20,"tag":44,"props":453,"children":454},{},[455],{"type":26,"value":456},"Chrome DevTools (automatic detection)",{"type":20,"tag":21,"props":458,"children":460},{"id":459},"form-accessibility",[461],{"type":26,"value":462},"Form Accessibility",{"type":20,"tag":29,"props":464,"children":465},{},[466],{"type":26,"value":467},"Forms are a common accessibility fail point:",{"type":20,"tag":157,"props":469,"children":472},{"className":470,"code":471,"language":162,"meta":8},[160],"\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",[473],{"type":20,"tag":165,"props":474,"children":475},{"__ignoreMap":8},[476],{"type":26,"value":471},{"type":20,"tag":29,"props":478,"children":479},{},[480],{"type":26,"value":481},"Key form patterns:",{"type":20,"tag":40,"props":483,"children":484},{},[485,495,506,516,521],{"type":20,"tag":44,"props":486,"children":487},{},[488,490],{"type":26,"value":489},"Every input has a ",{"type":20,"tag":165,"props":491,"children":493},{"className":492},[],[494],{"type":26,"value":260},{"type":20,"tag":44,"props":496,"children":497},{},[498,500],{"type":26,"value":499},"Labels are connected with ",{"type":20,"tag":165,"props":501,"children":503},{"className":502},[],[504],{"type":26,"value":505},"for=\"id\"",{"type":20,"tag":44,"props":507,"children":508},{},[509,511],{"type":26,"value":510},"Error messages use ",{"type":20,"tag":165,"props":512,"children":514},{"className":513},[],[515],{"type":26,"value":347},{"type":20,"tag":44,"props":517,"children":518},{},[519],{"type":26,"value":520},"Fieldset groups related inputs",{"type":20,"tag":44,"props":522,"children":523},{},[524,526,532],{"type":26,"value":525},"Use ",{"type":20,"tag":165,"props":527,"children":529},{"className":528},[],[530],{"type":26,"value":531},"aria-invalid",{"type":26,"value":533}," for validation states",{"type":20,"tag":21,"props":535,"children":537},{"id":536},"images-media",[538],{"type":26,"value":539},"Images & Media",{"type":20,"tag":157,"props":541,"children":544},{"className":542,"code":543,"language":162,"meta":8},[160],"\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",[545],{"type":20,"tag":165,"props":546,"children":547},{"__ignoreMap":8},[548],{"type":26,"value":543},{"type":20,"tag":29,"props":550,"children":551},{},[552],{"type":26,"value":553},"Alt text guidelines:",{"type":20,"tag":40,"props":555,"children":556},{},[557,562,573,578],{"type":20,"tag":44,"props":558,"children":559},{},[560],{"type":26,"value":561},"Describe the content, not \"image of\"",{"type":20,"tag":44,"props":563,"children":564},{},[565,567],{"type":26,"value":566},"Decorative images get ",{"type":20,"tag":165,"props":568,"children":570},{"className":569},[],[571],{"type":26,"value":572},"alt=\"\"",{"type":20,"tag":44,"props":574,"children":575},{},[576],{"type":26,"value":577},"Icons need aria-label if no visible text",{"type":20,"tag":44,"props":579,"children":580},{},[581],{"type":26,"value":582},"Keep under 125 characters",{"type":20,"tag":21,"props":584,"children":586},{"id":585},"automated-testing",[587],{"type":26,"value":588},"Automated Testing",{"type":20,"tag":29,"props":590,"children":591},{},[592],{"type":26,"value":593},"Use axe for automated accessibility testing:",{"type":20,"tag":157,"props":595,"children":600},{"className":596,"code":598,"language":599,"meta":8},[597],"language-bash","npm install -D @axe-core\u002Fplaywright jest-axe\n","bash",[601],{"type":20,"tag":165,"props":602,"children":603},{"__ignoreMap":8},[604],{"type":26,"value":598},{"type":20,"tag":157,"props":606,"children":611},{"className":607,"code":609,"language":610,"meta":8},[608],"language-typescript","\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","typescript",[612],{"type":20,"tag":165,"props":613,"children":614},{"__ignoreMap":8},[615],{"type":26,"value":609},{"type":20,"tag":29,"props":617,"children":618},{},[619],{"type":26,"value":620},"Jest testing:",{"type":20,"tag":157,"props":622,"children":625},{"className":623,"code":624,"language":610,"meta":8},[608],"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",[626],{"type":20,"tag":165,"props":627,"children":628},{"__ignoreMap":8},[629],{"type":26,"value":624},{"type":20,"tag":21,"props":631,"children":633},{"id":632},"accessibility-checklist",[634],{"type":26,"value":635},"Accessibility Checklist",{"type":20,"tag":29,"props":637,"children":638},{},[639],{"type":26,"value":640},"Before shipping:",{"type":20,"tag":40,"props":642,"children":645},{"className":643},[644],"contains-task-list",[646,665,680,695,710,725,740,755,770,785,800,815],{"type":20,"tag":44,"props":647,"children":650},{"className":648},[649],"task-list-item",[651,657,659,663],{"type":20,"tag":652,"props":653,"children":656},"input",{"disabled":654,"type":655},true,"checkbox",[],{"type":26,"value":658}," ",{"type":20,"tag":48,"props":660,"children":661},{},[662],{"type":26,"value":150},{"type":26,"value":664}," — use proper elements, not divs",{"type":20,"tag":44,"props":666,"children":668},{"className":667},[649],[669,672,673,678],{"type":20,"tag":652,"props":670,"children":671},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":674,"children":675},{},[676],{"type":26,"value":677},"Color contrast",{"type":26,"value":679}," — 4.5:1 for normal text",{"type":20,"tag":44,"props":681,"children":683},{"className":682},[649],[684,687,688,693],{"type":20,"tag":652,"props":685,"children":686},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":689,"children":690},{},[691],{"type":26,"value":692},"Keyboard navigation",{"type":26,"value":694}," — every interactive element accessible",{"type":20,"tag":44,"props":696,"children":698},{"className":697},[649],[699,702,703,708],{"type":20,"tag":652,"props":700,"children":701},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":704,"children":705},{},[706],{"type":26,"value":707},"Focus visible",{"type":26,"value":709}," — clear focus indicator visible",{"type":20,"tag":44,"props":711,"children":713},{"className":712},[649],[714,717,718,723],{"type":20,"tag":652,"props":715,"children":716},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":719,"children":720},{},[721],{"type":26,"value":722},"Forms labeled",{"type":26,"value":724}," — all inputs have labels",{"type":20,"tag":44,"props":726,"children":728},{"className":727},[649],[729,732,733,738],{"type":20,"tag":652,"props":730,"children":731},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":734,"children":735},{},[736],{"type":26,"value":737},"Images have alt text",{"type":26,"value":739}," — descriptive, not vague",{"type":20,"tag":44,"props":741,"children":743},{"className":742},[649],[744,747,748,753],{"type":20,"tag":652,"props":745,"children":746},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":749,"children":750},{},[751],{"type":26,"value":752},"Headings logical",{"type":26,"value":754}," — h1 → h2 → h3 (no skipping)",{"type":20,"tag":44,"props":756,"children":758},{"className":757},[649],[759,762,763,768],{"type":20,"tag":652,"props":760,"children":761},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":764,"children":765},{},[766],{"type":26,"value":767},"ARIA appropriate",{"type":26,"value":769}," — only when necessary",{"type":20,"tag":44,"props":771,"children":773},{"className":772},[649],[774,777,778,783],{"type":20,"tag":652,"props":775,"children":776},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":779,"children":780},{},[781],{"type":26,"value":782},"No flash",{"type":26,"value":784}," — no content flashing > 3 times\u002Fsecond",{"type":20,"tag":44,"props":786,"children":788},{"className":787},[649],[789,792,793,798],{"type":20,"tag":652,"props":790,"children":791},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":794,"children":795},{},[796],{"type":26,"value":797},"Responsive text",{"type":26,"value":799}," — readable at 200% zoom",{"type":20,"tag":44,"props":801,"children":803},{"className":802},[649],[804,807,808,813],{"type":20,"tag":652,"props":805,"children":806},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":809,"children":810},{},[811],{"type":26,"value":812},"Mobile accessible",{"type":26,"value":814}," — touch targets 48px minimum",{"type":20,"tag":44,"props":816,"children":818},{"className":817},[649],[819,822,823,828],{"type":20,"tag":652,"props":820,"children":821},{"disabled":654,"type":655},[],{"type":26,"value":658},{"type":20,"tag":48,"props":824,"children":825},{},[826],{"type":26,"value":827},"Automated tests pass",{"type":26,"value":829}," — axe, jest-axe",{"type":20,"tag":21,"props":831,"children":833},{"id":832},"real-impact",[834],{"type":26,"value":835},"Real Impact",{"type":20,"tag":29,"props":837,"children":838},{},[839],{"type":26,"value":840},"After implementing accessibility:",{"type":20,"tag":40,"props":842,"children":843},{},[844,849,854,859],{"type":20,"tag":44,"props":845,"children":846},{},[847],{"type":26,"value":848},"Accessibility score: 40 → 92\u002F100 (Lighthouse)",{"type":20,"tag":44,"props":850,"children":851},{},[852],{"type":26,"value":853},"Users with assistive tech: +8% traffic",{"type":20,"tag":44,"props":855,"children":856},{},[857],{"type":26,"value":858},"SEO improvement: +15% in search ranking",{"type":20,"tag":44,"props":860,"children":861},{},[862],{"type":26,"value":863},"General UX improvement: better for everyone",{"type":20,"tag":29,"props":865,"children":866},{},[867],{"type":26,"value":868},"Accessibility isn't a nice-to-have. It's engineering excellence.",{"title":8,"searchDepth":870,"depth":870,"links":871},2,[872,873,874,875,876,877,878,879,880,881,882],{"id":23,"depth":870,"text":27},{"id":92,"depth":870,"text":95},{"id":147,"depth":870,"text":150},{"id":291,"depth":870,"text":294},{"id":396,"depth":870,"text":399},{"id":416,"depth":870,"text":419},{"id":459,"depth":870,"text":462},{"id":536,"depth":870,"text":539},{"id":585,"depth":870,"text":588},{"id":632,"depth":870,"text":635},{"id":832,"depth":870,"text":835},"markdown","content:blog:accessibility-engineering-in-vue.md","content","blog\u002Faccessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue","md",{"_path":890,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":891,"description":892,"date":893,"updated":894,"readingTime":895,"tags":896,"featured":7,"body":899,"_type":883,"_id":1323,"_source":885,"_file":1324,"_stem":1325,"_extension":888},"\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","2026-07-05",12,[897,898],"CI\u002FCD","DevOps",{"type":17,"children":900,"toc":1306},[901,907,912,917,931,936,941,994,1000,1008,1014,1025,1031,1038,1043,1052,1057,1063,1072,1085,1091,1100,1105,1111,1120,1125,1153,1158,1164,1173,1178,1184,1193,1199,1204,1216,1222,1227,1232,1237,1242,1248,1301],{"type":20,"tag":21,"props":902,"children":904},{"id":903},"the-state-of-frontend-cicd",[905],{"type":26,"value":906},"The State of Frontend CI\u002FCD",{"type":20,"tag":29,"props":908,"children":909},{},[910],{"type":26,"value":911},"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":20,"tag":29,"props":913,"children":914},{},[915],{"type":26,"value":916},"Many teams still use basic CI pipelines:",{"type":20,"tag":918,"props":919,"children":920},"ol",{},[921,926],{"type":20,"tag":44,"props":922,"children":923},{},[924],{"type":26,"value":925},"Run tests",{"type":20,"tag":44,"props":927,"children":928},{},[929],{"type":26,"value":930},"If pass, deploy",{"type":20,"tag":29,"props":932,"children":933},{},[934],{"type":26,"value":935},"Result: slow feedback, missed bugs, brittle deployments.",{"type":20,"tag":29,"props":937,"children":938},{},[939],{"type":26,"value":940},"Modern CI\u002FCD is sophisticated:",{"type":20,"tag":918,"props":942,"children":943},{},[944,954,964,974,984],{"type":20,"tag":44,"props":945,"children":946},{},[947,952],{"type":20,"tag":48,"props":948,"children":949},{},[950],{"type":26,"value":951},"Parallel testing",{"type":26,"value":953}," — unit, integration, e2e at the same time",{"type":20,"tag":44,"props":955,"children":956},{},[957,962],{"type":20,"tag":48,"props":958,"children":959},{},[960],{"type":26,"value":961},"Performance monitoring",{"type":26,"value":963}," — reject PRs that slow down the app",{"type":20,"tag":44,"props":965,"children":966},{},[967,972],{"type":20,"tag":48,"props":968,"children":969},{},[970],{"type":26,"value":971},"Visual regression testing",{"type":26,"value":973}," — catch unintended design changes",{"type":20,"tag":44,"props":975,"children":976},{},[977,982],{"type":20,"tag":48,"props":978,"children":979},{},[980],{"type":26,"value":981},"Semantic versioning",{"type":26,"value":983}," — automate version bumps and releases",{"type":20,"tag":44,"props":985,"children":986},{},[987,992],{"type":20,"tag":48,"props":988,"children":989},{},[990],{"type":26,"value":991},"Multi-stage deployments",{"type":26,"value":993}," — preview → staging → production",{"type":20,"tag":21,"props":995,"children":997},{"id":996},"the-full-pipeline",[998],{"type":26,"value":999},"The Full Pipeline",{"type":20,"tag":157,"props":1001,"children":1003},{"code":1002},"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",[1004],{"type":20,"tag":165,"props":1005,"children":1006},{"__ignoreMap":8},[1007],{"type":26,"value":1002},{"type":20,"tag":21,"props":1009,"children":1011},{"id":1010},"github-actions-workflow",[1012],{"type":26,"value":1013},"GitHub Actions Workflow",{"type":20,"tag":157,"props":1015,"children":1020},{"code":1016,"language":1017,"meta":8,"className":1018},"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",[1019],"language-yaml",[1021],{"type":20,"tag":165,"props":1022,"children":1023},{"__ignoreMap":8},[1024],{"type":26,"value":1016},{"type":20,"tag":21,"props":1026,"children":1028},{"id":1027},"key-features-explained",[1029],{"type":26,"value":1030},"Key Features Explained",{"type":20,"tag":1032,"props":1033,"children":1035},"h3",{"id":1034},"_1-parallel-job-execution",[1036],{"type":26,"value":1037},"1. Parallel Job Execution",{"type":20,"tag":29,"props":1039,"children":1040},{},[1041],{"type":26,"value":1042},"Tests run simultaneously instead of sequentially:",{"type":20,"tag":157,"props":1044,"children":1047},{"code":1045,"language":1017,"meta":8,"className":1046},"strategy:\n  matrix:\n    shard: [1, 2, 3, 4]\nrun: pnpm test -- --shard=${{ matrix.shard }}\u002F4\n",[1019],[1048],{"type":20,"tag":165,"props":1049,"children":1050},{"__ignoreMap":8},[1051],{"type":26,"value":1045},{"type":20,"tag":29,"props":1053,"children":1054},{},[1055],{"type":26,"value":1056},"Reduces CI time from 12 minutes → 4 minutes.",{"type":20,"tag":1032,"props":1058,"children":1060},{"id":1059},"_2-dependency-caching",[1061],{"type":26,"value":1062},"2. Dependency Caching",{"type":20,"tag":157,"props":1064,"children":1067},{"code":1065,"language":1017,"meta":8,"className":1066},"- uses: actions\u002Fsetup-node@v3\n  with:\n    node-version: ${{ env.NODE_VERSION }}\n    cache: 'pnpm'\n",[1019],[1068],{"type":20,"tag":165,"props":1069,"children":1070},{"__ignoreMap":8},[1071],{"type":26,"value":1065},{"type":20,"tag":29,"props":1073,"children":1074},{},[1075,1077,1083],{"type":26,"value":1076},"Skips ",{"type":20,"tag":165,"props":1078,"children":1080},{"className":1079},[],[1081],{"type":26,"value":1082},"pnpm install",{"type":26,"value":1084}," if dependencies haven't changed. Saves 1-2 minutes per run.",{"type":20,"tag":1032,"props":1086,"children":1088},{"id":1087},"_3-conditional-deployment",[1089],{"type":26,"value":1090},"3. Conditional Deployment",{"type":20,"tag":157,"props":1092,"children":1095},{"code":1093,"language":1017,"meta":8,"className":1094},"if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n",[1019],[1096],{"type":20,"tag":165,"props":1097,"children":1098},{"__ignoreMap":8},[1099],{"type":26,"value":1093},{"type":20,"tag":29,"props":1101,"children":1102},{},[1103],{"type":26,"value":1104},"Only deploy to production on main branch pushes. PRs get preview deployments.",{"type":20,"tag":1032,"props":1106,"children":1108},{"id":1107},"_4-semantic-versioning",[1109],{"type":26,"value":1110},"4. Semantic Versioning",{"type":20,"tag":157,"props":1112,"children":1115},{"code":1113,"language":1017,"meta":8,"className":1114},"- uses: cycjimmy\u002Fsemantic-release-action@v3\n",[1019],[1116],{"type":20,"tag":165,"props":1117,"children":1118},{"__ignoreMap":8},[1119],{"type":26,"value":1113},{"type":20,"tag":29,"props":1121,"children":1122},{},[1123],{"type":26,"value":1124},"Automatically:",{"type":20,"tag":40,"props":1126,"children":1127},{},[1128,1133,1138,1143,1148],{"type":20,"tag":44,"props":1129,"children":1130},{},[1131],{"type":26,"value":1132},"Analyzes commits (feat → minor, fix → patch)",{"type":20,"tag":44,"props":1134,"children":1135},{},[1136],{"type":26,"value":1137},"Bumps version in package.json",{"type":20,"tag":44,"props":1139,"children":1140},{},[1141],{"type":26,"value":1142},"Creates GitHub release",{"type":20,"tag":44,"props":1144,"children":1145},{},[1146],{"type":26,"value":1147},"Publishes to npm",{"type":20,"tag":44,"props":1149,"children":1150},{},[1151],{"type":26,"value":1152},"Generates changelog",{"type":20,"tag":29,"props":1154,"children":1155},{},[1156],{"type":26,"value":1157},"One less manual step.",{"type":20,"tag":1032,"props":1159,"children":1161},{"id":1160},"_5-performance-monitoring",[1162],{"type":26,"value":1163},"5. Performance Monitoring",{"type":20,"tag":157,"props":1165,"children":1168},{"code":1166,"language":1017,"meta":8,"className":1167},"- uses: treosh\u002Flighthouse-ci-action@v9\n",[1019],[1169],{"type":20,"tag":165,"props":1170,"children":1171},{"__ignoreMap":8},[1172],{"type":26,"value":1166},{"type":20,"tag":29,"props":1174,"children":1175},{},[1176],{"type":26,"value":1177},"Blocks deployment if performance degrades. Ensures Lighthouse score stays above threshold.",{"type":20,"tag":21,"props":1179,"children":1181},{"id":1180},"caching-strategy",[1182],{"type":26,"value":1183},"Caching Strategy",{"type":20,"tag":157,"props":1185,"children":1188},{"code":1186,"language":1017,"meta":8,"className":1187},"# .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",[1019],[1189],{"type":20,"tag":165,"props":1190,"children":1191},{"__ignoreMap":8},[1192],{"type":26,"value":1186},{"type":20,"tag":21,"props":1194,"children":1196},{"id":1195},"results",[1197],{"type":26,"value":1198},"Results",{"type":20,"tag":29,"props":1200,"children":1201},{},[1202],{"type":26,"value":1203},"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":20,"tag":29,"props":1205,"children":1206},{},[1207,1209,1214],{"type":26,"value":1208},"My verified production result from building this kind of pipeline at Ordant: ",{"type":20,"tag":48,"props":1210,"children":1211},{},[1212],{"type":26,"value":1213},"merge-to-deploy time down 40%, with 99.9% uptime",{"type":26,"value":1215},".",{"type":20,"tag":21,"props":1217,"children":1219},{"id":1218},"cost-optimization",[1220],{"type":26,"value":1221},"Cost Optimization",{"type":20,"tag":29,"props":1223,"children":1224},{},[1225],{"type":26,"value":1226},"Worked example at GitHub Actions list pricing ($0.008\u002Fminute per job):",{"type":20,"tag":29,"props":1228,"children":1229},{},[1230],{"type":26,"value":1231},"A 5-minute pipeline with 6 parallel jobs = 30 job-minutes = $0.24\u002Frun.",{"type":20,"tag":29,"props":1233,"children":1234},{},[1235],{"type":26,"value":1236},"At 30 runs\u002Fday, that's $7.20\u002Fday ≈ $216\u002Fmonth.",{"type":20,"tag":29,"props":1238,"children":1239},{},[1240],{"type":26,"value":1241},"Worth it for the bugs prevented and deployment speed gained.",{"type":20,"tag":21,"props":1243,"children":1245},{"id":1244},"pro-tips",[1246],{"type":26,"value":1247},"Pro Tips",{"type":20,"tag":918,"props":1249,"children":1250},{},[1251,1261,1271,1281,1291],{"type":20,"tag":44,"props":1252,"children":1253},{},[1254,1259],{"type":20,"tag":48,"props":1255,"children":1256},{},[1257],{"type":26,"value":1258},"Cache aggressively",{"type":26,"value":1260}," — saves 1-3 minutes per run",{"type":20,"tag":44,"props":1262,"children":1263},{},[1264,1269],{"type":20,"tag":48,"props":1265,"children":1266},{},[1267],{"type":26,"value":1268},"Run tests in parallel",{"type":26,"value":1270}," — hardware is cheaper than time",{"type":20,"tag":44,"props":1272,"children":1273},{},[1274,1279],{"type":20,"tag":48,"props":1275,"children":1276},{},[1277],{"type":26,"value":1278},"Fail fast",{"type":26,"value":1280}," — lint before tests, tests before build",{"type":20,"tag":44,"props":1282,"children":1283},{},[1284,1289],{"type":20,"tag":48,"props":1285,"children":1286},{},[1287],{"type":26,"value":1288},"Monitor performance",{"type":26,"value":1290}," — performance regression is a deployment blocker",{"type":20,"tag":44,"props":1292,"children":1293},{},[1294,1299],{"type":20,"tag":48,"props":1295,"children":1296},{},[1297],{"type":26,"value":1298},"Automate versioning",{"type":26,"value":1300}," — semantic-release removes guesswork",{"type":20,"tag":29,"props":1302,"children":1303},{},[1304],{"type":26,"value":1305},"Good CI\u002FCD is a force multiplier. Invest in it early.",{"title":8,"searchDepth":870,"depth":870,"links":1307},[1308,1309,1310,1311,1319,1320,1321,1322],{"id":903,"depth":870,"text":906},{"id":996,"depth":870,"text":999},{"id":1010,"depth":870,"text":1013},{"id":1027,"depth":870,"text":1030,"children":1312},[1313,1315,1316,1317,1318],{"id":1034,"depth":1314,"text":1037},3,{"id":1059,"depth":1314,"text":1062},{"id":1087,"depth":1314,"text":1090},{"id":1107,"depth":1314,"text":1110},{"id":1160,"depth":1314,"text":1163},{"id":1180,"depth":870,"text":1183},{"id":1195,"depth":870,"text":1198},{"id":1218,"depth":870,"text":1221},{"id":1244,"depth":870,"text":1247},"content:blog:advanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows",{"_path":1327,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1328,"description":1329,"date":1330,"updated":894,"readingTime":12,"tags":1331,"featured":7,"body":1333,"_type":883,"_id":1870,"_source":885,"_file":1871,"_stem":1872,"_extension":888},"\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",[15,1332],"Architecture",{"type":17,"children":1334,"toc":1856},[1335,1341,1346,1351,1374,1387,1393,1399,1404,1412,1417,1440,1446,1451,1460,1465,1508,1514,1519,1528,1533,1556,1562,1567,1574,1583,1589,1598,1604,1609,1618,1624,1629,1638,1643,1676,1681,1692,1698,1711,1764,1770,1782,1788,1851],{"type":20,"tag":21,"props":1336,"children":1338},{"id":1337},"the-problem-with-ad-hoc-components",[1339],{"type":26,"value":1340},"The Problem with Ad-Hoc Components",{"type":20,"tag":29,"props":1342,"children":1343},{},[1344],{"type":26,"value":1345},"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":20,"tag":29,"props":1347,"children":1348},{},[1349],{"type":26,"value":1350},"Inconsistency was costing us:",{"type":20,"tag":40,"props":1352,"children":1353},{},[1354,1359,1364,1369],{"type":20,"tag":44,"props":1355,"children":1356},{},[1357],{"type":26,"value":1358},"New team members took weeks to learn existing patterns",{"type":20,"tag":44,"props":1360,"children":1361},{},[1362],{"type":26,"value":1363},"Components were re-implemented multiple times across products",{"type":20,"tag":44,"props":1365,"children":1366},{},[1367],{"type":26,"value":1368},"No single source of truth for design decisions",{"type":20,"tag":44,"props":1370,"children":1371},{},[1372],{"type":26,"value":1373},"Design-to-dev handoff took meetings just to clarify prop names",{"type":20,"tag":29,"props":1375,"children":1376},{},[1377,1379,1386],{"type":26,"value":1378},"We needed a design system. The production version of this story — with the monorepo layout, versioning policy, and adoption mechanics — is in the ",{"type":20,"tag":1380,"props":1381,"children":1383},"a",{"href":1382},"\u002Fcase-studies\u002Fmonorepo-design-system",[1384],{"type":26,"value":1385},"monorepo design system case study",{"type":26,"value":1215},{"type":20,"tag":21,"props":1388,"children":1390},{"id":1389},"architecture-the-foundation",[1391],{"type":26,"value":1392},"Architecture: The Foundation",{"type":20,"tag":1032,"props":1394,"children":1396},{"id":1395},"_1-monorepo-structure",[1397],{"type":26,"value":1398},"1. Monorepo Structure",{"type":20,"tag":29,"props":1400,"children":1401},{},[1402],{"type":26,"value":1403},"We used pnpm workspaces:",{"type":20,"tag":157,"props":1405,"children":1407},{"code":1406},"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",[1408],{"type":20,"tag":165,"props":1409,"children":1410},{"__ignoreMap":8},[1411],{"type":26,"value":1406},{"type":20,"tag":29,"props":1413,"children":1414},{},[1415],{"type":26,"value":1416},"Benefits:",{"type":20,"tag":40,"props":1418,"children":1419},{},[1420,1425,1430,1435],{"type":20,"tag":44,"props":1421,"children":1422},{},[1423],{"type":26,"value":1424},"Monorepo enforces consistency",{"type":20,"tag":44,"props":1426,"children":1427},{},[1428],{"type":26,"value":1429},"Shared tooling configuration",{"type":20,"tag":44,"props":1431,"children":1432},{},[1433],{"type":26,"value":1434},"Easy to test components in isolation",{"type":20,"tag":44,"props":1436,"children":1437},{},[1438],{"type":26,"value":1439},"Clear dependency management",{"type":20,"tag":1032,"props":1441,"children":1443},{"id":1442},"_2-component-api-design",[1444],{"type":26,"value":1445},"2. Component API Design",{"type":20,"tag":29,"props":1447,"children":1448},{},[1449],{"type":26,"value":1450},"We established strict rules for component props:",{"type":20,"tag":157,"props":1452,"children":1455},{"code":1453,"language":610,"meta":8,"className":1454},"\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",[608],[1456],{"type":20,"tag":165,"props":1457,"children":1458},{"__ignoreMap":8},[1459],{"type":26,"value":1453},{"type":20,"tag":29,"props":1461,"children":1462},{},[1463],{"type":26,"value":1464},"Guidelines:",{"type":20,"tag":918,"props":1466,"children":1467},{},[1468,1478,1488,1498],{"type":20,"tag":44,"props":1469,"children":1470},{},[1471,1476],{"type":20,"tag":48,"props":1472,"children":1473},{},[1474],{"type":26,"value":1475},"Keep props count \u003C 8",{"type":26,"value":1477}," — if you need more, you're probably missing a abstraction",{"type":20,"tag":44,"props":1479,"children":1480},{},[1481,1486],{"type":20,"tag":48,"props":1482,"children":1483},{},[1484],{"type":26,"value":1485},"Use CSS variables for styling",{"type":26,"value":1487}," — don't expose every style property",{"type":20,"tag":44,"props":1489,"children":1490},{},[1491,1496],{"type":20,"tag":48,"props":1492,"children":1493},{},[1494],{"type":26,"value":1495},"Provide sensible defaults",{"type":26,"value":1497}," — most components should work with zero props",{"type":20,"tag":44,"props":1499,"children":1500},{},[1501,1506],{"type":20,"tag":48,"props":1502,"children":1503},{},[1504],{"type":26,"value":1505},"Document the \"why\"",{"type":26,"value":1507}," — every prop should have a reason",{"type":20,"tag":1032,"props":1509,"children":1511},{"id":1510},"_3-design-tokens",[1512],{"type":26,"value":1513},"3. Design Tokens",{"type":20,"tag":29,"props":1515,"children":1516},{},[1517],{"type":26,"value":1518},"Tokens are the single source of truth:",{"type":20,"tag":157,"props":1520,"children":1523},{"code":1521,"language":610,"meta":8,"className":1522},"\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",[608],[1524],{"type":20,"tag":165,"props":1525,"children":1526},{"__ignoreMap":8},[1527],{"type":26,"value":1521},{"type":20,"tag":29,"props":1529,"children":1530},{},[1531],{"type":26,"value":1532},"This single file becomes the source of truth for:",{"type":20,"tag":40,"props":1534,"children":1535},{},[1536,1541,1546,1551],{"type":20,"tag":44,"props":1537,"children":1538},{},[1539],{"type":26,"value":1540},"Figma design tokens",{"type":20,"tag":44,"props":1542,"children":1543},{},[1544],{"type":26,"value":1545},"CSS variables",{"type":20,"tag":44,"props":1547,"children":1548},{},[1549],{"type":26,"value":1550},"Type definitions",{"type":20,"tag":44,"props":1552,"children":1553},{},[1554],{"type":26,"value":1555},"Storybook theming",{"type":20,"tag":1032,"props":1557,"children":1559},{"id":1558},"_4-component-patterns",[1560],{"type":26,"value":1561},"4. Component Patterns",{"type":20,"tag":29,"props":1563,"children":1564},{},[1565],{"type":26,"value":1566},"We established patterns for common scenarios:",{"type":20,"tag":1568,"props":1569,"children":1571},"h4",{"id":1570},"simple-component",[1572],{"type":26,"value":1573},"Simple Component",{"type":20,"tag":157,"props":1575,"children":1578},{"code":1576,"language":162,"meta":8,"className":1577},"\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",[160],[1579],{"type":20,"tag":165,"props":1580,"children":1581},{"__ignoreMap":8},[1582],{"type":26,"value":1576},{"type":20,"tag":1568,"props":1584,"children":1586},{"id":1585},"compound-component",[1587],{"type":26,"value":1588},"Compound Component",{"type":20,"tag":157,"props":1590,"children":1593},{"code":1591,"language":162,"meta":8,"className":1592},"\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",[160],[1594],{"type":20,"tag":165,"props":1595,"children":1596},{"__ignoreMap":8},[1597],{"type":26,"value":1591},{"type":20,"tag":21,"props":1599,"children":1601},{"id":1600},"documentation-storybook",[1602],{"type":26,"value":1603},"Documentation & Storybook",{"type":20,"tag":29,"props":1605,"children":1606},{},[1607],{"type":26,"value":1608},"Storybook is critical for design system success:",{"type":20,"tag":157,"props":1610,"children":1613},{"code":1611,"language":610,"meta":8,"className":1612},"\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",[608],[1614],{"type":20,"tag":165,"props":1615,"children":1616},{"__ignoreMap":8},[1617],{"type":26,"value":1611},{"type":20,"tag":21,"props":1619,"children":1621},{"id":1620},"versioning-releases",[1622],{"type":26,"value":1623},"Versioning & Releases",{"type":20,"tag":29,"props":1625,"children":1626},{},[1627],{"type":26,"value":1628},"Design systems are libraries. Treat them like it:",{"type":20,"tag":157,"props":1630,"children":1633},{"code":1631,"language":599,"meta":8,"className":1632},"# 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",[597],[1634],{"type":20,"tag":165,"props":1635,"children":1636},{"__ignoreMap":8},[1637],{"type":26,"value":1631},{"type":20,"tag":29,"props":1639,"children":1640},{},[1641],{"type":26,"value":1642},"We published:",{"type":20,"tag":40,"props":1644,"children":1645},{},[1646,1656,1666],{"type":20,"tag":44,"props":1647,"children":1648},{},[1649,1654],{"type":20,"tag":48,"props":1650,"children":1651},{},[1652],{"type":26,"value":1653},"v1.0.0",{"type":26,"value":1655}," — the core component set\n",{"type":20,"tag":44,"props":1657,"children":1658},{},[1659,1664],{"type":20,"tag":48,"props":1660,"children":1661},{},[1662],{"type":26,"value":1663},"v1.2.3",{"type":26,"value":1665}," — new tokens, backward compatible",{"type":20,"tag":44,"props":1667,"children":1668},{},[1669,1674],{"type":20,"tag":48,"props":1670,"children":1671},{},[1672],{"type":26,"value":1673},"v2.0.0",{"type":26,"value":1675}," — redesign with breaking changes",{"type":20,"tag":29,"props":1677,"children":1678},{},[1679],{"type":26,"value":1680},"Each version went to npm registry. Teams pinned versions:",{"type":20,"tag":157,"props":1682,"children":1687},{"code":1683,"language":1684,"meta":8,"className":1685},"{\n  \"dependencies\": {\n    \"@acme\u002Fdesign-system\": \"^1.2.0\"\n  }\n}\n","json",[1686],"language-json",[1688],{"type":20,"tag":165,"props":1689,"children":1690},{"__ignoreMap":8},[1691],{"type":26,"value":1683},{"type":20,"tag":21,"props":1693,"children":1695},{"id":1694},"adoption-strategy",[1696],{"type":26,"value":1697},"Adoption Strategy",{"type":20,"tag":29,"props":1699,"children":1700},{},[1701,1703,1709],{"type":26,"value":1702},"Getting teams to ",{"type":20,"tag":1704,"props":1705,"children":1706},"em",{},[1707],{"type":26,"value":1708},"use",{"type":26,"value":1710}," the system was as important as building it:",{"type":20,"tag":918,"props":1712,"children":1713},{},[1714,1724,1734,1744,1754],{"type":20,"tag":44,"props":1715,"children":1716},{},[1717,1722],{"type":20,"tag":48,"props":1718,"children":1719},{},[1720],{"type":26,"value":1721},"Dogfood internally",{"type":26,"value":1723}," — our flagship product used v1.0 before public release",{"type":20,"tag":44,"props":1725,"children":1726},{},[1727,1732],{"type":20,"tag":48,"props":1728,"children":1729},{},[1730],{"type":26,"value":1731},"Create migration guides",{"type":26,"value":1733}," — \"How to replace your Button with DesignSystemButton\"",{"type":20,"tag":44,"props":1735,"children":1736},{},[1737,1742],{"type":20,"tag":48,"props":1738,"children":1739},{},[1740],{"type":26,"value":1741},"Celebrate wins",{"type":26,"value":1743}," — \"we deleted three duplicate Button implementations this sprint\" beats any adoption mandate",{"type":20,"tag":44,"props":1745,"children":1746},{},[1747,1752],{"type":20,"tag":48,"props":1748,"children":1749},{},[1750],{"type":26,"value":1751},"Make it easier to use than not",{"type":26,"value":1753}," — publish to npm, show install in docs",{"type":20,"tag":44,"props":1755,"children":1756},{},[1757,1762],{"type":20,"tag":48,"props":1758,"children":1759},{},[1760],{"type":26,"value":1761},"Maintain it publicly",{"type":26,"value":1763}," — commit to performance, accessibility, TypeScript support",{"type":20,"tag":21,"props":1765,"children":1767},{"id":1766},"adoption-metrics",[1768],{"type":26,"value":1769},"Adoption Metrics",{"type":20,"tag":29,"props":1771,"children":1772},{},[1773,1775,1780],{"type":26,"value":1774},"The measured outcome on the production system this post draws from: ",{"type":20,"tag":48,"props":1776,"children":1777},{},[1778],{"type":26,"value":1779},"20% faster feature delivery",{"type":26,"value":1781}," on projects consuming the library — teams assemble features from documented components instead of rebuilding primitives.",{"type":20,"tag":21,"props":1783,"children":1785},{"id":1784},"key-lessons",[1786],{"type":26,"value":1787},"Key Lessons",{"type":20,"tag":918,"props":1789,"children":1790},{},[1791,1801,1811,1821,1831,1841],{"type":20,"tag":44,"props":1792,"children":1793},{},[1794,1799],{"type":20,"tag":48,"props":1795,"children":1796},{},[1797],{"type":26,"value":1798},"Tokens are the foundation",{"type":26,"value":1800}," — invest heavily in design tokens before components",{"type":20,"tag":44,"props":1802,"children":1803},{},[1804,1809],{"type":20,"tag":48,"props":1805,"children":1806},{},[1807],{"type":26,"value":1808},"API design is everything",{"type":26,"value":1810}," — a good API is adopted naturally; a bad one requires enforcement",{"type":20,"tag":44,"props":1812,"children":1813},{},[1814,1819],{"type":20,"tag":48,"props":1815,"children":1816},{},[1817],{"type":26,"value":1818},"Storybook isn't optional",{"type":26,"value":1820}," — it's how designers and developers communicate",{"type":20,"tag":44,"props":1822,"children":1823},{},[1824,1829],{"type":20,"tag":48,"props":1825,"children":1826},{},[1827],{"type":26,"value":1828},"Version properly",{"type":26,"value":1830}," — teams need confidence that upgrades won't break things",{"type":20,"tag":44,"props":1832,"children":1833},{},[1834,1839],{"type":20,"tag":48,"props":1835,"children":1836},{},[1837],{"type":26,"value":1838},"One person owns it",{"type":26,"value":1840}," — design systems need a maintainer, not a committee",{"type":20,"tag":44,"props":1842,"children":1843},{},[1844,1849],{"type":20,"tag":48,"props":1845,"children":1846},{},[1847],{"type":26,"value":1848},"Measure success by adoption",{"type":26,"value":1850}," — a perfect system nobody uses is useless",{"type":20,"tag":29,"props":1852,"children":1853},{},[1854],{"type":26,"value":1855},"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":8,"searchDepth":870,"depth":870,"links":1857},[1858,1859,1865,1866,1867,1868,1869],{"id":1337,"depth":870,"text":1340},{"id":1389,"depth":870,"text":1392,"children":1860},[1861,1862,1863,1864],{"id":1395,"depth":1314,"text":1398},{"id":1442,"depth":1314,"text":1445},{"id":1510,"depth":1314,"text":1513},{"id":1558,"depth":1314,"text":1561},{"id":1600,"depth":870,"text":1603},{"id":1620,"depth":870,"text":1623},{"id":1694,"depth":870,"text":1697},{"id":1766,"depth":870,"text":1769},{"id":1784,"depth":870,"text":1787},"content:blog:building-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue",{"_path":1874,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":1875,"description":1876,"date":1877,"readingTime":1878,"tags":1879,"featured":7,"body":1882,"_type":883,"_id":2555,"_source":885,"_file":2556,"_stem":2557,"_extension":888},"\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,[1880,1881],"Performance","Nuxt",{"type":17,"children":1883,"toc":2529},[1884,1890,1895,1972,1978,1984,1992,1997,2003,2008,2017,2022,2031,2036,2045,2051,2056,2062,2067,2076,2081,2099,2105,2114,2120,2125,2134,2140,2145,2154,2160,2165,2171,2180,2186,2195,2201,2210,2216,2221,2270,2276,2285,2291,2300,2306,2315,2321,2326,2431,2437,2442,2451,2456,2465,2471,2524],{"type":20,"tag":21,"props":1885,"children":1887},{"id":1886},"understanding-core-web-vitals",[1888],{"type":26,"value":1889},"Understanding Core Web Vitals",{"type":20,"tag":29,"props":1891,"children":1892},{},[1893],{"type":26,"value":1894},"Google's Core Web Vitals measure three aspects of user experience:",{"type":20,"tag":918,"props":1896,"children":1897},{},[1898,1921,1949],{"type":20,"tag":44,"props":1899,"children":1900},{},[1901,1906,1908],{"type":20,"tag":48,"props":1902,"children":1903},{},[1904],{"type":26,"value":1905},"LCP (Largest Contentful Paint)",{"type":26,"value":1907}," — How fast does content appear?",{"type":20,"tag":40,"props":1909,"children":1910},{},[1911,1916],{"type":20,"tag":44,"props":1912,"children":1913},{},[1914],{"type":26,"value":1915},"Target: \u003C 2.5s",{"type":20,"tag":44,"props":1917,"children":1918},{},[1919],{"type":26,"value":1920},"Measures: When largest element (image, heading, video) becomes visible",{"type":20,"tag":44,"props":1922,"children":1923},{},[1924,1929,1931],{"type":20,"tag":48,"props":1925,"children":1926},{},[1927],{"type":26,"value":1928},"FID (First Input Delay)",{"type":26,"value":1930}," — How responsive is the page?",{"type":20,"tag":40,"props":1932,"children":1933},{},[1934,1939,1944],{"type":20,"tag":44,"props":1935,"children":1936},{},[1937],{"type":26,"value":1938},"Target: \u003C 100ms",{"type":20,"tag":44,"props":1940,"children":1941},{},[1942],{"type":26,"value":1943},"Measures: Delay from user input to browser processing",{"type":20,"tag":44,"props":1945,"children":1946},{},[1947],{"type":26,"value":1948},"Being replaced by INP in 2024",{"type":20,"tag":44,"props":1950,"children":1951},{},[1952,1957,1959],{"type":20,"tag":48,"props":1953,"children":1954},{},[1955],{"type":26,"value":1956},"CLS (Cumulative Layout Shift)",{"type":26,"value":1958}," — How stable is the layout?",{"type":20,"tag":40,"props":1960,"children":1961},{},[1962,1967],{"type":20,"tag":44,"props":1963,"children":1964},{},[1965],{"type":26,"value":1966},"Target: \u003C 0.1",{"type":20,"tag":44,"props":1968,"children":1969},{},[1970],{"type":26,"value":1971},"Measures: Unexpected visual shifts during page load",{"type":20,"tag":21,"props":1973,"children":1975},{"id":1974},"measuring-web-vitals",[1976],{"type":26,"value":1977},"Measuring Web Vitals",{"type":20,"tag":1032,"props":1979,"children":1981},{"id":1980},"_1-google-pagespeed-insights",[1982],{"type":26,"value":1983},"1. Google PageSpeed Insights",{"type":20,"tag":157,"props":1985,"children":1987},{"code":1986},"https:\u002F\u002Fpagespeed.web.dev\u002F?url=yoursite.com\n",[1988],{"type":20,"tag":165,"props":1989,"children":1990},{"__ignoreMap":8},[1991],{"type":26,"value":1986},{"type":20,"tag":29,"props":1993,"children":1994},{},[1995],{"type":26,"value":1996},"Shows real-world data from actual users. This is what matters most.",{"type":20,"tag":1032,"props":1998,"children":2000},{"id":1999},"_2-local-testing",[2001],{"type":26,"value":2002},"2. Local Testing",{"type":20,"tag":29,"props":2004,"children":2005},{},[2006],{"type":26,"value":2007},"Install web-vitals library:",{"type":20,"tag":157,"props":2009,"children":2012},{"code":2010,"language":599,"meta":8,"className":2011},"npm install web-vitals\n",[597],[2013],{"type":20,"tag":165,"props":2014,"children":2015},{"__ignoreMap":8},[2016],{"type":26,"value":2010},{"type":20,"tag":29,"props":2018,"children":2019},{},[2020],{"type":26,"value":2021},"Track metrics in Nuxt:",{"type":20,"tag":157,"props":2023,"children":2026},{"code":2024,"language":610,"meta":8,"className":2025},"\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",[608],[2027],{"type":20,"tag":165,"props":2028,"children":2029},{"__ignoreMap":8},[2030],{"type":26,"value":2024},{"type":20,"tag":29,"props":2032,"children":2033},{},[2034],{"type":26,"value":2035},"Send to analytics:",{"type":20,"tag":157,"props":2037,"children":2040},{"code":2038,"language":610,"meta":8,"className":2039},"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",[608],[2041],{"type":20,"tag":165,"props":2042,"children":2043},{"__ignoreMap":8},[2044],{"type":26,"value":2038},{"type":20,"tag":21,"props":2046,"children":2048},{"id":2047},"optimizing-lcp",[2049],{"type":26,"value":2050},"Optimizing LCP",{"type":20,"tag":29,"props":2052,"children":2053},{},[2054],{"type":26,"value":2055},"LCP is usually the main blocker. Focus here first.",{"type":20,"tag":1032,"props":2057,"children":2059},{"id":2058},"strategy-1-optimize-largest-element",[2060],{"type":26,"value":2061},"Strategy 1: Optimize Largest Element",{"type":20,"tag":29,"props":2063,"children":2064},{},[2065],{"type":26,"value":2066},"Identify what's causing slow LCP:",{"type":20,"tag":157,"props":2068,"children":2071},{"code":2069,"language":610,"meta":8,"className":2070},"\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",[608],[2072],{"type":20,"tag":165,"props":2073,"children":2074},{"__ignoreMap":8},[2075],{"type":26,"value":2069},{"type":20,"tag":29,"props":2077,"children":2078},{},[2079],{"type":26,"value":2080},"Usually the largest element is:",{"type":20,"tag":40,"props":2082,"children":2083},{},[2084,2089,2094],{"type":20,"tag":44,"props":2085,"children":2086},{},[2087],{"type":26,"value":2088},"Hero image",{"type":20,"tag":44,"props":2090,"children":2091},{},[2092],{"type":26,"value":2093},"Main heading",{"type":20,"tag":44,"props":2095,"children":2096},{},[2097],{"type":26,"value":2098},"Large text block",{"type":20,"tag":1032,"props":2100,"children":2102},{"id":2101},"strategy-2-preload-critical-resources",[2103],{"type":26,"value":2104},"Strategy 2: Preload Critical Resources",{"type":20,"tag":157,"props":2106,"children":2109},{"code":2107,"language":610,"meta":8,"className":2108},"\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",[608],[2110],{"type":20,"tag":165,"props":2111,"children":2112},{"__ignoreMap":8},[2113],{"type":26,"value":2107},{"type":20,"tag":1032,"props":2115,"children":2117},{"id":2116},"strategy-3-server-side-rendering",[2118],{"type":26,"value":2119},"Strategy 3: Server-Side Rendering",{"type":20,"tag":29,"props":2121,"children":2122},{},[2123],{"type":26,"value":2124},"SSR ensures content is in HTML, not dependent on JavaScript:",{"type":20,"tag":157,"props":2126,"children":2129},{"code":2127,"language":610,"meta":8,"className":2128},"\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",[608],[2130],{"type":20,"tag":165,"props":2131,"children":2132},{"__ignoreMap":8},[2133],{"type":26,"value":2127},{"type":20,"tag":1032,"props":2135,"children":2137},{"id":2136},"strategy-4-image-optimization",[2138],{"type":26,"value":2139},"Strategy 4: Image Optimization",{"type":20,"tag":29,"props":2141,"children":2142},{},[2143],{"type":26,"value":2144},"Hero images often block LCP. Optimize aggressively:",{"type":20,"tag":157,"props":2146,"children":2149},{"code":2147,"language":162,"meta":8,"className":2148},"\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",[160],[2150],{"type":20,"tag":165,"props":2151,"children":2152},{"__ignoreMap":8},[2153],{"type":26,"value":2147},{"type":20,"tag":21,"props":2155,"children":2157},{"id":2156},"optimizing-fid-and-inp",[2158],{"type":26,"value":2159},"Optimizing FID (and INP)",{"type":20,"tag":29,"props":2161,"children":2162},{},[2163],{"type":26,"value":2164},"FID measures JavaScript execution blocking input. Reduce with:",{"type":20,"tag":1032,"props":2166,"children":2168},{"id":2167},"_1-code-splitting",[2169],{"type":26,"value":2170},"1. Code Splitting",{"type":20,"tag":157,"props":2172,"children":2175},{"code":2173,"language":610,"meta":8,"className":2174},"\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",[608],[2176],{"type":20,"tag":165,"props":2177,"children":2178},{"__ignoreMap":8},[2179],{"type":26,"value":2173},{"type":20,"tag":1032,"props":2181,"children":2183},{"id":2182},"_2-web-workers-for-heavy-tasks",[2184],{"type":26,"value":2185},"2. Web Workers for Heavy Tasks",{"type":20,"tag":157,"props":2187,"children":2190},{"code":2188,"language":610,"meta":8,"className":2189},"\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",[608],[2191],{"type":20,"tag":165,"props":2192,"children":2193},{"__ignoreMap":8},[2194],{"type":26,"value":2188},{"type":20,"tag":1032,"props":2196,"children":2198},{"id":2197},"_3-defer-non-critical-javascript",[2199],{"type":26,"value":2200},"3. Defer Non-Critical JavaScript",{"type":20,"tag":157,"props":2202,"children":2205},{"code":2203,"language":162,"meta":8,"className":2204},"\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",[160],[2206],{"type":20,"tag":165,"props":2207,"children":2208},{"__ignoreMap":8},[2209],{"type":26,"value":2203},{"type":20,"tag":21,"props":2211,"children":2213},{"id":2212},"optimizing-cls",[2214],{"type":26,"value":2215},"Optimizing CLS",{"type":20,"tag":29,"props":2217,"children":2218},{},[2219],{"type":26,"value":2220},"CLS is layout instability. Causes:",{"type":20,"tag":918,"props":2222,"children":2223},{},[2224,2234,2244,2260],{"type":20,"tag":44,"props":2225,"children":2226},{},[2227,2232],{"type":20,"tag":48,"props":2228,"children":2229},{},[2230],{"type":26,"value":2231},"Images without dimensions",{"type":26,"value":2233}," — add width\u002Fheight",{"type":20,"tag":44,"props":2235,"children":2236},{},[2237,2242],{"type":20,"tag":48,"props":2238,"children":2239},{},[2240],{"type":26,"value":2241},"Ads loading late",{"type":26,"value":2243}," — reserve space",{"type":20,"tag":44,"props":2245,"children":2246},{},[2247,2252,2254],{"type":20,"tag":48,"props":2248,"children":2249},{},[2250],{"type":26,"value":2251},"Fonts loading",{"type":26,"value":2253}," — use ",{"type":20,"tag":165,"props":2255,"children":2257},{"className":2256},[],[2258],{"type":26,"value":2259},"font-display: swap",{"type":20,"tag":44,"props":2261,"children":2262},{},[2263,2268],{"type":20,"tag":48,"props":2264,"children":2265},{},[2266],{"type":26,"value":2267},"Dynamic content",{"type":26,"value":2269}," — skeleton screens",{"type":20,"tag":1032,"props":2271,"children":2273},{"id":2272},"fix-1-image-dimensions",[2274],{"type":26,"value":2275},"Fix 1: Image Dimensions",{"type":20,"tag":157,"props":2277,"children":2280},{"code":2278,"language":162,"meta":8,"className":2279},"\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",[160],[2281],{"type":20,"tag":165,"props":2282,"children":2283},{"__ignoreMap":8},[2284],{"type":26,"value":2278},{"type":20,"tag":1032,"props":2286,"children":2288},{"id":2287},"fix-2-font-loading",[2289],{"type":26,"value":2290},"Fix 2: Font Loading",{"type":20,"tag":157,"props":2292,"children":2295},{"code":2293,"language":610,"meta":8,"className":2294},"\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",[608],[2296],{"type":20,"tag":165,"props":2297,"children":2298},{"__ignoreMap":8},[2299],{"type":26,"value":2293},{"type":20,"tag":1032,"props":2301,"children":2303},{"id":2302},"fix-3-skeleton-screens",[2304],{"type":26,"value":2305},"Fix 3: Skeleton Screens",{"type":20,"tag":157,"props":2307,"children":2310},{"code":2308,"language":162,"meta":8,"className":2309},"\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",[160],[2311],{"type":20,"tag":165,"props":2312,"children":2313},{"__ignoreMap":8},[2314],{"type":26,"value":2308},{"type":20,"tag":21,"props":2316,"children":2318},{"id":2317},"real-world-results",[2319],{"type":26,"value":2320},"Real-World Results",{"type":20,"tag":29,"props":2322,"children":2323},{},[2324],{"type":26,"value":2325},"After implementing these optimizations:",{"type":20,"tag":2327,"props":2328,"children":2329},"table",{},[2330,2354],{"type":20,"tag":2331,"props":2332,"children":2333},"thead",{},[2334],{"type":20,"tag":2335,"props":2336,"children":2337},"tr",{},[2338,2344,2349],{"type":20,"tag":2339,"props":2340,"children":2341},"th",{},[2342],{"type":26,"value":2343},"Metric",{"type":20,"tag":2339,"props":2345,"children":2346},{},[2347],{"type":26,"value":2348},"Before",{"type":20,"tag":2339,"props":2350,"children":2351},{},[2352],{"type":26,"value":2353},"After",{"type":20,"tag":2355,"props":2356,"children":2357},"tbody",{},[2358,2377,2395,2413],{"type":20,"tag":2335,"props":2359,"children":2360},{},[2361,2367,2372],{"type":20,"tag":2362,"props":2363,"children":2364},"td",{},[2365],{"type":26,"value":2366},"LCP",{"type":20,"tag":2362,"props":2368,"children":2369},{},[2370],{"type":26,"value":2371},"3.8s",{"type":20,"tag":2362,"props":2373,"children":2374},{},[2375],{"type":26,"value":2376},"1.6s",{"type":20,"tag":2335,"props":2378,"children":2379},{},[2380,2385,2390],{"type":20,"tag":2362,"props":2381,"children":2382},{},[2383],{"type":26,"value":2384},"FID",{"type":20,"tag":2362,"props":2386,"children":2387},{},[2388],{"type":26,"value":2389},"180ms",{"type":20,"tag":2362,"props":2391,"children":2392},{},[2393],{"type":26,"value":2394},"45ms",{"type":20,"tag":2335,"props":2396,"children":2397},{},[2398,2403,2408],{"type":20,"tag":2362,"props":2399,"children":2400},{},[2401],{"type":26,"value":2402},"CLS",{"type":20,"tag":2362,"props":2404,"children":2405},{},[2406],{"type":26,"value":2407},"0.22",{"type":20,"tag":2362,"props":2409,"children":2410},{},[2411],{"type":26,"value":2412},"0.04",{"type":20,"tag":2335,"props":2414,"children":2415},{},[2416,2421,2426],{"type":20,"tag":2362,"props":2417,"children":2418},{},[2419],{"type":26,"value":2420},"PageSpeed Score",{"type":20,"tag":2362,"props":2422,"children":2423},{},[2424],{"type":26,"value":2425},"42",{"type":20,"tag":2362,"props":2427,"children":2428},{},[2429],{"type":26,"value":2430},"92",{"type":20,"tag":21,"props":2432,"children":2434},{"id":2433},"continuous-monitoring",[2435],{"type":26,"value":2436},"Continuous Monitoring",{"type":20,"tag":29,"props":2438,"children":2439},{},[2440],{"type":26,"value":2441},"Set up alerts for regressions:",{"type":20,"tag":157,"props":2443,"children":2446},{"code":2444,"language":610,"meta":8,"className":2445},"\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",[608],[2447],{"type":20,"tag":165,"props":2448,"children":2449},{"__ignoreMap":8},[2450],{"type":26,"value":2444},{"type":20,"tag":29,"props":2452,"children":2453},{},[2454],{"type":26,"value":2455},"Run in CI:",{"type":20,"tag":157,"props":2457,"children":2460},{"code":2458,"language":599,"meta":8,"className":2459},"# .github\u002Fworkflows\u002Flighthouse.yml\n- name: Run Lighthouse CI\n  run: lhci autorun\n",[597],[2461],{"type":20,"tag":165,"props":2462,"children":2463},{"__ignoreMap":8},[2464],{"type":26,"value":2458},{"type":20,"tag":21,"props":2466,"children":2468},{"id":2467},"key-takeaways",[2469],{"type":26,"value":2470},"Key Takeaways",{"type":20,"tag":918,"props":2472,"children":2473},{},[2474,2484,2494,2504,2514],{"type":20,"tag":44,"props":2475,"children":2476},{},[2477,2482],{"type":20,"tag":48,"props":2478,"children":2479},{},[2480],{"type":26,"value":2481},"Measure real user data",{"type":26,"value":2483}," — lab data differs from production",{"type":20,"tag":44,"props":2485,"children":2486},{},[2487,2492],{"type":20,"tag":48,"props":2488,"children":2489},{},[2490],{"type":26,"value":2491},"LCP is usually the bottleneck",{"type":26,"value":2493}," — focus optimization efforts there",{"type":20,"tag":44,"props":2495,"children":2496},{},[2497,2502],{"type":20,"tag":48,"props":2498,"children":2499},{},[2500],{"type":26,"value":2501},"Images matter most",{"type":26,"value":2503}," — proper optimization yields biggest gains",{"type":20,"tag":44,"props":2505,"children":2506},{},[2507,2512],{"type":20,"tag":48,"props":2508,"children":2509},{},[2510],{"type":26,"value":2511},"Preload critical resources",{"type":26,"value":2513}," — hero images and fonts",{"type":20,"tag":44,"props":2515,"children":2516},{},[2517,2522],{"type":20,"tag":48,"props":2518,"children":2519},{},[2520],{"type":26,"value":2521},"Monitor continuously",{"type":26,"value":2523}," — regressions happen. Catch them early",{"type":20,"tag":29,"props":2525,"children":2526},{},[2527],{"type":26,"value":2528},"Core Web Vitals are no longer just SEO metrics—they're user experience fundamentals. Invest in getting them right.",{"title":8,"searchDepth":870,"depth":870,"links":2530},[2531,2532,2536,2542,2547,2552,2553,2554],{"id":1886,"depth":870,"text":1889},{"id":1974,"depth":870,"text":1977,"children":2533},[2534,2535],{"id":1980,"depth":1314,"text":1983},{"id":1999,"depth":1314,"text":2002},{"id":2047,"depth":870,"text":2050,"children":2537},[2538,2539,2540,2541],{"id":2058,"depth":1314,"text":2061},{"id":2101,"depth":1314,"text":2104},{"id":2116,"depth":1314,"text":2119},{"id":2136,"depth":1314,"text":2139},{"id":2156,"depth":870,"text":2159,"children":2543},[2544,2545,2546],{"id":2167,"depth":1314,"text":2170},{"id":2182,"depth":1314,"text":2185},{"id":2197,"depth":1314,"text":2200},{"id":2212,"depth":870,"text":2215,"children":2548},[2549,2550,2551],{"id":2272,"depth":1314,"text":2275},{"id":2287,"depth":1314,"text":2290},{"id":2302,"depth":1314,"text":2305},{"id":2317,"depth":870,"text":2320},{"id":2433,"depth":870,"text":2436},{"id":2467,"depth":870,"text":2470},"content:blog:core-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide",{"_path":2559,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":2560,"description":2561,"date":2562,"updated":894,"readingTime":2563,"tags":2564,"featured":7,"body":2565,"_type":883,"_id":3362,"_source":885,"_file":3363,"_stem":3364,"_extension":888},"\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,[1332,1880],{"type":17,"children":2566,"toc":3340},[2567,2573,2578,2631,2636,2642,2647,2655,2661,2714,2720,2763,2768,2774,2779,2787,2792,2815,2821,2827,2836,2842,2851,2856,2879,2885,2890,2943,2949,2954,2963,2967,3010,3016,3021,3030,3036,3042,3050,3055,3066,3072,3081,3087,3092,3100,3106,3111,3119,3124,3130,3135,3186,3192,3197,3221,3226,3232,3237,3335],{"type":20,"tag":21,"props":2568,"children":2570},{"id":2569},"the-scaling-problem",[2571],{"type":26,"value":2572},"The Scaling Problem",{"type":20,"tag":29,"props":2574,"children":2575},{},[2576],{"type":26,"value":2577},"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":20,"tag":40,"props":2579,"children":2580},{},[2581,2591,2601,2611,2621],{"type":20,"tag":44,"props":2582,"children":2583},{},[2584,2589],{"type":20,"tag":48,"props":2585,"children":2586},{},[2587],{"type":26,"value":2588},"Git conflicts",{"type":26,"value":2590}," — 20+ per day from different teams modifying the same files",{"type":20,"tag":44,"props":2592,"children":2593},{},[2594,2599],{"type":20,"tag":48,"props":2595,"children":2596},{},[2597],{"type":26,"value":2598},"Deployment bottlenecks",{"type":26,"value":2600}," — one bug in team A's feature blocked team B's release",{"type":20,"tag":44,"props":2602,"children":2603},{},[2604,2609],{"type":20,"tag":48,"props":2605,"children":2606},{},[2607],{"type":26,"value":2608},"Code ownership unclear",{"type":26,"value":2610}," — who owns the form validation layer? Three people claim they do",{"type":20,"tag":44,"props":2612,"children":2613},{},[2614,2619],{"type":20,"tag":48,"props":2615,"children":2616},{},[2617],{"type":26,"value":2618},"Bundle bloat",{"type":26,"value":2620}," — features for team A's customers were loading for team B's customers",{"type":20,"tag":44,"props":2622,"children":2623},{},[2624,2629],{"type":20,"tag":48,"props":2625,"children":2626},{},[2627],{"type":26,"value":2628},"Development friction",{"type":26,"value":2630}," — new team members took 4 weeks to understand the codebase",{"type":20,"tag":29,"props":2632,"children":2633},{},[2634],{"type":26,"value":2635},"Something had to change.",{"type":20,"tag":21,"props":2637,"children":2639},{"id":2638},"option-1-monorepo-what-we-chose",[2640],{"type":26,"value":2641},"Option 1: Monorepo (What We Chose)",{"type":20,"tag":29,"props":2643,"children":2644},{},[2645],{"type":26,"value":2646},"We chose a monorepo structure with pnpm workspaces:",{"type":20,"tag":157,"props":2648,"children":2650},{"code":2649},"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",[2651],{"type":20,"tag":165,"props":2652,"children":2653},{"__ignoreMap":8},[2654],{"type":26,"value":2649},{"type":20,"tag":1032,"props":2656,"children":2658},{"id":2657},"monorepo-benefits",[2659],{"type":26,"value":2660},"Monorepo Benefits",{"type":20,"tag":40,"props":2662,"children":2663},{},[2664,2674,2684,2694,2704],{"type":20,"tag":44,"props":2665,"children":2666},{},[2667,2672],{"type":20,"tag":48,"props":2668,"children":2669},{},[2670],{"type":26,"value":2671},"Single source of truth",{"type":26,"value":2673}," for shared code",{"type":20,"tag":44,"props":2675,"children":2676},{},[2677,2682],{"type":20,"tag":48,"props":2678,"children":2679},{},[2680],{"type":26,"value":2681},"Atomic commits",{"type":26,"value":2683}," — can update multiple packages together",{"type":20,"tag":44,"props":2685,"children":2686},{},[2687,2692],{"type":20,"tag":48,"props":2688,"children":2689},{},[2690],{"type":26,"value":2691},"Shared CI\u002FCD",{"type":26,"value":2693}," — one build pipeline for all apps",{"type":20,"tag":44,"props":2695,"children":2696},{},[2697,2702],{"type":20,"tag":48,"props":2698,"children":2699},{},[2700],{"type":26,"value":2701},"Consistent tooling",{"type":26,"value":2703}," — eslint, prettier, typescript config",{"type":20,"tag":44,"props":2705,"children":2706},{},[2707,2712],{"type":20,"tag":48,"props":2708,"children":2709},{},[2710],{"type":26,"value":2711},"Easy refactoring",{"type":26,"value":2713}," — move code between apps without npm publish",{"type":20,"tag":1032,"props":2715,"children":2717},{"id":2716},"monorepo-drawbacks",[2718],{"type":26,"value":2719},"Monorepo Drawbacks",{"type":20,"tag":40,"props":2721,"children":2722},{},[2723,2733,2743,2753],{"type":20,"tag":44,"props":2724,"children":2725},{},[2726,2731],{"type":20,"tag":48,"props":2727,"children":2728},{},[2729],{"type":26,"value":2730},"Build complexity",{"type":26,"value":2732}," — must rebuild only changed packages",{"type":20,"tag":44,"props":2734,"children":2735},{},[2736,2741],{"type":20,"tag":48,"props":2737,"children":2738},{},[2739],{"type":26,"value":2740},"Dependency management",{"type":26,"value":2742}," — circular dependencies possible",{"type":20,"tag":44,"props":2744,"children":2745},{},[2746,2751],{"type":20,"tag":48,"props":2747,"children":2748},{},[2749],{"type":26,"value":2750},"Harder to open-source",{"type":26,"value":2752}," — internal code in same repo",{"type":20,"tag":44,"props":2754,"children":2755},{},[2756,2761],{"type":20,"tag":48,"props":2757,"children":2758},{},[2759],{"type":26,"value":2760},"Steeper learning curve",{"type":26,"value":2762}," — new engineers need to understand workspace structure",{"type":20,"tag":29,"props":2764,"children":2765},{},[2766],{"type":26,"value":2767},"For us, benefits outweighed drawbacks.",{"type":20,"tag":21,"props":2769,"children":2771},{"id":2770},"option-2-micro-frontends-the-alternative",[2772],{"type":26,"value":2773},"Option 2: Micro-Frontends (The Alternative)",{"type":20,"tag":29,"props":2775,"children":2776},{},[2777],{"type":26,"value":2778},"For teams that want complete independence, micro-frontends are powerful:",{"type":20,"tag":157,"props":2780,"children":2782},{"code":2781},"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",[2783],{"type":20,"tag":165,"props":2784,"children":2785},{"__ignoreMap":8},[2786],{"type":26,"value":2781},{"type":20,"tag":29,"props":2788,"children":2789},{},[2790],{"type":26,"value":2791},"Each micro-frontend:",{"type":20,"tag":40,"props":2793,"children":2794},{},[2795,2800,2805,2810],{"type":20,"tag":44,"props":2796,"children":2797},{},[2798],{"type":26,"value":2799},"Has its own git repo",{"type":20,"tag":44,"props":2801,"children":2802},{},[2803],{"type":26,"value":2804},"Deploys independently",{"type":20,"tag":44,"props":2806,"children":2807},{},[2808],{"type":26,"value":2809},"Ships its own bundle",{"type":20,"tag":44,"props":2811,"children":2812},{},[2813],{"type":26,"value":2814},"Uses shared design tokens + api client",{"type":20,"tag":1032,"props":2816,"children":2818},{"id":2817},"micro-frontend-patterns",[2819],{"type":26,"value":2820},"Micro-Frontend Patterns",{"type":20,"tag":1568,"props":2822,"children":2824},{"id":2823},"_1-module-federation-webpack-5-vite",[2825],{"type":26,"value":2826},"1. Module Federation (Webpack 5 \u002F Vite)",{"type":20,"tag":157,"props":2828,"children":2831},{"code":2829,"language":610,"meta":8,"className":2830},"\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",[608],[2832],{"type":20,"tag":165,"props":2833,"children":2834},{"__ignoreMap":8},[2835],{"type":26,"value":2829},{"type":20,"tag":1568,"props":2837,"children":2839},{"id":2838},"_2-url-based-routing",[2840],{"type":26,"value":2841},"2. URL-based Routing",{"type":20,"tag":157,"props":2843,"children":2846},{"code":2844,"language":162,"meta":8,"className":2845},"\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",[160],[2847],{"type":20,"tag":165,"props":2848,"children":2849},{"__ignoreMap":8},[2850],{"type":26,"value":2844},{"type":20,"tag":29,"props":2852,"children":2853},{},[2854],{"type":26,"value":2855},"Each micro-frontend is a complete app that can:",{"type":20,"tag":40,"props":2857,"children":2858},{},[2859,2864,2869,2874],{"type":20,"tag":44,"props":2860,"children":2861},{},[2862],{"type":26,"value":2863},"Deploy independently",{"type":20,"tag":44,"props":2865,"children":2866},{},[2867],{"type":26,"value":2868},"Use different versions of dependencies (if needed)",{"type":20,"tag":44,"props":2870,"children":2871},{},[2872],{"type":26,"value":2873},"Be owned by a separate team",{"type":20,"tag":44,"props":2875,"children":2876},{},[2877],{"type":26,"value":2878},"Scale independently",{"type":20,"tag":21,"props":2880,"children":2882},{"id":2881},"our-monorepo-decision",[2883],{"type":26,"value":2884},"Our Monorepo Decision",{"type":20,"tag":29,"props":2886,"children":2887},{},[2888],{"type":26,"value":2889},"We chose monorepo because:",{"type":20,"tag":918,"props":2891,"children":2892},{},[2893,2903,2913,2923,2933],{"type":20,"tag":44,"props":2894,"children":2895},{},[2896,2901],{"type":20,"tag":48,"props":2897,"children":2898},{},[2899],{"type":26,"value":2900},"Team maturity",{"type":26,"value":2902}," — all teams understood git and npm workspaces",{"type":20,"tag":44,"props":2904,"children":2905},{},[2906,2911],{"type":20,"tag":48,"props":2907,"children":2908},{},[2909],{"type":26,"value":2910},"Shared design system",{"type":26,"value":2912}," — critical for consistent UX",{"type":20,"tag":44,"props":2914,"children":2915},{},[2916,2921],{"type":20,"tag":48,"props":2917,"children":2918},{},[2919],{"type":26,"value":2920},"Single deployment",{"type":26,"value":2922}," — easier for our ops team",{"type":20,"tag":44,"props":2924,"children":2925},{},[2926,2931],{"type":20,"tag":48,"props":2927,"children":2928},{},[2929],{"type":26,"value":2930},"Developer experience",{"type":26,"value":2932}," — IDE support better in monorepo",{"type":20,"tag":44,"props":2934,"children":2935},{},[2936,2941],{"type":20,"tag":48,"props":2937,"children":2938},{},[2939],{"type":26,"value":2940},"Build optimization",{"type":26,"value":2942}," — turborepo handles incremental builds well",{"type":20,"tag":21,"props":2944,"children":2946},{"id":2945},"implementation-turborepo",[2947],{"type":26,"value":2948},"Implementation: Turborepo",{"type":20,"tag":29,"props":2950,"children":2951},{},[2952],{"type":26,"value":2953},"We use Turborepo for build orchestration:",{"type":20,"tag":157,"props":2955,"children":2958},{"code":2956,"language":1684,"meta":8,"className":2957},"{\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",[1686],[2959],{"type":20,"tag":165,"props":2960,"children":2961},{"__ignoreMap":8},[2962],{"type":26,"value":2956},{"type":20,"tag":29,"props":2964,"children":2965},{},[2966],{"type":26,"value":1416},{"type":20,"tag":40,"props":2968,"children":2969},{},[2970,2980,2990,3000],{"type":20,"tag":44,"props":2971,"children":2972},{},[2973,2978],{"type":20,"tag":48,"props":2974,"children":2975},{},[2976],{"type":26,"value":2977},"Parallel execution",{"type":26,"value":2979}," — builds run in parallel when possible",{"type":20,"tag":44,"props":2981,"children":2982},{},[2983,2988],{"type":20,"tag":48,"props":2984,"children":2985},{},[2986],{"type":26,"value":2987},"Task pipelines",{"type":26,"value":2989}," — test only runs after build",{"type":20,"tag":44,"props":2991,"children":2992},{},[2993,2998],{"type":20,"tag":48,"props":2994,"children":2995},{},[2996],{"type":26,"value":2997},"Caching",{"type":26,"value":2999}," — skips tasks that haven't changed",{"type":20,"tag":44,"props":3001,"children":3002},{},[3003,3008],{"type":20,"tag":48,"props":3004,"children":3005},{},[3006],{"type":26,"value":3007},"CI\u002FCD integration",{"type":26,"value":3009}," — auto-detects changed packages",{"type":20,"tag":21,"props":3011,"children":3013},{"id":3012},"state-management-at-scale",[3014],{"type":26,"value":3015},"State Management at Scale",{"type":20,"tag":29,"props":3017,"children":3018},{},[3019],{"type":26,"value":3020},"Pinia (Vue's official state management) handles monorepo scales well:",{"type":20,"tag":157,"props":3022,"children":3025},{"code":3023,"language":610,"meta":8,"className":3024},"\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",[608],[3026],{"type":20,"tag":165,"props":3027,"children":3028},{"__ignoreMap":8},[3029],{"type":26,"value":3023},{"type":20,"tag":21,"props":3031,"children":3033},{"id":3032},"preventing-common-pitfalls",[3034],{"type":26,"value":3035},"Preventing Common Pitfalls",{"type":20,"tag":1032,"props":3037,"children":3039},{"id":3038},"_1-circular-dependencies",[3040],{"type":26,"value":3041},"1. Circular Dependencies",{"type":20,"tag":157,"props":3043,"children":3045},{"code":3044},"Bad: dashboard → api-client → shared-utils → dashboard\n",[3046],{"type":20,"tag":165,"props":3047,"children":3048},{"__ignoreMap":8},[3049],{"type":26,"value":3044},{"type":20,"tag":29,"props":3051,"children":3052},{},[3053],{"type":26,"value":3054},"Solution: Enforce dependency graph with eslint:",{"type":20,"tag":157,"props":3056,"children":3061},{"code":3057,"language":3058,"meta":8,"className":3059},"\u002F\u002F .eslintrc.js\n{\n  \"rules\": {\n    \"import\u002Fno-cycle\": \"error\"\n  }\n}\n","javascript",[3060],"language-javascript",[3062],{"type":20,"tag":165,"props":3063,"children":3064},{"__ignoreMap":8},[3065],{"type":26,"value":3057},{"type":20,"tag":1032,"props":3067,"children":3069},{"id":3068},"_2-shared-dependency-versions",[3070],{"type":26,"value":3071},"2. Shared Dependency Versions",{"type":20,"tag":157,"props":3073,"children":3076},{"code":3074,"language":610,"meta":8,"className":3075},"\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",[608],[3077],{"type":20,"tag":165,"props":3078,"children":3079},{"__ignoreMap":8},[3080],{"type":26,"value":3074},{"type":20,"tag":1032,"props":3082,"children":3084},{"id":3083},"_3-code-ownership",[3085],{"type":26,"value":3086},"3. Code Ownership",{"type":20,"tag":29,"props":3088,"children":3089},{},[3090],{"type":26,"value":3091},"Create a CODEOWNERS file:",{"type":20,"tag":157,"props":3093,"children":3095},{"code":3094},"# 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",[3096],{"type":20,"tag":165,"props":3097,"children":3098},{"__ignoreMap":8},[3099],{"type":26,"value":3094},{"type":20,"tag":21,"props":3101,"children":3103},{"id":3102},"performance-impact",[3104],{"type":26,"value":3105},"Performance Impact",{"type":20,"tag":29,"props":3107,"children":3108},{},[3109],{"type":26,"value":3110},"Monorepo with proper caching:",{"type":20,"tag":157,"props":3112,"children":3114},{"code":3113},"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",[3115],{"type":20,"tag":165,"props":3116,"children":3117},{"__ignoreMap":8},[3118],{"type":26,"value":3113},{"type":20,"tag":29,"props":3120,"children":3121},{},[3122],{"type":26,"value":3123},"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":20,"tag":21,"props":3125,"children":3127},{"id":3126},"team-autonomy-in-monorepo",[3128],{"type":26,"value":3129},"Team Autonomy in Monorepo",{"type":20,"tag":29,"props":3131,"children":3132},{},[3133],{"type":26,"value":3134},"To prevent chaos:",{"type":20,"tag":918,"props":3136,"children":3137},{},[3138,3148,3158,3176],{"type":20,"tag":44,"props":3139,"children":3140},{},[3141,3146],{"type":20,"tag":48,"props":3142,"children":3143},{},[3144],{"type":26,"value":3145},"Clear package boundaries",{"type":26,"value":3147}," — dashboard team doesn't touch billing code",{"type":20,"tag":44,"props":3149,"children":3150},{},[3151,3156],{"type":20,"tag":48,"props":3152,"children":3153},{},[3154],{"type":26,"value":3155},"Owned packages",{"type":26,"value":3157}," — each package has a code owner",{"type":20,"tag":44,"props":3159,"children":3160},{},[3161,3166,3168,3174],{"type":20,"tag":48,"props":3162,"children":3163},{},[3164],{"type":26,"value":3165},"Version contracts",{"type":26,"value":3167}," — ",{"type":20,"tag":165,"props":3169,"children":3171},{"className":3170},[],[3172],{"type":26,"value":3173},"@acme\u002Fapi-client",{"type":26,"value":3175}," v2.0.0 is stable, don't break it",{"type":20,"tag":44,"props":3177,"children":3178},{},[3179,3184],{"type":20,"tag":48,"props":3180,"children":3181},{},[3182],{"type":26,"value":3183},"Shared conventions",{"type":26,"value":3185}," — naming, folder structure, testing patterns",{"type":20,"tag":21,"props":3187,"children":3189},{"id":3188},"when-monorepo-isnt-enough",[3190],{"type":26,"value":3191},"When Monorepo Isn't Enough",{"type":20,"tag":29,"props":3193,"children":3194},{},[3195],{"type":26,"value":3196},"For teams that need complete independence, consider micro-frontends:",{"type":20,"tag":29,"props":3198,"children":3199},{},[3200,3205,3207,3212,3214,3219],{"type":20,"tag":48,"props":3201,"children":3202},{},[3203],{"type":26,"value":3204},"Monorepo",{"type":26,"value":3206}," → Single deployment, shared everything\n",{"type":20,"tag":48,"props":3208,"children":3209},{},[3210],{"type":26,"value":3211},"Micro-Frontends",{"type":26,"value":3213}," → Independent deployments, isolated teams\n",{"type":20,"tag":48,"props":3215,"children":3216},{},[3217],{"type":26,"value":3218},"Hybrid",{"type":26,"value":3220}," → Monorepo for shared packages, micro-frontends for apps",{"type":20,"tag":29,"props":3222,"children":3223},{},[3224],{"type":26,"value":3225},"We started with monorepo. If we grew to 200 engineers across 30 teams, we'd likely move to micro-frontends with Module Federation.",{"type":20,"tag":21,"props":3227,"children":3229},{"id":3228},"final-metrics",[3230],{"type":26,"value":3231},"Final Metrics",{"type":20,"tag":29,"props":3233,"children":3234},{},[3235],{"type":26,"value":3236},"The movement a monorepo migration typically produces (illustrative example — not measured claims from one project):",{"type":20,"tag":2327,"props":3238,"children":3239},{},[3240,3255],{"type":20,"tag":2331,"props":3241,"children":3242},{},[3243],{"type":20,"tag":2335,"props":3244,"children":3245},{},[3246,3250],{"type":20,"tag":2339,"props":3247,"children":3248},{},[3249],{"type":26,"value":2343},{"type":20,"tag":2339,"props":3251,"children":3252},{},[3253],{"type":26,"value":3254},"Direction",{"type":20,"tag":2355,"props":3256,"children":3257},{},[3258,3271,3284,3296,3309,3322],{"type":20,"tag":2335,"props":3259,"children":3260},{},[3261,3266],{"type":20,"tag":2362,"props":3262,"children":3263},{},[3264],{"type":26,"value":3265},"Build time",{"type":20,"tag":2362,"props":3267,"children":3268},{},[3269],{"type":26,"value":3270},"Sharply down with caching",{"type":20,"tag":2335,"props":3272,"children":3273},{},[3274,3279],{"type":20,"tag":2362,"props":3275,"children":3276},{},[3277],{"type":26,"value":3278},"CI\u002FCD time",{"type":20,"tag":2362,"props":3280,"children":3281},{},[3282],{"type":26,"value":3283},"Down, especially on unchanged packages",{"type":20,"tag":2335,"props":3285,"children":3286},{},[3287,3291],{"type":20,"tag":2362,"props":3288,"children":3289},{},[3290],{"type":26,"value":2588},{"type":20,"tag":2362,"props":3292,"children":3293},{},[3294],{"type":26,"value":3295},"Down — one repo, one integration point",{"type":20,"tag":2335,"props":3297,"children":3298},{},[3299,3304],{"type":20,"tag":2362,"props":3300,"children":3301},{},[3302],{"type":26,"value":3303},"Deployment failures",{"type":20,"tag":2362,"props":3305,"children":3306},{},[3307],{"type":26,"value":3308},"Down — shared tooling, fewer bespoke pipelines",{"type":20,"tag":2335,"props":3310,"children":3311},{},[3312,3317],{"type":20,"tag":2362,"props":3313,"children":3314},{},[3315],{"type":26,"value":3316},"Code duplication",{"type":20,"tag":2362,"props":3318,"children":3319},{},[3320],{"type":26,"value":3321},"Down — importing beats re-implementing",{"type":20,"tag":2335,"props":3323,"children":3324},{},[3325,3330],{"type":20,"tag":2362,"props":3326,"children":3327},{},[3328],{"type":26,"value":3329},"Onboarding time",{"type":20,"tag":2362,"props":3331,"children":3332},{},[3333],{"type":26,"value":3334},"Down — one setup, one set of conventions",{"type":20,"tag":29,"props":3336,"children":3337},{},[3338],{"type":26,"value":3339},"The architecture change pays off through clear ownership, faster development, and fewer bugs — if you enforce the boundaries.",{"title":8,"searchDepth":870,"depth":870,"links":3341},[3342,3343,3347,3350,3351,3352,3353,3358,3359,3360,3361],{"id":2569,"depth":870,"text":2572},{"id":2638,"depth":870,"text":2641,"children":3344},[3345,3346],{"id":2657,"depth":1314,"text":2660},{"id":2716,"depth":1314,"text":2719},{"id":2770,"depth":870,"text":2773,"children":3348},[3349],{"id":2817,"depth":1314,"text":2820},{"id":2881,"depth":870,"text":2884},{"id":2945,"depth":870,"text":2948},{"id":3012,"depth":870,"text":3015},{"id":3032,"depth":870,"text":3035,"children":3354},[3355,3356,3357],{"id":3038,"depth":1314,"text":3041},{"id":3068,"depth":1314,"text":3071},{"id":3083,"depth":1314,"text":3086},{"id":3102,"depth":870,"text":3105},{"id":3126,"depth":870,"text":3129},{"id":3188,"depth":870,"text":3191},{"id":3228,"depth":870,"text":3231},"content:blog:frontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas",{"_path":3366,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":3367,"description":3368,"date":3369,"readingTime":1878,"tags":3370,"featured":7,"body":3371,"_type":883,"_id":3825,"_source":885,"_file":3826,"_stem":3827,"_extension":888},"\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",[1332,15],{"type":17,"children":3372,"toc":3813},[3373,3379,3384,3394,3402,3407,3417,3425,3430,3440,3448,3453,3459,3471,3479,3483,3540,3546,3551,3560,3565,3574,3579,3588,3594,3599,3608,3613,3622,3628,3633,3642,3646,3655,3661,3666,3675,3680,3686,3695,3701,3706,3714,3720,3725,3733,3739,3808],{"type":20,"tag":21,"props":3374,"children":3376},{"id":3375},"the-folder-structure-problem",[3377],{"type":26,"value":3378},"The Folder Structure Problem",{"type":20,"tag":29,"props":3380,"children":3381},{},[3382],{"type":26,"value":3383},"I've seen three patterns:",{"type":20,"tag":29,"props":3385,"children":3386},{},[3387,3392],{"type":20,"tag":48,"props":3388,"children":3389},{},[3390],{"type":26,"value":3391},"Chaos Model",{"type":26,"value":3393}," (what startups do):",{"type":20,"tag":157,"props":3395,"children":3397},{"code":3396},"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",[3398],{"type":20,"tag":165,"props":3399,"children":3400},{"__ignoreMap":8},[3401],{"type":26,"value":3396},{"type":20,"tag":29,"props":3403,"children":3404},{},[3405],{"type":26,"value":3406},"After 6 months, nobody remembers where anything is.",{"type":20,"tag":29,"props":3408,"children":3409},{},[3410,3415],{"type":20,"tag":48,"props":3411,"children":3412},{},[3413],{"type":26,"value":3414},"Rails Model",{"type":26,"value":3416}," (what some teams copy):",{"type":20,"tag":157,"props":3418,"children":3420},{"code":3419},"src\u002F\n├── views\u002F\n├── components\u002F\n├── composables\u002F\n├── stores\u002F\n├── utils\u002F\n└── services\u002F\n",[3421],{"type":20,"tag":165,"props":3422,"children":3423},{"__ignoreMap":8},[3424],{"type":26,"value":3419},{"type":20,"tag":29,"props":3426,"children":3427},{},[3428],{"type":26,"value":3429},"Better, but still unclear what belongs where.",{"type":20,"tag":29,"props":3431,"children":3432},{},[3433,3438],{"type":20,"tag":48,"props":3434,"children":3435},{},[3436],{"type":26,"value":3437},"Domain-Driven Model",{"type":26,"value":3439}," (what scales):",{"type":20,"tag":157,"props":3441,"children":3443},{"code":3442},"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",[3444],{"type":20,"tag":165,"props":3445,"children":3446},{"__ignoreMap":8},[3447],{"type":26,"value":3442},{"type":20,"tag":29,"props":3449,"children":3450},{},[3451],{"type":26,"value":3452},"We'll focus on the domain-driven model.",{"type":20,"tag":21,"props":3454,"children":3456},{"id":3455},"the-domain-driven-structure",[3457],{"type":26,"value":3458},"The Domain-Driven Structure",{"type":20,"tag":29,"props":3460,"children":3461},{},[3462,3464,3469],{"type":26,"value":3463},"Organize by ",{"type":20,"tag":48,"props":3465,"children":3466},{},[3467],{"type":26,"value":3468},"business domain",{"type":26,"value":3470},", not technical layer:",{"type":20,"tag":157,"props":3472,"children":3474},{"code":3473},"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",[3475],{"type":20,"tag":165,"props":3476,"children":3477},{"__ignoreMap":8},[3478],{"type":26,"value":3473},{"type":20,"tag":29,"props":3480,"children":3481},{},[3482],{"type":26,"value":1416},{"type":20,"tag":40,"props":3484,"children":3485},{},[3486,3496,3520,3530],{"type":20,"tag":44,"props":3487,"children":3488},{},[3489,3494],{"type":20,"tag":48,"props":3490,"children":3491},{},[3492],{"type":26,"value":3493},"Feature cohesion",{"type":26,"value":3495}," — all code for \"users\" is in one place",{"type":20,"tag":44,"props":3497,"children":3498},{},[3499,3504,3506,3512,3514],{"type":20,"tag":48,"props":3500,"children":3501},{},[3502],{"type":26,"value":3503},"Independent shipping",{"type":26,"value":3505}," — one team owns ",{"type":20,"tag":165,"props":3507,"children":3509},{"className":3508},[],[3510],{"type":26,"value":3511},"domains\u002Fusers",{"type":26,"value":3513},", another owns ",{"type":20,"tag":165,"props":3515,"children":3517},{"className":3516},[],[3518],{"type":26,"value":3519},"domains\u002Fbilling",{"type":20,"tag":44,"props":3521,"children":3522},{},[3523,3528],{"type":20,"tag":48,"props":3524,"children":3525},{},[3526],{"type":26,"value":3527},"Clear dependencies",{"type":26,"value":3529}," — easy to see what each domain needs",{"type":20,"tag":44,"props":3531,"children":3532},{},[3533,3538],{"type":20,"tag":48,"props":3534,"children":3535},{},[3536],{"type":26,"value":3537},"Scaling",{"type":26,"value":3539}," — add new domains without touching existing ones",{"type":20,"tag":21,"props":3541,"children":3543},{"id":3542},"the-barrel-export-pattern",[3544],{"type":26,"value":3545},"The Barrel Export Pattern",{"type":20,"tag":29,"props":3547,"children":3548},{},[3549],{"type":26,"value":3550},"Each domain exports a public API:",{"type":20,"tag":157,"props":3552,"children":3555},{"code":3553,"language":610,"meta":8,"className":3554},"\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",[608],[3556],{"type":20,"tag":165,"props":3557,"children":3558},{"__ignoreMap":8},[3559],{"type":26,"value":3553},{"type":20,"tag":29,"props":3561,"children":3562},{},[3563],{"type":26,"value":3564},"Usage:",{"type":20,"tag":157,"props":3566,"children":3569},{"code":3567,"language":610,"meta":8,"className":3568},"\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",[608],[3570],{"type":20,"tag":165,"props":3571,"children":3572},{"__ignoreMap":8},[3573],{"type":26,"value":3567},{"type":20,"tag":29,"props":3575,"children":3576},{},[3577],{"type":26,"value":3578},"Enforce this with eslint:",{"type":20,"tag":157,"props":3580,"children":3583},{"code":3581,"language":3058,"meta":8,"className":3582},"\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",[3060],[3584],{"type":20,"tag":165,"props":3585,"children":3586},{"__ignoreMap":8},[3587],{"type":26,"value":3581},{"type":20,"tag":21,"props":3589,"children":3591},{"id":3590},"composables-the-shared-logic",[3592],{"type":26,"value":3593},"Composables: The Shared Logic",{"type":20,"tag":29,"props":3595,"children":3596},{},[3597],{"type":26,"value":3598},"Composables are where reusable logic lives:",{"type":20,"tag":157,"props":3600,"children":3603},{"code":3601,"language":610,"meta":8,"className":3602},"\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",[608],[3604],{"type":20,"tag":165,"props":3605,"children":3606},{"__ignoreMap":8},[3607],{"type":26,"value":3601},{"type":20,"tag":29,"props":3609,"children":3610},{},[3611],{"type":26,"value":3612},"Use it:",{"type":20,"tag":157,"props":3614,"children":3617},{"code":3615,"language":162,"meta":8,"className":3616},"\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",[160],[3618],{"type":20,"tag":165,"props":3619,"children":3620},{"__ignoreMap":8},[3621],{"type":26,"value":3615},{"type":20,"tag":21,"props":3623,"children":3625},{"id":3624},"dependency-injection",[3626],{"type":26,"value":3627},"Dependency Injection",{"type":20,"tag":29,"props":3629,"children":3630},{},[3631],{"type":26,"value":3632},"For large apps, inject dependencies instead of hardcoding:",{"type":20,"tag":157,"props":3634,"children":3637},{"code":3635,"language":610,"meta":8,"className":3636},"\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",[608],[3638],{"type":20,"tag":165,"props":3639,"children":3640},{"__ignoreMap":8},[3641],{"type":26,"value":3635},{"type":20,"tag":29,"props":3643,"children":3644},{},[3645],{"type":26,"value":3612},{"type":20,"tag":157,"props":3647,"children":3650},{"code":3648,"language":610,"meta":8,"className":3649},"\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",[608],[3651],{"type":20,"tag":165,"props":3652,"children":3653},{"__ignoreMap":8},[3654],{"type":26,"value":3648},{"type":20,"tag":21,"props":3656,"children":3658},{"id":3657},"types-organization",[3659],{"type":26,"value":3660},"Types Organization",{"type":20,"tag":29,"props":3662,"children":3663},{},[3664],{"type":26,"value":3665},"Centralize types:",{"type":20,"tag":157,"props":3667,"children":3670},{"code":3668,"language":610,"meta":8,"className":3669},"\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",[608],[3671],{"type":20,"tag":165,"props":3672,"children":3673},{"__ignoreMap":8},[3674],{"type":26,"value":3668},{"type":20,"tag":29,"props":3676,"children":3677},{},[3678],{"type":26,"value":3679},"Never use inline interfaces.",{"type":20,"tag":21,"props":3681,"children":3683},{"id":3682},"naming-conventions",[3684],{"type":26,"value":3685},"Naming Conventions",{"type":20,"tag":157,"props":3687,"children":3690},{"code":3688,"language":610,"meta":8,"className":3689},"\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",[608],[3691],{"type":20,"tag":165,"props":3692,"children":3693},{"__ignoreMap":8},[3694],{"type":26,"value":3688},{"type":20,"tag":21,"props":3696,"children":3698},{"id":3697},"growing-the-structure",[3699],{"type":26,"value":3700},"Growing the Structure",{"type":20,"tag":29,"props":3702,"children":3703},{},[3704],{"type":26,"value":3705},"As you add more code, follow these patterns:",{"type":20,"tag":157,"props":3707,"children":3709},{"code":3708},"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",[3710],{"type":20,"tag":165,"props":3711,"children":3712},{"__ignoreMap":8},[3713],{"type":26,"value":3708},{"type":20,"tag":21,"props":3715,"children":3717},{"id":3716},"scaling-to-100k-lines",[3718],{"type":26,"value":3719},"Scaling to 100K+ Lines",{"type":20,"tag":29,"props":3721,"children":3722},{},[3723],{"type":26,"value":3724},"At scale, add:",{"type":20,"tag":157,"props":3726,"children":3728},{"code":3727},"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",[3729],{"type":20,"tag":165,"props":3730,"children":3731},{"__ignoreMap":8},[3732],{"type":26,"value":3727},{"type":20,"tag":21,"props":3734,"children":3736},{"id":3735},"key-rules",[3737],{"type":26,"value":3738},"Key Rules",{"type":20,"tag":918,"props":3740,"children":3741},{},[3742,3752,3762,3772,3782,3798],{"type":20,"tag":44,"props":3743,"children":3744},{},[3745,3750],{"type":20,"tag":48,"props":3746,"children":3747},{},[3748],{"type":26,"value":3749},"Keep domains independent",{"type":26,"value":3751}," — Users domain can't import from Billing",{"type":20,"tag":44,"props":3753,"children":3754},{},[3755,3760],{"type":20,"tag":48,"props":3756,"children":3757},{},[3758],{"type":26,"value":3759},"Shared is truly shared",{"type":26,"value":3761}," — only reusable across domains",{"type":20,"tag":44,"props":3763,"children":3764},{},[3765,3770],{"type":20,"tag":48,"props":3766,"children":3767},{},[3768],{"type":26,"value":3769},"Use barrel exports",{"type":26,"value":3771}," — clean public APIs",{"type":20,"tag":44,"props":3773,"children":3774},{},[3775,3780],{"type":20,"tag":48,"props":3776,"children":3777},{},[3778],{"type":26,"value":3779},"Type everything",{"type":26,"value":3781}," — centralize types by domain",{"type":20,"tag":44,"props":3783,"children":3784},{},[3785,3790,3792],{"type":20,"tag":48,"props":3786,"children":3787},{},[3788],{"type":26,"value":3789},"One entry point",{"type":26,"value":3791}," — each domain exports from ",{"type":20,"tag":165,"props":3793,"children":3795},{"className":3794},[],[3796],{"type":26,"value":3797},"index.ts",{"type":20,"tag":44,"props":3799,"children":3800},{},[3801,3806],{"type":20,"tag":48,"props":3802,"children":3803},{},[3804],{"type":26,"value":3805},"Enforce with linting",{"type":26,"value":3807}," — catch violations early",{"type":20,"tag":29,"props":3809,"children":3810},{},[3811],{"type":26,"value":3812},"This structure scales from 10 engineers to 100. Your codebase will thank you.",{"title":8,"searchDepth":870,"depth":870,"links":3814},[3815,3816,3817,3818,3819,3820,3821,3822,3823,3824],{"id":3375,"depth":870,"text":3378},{"id":3455,"depth":870,"text":3458},{"id":3542,"depth":870,"text":3545},{"id":3590,"depth":870,"text":3593},{"id":3624,"depth":870,"text":3627},{"id":3657,"depth":870,"text":3660},{"id":3682,"depth":870,"text":3685},{"id":3697,"depth":870,"text":3700},{"id":3716,"depth":870,"text":3719},{"id":3735,"depth":870,"text":3738},"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":3829,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":3830,"description":3831,"date":3832,"updated":894,"readingTime":3833,"tags":3834,"featured":654,"body":3835,"_type":883,"_id":4387,"_source":885,"_file":4388,"_stem":4389,"_extension":888},"\u002Fblog\u002Fnuxt-3-performance-optimization-guide","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",14,[1881,1880],{"type":17,"children":3836,"toc":4372},[3837,3843,3855,3868,3874,3880,3885,3894,3899,3932,3938,3943,3952,3962,3980,3986,3991,4000,4005,4014,4027,4036,4046,4052,4057,4066,4071,4077,4082,4091,4101,4112,4117,4123,4128,4137,4142,4160,4165,4171,4176,4185,4190,4196,4201,4255,4267,4273,4278,4287,4292,4310,4314,4367],{"type":20,"tag":21,"props":3838,"children":3840},{"id":3839},"starting-point-a-slow-production-app",[3841],{"type":26,"value":3842},"Starting Point: A Slow Production App",{"type":20,"tag":29,"props":3844,"children":3845},{},[3846,3848,3853],{"type":26,"value":3847},"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":20,"tag":48,"props":3849,"children":3850},{},[3851],{"type":26,"value":3852},"4.2s",{"type":26,"value":3854}," on throttled mobile. By Google's standards, anything over 2.5s for LCP is failing, and every user felt it.",{"type":20,"tag":29,"props":3856,"children":3857},{},[3858,3860,3866],{"type":26,"value":3859},"The full program behind this post — what was tried, what was rejected, and how the results are enforced — is written up in the ",{"type":20,"tag":1380,"props":3861,"children":3863},{"href":3862},"\u002Fcase-studies\u002Fsaas-performance-program",[3864],{"type":26,"value":3865},"SaaS performance case study",{"type":26,"value":3867},". This post is the generalized playbook.",{"type":20,"tag":21,"props":3869,"children":3871},{"id":3870},"the-optimization-playbook",[3872],{"type":26,"value":3873},"The Optimization Playbook",{"type":20,"tag":1032,"props":3875,"children":3877},{"id":3876},"_1-analyze-with-lighthouse-bundle-analysis",[3878],{"type":26,"value":3879},"1. Analyze with Lighthouse & Bundle Analysis",{"type":20,"tag":29,"props":3881,"children":3882},{},[3883],{"type":26,"value":3884},"Before optimizing, we measured everything:",{"type":20,"tag":157,"props":3886,"children":3889},{"className":3887,"code":3888,"language":599,"meta":8},[597],"npm run build --analyze\n# Generated visual bundle report\n\n# Lighthouse CI integration\nnpm install -D @lhci\u002Fcli@0.9.x\n",[3890],{"type":20,"tag":165,"props":3891,"children":3892},{"__ignoreMap":8},[3893],{"type":26,"value":3888},{"type":20,"tag":29,"props":3895,"children":3896},{},[3897],{"type":26,"value":3898},"A typical analysis of an unoptimized app reveals something like this (illustrative example — run your own):",{"type":20,"tag":40,"props":3900,"children":3901},{},[3902,3912,3922],{"type":20,"tag":44,"props":3903,"children":3904},{},[3905,3910],{"type":20,"tag":48,"props":3906,"children":3907},{},[3908],{"type":26,"value":3909},"Main bundle",{"type":26,"value":3911},": a few hundred KB gzipped of code the first paint doesn't need",{"type":20,"tag":44,"props":3913,"children":3914},{},[3915,3920],{"type":20,"tag":48,"props":3916,"children":3917},{},[3918],{"type":26,"value":3919},"Vendor bundle",{"type":26,"value":3921},": duplicate versions of the same utility library, bundled separately",{"type":20,"tag":44,"props":3923,"children":3924},{},[3925,3930],{"type":20,"tag":48,"props":3926,"children":3927},{},[3928],{"type":26,"value":3929},"Images",{"type":26,"value":3931},": the largest single line item — no WebP, no lazy loading, full-resolution on mobile",{"type":20,"tag":1032,"props":3933,"children":3935},{"id":3934},"_2-code-splitting-strategy",[3936],{"type":26,"value":3937},"2. Code Splitting Strategy",{"type":20,"tag":29,"props":3939,"children":3940},{},[3941],{"type":26,"value":3942},"Nuxt 3 has better defaults than Nuxt 2, but we needed aggressive splitting:",{"type":20,"tag":157,"props":3944,"children":3947},{"className":3945,"code":3946,"language":610,"meta":8},[608],"\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",[3948],{"type":20,"tag":165,"props":3949,"children":3950},{"__ignoreMap":8},[3951],{"type":26,"value":3946},{"type":20,"tag":29,"props":3953,"children":3954},{},[3955,3960],{"type":20,"tag":48,"props":3956,"children":3957},{},[3958],{"type":26,"value":3959},"What to expect",{"type":26,"value":3961}," (illustrative example — your numbers will differ):",{"type":20,"tag":40,"props":3963,"children":3964},{},[3965,3970,3975],{"type":20,"tag":44,"props":3966,"children":3967},{},[3968],{"type":26,"value":3969},"The main bundle drops to whatever the first route genuinely needs",{"type":20,"tag":44,"props":3971,"children":3972},{},[3973],{"type":26,"value":3974},"Each lazy route loads a small chunk on demand",{"type":20,"tag":44,"props":3976,"children":3977},{},[3978],{"type":26,"value":3979},"Routes render without waiting for full app initialization",{"type":20,"tag":1032,"props":3981,"children":3983},{"id":3982},"_3-image-optimization",[3984],{"type":26,"value":3985},"3. Image Optimization",{"type":20,"tag":29,"props":3987,"children":3988},{},[3989],{"type":26,"value":3990},"This single change had the biggest impact:",{"type":20,"tag":157,"props":3992,"children":3995},{"className":3993,"code":3994,"language":162,"meta":8},[160],"\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",[3996],{"type":20,"tag":165,"props":3997,"children":3998},{"__ignoreMap":8},[3999],{"type":26,"value":3994},{"type":20,"tag":29,"props":4001,"children":4002},{},[4003],{"type":26,"value":4004},"Install the Nuxt Image module:",{"type":20,"tag":157,"props":4006,"children":4009},{"className":4007,"code":4008,"language":599,"meta":8},[597],"npm install @nuxt\u002Fimage\n",[4010],{"type":20,"tag":165,"props":4011,"children":4012},{"__ignoreMap":8},[4013],{"type":26,"value":4008},{"type":20,"tag":29,"props":4015,"children":4016},{},[4017,4019,4025],{"type":26,"value":4018},"Configure in ",{"type":20,"tag":165,"props":4020,"children":4022},{"className":4021},[],[4023],{"type":26,"value":4024},"nuxt.config.ts",{"type":26,"value":4026},":",{"type":20,"tag":157,"props":4028,"children":4031},{"className":4029,"code":4030,"language":610,"meta":8},[608],"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",[4032],{"type":20,"tag":165,"props":4033,"children":4034},{"__ignoreMap":8},[4035],{"type":26,"value":4030},{"type":20,"tag":29,"props":4037,"children":4038},{},[4039,4044],{"type":20,"tag":48,"props":4040,"children":4041},{},[4042],{"type":26,"value":4043},"Why this matters most",{"type":26,"value":4045},": 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":20,"tag":1032,"props":4047,"children":4049},{"id":4048},"_4-server-side-rendering-ssr-optimization",[4050],{"type":26,"value":4051},"4. Server-Side Rendering (SSR) Optimization",{"type":20,"tag":29,"props":4053,"children":4054},{},[4055],{"type":26,"value":4056},"Nuxt 3's SSR is better, but configuration matters:",{"type":20,"tag":157,"props":4058,"children":4061},{"className":4059,"code":4060,"language":610,"meta":8},[608],"\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",[4062],{"type":20,"tag":165,"props":4063,"children":4064},{"__ignoreMap":8},[4065],{"type":26,"value":4060},{"type":20,"tag":29,"props":4067,"children":4068},{},[4069],{"type":26,"value":4070},"The result: time-to-first-byte drops sharply once cacheable responses stop hitting the origin (illustrative — TTFB depends heavily on your hosting).",{"type":20,"tag":1032,"props":4072,"children":4074},{"id":4073},"_5-font-loading-optimization",[4075],{"type":26,"value":4076},"5. Font Loading Optimization",{"type":20,"tag":29,"props":4078,"children":4079},{},[4080],{"type":26,"value":4081},"Fonts are invisible but heavy. We optimized aggressively:",{"type":20,"tag":157,"props":4083,"children":4086},{"className":4084,"code":4085,"language":610,"meta":8},[608],"\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",[4087],{"type":20,"tag":165,"props":4088,"children":4089},{"__ignoreMap":8},[4090],{"type":26,"value":4085},{"type":20,"tag":29,"props":4092,"children":4093},{},[4094,4096,4100],{"type":26,"value":4095},"But the real win came from ",{"type":20,"tag":48,"props":4097,"children":4098},{},[4099],{"type":26,"value":2259},{"type":26,"value":4026},{"type":20,"tag":157,"props":4102,"children":4107},{"className":4103,"code":4105,"language":4106,"meta":8},[4104],"language-css","@import url('https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap');\n","css",[4108],{"type":20,"tag":165,"props":4109,"children":4110},{"__ignoreMap":8},[4111],{"type":26,"value":4105},{"type":20,"tag":29,"props":4113,"children":4114},{},[4115],{"type":26,"value":4116},"This tells the browser: \"Show text immediately with fallback, swap in custom font when ready.\" No invisible text delays.",{"type":20,"tag":1032,"props":4118,"children":4120},{"id":4119},"_6-hydration-optimization",[4121],{"type":26,"value":4122},"6. Hydration Optimization",{"type":20,"tag":29,"props":4124,"children":4125},{},[4126],{"type":26,"value":4127},"This is a Nuxt 3 secret weapon. Partial hydration means not every component needs JavaScript:",{"type":20,"tag":157,"props":4129,"children":4132},{"className":4130,"code":4131,"language":162,"meta":8},[160],"\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",[4133],{"type":20,"tag":165,"props":4134,"children":4135},{"__ignoreMap":8},[4136],{"type":26,"value":4131},{"type":20,"tag":29,"props":4138,"children":4139},{},[4140],{"type":26,"value":4141},"Use it for:",{"type":20,"tag":40,"props":4143,"children":4144},{},[4145,4150,4155],{"type":20,"tag":44,"props":4146,"children":4147},{},[4148],{"type":26,"value":4149},"Hero sections (no JS needed)",{"type":20,"tag":44,"props":4151,"children":4152},{},[4153],{"type":26,"value":4154},"Blog content (pure markup)",{"type":20,"tag":44,"props":4156,"children":4157},{},[4158],{"type":26,"value":4159},"Static sidebars",{"type":20,"tag":29,"props":4161,"children":4162},{},[4163],{"type":26,"value":4164},"Static sections that skip hydration are JavaScript the browser never has to parse.",{"type":20,"tag":1032,"props":4166,"children":4168},{"id":4167},"_7-caching-strategy",[4169],{"type":26,"value":4170},"7. Caching Strategy",{"type":20,"tag":29,"props":4172,"children":4173},{},[4174],{"type":26,"value":4175},"Implemented a tiered caching approach:",{"type":20,"tag":157,"props":4177,"children":4180},{"className":4178,"code":4179,"language":610,"meta":8},[608],"\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",[4181],{"type":20,"tag":165,"props":4182,"children":4183},{"__ignoreMap":8},[4184],{"type":26,"value":4179},{"type":20,"tag":29,"props":4186,"children":4187},{},[4188],{"type":26,"value":4189},"With a tiered cache, most repeat API reads never reach the database.",{"type":20,"tag":21,"props":4191,"children":4193},{"id":4192},"the-final-results",[4194],{"type":26,"value":4195},"The Final Results",{"type":20,"tag":29,"props":4197,"children":4198},{},[4199],{"type":26,"value":4200},"On the production program this playbook comes from, the headline result was:",{"type":20,"tag":2327,"props":4202,"children":4203},{},[4204,4227],{"type":20,"tag":2331,"props":4205,"children":4206},{},[4207],{"type":20,"tag":2335,"props":4208,"children":4209},{},[4210,4214,4218,4222],{"type":20,"tag":2339,"props":4211,"children":4212},{},[4213],{"type":26,"value":2343},{"type":20,"tag":2339,"props":4215,"children":4216},{},[4217],{"type":26,"value":2348},{"type":20,"tag":2339,"props":4219,"children":4220},{},[4221],{"type":26,"value":2353},{"type":20,"tag":2339,"props":4223,"children":4224},{},[4225],{"type":26,"value":4226},"Improvement",{"type":20,"tag":2355,"props":4228,"children":4229},{},[4230],{"type":20,"tag":2335,"props":4231,"children":4232},{},[4233,4238,4242,4247],{"type":20,"tag":2362,"props":4234,"children":4235},{},[4236],{"type":26,"value":4237},"Largest Contentful Paint",{"type":20,"tag":2362,"props":4239,"children":4240},{},[4241],{"type":26,"value":3852},{"type":20,"tag":2362,"props":4243,"children":4244},{},[4245],{"type":26,"value":4246},"2.5s",{"type":20,"tag":2362,"props":4248,"children":4249},{},[4250],{"type":20,"tag":48,"props":4251,"children":4252},{},[4253],{"type":26,"value":4254},"−40%",{"type":20,"tag":29,"props":4256,"children":4257},{},[4258,4260,4265],{"type":26,"value":4259},"The other vitals moved in the same direction; the LCP number is the one measured carefully enough to publish. The ",{"type":20,"tag":1380,"props":4261,"children":4262},{"href":3862},[4263],{"type":26,"value":4264},"case study",{"type":26,"value":4266}," covers how it was measured and enforced.",{"type":20,"tag":21,"props":4268,"children":4270},{"id":4269},"performance-budget-going-forward",[4271],{"type":26,"value":4272},"Performance Budget Going Forward",{"type":20,"tag":29,"props":4274,"children":4275},{},[4276],{"type":26,"value":4277},"We set strict budgets to prevent regressions:",{"type":20,"tag":157,"props":4279,"children":4282},{"className":4280,"code":4281,"language":599,"meta":8},[597],"# 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",[4283],{"type":20,"tag":165,"props":4284,"children":4285},{"__ignoreMap":8},[4286],{"type":26,"value":4281},{"type":20,"tag":29,"props":4288,"children":4289},{},[4290],{"type":26,"value":4291},"Thresholds:",{"type":20,"tag":40,"props":4293,"children":4294},{},[4295,4300,4305],{"type":20,"tag":44,"props":4296,"children":4297},{},[4298],{"type":26,"value":4299},"Main bundle: \u003C 200KB gzipped",{"type":20,"tag":44,"props":4301,"children":4302},{},[4303],{"type":26,"value":4304},"Per route: \u003C 80KB gzipped",{"type":20,"tag":44,"props":4306,"children":4307},{},[4308],{"type":26,"value":4309},"Images: \u003C 1MB total per page",{"type":20,"tag":21,"props":4311,"children":4312},{"id":2467},[4313],{"type":26,"value":2470},{"type":20,"tag":918,"props":4315,"children":4316},{},[4317,4327,4337,4347,4357],{"type":20,"tag":44,"props":4318,"children":4319},{},[4320,4325],{"type":20,"tag":48,"props":4321,"children":4322},{},[4323],{"type":26,"value":4324},"Measure first",{"type":26,"value":4326}," — every optimization should be data-driven",{"type":20,"tag":44,"props":4328,"children":4329},{},[4330,4335],{"type":20,"tag":48,"props":4331,"children":4332},{},[4333],{"type":26,"value":4334},"Images are the lowest-hanging fruit",{"type":26,"value":4336}," — image handling routinely delivers the largest share of the wins",{"type":20,"tag":44,"props":4338,"children":4339},{},[4340,4345],{"type":20,"tag":48,"props":4341,"children":4342},{},[4343],{"type":26,"value":4344},"HTTP caching > code optimization",{"type":26,"value":4346}," — smart caching beats micro-optimizations",{"type":20,"tag":44,"props":4348,"children":4349},{},[4350,4355],{"type":20,"tag":48,"props":4351,"children":4352},{},[4353],{"type":26,"value":4354},"Nuxt 3's primitives are powerful",{"type":26,"value":4356}," — when used correctly, you don't need complex workarounds",{"type":20,"tag":44,"props":4358,"children":4359},{},[4360,4365],{"type":20,"tag":48,"props":4361,"children":4362},{},[4363],{"type":26,"value":4364},"Monitor in production",{"type":26,"value":4366}," — real-world metrics differ from labs. Use tools like Sentry, Vercel Analytics",{"type":20,"tag":29,"props":4368,"children":4369},{},[4370],{"type":26,"value":4371},"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":8,"searchDepth":870,"depth":870,"links":4373},[4374,4375,4384,4385,4386],{"id":3839,"depth":870,"text":3842},{"id":3870,"depth":870,"text":3873,"children":4376},[4377,4378,4379,4380,4381,4382,4383],{"id":3876,"depth":1314,"text":3879},{"id":3934,"depth":1314,"text":3937},{"id":3982,"depth":1314,"text":3985},{"id":4048,"depth":1314,"text":4051},{"id":4073,"depth":1314,"text":4076},{"id":4119,"depth":1314,"text":4122},{"id":4167,"depth":1314,"text":4170},{"id":4192,"depth":870,"text":4195},{"id":4269,"depth":870,"text":4272},{"id":2467,"depth":870,"text":2470},"content:blog:nuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide",{"_path":4391,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":4392,"description":4393,"date":4394,"updated":894,"readingTime":4395,"tags":4396,"featured":7,"body":4397,"_type":883,"_id":5121,"_source":885,"_file":5122,"_stem":5123,"_extension":888},"\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,[1880],{"type":17,"children":4398,"toc":5099},[4399,4405,4417,4429,4460,4465,4471,4476,4485,4490,4508,4513,4569,4575,4581,4590,4596,4605,4611,4620,4626,4635,4640,4658,4668,4674,4679,4688,4694,4703,4709,4718,4731,4736,4754,4760,4765,4774,4783,4788,4797,4802,4808,4814,4823,4829,4838,4844,4854,4949,4961,4979,4985,5038,5042,5094],{"type":20,"tag":21,"props":4400,"children":4402},{"id":4401},"the-bundle-problem",[4403],{"type":26,"value":4404},"The Bundle Problem",{"type":20,"tag":29,"props":4406,"children":4407},{},[4408,4410,4415],{"type":26,"value":4409},"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":20,"tag":48,"props":4411,"children":4412},{},[4413],{"type":26,"value":4414},"35%",{"type":26,"value":4416}," using the approach in this guide — the strategy below is the repeatable part.",{"type":20,"tag":29,"props":4418,"children":4419},{},[4420,4422,4427],{"type":26,"value":4421},"To make the mechanics concrete, this guide walks through a representative large Vue app. ",{"type":20,"tag":48,"props":4423,"children":4424},{},[4425],{"type":26,"value":4426},"All specific sizes below are an illustrative example",{"type":26,"value":4428},", not measurements from one project:",{"type":20,"tag":40,"props":4430,"children":4431},{},[4432,4441,4450],{"type":20,"tag":44,"props":4433,"children":4434},{},[4435,4439],{"type":20,"tag":48,"props":4436,"children":4437},{},[4438],{"type":26,"value":3909},{"type":26,"value":4440},": 850 KB (gzipped: 280 KB)",{"type":20,"tag":44,"props":4442,"children":4443},{},[4444,4448],{"type":20,"tag":48,"props":4445,"children":4446},{},[4447],{"type":26,"value":3919},{"type":26,"value":4449},": 450 KB (gzipped: 150 KB)",{"type":20,"tag":44,"props":4451,"children":4452},{},[4453,4458],{"type":20,"tag":48,"props":4454,"children":4455},{},[4456],{"type":26,"value":4457},"Total",{"type":26,"value":4459},": 1.2 MB gzipped",{"type":20,"tag":29,"props":4461,"children":4462},{},[4463],{"type":26,"value":4464},"At 3G speeds (1 Mbps), that's 10+ seconds just for JavaScript.",{"type":20,"tag":21,"props":4466,"children":4468},{"id":4467},"step-1-analyze-the-damage",[4469],{"type":26,"value":4470},"Step 1: Analyze the Damage",{"type":20,"tag":29,"props":4472,"children":4473},{},[4474],{"type":26,"value":4475},"First, visualize where bytes are going:",{"type":20,"tag":157,"props":4477,"children":4480},{"className":4478,"code":4479,"language":599,"meta":8},[597],"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",[4481],{"type":20,"tag":165,"props":4482,"children":4483},{"__ignoreMap":8},[4484],{"type":26,"value":4479},{"type":20,"tag":29,"props":4486,"children":4487},{},[4488],{"type":26,"value":4489},"You'll see:",{"type":20,"tag":40,"props":4491,"children":4492},{},[4493,4498,4503],{"type":20,"tag":44,"props":4494,"children":4495},{},[4496],{"type":26,"value":4497},"What packages are large",{"type":20,"tag":44,"props":4499,"children":4500},{},[4501],{"type":26,"value":4502},"What's imported but unused",{"type":20,"tag":44,"props":4504,"children":4505},{},[4506],{"type":26,"value":4507},"Duplicate dependencies at different versions",{"type":20,"tag":29,"props":4509,"children":4510},{},[4511],{"type":26,"value":4512},"A typical analysis reveals (illustrative example):",{"type":20,"tag":40,"props":4514,"children":4515},{},[4516,4527,4545,4556],{"type":20,"tag":44,"props":4517,"children":4518},{},[4519,4525],{"type":20,"tag":165,"props":4520,"children":4522},{"className":4521},[],[4523],{"type":26,"value":4524},"lodash-es",{"type":26,"value":4526},": 71 KB (for 3 functions actually used)",{"type":20,"tag":44,"props":4528,"children":4529},{},[4530,4536,4538,4544],{"type":20,"tag":165,"props":4531,"children":4533},{"className":4532},[],[4534],{"type":26,"value":4535},"moment",{"type":26,"value":4537},": 65 KB (replaced with ",{"type":20,"tag":165,"props":4539,"children":4541},{"className":4540},[],[4542],{"type":26,"value":4543},"date-fns",{"type":26,"value":288},{"type":20,"tag":44,"props":4546,"children":4547},{},[4548,4554],{"type":20,"tag":165,"props":4549,"children":4551},{"className":4550},[],[4552],{"type":26,"value":4553},"chart.js",{"type":26,"value":4555},": 120 KB (only used on one page)",{"type":20,"tag":44,"props":4557,"children":4558},{},[4559,4561,4567],{"type":26,"value":4560},"Duplicate ",{"type":20,"tag":165,"props":4562,"children":4564},{"className":4563},[],[4565],{"type":26,"value":4566},"axios",{"type":26,"value":4568},": two versions imported by different packages",{"type":20,"tag":21,"props":4570,"children":4572},{"id":4571},"step-2-tree-shake-aggressively",[4573],{"type":26,"value":4574},"Step 2: Tree-Shake Aggressively",{"type":20,"tag":1032,"props":4576,"children":4578},{"id":4577},"remove-unused-packages",[4579],{"type":26,"value":4580},"Remove Unused Packages",{"type":20,"tag":157,"props":4582,"children":4585},{"className":4583,"code":4584,"language":599,"meta":8},[597],"# Find unused dependencies\nnpm install -g depcheck\ndepcheck\n\n# Removes 50+ KB of dead code\nnpm uninstall unused-package\n",[4586],{"type":20,"tag":165,"props":4587,"children":4588},{"__ignoreMap":8},[4589],{"type":26,"value":4584},{"type":20,"tag":1032,"props":4591,"children":4593},{"id":4592},"replace-heavy-packages",[4594],{"type":26,"value":4595},"Replace Heavy Packages",{"type":20,"tag":157,"props":4597,"children":4600},{"className":4598,"code":4599,"language":610,"meta":8},[608],"\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",[4601],{"type":20,"tag":165,"props":4602,"children":4603},{"__ignoreMap":8},[4604],{"type":26,"value":4599},{"type":20,"tag":1032,"props":4606,"children":4608},{"id":4607},"dependency-consolidation",[4609],{"type":26,"value":4610},"Dependency Consolidation",{"type":20,"tag":157,"props":4612,"children":4615},{"className":4613,"code":4614,"language":610,"meta":8},[608],"\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",[4616],{"type":20,"tag":165,"props":4617,"children":4618},{"__ignoreMap":8},[4619],{"type":26,"value":4614},{"type":20,"tag":1032,"props":4621,"children":4623},{"id":4622},"build-config-for-tree-shaking",[4624],{"type":26,"value":4625},"Build Config for Tree-Shaking",{"type":20,"tag":157,"props":4627,"children":4630},{"className":4628,"code":4629,"language":610,"meta":8},[608],"\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",[4631],{"type":20,"tag":165,"props":4632,"children":4633},{"__ignoreMap":8},[4634],{"type":26,"value":4629},{"type":20,"tag":29,"props":4636,"children":4637},{},[4638],{"type":26,"value":4639},"Results for the illustrative example app:",{"type":20,"tag":40,"props":4641,"children":4642},{},[4643,4648,4653],{"type":20,"tag":44,"props":4644,"children":4645},{},[4646],{"type":26,"value":4647},"Removed unused: -150 KB",{"type":20,"tag":44,"props":4649,"children":4650},{},[4651],{"type":26,"value":4652},"Replaced heavy packages: -120 KB",{"type":20,"tag":44,"props":4654,"children":4655},{},[4656],{"type":26,"value":4657},"Tree-shaking optimized: -40 KB",{"type":20,"tag":29,"props":4659,"children":4660},{},[4661,4666],{"type":20,"tag":48,"props":4662,"children":4663},{},[4664],{"type":26,"value":4665},"Total: -310 KB (27% reduction)",{"type":26,"value":4667}," — again, illustrative; the ratio between the three buckets is what generalizes.",{"type":20,"tag":21,"props":4669,"children":4671},{"id":4670},"step-3-smart-code-splitting",[4672],{"type":26,"value":4673},"Step 3: Smart Code Splitting",{"type":20,"tag":29,"props":4675,"children":4676},{},[4677],{"type":26,"value":4678},"Load code only when needed:",{"type":20,"tag":157,"props":4680,"children":4683},{"className":4681,"code":4682,"language":610,"meta":8},[608],"\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",[4684],{"type":20,"tag":165,"props":4685,"children":4686},{"__ignoreMap":8},[4687],{"type":26,"value":4682},{"type":20,"tag":1032,"props":4689,"children":4691},{"id":4690},"split-heavy-features",[4692],{"type":26,"value":4693},"Split Heavy Features",{"type":20,"tag":157,"props":4695,"children":4698},{"className":4696,"code":4697,"language":610,"meta":8},[608],"\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",[4699],{"type":20,"tag":165,"props":4700,"children":4701},{"__ignoreMap":8},[4702],{"type":26,"value":4697},{"type":20,"tag":1032,"props":4704,"children":4706},{"id":4705},"webpack-magic-comment",[4707],{"type":26,"value":4708},"Webpack Magic Comment",{"type":20,"tag":157,"props":4710,"children":4713},{"className":4711,"code":4712,"language":610,"meta":8},[608],"\u002F\u002F Name the chunk for better debugging\nconst Dashboard = () =>\n  import(\n    \u002F* webpackChunkName: \"dashboard\" *\u002F\n    '@\u002Fpages\u002FDashboard.vue'\n  )\n",[4714],{"type":20,"tag":165,"props":4715,"children":4716},{"__ignoreMap":8},[4717],{"type":26,"value":4712},{"type":20,"tag":29,"props":4719,"children":4720},{},[4721,4723,4729],{"type":26,"value":4722},"Generates: ",{"type":20,"tag":165,"props":4724,"children":4726},{"className":4725},[],[4727],{"type":26,"value":4728},"dist\u002Fdashboard.abc123.js",{"type":26,"value":4730}," (clear what it contains)",{"type":20,"tag":29,"props":4732,"children":4733},{},[4734],{"type":26,"value":4735},"Results of code splitting (illustrative example):",{"type":20,"tag":40,"props":4737,"children":4738},{},[4739,4744,4749],{"type":20,"tag":44,"props":4740,"children":4741},{},[4742],{"type":26,"value":4743},"The main bundle shrinks to what the first route actually needs",{"type":20,"tag":44,"props":4745,"children":4746},{},[4747],{"type":26,"value":4748},"Lazy routes load in small on-demand chunks",{"type":20,"tag":44,"props":4750,"children":4751},{},[4752],{"type":26,"value":4753},"First page load stops paying for pages the user never visits",{"type":20,"tag":21,"props":4755,"children":4757},{"id":4756},"step-4-monitor-continuously",[4758],{"type":26,"value":4759},"Step 4: Monitor Continuously",{"type":20,"tag":29,"props":4761,"children":4762},{},[4763],{"type":26,"value":4764},"Prevent regressions:",{"type":20,"tag":157,"props":4766,"children":4769},{"className":4767,"code":4768,"language":599,"meta":8},[597],"npm install -D bundlesize\n",[4770],{"type":20,"tag":165,"props":4771,"children":4772},{"__ignoreMap":8},[4773],{"type":26,"value":4768},{"type":20,"tag":157,"props":4775,"children":4778},{"className":4776,"code":4777,"language":1684,"meta":8},[1686],"\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",[4779],{"type":20,"tag":165,"props":4780,"children":4781},{"__ignoreMap":8},[4782],{"type":26,"value":4777},{"type":20,"tag":29,"props":4784,"children":4785},{},[4786],{"type":26,"value":4787},"GitHub Action:",{"type":20,"tag":157,"props":4789,"children":4792},{"className":4790,"code":4791,"language":1017,"meta":8},[1019],"# .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",[4793],{"type":20,"tag":165,"props":4794,"children":4795},{"__ignoreMap":8},[4796],{"type":26,"value":4791},{"type":20,"tag":29,"props":4798,"children":4799},{},[4800],{"type":26,"value":4801},"Fails PR if bundles exceed limits.",{"type":20,"tag":21,"props":4803,"children":4805},{"id":4804},"step-5-optimize-imports",[4806],{"type":26,"value":4807},"Step 5: Optimize Imports",{"type":20,"tag":1032,"props":4809,"children":4811},{"id":4810},"named-vs-default-exports",[4812],{"type":26,"value":4813},"Named vs Default Exports",{"type":20,"tag":157,"props":4815,"children":4818},{"className":4816,"code":4817,"language":610,"meta":8},[608],"\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",[4819],{"type":20,"tag":165,"props":4820,"children":4821},{"__ignoreMap":8},[4822],{"type":26,"value":4817},{"type":20,"tag":1032,"props":4824,"children":4826},{"id":4825},"avoid-wildcard-imports",[4827],{"type":26,"value":4828},"Avoid Wildcard Imports",{"type":20,"tag":157,"props":4830,"children":4833},{"className":4831,"code":4832,"language":610,"meta":8},[608],"\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",[4834],{"type":20,"tag":165,"props":4835,"children":4836},{"__ignoreMap":8},[4837],{"type":26,"value":4832},{"type":20,"tag":21,"props":4839,"children":4841},{"id":4840},"results-ongoing-monitoring",[4842],{"type":26,"value":4843},"Results & Ongoing Monitoring",{"type":20,"tag":29,"props":4845,"children":4846},{},[4847,4852],{"type":20,"tag":48,"props":4848,"children":4849},{},[4850],{"type":26,"value":4851},"Before & after for the illustrative example app",{"type":26,"value":4853}," (your ratios will vary; the direction won't):",{"type":20,"tag":2327,"props":4855,"children":4856},{},[4857,4879],{"type":20,"tag":2331,"props":4858,"children":4859},{},[4860],{"type":20,"tag":2335,"props":4861,"children":4862},{},[4863,4867,4871,4875],{"type":20,"tag":2339,"props":4864,"children":4865},{},[4866],{"type":26,"value":2343},{"type":20,"tag":2339,"props":4868,"children":4869},{},[4870],{"type":26,"value":2348},{"type":20,"tag":2339,"props":4872,"children":4873},{},[4874],{"type":26,"value":2353},{"type":20,"tag":2339,"props":4876,"children":4877},{},[4878],{"type":26,"value":4226},{"type":20,"tag":2355,"props":4880,"children":4881},{},[4882,4905,4927],{"type":20,"tag":2335,"props":4883,"children":4884},{},[4885,4890,4895,4900],{"type":20,"tag":2362,"props":4886,"children":4887},{},[4888],{"type":26,"value":4889},"Main Bundle",{"type":20,"tag":2362,"props":4891,"children":4892},{},[4893],{"type":26,"value":4894},"850 KB",{"type":20,"tag":2362,"props":4896,"children":4897},{},[4898],{"type":26,"value":4899},"~300 KB",{"type":20,"tag":2362,"props":4901,"children":4902},{},[4903],{"type":26,"value":4904},"large",{"type":20,"tag":2335,"props":4906,"children":4907},{},[4908,4913,4918,4923],{"type":20,"tag":2362,"props":4909,"children":4910},{},[4911],{"type":26,"value":4912},"Vendor Bundle",{"type":20,"tag":2362,"props":4914,"children":4915},{},[4916],{"type":26,"value":4917},"450 KB",{"type":20,"tag":2362,"props":4919,"children":4920},{},[4921],{"type":26,"value":4922},"~180 KB",{"type":20,"tag":2362,"props":4924,"children":4925},{},[4926],{"type":26,"value":4904},{"type":20,"tag":2335,"props":4928,"children":4929},{},[4930,4935,4940,4945],{"type":20,"tag":2362,"props":4931,"children":4932},{},[4933],{"type":26,"value":4934},"First Paint (3G)",{"type":20,"tag":2362,"props":4936,"children":4937},{},[4938],{"type":26,"value":4939},"~8s",{"type":20,"tag":2362,"props":4941,"children":4942},{},[4943],{"type":26,"value":4944},"~2s",{"type":20,"tag":2362,"props":4946,"children":4947},{},[4948],{"type":26,"value":4904},{"type":20,"tag":29,"props":4950,"children":4951},{},[4952,4954,4959],{"type":26,"value":4953},"In my own production use of this strategy, the verified outcome was a ",{"type":20,"tag":48,"props":4955,"children":4956},{},[4957],{"type":26,"value":4958},"35% bundle reduction",{"type":26,"value":4960},". Beyond raw size:",{"type":20,"tag":40,"props":4962,"children":4963},{},[4964,4969,4974],{"type":20,"tag":44,"props":4965,"children":4966},{},[4967],{"type":26,"value":4968},"Faster for all users, especially mobile",{"type":20,"tag":44,"props":4970,"children":4971},{},[4972],{"type":26,"value":4973},"Better SEO (Core Web Vitals improvement)",{"type":20,"tag":44,"props":4975,"children":4976},{},[4977],{"type":26,"value":4978},"Less battery drain on mobile",{"type":20,"tag":21,"props":4980,"children":4982},{"id":4981},"ongoing-strategy",[4983],{"type":26,"value":4984},"Ongoing Strategy",{"type":20,"tag":918,"props":4986,"children":4987},{},[4988,4998,5008,5018,5028],{"type":20,"tag":44,"props":4989,"children":4990},{},[4991,4996],{"type":20,"tag":48,"props":4992,"children":4993},{},[4994],{"type":26,"value":4995},"Weekly monitoring",{"type":26,"value":4997}," — bundlesize checks every PR",{"type":20,"tag":44,"props":4999,"children":5000},{},[5001,5006],{"type":20,"tag":48,"props":5002,"children":5003},{},[5004],{"type":26,"value":5005},"Quarterly audits",{"type":26,"value":5007}," — analyze with visualizer",{"type":20,"tag":44,"props":5009,"children":5010},{},[5011,5016],{"type":20,"tag":48,"props":5012,"children":5013},{},[5014],{"type":26,"value":5015},"Department rotation",{"type":26,"value":5017}," — one engineer owns bundle each month",{"type":20,"tag":44,"props":5019,"children":5020},{},[5021,5026],{"type":20,"tag":48,"props":5022,"children":5023},{},[5024],{"type":26,"value":5025},"Team education",{"type":26,"value":5027}," — awareness of bundle impact",{"type":20,"tag":44,"props":5029,"children":5030},{},[5031,5036],{"type":20,"tag":48,"props":5032,"children":5033},{},[5034],{"type":26,"value":5035},"Dependency reviews",{"type":26,"value":5037}," — approve new packages by size",{"type":20,"tag":21,"props":5039,"children":5040},{"id":1244},[5041],{"type":26,"value":1247},{"type":20,"tag":918,"props":5043,"children":5044},{},[5045,5055,5065,5075,5085],{"type":20,"tag":44,"props":5046,"children":5047},{},[5048,5053],{"type":20,"tag":48,"props":5049,"children":5050},{},[5051],{"type":26,"value":5052},"Lazy load routes by default",{"type":26,"value":5054}," — all routes should be dynamic",{"type":20,"tag":44,"props":5056,"children":5057},{},[5058,5063],{"type":20,"tag":48,"props":5059,"children":5060},{},[5061],{"type":26,"value":5062},"Split by feature",{"type":26,"value":5064}," — heavy features in separate chunks",{"type":20,"tag":44,"props":5066,"children":5067},{},[5068,5073],{"type":20,"tag":48,"props":5069,"children":5070},{},[5071],{"type":26,"value":5072},"Use es2015 module syntax",{"type":26,"value":5074}," — enables tree-shaking",{"type":20,"tag":44,"props":5076,"children":5077},{},[5078,5083],{"type":20,"tag":48,"props":5079,"children":5080},{},[5081],{"type":26,"value":5082},"Compress with Brotli",{"type":26,"value":5084}," — 15-20% smaller than gzip",{"type":20,"tag":44,"props":5086,"children":5087},{},[5088,5092],{"type":20,"tag":48,"props":5089,"children":5090},{},[5091],{"type":26,"value":4364},{"type":26,"value":5093}," — real user metrics matter most",{"type":20,"tag":29,"props":5095,"children":5096},{},[5097],{"type":26,"value":5098},"Bundle size affects every user, every time. It's worth optimizing.",{"title":8,"searchDepth":870,"depth":870,"links":5100},[5101,5102,5103,5109,5113,5114,5118,5119,5120],{"id":4401,"depth":870,"text":4404},{"id":4467,"depth":870,"text":4470},{"id":4571,"depth":870,"text":4574,"children":5104},[5105,5106,5107,5108],{"id":4577,"depth":1314,"text":4580},{"id":4592,"depth":1314,"text":4595},{"id":4607,"depth":1314,"text":4610},{"id":4622,"depth":1314,"text":4625},{"id":4670,"depth":870,"text":4673,"children":5110},[5111,5112],{"id":4690,"depth":1314,"text":4693},{"id":4705,"depth":1314,"text":4708},{"id":4756,"depth":870,"text":4759},{"id":4804,"depth":870,"text":4807,"children":5115},[5116,5117],{"id":4810,"depth":1314,"text":4813},{"id":4825,"depth":1314,"text":4828},{"id":4840,"depth":870,"text":4843},{"id":4981,"depth":870,"text":4984},{"id":1244,"depth":870,"text":1247},"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":5125,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":5126,"description":5127,"date":5128,"readingTime":4395,"tags":5129,"featured":7,"body":5132,"_type":883,"_id":5749,"_source":885,"_file":5750,"_stem":5751,"_extension":888},"\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",[5130,5131],"Remote","Culture",{"type":17,"children":5133,"toc":5724},[5134,5140,5145,5150,5172,5182,5188,5194,5199,5207,5212,5220,5225,5278,5284,5289,5297,5302,5308,5313,5318,5328,5333,5339,5344,5350,5359,5364,5370,5379,5385,5394,5400,5409,5415,5433,5439,5444,5452,5457,5462,5467,5472,5480,5485,5491,5496,5502,5507,5515,5521,5529,5535,5544,5550,5558,5564,5569,5622,5628,5633,5675,5681,5691,5696,5719],{"type":20,"tag":21,"props":5135,"children":5137},{"id":5136},"the-remote-frontend-engineer",[5138],{"type":26,"value":5139},"The Remote Frontend Engineer",{"type":20,"tag":29,"props":5141,"children":5142},{},[5143],{"type":26,"value":5144},"Remote work is now standard. Being a remote engineer requires a different mindset than co-located work.",{"type":20,"tag":29,"props":5146,"children":5147},{},[5148],{"type":26,"value":5149},"The difference:",{"type":20,"tag":40,"props":5151,"children":5152},{},[5153,5163],{"type":20,"tag":44,"props":5154,"children":5155},{},[5156,5161],{"type":20,"tag":48,"props":5157,"children":5158},{},[5159],{"type":26,"value":5160},"Co-located",{"type":26,"value":5162},": \"I'll ask Bob on Slack\"",{"type":20,"tag":44,"props":5164,"children":5165},{},[5166,5170],{"type":20,"tag":48,"props":5167,"children":5168},{},[5169],{"type":26,"value":5130},{"type":26,"value":5171},": \"Bob is asleep; I'll find the answer in docs\"",{"type":20,"tag":29,"props":5173,"children":5174},{},[5175,5177],{"type":26,"value":5176},"The principle: ",{"type":20,"tag":48,"props":5178,"children":5179},{},[5180],{"type":26,"value":5181},"Assume your teammates are asleep, in a meeting, or handling an emergency.",{"type":20,"tag":21,"props":5183,"children":5185},{"id":5184},"async-first-communication",[5186],{"type":26,"value":5187},"Async-First Communication",{"type":20,"tag":1032,"props":5189,"children":5191},{"id":5190},"write-instead-of-chat",[5192],{"type":26,"value":5193},"Write Instead of Chat",{"type":20,"tag":29,"props":5195,"children":5196},{},[5197],{"type":26,"value":5198},"Bad:",{"type":20,"tag":157,"props":5200,"children":5202},{"code":5201},"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",[5203],{"type":20,"tag":165,"props":5204,"children":5205},{"__ignoreMap":8},[5206],{"type":26,"value":5201},{"type":20,"tag":29,"props":5208,"children":5209},{},[5210],{"type":26,"value":5211},"Good:",{"type":20,"tag":157,"props":5213,"children":5215},{"code":5214},"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",[5216],{"type":20,"tag":165,"props":5217,"children":5218},{"__ignoreMap":8},[5219],{"type":26,"value":5214},{"type":20,"tag":29,"props":5221,"children":5222},{},[5223],{"type":26,"value":5224},"Include:",{"type":20,"tag":40,"props":5226,"children":5227},{},[5228,5238,5248,5258,5268],{"type":20,"tag":44,"props":5229,"children":5230},{},[5231,5236],{"type":20,"tag":48,"props":5232,"children":5233},{},[5234],{"type":26,"value":5235},"Context",{"type":26,"value":5237}," — what you're trying to do",{"type":20,"tag":44,"props":5239,"children":5240},{},[5241,5246],{"type":20,"tag":48,"props":5242,"children":5243},{},[5244],{"type":26,"value":5245},"What you've tried",{"type":26,"value":5247}," — don't ask before researching",{"type":20,"tag":44,"props":5249,"children":5250},{},[5251,5256],{"type":20,"tag":48,"props":5252,"children":5253},{},[5254],{"type":26,"value":5255},"Specific question",{"type":26,"value":5257}," — not \"does this work?\"",{"type":20,"tag":44,"props":5259,"children":5260},{},[5261,5266],{"type":20,"tag":48,"props":5262,"children":5263},{},[5264],{"type":26,"value":5265},"Deadline",{"type":26,"value":5267}," — when you need the answer",{"type":20,"tag":44,"props":5269,"children":5270},{},[5271,5276],{"type":20,"tag":48,"props":5272,"children":5273},{},[5274],{"type":26,"value":5275},"Links",{"type":26,"value":5277}," — PR\u002Fissue references",{"type":20,"tag":1032,"props":5279,"children":5281},{"id":5280},"use-threads",[5282],{"type":26,"value":5283},"Use Threads",{"type":20,"tag":29,"props":5285,"children":5286},{},[5287],{"type":26,"value":5288},"Slack threading keeps conversations organized:",{"type":20,"tag":157,"props":5290,"children":5292},{"code":5291},"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",[5293],{"type":20,"tag":165,"props":5294,"children":5295},{"__ignoreMap":8},[5296],{"type":26,"value":5291},{"type":20,"tag":29,"props":5298,"children":5299},{},[5300],{"type":26,"value":5301},"Main channel stays clean. Interested people can see the discussion.",{"type":20,"tag":1032,"props":5303,"children":5305},{"id":5304},"status-updates-not-standups",[5306],{"type":26,"value":5307},"Status Updates (Not Standups)",{"type":20,"tag":29,"props":5309,"children":5310},{},[5311],{"type":26,"value":5312},"Bad: Synchronous standup at 9am (3am for India team)",{"type":20,"tag":29,"props":5314,"children":5315},{},[5316],{"type":26,"value":5317},"Good: Async status in shared doc",{"type":20,"tag":157,"props":5319,"children":5323},{"code":5320,"language":883,"meta":8,"className":5321},"## 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",[5322],"language-markdown",[5324],{"type":20,"tag":165,"props":5325,"children":5326},{"__ignoreMap":8},[5327],{"type":26,"value":5320},{"type":20,"tag":29,"props":5329,"children":5330},{},[5331],{"type":26,"value":5332},"This works for everyone, any timezone.",{"type":20,"tag":21,"props":5334,"children":5336},{"id":5335},"documentation-is-law",[5337],{"type":26,"value":5338},"Documentation Is Law",{"type":20,"tag":29,"props":5340,"children":5341},{},[5342],{"type":26,"value":5343},"Remote engineers live in documentation.",{"type":20,"tag":1032,"props":5345,"children":5347},{"id":5346},"architecture-decisions",[5348],{"type":26,"value":5349},"Architecture Decisions",{"type":20,"tag":157,"props":5351,"children":5354},{"code":5352,"language":883,"meta":8,"className":5353},"## 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",[5322],[5355],{"type":20,"tag":165,"props":5356,"children":5357},{"__ignoreMap":8},[5358],{"type":26,"value":5352},{"type":20,"tag":29,"props":5360,"children":5361},{},[5362],{"type":26,"value":5363},"Store in shared repo, searchable, link from PRs.",{"type":20,"tag":1032,"props":5365,"children":5367},{"id":5366},"component-documentation",[5368],{"type":26,"value":5369},"Component Documentation",{"type":20,"tag":157,"props":5371,"children":5374},{"code":5372,"language":610,"meta":8,"className":5373},"\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",[608],[5375],{"type":20,"tag":165,"props":5376,"children":5377},{"__ignoreMap":8},[5378],{"type":26,"value":5372},{"type":20,"tag":1032,"props":5380,"children":5382},{"id":5381},"api-documentation",[5383],{"type":26,"value":5384},"API Documentation",{"type":20,"tag":157,"props":5386,"children":5389},{"code":5387,"language":883,"meta":8,"className":5388},"## 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",[5322],[5390],{"type":20,"tag":165,"props":5391,"children":5392},{"__ignoreMap":8},[5393],{"type":26,"value":5387},{"type":20,"tag":1032,"props":5395,"children":5397},{"id":5396},"response",[5398],{"type":26,"value":5399},"Response",{"type":20,"tag":157,"props":5401,"children":5404},{"code":5402,"language":1684,"meta":8,"className":5403},"{\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",[1686],[5405],{"type":20,"tag":165,"props":5406,"children":5407},{"__ignoreMap":8},[5408],{"type":26,"value":5402},{"type":20,"tag":1032,"props":5410,"children":5412},{"id":5411},"errors",[5413],{"type":26,"value":5414},"Errors",{"type":20,"tag":40,"props":5416,"children":5417},{},[5418,5423,5428],{"type":20,"tag":44,"props":5419,"children":5420},{},[5421],{"type":26,"value":5422},"400: Invalid email format",{"type":20,"tag":44,"props":5424,"children":5425},{},[5426],{"type":26,"value":5427},"409: Email already exists",{"type":20,"tag":44,"props":5429,"children":5430},{},[5431],{"type":26,"value":5432},"422: Missing required fields",{"type":20,"tag":1032,"props":5434,"children":5436},{"id":5435},"rate-limit",[5437],{"type":26,"value":5438},"Rate Limit",{"type":20,"tag":29,"props":5440,"children":5441},{},[5442],{"type":26,"value":5443},"100 requests per minute per API key",{"type":20,"tag":157,"props":5445,"children":5447},{"code":5446},"\n## Code Review Culture\n\nRemote code reviews must be thorough and asynchronous.\n\n### Good Code Review Comment\n\n",[5448],{"type":20,"tag":165,"props":5449,"children":5450},{"__ignoreMap":8},[5451],{"type":26,"value":5446},{"type":20,"tag":29,"props":5453,"children":5454},{},[5455],{"type":26,"value":5456},"\u002F\u002F ❌ Vague\n\"This looks off\"",{"type":20,"tag":29,"props":5458,"children":5459},{},[5460],{"type":26,"value":5461},"\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":20,"tag":29,"props":5463,"children":5464},{},[5465],{"type":26,"value":5466},"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":20,"tag":29,"props":5468,"children":5469},{},[5470],{"type":26,"value":5471},"See similar pattern in useUser.ts line 32\"",{"type":20,"tag":157,"props":5473,"children":5475},{"code":5474},"\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",[5476],{"type":20,"tag":165,"props":5477,"children":5478},{"__ignoreMap":8},[5479],{"type":26,"value":5474},{"type":20,"tag":29,"props":5481,"children":5482},{},[5483],{"type":26,"value":5484},"Use it for every PR.",{"type":20,"tag":21,"props":5486,"children":5488},{"id":5487},"timezone-management",[5489],{"type":26,"value":5490},"Timezone Management",{"type":20,"tag":29,"props":5492,"children":5493},{},[5494],{"type":26,"value":5495},"Working across timezones requires structure:",{"type":20,"tag":1032,"props":5497,"children":5499},{"id":5498},"core-collaboration-hours",[5500],{"type":26,"value":5501},"Core Collaboration Hours",{"type":20,"tag":29,"props":5503,"children":5504},{},[5505],{"type":26,"value":5506},"Define overlapping times when synchronous work happens:",{"type":20,"tag":157,"props":5508,"children":5510},{"code":5509},"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",[5511],{"type":20,"tag":165,"props":5512,"children":5513},{"__ignoreMap":8},[5514],{"type":26,"value":5509},{"type":20,"tag":1032,"props":5516,"children":5518},{"id":5517},"meeting-best-practices",[5519],{"type":26,"value":5520},"Meeting Best Practices",{"type":20,"tag":157,"props":5522,"children":5524},{"code":5523},"❌ 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",[5525],{"type":20,"tag":165,"props":5526,"children":5527},{"__ignoreMap":8},[5528],{"type":26,"value":5523},{"type":20,"tag":1032,"props":5530,"children":5532},{"id":5531},"communication-norms",[5533],{"type":26,"value":5534},"Communication Norms",{"type":20,"tag":157,"props":5536,"children":5539},{"code":5537,"language":883,"meta":8,"className":5538},"## 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",[5322],[5540],{"type":20,"tag":165,"props":5541,"children":5542},{"__ignoreMap":8},[5543],{"type":26,"value":5537},{"type":20,"tag":21,"props":5545,"children":5547},{"id":5546},"tools-that-work-remote",[5548],{"type":26,"value":5549},"Tools That Work Remote",{"type":20,"tag":157,"props":5551,"children":5553},{"code":5552},"✅ 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",[5554],{"type":20,"tag":165,"props":5555,"children":5556},{"__ignoreMap":8},[5557],{"type":26,"value":5552},{"type":20,"tag":21,"props":5559,"children":5561},{"id":5560},"remote-interview-tips-meta",[5562],{"type":26,"value":5563},"Remote Interview Tips (Meta)",{"type":20,"tag":29,"props":5565,"children":5566},{},[5567],{"type":26,"value":5568},"You're hiring a remote engineer. Watch for:",{"type":20,"tag":918,"props":5570,"children":5571},{},[5572,5582,5592,5602,5612],{"type":20,"tag":44,"props":5573,"children":5574},{},[5575,5580],{"type":20,"tag":48,"props":5576,"children":5577},{},[5578],{"type":26,"value":5579},"Written communication",{"type":26,"value":5581}," — can they explain clearly?",{"type":20,"tag":44,"props":5583,"children":5584},{},[5585,5590],{"type":20,"tag":48,"props":5586,"children":5587},{},[5588],{"type":26,"value":5589},"Self-motivation",{"type":26,"value":5591}," — do they ask good questions during prep?",{"type":20,"tag":44,"props":5593,"children":5594},{},[5595,5600],{"type":20,"tag":48,"props":5596,"children":5597},{},[5598],{"type":26,"value":5599},"Timezone awareness",{"type":26,"value":5601}," — do they mention working across timezones?",{"type":20,"tag":44,"props":5603,"children":5604},{},[5605,5610],{"type":20,"tag":48,"props":5606,"children":5607},{},[5608],{"type":26,"value":5609},"Documentation review",{"type":26,"value":5611}," — did they read our docs before interview?",{"type":20,"tag":44,"props":5613,"children":5614},{},[5615,5620],{"type":20,"tag":48,"props":5616,"children":5617},{},[5618],{"type":26,"value":5619},"Async examples",{"type":26,"value":5621}," — how have they worked async before?",{"type":20,"tag":21,"props":5623,"children":5625},{"id":5624},"results-of-good-remote-practices",[5626],{"type":26,"value":5627},"Results of Good Remote Practices",{"type":20,"tag":29,"props":5629,"children":5630},{},[5631],{"type":26,"value":5632},"After adopting these practices:",{"type":20,"tag":40,"props":5634,"children":5635},{},[5636,5646,5655,5665],{"type":20,"tag":44,"props":5637,"children":5638},{},[5639,5644],{"type":20,"tag":48,"props":5640,"children":5641},{},[5642],{"type":26,"value":5643},"Decision speed",{"type":26,"value":5645},": 2 days → 24 hours (async docs > meetings)",{"type":20,"tag":44,"props":5647,"children":5648},{},[5649,5653],{"type":20,"tag":48,"props":5650,"children":5651},{},[5652],{"type":26,"value":3329},{"type":26,"value":5654},": 4 weeks → 1.5 weeks (docs are source of truth)",{"type":20,"tag":44,"props":5656,"children":5657},{},[5658,5663],{"type":20,"tag":48,"props":5659,"children":5660},{},[5661],{"type":26,"value":5662},"Bug reports",{"type":26,"value":5664},": 4\u002Fweek → 1\u002Fweek (context is clear)",{"type":20,"tag":44,"props":5666,"children":5667},{},[5668,5673],{"type":20,"tag":48,"props":5669,"children":5670},{},[5671],{"type":26,"value":5672},"Developer satisfaction",{"type":26,"value":5674},": 7\u002F10 → 9\u002F10 (no sync meeting fatigue)",{"type":20,"tag":21,"props":5676,"children":5678},{"id":5677},"the-secret",[5679],{"type":26,"value":5680},"The Secret",{"type":20,"tag":29,"props":5682,"children":5683},{},[5684,5686],{"type":26,"value":5685},"Remote work isn't about Zoom calls. It's about ",{"type":20,"tag":48,"props":5687,"children":5688},{},[5689],{"type":26,"value":5690},"clear documentation, trust, and autonomy.",{"type":20,"tag":29,"props":5692,"children":5693},{},[5694],{"type":26,"value":5695},"Engineers who can work async:",{"type":20,"tag":40,"props":5697,"children":5698},{},[5699,5704,5709,5714],{"type":20,"tag":44,"props":5700,"children":5701},{},[5702],{"type":26,"value":5703},"Ship faster",{"type":20,"tag":44,"props":5705,"children":5706},{},[5707],{"type":26,"value":5708},"Need less supervision",{"type":20,"tag":44,"props":5710,"children":5711},{},[5712],{"type":26,"value":5713},"Solve problems independently",{"type":20,"tag":44,"props":5715,"children":5716},{},[5717],{"type":26,"value":5718},"Get better sleep (no midnight calls)",{"type":20,"tag":29,"props":5720,"children":5721},{},[5722],{"type":26,"value":5723},"The best remote engineers aren't chatty. They're clear writers.",{"title":8,"searchDepth":870,"depth":870,"links":5725},[5726,5727,5732,5740,5745,5746,5747,5748],{"id":5136,"depth":870,"text":5139},{"id":5184,"depth":870,"text":5187,"children":5728},[5729,5730,5731],{"id":5190,"depth":1314,"text":5193},{"id":5280,"depth":1314,"text":5283},{"id":5304,"depth":1314,"text":5307},{"id":5335,"depth":870,"text":5338,"children":5733},[5734,5735,5736,5737,5738,5739],{"id":5346,"depth":1314,"text":5349},{"id":5366,"depth":1314,"text":5369},{"id":5381,"depth":1314,"text":5384},{"id":5396,"depth":1314,"text":5399},{"id":5411,"depth":1314,"text":5414},{"id":5435,"depth":1314,"text":5438},{"id":5487,"depth":870,"text":5490,"children":5741},[5742,5743,5744],{"id":5498,"depth":1314,"text":5501},{"id":5517,"depth":1314,"text":5520},{"id":5531,"depth":1314,"text":5534},{"id":5546,"depth":870,"text":5549},{"id":5560,"depth":870,"text":5563},{"id":5624,"depth":870,"text":5627},{"id":5677,"depth":870,"text":5680},"content:blog:remote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices",{"_path":5753,"_dir":6,"_draft":7,"_partial":7,"_locale":8,"title":5754,"description":5755,"date":5756,"updated":894,"readingTime":895,"tags":5757,"featured":654,"body":5758,"_type":883,"_id":6284,"_source":885,"_file":6285,"_stem":6286,"_extension":888},"\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",[15,1332,1880],{"type":17,"children":5759,"toc":6272},[5760,5766,5771,5776,5826,5831,5837,5843,5862,5871,5876,5894,5900,5905,5938,5944,5953,5958,6007,6013,6025,6031,6040,6045,6051,6060,6065,6071,6080,6085,6091,6096,6105,6110,6115,6121,6126,6134,6159,6165,6218,6224,6267],{"type":20,"tag":21,"props":5761,"children":5763},{"id":5762},"the-challenge-why-we-needed-to-migrate",[5764],{"type":26,"value":5765},"The Challenge: Why We Needed to Migrate",{"type":20,"tag":29,"props":5767,"children":5768},{},[5769],{"type":26,"value":5770},"We faced a critical decision when Vue 3 became stable in 2021. Our Vue 2 codebase had grown large across multiple teams.",{"type":20,"tag":29,"props":5772,"children":5773},{},[5774],{"type":26,"value":5775},"The pain points were real:",{"type":20,"tag":40,"props":5777,"children":5778},{},[5779,5789,5799,5808],{"type":20,"tag":44,"props":5780,"children":5781},{},[5782,5787],{"type":20,"tag":48,"props":5783,"children":5784},{},[5785],{"type":26,"value":5786},"Type Safety",{"type":26,"value":5788},": Vue 2 + TypeScript was never fully integrated. The IDE caught some issues; the rest surfaced in QA",{"type":20,"tag":44,"props":5790,"children":5791},{},[5792,5797],{"type":20,"tag":48,"props":5793,"children":5794},{},[5795],{"type":26,"value":5796},"Composition Reuse",{"type":26,"value":5798},": Mixins led to naming collisions and unclear data flow. Multiple teams independently \"solved\" the same problems",{"type":20,"tag":44,"props":5800,"children":5801},{},[5802,5806],{"type":20,"tag":48,"props":5803,"children":5804},{},[5805],{"type":26,"value":1880},{"type":26,"value":5807},": Bundle size kept growing. Tree-shaking didn't work well with Vue 2's runtime structure",{"type":20,"tag":44,"props":5809,"children":5810},{},[5811,5816,5818,5824],{"type":20,"tag":48,"props":5812,"children":5813},{},[5814],{"type":26,"value":5815},"Developer Experience",{"type":26,"value":5817},": New team members struggled with implicit ",{"type":20,"tag":165,"props":5819,"children":5821},{"className":5820},[],[5822],{"type":26,"value":5823},"this",{"type":26,"value":5825}," context and composition patterns",{"type":20,"tag":29,"props":5827,"children":5828},{},[5829],{"type":26,"value":5830},"The business case was compelling: fewer bugs in production, faster onboarding, a smaller bundle.",{"type":20,"tag":21,"props":5832,"children":5834},{"id":5833},"our-migration-strategy",[5835],{"type":26,"value":5836},"Our Migration Strategy",{"type":20,"tag":1032,"props":5838,"children":5840},{"id":5839},"phase-1-set-up-build-tooling-1-week",[5841],{"type":26,"value":5842},"Phase 1: Set Up Build Tooling (1 week)",{"type":20,"tag":29,"props":5844,"children":5845},{},[5846,5848,5853,5855,5860],{"type":26,"value":5847},"We chose ",{"type":20,"tag":48,"props":5849,"children":5850},{},[5851],{"type":26,"value":5852},"Vite",{"type":26,"value":5854}," + ",{"type":20,"tag":48,"props":5856,"children":5857},{},[5858],{"type":26,"value":5859},"@vitejs\u002Fplugin-vue",{"type":26,"value":5861}," instead of webpack. This decision paid dividends:",{"type":20,"tag":157,"props":5863,"children":5866},{"className":5864,"code":5865,"language":610,"meta":8},[608],"\u002F\u002F vite.config.ts\nexport default {\n  plugins: [vue(), visualizer()],\n  build: {\n    rollupOptions: {\n      output: { manualChunks: customChunks }\n    }\n  }\n}\n",[5867],{"type":20,"tag":165,"props":5868,"children":5869},{"__ignoreMap":8},[5870],{"type":26,"value":5865},{"type":20,"tag":29,"props":5872,"children":5873},{},[5874],{"type":26,"value":5875},"Bundle analysis immediately showed why the tooling move pays for itself:",{"type":20,"tag":40,"props":5877,"children":5878},{},[5879,5884,5889],{"type":20,"tag":44,"props":5880,"children":5881},{},[5882],{"type":26,"value":5883},"Vite's default code splitting cut the main bundle substantially before we touched a component",{"type":20,"tag":44,"props":5885,"children":5886},{},[5887],{"type":26,"value":5888},"Build times dropped from tens of seconds under webpack to single digits under Vite",{"type":20,"tag":44,"props":5890,"children":5891},{},[5892],{"type":26,"value":5893},"Hot Module Replacement went from a coffee-sip wait to effectively instant",{"type":20,"tag":1032,"props":5895,"children":5897},{"id":5896},"phase-2-incremental-component-migration-6-weeks",[5898],{"type":26,"value":5899},"Phase 2: Incremental Component Migration (6 weeks)",{"type":20,"tag":29,"props":5901,"children":5902},{},[5903],{"type":26,"value":5904},"We didn't rewrite everything. Instead, we:",{"type":20,"tag":918,"props":5906,"children":5907},{},[5908,5918,5928],{"type":20,"tag":44,"props":5909,"children":5910},{},[5911,5916],{"type":20,"tag":48,"props":5912,"children":5913},{},[5914],{"type":26,"value":5915},"Identified high-ROI components",{"type":26,"value":5917}," — components used across multiple products, or frequently modified",{"type":20,"tag":44,"props":5919,"children":5920},{},[5921,5926],{"type":20,"tag":48,"props":5922,"children":5923},{},[5924],{"type":26,"value":5925},"Created a co-existence layer",{"type":26,"value":5927}," — Vue 2 and Vue 3 components lived side-by-side",{"type":20,"tag":44,"props":5929,"children":5930},{},[5931,5936],{"type":20,"tag":48,"props":5932,"children":5933},{},[5934],{"type":26,"value":5935},"Migrated by priority",{"type":26,"value":5937}," — a steady per-engineer cadence, not a big-bang rewrite",{"type":20,"tag":1568,"props":5939,"children":5941},{"id":5940},"key-pattern-the-composition-api-advantage",[5942],{"type":26,"value":5943},"Key Pattern: The Composition API Advantage",{"type":20,"tag":157,"props":5945,"children":5948},{"className":5946,"code":5947,"language":610,"meta":8},[608],"\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",[5949],{"type":20,"tag":165,"props":5950,"children":5951},{"__ignoreMap":8},[5952],{"type":26,"value":5947},{"type":20,"tag":29,"props":5954,"children":5955},{},[5956],{"type":26,"value":5957},"The Composition API version:",{"type":20,"tag":40,"props":5959,"children":5960},{},[5961,5971,5981,5991],{"type":20,"tag":44,"props":5962,"children":5963},{},[5964,5969],{"type":20,"tag":48,"props":5965,"children":5966},{},[5967],{"type":26,"value":5968},"Eliminates naming collisions",{"type":26,"value":5970}," — composables are just functions, no implicit merging",{"type":20,"tag":44,"props":5972,"children":5973},{},[5974,5979],{"type":20,"tag":48,"props":5975,"children":5976},{},[5977],{"type":26,"value":5978},"Makes data flow explicit",{"type":26,"value":5980}," — you see exactly which data is used",{"type":20,"tag":44,"props":5982,"children":5983},{},[5984,5989],{"type":20,"tag":48,"props":5985,"children":5986},{},[5987],{"type":26,"value":5988},"Enables better tree-shaking",{"type":26,"value":5990}," — unused composables are easier to identify",{"type":20,"tag":44,"props":5992,"children":5993},{},[5994,5999,6001],{"type":20,"tag":48,"props":5995,"children":5996},{},[5997],{"type":26,"value":5998},"Improves TypeScript",{"type":26,"value":6000}," — full type inference without ",{"type":20,"tag":165,"props":6002,"children":6004},{"className":6003},[],[6005],{"type":26,"value":6006},"as any",{"type":20,"tag":1032,"props":6008,"children":6010},{"id":6009},"phase-3-handle-the-breaking-changes",[6011],{"type":26,"value":6012},"Phase 3: Handle the Breaking Changes",{"type":20,"tag":29,"props":6014,"children":6015},{},[6016,6018,6023],{"type":26,"value":6017},"Vue 3's breaking changes were documented, but the ",{"type":20,"tag":1704,"props":6019,"children":6020},{},[6021],{"type":26,"value":6022},"scale",{"type":26,"value":6024}," matters. Here's what actually cost us the most time:",{"type":20,"tag":1568,"props":6026,"children":6028},{"id":6027},"_1-event-handling-3-days-spent",[6029],{"type":26,"value":6030},"1. Event Handling (3 days spent)",{"type":20,"tag":157,"props":6032,"children":6035},{"className":6033,"code":6034,"language":3058,"meta":8},[3060],"\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",[6036],{"type":20,"tag":165,"props":6037,"children":6038},{"__ignoreMap":8},[6039],{"type":26,"value":6034},{"type":20,"tag":29,"props":6041,"children":6042},{},[6043],{"type":26,"value":6044},"We had instances of this all over the codebase. Automated search + manual review took longer than the code changes themselves.",{"type":20,"tag":1568,"props":6046,"children":6048},{"id":6047},"_2-async-components",[6049],{"type":26,"value":6050},"2. Async Components",{"type":20,"tag":157,"props":6052,"children":6055},{"className":6053,"code":6054,"language":3058,"meta":8},[3060],"\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",[6056],{"type":20,"tag":165,"props":6057,"children":6058},{"__ignoreMap":8},[6059],{"type":26,"value":6054},{"type":20,"tag":29,"props":6061,"children":6062},{},[6063],{"type":26,"value":6064},"Dozens more locations to update. Pro tip: codemods exist for this, but they're not perfect. Invest time in understanding edge cases.",{"type":20,"tag":1568,"props":6066,"children":6068},{"id":6067},"_3-filters-computed-properties",[6069],{"type":26,"value":6070},"3. Filters → Computed Properties",{"type":20,"tag":157,"props":6072,"children":6075},{"className":6073,"code":6074,"language":3058,"meta":8},[3060],"\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",[6076],{"type":20,"tag":165,"props":6077,"children":6078},{"__ignoreMap":8},[6079],{"type":26,"value":6074},{"type":20,"tag":29,"props":6081,"children":6082},{},[6083],{"type":26,"value":6084},"Dozens of filter usages. We created a utility module to centralize custom filters as functions.",{"type":20,"tag":1032,"props":6086,"children":6088},{"id":6087},"phase-4-test-migration-parallel-4-weeks",[6089],{"type":26,"value":6090},"Phase 4: Test Migration (Parallel, 4 weeks)",{"type":20,"tag":29,"props":6092,"children":6093},{},[6094],{"type":26,"value":6095},"Our Jest test suite had to be updated to work with Vue 3. The good news: tests were actually easier to write.",{"type":20,"tag":157,"props":6097,"children":6100},{"className":6098,"code":6099,"language":610,"meta":8},[608],"\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",[6101],{"type":20,"tag":165,"props":6102,"children":6103},{"__ignoreMap":8},[6104],{"type":26,"value":6099},{"type":20,"tag":29,"props":6106,"children":6107},{},[6108],{"type":26,"value":6109},"We migrated the full test suite.",{"type":20,"tag":29,"props":6111,"children":6112},{},[6113],{"type":26,"value":6114},"It took weeks, not days — but many tests revealed brittleness in the original code. This was actually valuable.",{"type":20,"tag":21,"props":6116,"children":6118},{"id":6117},"the-results",[6119],{"type":26,"value":6120},"The Results",{"type":20,"tag":29,"props":6122,"children":6123},{},[6124],{"type":26,"value":6125},"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":20,"tag":29,"props":6127,"children":6128},{},[6129],{"type":20,"tag":48,"props":6130,"children":6131},{},[6132],{"type":26,"value":6133},"What held up qualitatively:",{"type":20,"tag":40,"props":6135,"children":6136},{},[6137,6142,6154],{"type":20,"tag":44,"props":6138,"children":6139},{},[6140],{"type":26,"value":6141},"Tree-shaking finally worked, so dead code stopped shipping",{"type":20,"tag":44,"props":6143,"children":6144},{},[6145,6147,6152],{"type":26,"value":6146},"Full TypeScript inference replaced ",{"type":20,"tag":165,"props":6148,"children":6150},{"className":6149},[],[6151],{"type":26,"value":6006},{"type":26,"value":6153}," scattered through the codebase",{"type":20,"tag":44,"props":6155,"children":6156},{},[6157],{"type":26,"value":6158},"Vite's dev server made the edit-refresh loop feel free again",{"type":20,"tag":21,"props":6160,"children":6162},{"id":6161},"lessons-for-your-codebase",[6163],{"type":26,"value":6164},"Lessons for Your Codebase",{"type":20,"tag":918,"props":6166,"children":6167},{},[6168,6178,6188,6198,6208],{"type":20,"tag":44,"props":6169,"children":6170},{},[6171,6176],{"type":20,"tag":48,"props":6172,"children":6173},{},[6174],{"type":26,"value":6175},"Tooling matters as much as code",{"type":26,"value":6177}," — Vite's superior DX made the whole migration feel faster",{"type":20,"tag":44,"props":6179,"children":6180},{},[6181,6186],{"type":20,"tag":48,"props":6182,"children":6183},{},[6184],{"type":26,"value":6185},"Go incremental",{"type":26,"value":6187}," — don't try a big bang rewrite. Parallel migration kept the team shipping",{"type":20,"tag":44,"props":6189,"children":6190},{},[6191,6196],{"type":20,"tag":48,"props":6192,"children":6193},{},[6194],{"type":26,"value":6195},"Type safety is an enabler",{"type":26,"value":6197}," — we spent 20% on migration, but gained 80% in error prevention",{"type":20,"tag":44,"props":6199,"children":6200},{},[6201,6206],{"type":20,"tag":48,"props":6202,"children":6203},{},[6204],{"type":26,"value":6205},"Test coverage reveals truth",{"type":26,"value":6207}," — broken tests weren't a migration problem; they revealed logic bugs",{"type":20,"tag":44,"props":6209,"children":6210},{},[6211,6216],{"type":20,"tag":48,"props":6212,"children":6213},{},[6214],{"type":26,"value":6215},"Plan for edge cases",{"type":26,"value":6217}," — our 10% \"final polish\" took 20% of the time (event handling, filters, etc.)",{"type":20,"tag":21,"props":6219,"children":6221},{"id":6220},"what-wed-do-differently",[6222],{"type":26,"value":6223},"What We'd Do Differently",{"type":20,"tag":40,"props":6225,"children":6226},{},[6227,6237,6247,6257],{"type":20,"tag":44,"props":6228,"children":6229},{},[6230,6235],{"type":20,"tag":48,"props":6231,"children":6232},{},[6233],{"type":26,"value":6234},"Start with Vite first",{"type":26,"value":6236}," — migrate tooling before components (the performance boost motivated the team)",{"type":20,"tag":44,"props":6238,"children":6239},{},[6240,6245],{"type":20,"tag":48,"props":6241,"children":6242},{},[6243],{"type":26,"value":6244},"Create better codemods",{"type":26,"value":6246}," — we wrote several after the fact that would have saved days",{"type":20,"tag":44,"props":6248,"children":6249},{},[6250,6255],{"type":20,"tag":48,"props":6251,"children":6252},{},[6253],{"type":26,"value":6254},"Allocate 15-20% for unknowns",{"type":26,"value":6256}," — we estimated 6 weeks; it took 8. That's normal for large migrations",{"type":20,"tag":44,"props":6258,"children":6259},{},[6260,6265],{"type":20,"tag":48,"props":6261,"children":6262},{},[6263],{"type":26,"value":6264},"Train junior engineers",{"type":26,"value":6266}," — force 2-3 people to migrate components solo. They become experts quickly",{"type":20,"tag":29,"props":6268,"children":6269},{},[6270],{"type":26,"value":6271},"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":8,"searchDepth":870,"depth":870,"links":6273},[6274,6275,6281,6282,6283],{"id":5762,"depth":870,"text":5765},{"id":5833,"depth":870,"text":5836,"children":6276},[6277,6278,6279,6280],{"id":5839,"depth":1314,"text":5842},{"id":5896,"depth":1314,"text":5899},{"id":6009,"depth":1314,"text":6012},{"id":6087,"depth":1314,"text":6090},{"id":6117,"depth":870,"text":6120},{"id":6161,"depth":870,"text":6164},{"id":6220,"depth":870,"text":6223},"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",1783253292097]