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