[{"data":1,"prerenderedAt":19876},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-9":4,"blog-paginated-cats":19231},640,[5,577,872,1165,1553,4065,4442,7240,9104,9959,11732,12438,14051,16607,17944],{"id":6,"title":7,"author":8,"body":11,"category":557,"date":558,"description":559,"extension":560,"featured":561,"image":562,"keywords":563,"meta":566,"navigation":122,"path":567,"readTime":126,"seo":568,"stem":569,"tags":570,"__hash__":576},"blog/blog/multi-tenant-architecture.md","Multi-Tenant Architecture: Patterns for Building Software That Serves Many Clients",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":547},"minimark",[14,19,23,26,29,33,52,58,136,142,151,154,160,166,170,185,189,229,242,248,254,259,264,268,271,276,282,288,293,298,302,305,308,311,314,448,451,455,458,464,470,476,482,486,489,492,495,498,508,511,515,543],[15,16,18],"h2",{"id":17},"the-decision-that-shapes-everything","The Decision That Shapes Everything",[20,21,22],"p",{},"When you're building software that will serve multiple clients, the multi-tenancy architecture decision is foundational. It determines your database design, your security model, your cost structure, your deployment complexity, and your ability to scale. Get it right and you have a solid foundation. Get it wrong and you spend years fighting the architecture instead of building features.",[20,24,25],{},"There are three core multi-tenant patterns. Most discussions oversimplify them as \"one database vs. Many databases\" — but the reality is more nuanced, and each pattern has real engineering tradeoffs that matter at different scales and for different customer types.",[20,27,28],{},"Let me walk through each pattern honestly, including where each breaks down.",[15,30,32],{"id":31},"pattern-1-shared-schema-row-level-tenancy","Pattern 1: Shared Schema (Row-Level Tenancy)",[20,34,35,36,40,41,44,45,48,49,51],{},"In a shared schema architecture, all tenants live in the same database, the same tables. A ",[37,38,39],"code",{},"tenant_id"," column on every table distinguishes one tenant's data from another's. A row in the ",[37,42,43],{},"orders"," table with ",[37,46,47],{},"tenant_id = 42"," belongs to tenant 42. All application code filters queries by ",[37,50,39],{},".",[20,53,54],{},[55,56,57],"strong",{},"What it looks like in practice:",[59,60,65],"pre",{"className":61,"code":62,"language":63,"meta":64,"style":64},"language-sql shiki shiki-themes github-dark","-- Every table has a tenant_id\nCREATE TABLE orders (\n id UUID PRIMARY KEY,\n tenant_id UUID NOT NULL REFERENCES tenants(id),\n customer_name TEXT,\n total_amount NUMERIC,\n created_at TIMESTAMPTZ\n);\n\n-- Every query filters by tenant\nSELECT * FROM orders WHERE tenant_id = $1 AND status = 'pending';\n","sql","",[37,66,67,75,81,87,93,99,105,111,117,124,130],{"__ignoreMap":64},[68,69,72],"span",{"class":70,"line":71},"line",1,[68,73,74],{},"-- Every table has a tenant_id\n",[68,76,78],{"class":70,"line":77},2,[68,79,80],{},"CREATE TABLE orders (\n",[68,82,84],{"class":70,"line":83},3,[68,85,86],{}," id UUID PRIMARY KEY,\n",[68,88,90],{"class":70,"line":89},4,[68,91,92],{}," tenant_id UUID NOT NULL REFERENCES tenants(id),\n",[68,94,96],{"class":70,"line":95},5,[68,97,98],{}," customer_name TEXT,\n",[68,100,102],{"class":70,"line":101},6,[68,103,104],{}," total_amount NUMERIC,\n",[68,106,108],{"class":70,"line":107},7,[68,109,110],{}," created_at TIMESTAMPTZ\n",[68,112,114],{"class":70,"line":113},8,[68,115,116],{},");\n",[68,118,120],{"class":70,"line":119},9,[68,121,123],{"emptyLinePlaceholder":122},true,"\n",[68,125,127],{"class":70,"line":126},10,[68,128,129],{},"-- Every query filters by tenant\n",[68,131,133],{"class":70,"line":132},11,[68,134,135],{},"SELECT * FROM orders WHERE tenant_id = $1 AND status = 'pending';\n",[20,137,138,141],{},[55,139,140],{},"The advantages are real."," Operational overhead is minimal. You run one database. Schema migrations run once. Infrastructure costs are low. Adding a new tenant is a database insert, not a deployment.",[20,143,144,147,148,150],{},[55,145,146],{},"The risks are real too."," The entire security model depends on never forgetting the ",[37,149,39],{}," filter. One missed WHERE clause exposes all tenants' data. Row-level security at the database level (PostgreSQL RLS is excellent for this) provides a defense-in-depth layer, but it requires consistent implementation.",[20,152,153],{},"Performance also gets complicated at scale. You have tenants sharing indexes. A tenant with 10 million records shares query plan resources with a tenant with 100 records. Without careful partitioning and index design, large tenants degrade the experience for small tenants — the noisy neighbor problem.",[20,155,156,159],{},[55,157,158],{},"Best for:"," Early-stage SaaS, SMB-focused products, homogeneous customer base with similar data volumes, teams that want operational simplicity over isolation.",[20,161,162,165],{},[55,163,164],{},"Breaks down when:"," You have enterprise customers with contractual data isolation requirements, wildly different data volumes between tenants, or strict regulatory requirements around data co-mingling.",[15,167,169],{"id":168},"pattern-2-shared-database-separate-schemas-schema-level-tenancy","Pattern 2: Shared Database, Separate Schemas (Schema-Level Tenancy)",[20,171,172,173,176,177,180,181,184],{},"In this pattern, all tenants live in the same database but each gets their own schema namespace. Tenant 42 has their data in schema ",[37,174,175],{},"tenant_42",". The same tables exist in every schema — ",[37,178,179],{},"tenant_42.orders",", ",[37,182,183],{},"tenant_43.orders"," — but the data is physically separated at the schema level.",[20,186,187],{},[55,188,57],{},[59,190,192],{"className":61,"code":191,"language":63,"meta":64,"style":64},"-- Tenant isolation at schema level\nCREATE SCHEMA tenant_42;\nCREATE TABLE tenant_42.orders (\n id UUID PRIMARY KEY,\n customer_name TEXT,\n total_amount NUMERIC,\n created_at TIMESTAMPTZ\n);\n",[37,193,194,199,204,209,213,217,221,225],{"__ignoreMap":64},[68,195,196],{"class":70,"line":71},[68,197,198],{},"-- Tenant isolation at schema level\n",[68,200,201],{"class":70,"line":77},[68,202,203],{},"CREATE SCHEMA tenant_42;\n",[68,205,206],{"class":70,"line":83},[68,207,208],{},"CREATE TABLE tenant_42.orders (\n",[68,210,211],{"class":70,"line":89},[68,212,86],{},[68,214,215],{"class":70,"line":95},[68,216,98],{},[68,218,219],{"class":70,"line":101},[68,220,104],{},[68,222,223],{"class":70,"line":107},[68,224,110],{},[68,226,227],{"class":70,"line":113},[68,228,116],{},[20,230,231,234,235,237,238,241],{},[55,232,233],{},"The advantages over row-level tenancy:"," Data is physically isolated at the schema level. A query in ",[37,236,179],{}," can only ever see tenant 42's orders — there's no ",[37,239,240],{},"WHERE tenant_id = ?"," to forget. You get stronger isolation without the operational complexity of separate databases.",[20,243,244,247],{},[55,245,246],{},"The operational cost:"," Schema migrations become tenant-aware. When you add a column to the orders table, you run that migration once per tenant schema, not once globally. With 100 tenants, that's manageable. With 10,000 tenants, you need a migration orchestration system. Some databases have limits on the number of schemas that affect performance.",[20,249,250,253],{},[55,251,252],{},"Connection pooling also gets complicated."," Your connection pool strategy needs to handle schema-switching cleanly, and most ORMs need careful configuration to work properly with dynamic schema selection.",[20,255,256,258],{},[55,257,158],{}," B2B SaaS with dozens to low hundreds of enterprise tenants, products where customers have data isolation expectations but don't require separate databases, teams comfortable with migration orchestration complexity.",[20,260,261,263],{},[55,262,164],{}," Tenant count grows into the thousands (migration orchestration becomes a project), or when regulatory requirements demand truly separate databases.",[15,265,267],{"id":266},"pattern-3-separate-databases-database-level-tenancy","Pattern 3: Separate Databases (Database-Level Tenancy)",[20,269,270],{},"The most isolated option: each tenant gets their own database. Strongest isolation, highest operational overhead.",[20,272,273,275],{},[55,274,57],{}," Your application dynamically resolves the database connection string for each tenant, connects to their database, and executes queries. No cross-tenant data contamination is architecturally possible. Each tenant database can be sized, backed up, and restored independently.",[20,277,278,281],{},[55,279,280],{},"The advantages are significant for enterprise:"," Data isolation is complete and demonstrable to compliance auditors. You can offer tenant-level backup and restore. A large tenant's query volume doesn't affect small tenants. You can migrate tenants to higher-tier infrastructure without touching other tenants.",[20,283,284,287],{},[55,285,286],{},"The operational cost is high."," With 500 tenants, you're managing 500 databases. Schema migrations run per-database and need orchestration. Connection pooling requires careful management to avoid opening thousands of connections. Monitoring and observability need tenant-aware dashboards. The operational engineering investment is substantial.",[20,289,290,292],{},[55,291,158],{}," Enterprise-focused SaaS with strict compliance requirements (healthcare, finance, government), customers who contractually require dedicated infrastructure, lower-volume platforms where the operational overhead is manageable.",[20,294,295,297],{},[55,296,164],{}," Tenant count grows large — hundreds of databases is manageable with good tooling, thousands starts becoming untenable without significant platform investment.",[15,299,301],{"id":300},"the-hybrid-approach-what-most-mature-platforms-do","The Hybrid Approach (What Most Mature Platforms Do)",[20,303,304],{},"After a few years of scale, most SaaS platforms end up with a hybrid: small and mid-tier customers on shared schema, enterprise customers on separate databases or schemas.",[20,306,307],{},"This makes economic sense. You can't run 10,000 SMB customers on separate databases — the infrastructure cost would make the product uneconomical at SMB price points. But your enterprise customers are paying 20x the SMB price and have contractual requirements that justify dedicated infrastructure.",[20,309,310],{},"The engineering challenge of a hybrid is that you're building and maintaining two paths through your application: one that's tenant-aware in the shared model, and one that resolves to a dedicated database. This isn't impossible, but it's non-trivial to do cleanly.",[20,312,313],{},"The clean way to handle this is a tenant resolution layer that abstracts the underlying architecture:",[59,315,319],{"className":316,"code":317,"language":318,"meta":64,"style":64},"language-typescript shiki shiki-themes github-dark","// Tenant resolver returns a database connection regardless of architecture\nasync function getTenantDatabase(tenantId: string): Promise\u003CDatabaseConnection> {\n const tenant = await resolveTenant(tenantId);\n\n if (tenant.plan === 'enterprise') {\n return getDedicatedConnection(tenant.databaseUrl);\n }\n\n return getSharedConnection(tenantId);\n}\n","typescript",[37,320,321,327,372,392,396,414,425,430,434,443],{"__ignoreMap":64},[68,322,323],{"class":70,"line":71},[68,324,326],{"class":325},"sAwPA","// Tenant resolver returns a database connection regardless of architecture\n",[68,328,329,333,336,340,344,348,351,355,358,360,363,366,369],{"class":70,"line":77},[68,330,332],{"class":331},"snl16","async",[68,334,335],{"class":331}," function",[68,337,339],{"class":338},"svObZ"," getTenantDatabase",[68,341,343],{"class":342},"s95oV","(",[68,345,347],{"class":346},"s9osk","tenantId",[68,349,350],{"class":331},":",[68,352,354],{"class":353},"sDLfK"," string",[68,356,357],{"class":342},")",[68,359,350],{"class":331},[68,361,362],{"class":338}," Promise",[68,364,365],{"class":342},"\u003C",[68,367,368],{"class":338},"DatabaseConnection",[68,370,371],{"class":342},"> {\n",[68,373,374,377,380,383,386,389],{"class":70,"line":83},[68,375,376],{"class":331}," const",[68,378,379],{"class":353}," tenant",[68,381,382],{"class":331}," =",[68,384,385],{"class":331}," await",[68,387,388],{"class":338}," resolveTenant",[68,390,391],{"class":342},"(tenantId);\n",[68,393,394],{"class":70,"line":89},[68,395,123],{"emptyLinePlaceholder":122},[68,397,398,401,404,407,411],{"class":70,"line":95},[68,399,400],{"class":331}," if",[68,402,403],{"class":342}," (tenant.plan ",[68,405,406],{"class":331},"===",[68,408,410],{"class":409},"sU2Wk"," 'enterprise'",[68,412,413],{"class":342},") {\n",[68,415,416,419,422],{"class":70,"line":101},[68,417,418],{"class":331}," return",[68,420,421],{"class":338}," getDedicatedConnection",[68,423,424],{"class":342},"(tenant.databaseUrl);\n",[68,426,427],{"class":70,"line":107},[68,428,429],{"class":342}," }\n",[68,431,432],{"class":70,"line":113},[68,433,123],{"emptyLinePlaceholder":122},[68,435,436,438,441],{"class":70,"line":119},[68,437,418],{"class":331},[68,439,440],{"class":338}," getSharedConnection",[68,442,391],{"class":342},[68,444,445],{"class":70,"line":126},[68,446,447],{"class":342},"}\n",[20,449,450],{},"The application code above this layer doesn't need to know which architecture the tenant is on. This abstraction pays significant dividends as you scale.",[15,452,454],{"id":453},"data-architecture-considerations-that-cut-across-all-patterns","Data Architecture Considerations That Cut Across All Patterns",[20,456,457],{},"Regardless of which isolation pattern you choose, a few architectural decisions apply universally.",[20,459,460,463],{},[55,461,462],{},"Tenant context propagation."," The tenant ID needs to be available everywhere it's needed — from the HTTP request through the service layer to the data layer. The cleanest approach is to resolve tenant context early in the request lifecycle (middleware or request context) and make it available via dependency injection or context propagation rather than passing it through every function signature.",[20,465,466,469],{},[55,467,468],{},"Cross-tenant operations."," Administration operations — running a report across all tenants, updating a feature flag for a tenant tier, processing renewals — need to access data across tenant boundaries. This needs a clearly defined service account model with auditing, separate from the normal application flow.",[20,471,472,475],{},[55,473,474],{},"Search and analytics."," Full-text search and analytics often need different approaches in multi-tenant systems. A search index built on Elasticsearch might use tenant-level index naming. An analytics warehouse might aggregate data to a separate schema with explicit tenant partitioning. Design these systems explicitly — don't bolt them on later.",[20,477,478,481],{},[55,479,480],{},"Schema evolution."," How you evolve the database schema matters enormously in multi-tenant systems. Additive changes (adding columns with defaults, adding tables) are safe. Destructive changes (dropping columns, renaming) are dangerous and need migration strategies that don't break existing tenants in flight.",[15,483,485],{"id":484},"the-conversation-to-have-before-you-design","The Conversation to Have Before You Design",[20,487,488],{},"The most important question to answer before choosing a multi-tenancy pattern is: who are your customers?",[20,490,491],{},"If your customers are small businesses who will never ask about data isolation, start with shared schema and invest the savings in features. If your customers are enterprises who will send you questionnaires about their data isolation controls, design for separate schemas or databases from the start — retrofitting stronger isolation into a shared schema system is painful.",[20,493,494],{},"The second most important question: what's your 3-year tenant count projection? A system with 500 tenants and a system with 50,000 tenants have different optimal architectures even if they serve the same customer segment.",[20,496,497],{},"Design for your realistic scale trajectory, not for arbitrary theoretical maximums. The best architecture is the simplest one that meets your requirements — not the most isolated one.",[20,499,500,501,51],{},"If you're designing a multi-tenant platform and want to work through the architecture decision with someone who has built this at multiple scales, ",[502,503,507],"a",{"href":504,"rel":505},"https://calendly.com/jamesrossjr",[506],"nofollow","schedule a conversation at calendly.com/jamesrossjr",[509,510],"hr",{},[15,512,514],{"id":513},"keep-reading","Keep Reading",[516,517,518,525,531,537],"ul",{},[519,520,521],"li",{},[502,522,524],{"href":523},"/blog/api-first-architecture","API-First Architecture: Building Software That Integrates by Default",[519,526,527],{},[502,528,530],{"href":529},"/blog/build-vs-buy-enterprise-software","Build vs Buy Enterprise Software: A Framework for the Decision",[519,532,533],{},[502,534,536],{"href":535},"/blog/enterprise-software-scalability","How to Design Enterprise Software That Scales With Your Business",[519,538,539],{},[502,540,542],{"href":541},"/blog/saas-vs-on-premise","SaaS vs On-Premise Enterprise Software: How to Make the Right Call",[544,545,546],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":64,"searchDepth":83,"depth":83,"links":548},[549,550,551,552,553,554,555,556],{"id":17,"depth":77,"text":18},{"id":31,"depth":77,"text":32},{"id":168,"depth":77,"text":169},{"id":266,"depth":77,"text":267},{"id":300,"depth":77,"text":301},{"id":453,"depth":77,"text":454},{"id":484,"depth":77,"text":485},{"id":513,"depth":77,"text":514},"Engineering","2026-03-03","Multi-tenant architecture decisions made early define your SaaS platform's cost, security, and scalability ceiling. Here's how to choose the right pattern for your use case.","md",false,null,[564,565],"multi-tenant architecture","SaaS architecture",{},"/blog/multi-tenant-architecture",{"title":7,"description":559},"blog/multi-tenant-architecture",[571,572,573,574,575],"Architecture","SaaS","Multi-Tenancy","Database Design","Systems Design","3novsVrKya9Sj9OtiV8PPB1lcWNQE1keAb_nhfzAACk",{"id":578,"title":579,"author":580,"body":581,"category":858,"date":558,"description":859,"extension":560,"featured":561,"image":562,"keywords":860,"meta":863,"navigation":122,"path":864,"readTime":107,"seo":865,"stem":866,"tags":867,"__hash__":871},"blog/blog/mvp-development-guide.md","MVP Development: How to Build the Right Thing Fast Without Building the Wrong Thing",{"name":9,"bio":10},{"type":12,"value":582,"toc":848},[583,587,590,593,595,599,602,605,616,619,641,644,646,650,653,656,667,670,687,690,692,696,699,705,711,717,723,726,728,732,735,741,747,753,759,761,765,768,771,774,777,779,783,786,789,806,809,811,818,820,822],[15,584,586],{"id":585},"the-mvp-misunderstanding-that-wastes-millions","The MVP Misunderstanding That Wastes Millions",[20,588,589],{},"Minimum Viable Product is one of the most misunderstood concepts in product development. I've seen it used to justify shipping broken software (\"it's just an MVP\"), to describe what is essentially a full product (\"we haven't launched yet, it's still MVP\"), and to avoid making hard scoping decisions by putting everything on the \"MVP list.\"",[20,591,592],{},"None of these are the original concept. An MVP is a learning instrument. Its purpose is to test a specific hypothesis about your product, your customer, or your market with the minimum amount of build effort required to get a credible answer. Everything about how you scope and build an MVP should flow from that purpose.",[509,594],{},[15,596,598],{"id":597},"start-with-the-hypothesis","Start With the Hypothesis",[20,600,601],{},"Before you write a line of code or design a single screen, you need to articulate clearly what you're trying to learn. What is the specific hypothesis your MVP will test?",[20,603,604],{},"Good hypotheses are specific and falsifiable:",[516,606,607,610,613],{},[519,608,609],{},"\"Small business owners will pay $79/month for automated bookkeeping that requires no accountant review\"",[519,611,612],{},"\"E-commerce stores with more than 1,000 monthly orders will value a returns automation tool enough to integrate it\"",[519,614,615],{},"\"Restaurant managers will use a scheduling tool daily if it reduces the time spent on weekly scheduling by at least 50%\"",[20,617,618],{},"Bad hypotheses are vague:",[516,620,621,628,635],{},[519,622,623,624,627],{},"\"People want a better ",[68,625,626],{},"category"," product\"",[519,629,630,631,634],{},"\"There's a market for ",[68,632,633],{},"solution","\"",[519,636,637,638,634],{},"\"Our target customer is frustrated with ",[68,639,640],{},"incumbent",[20,642,643],{},"If you can't write a specific, testable hypothesis, you're not ready to build an MVP. You need more customer discovery.",[509,645],{},[15,647,649],{"id":648},"scoping-what-minimum-actually-means","Scoping: What \"Minimum\" Actually Means",[20,651,652],{},"\"Minimum\" does not mean \"low quality.\" It means the smallest set of functionality that produces a genuine test of your hypothesis. These are not the same thing.",[20,654,655],{},"A minimum viable product must:",[516,657,658,661,664],{},[519,659,660],{},"Solve the core problem that your target customer actually has",[519,662,663],{},"Work reliably enough that customer feedback reflects their experience with the value proposition, not frustration with bugs",[519,665,666],{},"Be used by real potential customers in real conditions",[20,668,669],{},"A minimum viable product does not need:",[516,671,672,675,678,681,684],{},[519,673,674],{},"Full feature parity with existing solutions",[519,676,677],{},"Polished UI beyond the point of usability",[519,679,680],{},"Edge case handling for scenarios that don't apply to your initial customers",[519,682,683],{},"Scalability infrastructure for load you won't see for years",[519,685,686],{},"An admin interface beyond what you personally need to support early customers",[20,688,689],{},"The question to ask for every proposed feature: \"Does including or excluding this feature affect whether we can test our core hypothesis?\" If yes, include it. If no, it's scope creep with better branding.",[509,691],{},[15,693,695],{"id":694},"the-pre-build-validation-options-people-skip","The Pre-Build Validation Options People Skip",[20,697,698],{},"Building software is expensive, even if you're building it yourself. Before committing to a build, explore whether a cheaper test can answer your hypothesis.",[20,700,701,704],{},[55,702,703],{},"Landing page + waitlist."," Build a single-page description of the product and drive traffic to it. If people give you their email address in exchange for early access, that's a signal of interest. Add a price on the page and see if it changes the conversion rate. This can be built in a day.",[20,706,707,710],{},[55,708,709],{},"Wizard of Oz test."," Present the user with a product interface that looks automated, but a human (you) manually performs the operation behind the scenes. If customers are willing to pay for the outcome, you've validated the demand before writing the automated version.",[20,712,713,716],{},[55,714,715],{},"Concierge MVP."," Offer to do the thing your product will eventually do — manually, as a service — for a small number of customers at a price. If they pay and keep paying, you have product-market fit evidence before you've automated anything.",[20,718,719,722],{},[55,720,721],{},"Prototype with no backend."," A clickable Figma prototype or a frontend-only demo with hardcoded data can validate UX flow and the general concept without requiring any backend infrastructure.",[20,724,725],{},"Each of these is faster and cheaper than building. Use them first. Build only when you've exhausted the cheaper options or when the build is genuinely necessary to test the hypothesis.",[509,727],{},[15,729,731],{"id":730},"when-to-build-the-technical-scope-that-actually-matters","When to Build: The Technical Scope That Actually Matters",[20,733,734],{},"If you're building, here's the scope philosophy that works for most early-stage SaaS products:",[20,736,737,740],{},[55,738,739],{},"Build the core value loop only."," The core value loop is the minimum set of actions a user needs to take to experience the value your product promises. Identify those 3-5 actions and build them well. Everything else goes on a backlog.",[20,742,743,746],{},[55,744,745],{},"Use managed services for everything non-core."," Authentication (Auth0, Clerk, better-auth), email (Resend, Postmark), file storage (Cloudflare R2, AWS S3), payments (Stripe) — these are not your competitive advantage. Use the managed service and keep your build effort for the things only you can build.",[20,748,749,752],{},[55,750,751],{},"Don't optimize for scale you don't have."," A product with 50 users doesn't need Redis caching, read replicas, or a message queue. These are problems to solve when you have the load to justify them. Premature infrastructure optimization is how MVPs become 18-month projects.",[20,754,755,758],{},[55,756,757],{},"Do not skip error handling and monitoring."," This is the one place where the \"minimum\" principle needs a carve-out. An MVP that breaks silently and you find out about from a customer gives you bad data and loses you the relationship. Set up Sentry from day one. Instrument the core actions. Know when things break.",[509,760],{},[15,762,764],{"id":763},"the-development-timeline-thats-actually-achievable","The Development Timeline That's Actually Achievable",[20,766,767],{},"For a solo developer or a two-person team building a SaaS MVP with a focused scope:",[20,769,770],{},"Weeks 1-2: Core data model, authentication, basic UI scaffolding\nWeeks 3-4: Core feature 1 (the most essential part of the value loop)\nWeek 5: Core feature 2 + integration (if there is one)\nWeek 6: Basic billing integration (Stripe Checkout)\nWeek 7: Bug fixes, polish, and internal testing\nWeek 8: Soft launch to beta users",[20,772,773],{},"This assumes the requirements are locked and there's no major uncertainty in the technical approach. Add buffer for third-party integrations, which always take longer than documented.",[20,775,776],{},"Anything beyond 12 weeks to a working, paying-customer-testable product is too long for an MVP. If your MVP takes longer, either the scope has grown beyond \"minimum\" or the hypothesis isn't testable with a small product.",[509,778],{},[15,780,782],{"id":781},"reading-the-results","Reading the Results",[20,784,785],{},"After launch, the question isn't \"are people using it?\" The question is \"does what I'm observing confirm or deny my hypothesis?\"",[20,787,788],{},"Metrics to watch:",[516,790,791,794,797,800,803],{},[519,792,793],{},"Activation rate (are new users completing the core loop?)",[519,795,796],{},"Retention at 7 and 30 days (are they coming back?)",[519,798,799],{},"Willingness to pay (are they converting from trial/free to paid?)",[519,801,802],{},"The questions they ask (what's missing? what's confusing?)",[519,804,805],{},"The reasons they churn (what isn't working?)",[20,807,808],{},"Talk to your early users. Directly. Not surveys — conversations. The richest learning comes from asking someone \"walk me through how you used the product this week\" and watching where they stumble, where they feel delight, and what they expected to be there that wasn't.",[509,810],{},[20,812,813,814,51],{},"An MVP is not a destination — it's a learning instrument. Get the instrument working, get it in front of real users, and learn as fast as you can. If you're scoping an MVP and want help figuring out what's minimum and what's not, book a call at ",[502,815,817],{"href":504,"rel":816},[506],"calendly.com/jamesrossjr",[509,819],{},[15,821,514],{"id":513},[516,823,824,830,836,842],{},[519,825,826],{},[502,827,829],{"href":828},"/blog/remote-software-development","Remote Software Development: How Distributed Teams Can Build Better Products",[519,831,832],{},[502,833,835],{"href":834},"/blog/building-tech-business","Building a Tech Business Without Burning Out: What I've Learned",[519,837,838],{},[502,839,841],{"href":840},"/blog/client-communication-developers","Client Communication for Developers: How to Build Trust While You Build Software",[519,843,844],{},[502,845,847],{"href":846},"/blog/freelance-developer-vs-agency","Freelance Developer vs Software Agency: How to Choose the Right Partner",{"title":64,"searchDepth":83,"depth":83,"links":849},[850,851,852,853,854,855,856,857],{"id":585,"depth":77,"text":586},{"id":597,"depth":77,"text":598},{"id":648,"depth":77,"text":649},{"id":694,"depth":77,"text":695},{"id":730,"depth":77,"text":731},{"id":763,"depth":77,"text":764},{"id":781,"depth":77,"text":782},{"id":513,"depth":77,"text":514},"Business","An MVP is not a bad version of your product — it's a learning instrument. Here's how to scope, build, and ship an MVP that actually validates what you need to know.",[861,862],"MVP development","minimum viable product",{},"/blog/mvp-development-guide",{"title":579,"description":859},"blog/mvp-development-guide",[868,869,870],"MVP","Product Development","Startups","cmLrmEngxn5QR5sRa6cG_9Jdm-v5cSY0pgmiHu6wfC8",{"id":873,"title":874,"author":875,"body":876,"category":1150,"date":558,"description":1151,"extension":560,"featured":561,"image":562,"keywords":1152,"meta":1155,"navigation":122,"path":1156,"readTime":113,"seo":1157,"stem":1158,"tags":1159,"__hash__":1164},"blog/blog/natural-language-sql.md","Natural Language to SQL: Building Business Intelligence Without the Complexity",{"name":9,"bio":10},{"type":12,"value":877,"toc":1141},[878,882,885,888,891,893,897,900,903,909,915,921,923,927,930,933,954,966,972,978,980,984,987,990,1004,1007,1010,1016,1022,1028,1034,1040,1042,1046,1049,1052,1058,1064,1070,1072,1076,1082,1088,1094,1100,1103,1111,1113,1115],[15,879,881],{"id":880},"the-promise-and-the-problem","The Promise and the Problem",[20,883,884],{},"The promise of natural language to SQL is compelling: let non-technical business users ask questions about their data in plain English, have the system generate and run the appropriate query, and return the results. No SQL knowledge required, no dependency on a data analyst or developer for every business question, faster decisions from better data access.",[20,886,887],{},"The reality is more complex. Natural language SQL systems can work extremely well. They can also generate incorrect queries that look correct, expose sensitive data to unauthorized users, hammer databases with unoptimized queries, and give non-technical users false confidence in data they don't understand.",[20,889,890],{},"I've built natural language SQL systems for business clients. The ones that work well are the ones that take the architecture seriously. Here's what that looks like.",[509,892],{},[15,894,896],{"id":895},"how-natural-language-to-sql-works","How Natural Language to SQL Works",[20,898,899],{},"The basic mechanism is straightforward: you give a language model your database schema (table names, column names, relationships, data types) and a natural language question, and ask it to generate a SQL query that answers the question. Modern language models are remarkably good at this when the schema is well-described.",[20,901,902],{},"The architecture around this basic mechanism is what determines whether the system is reliable and safe:",[20,904,905,908],{},[55,906,907],{},"Schema context management",": The model needs to understand your schema. For small databases (10-20 tables), you can include the full schema in every prompt. For larger databases, you need schema filtering — providing only the tables relevant to the question — which requires a preprocessing step to determine relevance.",[20,910,911,914],{},[55,912,913],{},"Query validation and sanitization",": Before executing any generated query, validate it. At minimum: reject queries that contain writes (INSERT, UPDATE, DELETE, DROP) if the system is read-only, validate table and column names exist in the actual schema, check query complexity against defined limits.",[20,916,917,920],{},[55,918,919],{},"Result interpretation",": Generated SQL executes against real data and returns results. The model can help interpret those results — transforming raw query output into a natural language answer, suggesting visualizations, or identifying patterns. This completes the natural language interface.",[509,922],{},[15,924,926],{"id":925},"schema-design-that-enables-good-query-generation","Schema Design That Enables Good Query Generation",[20,928,929],{},"The quality of natural language SQL outputs is heavily influenced by how well the schema is described to the model. Tables with cryptic names and undocumented columns produce poor results. Well-annotated schemas produce much better results.",[20,931,932],{},"The investments that improve natural language SQL quality:",[20,934,935,938,939,942,943,946,947,942,950,953],{},[55,936,937],{},"Descriptive column names",": ",[37,940,941],{},"customer_lifetime_value"," is better than ",[37,944,945],{},"c_ltv",". ",[37,948,949],{},"order_status",[37,951,952],{},"status",". The model uses column names as semantic signals.",[20,955,956,959,960,962,963,965],{},[55,957,958],{},"Schema annotations",": Include natural language descriptions of tables and columns in the schema context you provide to the model. \"The ",[37,961,43],{}," table contains customer purchase records. The ",[37,964,952],{}," column values are: 'pending', 'processing', 'shipped', 'delivered', 'cancelled'.\" These annotations dramatically improve query correctness.",[20,967,968,971],{},[55,969,970],{},"Example queries",": Including a few examples of correctly-answered questions with their SQL queries is one of the most effective techniques for improving generation quality. The model learns your patterns and terminology.",[20,973,974,977],{},[55,975,976],{},"Business term mapping",": Business users ask about \"revenue\" and \"customers\" and \"active accounts.\" Your schema might use different terminology. A business term dictionary that maps user language to schema objects — documented and included in the prompt — closes this gap.",[509,979],{},[15,981,983],{"id":982},"the-safety-architecture-is-non-negotiable","The Safety Architecture Is Non-Negotiable",[20,985,986],{},"Here is the part where I'm going to be emphatic: a natural language SQL system with inadequate safety architecture is a data breach waiting to happen.",[20,988,989],{},"Language models will, if not appropriately constrained, generate queries that:",[516,991,992,995,998,1001],{},[519,993,994],{},"Access tables the user shouldn't have access to",[519,996,997],{},"Join across data boundaries in ways that expose relationships the user shouldn't see",[519,999,1000],{},"Return individual PII records rather than aggregate data",[519,1002,1003],{},"Execute expensive full-table scans that damage database performance",[20,1005,1006],{},"None of this is theoretical. These are failure modes I've tested for in systems I've built. Unguarded natural language SQL is not suitable for production deployment.",[20,1008,1009],{},"The safety architecture I implement:",[20,1011,1012,1015],{},[55,1013,1014],{},"Schema exposure control",": Only include tables the user has access to in the schema context provided to the model. If the user's role grants access to sales data but not HR data, the HR tables are not present in the schema context — the model cannot query what it doesn't know exists.",[20,1017,1018,1021],{},[55,1019,1020],{},"Generated query review layer",": Before execution, the generated query is parsed and validated programmatically: table names against the allowed set, no write operations, no functions that expose system information, query complexity within defined limits. This review is automatic and happens on every query.",[20,1023,1024,1027],{},[55,1025,1026],{},"Row-level security enforcement",": Even within allowed tables, row-level security may apply. Generated queries must be wrapped with the appropriate WHERE clauses for the user's data scope before execution. I inject these conditions programmatically, not relying on the model to include them.",[20,1029,1030,1033],{},[55,1031,1032],{},"Result sanitization",": Query results should be reviewed before display — specifically to ensure that PII fields aren't being returned when not appropriate for the query intent.",[20,1035,1036,1039],{},[55,1037,1038],{},"Audit logging",": Every natural language query, the generated SQL, the user who asked, and the timestamp should be logged. This is mandatory for compliance and invaluable for debugging.",[509,1041],{},[15,1043,1045],{"id":1044},"handling-ambiguous-and-unanswerable-questions","Handling Ambiguous and Unanswerable Questions",[20,1047,1048],{},"Natural language is inherently ambiguous. \"Show me our best customers\" could mean customers with the highest revenue, highest order count, best payment history, or best loyalty score. A natural language SQL system needs to handle ambiguity gracefully.",[20,1050,1051],{},"The approaches I use:",[20,1053,1054,1057],{},[55,1055,1056],{},"Clarification requests",": When the model cannot determine a unique interpretation of the question, have it ask for clarification rather than guessing. \"Do you mean top customers by total revenue, or by number of orders?\" is better than guessing and returning potentially misleading data.",[20,1059,1060,1063],{},[55,1061,1062],{},"Assumption disclosure",": When the model makes an assumption to resolve ambiguity, disclose it in the response. \"I interpreted 'best customers' as highest revenue in the last 12 months. Here are the results:\" makes the interpretation explicit so users can correct it.",[20,1065,1066,1069],{},[55,1067,1068],{},"Graceful failure for unanswerable questions",": Some questions can't be answered from the available data. \"What will our Q4 revenue be?\" is not answerable by SQL on historical data. The system should recognize this and explain why rather than generating a query that returns meaningless results.",[509,1071],{},[15,1073,1075],{"id":1074},"practical-implementation-considerations","Practical Implementation Considerations",[20,1077,1078,1081],{},[55,1079,1080],{},"Start with a constrained scope",": Don't launch with your entire data model exposed to natural language query. Start with a curated set of tables and metrics that represent the most common business questions, verify the system works well on those, then expand incrementally.",[20,1083,1084,1087],{},[55,1085,1086],{},"Build a question library",": Track the questions users ask, which ones generate correct queries, which generate errors, and which generate correct-looking but semantically wrong results. Use this library to improve both the schema annotations and the example queries in your prompts.",[20,1089,1090,1093],{},[55,1091,1092],{},"Provide result context",": Raw SQL results can be misleading to non-technical users. Supplement results with context: what the query measured, what time period it covers, how complete the underlying data is. Business intelligence value comes from interpreted data, not raw results.",[20,1095,1096,1099],{},[55,1097,1098],{},"Don't hide the SQL",": For business users who want to understand or validate the query, show them the generated SQL. Advanced users appreciate being able to verify what ran. And it builds appropriate trust — users know the system is querying data, not hallucinating answers.",[20,1101,1102],{},"Natural language to SQL is a genuine capability that can significantly improve data access for businesses with non-technical users. The difference between an implementation that adds value and one that adds risk is architectural rigor — specifically around safety and access control.",[20,1104,1105,1106,1110],{},"If you're evaluating natural language data access for your business and want to understand what a well-architected implementation looks like, ",[502,1107,1109],{"href":504,"rel":1108},[506],"schedule a consultation at Calendly",". I'll help you understand what's possible and what safeguards are essential.",[509,1112],{},[15,1114,514],{"id":513},[516,1116,1117,1123,1129,1135],{},[519,1118,1119],{},[502,1120,1122],{"href":1121},"/blog/ai-data-analysis-business","AI for Business Data Analysis: Moving Beyond Spreadsheets",[519,1124,1125],{},[502,1126,1128],{"href":1127},"/blog/building-chatbots-for-business","Building Chatbots for Business: Beyond the Demo",[519,1130,1131],{},[502,1132,1134],{"href":1133},"/blog/building-ai-native-applications","Building AI-Native Applications: Architecture Patterns That Actually Work",[519,1136,1137],{},[502,1138,1140],{"href":1139},"/blog/prompt-engineering-for-developers","Prompt Engineering for Software Developers: A Practical Guide",{"title":64,"searchDepth":83,"depth":83,"links":1142},[1143,1144,1145,1146,1147,1148,1149],{"id":880,"depth":77,"text":881},{"id":895,"depth":77,"text":896},{"id":925,"depth":77,"text":926},{"id":982,"depth":77,"text":983},{"id":1044,"depth":77,"text":1045},{"id":1074,"depth":77,"text":1075},{"id":513,"depth":77,"text":514},"AI","A practical guide to natural language SQL systems — how they work, how to build them reliably, and how to give non-technical users genuine data access without the risks of uncontrolled query generation.",[1153,1154],"natural language to SQL","AI data analysis",{},"/blog/natural-language-sql",{"title":874,"description":1151},"blog/natural-language-sql",[1160,1161,1150,1162,1163],"Natural Language SQL","Business Intelligence","Data Analysis","LLM","-pBsPxhJDrfQK-Lj6ktPy4ALXt-Sq4WVkQVCDzGwgk4",{"id":1166,"title":1167,"author":1168,"body":1170,"category":1532,"date":558,"description":1533,"extension":560,"featured":561,"image":562,"keywords":1534,"meta":1542,"navigation":122,"path":1543,"readTime":126,"seo":1544,"stem":1545,"tags":1546,"__hash__":1552},"blog/blog/niall-of-the-nine-hostages-ross-connection.md","Are You a Descendant of Niall of the Nine Hostages? The Ross Connection",{"name":9,"bio":1169},"Author of The Forge of Tongues — 22,000 Years of Migration, Mutation, and Memory",{"type":12,"value":1171,"toc":1520},[1172,1176,1179,1182,1185,1188,1190,1194,1197,1200,1203,1206,1208,1212,1215,1218,1223,1226,1258,1261,1264,1266,1270,1273,1276,1279,1284,1287,1290,1292,1296,1299,1307,1310,1317,1320,1323,1325,1329,1332,1338,1344,1350,1356,1358,1362,1365,1394,1400,1406,1412,1414,1418,1421,1428,1431,1434,1436,1440,1466,1473,1479,1481,1485,1488,1514,1517],[15,1173,1175],{"id":1174},"the-most-common-ancestor-youve-never-heard-of","The Most Common Ancestor You've Never Heard Of",[20,1177,1178],{},"If you carry an Irish or Scottish surname, there is a reasonable chance you share patrilineal descent with one of history's most prolific fathers.",[20,1180,1181],{},"Niall of the Nine Hostages — Niall Noígíallach in Old Irish — was a semi-legendary High King of Ireland, likely active around 400–450 AD. His genealogical claim is staggering: an estimated 2 to 3 million men worldwide are believed to carry his Y-chromosome signature. The concentration is highest in northwestern Ireland — Donegal, Mayo, Sligo — and among the Scottish descendants of the Dal Riata. Surnames associated with Niall's line include O'Neill, McLaughlin, Gallagher, O'Donnell, O'Boyle, Doherty, and dozens of others.",[20,1183,1184],{},"If you have one of those surnames, or if you're of Irish or Scottish Highland descent, you've probably wondered: am I related to Niall?",[20,1186,1187],{},"Here's what the genetics actually says — and how the Ross surname fits into the picture in a way that surprised me.",[509,1189],{},[15,1191,1193],{"id":1192},"who-was-niall-of-the-nine-hostages","Who Was Niall of the Nine Hostages?",[20,1195,1196],{},"The historical Niall is difficult to separate from the legendary one. The medieval sources describe him as High King of Ireland — a contested title that meant something like \"paramount king among competing kings\" — who conducted raids on Roman Britain and possibly on Gaul. The name \"Nine Hostages\" refers to the practice of taking hostages from subordinate kingdoms as guarantees of good behaviour: nine kingdoms, nine sets of hostages.",[20,1198,1199],{},"The one historically attested fact about Niall is his influence through his descendants. The Uí Néill dynasty — \"the grandsons of Niall\" — dominated Irish kingship for centuries. The northern Uí Néill (including the O'Neills of Ulster and their branches) and the southern Uí Néill (the O'Briens and others) controlled competing halves of the high kingship through most of the first millennium AD.",[20,1201,1202],{},"The genealogical records connecting modern surnames to Niall are extensive, detailed, and — as medieval genealogies almost always are — at least partially fabricated. Medieval Irish genealogists had a professional incentive to connect their patrons to prestigious lineages. You don't commission a genealogist to discover that your great-great-grandfather was a nobody.",[20,1204,1205],{},"What changed the picture was DNA.",[509,1207],{},[15,1209,1211],{"id":1210},"the-m222-marker-nialls-genetic-signature","The M222 Marker: Niall's Genetic Signature",[20,1213,1214],{},"In 2006, researchers led by Emmeline Hill at Trinity College Dublin published a study that identified a specific Y-chromosome mutation — M222 — as a probable marker for Niall's patrilineal descent. The mutation is most common in northwestern Ireland (where Uí Néill dominance was strongest) and in Scotland (where Uí Néill-connected Dal Riata families settled). It's found at lower but significant frequency in the Irish diaspora in the US, Canada, and Australia.",[20,1216,1217],{},"The numbers are striking. An estimated 21% of men in northwestern Ireland carry M222. In some counties in Donegal and Derry, the frequency approaches 40%. If those figures hold, and if the M222-Niall connection is correct, then Niall's Y-chromosome is among the most successfully propagated in human history.",[20,1219,1220],{},[55,1221,1222],{},"Which surnames tend to carry M222?",[20,1224,1225],{},"The highest frequencies are in surnames directly associated with the Uí Néill genealogies:",[516,1227,1228,1231,1234,1237,1240,1243,1246,1249,1252,1255],{},[519,1229,1230],{},"O'Neill (and variants: Neal, Neil, Neall)",[519,1232,1233],{},"McLaughlin / MacLochlainn",[519,1235,1236],{},"Gallagher / O'Gallchobhair",[519,1238,1239],{},"O'Donnell",[519,1241,1242],{},"Doherty / O'Dochartaigh",[519,1244,1245],{},"O'Boyle",[519,1247,1248],{},"Flanagan",[519,1250,1251],{},"Bradley",[519,1253,1254],{},"O'Kane",[519,1256,1257],{},"Quinn",[20,1259,1260],{},"This is not a complete list. M222 is also found in surnames outside the traditional Uí Néill cluster — either because the lineage spread beyond those direct descendants, or because some men with M222 aren't Niall's descendants at all (the marker predates Niall; he's not the origin of the mutation, he's just a famous early carrier).",[20,1262,1263],{},"If your surname appears in the Uí Néill lists, getting a Y-chromosome DNA test is the most direct way to find out if you carry M222.",[509,1265],{},[15,1267,1269],{"id":1268},"the-ross-clan-and-the-m222-question","The Ross Clan and the M222 Question",[20,1271,1272],{},"The Ross clan complicates the simple picture.",[20,1274,1275],{},"The Rosses are a Highland Scottish clan whose territory — Ross-shire in the northern Highlands — sits within the zone of elevated M222 frequency. The Dal Riata, who brought Irish settlers to Scotland from around 500 AD, were themselves partly of Uí Néill descent or Uí Néill-adjacent. It would be entirely plausible for a Ross patriarch to carry M222.",[20,1277,1278],{},"When I had my Y-chromosome tested through tellmegen, I went straight to the M222 result.",[20,1280,1281],{},[55,1282,1283],{},"rs11575897: GG. Ancestral state. No mutation.",[20,1285,1286],{},"I don't carry M222.",[20,1288,1289],{},"The Ross line is not a branch of Niall's dynasty.",[509,1291],{},[15,1293,1295],{"id":1294},"what-the-absence-of-m222-actually-means","What the Absence of M222 Actually Means",[20,1297,1298],{},"This was not a disappointment. It was a door opening in an unexpected direction.",[20,1300,1301,1302,1306],{},"Within the L21 haplogroup — the broader Atlantic Celtic marker that encompasses both the Ross line and the M222 clades — the absence of M222 means the Ross patriline diverged from the Niall branch ",[1303,1304,1305],"em",{},"before"," M222 occurred. Probably well before, given the estimated age of M222 (roughly 1,700–2,000 years ago).",[20,1308,1309],{},"The traditional genealogy had been saying exactly this for centuries.",[20,1311,1312,1313,1316],{},"The clan histories trace the Ross chiefs back through the earls of Ross to the O'Beolan abbots of Applecross, through the abbots to Cenel Loairn — the \"kindred of Loarn\" — and through Loarn to Erc, King of Dal Riata, who sailed from Ireland to Scotland around 500 AD. And there's the critical point: ",[55,1314,1315],{},"Loarn was the elder brother",". His younger brother Fergus took the kingship and became the ancestor of most of the Dal Riata royal lines. Loarn took the northern territories.",[20,1318,1319],{},"The traditional genealogy says the Ross line descends from the elder brother, not the line that became dominant. The DNA confirms the branch point is early — before M222, before the Uí Néill ascendancy defined the main trunk of the Irish royal lineage.",[20,1321,1322],{},"Senior Blood. Older line. Parallel to Niall, not descended from him.",[509,1324],{},[15,1326,1328],{"id":1327},"surnames-that-may-indicate-niall-descent-and-some-that-dont","Surnames That May Indicate Niall Descent (and Some That Don't)",[20,1330,1331],{},"Based on the genetic research and the medieval genealogies, here's a rough guide:",[20,1333,1334,1337],{},[55,1335,1336],{},"High M222 probability surnames:","\nO'Neill, McLaughlin, Gallagher, O'Donnell, Doherty, O'Boyle, Quinn, Bradley, Flanagan, O'Kane, Mullan, Devlin, Donnelly, Hagan, O'Hara",[20,1339,1340,1343],{},[55,1341,1342],{},"Moderate M222 probability (Uí Néill adjacent):","\nMcCarron, McGinley, McColgan, Mullan, O'Gorman, Maguire, McManus, O'Reilly (some branches)",[20,1345,1346,1349],{},[55,1347,1348],{},"Notable non-M222 lines despite Highland Scottish connection:","\nRoss (confirmed not M222), MacKay (different L21 subclade), Sutherland (mixed)",[20,1351,1352,1355],{},[55,1353,1354],{},"Important caveat:"," A surname alone cannot tell you your haplogroup. Many surnames have multiple genetic origins — some O'Neills carry M222, some don't, because the surname was shared by unrelated families who took it for different reasons. The only way to know is to test.",[509,1357],{},[15,1359,1361],{"id":1360},"how-to-find-out-if-youre-a-niall-descendant","How to Find Out If You're a Niall Descendant",[20,1363,1364],{},"Y-chromosome DNA testing has become accessible and relatively inexpensive. The steps:",[20,1366,1367,1370,1371,180,1376,1381,1382,1387,1388,1393],{},[55,1368,1369],{},"1. Choose a testing company."," ",[502,1372,1375],{"href":1373,"rel":1374},"https://www.familytreedna.com",[506],"FamilyTreeDNA",[502,1377,1380],{"href":1378,"rel":1379},"https://www.ancestry.com/dna/",[506],"AncestryDNA"," (paternal line), and ",[502,1383,1386],{"href":1384,"rel":1385},"https://www.23andme.com",[506],"23andMe"," all test some Y-chromosome markers. For haplogroup depth that includes M222, FamilyTreeDNA's Y-37 or Y-111 tests are the most informative. The ",[502,1389,1392],{"href":1390,"rel":1391},"https://www.familytreedna.com/groups/ross/about",[506],"Ross Surname DNA Project at FamilyTreeDNA"," is an existing project that aggregates results from Ross men worldwide.",[20,1395,1396,1399],{},[55,1397,1398],{},"2. Test a direct male-line relative."," Your Y-chromosome is only inherited patrilineally — father to son, unchanged (except for new mutations) through every generation. If you're testing for Niall descent, the person being tested must be a male who carries the relevant surname in their direct paternal line. Women can participate by testing a brother, father, or paternal uncle.",[20,1401,1402,1405],{},[55,1403,1404],{},"3. Look for M222 in your results."," If you test with FamilyTreeDNA and join the relevant surname project, your result will be interpreted against the reference population. M222 will appear in your haplogroup designation if you carry it.",[20,1407,1408,1411],{},[55,1409,1410],{},"4. Interpret the result correctly."," Carrying M222 doesn't mean you're definitely descended from the historical Niall — M222 predates him and some M222 carriers have no Uí Néill ancestry at all. It means you're in the same broad clade, which was heavily associated with Niall's dynasty. Not carrying M222 doesn't mean you have no Niall ancestry — it means your patrilineal line doesn't run through him.",[509,1413],{},[15,1415,1417],{"id":1416},"the-bigger-story-what-your-dna-says-about-ancient-migration","The Bigger Story: What Your DNA Says About Ancient Migration",[20,1419,1420],{},"The M222 question is one chapter of a much longer story. The R1b-L21 haplogroup that contains both M222 and the Ross patriline is itself the product of a migration that began on the Pontic-Caspian Steppe around 5,000 years ago and swept westward through what is now Ukraine, Eastern Europe, the Iberian Peninsula, and finally to Ireland and Britain.",[20,1422,1423,1424,1427],{},"The Irish ",[1303,1425,1426],{},"Lebor Gabála Érenn"," — the Book of Invasions — describes this journey in mythological terms: the Gaelic ancestors coming from Scythia, passing through Egypt, through Spain, and finally invading Ireland. For two centuries, historians dismissed this as medieval flattery.",[20,1429,1430],{},"The DNA doesn't.",[20,1432,1433],{},"The Steppe origin of R1b-L21 corresponds to Scythia. The Bell Beaker corridor through Iberia corresponds to the \"Spanish route.\" The R1b-L21 arrival in Ireland corresponds to the Milesian invasion the tradition describes.",[509,1435],{},[15,1437,1439],{"id":1438},"related-articles","Related Articles",[516,1441,1442,1448,1454,1460],{},[519,1443,1444],{},[502,1445,1447],{"href":1446},"/blog/r1b-l21-atlantic-celtic-haplogroup","What Is R1b-L21? The Atlantic Celtic Haplogroup Explained",[519,1449,1450],{},[502,1451,1453],{"href":1452},"/blog/dal-riata-irish-kingdom-created-scotland","Dal Riata: The Irish Kingdom That Created Scotland",[519,1455,1456],{},[502,1457,1459],{"href":1458},"/blog/loarn-mac-eirc-elder-brother","Loarn mac Eirc: The Elder Brother and the Senior Blood",[519,1461,1462],{},[502,1463,1465],{"href":1464},"/blog/what-is-genetic-genealogy","What Is Genetic Genealogy? A Beginner's Guide to DNA Ancestry Research",[20,1467,1468,1469,1472],{},"This convergence — genetics and tradition pointing to the same broad journey — is the argument at the centre of my book, ",[1303,1470,1471],{},"The Forge of Tongues: 22,000 Years of Migration, Mutation, and Memory",". If you want to understand not just whether you might descend from Niall, but what the full lineage behind that descent means — where it came from, how far back it goes, and what the tradition preserved that historians thought was fiction — that's what the book explores.",[20,1474,1475],{},[502,1476,1478],{"href":1477},"/book","The Forge of Tongues is available to request here.",[509,1480],{},[15,1482,1484],{"id":1483},"the-bottom-line","The Bottom Line",[20,1486,1487],{},"If you have Irish or Scottish Highland ancestry:",[516,1489,1490,1496,1502,1508],{},[519,1491,1492,1495],{},[55,1493,1494],{},"Get a Y-chromosome test"," if you haven't already. FamilyTreeDNA is the most useful for haplogroup depth.",[519,1497,1498,1501],{},[55,1499,1500],{},"Look for M222"," in your results to assess Niall descent probability.",[519,1503,1504,1507],{},[55,1505,1506],{},"Don't over-interpret"," the surname lists — surnames alone aren't reliable indicators.",[519,1509,1510,1513],{},[55,1511,1512],{},"Understand that absence of M222 doesn't close the door."," The Ross line's absence of M222 didn't end the investigation — it opened a deeper one.",[20,1515,1516],{},"The genetics of the Gaelic world is richer and more complex than any single lineage. Whether your patriline runs through Niall's dynasty, through the elder branch like the Rosses, or through any of the dozens of other L21 clades that populated these islands, the chain behind you is 22,000 years long.",[20,1518,1519],{},"Worth following to the source.",{"title":64,"searchDepth":83,"depth":83,"links":1521},[1522,1523,1524,1525,1526,1527,1528,1529,1530,1531],{"id":1174,"depth":77,"text":1175},{"id":1192,"depth":77,"text":1193},{"id":1210,"depth":77,"text":1211},{"id":1268,"depth":77,"text":1269},{"id":1294,"depth":77,"text":1295},{"id":1327,"depth":77,"text":1328},{"id":1360,"depth":77,"text":1361},{"id":1416,"depth":77,"text":1417},{"id":1438,"depth":77,"text":1439},{"id":1483,"depth":77,"text":1484},"Heritage","Niall of the Nine Hostages is one of the most prolific patrilineal ancestors in history. If you have Ross, O'Neill, or Gallagher ancestry, here's what the DNA actually says about whether you carry his lineage.",[1535,1536,1537,1538,1539,1540,1541],"niall of the nine hostages","niall of the nine hostages descendants today","niall of the nine hostages surnames","clan ross history","ross surname origin","genetic genealogy","scottish clan history",{},"/blog/niall-of-the-nine-hostages-ross-connection",{"title":1167,"description":1533},"blog/niall-of-the-nine-hostages-ross-connection",[1547,1548,1549,1550,1551],"Clan Ross","Genetic Genealogy","Scottish History","Niall of the Nine Hostages","Ross Surname","c2iJ3uZDzIPYT7y1k3BTBW9a7ZWv8FVXo74S3gVB7K8",{"id":1554,"title":1555,"author":1556,"body":1557,"category":557,"date":558,"description":4052,"extension":560,"featured":561,"image":562,"keywords":4053,"meta":4056,"navigation":122,"path":4057,"readTime":107,"seo":4058,"stem":4059,"tags":4060,"__hash__":4064},"blog/blog/nodejs-performance-optimization.md","Node.js Performance Optimization: The Practical Guide",{"name":9,"bio":10},{"type":12,"value":1558,"toc":4042},[1559,1562,1565,1569,1572,1578,1717,1720,1726,1873,1879,1883,1886,1904,1907,1934,1937,1944,1958,1965,1969,1972,1977,2144,2149,2202,2207,2276,2280,2283,2453,2700,2707,2829,2833,2836,3137,3140,3144,3147,3152,3371,3376,3531,3536,3761,3764,3833,3836,3840,3843,3991,3998,4001,4003,4009,4011,4013,4039],[20,1560,1561],{},"Node.js performance problems are almost always one of three things: event loop blocking, memory leaks, or inefficient I/O. Get these three right and most Node.js applications run well without exotic optimization. The challenge is diagnosing which one you have and finding it in a production codebase.",[20,1563,1564],{},"This article walks through practical techniques I use when a Node.js application is not performing as expected.",[15,1566,1568],{"id":1567},"measuring-before-optimizing","Measuring Before Optimizing",[20,1570,1571],{},"The first rule is to measure. Node.js performance problems often lurk in unexpected places. Profile before you optimize.",[20,1573,1574,1577],{},[55,1575,1576],{},"Event loop lag"," measures how delayed the event loop is. A healthy Node.js application has near-zero event loop lag. Anything consistently above 100ms indicates blocked I/O or synchronous work on the main thread:",[59,1579,1581],{"className":316,"code":1580,"language":318,"meta":64,"style":64},"let lastCheck = Date.now()\n\nSetInterval(() => {\n const lag = Date.now() - lastCheck - 1000 // Expected 1000ms\n lastCheck = Date.now()\n\n if (lag > 100) {\n console.warn(`Event loop lag: ${lag}ms`)\n }\n}, 1000)\n",[37,1582,1583,1603,1607,1621,1650,1662,1666,1681,1703,1707],{"__ignoreMap":64},[68,1584,1585,1588,1591,1594,1597,1600],{"class":70,"line":71},[68,1586,1587],{"class":331},"let",[68,1589,1590],{"class":342}," lastCheck ",[68,1592,1593],{"class":331},"=",[68,1595,1596],{"class":342}," Date.",[68,1598,1599],{"class":338},"now",[68,1601,1602],{"class":342},"()\n",[68,1604,1605],{"class":70,"line":77},[68,1606,123],{"emptyLinePlaceholder":122},[68,1608,1609,1612,1615,1618],{"class":70,"line":83},[68,1610,1611],{"class":338},"SetInterval",[68,1613,1614],{"class":342},"(() ",[68,1616,1617],{"class":331},"=>",[68,1619,1620],{"class":342}," {\n",[68,1622,1623,1625,1628,1630,1632,1634,1637,1640,1642,1644,1647],{"class":70,"line":89},[68,1624,376],{"class":331},[68,1626,1627],{"class":353}," lag",[68,1629,382],{"class":331},[68,1631,1596],{"class":342},[68,1633,1599],{"class":338},[68,1635,1636],{"class":342},"() ",[68,1638,1639],{"class":331},"-",[68,1641,1590],{"class":342},[68,1643,1639],{"class":331},[68,1645,1646],{"class":353}," 1000",[68,1648,1649],{"class":325}," // Expected 1000ms\n",[68,1651,1652,1654,1656,1658,1660],{"class":70,"line":95},[68,1653,1590],{"class":342},[68,1655,1593],{"class":331},[68,1657,1596],{"class":342},[68,1659,1599],{"class":338},[68,1661,1602],{"class":342},[68,1663,1664],{"class":70,"line":101},[68,1665,123],{"emptyLinePlaceholder":122},[68,1667,1668,1670,1673,1676,1679],{"class":70,"line":107},[68,1669,400],{"class":331},[68,1671,1672],{"class":342}," (lag ",[68,1674,1675],{"class":331},">",[68,1677,1678],{"class":353}," 100",[68,1680,413],{"class":342},[68,1682,1683,1686,1689,1691,1694,1697,1700],{"class":70,"line":113},[68,1684,1685],{"class":342}," console.",[68,1687,1688],{"class":338},"warn",[68,1690,343],{"class":342},[68,1692,1693],{"class":409},"`Event loop lag: ${",[68,1695,1696],{"class":342},"lag",[68,1698,1699],{"class":409},"}ms`",[68,1701,1702],{"class":342},")\n",[68,1704,1705],{"class":70,"line":119},[68,1706,429],{"class":342},[68,1708,1709,1712,1715],{"class":70,"line":126},[68,1710,1711],{"class":342},"}, ",[68,1713,1714],{"class":353},"1000",[68,1716,1702],{"class":342},[20,1718,1719],{},"In production, report this metric to your observability system (Datadog, Prometheus). A spike in event loop lag correlates directly with poor response times and user experience degradation.",[20,1721,1722,1725],{},[55,1723,1724],{},"Memory tracking"," catches leaks before they take the process down:",[59,1727,1729],{"className":316,"code":1728,"language":318,"meta":64,"style":64},"setInterval(() => {\n const { heapUsed, heapTotal, external, rss } = process.memoryUsage()\n console.log({\n heapUsedMB: Math.round(heapUsed / 1024 / 1024),\n heapTotalMB: Math.round(heapTotal / 1024 / 1024),\n rssMB: Math.round(rss / 1024 / 1024),\n })\n}, 30000) // Every 30 seconds\n",[37,1730,1731,1742,1780,1790,1815,1835,1855,1860],{"__ignoreMap":64},[68,1732,1733,1736,1738,1740],{"class":70,"line":71},[68,1734,1735],{"class":338},"setInterval",[68,1737,1614],{"class":342},[68,1739,1617],{"class":331},[68,1741,1620],{"class":342},[68,1743,1744,1746,1749,1752,1754,1757,1759,1762,1764,1767,1770,1772,1775,1778],{"class":70,"line":77},[68,1745,376],{"class":331},[68,1747,1748],{"class":342}," { ",[68,1750,1751],{"class":353},"heapUsed",[68,1753,180],{"class":342},[68,1755,1756],{"class":353},"heapTotal",[68,1758,180],{"class":342},[68,1760,1761],{"class":353},"external",[68,1763,180],{"class":342},[68,1765,1766],{"class":353},"rss",[68,1768,1769],{"class":342}," } ",[68,1771,1593],{"class":331},[68,1773,1774],{"class":342}," process.",[68,1776,1777],{"class":338},"memoryUsage",[68,1779,1602],{"class":342},[68,1781,1782,1784,1787],{"class":70,"line":83},[68,1783,1685],{"class":342},[68,1785,1786],{"class":338},"log",[68,1788,1789],{"class":342},"({\n",[68,1791,1792,1795,1798,1801,1804,1807,1810,1812],{"class":70,"line":89},[68,1793,1794],{"class":342}," heapUsedMB: Math.",[68,1796,1797],{"class":338},"round",[68,1799,1800],{"class":342},"(heapUsed ",[68,1802,1803],{"class":331},"/",[68,1805,1806],{"class":353}," 1024",[68,1808,1809],{"class":331}," /",[68,1811,1806],{"class":353},[68,1813,1814],{"class":342},"),\n",[68,1816,1817,1820,1822,1825,1827,1829,1831,1833],{"class":70,"line":95},[68,1818,1819],{"class":342}," heapTotalMB: Math.",[68,1821,1797],{"class":338},[68,1823,1824],{"class":342},"(heapTotal ",[68,1826,1803],{"class":331},[68,1828,1806],{"class":353},[68,1830,1809],{"class":331},[68,1832,1806],{"class":353},[68,1834,1814],{"class":342},[68,1836,1837,1840,1842,1845,1847,1849,1851,1853],{"class":70,"line":101},[68,1838,1839],{"class":342}," rssMB: Math.",[68,1841,1797],{"class":338},[68,1843,1844],{"class":342},"(rss ",[68,1846,1803],{"class":331},[68,1848,1806],{"class":353},[68,1850,1809],{"class":331},[68,1852,1806],{"class":353},[68,1854,1814],{"class":342},[68,1856,1857],{"class":70,"line":107},[68,1858,1859],{"class":342}," })\n",[68,1861,1862,1864,1867,1870],{"class":70,"line":113},[68,1863,1711],{"class":342},[68,1865,1866],{"class":353},"30000",[68,1868,1869],{"class":342},") ",[68,1871,1872],{"class":325},"// Every 30 seconds\n",[20,1874,1875,1876,1878],{},"If ",[37,1877,1751],{}," grows monotonically over hours, you have a memory leak. If it grows and shrinks, the garbage collector is working normally.",[15,1880,1882],{"id":1881},"profiling-cpu-usage","Profiling CPU Usage",[20,1884,1885],{},"When you know the event loop is slow but not why, use Node.js's built-in profiler:",[59,1887,1891],{"className":1888,"code":1889,"language":1890,"meta":64,"style":64},"language-bash shiki shiki-themes github-dark","node --prof app.js\n","bash",[37,1892,1893],{"__ignoreMap":64},[68,1894,1895,1898,1901],{"class":70,"line":71},[68,1896,1897],{"class":338},"node",[68,1899,1900],{"class":353}," --prof",[68,1902,1903],{"class":409}," app.js\n",[20,1905,1906],{},"After running under load, process the profile:",[59,1908,1910],{"className":1888,"code":1909,"language":1890,"meta":64,"style":64},"node --prof-process isolate-*.log > processed.txt\n",[37,1911,1912],{"__ignoreMap":64},[68,1913,1914,1916,1919,1922,1925,1928,1931],{"class":70,"line":71},[68,1915,1897],{"class":338},[68,1917,1918],{"class":353}," --prof-process",[68,1920,1921],{"class":409}," isolate-",[68,1923,1924],{"class":353},"*",[68,1926,1927],{"class":409},".log",[68,1929,1930],{"class":331}," >",[68,1932,1933],{"class":409}," processed.txt\n",[20,1935,1936],{},"The output shows which functions are consuming CPU time. Look for synchronous operations — JSON parsing, cryptography, string manipulation — in hot paths.",[20,1938,1939,1940,1943],{},"For more modern profiling, use the ",[37,1941,1942],{},"--inspect"," flag with Chrome DevTools:",[59,1945,1947],{"className":1888,"code":1946,"language":1890,"meta":64,"style":64},"node --inspect app.js\n",[37,1948,1949],{"__ignoreMap":64},[68,1950,1951,1953,1956],{"class":70,"line":71},[68,1952,1897],{"class":338},[68,1954,1955],{"class":353}," --inspect",[68,1957,1903],{"class":409},[20,1959,1960,1961,1964],{},"Open ",[37,1962,1963],{},"chrome://inspect"," in Chrome and attach to the Node process. The Performance tab provides flame charts that show exactly where time is spent.",[15,1966,1968],{"id":1967},"the-event-loop-blocking-patterns","The Event Loop Blocking Patterns",[20,1970,1971],{},"The most common Node.js performance mistakes all share a root cause: blocking the single-threaded event loop with synchronous work.",[20,1973,1974],{},[55,1975,1976],{},"Synchronous JSON parsing of large objects:",[59,1978,1980],{"className":316,"code":1979,"language":318,"meta":64,"style":64},"// BAD: Blocks the event loop for the duration of parsing\nconst huge = JSON.parse(fs.readFileSync('huge-file.json', 'utf8'))\n\n// BETTER: Use async file reading + streaming for very large files\nimport { createReadStream } from 'fs'\nimport { pipeline } from 'stream/promises'\nimport JSONStream from 'JSONStream'\n\nAsync function processLargeJSON(filePath: string) {\n const stream = createReadStream(filePath)\n const parser = JSONStream.parse('*')\n // Process items as they stream rather than loading all at once\n}\n",[37,1981,1982,1987,2024,2028,2033,2047,2059,2071,2075,2097,2112,2133,2139],{"__ignoreMap":64},[68,1983,1984],{"class":70,"line":71},[68,1985,1986],{"class":325},"// BAD: Blocks the event loop for the duration of parsing\n",[68,1988,1989,1992,1995,1997,2000,2002,2005,2008,2011,2013,2016,2018,2021],{"class":70,"line":77},[68,1990,1991],{"class":331},"const",[68,1993,1994],{"class":353}," huge",[68,1996,382],{"class":331},[68,1998,1999],{"class":353}," JSON",[68,2001,51],{"class":342},[68,2003,2004],{"class":338},"parse",[68,2006,2007],{"class":342},"(fs.",[68,2009,2010],{"class":338},"readFileSync",[68,2012,343],{"class":342},[68,2014,2015],{"class":409},"'huge-file.json'",[68,2017,180],{"class":342},[68,2019,2020],{"class":409},"'utf8'",[68,2022,2023],{"class":342},"))\n",[68,2025,2026],{"class":70,"line":83},[68,2027,123],{"emptyLinePlaceholder":122},[68,2029,2030],{"class":70,"line":89},[68,2031,2032],{"class":325},"// BETTER: Use async file reading + streaming for very large files\n",[68,2034,2035,2038,2041,2044],{"class":70,"line":95},[68,2036,2037],{"class":331},"import",[68,2039,2040],{"class":342}," { createReadStream } ",[68,2042,2043],{"class":331},"from",[68,2045,2046],{"class":409}," 'fs'\n",[68,2048,2049,2051,2054,2056],{"class":70,"line":101},[68,2050,2037],{"class":331},[68,2052,2053],{"class":342}," { pipeline } ",[68,2055,2043],{"class":331},[68,2057,2058],{"class":409}," 'stream/promises'\n",[68,2060,2061,2063,2066,2068],{"class":70,"line":107},[68,2062,2037],{"class":331},[68,2064,2065],{"class":342}," JSONStream ",[68,2067,2043],{"class":331},[68,2069,2070],{"class":409}," 'JSONStream'\n",[68,2072,2073],{"class":70,"line":113},[68,2074,123],{"emptyLinePlaceholder":122},[68,2076,2077,2080,2083,2086,2088,2091,2093,2095],{"class":70,"line":119},[68,2078,2079],{"class":342},"Async ",[68,2081,2082],{"class":331},"function",[68,2084,2085],{"class":338}," processLargeJSON",[68,2087,343],{"class":342},[68,2089,2090],{"class":346},"filePath",[68,2092,350],{"class":331},[68,2094,354],{"class":353},[68,2096,413],{"class":342},[68,2098,2099,2101,2104,2106,2109],{"class":70,"line":126},[68,2100,376],{"class":331},[68,2102,2103],{"class":353}," stream",[68,2105,382],{"class":331},[68,2107,2108],{"class":338}," createReadStream",[68,2110,2111],{"class":342},"(filePath)\n",[68,2113,2114,2116,2119,2121,2124,2126,2128,2131],{"class":70,"line":132},[68,2115,376],{"class":331},[68,2117,2118],{"class":353}," parser",[68,2120,382],{"class":331},[68,2122,2123],{"class":342}," JSONStream.",[68,2125,2004],{"class":338},[68,2127,343],{"class":342},[68,2129,2130],{"class":409},"'*'",[68,2132,1702],{"class":342},[68,2134,2136],{"class":70,"line":2135},12,[68,2137,2138],{"class":325}," // Process items as they stream rather than loading all at once\n",[68,2140,2142],{"class":70,"line":2141},13,[68,2143,447],{"class":342},[20,2145,2146],{},[55,2147,2148],{},"Regular expressions with catastrophic backtracking:",[59,2150,2152],{"className":316,"code":2151,"language":318,"meta":64,"style":64},"// This regex can block for seconds on certain inputs (ReDoS)\nconst BAD_REGEX = /^(a+)+$/\n\n// Test your regex against adversarial inputs before production\n// Use a ReDoS checker tool\n",[37,2153,2154,2159,2188,2192,2197],{"__ignoreMap":64},[68,2155,2156],{"class":70,"line":71},[68,2157,2158],{"class":325},"// This regex can block for seconds on certain inputs (ReDoS)\n",[68,2160,2161,2163,2166,2168,2170,2173,2177,2180,2182,2185],{"class":70,"line":77},[68,2162,1991],{"class":331},[68,2164,2165],{"class":353}," BAD_REGEX",[68,2167,382],{"class":331},[68,2169,1809],{"class":409},[68,2171,2172],{"class":331},"^",[68,2174,2176],{"class":2175},"sns5M","(a",[68,2178,2179],{"class":331},"+",[68,2181,357],{"class":2175},[68,2183,2184],{"class":331},"+$",[68,2186,2187],{"class":409},"/\n",[68,2189,2190],{"class":70,"line":83},[68,2191,123],{"emptyLinePlaceholder":122},[68,2193,2194],{"class":70,"line":89},[68,2195,2196],{"class":325},"// Test your regex against adversarial inputs before production\n",[68,2198,2199],{"class":70,"line":95},[68,2200,2201],{"class":325},"// Use a ReDoS checker tool\n",[20,2203,2204],{},[55,2205,2206],{},"Synchronous cryptography:",[59,2208,2210],{"className":316,"code":2209,"language":318,"meta":64,"style":64},"// BAD: bcrypt.hashSync blocks the event loop\nconst hash = bcrypt.hashSync(password, 12) // Can take 200-500ms\n\n// GOOD: Use async version\nconst hash = await bcrypt.hash(password, 12) // Non-blocking\n",[37,2211,2212,2217,2243,2247,2252],{"__ignoreMap":64},[68,2213,2214],{"class":70,"line":71},[68,2215,2216],{"class":325},"// BAD: bcrypt.hashSync blocks the event loop\n",[68,2218,2219,2221,2224,2226,2229,2232,2235,2238,2240],{"class":70,"line":77},[68,2220,1991],{"class":331},[68,2222,2223],{"class":353}," hash",[68,2225,382],{"class":331},[68,2227,2228],{"class":342}," bcrypt.",[68,2230,2231],{"class":338},"hashSync",[68,2233,2234],{"class":342},"(password, ",[68,2236,2237],{"class":353},"12",[68,2239,1869],{"class":342},[68,2241,2242],{"class":325},"// Can take 200-500ms\n",[68,2244,2245],{"class":70,"line":83},[68,2246,123],{"emptyLinePlaceholder":122},[68,2248,2249],{"class":70,"line":89},[68,2250,2251],{"class":325},"// GOOD: Use async version\n",[68,2253,2254,2256,2258,2260,2262,2264,2267,2269,2271,2273],{"class":70,"line":95},[68,2255,1991],{"class":331},[68,2257,2223],{"class":353},[68,2259,382],{"class":331},[68,2261,385],{"class":331},[68,2263,2228],{"class":342},[68,2265,2266],{"class":338},"hash",[68,2268,2234],{"class":342},[68,2270,2237],{"class":353},[68,2272,1869],{"class":342},[68,2274,2275],{"class":325},"// Non-blocking\n",[15,2277,2279],{"id":2278},"worker-threads-for-cpu-intensive-work","Worker Threads for CPU-Intensive Work",[20,2281,2282],{},"For genuinely CPU-intensive tasks (image processing, PDF generation, data transformation), offload to worker threads:",[59,2284,2286],{"className":316,"code":2285,"language":318,"meta":64,"style":64},"// workers/imageProcessor.ts\nimport { parentPort, workerData } from 'worker_threads'\nimport sharp from 'sharp'\n\nAsync function processImage() {\n const { inputBuffer, width, height, format } = workerData\n\n const result = await sharp(inputBuffer)\n .resize(width, height, { fit: 'inside' })\n .toFormat(format)\n .toBuffer()\n\n parentPort?.postMessage(result, [result.buffer])\n}\n\nProcessImage()\n",[37,2287,2288,2293,2305,2317,2321,2333,2364,2368,2385,2401,2411,2420,2424,2435,2440,2445],{"__ignoreMap":64},[68,2289,2290],{"class":70,"line":71},[68,2291,2292],{"class":325},"// workers/imageProcessor.ts\n",[68,2294,2295,2297,2300,2302],{"class":70,"line":77},[68,2296,2037],{"class":331},[68,2298,2299],{"class":342}," { parentPort, workerData } ",[68,2301,2043],{"class":331},[68,2303,2304],{"class":409}," 'worker_threads'\n",[68,2306,2307,2309,2312,2314],{"class":70,"line":83},[68,2308,2037],{"class":331},[68,2310,2311],{"class":342}," sharp ",[68,2313,2043],{"class":331},[68,2315,2316],{"class":409}," 'sharp'\n",[68,2318,2319],{"class":70,"line":89},[68,2320,123],{"emptyLinePlaceholder":122},[68,2322,2323,2325,2327,2330],{"class":70,"line":95},[68,2324,2079],{"class":342},[68,2326,2082],{"class":331},[68,2328,2329],{"class":338}," processImage",[68,2331,2332],{"class":342},"() {\n",[68,2334,2335,2337,2339,2342,2344,2347,2349,2352,2354,2357,2359,2361],{"class":70,"line":101},[68,2336,376],{"class":331},[68,2338,1748],{"class":342},[68,2340,2341],{"class":353},"inputBuffer",[68,2343,180],{"class":342},[68,2345,2346],{"class":353},"width",[68,2348,180],{"class":342},[68,2350,2351],{"class":353},"height",[68,2353,180],{"class":342},[68,2355,2356],{"class":353},"format",[68,2358,1769],{"class":342},[68,2360,1593],{"class":331},[68,2362,2363],{"class":342}," workerData\n",[68,2365,2366],{"class":70,"line":107},[68,2367,123],{"emptyLinePlaceholder":122},[68,2369,2370,2372,2375,2377,2379,2382],{"class":70,"line":113},[68,2371,376],{"class":331},[68,2373,2374],{"class":353}," result",[68,2376,382],{"class":331},[68,2378,385],{"class":331},[68,2380,2381],{"class":338}," sharp",[68,2383,2384],{"class":342},"(inputBuffer)\n",[68,2386,2387,2390,2393,2396,2399],{"class":70,"line":119},[68,2388,2389],{"class":342}," .",[68,2391,2392],{"class":338},"resize",[68,2394,2395],{"class":342},"(width, height, { fit: ",[68,2397,2398],{"class":409},"'inside'",[68,2400,1859],{"class":342},[68,2402,2403,2405,2408],{"class":70,"line":126},[68,2404,2389],{"class":342},[68,2406,2407],{"class":338},"toFormat",[68,2409,2410],{"class":342},"(format)\n",[68,2412,2413,2415,2418],{"class":70,"line":132},[68,2414,2389],{"class":342},[68,2416,2417],{"class":338},"toBuffer",[68,2419,1602],{"class":342},[68,2421,2422],{"class":70,"line":2135},[68,2423,123],{"emptyLinePlaceholder":122},[68,2425,2426,2429,2432],{"class":70,"line":2141},[68,2427,2428],{"class":342}," parentPort?.",[68,2430,2431],{"class":338},"postMessage",[68,2433,2434],{"class":342},"(result, [result.buffer])\n",[68,2436,2438],{"class":70,"line":2437},14,[68,2439,447],{"class":342},[68,2441,2443],{"class":70,"line":2442},15,[68,2444,123],{"emptyLinePlaceholder":122},[68,2446,2448,2451],{"class":70,"line":2447},16,[68,2449,2450],{"class":338},"ProcessImage",[68,2452,1602],{"class":342},[59,2454,2456],{"className":316,"code":2455,"language":318,"meta":64,"style":64},"// In your main application\nimport { Worker } from 'worker_threads'\n\nFunction processImageInWorker(\n inputBuffer: Buffer,\n options: { width: number; height: number; format: string }\n): Promise\u003CBuffer> {\n return new Promise((resolve, reject) => {\n const worker = new Worker('./dist/workers/imageProcessor.js', {\n workerData: { inputBuffer, ...options },\n transferList: [inputBuffer.buffer],\n })\n\n worker.on('message', resolve)\n worker.on('error', reject)\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker exited with code ${code}`))\n })\n })\n}\n",[37,2457,2458,2463,2474,2478,2489,2494,2499,2516,2540,2563,2574,2579,2583,2587,2607,2626,2650,2685,2690,2695],{"__ignoreMap":64},[68,2459,2460],{"class":70,"line":71},[68,2461,2462],{"class":325},"// In your main application\n",[68,2464,2465,2467,2470,2472],{"class":70,"line":77},[68,2466,2037],{"class":331},[68,2468,2469],{"class":342}," { Worker } ",[68,2471,2043],{"class":331},[68,2473,2304],{"class":409},[68,2475,2476],{"class":70,"line":83},[68,2477,123],{"emptyLinePlaceholder":122},[68,2479,2480,2483,2486],{"class":70,"line":89},[68,2481,2482],{"class":342},"Function ",[68,2484,2485],{"class":338},"processImageInWorker",[68,2487,2488],{"class":342},"(\n",[68,2490,2491],{"class":70,"line":95},[68,2492,2493],{"class":342}," inputBuffer: Buffer,\n",[68,2495,2496],{"class":70,"line":101},[68,2497,2498],{"class":342}," options: { width: number; height: number; format: string }\n",[68,2500,2501,2504,2507,2509,2512,2514],{"class":70,"line":107},[68,2502,2503],{"class":342},"): ",[68,2505,2506],{"class":353},"Promise",[68,2508,365],{"class":331},[68,2510,2511],{"class":342},"Buffer",[68,2513,1675],{"class":331},[68,2515,1620],{"class":342},[68,2517,2518,2521,2523,2526,2529,2531,2534,2536,2538],{"class":70,"line":113},[68,2519,2520],{"class":342}," return new ",[68,2522,2506],{"class":338},[68,2524,2525],{"class":342},"((",[68,2527,2528],{"class":346},"resolve",[68,2530,180],{"class":342},[68,2532,2533],{"class":346},"reject",[68,2535,1869],{"class":342},[68,2537,1617],{"class":331},[68,2539,1620],{"class":342},[68,2541,2542,2544,2547,2549,2552,2555,2557,2560],{"class":70,"line":119},[68,2543,376],{"class":338},[68,2545,2546],{"class":346}," worker",[68,2548,382],{"class":331},[68,2550,2551],{"class":331}," new",[68,2553,2554],{"class":338}," Worker",[68,2556,343],{"class":342},[68,2558,2559],{"class":409},"'./dist/workers/imageProcessor.js'",[68,2561,2562],{"class":342},", {\n",[68,2564,2565,2568,2571],{"class":70,"line":126},[68,2566,2567],{"class":342}," workerData: { inputBuffer, ",[68,2569,2570],{"class":331},"...",[68,2572,2573],{"class":342},"options },\n",[68,2575,2576],{"class":70,"line":132},[68,2577,2578],{"class":342}," transferList: [inputBuffer.buffer],\n",[68,2580,2581],{"class":70,"line":2135},[68,2582,1859],{"class":342},[68,2584,2585],{"class":70,"line":2141},[68,2586,123],{"emptyLinePlaceholder":122},[68,2588,2589,2591,2593,2596,2598,2601,2603,2605],{"class":70,"line":2437},[68,2590,2546],{"class":338},[68,2592,51],{"class":342},[68,2594,2595],{"class":338},"on",[68,2597,343],{"class":342},[68,2599,2600],{"class":409},"'message'",[68,2602,180],{"class":342},[68,2604,2528],{"class":338},[68,2606,1702],{"class":342},[68,2608,2609,2611,2613,2615,2617,2620,2622,2624],{"class":70,"line":2442},[68,2610,2546],{"class":338},[68,2612,51],{"class":342},[68,2614,2595],{"class":338},[68,2616,343],{"class":342},[68,2618,2619],{"class":409},"'error'",[68,2621,180],{"class":342},[68,2623,2533],{"class":338},[68,2625,1702],{"class":342},[68,2627,2628,2630,2632,2634,2636,2639,2642,2644,2646,2648],{"class":70,"line":2447},[68,2629,2546],{"class":338},[68,2631,51],{"class":342},[68,2633,2595],{"class":338},[68,2635,343],{"class":342},[68,2637,2638],{"class":409},"'exit'",[68,2640,2641],{"class":342},", (",[68,2643,37],{"class":346},[68,2645,1869],{"class":342},[68,2647,1617],{"class":331},[68,2649,1620],{"class":342},[68,2651,2653,2655,2658,2660,2663,2665,2667,2670,2673,2675,2678,2680,2683],{"class":70,"line":2652},17,[68,2654,400],{"class":338},[68,2656,2657],{"class":342}," (",[68,2659,37],{"class":346},[68,2661,2662],{"class":342}," !== 0) ",[68,2664,2533],{"class":338},[68,2666,343],{"class":342},[68,2668,2669],{"class":346},"new",[68,2671,2672],{"class":346}," Error",[68,2674,343],{"class":342},[68,2676,2677],{"class":409},"`Worker exited with code ${",[68,2679,37],{"class":342},[68,2681,2682],{"class":409},"}`",[68,2684,2023],{"class":342},[68,2686,2688],{"class":70,"line":2687},18,[68,2689,1859],{"class":342},[68,2691,2693],{"class":70,"line":2692},19,[68,2694,1859],{"class":342},[68,2696,2698],{"class":70,"line":2697},20,[68,2699,447],{"class":342},[20,2701,2702,2703,2706],{},"A worker thread pool is more efficient than creating a new worker per request. Libraries like ",[37,2704,2705],{},"piscina"," provide worker pool management:",[59,2708,2710],{"className":316,"code":2709,"language":318,"meta":64,"style":64},"import Piscina from 'piscina'\n\nConst pool = new Piscina({\n filename: './dist/workers/imageProcessor.js',\n maxThreads: Math.max(1, os.cpus().length - 1),\n})\n\nConst result = await pool.run({ inputBuffer, width: 800, height: 600, format: 'webp' })\n",[37,2711,2712,2724,2728,2742,2752,2785,2790,2794],{"__ignoreMap":64},[68,2713,2714,2716,2719,2721],{"class":70,"line":71},[68,2715,2037],{"class":331},[68,2717,2718],{"class":342}," Piscina ",[68,2720,2043],{"class":331},[68,2722,2723],{"class":409}," 'piscina'\n",[68,2725,2726],{"class":70,"line":77},[68,2727,123],{"emptyLinePlaceholder":122},[68,2729,2730,2733,2735,2737,2740],{"class":70,"line":83},[68,2731,2732],{"class":342},"Const pool ",[68,2734,1593],{"class":331},[68,2736,2551],{"class":331},[68,2738,2739],{"class":338}," Piscina",[68,2741,1789],{"class":342},[68,2743,2744,2747,2749],{"class":70,"line":89},[68,2745,2746],{"class":342}," filename: ",[68,2748,2559],{"class":409},[68,2750,2751],{"class":342},",\n",[68,2753,2754,2757,2760,2762,2765,2768,2771,2774,2777,2780,2783],{"class":70,"line":95},[68,2755,2756],{"class":342}," maxThreads: Math.",[68,2758,2759],{"class":338},"max",[68,2761,343],{"class":342},[68,2763,2764],{"class":353},"1",[68,2766,2767],{"class":342},", os.",[68,2769,2770],{"class":338},"cpus",[68,2772,2773],{"class":342},"().",[68,2775,2776],{"class":353},"length",[68,2778,2779],{"class":331}," -",[68,2781,2782],{"class":353}," 1",[68,2784,1814],{"class":342},[68,2786,2787],{"class":70,"line":101},[68,2788,2789],{"class":342},"})\n",[68,2791,2792],{"class":70,"line":107},[68,2793,123],{"emptyLinePlaceholder":122},[68,2795,2796,2799,2801,2803,2806,2809,2812,2815,2818,2821,2824,2827],{"class":70,"line":113},[68,2797,2798],{"class":342},"Const result ",[68,2800,1593],{"class":331},[68,2802,385],{"class":331},[68,2804,2805],{"class":342}," pool.",[68,2807,2808],{"class":338},"run",[68,2810,2811],{"class":342},"({ inputBuffer, width: ",[68,2813,2814],{"class":353},"800",[68,2816,2817],{"class":342},", height: ",[68,2819,2820],{"class":353},"600",[68,2822,2823],{"class":342},", format: ",[68,2825,2826],{"class":409},"'webp'",[68,2828,1859],{"class":342},[15,2830,2832],{"id":2831},"clustering-for-multi-core-use","Clustering for Multi-Core use",[20,2834,2835],{},"Node.js runs on a single CPU core by default. For web servers, use clustering to use all available cores:",[59,2837,2839],{"className":316,"code":2838,"language":318,"meta":64,"style":64},"// cluster.ts\nimport cluster from 'cluster'\nimport os from 'os'\nimport { createServer } from './app'\n\nIf (cluster.isPrimary) {\n const numCPUs = os.cpus().length\n console.log(`Primary ${process.pid} is running. Spawning ${numCPUs} workers.`)\n\n for (let i = 0; i \u003C numCPUs; i++) {\n cluster.fork()\n }\n\n cluster.on('exit', (worker, code, signal) => {\n console.warn(`Worker ${worker.process.pid} died (${signal || code}). Restarting.`)\n cluster.fork()\n })\n} else {\n createServer().listen(3000, () => {\n console.log(`Worker ${process.pid} started`)\n })\n}\n",[37,2840,2841,2846,2858,2870,2882,2886,2894,2913,2943,2947,2977,2987,2991,2995,3025,3062,3070,3074,3084,3106,3127,3132],{"__ignoreMap":64},[68,2842,2843],{"class":70,"line":71},[68,2844,2845],{"class":325},"// cluster.ts\n",[68,2847,2848,2850,2853,2855],{"class":70,"line":77},[68,2849,2037],{"class":331},[68,2851,2852],{"class":342}," cluster ",[68,2854,2043],{"class":331},[68,2856,2857],{"class":409}," 'cluster'\n",[68,2859,2860,2862,2865,2867],{"class":70,"line":83},[68,2861,2037],{"class":331},[68,2863,2864],{"class":342}," os ",[68,2866,2043],{"class":331},[68,2868,2869],{"class":409}," 'os'\n",[68,2871,2872,2874,2877,2879],{"class":70,"line":89},[68,2873,2037],{"class":331},[68,2875,2876],{"class":342}," { createServer } ",[68,2878,2043],{"class":331},[68,2880,2881],{"class":409}," './app'\n",[68,2883,2884],{"class":70,"line":95},[68,2885,123],{"emptyLinePlaceholder":122},[68,2887,2888,2891],{"class":70,"line":101},[68,2889,2890],{"class":338},"If",[68,2892,2893],{"class":342}," (cluster.isPrimary) {\n",[68,2895,2896,2898,2901,2903,2906,2908,2910],{"class":70,"line":107},[68,2897,376],{"class":331},[68,2899,2900],{"class":353}," numCPUs",[68,2902,382],{"class":331},[68,2904,2905],{"class":342}," os.",[68,2907,2770],{"class":338},[68,2909,2773],{"class":342},[68,2911,2912],{"class":353},"length\n",[68,2914,2915,2917,2919,2921,2924,2927,2929,2932,2935,2938,2941],{"class":70,"line":113},[68,2916,1685],{"class":342},[68,2918,1786],{"class":338},[68,2920,343],{"class":342},[68,2922,2923],{"class":409},"`Primary ${",[68,2925,2926],{"class":342},"process",[68,2928,51],{"class":409},[68,2930,2931],{"class":342},"pid",[68,2933,2934],{"class":409},"} is running. Spawning ${",[68,2936,2937],{"class":342},"numCPUs",[68,2939,2940],{"class":409},"} workers.`",[68,2942,1702],{"class":342},[68,2944,2945],{"class":70,"line":119},[68,2946,123],{"emptyLinePlaceholder":122},[68,2948,2949,2952,2954,2956,2959,2961,2964,2967,2969,2972,2975],{"class":70,"line":126},[68,2950,2951],{"class":331}," for",[68,2953,2657],{"class":342},[68,2955,1587],{"class":331},[68,2957,2958],{"class":342}," i ",[68,2960,1593],{"class":331},[68,2962,2963],{"class":353}," 0",[68,2965,2966],{"class":342},"; i ",[68,2968,365],{"class":331},[68,2970,2971],{"class":342}," numCPUs; i",[68,2973,2974],{"class":331},"++",[68,2976,413],{"class":342},[68,2978,2979,2982,2985],{"class":70,"line":132},[68,2980,2981],{"class":342}," cluster.",[68,2983,2984],{"class":338},"fork",[68,2986,1602],{"class":342},[68,2988,2989],{"class":70,"line":2135},[68,2990,429],{"class":342},[68,2992,2993],{"class":70,"line":2141},[68,2994,123],{"emptyLinePlaceholder":122},[68,2996,2997,2999,3001,3003,3005,3007,3010,3012,3014,3016,3019,3021,3023],{"class":70,"line":2437},[68,2998,2981],{"class":342},[68,3000,2595],{"class":338},[68,3002,343],{"class":342},[68,3004,2638],{"class":409},[68,3006,2641],{"class":342},[68,3008,3009],{"class":346},"worker",[68,3011,180],{"class":342},[68,3013,37],{"class":346},[68,3015,180],{"class":342},[68,3017,3018],{"class":346},"signal",[68,3020,1869],{"class":342},[68,3022,1617],{"class":331},[68,3024,1620],{"class":342},[68,3026,3027,3029,3031,3033,3036,3038,3040,3042,3044,3046,3049,3051,3054,3057,3060],{"class":70,"line":2442},[68,3028,1685],{"class":342},[68,3030,1688],{"class":338},[68,3032,343],{"class":342},[68,3034,3035],{"class":409},"`Worker ${",[68,3037,3009],{"class":342},[68,3039,51],{"class":409},[68,3041,2926],{"class":342},[68,3043,51],{"class":409},[68,3045,2931],{"class":342},[68,3047,3048],{"class":409},"} died (${",[68,3050,3018],{"class":342},[68,3052,3053],{"class":331}," ||",[68,3055,3056],{"class":342}," code",[68,3058,3059],{"class":409},"}). Restarting.`",[68,3061,1702],{"class":342},[68,3063,3064,3066,3068],{"class":70,"line":2447},[68,3065,2981],{"class":342},[68,3067,2984],{"class":338},[68,3069,1602],{"class":342},[68,3071,3072],{"class":70,"line":2652},[68,3073,1859],{"class":342},[68,3075,3076,3079,3082],{"class":70,"line":2687},[68,3077,3078],{"class":342},"} ",[68,3080,3081],{"class":331},"else",[68,3083,1620],{"class":342},[68,3085,3086,3089,3091,3094,3096,3099,3102,3104],{"class":70,"line":2692},[68,3087,3088],{"class":338}," createServer",[68,3090,2773],{"class":342},[68,3092,3093],{"class":338},"listen",[68,3095,343],{"class":342},[68,3097,3098],{"class":353},"3000",[68,3100,3101],{"class":342},", () ",[68,3103,1617],{"class":331},[68,3105,1620],{"class":342},[68,3107,3108,3110,3112,3114,3116,3118,3120,3122,3125],{"class":70,"line":2697},[68,3109,1685],{"class":342},[68,3111,1786],{"class":338},[68,3113,343],{"class":342},[68,3115,3035],{"class":409},[68,3117,2926],{"class":342},[68,3119,51],{"class":409},[68,3121,2931],{"class":342},[68,3123,3124],{"class":409},"} started`",[68,3126,1702],{"class":342},[68,3128,3130],{"class":70,"line":3129},21,[68,3131,1859],{"class":342},[68,3133,3135],{"class":70,"line":3134},22,[68,3136,447],{"class":342},[20,3138,3139],{},"In practice, I prefer running multiple single-process instances behind a load balancer (with PM2 or Docker) rather than Node.js clustering. The isolation is better — a crash in one process does not affect others, and rolling restarts are cleaner.",[15,3141,3143],{"id":3142},"memory-leak-detection","Memory Leak Detection",[20,3145,3146],{},"Memory leaks in Node.js applications typically come from:",[20,3148,3149],{},[55,3150,3151],{},"Event listeners not removed:",[59,3153,3155],{"className":316,"code":3154,"language":318,"meta":64,"style":64},"// BAD: Every request attaches a listener that never gets removed\napp.get('/stream', (req, res) => {\n const dataSource = new EventEmitter()\n dataSource.on('data', (chunk) => res.write(chunk))\n // dataSource is never cleaned up if the request closes early\n})\n\n// GOOD: Clean up when the connection closes\napp.get('/stream', (req, res) => {\n const dataSource = new EventEmitter()\n const handler = (chunk: Buffer) => res.write(chunk)\n\n dataSource.on('data', handler)\n req.on('close', () => dataSource.off('data', handler))\n})\n",[37,3156,3157,3162,3191,3207,3237,3242,3246,3250,3255,3279,3293,3322,3326,3339,3367],{"__ignoreMap":64},[68,3158,3159],{"class":70,"line":71},[68,3160,3161],{"class":325},"// BAD: Every request attaches a listener that never gets removed\n",[68,3163,3164,3167,3170,3172,3175,3177,3180,3182,3185,3187,3189],{"class":70,"line":77},[68,3165,3166],{"class":342},"app.",[68,3168,3169],{"class":338},"get",[68,3171,343],{"class":342},[68,3173,3174],{"class":409},"'/stream'",[68,3176,2641],{"class":342},[68,3178,3179],{"class":346},"req",[68,3181,180],{"class":342},[68,3183,3184],{"class":346},"res",[68,3186,1869],{"class":342},[68,3188,1617],{"class":331},[68,3190,1620],{"class":342},[68,3192,3193,3195,3198,3200,3202,3205],{"class":70,"line":83},[68,3194,376],{"class":331},[68,3196,3197],{"class":353}," dataSource",[68,3199,382],{"class":331},[68,3201,2551],{"class":331},[68,3203,3204],{"class":338}," EventEmitter",[68,3206,1602],{"class":342},[68,3208,3209,3212,3214,3216,3219,3221,3224,3226,3228,3231,3234],{"class":70,"line":89},[68,3210,3211],{"class":342}," dataSource.",[68,3213,2595],{"class":338},[68,3215,343],{"class":342},[68,3217,3218],{"class":409},"'data'",[68,3220,2641],{"class":342},[68,3222,3223],{"class":346},"chunk",[68,3225,1869],{"class":342},[68,3227,1617],{"class":331},[68,3229,3230],{"class":342}," res.",[68,3232,3233],{"class":338},"write",[68,3235,3236],{"class":342},"(chunk))\n",[68,3238,3239],{"class":70,"line":95},[68,3240,3241],{"class":325}," // dataSource is never cleaned up if the request closes early\n",[68,3243,3244],{"class":70,"line":101},[68,3245,2789],{"class":342},[68,3247,3248],{"class":70,"line":107},[68,3249,123],{"emptyLinePlaceholder":122},[68,3251,3252],{"class":70,"line":113},[68,3253,3254],{"class":325},"// GOOD: Clean up when the connection closes\n",[68,3256,3257,3259,3261,3263,3265,3267,3269,3271,3273,3275,3277],{"class":70,"line":119},[68,3258,3166],{"class":342},[68,3260,3169],{"class":338},[68,3262,343],{"class":342},[68,3264,3174],{"class":409},[68,3266,2641],{"class":342},[68,3268,3179],{"class":346},[68,3270,180],{"class":342},[68,3272,3184],{"class":346},[68,3274,1869],{"class":342},[68,3276,1617],{"class":331},[68,3278,1620],{"class":342},[68,3280,3281,3283,3285,3287,3289,3291],{"class":70,"line":126},[68,3282,376],{"class":331},[68,3284,3197],{"class":353},[68,3286,382],{"class":331},[68,3288,2551],{"class":331},[68,3290,3204],{"class":338},[68,3292,1602],{"class":342},[68,3294,3295,3297,3300,3302,3304,3306,3308,3311,3313,3315,3317,3319],{"class":70,"line":132},[68,3296,376],{"class":331},[68,3298,3299],{"class":338}," handler",[68,3301,382],{"class":331},[68,3303,2657],{"class":342},[68,3305,3223],{"class":346},[68,3307,350],{"class":331},[68,3309,3310],{"class":338}," Buffer",[68,3312,1869],{"class":342},[68,3314,1617],{"class":331},[68,3316,3230],{"class":342},[68,3318,3233],{"class":338},[68,3320,3321],{"class":342},"(chunk)\n",[68,3323,3324],{"class":70,"line":2135},[68,3325,123],{"emptyLinePlaceholder":122},[68,3327,3328,3330,3332,3334,3336],{"class":70,"line":2141},[68,3329,3211],{"class":342},[68,3331,2595],{"class":338},[68,3333,343],{"class":342},[68,3335,3218],{"class":409},[68,3337,3338],{"class":342},", handler)\n",[68,3340,3341,3344,3346,3348,3351,3353,3355,3357,3360,3362,3364],{"class":70,"line":2437},[68,3342,3343],{"class":342}," req.",[68,3345,2595],{"class":338},[68,3347,343],{"class":342},[68,3349,3350],{"class":409},"'close'",[68,3352,3101],{"class":342},[68,3354,1617],{"class":331},[68,3356,3211],{"class":342},[68,3358,3359],{"class":338},"off",[68,3361,343],{"class":342},[68,3363,3218],{"class":409},[68,3365,3366],{"class":342},", handler))\n",[68,3368,3369],{"class":70,"line":2442},[68,3370,2789],{"class":342},[20,3372,3373],{},[55,3374,3375],{},"Growing caches without eviction:",[59,3377,3379],{"className":316,"code":3378,"language":318,"meta":64,"style":64},"// BAD: Cache grows forever\nconst cache = new Map()\nfunction getCached(key: string) {\n if (!cache.has(key)) {\n cache.set(key, expensiveOperation(key))\n }\n return cache.get(key)\n}\n\n// GOOD: Use LRU cache with size limit\nimport LRU from 'lru-cache'\nconst cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 })\n",[37,3380,3381,3386,3402,3420,3438,3455,3459,3470,3474,3478,3483,3495],{"__ignoreMap":64},[68,3382,3383],{"class":70,"line":71},[68,3384,3385],{"class":325},"// BAD: Cache grows forever\n",[68,3387,3388,3390,3393,3395,3397,3400],{"class":70,"line":77},[68,3389,1991],{"class":331},[68,3391,3392],{"class":353}," cache",[68,3394,382],{"class":331},[68,3396,2551],{"class":331},[68,3398,3399],{"class":338}," Map",[68,3401,1602],{"class":342},[68,3403,3404,3406,3409,3411,3414,3416,3418],{"class":70,"line":83},[68,3405,2082],{"class":331},[68,3407,3408],{"class":338}," getCached",[68,3410,343],{"class":342},[68,3412,3413],{"class":346},"key",[68,3415,350],{"class":331},[68,3417,354],{"class":353},[68,3419,413],{"class":342},[68,3421,3422,3424,3426,3429,3432,3435],{"class":70,"line":89},[68,3423,400],{"class":331},[68,3425,2657],{"class":342},[68,3427,3428],{"class":331},"!",[68,3430,3431],{"class":342},"cache.",[68,3433,3434],{"class":338},"has",[68,3436,3437],{"class":342},"(key)) {\n",[68,3439,3440,3443,3446,3449,3452],{"class":70,"line":95},[68,3441,3442],{"class":342}," cache.",[68,3444,3445],{"class":338},"set",[68,3447,3448],{"class":342},"(key, ",[68,3450,3451],{"class":338},"expensiveOperation",[68,3453,3454],{"class":342},"(key))\n",[68,3456,3457],{"class":70,"line":101},[68,3458,429],{"class":342},[68,3460,3461,3463,3465,3467],{"class":70,"line":107},[68,3462,418],{"class":331},[68,3464,3442],{"class":342},[68,3466,3169],{"class":338},[68,3468,3469],{"class":342},"(key)\n",[68,3471,3472],{"class":70,"line":113},[68,3473,447],{"class":342},[68,3475,3476],{"class":70,"line":119},[68,3477,123],{"emptyLinePlaceholder":122},[68,3479,3480],{"class":70,"line":126},[68,3481,3482],{"class":325},"// GOOD: Use LRU cache with size limit\n",[68,3484,3485,3487,3490,3492],{"class":70,"line":132},[68,3486,2037],{"class":331},[68,3488,3489],{"class":342}," LRU ",[68,3491,2043],{"class":331},[68,3493,3494],{"class":409}," 'lru-cache'\n",[68,3496,3497,3499,3501,3503,3505,3508,3511,3513,3516,3518,3521,3524,3526,3529],{"class":70,"line":2135},[68,3498,1991],{"class":331},[68,3500,3392],{"class":353},[68,3502,382],{"class":331},[68,3504,2551],{"class":331},[68,3506,3507],{"class":338}," LRU",[68,3509,3510],{"class":342},"({ max: ",[68,3512,1714],{"class":353},[68,3514,3515],{"class":342},", ttl: ",[68,3517,1714],{"class":353},[68,3519,3520],{"class":331}," *",[68,3522,3523],{"class":353}," 60",[68,3525,3520],{"class":331},[68,3527,3528],{"class":353}," 5",[68,3530,1859],{"class":342},[20,3532,3533],{},[55,3534,3535],{},"Closures capturing large objects:",[59,3537,3539],{"className":316,"code":3538,"language":318,"meta":64,"style":64},"// BAD: The closure captures the entire largeData array\nasync function processLargeData(largeData: Record\u003Cstring, unknown>[]) {\n const results = largeData.map(item => ({\n ...item,\n processed: true,\n }))\n\n // If this promise stays in memory, largeData does too\n return longRunningOperation().then(() => results)\n}\n\n// GOOD: Process and release\nasync function processLargeData(largeData: Record\u003Cstring, unknown>[]) {\n const ids = largeData.map(item => item.id) // Extract only what you need\n largeData = [] as any // Release the original\n\n await longRunningOperation()\n return ids\n}\n",[37,3540,3541,3546,3578,3604,3612,3622,3627,3631,3636,3655,3659,3663,3668,3694,3719,3738,3742,3750,3757],{"__ignoreMap":64},[68,3542,3543],{"class":70,"line":71},[68,3544,3545],{"class":325},"// BAD: The closure captures the entire largeData array\n",[68,3547,3548,3550,3552,3555,3557,3560,3562,3565,3567,3570,3572,3575],{"class":70,"line":77},[68,3549,332],{"class":331},[68,3551,335],{"class":331},[68,3553,3554],{"class":338}," processLargeData",[68,3556,343],{"class":342},[68,3558,3559],{"class":346},"largeData",[68,3561,350],{"class":331},[68,3563,3564],{"class":338}," Record",[68,3566,365],{"class":342},[68,3568,3569],{"class":353},"string",[68,3571,180],{"class":342},[68,3573,3574],{"class":353},"unknown",[68,3576,3577],{"class":342},">[]) {\n",[68,3579,3580,3582,3585,3587,3590,3593,3595,3598,3601],{"class":70,"line":83},[68,3581,376],{"class":331},[68,3583,3584],{"class":353}," results",[68,3586,382],{"class":331},[68,3588,3589],{"class":342}," largeData.",[68,3591,3592],{"class":338},"map",[68,3594,343],{"class":342},[68,3596,3597],{"class":346},"item",[68,3599,3600],{"class":331}," =>",[68,3602,3603],{"class":342}," ({\n",[68,3605,3606,3609],{"class":70,"line":89},[68,3607,3608],{"class":331}," ...",[68,3610,3611],{"class":342},"item,\n",[68,3613,3614,3617,3620],{"class":70,"line":95},[68,3615,3616],{"class":342}," processed: ",[68,3618,3619],{"class":353},"true",[68,3621,2751],{"class":342},[68,3623,3624],{"class":70,"line":101},[68,3625,3626],{"class":342}," }))\n",[68,3628,3629],{"class":70,"line":107},[68,3630,123],{"emptyLinePlaceholder":122},[68,3632,3633],{"class":70,"line":113},[68,3634,3635],{"class":325}," // If this promise stays in memory, largeData does too\n",[68,3637,3638,3640,3643,3645,3648,3650,3652],{"class":70,"line":119},[68,3639,418],{"class":331},[68,3641,3642],{"class":338}," longRunningOperation",[68,3644,2773],{"class":342},[68,3646,3647],{"class":338},"then",[68,3649,1614],{"class":342},[68,3651,1617],{"class":331},[68,3653,3654],{"class":342}," results)\n",[68,3656,3657],{"class":70,"line":126},[68,3658,447],{"class":342},[68,3660,3661],{"class":70,"line":132},[68,3662,123],{"emptyLinePlaceholder":122},[68,3664,3665],{"class":70,"line":2135},[68,3666,3667],{"class":325},"// GOOD: Process and release\n",[68,3669,3670,3672,3674,3676,3678,3680,3682,3684,3686,3688,3690,3692],{"class":70,"line":2141},[68,3671,332],{"class":331},[68,3673,335],{"class":331},[68,3675,3554],{"class":338},[68,3677,343],{"class":342},[68,3679,3559],{"class":346},[68,3681,350],{"class":331},[68,3683,3564],{"class":338},[68,3685,365],{"class":342},[68,3687,3569],{"class":353},[68,3689,180],{"class":342},[68,3691,3574],{"class":353},[68,3693,3577],{"class":342},[68,3695,3696,3698,3701,3703,3705,3707,3709,3711,3713,3716],{"class":70,"line":2437},[68,3697,376],{"class":331},[68,3699,3700],{"class":353}," ids",[68,3702,382],{"class":331},[68,3704,3589],{"class":342},[68,3706,3592],{"class":338},[68,3708,343],{"class":342},[68,3710,3597],{"class":346},[68,3712,3600],{"class":331},[68,3714,3715],{"class":342}," item.id) ",[68,3717,3718],{"class":325},"// Extract only what you need\n",[68,3720,3721,3724,3726,3729,3732,3735],{"class":70,"line":2442},[68,3722,3723],{"class":342}," largeData ",[68,3725,1593],{"class":331},[68,3727,3728],{"class":342}," [] ",[68,3730,3731],{"class":331},"as",[68,3733,3734],{"class":353}," any",[68,3736,3737],{"class":325}," // Release the original\n",[68,3739,3740],{"class":70,"line":2447},[68,3741,123],{"emptyLinePlaceholder":122},[68,3743,3744,3746,3748],{"class":70,"line":2652},[68,3745,385],{"class":331},[68,3747,3642],{"class":338},[68,3749,1602],{"class":342},[68,3751,3752,3754],{"class":70,"line":2687},[68,3753,418],{"class":331},[68,3755,3756],{"class":342}," ids\n",[68,3758,3759],{"class":70,"line":2692},[68,3760,447],{"class":342},[20,3762,3763],{},"To find leaks, take heap snapshots before and after suspected leak scenarios:",[59,3765,3767],{"className":316,"code":3766,"language":318,"meta":64,"style":64},"import v8 from 'v8'\nimport fs from 'fs'\n\n// Take a snapshot\nconst snapshot = v8.writeHeapSnapshot()\nconsole.log('Heap snapshot written to:', snapshot)\n",[37,3768,3769,3781,3792,3796,3801,3818],{"__ignoreMap":64},[68,3770,3771,3773,3776,3778],{"class":70,"line":71},[68,3772,2037],{"class":331},[68,3774,3775],{"class":342}," v8 ",[68,3777,2043],{"class":331},[68,3779,3780],{"class":409}," 'v8'\n",[68,3782,3783,3785,3788,3790],{"class":70,"line":77},[68,3784,2037],{"class":331},[68,3786,3787],{"class":342}," fs ",[68,3789,2043],{"class":331},[68,3791,2046],{"class":409},[68,3793,3794],{"class":70,"line":83},[68,3795,123],{"emptyLinePlaceholder":122},[68,3797,3798],{"class":70,"line":89},[68,3799,3800],{"class":325},"// Take a snapshot\n",[68,3802,3803,3805,3808,3810,3813,3816],{"class":70,"line":95},[68,3804,1991],{"class":331},[68,3806,3807],{"class":353}," snapshot",[68,3809,382],{"class":331},[68,3811,3812],{"class":342}," v8.",[68,3814,3815],{"class":338},"writeHeapSnapshot",[68,3817,1602],{"class":342},[68,3819,3820,3823,3825,3827,3830],{"class":70,"line":101},[68,3821,3822],{"class":342},"console.",[68,3824,1786],{"class":338},[68,3826,343],{"class":342},[68,3828,3829],{"class":409},"'Heap snapshot written to:'",[68,3831,3832],{"class":342},", snapshot)\n",[20,3834,3835],{},"Load snapshots in Chrome DevTools Memory tab to find retained objects.",[15,3837,3839],{"id":3838},"connection-pool-tuning","Connection Pool Tuning",[20,3841,3842],{},"Database connection pools are a common performance bottleneck. The default pool sizes are conservative:",[59,3844,3846],{"className":316,"code":3845,"language":318,"meta":64,"style":64},"// Prisma\nconst prisma = new PrismaClient({\n datasources: {\n db: {\n url: process.env.DATABASE_URL,\n },\n },\n // connection_limit in the URL: postgresql://...?connection_limit=20\n})\n\n// Drizzle with postgres.js\nimport postgres from 'postgres'\n\nConst sql = postgres(process.env.DATABASE_URL!, {\n max: 20, // Maximum pool size\n idle_timeout: 30, // Close idle connections after 30 seconds\n connect_timeout: 10,\n})\n",[37,3847,3848,3853,3869,3874,3879,3889,3894,3898,3903,3907,3911,3916,3928,3932,3951,3964,3977,3987],{"__ignoreMap":64},[68,3849,3850],{"class":70,"line":71},[68,3851,3852],{"class":325},"// Prisma\n",[68,3854,3855,3857,3860,3862,3864,3867],{"class":70,"line":77},[68,3856,1991],{"class":331},[68,3858,3859],{"class":353}," prisma",[68,3861,382],{"class":331},[68,3863,2551],{"class":331},[68,3865,3866],{"class":338}," PrismaClient",[68,3868,1789],{"class":342},[68,3870,3871],{"class":70,"line":83},[68,3872,3873],{"class":342}," datasources: {\n",[68,3875,3876],{"class":70,"line":89},[68,3877,3878],{"class":342}," db: {\n",[68,3880,3881,3884,3887],{"class":70,"line":95},[68,3882,3883],{"class":342}," url: process.env.",[68,3885,3886],{"class":353},"DATABASE_URL",[68,3888,2751],{"class":342},[68,3890,3891],{"class":70,"line":101},[68,3892,3893],{"class":342}," },\n",[68,3895,3896],{"class":70,"line":107},[68,3897,3893],{"class":342},[68,3899,3900],{"class":70,"line":113},[68,3901,3902],{"class":325}," // connection_limit in the URL: postgresql://...?connection_limit=20\n",[68,3904,3905],{"class":70,"line":119},[68,3906,2789],{"class":342},[68,3908,3909],{"class":70,"line":126},[68,3910,123],{"emptyLinePlaceholder":122},[68,3912,3913],{"class":70,"line":132},[68,3914,3915],{"class":325},"// Drizzle with postgres.js\n",[68,3917,3918,3920,3923,3925],{"class":70,"line":2135},[68,3919,2037],{"class":331},[68,3921,3922],{"class":342}," postgres ",[68,3924,2043],{"class":331},[68,3926,3927],{"class":409}," 'postgres'\n",[68,3929,3930],{"class":70,"line":2141},[68,3931,123],{"emptyLinePlaceholder":122},[68,3933,3934,3937,3939,3942,3945,3947,3949],{"class":70,"line":2437},[68,3935,3936],{"class":342},"Const sql ",[68,3938,1593],{"class":331},[68,3940,3941],{"class":338}," postgres",[68,3943,3944],{"class":342},"(process.env.",[68,3946,3886],{"class":353},[68,3948,3428],{"class":331},[68,3950,2562],{"class":342},[68,3952,3953,3956,3959,3961],{"class":70,"line":2442},[68,3954,3955],{"class":342}," max: ",[68,3957,3958],{"class":353},"20",[68,3960,180],{"class":342},[68,3962,3963],{"class":325},"// Maximum pool size\n",[68,3965,3966,3969,3972,3974],{"class":70,"line":2447},[68,3967,3968],{"class":342}," idle_timeout: ",[68,3970,3971],{"class":353},"30",[68,3973,180],{"class":342},[68,3975,3976],{"class":325},"// Close idle connections after 30 seconds\n",[68,3978,3979,3982,3985],{"class":70,"line":2652},[68,3980,3981],{"class":342}," connect_timeout: ",[68,3983,3984],{"class":353},"10",[68,3986,2751],{"class":342},[68,3988,3989],{"class":70,"line":2687},[68,3990,2789],{"class":342},[20,3992,3993,3994,3997],{},"The right pool size is not \"as large as possible.\" Too many connections exhaust the database's connection limit and increase context switching overhead. For PostgreSQL, a good starting point is ",[37,3995,3996],{},"2 * CPU_cores + 1"," connections per application instance.",[20,3999,4000],{},"Node.js performance is almost always about understanding what blocks the event loop, what leaks memory, and how efficiently you use database and external resources. Measure first, optimize what the data shows, and test under realistic load.",[509,4002],{},[20,4004,4005,4006,51],{},"Dealing with performance issues in a Node.js application, or want help setting up monitoring to catch problems before they hit production? Book a call: ",[502,4007,817],{"href":504,"rel":4008},[506],[509,4010],{},[15,4012,514],{"id":513},[516,4014,4015,4021,4027,4033],{},[519,4016,4017],{},[502,4018,4020],{"href":4019},"/blog/api-performance-optimization","API Performance Optimization: Making Your Endpoints Fast at Scale",[519,4022,4023],{},[502,4024,4026],{"href":4025},"/blog/background-jobs-nodejs","Background Jobs in Node.js: Queues, Workers, and Failure Recovery",[519,4028,4029],{},[502,4030,4032],{"href":4031},"/blog/core-web-vitals-optimization","Core Web Vitals Optimization: A Developer's Complete Guide",[519,4034,4035],{},[502,4036,4038],{"href":4037},"/blog/typescript-backend-development","TypeScript for Backend Development: Patterns I Use on Every Project",[544,4040,4041],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}",{"title":64,"searchDepth":83,"depth":83,"links":4043},[4044,4045,4046,4047,4048,4049,4050,4051],{"id":1567,"depth":77,"text":1568},{"id":1881,"depth":77,"text":1882},{"id":1967,"depth":77,"text":1968},{"id":2278,"depth":77,"text":2279},{"id":2831,"depth":77,"text":2832},{"id":3142,"depth":77,"text":3143},{"id":3838,"depth":77,"text":3839},{"id":513,"depth":77,"text":514},"Real Node.js performance optimization techniques — event loop monitoring, memory leak detection, clustering, worker threads, profiling, and the patterns that actually move the needle.",[4054,4055],"Node.js performance","Node.js optimization",{},"/blog/nodejs-performance-optimization",{"title":1555,"description":4052},"blog/nodejs-performance-optimization",[4061,4062,4063],"Node.js","Performance","Backend","79oJeXtWmtuBuCyJ6l9r78aWmj6SlCHG1M2nQVn8yJA",{"id":4066,"title":4067,"author":4068,"body":4069,"category":557,"date":558,"description":4429,"extension":560,"featured":561,"image":562,"keywords":4430,"meta":4433,"navigation":122,"path":4434,"readTime":107,"seo":4435,"stem":4436,"tags":4437,"__hash__":4441},"blog/blog/nuxt-4-features-guide.md","Nuxt 4: What Changed and Why It Matters",{"name":9,"bio":10},{"type":12,"value":4070,"toc":4418},[4071,4074,4077,4081,4088,4093,4101,4111,4117,4121,4131,4137,4144,4223,4226,4230,4233,4244,4255,4296,4300,4303,4306,4309,4313,4316,4319,4323,4326,4335,4338,4344,4348,4351,4358,4361,4365,4368,4371,4374,4377,4379,4385,4387,4389,4415],[20,4072,4073],{},"When Nuxt 4 landed, my first reaction was relief. Not because Nuxt 3 was bad — it was excellent — but because several things that required workarounds or careful configuration just got fixed. After shipping half a dozen production Nuxt applications over the past few years, I had a running list of friction points. Nuxt 4 addressed most of them.",[20,4075,4076],{},"This is not a changelog recap. You can read the official docs for that. This is my practical take on what actually changed, what it means for day-to-day development, and where I think the framework is heading.",[15,4078,4080],{"id":4079},"the-app-directory-shift","The App Directory Shift",[20,4082,4083,4084,4087],{},"The most visible change in Nuxt 4 is the new ",[37,4085,4086],{},"app/"," directory structure. In Nuxt 3, your pages, components, and composables lived at the root of the project alongside configuration files. That works fine for small apps, but it creates noise as projects grow. You end up with a root directory full of both configuration and application code.",[20,4089,4090,4091,350],{},"Nuxt 4 moves application code into ",[37,4092,4086],{},[59,4094,4099],{"className":4095,"code":4097,"language":4098},[4096],"language-text","app/\n pages/\n components/\n composables/\n layouts/\nnuxt.config.ts\nserver/\n","text",[37,4100,4097],{"__ignoreMap":64},[20,4102,4103,4104,4106,4107,4110],{},"This is a cleaner mental model. Configuration at the root, application code in ",[37,4105,4086],{},", server code in ",[37,4108,4109],{},"server/",". The separation makes it immediately obvious where things belong, and it mirrors how most mature backend frameworks have organized projects for years.",[20,4112,4113,4114,4116],{},"The migration is straightforward — move your directories into ",[37,4115,4086],{}," and update any explicit imports. Most projects can do this in under an hour. I have run the migration on three codebases now and it has been painless each time.",[15,4118,4120],{"id":4119},"data-fetching-gets-smarter","Data Fetching Gets Smarter",[20,4122,4123,4124,180,4127,4130],{},"Data fetching was always one of Nuxt's strong suits, but it had sharp edges. The relationship between ",[37,4125,4126],{},"useAsyncData",[37,4128,4129],{},"useFetch",", and when each ran (server vs. Client vs. Both) caused confusion, especially when composables were nested.",[20,4132,4133,4134,4136],{},"Nuxt 4 introduces clearer lifecycle semantics. The deduplication logic is improved — if you call the same ",[37,4135,4129],{}," key in multiple components during a single request, the fetch only happens once and the result is shared. This was technically possible before but required deliberate key management. Now it's the default behavior.",[20,4138,4139,4140,4143],{},"The ",[37,4141,4142],{},"getCachedData"," option is more prominently documented and the caching layer integrates better with Nuxt's payload hydration system. In practice, this means fewer double-fetches on page transitions and better performance out of the box on data-heavy pages.",[59,4145,4147],{"className":316,"code":4146,"language":318,"meta":64,"style":64},"const { data: posts } = await useFetch('/api/posts', {\n key: 'posts-list',\n getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key] ?? null,\n})\n",[37,4148,4149,4179,4189,4219],{"__ignoreMap":64},[68,4150,4151,4153,4155,4158,4160,4163,4165,4167,4169,4172,4174,4177],{"class":70,"line":71},[68,4152,1991],{"class":331},[68,4154,1748],{"class":342},[68,4156,4157],{"class":346},"data",[68,4159,938],{"class":342},[68,4161,4162],{"class":353},"posts",[68,4164,1769],{"class":342},[68,4166,1593],{"class":331},[68,4168,385],{"class":331},[68,4170,4171],{"class":338}," useFetch",[68,4173,343],{"class":342},[68,4175,4176],{"class":409},"'/api/posts'",[68,4178,2562],{"class":342},[68,4180,4181,4184,4187],{"class":70,"line":77},[68,4182,4183],{"class":342}," key: ",[68,4185,4186],{"class":409},"'posts-list'",[68,4188,2751],{"class":342},[68,4190,4191,4194,4197,4199,4201,4204,4206,4208,4211,4214,4217],{"class":70,"line":83},[68,4192,4193],{"class":338}," getCachedData",[68,4195,4196],{"class":342},": (",[68,4198,3413],{"class":346},[68,4200,180],{"class":342},[68,4202,4203],{"class":346},"nuxtApp",[68,4205,1869],{"class":342},[68,4207,1617],{"class":331},[68,4209,4210],{"class":342}," nuxtApp.payload.data[key] ",[68,4212,4213],{"class":331},"??",[68,4215,4216],{"class":353}," null",[68,4218,2751],{"class":342},[68,4220,4221],{"class":70,"line":89},[68,4222,2789],{"class":342},[20,4224,4225],{},"That pattern prevents a round trip on navigation. In Nuxt 3 you had to be more deliberate about setting this up. In Nuxt 4 it integrates more naturally.",[15,4227,4229],{"id":4228},"improved-typescript-experience","Improved TypeScript Experience",[20,4231,4232],{},"TypeScript support in Nuxt 3 was good. Nuxt 4 makes it genuinely excellent. Auto-imported composables and components now produce accurate type definitions without requiring manual augmentation files in most cases. The Nuxt DevTools integration also surfaces type errors in a more actionable way.",[20,4234,4235,4236,4239,4240,4243],{},"The template type checking story improved significantly. Running ",[37,4237,4238],{},"nuxi typecheck"," now catches more errors in ",[37,4241,4242],{},".vue"," templates, including props passed to auto-imported components. This catches a whole class of bugs that previously only appeared at runtime.",[20,4245,4246,4247,4250,4251,4254],{},"One specific improvement that matters in production codebases: the typed router. Route params and query strings are now inferred from your ",[37,4248,4249],{},"pages/"," directory structure. If you rename a page, TypeScript will flag all the places calling ",[37,4252,4253],{},"navigateTo"," with the old path. That is a meaningful safety net in large applications.",[59,4256,4258],{"className":316,"code":4257,"language":318,"meta":64,"style":64},"// Nuxt 4 infers route params from pages/users/[id].vue\nconst route = useRoute('users-id')\nconsole.log(route.params.id) // typed as string\n",[37,4259,4260,4265,4284],{"__ignoreMap":64},[68,4261,4262],{"class":70,"line":71},[68,4263,4264],{"class":325},"// Nuxt 4 infers route params from pages/users/[id].vue\n",[68,4266,4267,4269,4272,4274,4277,4279,4282],{"class":70,"line":77},[68,4268,1991],{"class":331},[68,4270,4271],{"class":353}," route",[68,4273,382],{"class":331},[68,4275,4276],{"class":338}," useRoute",[68,4278,343],{"class":342},[68,4280,4281],{"class":409},"'users-id'",[68,4283,1702],{"class":342},[68,4285,4286,4288,4290,4293],{"class":70,"line":83},[68,4287,3822],{"class":342},[68,4289,1786],{"class":338},[68,4291,4292],{"class":342},"(route.params.id) ",[68,4294,4295],{"class":325},"// typed as string\n",[15,4297,4299],{"id":4298},"rendering-performance","Rendering Performance",[20,4301,4302],{},"Nuxt 4 ships with improvements to the island component system introduced in Nuxt 3. Server components are more stable and the boundary between hydrated and non-hydrated content is better defined.",[20,4304,4305],{},"The key practical benefit is that you can now more aggressively defer hydration on components that do not need interactivity. Marketing pages, blog posts, documentation — large portions of these pages do not need JavaScript on the client at all. With server components and lazy hydration, you can ship significantly less JavaScript without losing any functionality.",[20,4307,4308],{},"I rebuilt a client's content site with these patterns and cut the JavaScript payload by about 60%. The Lighthouse scores went from good to excellent. More importantly, the Core Web Vitals improved in a way that correlated with actual organic traffic improvements over the following weeks.",[15,4310,4312],{"id":4311},"build-tooling-and-vite-6","Build Tooling and Vite 6",[20,4314,4315],{},"Nuxt 4 moves to Vite 6, which brings a faster development server start time and improved HMR stability. In large projects with hundreds of components, the difference in cold start time is noticeable. Projects that took 8-10 seconds to start now come up in 3-4 seconds on the same hardware.",[20,4317,4318],{},"The Nitro server runtime was also updated, which affects deployments. The edge runtime support is more mature — deploying to Cloudflare Workers or Netlify Edge now works without the manual tweaks that were sometimes needed in Nuxt 3.",[15,4320,4322],{"id":4321},"breaking-changes-worth-knowing","Breaking Changes Worth Knowing",[20,4324,4325],{},"There are a few things that will bite you if you are not paying attention.",[20,4327,4328,4329,4331,4332,4334],{},"The default fetch behavior changed. In Nuxt 3, ",[37,4330,4129],{}," ran on both server and client by default. In Nuxt 4 with the new app directory, you need to be more explicit about certain hydration scenarios. Check your ",[37,4333,4126],{}," calls if you see missing data after client-side navigation.",[20,4336,4337],{},"Some module APIs changed. If you maintain a Nuxt module or use community modules heavily, check their compatibility with Nuxt 4 before upgrading. Most popular modules updated quickly, but there will always be stragglers.",[20,4339,4139,4340,4343],{},[37,4341,4342],{},"useState"," composable behavior was clarified around server/client boundaries. If you were relying on undocumented behavior for cross-component state, audit those patterns before migrating.",[15,4345,4347],{"id":4346},"should-you-migrate-now","Should You Migrate Now?",[20,4349,4350],{},"For new projects, start with Nuxt 4. There is no reason to start on Nuxt 3 unless you have specific module compatibility requirements.",[20,4352,4353,4354,4357],{},"For existing Nuxt 3 projects, the migration is worth doing on your timeline but not urgent. The improvements are real but not critical for running production applications. I would plan the migration as a dedicated sprint rather than doing it alongside feature work. Run the compatibility mode (",[37,4355,4356],{},"compatibilityVersion: 3"," in your config) first — this lets you adopt Nuxt 4 gradually rather than all at once.",[20,4359,4360],{},"The Nuxt team has done a good job on the migration guide. Follow it in order, run your test suite at each step, and do not skip the compatibility mode phase.",[15,4362,4364],{"id":4363},"where-nuxt-is-heading","Where Nuxt Is Heading",[20,4366,4367],{},"The direction is clear: Nuxt is positioning itself as the full-stack Vue framework. The combination of server components, server routes, and edge deployment support means you can build entire applications — frontend and backend — in a single codebase, deployed to the edge, with excellent performance and developer experience.",[20,4369,4370],{},"That is a compelling proposition, and it is increasingly competitive with Next.js in the React ecosystem. The tooling quality has caught up, the ecosystem has matured, and the framework opinions are well-considered.",[20,4372,4373],{},"I have been building with Nuxt since version 2, and Nuxt 4 feels like the version where everything came together. The rough edges are gone, the performance story is strong, and the TypeScript experience is no longer something you have to fight.",[20,4375,4376],{},"If you are evaluating frameworks for a new project in 2026, Nuxt deserves serious consideration. If you are already on Nuxt 3, the upgrade path is clear and the benefits are real.",[509,4378],{},[20,4380,4381,4382,51],{},"If you are building a Nuxt application and want a second set of eyes on your architecture decisions or deployment strategy, I am happy to talk through it. Book a call at ",[502,4383,817],{"href":504,"rel":4384},[506],[509,4386],{},[15,4388,514],{"id":513},[516,4390,4391,4397,4403,4409],{},[519,4392,4393],{},[502,4394,4396],{"href":4395},"/blog/nuxt-authentication-guide","Authentication in Nuxt: Patterns That Actually Scale",[519,4398,4399],{},[502,4400,4402],{"href":4401},"/blog/nuxt-content-module-guide","Building a Blog With Nuxt Content: The Complete Guide",[519,4404,4405],{},[502,4406,4408],{"href":4407},"/blog/nuxt-pwa-guide","Building a PWA With Nuxt: Offline Support and App-Like Features",[519,4410,4411],{},[502,4412,4414],{"href":4413},"/blog/nuxt-cloudflare-deployment","Deploying Nuxt to Cloudflare Pages: The Complete Walkthrough",[544,4416,4417],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":64,"searchDepth":83,"depth":83,"links":4419},[4420,4421,4422,4423,4424,4425,4426,4427,4428],{"id":4079,"depth":77,"text":4080},{"id":4119,"depth":77,"text":4120},{"id":4228,"depth":77,"text":4229},{"id":4298,"depth":77,"text":4299},{"id":4311,"depth":77,"text":4312},{"id":4321,"depth":77,"text":4322},{"id":4346,"depth":77,"text":4347},{"id":4363,"depth":77,"text":4364},{"id":513,"depth":77,"text":514},"A hands-on breakdown of Nuxt 4's biggest changes — from the new app directory to improved data fetching — and what they mean for your projects.",[4431,4432],"Nuxt 4","Nuxt features",{},"/blog/nuxt-4-features-guide",{"title":4067,"description":4429},"blog/nuxt-4-features-guide",[4438,4439,4440],"Nuxt","Vue","Web Development","lW3Me0DU9GRYuqe9-1rCr52XquSK78bXwMqAq2rhhPU",{"id":4443,"title":4444,"author":4445,"body":4446,"category":557,"date":558,"description":7229,"extension":560,"featured":561,"image":562,"keywords":7230,"meta":7233,"navigation":122,"path":7234,"readTime":107,"seo":7235,"stem":7236,"tags":7237,"__hash__":7239},"blog/blog/nuxt-api-routes-nitro.md","Nuxt API Routes With Nitro: Building Your Backend in the Same Repo",{"name":9,"bio":10},{"type":12,"value":4447,"toc":7216},[4448,4454,4457,4461,4464,4470,4485,4489,4827,4830,4874,4878,5050,5054,5057,5365,5368,5543,5546,5598,5602,5609,5740,5908,5912,5915,6090,6093,6097,6100,6314,6317,6321,6324,6555,6677,6681,6684,6796,6802,6909,6913,6920,7178,7181,7183,7189,7191,7193,7213],[20,4449,4450,4451,4453],{},"One of the most underappreciated features in Nuxt is the Nitro server — the universal JavaScript server runtime that powers ",[37,4452,4109],{}," directory routes. With Nitro, you can build a complete backend API alongside your frontend in a single repository, with shared TypeScript types, shared utilities, and a single deployment artifact.",[20,4455,4456],{},"This is not a toy pattern. I have shipped full-stack Nuxt applications handling thousands of requests per day with Nitro handling all the backend logic. The developer experience is excellent and the performance is solid.",[15,4458,4460],{"id":4459},"the-server-directory-structure","The server/ Directory Structure",[20,4462,4463],{},"Nitro uses file-based routing that mirrors your API structure:",[59,4465,4468],{"className":4466,"code":4467,"language":4098},[4096],"server/\n api/\n users/\n index.get.ts → GET /api/users\n index.post.ts → POST /api/users\n [id].get.ts → GET /api/users/:id\n [id].put.ts → PUT /api/users/:id\n [id].delete.ts → DELETE /api/users/:id\n middleware/\n auth.ts → Runs on every request\n cors.ts\n utils/\n prisma.ts → Shared utilities\n auth.ts\n routes/\n health.get.ts → GET /health (non-api routes)\n",[37,4469,4467],{"__ignoreMap":64},[20,4471,4472,4473,4476,4477,4480,4481,4484],{},"The HTTP method is part of the filename. ",[37,4474,4475],{},"users.get.ts"," handles GET, ",[37,4478,4479],{},"users.post.ts"," handles POST. You can use ",[37,4482,4483],{},"index.ts"," (handles all methods) for cases where you want to handle routing manually.",[15,4486,4488],{"id":4487},"your-first-api-route","Your First API Route",[59,4490,4492],{"className":316,"code":4491,"language":318,"meta":64,"style":64},"// server/api/users/index.get.ts\nimport { prisma } from '~/server/utils/prisma'\n\nExport default defineEventHandler(async (event) => {\n const query = getQuery(event)\n const page = Number(query.page ?? 1)\n const limit = Number(query.limit ?? 20)\n const skip = (page - 1) * limit\n\n const [users, total] = await Promise.all([\n prisma.user.findMany({\n skip,\n take: limit,\n select: {\n id: true,\n name: true,\n email: true,\n createdAt: true,\n },\n orderBy: { createdAt: 'desc' },\n }),\n prisma.user.count(),\n ])\n\n return {\n data: users,\n pagination: {\n page,\n limit,\n total,\n pages: Math.ceil(total / limit),\n },\n }\n})\n",[37,4493,4494,4499,4511,4515,4541,4556,4577,4598,4621,4625,4657,4667,4672,4677,4682,4691,4700,4709,4718,4722,4732,4737,4747,4753,4758,4765,4771,4777,4783,4789,4795,4812,4817,4822],{"__ignoreMap":64},[68,4495,4496],{"class":70,"line":71},[68,4497,4498],{"class":325},"// server/api/users/index.get.ts\n",[68,4500,4501,4503,4506,4508],{"class":70,"line":77},[68,4502,2037],{"class":331},[68,4504,4505],{"class":342}," { prisma } ",[68,4507,2043],{"class":331},[68,4509,4510],{"class":409}," '~/server/utils/prisma'\n",[68,4512,4513],{"class":70,"line":83},[68,4514,123],{"emptyLinePlaceholder":122},[68,4516,4517,4520,4523,4526,4528,4530,4532,4535,4537,4539],{"class":70,"line":89},[68,4518,4519],{"class":342},"Export ",[68,4521,4522],{"class":331},"default",[68,4524,4525],{"class":338}," defineEventHandler",[68,4527,343],{"class":342},[68,4529,332],{"class":331},[68,4531,2657],{"class":342},[68,4533,4534],{"class":346},"event",[68,4536,1869],{"class":342},[68,4538,1617],{"class":331},[68,4540,1620],{"class":342},[68,4542,4543,4545,4548,4550,4553],{"class":70,"line":95},[68,4544,376],{"class":331},[68,4546,4547],{"class":353}," query",[68,4549,382],{"class":331},[68,4551,4552],{"class":338}," getQuery",[68,4554,4555],{"class":342},"(event)\n",[68,4557,4558,4560,4563,4565,4568,4571,4573,4575],{"class":70,"line":101},[68,4559,376],{"class":331},[68,4561,4562],{"class":353}," page",[68,4564,382],{"class":331},[68,4566,4567],{"class":338}," Number",[68,4569,4570],{"class":342},"(query.page ",[68,4572,4213],{"class":331},[68,4574,2782],{"class":353},[68,4576,1702],{"class":342},[68,4578,4579,4581,4584,4586,4588,4591,4593,4596],{"class":70,"line":107},[68,4580,376],{"class":331},[68,4582,4583],{"class":353}," limit",[68,4585,382],{"class":331},[68,4587,4567],{"class":338},[68,4589,4590],{"class":342},"(query.limit ",[68,4592,4213],{"class":331},[68,4594,4595],{"class":353}," 20",[68,4597,1702],{"class":342},[68,4599,4600,4602,4605,4607,4610,4612,4614,4616,4618],{"class":70,"line":113},[68,4601,376],{"class":331},[68,4603,4604],{"class":353}," skip",[68,4606,382],{"class":331},[68,4608,4609],{"class":342}," (page ",[68,4611,1639],{"class":331},[68,4613,2782],{"class":353},[68,4615,1869],{"class":342},[68,4617,1924],{"class":331},[68,4619,4620],{"class":342}," limit\n",[68,4622,4623],{"class":70,"line":119},[68,4624,123],{"emptyLinePlaceholder":122},[68,4626,4627,4629,4632,4635,4637,4640,4643,4645,4647,4649,4651,4654],{"class":70,"line":126},[68,4628,376],{"class":331},[68,4630,4631],{"class":342}," [",[68,4633,4634],{"class":353},"users",[68,4636,180],{"class":342},[68,4638,4639],{"class":353},"total",[68,4641,4642],{"class":342},"] ",[68,4644,1593],{"class":331},[68,4646,385],{"class":331},[68,4648,362],{"class":353},[68,4650,51],{"class":342},[68,4652,4653],{"class":338},"all",[68,4655,4656],{"class":342},"([\n",[68,4658,4659,4662,4665],{"class":70,"line":132},[68,4660,4661],{"class":342}," prisma.user.",[68,4663,4664],{"class":338},"findMany",[68,4666,1789],{"class":342},[68,4668,4669],{"class":70,"line":2135},[68,4670,4671],{"class":342}," skip,\n",[68,4673,4674],{"class":70,"line":2141},[68,4675,4676],{"class":342}," take: limit,\n",[68,4678,4679],{"class":70,"line":2437},[68,4680,4681],{"class":342}," select: {\n",[68,4683,4684,4687,4689],{"class":70,"line":2442},[68,4685,4686],{"class":342}," id: ",[68,4688,3619],{"class":353},[68,4690,2751],{"class":342},[68,4692,4693,4696,4698],{"class":70,"line":2447},[68,4694,4695],{"class":342}," name: ",[68,4697,3619],{"class":353},[68,4699,2751],{"class":342},[68,4701,4702,4705,4707],{"class":70,"line":2652},[68,4703,4704],{"class":342}," email: ",[68,4706,3619],{"class":353},[68,4708,2751],{"class":342},[68,4710,4711,4714,4716],{"class":70,"line":2687},[68,4712,4713],{"class":342}," createdAt: ",[68,4715,3619],{"class":353},[68,4717,2751],{"class":342},[68,4719,4720],{"class":70,"line":2692},[68,4721,3893],{"class":342},[68,4723,4724,4727,4730],{"class":70,"line":2697},[68,4725,4726],{"class":342}," orderBy: { createdAt: ",[68,4728,4729],{"class":409},"'desc'",[68,4731,3893],{"class":342},[68,4733,4734],{"class":70,"line":3129},[68,4735,4736],{"class":342}," }),\n",[68,4738,4739,4741,4744],{"class":70,"line":3134},[68,4740,4661],{"class":342},[68,4742,4743],{"class":338},"count",[68,4745,4746],{"class":342},"(),\n",[68,4748,4750],{"class":70,"line":4749},23,[68,4751,4752],{"class":342}," ])\n",[68,4754,4756],{"class":70,"line":4755},24,[68,4757,123],{"emptyLinePlaceholder":122},[68,4759,4761,4763],{"class":70,"line":4760},25,[68,4762,418],{"class":331},[68,4764,1620],{"class":342},[68,4766,4768],{"class":70,"line":4767},26,[68,4769,4770],{"class":342}," data: users,\n",[68,4772,4774],{"class":70,"line":4773},27,[68,4775,4776],{"class":342}," pagination: {\n",[68,4778,4780],{"class":70,"line":4779},28,[68,4781,4782],{"class":342}," page,\n",[68,4784,4786],{"class":70,"line":4785},29,[68,4787,4788],{"class":342}," limit,\n",[68,4790,4792],{"class":70,"line":4791},30,[68,4793,4794],{"class":342}," total,\n",[68,4796,4798,4801,4804,4807,4809],{"class":70,"line":4797},31,[68,4799,4800],{"class":342}," pages: Math.",[68,4802,4803],{"class":338},"ceil",[68,4805,4806],{"class":342},"(total ",[68,4808,1803],{"class":331},[68,4810,4811],{"class":342}," limit),\n",[68,4813,4815],{"class":70,"line":4814},32,[68,4816,3893],{"class":342},[68,4818,4820],{"class":70,"line":4819},33,[68,4821,429],{"class":342},[68,4823,4825],{"class":70,"line":4824},34,[68,4826,2789],{"class":342},[20,4828,4829],{},"Nitro handles JSON serialization automatically. Throw a typed error for error cases:",[59,4831,4833],{"className":316,"code":4832,"language":318,"meta":64,"style":64},"throw createError({\n statusCode: 404,\n statusMessage: 'User not found',\n data: { userId: id },\n})\n",[37,4834,4835,4845,4855,4865,4870],{"__ignoreMap":64},[68,4836,4837,4840,4843],{"class":70,"line":71},[68,4838,4839],{"class":331},"throw",[68,4841,4842],{"class":338}," createError",[68,4844,1789],{"class":342},[68,4846,4847,4850,4853],{"class":70,"line":77},[68,4848,4849],{"class":342}," statusCode: ",[68,4851,4852],{"class":353},"404",[68,4854,2751],{"class":342},[68,4856,4857,4860,4863],{"class":70,"line":83},[68,4858,4859],{"class":342}," statusMessage: ",[68,4861,4862],{"class":409},"'User not found'",[68,4864,2751],{"class":342},[68,4866,4867],{"class":70,"line":89},[68,4868,4869],{"class":342}," data: { userId: id },\n",[68,4871,4872],{"class":70,"line":95},[68,4873,2789],{"class":342},[15,4875,4877],{"id":4876},"reading-request-data","Reading Request Data",[59,4879,4881],{"className":316,"code":4880,"language":318,"meta":64,"style":64},"// server/api/users/index.post.ts\nexport default defineEventHandler(async (event) => {\n // Read and parse JSON body\n const body = await readBody(event)\n\n // Read query parameters\n const query = getQuery(event)\n\n // Read route parameters (from [id].get.ts)\n const params = getRouterParams(event)\n const userId = params.id\n\n // Read specific headers\n const authHeader = getHeader(event, 'authorization')\n\n // Read cookies\n const sessionToken = getCookie(event, 'session')\n})\n",[37,4882,4883,4888,4912,4917,4933,4937,4942,4954,4958,4963,4977,4989,4993,4998,5018,5022,5027,5046],{"__ignoreMap":64},[68,4884,4885],{"class":70,"line":71},[68,4886,4887],{"class":325},"// server/api/users/index.post.ts\n",[68,4889,4890,4893,4896,4898,4900,4902,4904,4906,4908,4910],{"class":70,"line":77},[68,4891,4892],{"class":331},"export",[68,4894,4895],{"class":331}," default",[68,4897,4525],{"class":338},[68,4899,343],{"class":342},[68,4901,332],{"class":331},[68,4903,2657],{"class":342},[68,4905,4534],{"class":346},[68,4907,1869],{"class":342},[68,4909,1617],{"class":331},[68,4911,1620],{"class":342},[68,4913,4914],{"class":70,"line":83},[68,4915,4916],{"class":325}," // Read and parse JSON body\n",[68,4918,4919,4921,4924,4926,4928,4931],{"class":70,"line":89},[68,4920,376],{"class":331},[68,4922,4923],{"class":353}," body",[68,4925,382],{"class":331},[68,4927,385],{"class":331},[68,4929,4930],{"class":338}," readBody",[68,4932,4555],{"class":342},[68,4934,4935],{"class":70,"line":95},[68,4936,123],{"emptyLinePlaceholder":122},[68,4938,4939],{"class":70,"line":101},[68,4940,4941],{"class":325}," // Read query parameters\n",[68,4943,4944,4946,4948,4950,4952],{"class":70,"line":107},[68,4945,376],{"class":331},[68,4947,4547],{"class":353},[68,4949,382],{"class":331},[68,4951,4552],{"class":338},[68,4953,4555],{"class":342},[68,4955,4956],{"class":70,"line":113},[68,4957,123],{"emptyLinePlaceholder":122},[68,4959,4960],{"class":70,"line":119},[68,4961,4962],{"class":325}," // Read route parameters (from [id].get.ts)\n",[68,4964,4965,4967,4970,4972,4975],{"class":70,"line":126},[68,4966,376],{"class":331},[68,4968,4969],{"class":353}," params",[68,4971,382],{"class":331},[68,4973,4974],{"class":338}," getRouterParams",[68,4976,4555],{"class":342},[68,4978,4979,4981,4984,4986],{"class":70,"line":132},[68,4980,376],{"class":331},[68,4982,4983],{"class":353}," userId",[68,4985,382],{"class":331},[68,4987,4988],{"class":342}," params.id\n",[68,4990,4991],{"class":70,"line":2135},[68,4992,123],{"emptyLinePlaceholder":122},[68,4994,4995],{"class":70,"line":2141},[68,4996,4997],{"class":325}," // Read specific headers\n",[68,4999,5000,5002,5005,5007,5010,5013,5016],{"class":70,"line":2437},[68,5001,376],{"class":331},[68,5003,5004],{"class":353}," authHeader",[68,5006,382],{"class":331},[68,5008,5009],{"class":338}," getHeader",[68,5011,5012],{"class":342},"(event, ",[68,5014,5015],{"class":409},"'authorization'",[68,5017,1702],{"class":342},[68,5019,5020],{"class":70,"line":2442},[68,5021,123],{"emptyLinePlaceholder":122},[68,5023,5024],{"class":70,"line":2447},[68,5025,5026],{"class":325}," // Read cookies\n",[68,5028,5029,5031,5034,5036,5039,5041,5044],{"class":70,"line":2652},[68,5030,376],{"class":331},[68,5032,5033],{"class":353}," sessionToken",[68,5035,382],{"class":331},[68,5037,5038],{"class":338}," getCookie",[68,5040,5012],{"class":342},[68,5042,5043],{"class":409},"'session'",[68,5045,1702],{"class":342},[68,5047,5048],{"class":70,"line":2687},[68,5049,2789],{"class":342},[15,5051,5053],{"id":5052},"input-validation-with-zod","Input Validation With Zod",[20,5055,5056],{},"Never trust client-provided data. Validate every input with Zod:",[59,5058,5060],{"className":316,"code":5059,"language":318,"meta":64,"style":64},"// server/api/users/index.post.ts\nimport { z } from 'zod'\n\nConst createUserSchema = z.object({\n name: z.string().min(1).max(100),\n email: z.string().email(),\n password: z.string().min(8).max(100),\n role: z.enum(['admin', 'editor', 'viewer']).default('viewer'),\n})\n\nExport default defineEventHandler(async (event) => {\n const body = await readBody(event)\n\n const parsed = createUserSchema.safeParse(body)\n if (!parsed.success) {\n throw createError({\n statusCode: 422,\n statusMessage: 'Validation failed',\n data: parsed.error.flatten(),\n })\n }\n\n const { name, email, password, role } = parsed.data\n // ... Create user\n})\n",[37,5061,5062,5066,5078,5082,5097,5125,5139,5165,5200,5204,5208,5230,5244,5248,5266,5277,5286,5295,5304,5314,5318,5322,5326,5356,5361],{"__ignoreMap":64},[68,5063,5064],{"class":70,"line":71},[68,5065,4887],{"class":325},[68,5067,5068,5070,5073,5075],{"class":70,"line":77},[68,5069,2037],{"class":331},[68,5071,5072],{"class":342}," { z } ",[68,5074,2043],{"class":331},[68,5076,5077],{"class":409}," 'zod'\n",[68,5079,5080],{"class":70,"line":83},[68,5081,123],{"emptyLinePlaceholder":122},[68,5083,5084,5087,5089,5092,5095],{"class":70,"line":89},[68,5085,5086],{"class":342},"Const createUserSchema ",[68,5088,1593],{"class":331},[68,5090,5091],{"class":342}," z.",[68,5093,5094],{"class":338},"object",[68,5096,1789],{"class":342},[68,5098,5099,5102,5104,5106,5109,5111,5113,5116,5118,5120,5123],{"class":70,"line":95},[68,5100,5101],{"class":342}," name: z.",[68,5103,3569],{"class":338},[68,5105,2773],{"class":342},[68,5107,5108],{"class":338},"min",[68,5110,343],{"class":342},[68,5112,2764],{"class":353},[68,5114,5115],{"class":342},").",[68,5117,2759],{"class":338},[68,5119,343],{"class":342},[68,5121,5122],{"class":353},"100",[68,5124,1814],{"class":342},[68,5126,5127,5130,5132,5134,5137],{"class":70,"line":101},[68,5128,5129],{"class":342}," email: z.",[68,5131,3569],{"class":338},[68,5133,2773],{"class":342},[68,5135,5136],{"class":338},"email",[68,5138,4746],{"class":342},[68,5140,5141,5144,5146,5148,5150,5152,5155,5157,5159,5161,5163],{"class":70,"line":107},[68,5142,5143],{"class":342}," password: z.",[68,5145,3569],{"class":338},[68,5147,2773],{"class":342},[68,5149,5108],{"class":338},[68,5151,343],{"class":342},[68,5153,5154],{"class":353},"8",[68,5156,5115],{"class":342},[68,5158,2759],{"class":338},[68,5160,343],{"class":342},[68,5162,5122],{"class":353},[68,5164,1814],{"class":342},[68,5166,5167,5170,5173,5176,5179,5181,5184,5186,5189,5192,5194,5196,5198],{"class":70,"line":113},[68,5168,5169],{"class":342}," role: z.",[68,5171,5172],{"class":338},"enum",[68,5174,5175],{"class":342},"([",[68,5177,5178],{"class":409},"'admin'",[68,5180,180],{"class":342},[68,5182,5183],{"class":409},"'editor'",[68,5185,180],{"class":342},[68,5187,5188],{"class":409},"'viewer'",[68,5190,5191],{"class":342},"]).",[68,5193,4522],{"class":338},[68,5195,343],{"class":342},[68,5197,5188],{"class":409},[68,5199,1814],{"class":342},[68,5201,5202],{"class":70,"line":119},[68,5203,2789],{"class":342},[68,5205,5206],{"class":70,"line":126},[68,5207,123],{"emptyLinePlaceholder":122},[68,5209,5210,5212,5214,5216,5218,5220,5222,5224,5226,5228],{"class":70,"line":132},[68,5211,4519],{"class":342},[68,5213,4522],{"class":331},[68,5215,4525],{"class":338},[68,5217,343],{"class":342},[68,5219,332],{"class":331},[68,5221,2657],{"class":342},[68,5223,4534],{"class":346},[68,5225,1869],{"class":342},[68,5227,1617],{"class":331},[68,5229,1620],{"class":342},[68,5231,5232,5234,5236,5238,5240,5242],{"class":70,"line":2135},[68,5233,376],{"class":331},[68,5235,4923],{"class":353},[68,5237,382],{"class":331},[68,5239,385],{"class":331},[68,5241,4930],{"class":338},[68,5243,4555],{"class":342},[68,5245,5246],{"class":70,"line":2141},[68,5247,123],{"emptyLinePlaceholder":122},[68,5249,5250,5252,5255,5257,5260,5263],{"class":70,"line":2437},[68,5251,376],{"class":331},[68,5253,5254],{"class":353}," parsed",[68,5256,382],{"class":331},[68,5258,5259],{"class":342}," createUserSchema.",[68,5261,5262],{"class":338},"safeParse",[68,5264,5265],{"class":342},"(body)\n",[68,5267,5268,5270,5272,5274],{"class":70,"line":2442},[68,5269,400],{"class":331},[68,5271,2657],{"class":342},[68,5273,3428],{"class":331},[68,5275,5276],{"class":342},"parsed.success) {\n",[68,5278,5279,5282,5284],{"class":70,"line":2447},[68,5280,5281],{"class":331}," throw",[68,5283,4842],{"class":338},[68,5285,1789],{"class":342},[68,5287,5288,5290,5293],{"class":70,"line":2652},[68,5289,4849],{"class":342},[68,5291,5292],{"class":353},"422",[68,5294,2751],{"class":342},[68,5296,5297,5299,5302],{"class":70,"line":2687},[68,5298,4859],{"class":342},[68,5300,5301],{"class":409},"'Validation failed'",[68,5303,2751],{"class":342},[68,5305,5306,5309,5312],{"class":70,"line":2692},[68,5307,5308],{"class":342}," data: parsed.error.",[68,5310,5311],{"class":338},"flatten",[68,5313,4746],{"class":342},[68,5315,5316],{"class":70,"line":2697},[68,5317,1859],{"class":342},[68,5319,5320],{"class":70,"line":3129},[68,5321,429],{"class":342},[68,5323,5324],{"class":70,"line":3134},[68,5325,123],{"emptyLinePlaceholder":122},[68,5327,5328,5330,5332,5335,5337,5339,5341,5344,5346,5349,5351,5353],{"class":70,"line":4749},[68,5329,376],{"class":331},[68,5331,1748],{"class":342},[68,5333,5334],{"class":353},"name",[68,5336,180],{"class":342},[68,5338,5136],{"class":353},[68,5340,180],{"class":342},[68,5342,5343],{"class":353},"password",[68,5345,180],{"class":342},[68,5347,5348],{"class":353},"role",[68,5350,1769],{"class":342},[68,5352,1593],{"class":331},[68,5354,5355],{"class":342}," parsed.data\n",[68,5357,5358],{"class":70,"line":4755},[68,5359,5360],{"class":325}," // ... Create user\n",[68,5362,5363],{"class":70,"line":4760},[68,5364,2789],{"class":342},[20,5366,5367],{},"Create a reusable validation utility:",[59,5369,5371],{"className":316,"code":5370,"language":318,"meta":64,"style":64},"// server/utils/validate.ts\nimport { z, ZodSchema } from 'zod'\n\nExport async function validate\u003CT>(event: H3Event, schema: ZodSchema\u003CT>): Promise\u003CT> {\n const body = await readBody(event)\n const parsed = schema.safeParse(body)\n\n if (!parsed.success) {\n throw createError({\n statusCode: 422,\n statusMessage: 'Validation failed',\n data: parsed.error.flatten(),\n })\n }\n\n return parsed.data\n}\n",[37,5372,5373,5378,5389,5393,5446,5460,5475,5479,5489,5497,5505,5513,5521,5525,5529,5533,5539],{"__ignoreMap":64},[68,5374,5375],{"class":70,"line":71},[68,5376,5377],{"class":325},"// server/utils/validate.ts\n",[68,5379,5380,5382,5385,5387],{"class":70,"line":77},[68,5381,2037],{"class":331},[68,5383,5384],{"class":342}," { z, ZodSchema } ",[68,5386,2043],{"class":331},[68,5388,5077],{"class":409},[68,5390,5391],{"class":70,"line":83},[68,5392,123],{"emptyLinePlaceholder":122},[68,5394,5395,5397,5399,5401,5404,5406,5409,5412,5414,5416,5419,5421,5424,5426,5429,5431,5433,5436,5438,5440,5442,5444],{"class":70,"line":89},[68,5396,4519],{"class":342},[68,5398,332],{"class":331},[68,5400,335],{"class":331},[68,5402,5403],{"class":338}," validate",[68,5405,365],{"class":342},[68,5407,5408],{"class":338},"T",[68,5410,5411],{"class":342},">(",[68,5413,4534],{"class":346},[68,5415,350],{"class":331},[68,5417,5418],{"class":338}," H3Event",[68,5420,180],{"class":342},[68,5422,5423],{"class":346},"schema",[68,5425,350],{"class":331},[68,5427,5428],{"class":338}," ZodSchema",[68,5430,365],{"class":342},[68,5432,5408],{"class":338},[68,5434,5435],{"class":342},">)",[68,5437,350],{"class":331},[68,5439,362],{"class":338},[68,5441,365],{"class":342},[68,5443,5408],{"class":338},[68,5445,371],{"class":342},[68,5447,5448,5450,5452,5454,5456,5458],{"class":70,"line":95},[68,5449,376],{"class":331},[68,5451,4923],{"class":353},[68,5453,382],{"class":331},[68,5455,385],{"class":331},[68,5457,4930],{"class":338},[68,5459,4555],{"class":342},[68,5461,5462,5464,5466,5468,5471,5473],{"class":70,"line":101},[68,5463,376],{"class":331},[68,5465,5254],{"class":353},[68,5467,382],{"class":331},[68,5469,5470],{"class":342}," schema.",[68,5472,5262],{"class":338},[68,5474,5265],{"class":342},[68,5476,5477],{"class":70,"line":107},[68,5478,123],{"emptyLinePlaceholder":122},[68,5480,5481,5483,5485,5487],{"class":70,"line":113},[68,5482,400],{"class":331},[68,5484,2657],{"class":342},[68,5486,3428],{"class":331},[68,5488,5276],{"class":342},[68,5490,5491,5493,5495],{"class":70,"line":119},[68,5492,5281],{"class":331},[68,5494,4842],{"class":338},[68,5496,1789],{"class":342},[68,5498,5499,5501,5503],{"class":70,"line":126},[68,5500,4849],{"class":342},[68,5502,5292],{"class":353},[68,5504,2751],{"class":342},[68,5506,5507,5509,5511],{"class":70,"line":132},[68,5508,4859],{"class":342},[68,5510,5301],{"class":409},[68,5512,2751],{"class":342},[68,5514,5515,5517,5519],{"class":70,"line":2135},[68,5516,5308],{"class":342},[68,5518,5311],{"class":338},[68,5520,4746],{"class":342},[68,5522,5523],{"class":70,"line":2141},[68,5524,1859],{"class":342},[68,5526,5527],{"class":70,"line":2437},[68,5528,429],{"class":342},[68,5530,5531],{"class":70,"line":2442},[68,5532,123],{"emptyLinePlaceholder":122},[68,5534,5535,5537],{"class":70,"line":2447},[68,5536,418],{"class":331},[68,5538,5355],{"class":342},[68,5540,5541],{"class":70,"line":2652},[68,5542,447],{"class":342},[20,5544,5545],{},"Now your route handlers stay clean:",[59,5547,5549],{"className":316,"code":5548,"language":318,"meta":64,"style":64},"export default defineEventHandler(async (event) => {\n const data = await validate(event, createUserSchema)\n // data is fully typed\n})\n",[37,5550,5551,5573,5589,5594],{"__ignoreMap":64},[68,5552,5553,5555,5557,5559,5561,5563,5565,5567,5569,5571],{"class":70,"line":71},[68,5554,4892],{"class":331},[68,5556,4895],{"class":331},[68,5558,4525],{"class":338},[68,5560,343],{"class":342},[68,5562,332],{"class":331},[68,5564,2657],{"class":342},[68,5566,4534],{"class":346},[68,5568,1869],{"class":342},[68,5570,1617],{"class":331},[68,5572,1620],{"class":342},[68,5574,5575,5577,5580,5582,5584,5586],{"class":70,"line":77},[68,5576,376],{"class":331},[68,5578,5579],{"class":353}," data",[68,5581,382],{"class":331},[68,5583,385],{"class":331},[68,5585,5403],{"class":338},[68,5587,5588],{"class":342},"(event, createUserSchema)\n",[68,5590,5591],{"class":70,"line":83},[68,5592,5593],{"class":325}," // data is fully typed\n",[68,5595,5596],{"class":70,"line":89},[68,5597,2789],{"class":342},[15,5599,5601],{"id":5600},"server-middleware","Server Middleware",[20,5603,5604,5605,5608],{},"Server middleware in ",[37,5606,5607],{},"server/middleware/"," runs before every request. Use it for CORS, authentication, logging, and request context setup:",[59,5610,5612],{"className":316,"code":5611,"language":318,"meta":64,"style":64},"// server/middleware/cors.ts\nexport default defineEventHandler((event) => {\n setResponseHeaders(event, {\n 'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN ?? '*',\n 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n })\n\n if (getMethod(event) === 'OPTIONS') {\n event.node.res.statusCode = 204\n return 'OK'\n }\n})\n",[37,5613,5614,5619,5637,5645,5664,5676,5688,5692,5696,5715,5725,5732,5736],{"__ignoreMap":64},[68,5615,5616],{"class":70,"line":71},[68,5617,5618],{"class":325},"// server/middleware/cors.ts\n",[68,5620,5621,5623,5625,5627,5629,5631,5633,5635],{"class":70,"line":77},[68,5622,4892],{"class":331},[68,5624,4895],{"class":331},[68,5626,4525],{"class":338},[68,5628,2525],{"class":342},[68,5630,4534],{"class":346},[68,5632,1869],{"class":342},[68,5634,1617],{"class":331},[68,5636,1620],{"class":342},[68,5638,5639,5642],{"class":70,"line":83},[68,5640,5641],{"class":338}," setResponseHeaders",[68,5643,5644],{"class":342},"(event, {\n",[68,5646,5647,5650,5653,5656,5659,5662],{"class":70,"line":89},[68,5648,5649],{"class":409}," 'Access-Control-Allow-Origin'",[68,5651,5652],{"class":342},": process.env.",[68,5654,5655],{"class":353},"ALLOWED_ORIGIN",[68,5657,5658],{"class":331}," ??",[68,5660,5661],{"class":409}," '*'",[68,5663,2751],{"class":342},[68,5665,5666,5669,5671,5674],{"class":70,"line":95},[68,5667,5668],{"class":409}," 'Access-Control-Allow-Methods'",[68,5670,938],{"class":342},[68,5672,5673],{"class":409},"'GET, POST, PUT, DELETE, PATCH, OPTIONS'",[68,5675,2751],{"class":342},[68,5677,5678,5681,5683,5686],{"class":70,"line":101},[68,5679,5680],{"class":409}," 'Access-Control-Allow-Headers'",[68,5682,938],{"class":342},[68,5684,5685],{"class":409},"'Content-Type, Authorization'",[68,5687,2751],{"class":342},[68,5689,5690],{"class":70,"line":107},[68,5691,1859],{"class":342},[68,5693,5694],{"class":70,"line":113},[68,5695,123],{"emptyLinePlaceholder":122},[68,5697,5698,5700,5702,5705,5708,5710,5713],{"class":70,"line":119},[68,5699,400],{"class":331},[68,5701,2657],{"class":342},[68,5703,5704],{"class":338},"getMethod",[68,5706,5707],{"class":342},"(event) ",[68,5709,406],{"class":331},[68,5711,5712],{"class":409}," 'OPTIONS'",[68,5714,413],{"class":342},[68,5716,5717,5720,5722],{"class":70,"line":126},[68,5718,5719],{"class":342}," event.node.res.statusCode ",[68,5721,1593],{"class":331},[68,5723,5724],{"class":353}," 204\n",[68,5726,5727,5729],{"class":70,"line":132},[68,5728,418],{"class":331},[68,5730,5731],{"class":409}," 'OK'\n",[68,5733,5734],{"class":70,"line":2135},[68,5735,429],{"class":342},[68,5737,5738],{"class":70,"line":2141},[68,5739,2789],{"class":342},[59,5741,5743],{"className":316,"code":5742,"language":318,"meta":64,"style":64},"// server/middleware/logger.ts\nexport default defineEventHandler((event) => {\n const start = Date.now()\n const url = getRequestURL(event)\n\n // After response\n event.node.res.on('finish', () => {\n const duration = Date.now() - start\n console.log(`${getMethod(event)} ${url.pathname} ${event.node.res.statusCode} ${duration}ms`)\n })\n})\n",[37,5744,5745,5750,5768,5783,5797,5801,5806,5824,5844,5900,5904],{"__ignoreMap":64},[68,5746,5747],{"class":70,"line":71},[68,5748,5749],{"class":325},"// server/middleware/logger.ts\n",[68,5751,5752,5754,5756,5758,5760,5762,5764,5766],{"class":70,"line":77},[68,5753,4892],{"class":331},[68,5755,4895],{"class":331},[68,5757,4525],{"class":338},[68,5759,2525],{"class":342},[68,5761,4534],{"class":346},[68,5763,1869],{"class":342},[68,5765,1617],{"class":331},[68,5767,1620],{"class":342},[68,5769,5770,5772,5775,5777,5779,5781],{"class":70,"line":83},[68,5771,376],{"class":331},[68,5773,5774],{"class":353}," start",[68,5776,382],{"class":331},[68,5778,1596],{"class":342},[68,5780,1599],{"class":338},[68,5782,1602],{"class":342},[68,5784,5785,5787,5790,5792,5795],{"class":70,"line":89},[68,5786,376],{"class":331},[68,5788,5789],{"class":353}," url",[68,5791,382],{"class":331},[68,5793,5794],{"class":338}," getRequestURL",[68,5796,4555],{"class":342},[68,5798,5799],{"class":70,"line":95},[68,5800,123],{"emptyLinePlaceholder":122},[68,5802,5803],{"class":70,"line":101},[68,5804,5805],{"class":325}," // After response\n",[68,5807,5808,5811,5813,5815,5818,5820,5822],{"class":70,"line":107},[68,5809,5810],{"class":342}," event.node.res.",[68,5812,2595],{"class":338},[68,5814,343],{"class":342},[68,5816,5817],{"class":409},"'finish'",[68,5819,3101],{"class":342},[68,5821,1617],{"class":331},[68,5823,1620],{"class":342},[68,5825,5826,5828,5831,5833,5835,5837,5839,5841],{"class":70,"line":113},[68,5827,376],{"class":331},[68,5829,5830],{"class":353}," duration",[68,5832,382],{"class":331},[68,5834,1596],{"class":342},[68,5836,1599],{"class":338},[68,5838,1636],{"class":342},[68,5840,1639],{"class":331},[68,5842,5843],{"class":342}," start\n",[68,5845,5846,5848,5850,5852,5855,5857,5859,5861,5863,5866,5869,5871,5874,5876,5878,5880,5882,5884,5886,5888,5891,5893,5896,5898],{"class":70,"line":119},[68,5847,1685],{"class":342},[68,5849,1786],{"class":338},[68,5851,343],{"class":342},[68,5853,5854],{"class":409},"`${",[68,5856,5704],{"class":338},[68,5858,343],{"class":409},[68,5860,4534],{"class":342},[68,5862,357],{"class":409},[68,5864,5865],{"class":409},"} ${",[68,5867,5868],{"class":342},"url",[68,5870,51],{"class":409},[68,5872,5873],{"class":342},"pathname",[68,5875,5865],{"class":409},[68,5877,4534],{"class":342},[68,5879,51],{"class":409},[68,5881,1897],{"class":342},[68,5883,51],{"class":409},[68,5885,3184],{"class":342},[68,5887,51],{"class":409},[68,5889,5890],{"class":342},"statusCode",[68,5892,5865],{"class":409},[68,5894,5895],{"class":342},"duration",[68,5897,1699],{"class":409},[68,5899,1702],{"class":342},[68,5901,5902],{"class":70,"line":126},[68,5903,1859],{"class":342},[68,5905,5906],{"class":70,"line":132},[68,5907,2789],{"class":342},[15,5909,5911],{"id":5910},"database-integration","Database Integration",[20,5913,5914],{},"Initialize Prisma as a singleton to avoid connection pool exhaustion:",[59,5916,5918],{"className":316,"code":5917,"language":318,"meta":64,"style":64},"// server/utils/prisma.ts\nimport { PrismaClient } from '@prisma/client'\n\nConst globalForPrisma = globalThis as unknown as {\n prisma: PrismaClient | undefined\n}\n\nExport const prisma =\n globalForPrisma.prisma ??\n new PrismaClient({\n log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],\n })\n\nIf (process.env.NODE_ENV !== 'production') {\n globalForPrisma.prisma = prisma\n}\n",[37,5919,5920,5925,5937,5941,5961,5975,5979,5983,5994,6002,6010,6052,6056,6060,6077,6086],{"__ignoreMap":64},[68,5921,5922],{"class":70,"line":71},[68,5923,5924],{"class":325},"// server/utils/prisma.ts\n",[68,5926,5927,5929,5932,5934],{"class":70,"line":77},[68,5928,2037],{"class":331},[68,5930,5931],{"class":342}," { PrismaClient } ",[68,5933,2043],{"class":331},[68,5935,5936],{"class":409}," '@prisma/client'\n",[68,5938,5939],{"class":70,"line":83},[68,5940,123],{"emptyLinePlaceholder":122},[68,5942,5943,5946,5948,5951,5953,5956,5959],{"class":70,"line":89},[68,5944,5945],{"class":342},"Const globalForPrisma ",[68,5947,1593],{"class":331},[68,5949,5950],{"class":342}," globalThis ",[68,5952,3731],{"class":331},[68,5954,5955],{"class":353}," unknown",[68,5957,5958],{"class":331}," as",[68,5960,1620],{"class":342},[68,5962,5963,5965,5967,5969,5972],{"class":70,"line":95},[68,5964,3859],{"class":346},[68,5966,350],{"class":331},[68,5968,3866],{"class":338},[68,5970,5971],{"class":331}," |",[68,5973,5974],{"class":353}," undefined\n",[68,5976,5977],{"class":70,"line":101},[68,5978,447],{"class":342},[68,5980,5981],{"class":70,"line":107},[68,5982,123],{"emptyLinePlaceholder":122},[68,5984,5985,5987,5989,5991],{"class":70,"line":113},[68,5986,4519],{"class":342},[68,5988,1991],{"class":331},[68,5990,3859],{"class":353},[68,5992,5993],{"class":331}," =\n",[68,5995,5996,5999],{"class":70,"line":119},[68,5997,5998],{"class":342}," globalForPrisma.prisma ",[68,6000,6001],{"class":331},"??\n",[68,6003,6004,6006,6008],{"class":70,"line":126},[68,6005,2551],{"class":331},[68,6007,3866],{"class":338},[68,6009,1789],{"class":342},[68,6011,6012,6015,6018,6021,6024,6027,6029,6032,6034,6036,6038,6041,6043,6045,6047,6049],{"class":70,"line":132},[68,6013,6014],{"class":342}," log: process.env.",[68,6016,6017],{"class":353},"NODE_ENV",[68,6019,6020],{"class":331}," ===",[68,6022,6023],{"class":409}," 'development'",[68,6025,6026],{"class":331}," ?",[68,6028,4631],{"class":342},[68,6030,6031],{"class":409},"'query'",[68,6033,180],{"class":342},[68,6035,2619],{"class":409},[68,6037,180],{"class":342},[68,6039,6040],{"class":409},"'warn'",[68,6042,4642],{"class":342},[68,6044,350],{"class":331},[68,6046,4631],{"class":342},[68,6048,2619],{"class":409},[68,6050,6051],{"class":342},"],\n",[68,6053,6054],{"class":70,"line":2135},[68,6055,1859],{"class":342},[68,6057,6058],{"class":70,"line":2141},[68,6059,123],{"emptyLinePlaceholder":122},[68,6061,6062,6064,6067,6069,6072,6075],{"class":70,"line":2437},[68,6063,2890],{"class":338},[68,6065,6066],{"class":342}," (process.env.",[68,6068,6017],{"class":353},[68,6070,6071],{"class":331}," !==",[68,6073,6074],{"class":409}," 'production'",[68,6076,413],{"class":342},[68,6078,6079,6081,6083],{"class":70,"line":2442},[68,6080,5998],{"class":342},[68,6082,1593],{"class":331},[68,6084,6085],{"class":342}," prisma\n",[68,6087,6088],{"class":70,"line":2447},[68,6089,447],{"class":342},[20,6091,6092],{},"In development, this prevents creating a new Prisma client on every hot reload, which would exhaust your database connection limit quickly.",[15,6094,6096],{"id":6095},"shared-typescript-types","Shared TypeScript Types",[20,6098,6099],{},"The big win of full-stack Nuxt is sharing types between frontend and backend. Define your API response types once:",[59,6101,6103],{"className":316,"code":6102,"language":318,"meta":64,"style":64},"// types/api.ts\nexport interface User {\n id: string\n name: string\n email: string\n role: 'admin' | 'editor' | 'viewer'\n createdAt: string\n}\n\nExport interface PaginatedResponse\u003CT> {\n data: T[]\n pagination: {\n page: number\n limit: number\n total: number\n pages: number\n }\n}\n\nExport interface ApiError {\n statusCode: number\n statusMessage: string\n data?: unknown\n}\n",[37,6104,6105,6110,6122,6132,6141,6150,6170,6179,6183,6187,6203,6215,6224,6233,6241,6250,6259,6263,6267,6271,6282,6291,6300,6310],{"__ignoreMap":64},[68,6106,6107],{"class":70,"line":71},[68,6108,6109],{"class":325},"// types/api.ts\n",[68,6111,6112,6114,6117,6120],{"class":70,"line":77},[68,6113,4892],{"class":331},[68,6115,6116],{"class":331}," interface",[68,6118,6119],{"class":338}," User",[68,6121,1620],{"class":342},[68,6123,6124,6127,6129],{"class":70,"line":83},[68,6125,6126],{"class":346}," id",[68,6128,350],{"class":331},[68,6130,6131],{"class":353}," string\n",[68,6133,6134,6137,6139],{"class":70,"line":89},[68,6135,6136],{"class":346}," name",[68,6138,350],{"class":331},[68,6140,6131],{"class":353},[68,6142,6143,6146,6148],{"class":70,"line":95},[68,6144,6145],{"class":346}," email",[68,6147,350],{"class":331},[68,6149,6131],{"class":353},[68,6151,6152,6155,6157,6160,6162,6165,6167],{"class":70,"line":101},[68,6153,6154],{"class":346}," role",[68,6156,350],{"class":331},[68,6158,6159],{"class":409}," 'admin'",[68,6161,5971],{"class":331},[68,6163,6164],{"class":409}," 'editor'",[68,6166,5971],{"class":331},[68,6168,6169],{"class":409}," 'viewer'\n",[68,6171,6172,6175,6177],{"class":70,"line":107},[68,6173,6174],{"class":346}," createdAt",[68,6176,350],{"class":331},[68,6178,6131],{"class":353},[68,6180,6181],{"class":70,"line":113},[68,6182,447],{"class":342},[68,6184,6185],{"class":70,"line":119},[68,6186,123],{"emptyLinePlaceholder":122},[68,6188,6189,6191,6194,6197,6199,6201],{"class":70,"line":126},[68,6190,4519],{"class":342},[68,6192,6193],{"class":331},"interface",[68,6195,6196],{"class":338}," PaginatedResponse",[68,6198,365],{"class":342},[68,6200,5408],{"class":338},[68,6202,371],{"class":342},[68,6204,6205,6207,6209,6212],{"class":70,"line":132},[68,6206,5579],{"class":346},[68,6208,350],{"class":331},[68,6210,6211],{"class":338}," T",[68,6213,6214],{"class":342},"[]\n",[68,6216,6217,6220,6222],{"class":70,"line":2135},[68,6218,6219],{"class":346}," pagination",[68,6221,350],{"class":331},[68,6223,1620],{"class":342},[68,6225,6226,6228,6230],{"class":70,"line":2141},[68,6227,4562],{"class":346},[68,6229,350],{"class":331},[68,6231,6232],{"class":353}," number\n",[68,6234,6235,6237,6239],{"class":70,"line":2437},[68,6236,4583],{"class":346},[68,6238,350],{"class":331},[68,6240,6232],{"class":353},[68,6242,6243,6246,6248],{"class":70,"line":2442},[68,6244,6245],{"class":346}," total",[68,6247,350],{"class":331},[68,6249,6232],{"class":353},[68,6251,6252,6255,6257],{"class":70,"line":2447},[68,6253,6254],{"class":346}," pages",[68,6256,350],{"class":331},[68,6258,6232],{"class":353},[68,6260,6261],{"class":70,"line":2652},[68,6262,429],{"class":342},[68,6264,6265],{"class":70,"line":2687},[68,6266,447],{"class":342},[68,6268,6269],{"class":70,"line":2692},[68,6270,123],{"emptyLinePlaceholder":122},[68,6272,6273,6275,6277,6280],{"class":70,"line":2697},[68,6274,4519],{"class":342},[68,6276,6193],{"class":331},[68,6278,6279],{"class":338}," ApiError",[68,6281,1620],{"class":342},[68,6283,6284,6287,6289],{"class":70,"line":3129},[68,6285,6286],{"class":346}," statusCode",[68,6288,350],{"class":331},[68,6290,6232],{"class":353},[68,6292,6293,6296,6298],{"class":70,"line":3134},[68,6294,6295],{"class":346}," statusMessage",[68,6297,350],{"class":331},[68,6299,6131],{"class":353},[68,6301,6302,6304,6307],{"class":70,"line":4749},[68,6303,5579],{"class":346},[68,6305,6306],{"class":331},"?:",[68,6308,6309],{"class":353}," unknown\n",[68,6311,6312],{"class":70,"line":4755},[68,6313,447],{"class":342},[20,6315,6316],{},"Your API routes return these types and your frontend composables consume them — with full TypeScript inference end-to-end.",[15,6318,6320],{"id":6319},"rate-limiting","Rate Limiting",[20,6322,6323],{},"Add rate limiting to protect your endpoints:",[59,6325,6327],{"className":316,"code":6326,"language":318,"meta":64,"style":64},"// server/utils/rateLimit.ts\nconst requests = new Map\u003Cstring, { count: number; resetAt: number }>()\n\nExport function rateLimit(ip: string, limit = 100, windowMs = 60000) {\n const now = Date.now()\n const record = requests.get(ip)\n\n if (!record || record.resetAt \u003C now) {\n requests.set(ip, { count: 1, resetAt: now + windowMs })\n return true\n }\n\n if (record.count >= limit) {\n return false\n }\n\n record.count++\n return true\n}\n",[37,6328,6329,6334,6374,6378,6417,6432,6449,6453,6475,6494,6501,6505,6509,6522,6529,6533,6537,6545,6551],{"__ignoreMap":64},[68,6330,6331],{"class":70,"line":71},[68,6332,6333],{"class":325},"// server/utils/rateLimit.ts\n",[68,6335,6336,6338,6341,6343,6345,6347,6349,6351,6354,6356,6358,6361,6364,6367,6369,6371],{"class":70,"line":77},[68,6337,1991],{"class":331},[68,6339,6340],{"class":353}," requests",[68,6342,382],{"class":331},[68,6344,2551],{"class":331},[68,6346,3399],{"class":338},[68,6348,365],{"class":342},[68,6350,3569],{"class":353},[68,6352,6353],{"class":342},", { ",[68,6355,4743],{"class":346},[68,6357,350],{"class":331},[68,6359,6360],{"class":353}," number",[68,6362,6363],{"class":342},"; ",[68,6365,6366],{"class":346},"resetAt",[68,6368,350],{"class":331},[68,6370,6360],{"class":353},[68,6372,6373],{"class":342}," }>()\n",[68,6375,6376],{"class":70,"line":83},[68,6377,123],{"emptyLinePlaceholder":122},[68,6379,6380,6382,6384,6387,6389,6392,6394,6396,6398,6401,6403,6405,6407,6410,6412,6415],{"class":70,"line":89},[68,6381,4519],{"class":342},[68,6383,2082],{"class":331},[68,6385,6386],{"class":338}," rateLimit",[68,6388,343],{"class":342},[68,6390,6391],{"class":346},"ip",[68,6393,350],{"class":331},[68,6395,354],{"class":353},[68,6397,180],{"class":342},[68,6399,6400],{"class":346},"limit",[68,6402,382],{"class":331},[68,6404,1678],{"class":353},[68,6406,180],{"class":342},[68,6408,6409],{"class":346},"windowMs",[68,6411,382],{"class":331},[68,6413,6414],{"class":353}," 60000",[68,6416,413],{"class":342},[68,6418,6419,6421,6424,6426,6428,6430],{"class":70,"line":95},[68,6420,376],{"class":331},[68,6422,6423],{"class":353}," now",[68,6425,382],{"class":331},[68,6427,1596],{"class":342},[68,6429,1599],{"class":338},[68,6431,1602],{"class":342},[68,6433,6434,6436,6439,6441,6444,6446],{"class":70,"line":101},[68,6435,376],{"class":331},[68,6437,6438],{"class":353}," record",[68,6440,382],{"class":331},[68,6442,6443],{"class":342}," requests.",[68,6445,3169],{"class":338},[68,6447,6448],{"class":342},"(ip)\n",[68,6450,6451],{"class":70,"line":107},[68,6452,123],{"emptyLinePlaceholder":122},[68,6454,6455,6457,6459,6461,6464,6467,6470,6472],{"class":70,"line":113},[68,6456,400],{"class":331},[68,6458,2657],{"class":342},[68,6460,3428],{"class":331},[68,6462,6463],{"class":342},"record ",[68,6465,6466],{"class":331},"||",[68,6468,6469],{"class":342}," record.resetAt ",[68,6471,365],{"class":331},[68,6473,6474],{"class":342}," now) {\n",[68,6476,6477,6479,6481,6484,6486,6489,6491],{"class":70,"line":119},[68,6478,6443],{"class":342},[68,6480,3445],{"class":338},[68,6482,6483],{"class":342},"(ip, { count: ",[68,6485,2764],{"class":353},[68,6487,6488],{"class":342},", resetAt: now ",[68,6490,2179],{"class":331},[68,6492,6493],{"class":342}," windowMs })\n",[68,6495,6496,6498],{"class":70,"line":126},[68,6497,418],{"class":331},[68,6499,6500],{"class":353}," true\n",[68,6502,6503],{"class":70,"line":132},[68,6504,429],{"class":342},[68,6506,6507],{"class":70,"line":2135},[68,6508,123],{"emptyLinePlaceholder":122},[68,6510,6511,6513,6516,6519],{"class":70,"line":2141},[68,6512,400],{"class":331},[68,6514,6515],{"class":342}," (record.count ",[68,6517,6518],{"class":331},">=",[68,6520,6521],{"class":342}," limit) {\n",[68,6523,6524,6526],{"class":70,"line":2437},[68,6525,418],{"class":331},[68,6527,6528],{"class":353}," false\n",[68,6530,6531],{"class":70,"line":2442},[68,6532,429],{"class":342},[68,6534,6535],{"class":70,"line":2447},[68,6536,123],{"emptyLinePlaceholder":122},[68,6538,6539,6542],{"class":70,"line":2652},[68,6540,6541],{"class":342}," record.count",[68,6543,6544],{"class":331},"++\n",[68,6546,6547,6549],{"class":70,"line":2687},[68,6548,418],{"class":331},[68,6550,6500],{"class":353},[68,6552,6553],{"class":70,"line":2692},[68,6554,447],{"class":342},[59,6556,6558],{"className":316,"code":6557,"language":318,"meta":64,"style":64},"// In your API route\nexport default defineEventHandler(async (event) => {\n const ip = getRequestIP(event, { xForwardedFor: true }) ?? 'unknown'\n\n if (!rateLimit(ip, 100, 60000)) {\n throw createError({ statusCode: 429, statusMessage: 'Too Many Requests' })\n }\n\n // ... Handle request\n})\n",[37,6559,6560,6565,6587,6612,6616,6640,6660,6664,6668,6673],{"__ignoreMap":64},[68,6561,6562],{"class":70,"line":71},[68,6563,6564],{"class":325},"// In your API route\n",[68,6566,6567,6569,6571,6573,6575,6577,6579,6581,6583,6585],{"class":70,"line":77},[68,6568,4892],{"class":331},[68,6570,4895],{"class":331},[68,6572,4525],{"class":338},[68,6574,343],{"class":342},[68,6576,332],{"class":331},[68,6578,2657],{"class":342},[68,6580,4534],{"class":346},[68,6582,1869],{"class":342},[68,6584,1617],{"class":331},[68,6586,1620],{"class":342},[68,6588,6589,6591,6594,6596,6599,6602,6604,6607,6609],{"class":70,"line":83},[68,6590,376],{"class":331},[68,6592,6593],{"class":353}," ip",[68,6595,382],{"class":331},[68,6597,6598],{"class":338}," getRequestIP",[68,6600,6601],{"class":342},"(event, { xForwardedFor: ",[68,6603,3619],{"class":353},[68,6605,6606],{"class":342}," }) ",[68,6608,4213],{"class":331},[68,6610,6611],{"class":409}," 'unknown'\n",[68,6613,6614],{"class":70,"line":89},[68,6615,123],{"emptyLinePlaceholder":122},[68,6617,6618,6620,6622,6624,6627,6630,6632,6634,6637],{"class":70,"line":95},[68,6619,400],{"class":331},[68,6621,2657],{"class":342},[68,6623,3428],{"class":331},[68,6625,6626],{"class":338},"rateLimit",[68,6628,6629],{"class":342},"(ip, ",[68,6631,5122],{"class":353},[68,6633,180],{"class":342},[68,6635,6636],{"class":353},"60000",[68,6638,6639],{"class":342},")) {\n",[68,6641,6642,6644,6646,6649,6652,6655,6658],{"class":70,"line":101},[68,6643,5281],{"class":331},[68,6645,4842],{"class":338},[68,6647,6648],{"class":342},"({ statusCode: ",[68,6650,6651],{"class":353},"429",[68,6653,6654],{"class":342},", statusMessage: ",[68,6656,6657],{"class":409},"'Too Many Requests'",[68,6659,1859],{"class":342},[68,6661,6662],{"class":70,"line":107},[68,6663,429],{"class":342},[68,6665,6666],{"class":70,"line":113},[68,6667,123],{"emptyLinePlaceholder":122},[68,6669,6670],{"class":70,"line":119},[68,6671,6672],{"class":325}," // ... Handle request\n",[68,6674,6675],{"class":70,"line":126},[68,6676,2789],{"class":342},[15,6678,6680],{"id":6679},"caching-responses","Caching Responses",[20,6682,6683],{},"Nitro has built-in caching utilities for expensive operations:",[59,6685,6687],{"className":316,"code":6686,"language":318,"meta":64,"style":64},"// server/api/stats.get.ts\nexport default defineCachedEventHandler(\n async (event) => {\n // This function runs at most once per minute\n const stats = await computeExpensiveStats()\n return stats\n },\n {\n maxAge: 60, // seconds\n name: 'site-stats',\n group: 'api',\n }\n)\n",[37,6688,6689,6694,6705,6720,6725,6741,6748,6752,6756,6769,6778,6788,6792],{"__ignoreMap":64},[68,6690,6691],{"class":70,"line":71},[68,6692,6693],{"class":325},"// server/api/stats.get.ts\n",[68,6695,6696,6698,6700,6703],{"class":70,"line":77},[68,6697,4892],{"class":331},[68,6699,4895],{"class":331},[68,6701,6702],{"class":338}," defineCachedEventHandler",[68,6704,2488],{"class":342},[68,6706,6707,6710,6712,6714,6716,6718],{"class":70,"line":83},[68,6708,6709],{"class":331}," async",[68,6711,2657],{"class":342},[68,6713,4534],{"class":346},[68,6715,1869],{"class":342},[68,6717,1617],{"class":331},[68,6719,1620],{"class":342},[68,6721,6722],{"class":70,"line":89},[68,6723,6724],{"class":325}," // This function runs at most once per minute\n",[68,6726,6727,6729,6732,6734,6736,6739],{"class":70,"line":95},[68,6728,376],{"class":331},[68,6730,6731],{"class":353}," stats",[68,6733,382],{"class":331},[68,6735,385],{"class":331},[68,6737,6738],{"class":338}," computeExpensiveStats",[68,6740,1602],{"class":342},[68,6742,6743,6745],{"class":70,"line":101},[68,6744,418],{"class":331},[68,6746,6747],{"class":342}," stats\n",[68,6749,6750],{"class":70,"line":107},[68,6751,3893],{"class":342},[68,6753,6754],{"class":70,"line":113},[68,6755,1620],{"class":342},[68,6757,6758,6761,6764,6766],{"class":70,"line":119},[68,6759,6760],{"class":342}," maxAge: ",[68,6762,6763],{"class":353},"60",[68,6765,180],{"class":342},[68,6767,6768],{"class":325},"// seconds\n",[68,6770,6771,6773,6776],{"class":70,"line":126},[68,6772,4695],{"class":342},[68,6774,6775],{"class":409},"'site-stats'",[68,6777,2751],{"class":342},[68,6779,6780,6783,6786],{"class":70,"line":132},[68,6781,6782],{"class":342}," group: ",[68,6784,6785],{"class":409},"'api'",[68,6787,2751],{"class":342},[68,6789,6790],{"class":70,"line":2135},[68,6791,429],{"class":342},[68,6793,6794],{"class":70,"line":2141},[68,6795,1702],{"class":342},[20,6797,6798,6799,350],{},"For fine-grained cache control, use ",[37,6800,6801],{},"useStorage",[59,6803,6805],{"className":316,"code":6804,"language":318,"meta":64,"style":64},"const cache = useStorage('cache')\nconst cached = await cache.getItem('my-key')\nif (cached) return cached\n\nConst fresh = await fetchFreshData()\nawait cache.setItem('my-key', fresh, { ttl: 300 })\nreturn fresh\n",[37,6806,6807,6825,6848,6862,6866,6880,6902],{"__ignoreMap":64},[68,6808,6809,6811,6813,6815,6818,6820,6823],{"class":70,"line":71},[68,6810,1991],{"class":331},[68,6812,3392],{"class":353},[68,6814,382],{"class":331},[68,6816,6817],{"class":338}," useStorage",[68,6819,343],{"class":342},[68,6821,6822],{"class":409},"'cache'",[68,6824,1702],{"class":342},[68,6826,6827,6829,6832,6834,6836,6838,6841,6843,6846],{"class":70,"line":77},[68,6828,1991],{"class":331},[68,6830,6831],{"class":353}," cached",[68,6833,382],{"class":331},[68,6835,385],{"class":331},[68,6837,3442],{"class":342},[68,6839,6840],{"class":338},"getItem",[68,6842,343],{"class":342},[68,6844,6845],{"class":409},"'my-key'",[68,6847,1702],{"class":342},[68,6849,6850,6853,6856,6859],{"class":70,"line":83},[68,6851,6852],{"class":331},"if",[68,6854,6855],{"class":342}," (cached) ",[68,6857,6858],{"class":331},"return",[68,6860,6861],{"class":342}," cached\n",[68,6863,6864],{"class":70,"line":89},[68,6865,123],{"emptyLinePlaceholder":122},[68,6867,6868,6871,6873,6875,6878],{"class":70,"line":95},[68,6869,6870],{"class":342},"Const fresh ",[68,6872,1593],{"class":331},[68,6874,385],{"class":331},[68,6876,6877],{"class":338}," fetchFreshData",[68,6879,1602],{"class":342},[68,6881,6882,6885,6887,6890,6892,6894,6897,6900],{"class":70,"line":101},[68,6883,6884],{"class":331},"await",[68,6886,3442],{"class":342},[68,6888,6889],{"class":338},"setItem",[68,6891,343],{"class":342},[68,6893,6845],{"class":409},[68,6895,6896],{"class":342},", fresh, { ttl: ",[68,6898,6899],{"class":353},"300",[68,6901,1859],{"class":342},[68,6903,6904,6906],{"class":70,"line":107},[68,6905,6858],{"class":331},[68,6907,6908],{"class":342}," fresh\n",[15,6910,6912],{"id":6911},"testing-your-api-routes","Testing Your API Routes",[20,6914,6915,6916,6919],{},"Use Vitest with ",[37,6917,6918],{},"@nuxt/test-utils"," for API route tests:",[59,6921,6923],{"className":316,"code":6922,"language":318,"meta":64,"style":64},"import { describe, it, expect } from 'vitest'\nimport { setup, $fetch } from '@nuxt/test-utils'\n\nDescribe('Users API', async () => {\n await setup({ server: true })\n\n it('returns paginated users', async () => {\n const result = await $fetch('/api/users')\n expect(result).toHaveProperty('data')\n expect(result).toHaveProperty('pagination')\n expect(Array.isArray(result.data)).toBe(true)\n })\n\n it('returns 422 for invalid user creation', async () => {\n await expect(\n $fetch('/api/users', {\n method: 'POST',\n body: { email: 'not-an-email' },\n })\n ).rejects.toMatchObject({ status: 422 })\n })\n})\n",[37,6924,6925,6937,6949,6953,6974,6988,6992,7012,7032,7049,7064,7086,7090,7094,7113,7121,7131,7141,7151,7155,7170,7174],{"__ignoreMap":64},[68,6926,6927,6929,6932,6934],{"class":70,"line":71},[68,6928,2037],{"class":331},[68,6930,6931],{"class":342}," { describe, it, expect } ",[68,6933,2043],{"class":331},[68,6935,6936],{"class":409}," 'vitest'\n",[68,6938,6939,6941,6944,6946],{"class":70,"line":77},[68,6940,2037],{"class":331},[68,6942,6943],{"class":342}," { setup, $fetch } ",[68,6945,2043],{"class":331},[68,6947,6948],{"class":409}," '@nuxt/test-utils'\n",[68,6950,6951],{"class":70,"line":83},[68,6952,123],{"emptyLinePlaceholder":122},[68,6954,6955,6958,6960,6963,6965,6967,6970,6972],{"class":70,"line":89},[68,6956,6957],{"class":338},"Describe",[68,6959,343],{"class":342},[68,6961,6962],{"class":409},"'Users API'",[68,6964,180],{"class":342},[68,6966,332],{"class":331},[68,6968,6969],{"class":342}," () ",[68,6971,1617],{"class":331},[68,6973,1620],{"class":342},[68,6975,6976,6978,6981,6984,6986],{"class":70,"line":95},[68,6977,385],{"class":331},[68,6979,6980],{"class":338}," setup",[68,6982,6983],{"class":342},"({ server: ",[68,6985,3619],{"class":353},[68,6987,1859],{"class":342},[68,6989,6990],{"class":70,"line":101},[68,6991,123],{"emptyLinePlaceholder":122},[68,6993,6994,6997,6999,7002,7004,7006,7008,7010],{"class":70,"line":107},[68,6995,6996],{"class":338}," it",[68,6998,343],{"class":342},[68,7000,7001],{"class":409},"'returns paginated users'",[68,7003,180],{"class":342},[68,7005,332],{"class":331},[68,7007,6969],{"class":342},[68,7009,1617],{"class":331},[68,7011,1620],{"class":342},[68,7013,7014,7016,7018,7020,7022,7025,7027,7030],{"class":70,"line":113},[68,7015,376],{"class":331},[68,7017,2374],{"class":353},[68,7019,382],{"class":331},[68,7021,385],{"class":331},[68,7023,7024],{"class":338}," $fetch",[68,7026,343],{"class":342},[68,7028,7029],{"class":409},"'/api/users'",[68,7031,1702],{"class":342},[68,7033,7034,7037,7040,7043,7045,7047],{"class":70,"line":119},[68,7035,7036],{"class":338}," expect",[68,7038,7039],{"class":342},"(result).",[68,7041,7042],{"class":338},"toHaveProperty",[68,7044,343],{"class":342},[68,7046,3218],{"class":409},[68,7048,1702],{"class":342},[68,7050,7051,7053,7055,7057,7059,7062],{"class":70,"line":126},[68,7052,7036],{"class":338},[68,7054,7039],{"class":342},[68,7056,7042],{"class":338},[68,7058,343],{"class":342},[68,7060,7061],{"class":409},"'pagination'",[68,7063,1702],{"class":342},[68,7065,7066,7068,7071,7074,7077,7080,7082,7084],{"class":70,"line":132},[68,7067,7036],{"class":338},[68,7069,7070],{"class":342},"(Array.",[68,7072,7073],{"class":338},"isArray",[68,7075,7076],{"class":342},"(result.data)).",[68,7078,7079],{"class":338},"toBe",[68,7081,343],{"class":342},[68,7083,3619],{"class":353},[68,7085,1702],{"class":342},[68,7087,7088],{"class":70,"line":2135},[68,7089,1859],{"class":342},[68,7091,7092],{"class":70,"line":2141},[68,7093,123],{"emptyLinePlaceholder":122},[68,7095,7096,7098,7100,7103,7105,7107,7109,7111],{"class":70,"line":2437},[68,7097,6996],{"class":338},[68,7099,343],{"class":342},[68,7101,7102],{"class":409},"'returns 422 for invalid user creation'",[68,7104,180],{"class":342},[68,7106,332],{"class":331},[68,7108,6969],{"class":342},[68,7110,1617],{"class":331},[68,7112,1620],{"class":342},[68,7114,7115,7117,7119],{"class":70,"line":2442},[68,7116,385],{"class":331},[68,7118,7036],{"class":338},[68,7120,2488],{"class":342},[68,7122,7123,7125,7127,7129],{"class":70,"line":2447},[68,7124,7024],{"class":338},[68,7126,343],{"class":342},[68,7128,7029],{"class":409},[68,7130,2562],{"class":342},[68,7132,7133,7136,7139],{"class":70,"line":2652},[68,7134,7135],{"class":342}," method: ",[68,7137,7138],{"class":409},"'POST'",[68,7140,2751],{"class":342},[68,7142,7143,7146,7149],{"class":70,"line":2687},[68,7144,7145],{"class":342}," body: { email: ",[68,7147,7148],{"class":409},"'not-an-email'",[68,7150,3893],{"class":342},[68,7152,7153],{"class":70,"line":2692},[68,7154,1859],{"class":342},[68,7156,7157,7160,7163,7166,7168],{"class":70,"line":2697},[68,7158,7159],{"class":342}," ).rejects.",[68,7161,7162],{"class":338},"toMatchObject",[68,7164,7165],{"class":342},"({ status: ",[68,7167,5292],{"class":353},[68,7169,1859],{"class":342},[68,7171,7172],{"class":70,"line":3129},[68,7173,1859],{"class":342},[68,7175,7176],{"class":70,"line":3134},[68,7177,2789],{"class":342},[20,7179,7180],{},"Nitro makes building a backend alongside your Nuxt frontend genuinely pleasant. The file-based routing is intuitive, the TypeScript integration is excellent, and the deployment story is clean — one codebase, one build, one deployment. For projects that do not need a separate dedicated backend, this is a compelling architecture.",[509,7182],{},[20,7184,7185,7186,51],{},"Building a full-stack Nuxt application and want help designing your API architecture or database schema? Let's talk through it: ",[502,7187,817],{"href":504,"rel":7188},[506],[509,7190],{},[15,7192,514],{"id":513},[516,7194,7195,7199,7203,7207],{},[519,7196,7197],{},[502,7198,4402],{"href":4401},[519,7200,7201],{},[502,7202,4408],{"href":4407},[519,7204,7205],{},[502,7206,4020],{"href":4019},[519,7208,7209],{},[502,7210,7212],{"href":7211},"/blog/api-rate-limiting","API Rate Limiting: Protecting Your Services Without Hurting Your Users",[544,7214,7215],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":64,"searchDepth":83,"depth":83,"links":7217},[7218,7219,7220,7221,7222,7223,7224,7225,7226,7227,7228],{"id":4459,"depth":77,"text":4460},{"id":4487,"depth":77,"text":4488},{"id":4876,"depth":77,"text":4877},{"id":5052,"depth":77,"text":5053},{"id":5600,"depth":77,"text":5601},{"id":5910,"depth":77,"text":5911},{"id":6095,"depth":77,"text":6096},{"id":6319,"depth":77,"text":6320},{"id":6679,"depth":77,"text":6680},{"id":6911,"depth":77,"text":6912},{"id":513,"depth":77,"text":514},"A practical guide to Nuxt server routes powered by Nitro — file-based routing, middleware, validation, database access, and deploying a full-stack application from one codebase.",[7231,7232],"Nuxt API routes","Nitro server",{},"/blog/nuxt-api-routes-nitro",{"title":4444,"description":7229},"blog/nuxt-api-routes-nitro",[4438,7238,4063],"Nitro","ox9Y9mV9MnyE4uEFG4bQPCOEsyjfSyDWWaN-ccVRYKs",{"id":7241,"title":4396,"author":7242,"body":7243,"category":557,"date":558,"description":9093,"extension":560,"featured":561,"image":562,"keywords":9094,"meta":9097,"navigation":122,"path":4395,"readTime":107,"seo":9098,"stem":9099,"tags":9100,"__hash__":9103},"blog/blog/nuxt-authentication-guide.md",{"name":9,"bio":10},{"type":12,"value":7244,"toc":9082},[7245,7248,7251,7255,7258,7264,7270,7273,7288,7291,7295,7302,7318,7321,7556,7559,7625,7629,7632,7750,7753,7816,7822,7851,7854,7906,7910,7913,7916,8125,8128,8246,8300,8304,8307,8441,8444,8514,8518,8521,8865,8869,8872,9010,9014,9017,9040,9043,9045,9051,9053,9055,9079],[20,7246,7247],{},"Authentication is one of those topics where bad advice is everywhere and the consequences of getting it wrong are severe. I have seen production Nuxt applications storing JWTs in localStorage (vulnerable to XSS), using client-side route guards as the only protection (trivially bypassable), and implementing custom session management that had subtle security holes.",[20,7249,7250],{},"This guide is about building authentication correctly. Not the fastest approach, not the simplest demo — the patterns that are actually secure and maintainable in production.",[15,7252,7254],{"id":7253},"the-core-decision-sessions-vs-jwts","The Core Decision: Sessions vs JWTs",[20,7256,7257],{},"Before writing any code, you need to make this architectural decision clearly, because it affects everything downstream.",[20,7259,7260,7263],{},[55,7261,7262],{},"HTTP-only cookie sessions"," store the session identifier in a cookie that JavaScript cannot read. The session data lives on the server (in a database or Redis). This is the approach web applications used for decades and it remains the most secure option for most applications.",[20,7265,7266,7269],{},[55,7267,7268],{},"JWTs (JSON Web Tokens)"," are self-contained tokens that encode session data. They are typically stored in memory or localStorage. The appeal is statelessness — the server does not need to look up session data on every request.",[20,7271,7272],{},"My recommendation: use sessions with HTTP-only cookies for most Nuxt applications. Here is why:",[516,7274,7275,7282,7285],{},[519,7276,7277,7278,7281],{},"HTTP-only cookies cannot be stolen by XSS attacks. A ",[37,7279,7280],{},"localStorage"," JWT can be.",[519,7283,7284],{},"Session invalidation is immediate. To invalidate a JWT you need a blocklist, which eliminates the statelessness benefit.",[519,7286,7287],{},"Session data can grow without affecting the token size. JWTs are sent on every request — large JWTs have real performance cost.",[20,7289,7290],{},"The JWT case is legitimate when: you have multiple backend services that need to verify identity without database calls, you are building a public API where the clients are not browsers, or you are using a third-party auth provider that issues JWTs.",[15,7292,7294],{"id":7293},"using-better-auth","Using better-auth",[20,7296,7297,7298,7301],{},"I have standardized on ",[37,7299,7300],{},"better-auth"," for Nuxt applications. It handles the session management correctly, supports multiple OAuth providers, and integrates cleanly with Prisma. The name is apt — it is a meaningfully better solution than rolling your own.",[59,7303,7305],{"className":1888,"code":7304,"language":1890,"meta":64,"style":64},"npm install better-auth\n",[37,7306,7307],{"__ignoreMap":64},[68,7308,7309,7312,7315],{"class":70,"line":71},[68,7310,7311],{"class":338},"npm",[68,7313,7314],{"class":409}," install",[68,7316,7317],{"class":409}," better-auth\n",[20,7319,7320],{},"Configure it with your database adapter:",[59,7322,7324],{"className":316,"code":7323,"language":318,"meta":64,"style":64},"// server/lib/auth.ts\nimport { betterAuth } from 'better-auth'\nimport { prismaAdapter } from 'better-auth/adapters/prisma'\nimport { prisma } from './prisma'\n\nExport const auth = betterAuth({\n database: prismaAdapter(prisma, {\n provider: 'postgresql',\n }),\n session: {\n cookieCache: {\n enabled: true,\n maxAge: 60 * 5, // 5 minutes\n },\n },\n emailAndPassword: {\n enabled: true,\n requireEmailVerification: true,\n },\n socialProviders: {\n github: {\n clientId: process.env.GITHUB_CLIENT_ID!,\n clientSecret: process.env.GITHUB_CLIENT_SECRET!,\n },\n google: {\n clientId: process.env.GOOGLE_CLIENT_ID!,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n },\n },\n})\n",[37,7325,7326,7331,7343,7355,7366,7370,7386,7397,7407,7411,7416,7421,7430,7445,7449,7453,7458,7466,7475,7479,7484,7489,7501,7513,7517,7522,7533,7544,7548,7552],{"__ignoreMap":64},[68,7327,7328],{"class":70,"line":71},[68,7329,7330],{"class":325},"// server/lib/auth.ts\n",[68,7332,7333,7335,7338,7340],{"class":70,"line":77},[68,7334,2037],{"class":331},[68,7336,7337],{"class":342}," { betterAuth } ",[68,7339,2043],{"class":331},[68,7341,7342],{"class":409}," 'better-auth'\n",[68,7344,7345,7347,7350,7352],{"class":70,"line":83},[68,7346,2037],{"class":331},[68,7348,7349],{"class":342}," { prismaAdapter } ",[68,7351,2043],{"class":331},[68,7353,7354],{"class":409}," 'better-auth/adapters/prisma'\n",[68,7356,7357,7359,7361,7363],{"class":70,"line":89},[68,7358,2037],{"class":331},[68,7360,4505],{"class":342},[68,7362,2043],{"class":331},[68,7364,7365],{"class":409}," './prisma'\n",[68,7367,7368],{"class":70,"line":95},[68,7369,123],{"emptyLinePlaceholder":122},[68,7371,7372,7374,7376,7379,7381,7384],{"class":70,"line":101},[68,7373,4519],{"class":342},[68,7375,1991],{"class":331},[68,7377,7378],{"class":353}," auth",[68,7380,382],{"class":331},[68,7382,7383],{"class":338}," betterAuth",[68,7385,1789],{"class":342},[68,7387,7388,7391,7394],{"class":70,"line":107},[68,7389,7390],{"class":342}," database: ",[68,7392,7393],{"class":338},"prismaAdapter",[68,7395,7396],{"class":342},"(prisma, {\n",[68,7398,7399,7402,7405],{"class":70,"line":113},[68,7400,7401],{"class":342}," provider: ",[68,7403,7404],{"class":409},"'postgresql'",[68,7406,2751],{"class":342},[68,7408,7409],{"class":70,"line":119},[68,7410,4736],{"class":342},[68,7412,7413],{"class":70,"line":126},[68,7414,7415],{"class":342}," session: {\n",[68,7417,7418],{"class":70,"line":132},[68,7419,7420],{"class":342}," cookieCache: {\n",[68,7422,7423,7426,7428],{"class":70,"line":2135},[68,7424,7425],{"class":342}," enabled: ",[68,7427,3619],{"class":353},[68,7429,2751],{"class":342},[68,7431,7432,7434,7436,7438,7440,7442],{"class":70,"line":2141},[68,7433,6760],{"class":342},[68,7435,6763],{"class":353},[68,7437,3520],{"class":331},[68,7439,3528],{"class":353},[68,7441,180],{"class":342},[68,7443,7444],{"class":325},"// 5 minutes\n",[68,7446,7447],{"class":70,"line":2437},[68,7448,3893],{"class":342},[68,7450,7451],{"class":70,"line":2442},[68,7452,3893],{"class":342},[68,7454,7455],{"class":70,"line":2447},[68,7456,7457],{"class":342}," emailAndPassword: {\n",[68,7459,7460,7462,7464],{"class":70,"line":2652},[68,7461,7425],{"class":342},[68,7463,3619],{"class":353},[68,7465,2751],{"class":342},[68,7467,7468,7471,7473],{"class":70,"line":2687},[68,7469,7470],{"class":342}," requireEmailVerification: ",[68,7472,3619],{"class":353},[68,7474,2751],{"class":342},[68,7476,7477],{"class":70,"line":2692},[68,7478,3893],{"class":342},[68,7480,7481],{"class":70,"line":2697},[68,7482,7483],{"class":342}," socialProviders: {\n",[68,7485,7486],{"class":70,"line":3129},[68,7487,7488],{"class":342}," github: {\n",[68,7490,7491,7494,7497,7499],{"class":70,"line":3134},[68,7492,7493],{"class":342}," clientId: process.env.",[68,7495,7496],{"class":353},"GITHUB_CLIENT_ID",[68,7498,3428],{"class":331},[68,7500,2751],{"class":342},[68,7502,7503,7506,7509,7511],{"class":70,"line":4749},[68,7504,7505],{"class":342}," clientSecret: process.env.",[68,7507,7508],{"class":353},"GITHUB_CLIENT_SECRET",[68,7510,3428],{"class":331},[68,7512,2751],{"class":342},[68,7514,7515],{"class":70,"line":4755},[68,7516,3893],{"class":342},[68,7518,7519],{"class":70,"line":4760},[68,7520,7521],{"class":342}," google: {\n",[68,7523,7524,7526,7529,7531],{"class":70,"line":4767},[68,7525,7493],{"class":342},[68,7527,7528],{"class":353},"GOOGLE_CLIENT_ID",[68,7530,3428],{"class":331},[68,7532,2751],{"class":342},[68,7534,7535,7537,7540,7542],{"class":70,"line":4773},[68,7536,7505],{"class":342},[68,7538,7539],{"class":353},"GOOGLE_CLIENT_SECRET",[68,7541,3428],{"class":331},[68,7543,2751],{"class":342},[68,7545,7546],{"class":70,"line":4779},[68,7547,3893],{"class":342},[68,7549,7550],{"class":70,"line":4785},[68,7551,3893],{"class":342},[68,7553,7554],{"class":70,"line":4791},[68,7555,2789],{"class":342},[20,7557,7558],{},"Create the catch-all API route:",[59,7560,7562],{"className":316,"code":7561,"language":318,"meta":64,"style":64},"// server/api/auth/[...all].ts\nimport { auth } from '~/server/lib/auth'\n\nExport default defineEventHandler((event) => {\n return auth.handler(toWebRequest(event))\n})\n",[37,7563,7564,7569,7581,7585,7603,7621],{"__ignoreMap":64},[68,7565,7566],{"class":70,"line":71},[68,7567,7568],{"class":325},"// server/api/auth/[...all].ts\n",[68,7570,7571,7573,7576,7578],{"class":70,"line":77},[68,7572,2037],{"class":331},[68,7574,7575],{"class":342}," { auth } ",[68,7577,2043],{"class":331},[68,7579,7580],{"class":409}," '~/server/lib/auth'\n",[68,7582,7583],{"class":70,"line":83},[68,7584,123],{"emptyLinePlaceholder":122},[68,7586,7587,7589,7591,7593,7595,7597,7599,7601],{"class":70,"line":89},[68,7588,4519],{"class":342},[68,7590,4522],{"class":331},[68,7592,4525],{"class":338},[68,7594,2525],{"class":342},[68,7596,4534],{"class":346},[68,7598,1869],{"class":342},[68,7600,1617],{"class":331},[68,7602,1620],{"class":342},[68,7604,7605,7607,7610,7613,7615,7618],{"class":70,"line":95},[68,7606,418],{"class":331},[68,7608,7609],{"class":342}," auth.",[68,7611,7612],{"class":338},"handler",[68,7614,343],{"class":342},[68,7616,7617],{"class":338},"toWebRequest",[68,7619,7620],{"class":342},"(event))\n",[68,7622,7623],{"class":70,"line":101},[68,7624,2789],{"class":342},[15,7626,7628],{"id":7627},"protecting-routes-with-middleware","Protecting Routes With Middleware",[20,7630,7631],{},"Nuxt middleware runs before route navigation. Use it to protect authenticated routes:",[59,7633,7635],{"className":316,"code":7634,"language":318,"meta":64,"style":64},"// middleware/auth.ts\nexport default defineNuxtRouteMiddleware(async (to) => {\n const { data: session } = await useAuth()\n\n if (!session.value && to.path !== '/login') {\n return navigateTo(`/login?redirect=${to.path}`)\n }\n})\n",[37,7636,7637,7642,7666,7690,7694,7719,7742,7746],{"__ignoreMap":64},[68,7638,7639],{"class":70,"line":71},[68,7640,7641],{"class":325},"// middleware/auth.ts\n",[68,7643,7644,7646,7648,7651,7653,7655,7657,7660,7662,7664],{"class":70,"line":77},[68,7645,4892],{"class":331},[68,7647,4895],{"class":331},[68,7649,7650],{"class":338}," defineNuxtRouteMiddleware",[68,7652,343],{"class":342},[68,7654,332],{"class":331},[68,7656,2657],{"class":342},[68,7658,7659],{"class":346},"to",[68,7661,1869],{"class":342},[68,7663,1617],{"class":331},[68,7665,1620],{"class":342},[68,7667,7668,7670,7672,7674,7676,7679,7681,7683,7685,7688],{"class":70,"line":83},[68,7669,376],{"class":331},[68,7671,1748],{"class":342},[68,7673,4157],{"class":346},[68,7675,938],{"class":342},[68,7677,7678],{"class":353},"session",[68,7680,1769],{"class":342},[68,7682,1593],{"class":331},[68,7684,385],{"class":331},[68,7686,7687],{"class":338}," useAuth",[68,7689,1602],{"class":342},[68,7691,7692],{"class":70,"line":89},[68,7693,123],{"emptyLinePlaceholder":122},[68,7695,7696,7698,7700,7702,7705,7708,7711,7714,7717],{"class":70,"line":95},[68,7697,400],{"class":331},[68,7699,2657],{"class":342},[68,7701,3428],{"class":331},[68,7703,7704],{"class":342},"session.value ",[68,7706,7707],{"class":331},"&&",[68,7709,7710],{"class":342}," to.path ",[68,7712,7713],{"class":331},"!==",[68,7715,7716],{"class":409}," '/login'",[68,7718,413],{"class":342},[68,7720,7721,7723,7726,7728,7731,7733,7735,7738,7740],{"class":70,"line":101},[68,7722,418],{"class":331},[68,7724,7725],{"class":338}," navigateTo",[68,7727,343],{"class":342},[68,7729,7730],{"class":409},"`/login?redirect=${",[68,7732,7659],{"class":342},[68,7734,51],{"class":409},[68,7736,7737],{"class":342},"path",[68,7739,2682],{"class":409},[68,7741,1702],{"class":342},[68,7743,7744],{"class":70,"line":107},[68,7745,429],{"class":342},[68,7747,7748],{"class":70,"line":113},[68,7749,2789],{"class":342},[20,7751,7752],{},"Apply it to protected pages:",[59,7754,7758],{"className":7755,"code":7756,"language":7757,"meta":64,"style":64},"language-vue shiki shiki-themes github-dark","\u003C!-- pages/dashboard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n middleware: 'auth',\n})\n\u003C/script>\n","vue",[37,7759,7760,7765,7786,7793,7803,7807],{"__ignoreMap":64},[68,7761,7762],{"class":70,"line":71},[68,7763,7764],{"class":325},"\u003C!-- pages/dashboard.vue -->\n",[68,7766,7767,7769,7773,7775,7778,7780,7783],{"class":70,"line":77},[68,7768,365],{"class":342},[68,7770,7772],{"class":7771},"s4JwU","script",[68,7774,6980],{"class":338},[68,7776,7777],{"class":338}," lang",[68,7779,1593],{"class":342},[68,7781,7782],{"class":409},"\"ts\"",[68,7784,7785],{"class":342},">\n",[68,7787,7788,7791],{"class":70,"line":83},[68,7789,7790],{"class":338},"definePageMeta",[68,7792,1789],{"class":342},[68,7794,7795,7798,7801],{"class":70,"line":89},[68,7796,7797],{"class":342}," middleware: ",[68,7799,7800],{"class":409},"'auth'",[68,7802,2751],{"class":342},[68,7804,7805],{"class":70,"line":95},[68,7806,2789],{"class":342},[68,7808,7809,7812,7814],{"class":70,"line":101},[68,7810,7811],{"class":342},"\u003C/",[68,7813,7772],{"class":7771},[68,7815,7785],{"class":342},[20,7817,7818,7819,350],{},"Or apply it globally in ",[37,7820,7821],{},"nuxt.config.ts",[59,7823,7825],{"className":316,"code":7824,"language":318,"meta":64,"style":64},"router: {\n middleware: ['auth'],\n}\n",[37,7826,7827,7835,7847],{"__ignoreMap":64},[68,7828,7829,7832],{"class":70,"line":71},[68,7830,7831],{"class":338},"router",[68,7833,7834],{"class":342},": {\n",[68,7836,7837,7840,7843,7845],{"class":70,"line":77},[68,7838,7839],{"class":338}," middleware",[68,7841,7842],{"class":342},": [",[68,7844,7800],{"class":409},[68,7846,6051],{"class":342},[68,7848,7849],{"class":70,"line":83},[68,7850,447],{"class":342},[20,7852,7853],{},"With the global approach, opt specific public pages out:",[59,7855,7857],{"className":7755,"code":7856,"language":7757,"meta":64,"style":64},"\u003C!-- pages/index.vue -->\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n middleware: [], // Override: no auth required\n})\n\u003C/script>\n",[37,7858,7859,7864,7880,7886,7894,7898],{"__ignoreMap":64},[68,7860,7861],{"class":70,"line":71},[68,7862,7863],{"class":325},"\u003C!-- pages/index.vue -->\n",[68,7865,7866,7868,7870,7872,7874,7876,7878],{"class":70,"line":77},[68,7867,365],{"class":342},[68,7869,7772],{"class":7771},[68,7871,6980],{"class":338},[68,7873,7777],{"class":338},[68,7875,1593],{"class":342},[68,7877,7782],{"class":409},[68,7879,7785],{"class":342},[68,7881,7882,7884],{"class":70,"line":83},[68,7883,7790],{"class":338},[68,7885,1789],{"class":342},[68,7887,7888,7891],{"class":70,"line":89},[68,7889,7890],{"class":342}," middleware: [], ",[68,7892,7893],{"class":325},"// Override: no auth required\n",[68,7895,7896],{"class":70,"line":95},[68,7897,2789],{"class":342},[68,7899,7900,7902,7904],{"class":70,"line":101},[68,7901,7811],{"class":342},[68,7903,7772],{"class":7771},[68,7905,7785],{"class":342},[15,7907,7909],{"id":7908},"server-side-route-protection","Server-Side Route Protection",[20,7911,7912],{},"This is the part most tutorials skip. Client-side middleware is a user experience enhancement, not a security boundary. A determined user can disable JavaScript and bypass client-side middleware entirely.",[20,7914,7915],{},"Every API route and server-rendered page that contains private data must verify authentication on the server:",[59,7917,7919],{"className":316,"code":7918,"language":318,"meta":64,"style":64},"// server/api/user/profile.get.ts\nimport { auth } from '~/server/lib/auth'\n\nExport default defineEventHandler(async (event) => {\n const session = await auth.api.getSession({\n headers: event.headers,\n })\n\n if (!session) {\n throw createError({\n statusCode: 401,\n statusMessage: 'Unauthorized',\n })\n }\n\n const profile = await prisma.user.findUnique({\n where: { id: session.user.id },\n select: {\n id: true,\n name: true,\n email: true,\n createdAt: true,\n },\n })\n\n return profile\n})\n",[37,7920,7921,7926,7936,7940,7962,7981,7986,7990,7994,8005,8013,8022,8031,8035,8039,8043,8061,8066,8070,8078,8086,8094,8102,8106,8110,8114,8121],{"__ignoreMap":64},[68,7922,7923],{"class":70,"line":71},[68,7924,7925],{"class":325},"// server/api/user/profile.get.ts\n",[68,7927,7928,7930,7932,7934],{"class":70,"line":77},[68,7929,2037],{"class":331},[68,7931,7575],{"class":342},[68,7933,2043],{"class":331},[68,7935,7580],{"class":409},[68,7937,7938],{"class":70,"line":83},[68,7939,123],{"emptyLinePlaceholder":122},[68,7941,7942,7944,7946,7948,7950,7952,7954,7956,7958,7960],{"class":70,"line":89},[68,7943,4519],{"class":342},[68,7945,4522],{"class":331},[68,7947,4525],{"class":338},[68,7949,343],{"class":342},[68,7951,332],{"class":331},[68,7953,2657],{"class":342},[68,7955,4534],{"class":346},[68,7957,1869],{"class":342},[68,7959,1617],{"class":331},[68,7961,1620],{"class":342},[68,7963,7964,7966,7969,7971,7973,7976,7979],{"class":70,"line":95},[68,7965,376],{"class":331},[68,7967,7968],{"class":353}," session",[68,7970,382],{"class":331},[68,7972,385],{"class":331},[68,7974,7975],{"class":342}," auth.api.",[68,7977,7978],{"class":338},"getSession",[68,7980,1789],{"class":342},[68,7982,7983],{"class":70,"line":101},[68,7984,7985],{"class":342}," headers: event.headers,\n",[68,7987,7988],{"class":70,"line":107},[68,7989,1859],{"class":342},[68,7991,7992],{"class":70,"line":113},[68,7993,123],{"emptyLinePlaceholder":122},[68,7995,7996,7998,8000,8002],{"class":70,"line":119},[68,7997,400],{"class":331},[68,7999,2657],{"class":342},[68,8001,3428],{"class":331},[68,8003,8004],{"class":342},"session) {\n",[68,8006,8007,8009,8011],{"class":70,"line":126},[68,8008,5281],{"class":331},[68,8010,4842],{"class":338},[68,8012,1789],{"class":342},[68,8014,8015,8017,8020],{"class":70,"line":132},[68,8016,4849],{"class":342},[68,8018,8019],{"class":353},"401",[68,8021,2751],{"class":342},[68,8023,8024,8026,8029],{"class":70,"line":2135},[68,8025,4859],{"class":342},[68,8027,8028],{"class":409},"'Unauthorized'",[68,8030,2751],{"class":342},[68,8032,8033],{"class":70,"line":2141},[68,8034,1859],{"class":342},[68,8036,8037],{"class":70,"line":2437},[68,8038,429],{"class":342},[68,8040,8041],{"class":70,"line":2442},[68,8042,123],{"emptyLinePlaceholder":122},[68,8044,8045,8047,8050,8052,8054,8056,8059],{"class":70,"line":2447},[68,8046,376],{"class":331},[68,8048,8049],{"class":353}," profile",[68,8051,382],{"class":331},[68,8053,385],{"class":331},[68,8055,4661],{"class":342},[68,8057,8058],{"class":338},"findUnique",[68,8060,1789],{"class":342},[68,8062,8063],{"class":70,"line":2652},[68,8064,8065],{"class":342}," where: { id: session.user.id },\n",[68,8067,8068],{"class":70,"line":2687},[68,8069,4681],{"class":342},[68,8071,8072,8074,8076],{"class":70,"line":2692},[68,8073,4686],{"class":342},[68,8075,3619],{"class":353},[68,8077,2751],{"class":342},[68,8079,8080,8082,8084],{"class":70,"line":2697},[68,8081,4695],{"class":342},[68,8083,3619],{"class":353},[68,8085,2751],{"class":342},[68,8087,8088,8090,8092],{"class":70,"line":3129},[68,8089,4704],{"class":342},[68,8091,3619],{"class":353},[68,8093,2751],{"class":342},[68,8095,8096,8098,8100],{"class":70,"line":3134},[68,8097,4713],{"class":342},[68,8099,3619],{"class":353},[68,8101,2751],{"class":342},[68,8103,8104],{"class":70,"line":4749},[68,8105,3893],{"class":342},[68,8107,8108],{"class":70,"line":4755},[68,8109,1859],{"class":342},[68,8111,8112],{"class":70,"line":4760},[68,8113,123],{"emptyLinePlaceholder":122},[68,8115,8116,8118],{"class":70,"line":4767},[68,8117,418],{"class":331},[68,8119,8120],{"class":342}," profile\n",[68,8122,8123],{"class":70,"line":4773},[68,8124,2789],{"class":342},[20,8126,8127],{},"Create a utility to avoid repeating this check:",[59,8129,8131],{"className":316,"code":8130,"language":318,"meta":64,"style":64},"// server/utils/requireAuth.ts\nimport { auth } from '~/server/lib/auth'\n\nExport async function requireAuth(event: H3Event) {\n const session = await auth.api.getSession({\n headers: event.headers,\n })\n\n if (!session) {\n throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })\n }\n\n return session\n}\n",[37,8132,8133,8138,8148,8152,8173,8189,8193,8197,8201,8211,8227,8231,8235,8242],{"__ignoreMap":64},[68,8134,8135],{"class":70,"line":71},[68,8136,8137],{"class":325},"// server/utils/requireAuth.ts\n",[68,8139,8140,8142,8144,8146],{"class":70,"line":77},[68,8141,2037],{"class":331},[68,8143,7575],{"class":342},[68,8145,2043],{"class":331},[68,8147,7580],{"class":409},[68,8149,8150],{"class":70,"line":83},[68,8151,123],{"emptyLinePlaceholder":122},[68,8153,8154,8156,8158,8160,8163,8165,8167,8169,8171],{"class":70,"line":89},[68,8155,4519],{"class":342},[68,8157,332],{"class":331},[68,8159,335],{"class":331},[68,8161,8162],{"class":338}," requireAuth",[68,8164,343],{"class":342},[68,8166,4534],{"class":346},[68,8168,350],{"class":331},[68,8170,5418],{"class":338},[68,8172,413],{"class":342},[68,8174,8175,8177,8179,8181,8183,8185,8187],{"class":70,"line":95},[68,8176,376],{"class":331},[68,8178,7968],{"class":353},[68,8180,382],{"class":331},[68,8182,385],{"class":331},[68,8184,7975],{"class":342},[68,8186,7978],{"class":338},[68,8188,1789],{"class":342},[68,8190,8191],{"class":70,"line":101},[68,8192,7985],{"class":342},[68,8194,8195],{"class":70,"line":107},[68,8196,1859],{"class":342},[68,8198,8199],{"class":70,"line":113},[68,8200,123],{"emptyLinePlaceholder":122},[68,8202,8203,8205,8207,8209],{"class":70,"line":119},[68,8204,400],{"class":331},[68,8206,2657],{"class":342},[68,8208,3428],{"class":331},[68,8210,8004],{"class":342},[68,8212,8213,8215,8217,8219,8221,8223,8225],{"class":70,"line":126},[68,8214,5281],{"class":331},[68,8216,4842],{"class":338},[68,8218,6648],{"class":342},[68,8220,8019],{"class":353},[68,8222,6654],{"class":342},[68,8224,8028],{"class":409},[68,8226,1859],{"class":342},[68,8228,8229],{"class":70,"line":132},[68,8230,429],{"class":342},[68,8232,8233],{"class":70,"line":2135},[68,8234,123],{"emptyLinePlaceholder":122},[68,8236,8237,8239],{"class":70,"line":2141},[68,8238,418],{"class":331},[68,8240,8241],{"class":342}," session\n",[68,8243,8244],{"class":70,"line":2437},[68,8245,447],{"class":342},[59,8247,8249],{"className":316,"code":8248,"language":318,"meta":64,"style":64},"// server/api/user/profile.get.ts\nexport default defineEventHandler(async (event) => {\n const session = await requireAuth(event)\n // session.user is available here\n})\n",[37,8250,8251,8255,8277,8291,8296],{"__ignoreMap":64},[68,8252,8253],{"class":70,"line":71},[68,8254,7925],{"class":325},[68,8256,8257,8259,8261,8263,8265,8267,8269,8271,8273,8275],{"class":70,"line":77},[68,8258,4892],{"class":331},[68,8260,4895],{"class":331},[68,8262,4525],{"class":338},[68,8264,343],{"class":342},[68,8266,332],{"class":331},[68,8268,2657],{"class":342},[68,8270,4534],{"class":346},[68,8272,1869],{"class":342},[68,8274,1617],{"class":331},[68,8276,1620],{"class":342},[68,8278,8279,8281,8283,8285,8287,8289],{"class":70,"line":83},[68,8280,376],{"class":331},[68,8282,7968],{"class":353},[68,8284,382],{"class":331},[68,8286,385],{"class":331},[68,8288,8162],{"class":338},[68,8290,4555],{"class":342},[68,8292,8293],{"class":70,"line":89},[68,8294,8295],{"class":325}," // session.user is available here\n",[68,8297,8298],{"class":70,"line":95},[68,8299,2789],{"class":342},[15,8301,8303],{"id":8302},"role-based-access-control","Role-Based Access Control",[20,8305,8306],{},"For applications with multiple user roles (admin, editor, viewer), add a role check utility:",[59,8308,8310],{"className":316,"code":8309,"language":318,"meta":64,"style":64},"// server/utils/requireRole.ts\ntype Role = 'admin' | 'editor' | 'viewer'\n\nExport async function requireRole(event: H3Event, role: Role) {\n const session = await requireAuth(event)\n\n if (session.user.role !== role && session.user.role !== 'admin') {\n throw createError({ statusCode: 403, statusMessage: 'Forbidden' })\n }\n\n return session\n}\n",[37,8311,8312,8317,8337,8341,8371,8385,8389,8405,8423,8427,8431,8437],{"__ignoreMap":64},[68,8313,8314],{"class":70,"line":71},[68,8315,8316],{"class":325},"// server/utils/requireRole.ts\n",[68,8318,8319,8322,8325,8327,8329,8331,8333,8335],{"class":70,"line":77},[68,8320,8321],{"class":331},"type",[68,8323,8324],{"class":338}," Role",[68,8326,382],{"class":331},[68,8328,6159],{"class":409},[68,8330,5971],{"class":331},[68,8332,6164],{"class":409},[68,8334,5971],{"class":331},[68,8336,6169],{"class":409},[68,8338,8339],{"class":70,"line":83},[68,8340,123],{"emptyLinePlaceholder":122},[68,8342,8343,8346,8348,8350,8353,8355,8357,8359,8361,8363,8365,8367,8369],{"class":70,"line":89},[68,8344,8345],{"class":338},"Export",[68,8347,6709],{"class":338},[68,8349,335],{"class":338},[68,8351,8352],{"class":338}," requireRole",[68,8354,343],{"class":342},[68,8356,4534],{"class":346},[68,8358,350],{"class":331},[68,8360,5418],{"class":338},[68,8362,180],{"class":342},[68,8364,5348],{"class":346},[68,8366,350],{"class":331},[68,8368,8324],{"class":338},[68,8370,413],{"class":342},[68,8372,8373,8375,8377,8379,8381,8383],{"class":70,"line":95},[68,8374,376],{"class":338},[68,8376,7968],{"class":346},[68,8378,382],{"class":331},[68,8380,385],{"class":331},[68,8382,8162],{"class":338},[68,8384,4555],{"class":342},[68,8386,8387],{"class":70,"line":101},[68,8388,123],{"emptyLinePlaceholder":122},[68,8390,8391,8393,8396,8398,8401,8403],{"class":70,"line":107},[68,8392,400],{"class":338},[68,8394,8395],{"class":342}," (session.user.role !== ",[68,8397,5348],{"class":346},[68,8399,8400],{"class":342}," && session.user.role !== ",[68,8402,5178],{"class":409},[68,8404,413],{"class":342},[68,8406,8407,8409,8411,8413,8416,8418,8421],{"class":70,"line":113},[68,8408,5281],{"class":331},[68,8410,4842],{"class":338},[68,8412,6648],{"class":342},[68,8414,8415],{"class":353},"403",[68,8417,6654],{"class":342},[68,8419,8420],{"class":409},"'Forbidden'",[68,8422,1859],{"class":342},[68,8424,8425],{"class":70,"line":119},[68,8426,429],{"class":342},[68,8428,8429],{"class":70,"line":126},[68,8430,123],{"emptyLinePlaceholder":122},[68,8432,8433,8435],{"class":70,"line":132},[68,8434,418],{"class":338},[68,8436,8241],{"class":346},[68,8438,8439],{"class":70,"line":2135},[68,8440,447],{"class":342},[20,8442,8443],{},"Define roles in your Prisma schema and populate them on the session object through better-auth's session customization:",[59,8445,8447],{"className":316,"code":8446,"language":318,"meta":64,"style":64},"export const auth = betterAuth({\n // ... Session: {\n additionalFields: {\n role: {\n type: 'string',\n required: false,\n },\n },\n },\n})\n",[37,8448,8449,8463,8468,8473,8478,8488,8498,8502,8506,8510],{"__ignoreMap":64},[68,8450,8451,8453,8455,8457,8459,8461],{"class":70,"line":71},[68,8452,4892],{"class":331},[68,8454,376],{"class":331},[68,8456,7378],{"class":353},[68,8458,382],{"class":331},[68,8460,7383],{"class":338},[68,8462,1789],{"class":342},[68,8464,8465],{"class":70,"line":77},[68,8466,8467],{"class":325}," // ... Session: {\n",[68,8469,8470],{"class":70,"line":83},[68,8471,8472],{"class":342}," additionalFields: {\n",[68,8474,8475],{"class":70,"line":89},[68,8476,8477],{"class":342}," role: {\n",[68,8479,8480,8483,8486],{"class":70,"line":95},[68,8481,8482],{"class":342}," type: ",[68,8484,8485],{"class":409},"'string'",[68,8487,2751],{"class":342},[68,8489,8490,8493,8496],{"class":70,"line":101},[68,8491,8492],{"class":342}," required: ",[68,8494,8495],{"class":353},"false",[68,8497,2751],{"class":342},[68,8499,8500],{"class":70,"line":107},[68,8501,3893],{"class":342},[68,8503,8504],{"class":70,"line":113},[68,8505,3893],{"class":342},[68,8507,8508],{"class":70,"line":119},[68,8509,3893],{"class":342},[68,8511,8512],{"class":70,"line":126},[68,8513,2789],{"class":342},[15,8515,8517],{"id":8516},"the-useauth-composable","The useAuth Composable",[20,8519,8520],{},"Build a clean composable that your components use — do not scatter raw auth calls throughout your pages:",[59,8522,8524],{"className":316,"code":8523,"language":318,"meta":64,"style":64},"// composables/useAuth.ts\nexport function useAuth() {\n const session = useState\u003CSession | null>('session', () => null)\n const loading = ref(false)\n\n async function login(email: string, password: string) {\n loading.value = true\n try {\n const result = await $fetch('/api/auth/sign-in/email', {\n method: 'POST',\n body: { email, password },\n })\n session.value = result.session\n await navigateTo('/dashboard')\n } catch (error) {\n throw error\n } finally {\n loading.value = false\n }\n }\n\n async function logout() {\n await $fetch('/api/auth/sign-out', { method: 'POST' })\n session.value = null\n await navigateTo('/login')\n }\n\n const isAuthenticated = computed(() => session.value !== null)\n const user = computed(() => session.value?.user ?? null)\n\n return { session, loading, isAuthenticated, user, login, logout }\n}\n",[37,8525,8526,8531,8541,8573,8591,8595,8622,8631,8638,8657,8665,8670,8674,8684,8697,8707,8714,8723,8731,8735,8739,8743,8754,8772,8781,8794,8798,8802,8826,8850,8854,8861],{"__ignoreMap":64},[68,8527,8528],{"class":70,"line":71},[68,8529,8530],{"class":325},"// composables/useAuth.ts\n",[68,8532,8533,8535,8537,8539],{"class":70,"line":77},[68,8534,4892],{"class":331},[68,8536,335],{"class":331},[68,8538,7687],{"class":338},[68,8540,2332],{"class":342},[68,8542,8543,8545,8547,8549,8552,8554,8557,8559,8561,8563,8565,8567,8569,8571],{"class":70,"line":83},[68,8544,376],{"class":331},[68,8546,7968],{"class":353},[68,8548,382],{"class":331},[68,8550,8551],{"class":338}," useState",[68,8553,365],{"class":342},[68,8555,8556],{"class":338},"Session",[68,8558,5971],{"class":331},[68,8560,4216],{"class":353},[68,8562,5411],{"class":342},[68,8564,5043],{"class":409},[68,8566,3101],{"class":342},[68,8568,1617],{"class":331},[68,8570,4216],{"class":353},[68,8572,1702],{"class":342},[68,8574,8575,8577,8580,8582,8585,8587,8589],{"class":70,"line":89},[68,8576,376],{"class":331},[68,8578,8579],{"class":353}," loading",[68,8581,382],{"class":331},[68,8583,8584],{"class":338}," ref",[68,8586,343],{"class":342},[68,8588,8495],{"class":353},[68,8590,1702],{"class":342},[68,8592,8593],{"class":70,"line":95},[68,8594,123],{"emptyLinePlaceholder":122},[68,8596,8597,8599,8601,8604,8606,8608,8610,8612,8614,8616,8618,8620],{"class":70,"line":101},[68,8598,6709],{"class":331},[68,8600,335],{"class":331},[68,8602,8603],{"class":338}," login",[68,8605,343],{"class":342},[68,8607,5136],{"class":346},[68,8609,350],{"class":331},[68,8611,354],{"class":353},[68,8613,180],{"class":342},[68,8615,5343],{"class":346},[68,8617,350],{"class":331},[68,8619,354],{"class":353},[68,8621,413],{"class":342},[68,8623,8624,8627,8629],{"class":70,"line":107},[68,8625,8626],{"class":342}," loading.value ",[68,8628,1593],{"class":331},[68,8630,6500],{"class":353},[68,8632,8633,8636],{"class":70,"line":113},[68,8634,8635],{"class":331}," try",[68,8637,1620],{"class":342},[68,8639,8640,8642,8644,8646,8648,8650,8652,8655],{"class":70,"line":119},[68,8641,376],{"class":331},[68,8643,2374],{"class":353},[68,8645,382],{"class":331},[68,8647,385],{"class":331},[68,8649,7024],{"class":338},[68,8651,343],{"class":342},[68,8653,8654],{"class":409},"'/api/auth/sign-in/email'",[68,8656,2562],{"class":342},[68,8658,8659,8661,8663],{"class":70,"line":126},[68,8660,7135],{"class":342},[68,8662,7138],{"class":409},[68,8664,2751],{"class":342},[68,8666,8667],{"class":70,"line":132},[68,8668,8669],{"class":342}," body: { email, password },\n",[68,8671,8672],{"class":70,"line":2135},[68,8673,1859],{"class":342},[68,8675,8676,8679,8681],{"class":70,"line":2141},[68,8677,8678],{"class":342}," session.value ",[68,8680,1593],{"class":331},[68,8682,8683],{"class":342}," result.session\n",[68,8685,8686,8688,8690,8692,8695],{"class":70,"line":2437},[68,8687,385],{"class":331},[68,8689,7725],{"class":338},[68,8691,343],{"class":342},[68,8693,8694],{"class":409},"'/dashboard'",[68,8696,1702],{"class":342},[68,8698,8699,8701,8704],{"class":70,"line":2442},[68,8700,1769],{"class":342},[68,8702,8703],{"class":331},"catch",[68,8705,8706],{"class":342}," (error) {\n",[68,8708,8709,8711],{"class":70,"line":2447},[68,8710,5281],{"class":331},[68,8712,8713],{"class":342}," error\n",[68,8715,8716,8718,8721],{"class":70,"line":2652},[68,8717,1769],{"class":342},[68,8719,8720],{"class":331},"finally",[68,8722,1620],{"class":342},[68,8724,8725,8727,8729],{"class":70,"line":2687},[68,8726,8626],{"class":342},[68,8728,1593],{"class":331},[68,8730,6528],{"class":353},[68,8732,8733],{"class":70,"line":2692},[68,8734,429],{"class":342},[68,8736,8737],{"class":70,"line":2697},[68,8738,429],{"class":342},[68,8740,8741],{"class":70,"line":3129},[68,8742,123],{"emptyLinePlaceholder":122},[68,8744,8745,8747,8749,8752],{"class":70,"line":3134},[68,8746,6709],{"class":331},[68,8748,335],{"class":331},[68,8750,8751],{"class":338}," logout",[68,8753,2332],{"class":342},[68,8755,8756,8758,8760,8762,8765,8768,8770],{"class":70,"line":4749},[68,8757,385],{"class":331},[68,8759,7024],{"class":338},[68,8761,343],{"class":342},[68,8763,8764],{"class":409},"'/api/auth/sign-out'",[68,8766,8767],{"class":342},", { method: ",[68,8769,7138],{"class":409},[68,8771,1859],{"class":342},[68,8773,8774,8776,8778],{"class":70,"line":4755},[68,8775,8678],{"class":342},[68,8777,1593],{"class":331},[68,8779,8780],{"class":353}," null\n",[68,8782,8783,8785,8787,8789,8792],{"class":70,"line":4760},[68,8784,385],{"class":331},[68,8786,7725],{"class":338},[68,8788,343],{"class":342},[68,8790,8791],{"class":409},"'/login'",[68,8793,1702],{"class":342},[68,8795,8796],{"class":70,"line":4767},[68,8797,429],{"class":342},[68,8799,8800],{"class":70,"line":4773},[68,8801,123],{"emptyLinePlaceholder":122},[68,8803,8804,8806,8809,8811,8814,8816,8818,8820,8822,8824],{"class":70,"line":4779},[68,8805,376],{"class":331},[68,8807,8808],{"class":353}," isAuthenticated",[68,8810,382],{"class":331},[68,8812,8813],{"class":338}," computed",[68,8815,1614],{"class":342},[68,8817,1617],{"class":331},[68,8819,8678],{"class":342},[68,8821,7713],{"class":331},[68,8823,4216],{"class":353},[68,8825,1702],{"class":342},[68,8827,8828,8830,8833,8835,8837,8839,8841,8844,8846,8848],{"class":70,"line":4785},[68,8829,376],{"class":331},[68,8831,8832],{"class":353}," user",[68,8834,382],{"class":331},[68,8836,8813],{"class":338},[68,8838,1614],{"class":342},[68,8840,1617],{"class":331},[68,8842,8843],{"class":342}," session.value?.user ",[68,8845,4213],{"class":331},[68,8847,4216],{"class":353},[68,8849,1702],{"class":342},[68,8851,8852],{"class":70,"line":4791},[68,8853,123],{"emptyLinePlaceholder":122},[68,8855,8856,8858],{"class":70,"line":4797},[68,8857,418],{"class":331},[68,8859,8860],{"class":342}," { session, loading, isAuthenticated, user, login, logout }\n",[68,8862,8863],{"class":70,"line":4814},[68,8864,447],{"class":342},[15,8866,8868],{"id":8867},"handling-token-refresh","Handling Token Refresh",[20,8870,8871],{},"If you are using JWTs (for a legitimate use case), handle token refresh automatically:",[59,8873,8875],{"className":316,"code":8874,"language":318,"meta":64,"style":64},"// plugins/auth-refresh.ts\nexport default defineNuxtPlugin(() => {\n $fetch.create({\n onResponseError: async ({ response }) => {\n if (response.status === 401) {\n try {\n await $fetch('/api/auth/refresh', { method: 'POST' })\n // Retry the original request\n } catch {\n await navigateTo('/login')\n }\n }\n },\n })\n})\n",[37,8876,8877,8882,8897,8907,8928,8942,8948,8965,8970,8978,8990,8994,8998,9002,9006],{"__ignoreMap":64},[68,8878,8879],{"class":70,"line":71},[68,8880,8881],{"class":325},"// plugins/auth-refresh.ts\n",[68,8883,8884,8886,8888,8891,8893,8895],{"class":70,"line":77},[68,8885,4892],{"class":331},[68,8887,4895],{"class":331},[68,8889,8890],{"class":338}," defineNuxtPlugin",[68,8892,1614],{"class":342},[68,8894,1617],{"class":331},[68,8896,1620],{"class":342},[68,8898,8899,8902,8905],{"class":70,"line":83},[68,8900,8901],{"class":342}," $fetch.",[68,8903,8904],{"class":338},"create",[68,8906,1789],{"class":342},[68,8908,8909,8912,8914,8916,8919,8922,8924,8926],{"class":70,"line":89},[68,8910,8911],{"class":338}," onResponseError",[68,8913,938],{"class":342},[68,8915,332],{"class":331},[68,8917,8918],{"class":342}," ({ ",[68,8920,8921],{"class":346},"response",[68,8923,6606],{"class":342},[68,8925,1617],{"class":331},[68,8927,1620],{"class":342},[68,8929,8930,8932,8935,8937,8940],{"class":70,"line":95},[68,8931,400],{"class":331},[68,8933,8934],{"class":342}," (response.status ",[68,8936,406],{"class":331},[68,8938,8939],{"class":353}," 401",[68,8941,413],{"class":342},[68,8943,8944,8946],{"class":70,"line":101},[68,8945,8635],{"class":331},[68,8947,1620],{"class":342},[68,8949,8950,8952,8954,8956,8959,8961,8963],{"class":70,"line":107},[68,8951,385],{"class":331},[68,8953,7024],{"class":338},[68,8955,343],{"class":342},[68,8957,8958],{"class":409},"'/api/auth/refresh'",[68,8960,8767],{"class":342},[68,8962,7138],{"class":409},[68,8964,1859],{"class":342},[68,8966,8967],{"class":70,"line":113},[68,8968,8969],{"class":325}," // Retry the original request\n",[68,8971,8972,8974,8976],{"class":70,"line":119},[68,8973,1769],{"class":342},[68,8975,8703],{"class":331},[68,8977,1620],{"class":342},[68,8979,8980,8982,8984,8986,8988],{"class":70,"line":126},[68,8981,385],{"class":331},[68,8983,7725],{"class":338},[68,8985,343],{"class":342},[68,8987,8791],{"class":409},[68,8989,1702],{"class":342},[68,8991,8992],{"class":70,"line":132},[68,8993,429],{"class":342},[68,8995,8996],{"class":70,"line":2135},[68,8997,429],{"class":342},[68,8999,9000],{"class":70,"line":2141},[68,9001,3893],{"class":342},[68,9003,9004],{"class":70,"line":2437},[68,9005,1859],{"class":342},[68,9007,9008],{"class":70,"line":2442},[68,9009,2789],{"class":342},[15,9011,9013],{"id":9012},"security-checklist-before-launch","Security Checklist Before Launch",[20,9015,9016],{},"Before shipping auth to production, verify:",[516,9018,9019,9022,9025,9028,9031,9034,9037],{},[519,9020,9021],{},"Passwords are hashed with bcrypt or argon2 (better-auth handles this)",[519,9023,9024],{},"Session cookies are HTTP-only and SameSite=Strict",[519,9026,9027],{},"All private API routes check authentication server-side",[519,9029,9030],{},"Password reset tokens expire after a reasonable window (1 hour)",[519,9032,9033],{},"Login rate limiting is in place (better-auth has built-in rate limiting)",[519,9035,9036],{},"Email verification is required before accessing protected features",[519,9038,9039],{},"Your database does not store plain-text passwords anywhere in logs",[20,9041,9042],{},"Authentication is not a feature you build and forget. Review your implementation annually, keep your auth libraries updated, and take security reports seriously.",[509,9044],{},[20,9046,9047,9048,51],{},"If you are designing the authentication architecture for a Nuxt application or need a security review of an existing implementation, I can help. Book a call at ",[502,9049,817],{"href":504,"rel":9050},[506],[509,9052],{},[15,9054,514],{"id":513},[516,9056,9057,9063,9069,9075],{},[519,9058,9059],{},[502,9060,9062],{"href":9061},"/blog/jwt-authentication-guide","JWT Authentication: What It Is, How It Works, and Where It Gets Tricky",[519,9064,9065],{},[502,9066,9068],{"href":9067},"/blog/nuxt-typescript-guide","TypeScript in Nuxt: Getting the Type Safety You Actually Want",[519,9070,9071],{},[502,9072,9074],{"href":9073},"/blog/oauth-2-explained","OAuth 2.0 Explained for Developers: The Flows That Matter",[519,9076,9077],{},[502,9078,4402],{"href":4401},[544,9080,9081],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":64,"searchDepth":83,"depth":83,"links":9083},[9084,9085,9086,9087,9088,9089,9090,9091,9092],{"id":7253,"depth":77,"text":7254},{"id":7293,"depth":77,"text":7294},{"id":7627,"depth":77,"text":7628},{"id":7908,"depth":77,"text":7909},{"id":8302,"depth":77,"text":8303},{"id":8516,"depth":77,"text":8517},{"id":8867,"depth":77,"text":8868},{"id":9012,"depth":77,"text":9013},{"id":513,"depth":77,"text":514},"A practical guide to Nuxt authentication — from session cookies vs JWTs to better-auth integration, middleware protection, and patterns that hold up in production.",[9095,9096],"Nuxt authentication","Nuxt auth",{},{"title":4396,"description":9093},"blog/nuxt-authentication-guide",[4438,9101,9102],"Authentication","Security","DOOJ74zxHJ7a5A9HpEtMiV1PpG2qjcNXQGSqffhfYtA",{"id":9105,"title":4414,"author":9106,"body":9107,"category":557,"date":558,"description":9948,"extension":560,"featured":561,"image":562,"keywords":9949,"meta":9952,"navigation":122,"path":4413,"readTime":107,"seo":9953,"stem":9954,"tags":9955,"__hash__":9958},"blog/blog/nuxt-cloudflare-deployment.md",{"name":9,"bio":10},{"type":12,"value":9108,"toc":9935},[9109,9112,9115,9119,9122,9125,9135,9139,9142,9186,9189,9192,9209,9213,9216,9231,9234,9240,9243,9249,9253,9256,9259,9265,9275,9278,9343,9402,9406,9409,9412,9424,9427,9621,9624,9628,9631,9675,9678,9681,9713,9717,9720,9731,9738,9742,9749,9752,9756,9759,9765,9774,9855,9861,9865,9871,9877,9887,9897,9900,9902,9908,9910,9912,9932],[20,9110,9111],{},"Cloudflare Pages has become my default deployment target for Nuxt applications that do not need a full server. Free tier is genuinely generous, the edge network is excellent, and the developer experience has improved substantially over the past year. When you combine it with Nuxt's Nitro server, you get SSR running at the edge — globally distributed, fast, and cheap to operate.",[20,9113,9114],{},"This article walks through the complete setup: initial deployment, environment variables, edge SSR configuration, KV storage for caching, and custom domains. I am assuming you have a working Nuxt application and a Cloudflare account.",[15,9116,9118],{"id":9117},"understanding-what-edge-ssr-means","Understanding What \"Edge SSR\" Means",[20,9120,9121],{},"Before getting into the setup, it is worth understanding what you are actually deploying. Cloudflare Pages with edge SSR uses Cloudflare Workers under the hood. Your Nuxt server code runs in Cloudflare's edge runtime — a V8-based JavaScript environment that runs in over 300 data centers worldwide.",[20,9123,9124],{},"When a user in Tokyo requests your page, the Worker runs in a Tokyo data center, renders the Nuxt page, and returns HTML. There is no round trip to a central server. The latency difference between a global CDN and a single-region server can be 300-500ms for distant users. For Core Web Vitals, that difference is significant.",[20,9126,9127,9128,180,9131,9134],{},"The trade-off is that the Cloudflare Workers runtime is not Node.js. Not all Node.js APIs are available. If your server code depends on ",[37,9129,9130],{},"fs",[37,9132,9133],{},"child_process",", or Node.js-specific modules, it will not work. Most Nuxt applications do not need these — but it is worth checking before committing to this deployment target.",[15,9136,9138],{"id":9137},"project-configuration","Project Configuration",[20,9140,9141],{},"Configure Nuxt to target the Cloudflare Pages preset:",[59,9143,9145],{"className":316,"code":9144,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nexport default defineNuxtConfig({\n nitro: {\n preset: 'cloudflare-pages',\n },\n})\n",[37,9146,9147,9152,9163,9168,9178,9182],{"__ignoreMap":64},[68,9148,9149],{"class":70,"line":71},[68,9150,9151],{"class":325},"// nuxt.config.ts\n",[68,9153,9154,9156,9158,9161],{"class":70,"line":77},[68,9155,4892],{"class":331},[68,9157,4895],{"class":331},[68,9159,9160],{"class":338}," defineNuxtConfig",[68,9162,1789],{"class":342},[68,9164,9165],{"class":70,"line":83},[68,9166,9167],{"class":342}," nitro: {\n",[68,9169,9170,9173,9176],{"class":70,"line":89},[68,9171,9172],{"class":342}," preset: ",[68,9174,9175],{"class":409},"'cloudflare-pages'",[68,9177,2751],{"class":342},[68,9179,9180],{"class":70,"line":95},[68,9181,3893],{"class":342},[68,9183,9184],{"class":70,"line":101},[68,9185,2789],{"class":342},[20,9187,9188],{},"That is the only required configuration change. Nuxt and Nitro handle the rest of the build output formatting for Cloudflare Pages compatibility.",[20,9190,9191],{},"Install the Cloudflare Pages adapter if you plan to use Cloudflare-specific features like KV or D1:",[59,9193,9195],{"className":1888,"code":9194,"language":1890,"meta":64,"style":64},"npm install wrangler --save-dev\n",[37,9196,9197],{"__ignoreMap":64},[68,9198,9199,9201,9203,9206],{"class":70,"line":71},[68,9200,7311],{"class":338},[68,9202,7314],{"class":409},[68,9204,9205],{"class":409}," wrangler",[68,9207,9208],{"class":353}," --save-dev\n",[15,9210,9212],{"id":9211},"setting-up-cloudflare-pages","Setting Up Cloudflare Pages",[20,9214,9215],{},"Log in to the Cloudflare dashboard and create a new Pages project:",[9217,9218,9219,9222,9225,9228],"ol",{},[519,9220,9221],{},"Go to Workers & Pages",[519,9223,9224],{},"Click \"Create application\" then \"Pages\"",[519,9226,9227],{},"Connect to Git (GitHub or GitLab)",[519,9229,9230],{},"Select your repository",[20,9232,9233],{},"Configure the build settings:",[59,9235,9238],{"className":9236,"code":9237,"language":4098},[4096],"Build command: npm run build\nBuild output dir: .output/public\nRoot directory: / (or your project subdirectory)\n",[37,9239,9237],{"__ignoreMap":64},[20,9241,9242],{},"Set the Node.js version to match your local development environment. Cloudflare Pages supports Node.js 18 and 20. Set this in the environment variables section:",[59,9244,9247],{"className":9245,"code":9246,"language":4098},[4096],"NODE_VERSION = 20\n",[37,9248,9246],{"__ignoreMap":64},[15,9250,9252],{"id":9251},"environment-variables","Environment Variables",[20,9254,9255],{},"Cloudflare Pages has two types of environment variables: plain variables and secrets. Both are set in the dashboard under Settings > Environment variables.",[20,9257,9258],{},"Add them for the Production environment (and Preview if needed):",[59,9260,9263],{"className":9261,"code":9262,"language":4098},[4096],"DATABASE_URL = postgresql://...\nAPI_KEY = your-api-key\nNUXT_PUBLIC_API_BASE = https://api.yourdomain.com\n",[37,9264,9262],{"__ignoreMap":64},[20,9266,9267,9268,9271,9272,9274],{},"Variables prefixed with ",[37,9269,9270],{},"NUXT_PUBLIC_"," are automatically available in the browser. Variables without this prefix are server-only. Never put secrets in ",[37,9273,9270],{}," variables — they will be visible in the JavaScript bundle.",[20,9276,9277],{},"Access them in your Nuxt application:",[59,9279,9281],{"className":316,"code":9280,"language":318,"meta":64,"style":64},"// composables/useConfig.ts\nexport function useConfig() {\n const config = useRuntimeConfig()\n return {\n apiBase: config.public.apiBase, // Available client + server\n apiKey: config.apiKey, // Server only\n }\n}\n",[37,9282,9283,9288,9299,9313,9319,9327,9335,9339],{"__ignoreMap":64},[68,9284,9285],{"class":70,"line":71},[68,9286,9287],{"class":325},"// composables/useConfig.ts\n",[68,9289,9290,9292,9294,9297],{"class":70,"line":77},[68,9291,4892],{"class":331},[68,9293,335],{"class":331},[68,9295,9296],{"class":338}," useConfig",[68,9298,2332],{"class":342},[68,9300,9301,9303,9306,9308,9311],{"class":70,"line":83},[68,9302,376],{"class":331},[68,9304,9305],{"class":353}," config",[68,9307,382],{"class":331},[68,9309,9310],{"class":338}," useRuntimeConfig",[68,9312,1602],{"class":342},[68,9314,9315,9317],{"class":70,"line":89},[68,9316,418],{"class":331},[68,9318,1620],{"class":342},[68,9320,9321,9324],{"class":70,"line":95},[68,9322,9323],{"class":342}," apiBase: config.public.apiBase, ",[68,9325,9326],{"class":325},"// Available client + server\n",[68,9328,9329,9332],{"class":70,"line":101},[68,9330,9331],{"class":342}," apiKey: config.apiKey, ",[68,9333,9334],{"class":325},"// Server only\n",[68,9336,9337],{"class":70,"line":107},[68,9338,429],{"class":342},[68,9340,9341],{"class":70,"line":113},[68,9342,447],{"class":342},[59,9344,9346],{"className":316,"code":9345,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nruntimeConfig: {\n apiKey: '', // Override with NUXT_API_KEY env var\n public: {\n apiBase: 'https://api.example.com' // Override with NUXT_PUBLIC_API_BASE\n }\n}\n",[37,9347,9348,9352,9359,9374,9381,9394,9398],{"__ignoreMap":64},[68,9349,9350],{"class":70,"line":71},[68,9351,9151],{"class":325},[68,9353,9354,9357],{"class":70,"line":77},[68,9355,9356],{"class":338},"runtimeConfig",[68,9358,7834],{"class":342},[68,9360,9361,9364,9366,9369,9371],{"class":70,"line":83},[68,9362,9363],{"class":338}," apiKey",[68,9365,938],{"class":342},[68,9367,9368],{"class":409},"''",[68,9370,180],{"class":342},[68,9372,9373],{"class":325},"// Override with NUXT_API_KEY env var\n",[68,9375,9376,9379],{"class":70,"line":89},[68,9377,9378],{"class":338}," public",[68,9380,7834],{"class":342},[68,9382,9383,9386,9388,9391],{"class":70,"line":95},[68,9384,9385],{"class":338}," apiBase",[68,9387,938],{"class":342},[68,9389,9390],{"class":409},"'https://api.example.com'",[68,9392,9393],{"class":325}," // Override with NUXT_PUBLIC_API_BASE\n",[68,9395,9396],{"class":70,"line":101},[68,9397,429],{"class":342},[68,9399,9400],{"class":70,"line":107},[68,9401,447],{"class":342},[15,9403,9405],{"id":9404},"using-cloudflare-kv-for-caching","Using Cloudflare KV for Caching",[20,9407,9408],{},"KV (Key-Value) storage is Cloudflare's globally replicated edge storage. You can use it to cache API responses, session data, or any string/blob data that benefits from edge proximity.",[20,9410,9411],{},"Create a KV namespace in the Cloudflare dashboard, then bind it to your Pages project:",[9217,9413,9414,9417],{},[519,9415,9416],{},"Settings > Functions > KV namespace bindings",[519,9418,9419,9420,9423],{},"Add a binding: Variable name = ",[37,9421,9422],{},"CACHE",", KV namespace = your namespace",[20,9425,9426],{},"Access the KV store in Nuxt server routes:",[59,9428,9430],{"className":316,"code":9429,"language":318,"meta":64,"style":64},"// server/api/products.ts\nexport default defineEventHandler(async (event) => {\n const cf = event.context.cloudflare\n const cacheKey = 'products:all'\n\n // Try cache first\n const cached = await cf.env.CACHE.get(cacheKey, 'json')\n if (cached) return cached\n\n // Fetch from your API\n const products = await $fetch('https://api.yourdomain.com/products')\n\n // Cache for 5 minutes\n await cf.env.CACHE.put(cacheKey, JSON.stringify(products), {\n expirationTtl: 300,\n })\n\n return products\n})\n",[37,9431,9432,9437,9459,9471,9483,9487,9492,9519,9529,9533,9538,9558,9562,9567,9593,9602,9606,9610,9617],{"__ignoreMap":64},[68,9433,9434],{"class":70,"line":71},[68,9435,9436],{"class":325},"// server/api/products.ts\n",[68,9438,9439,9441,9443,9445,9447,9449,9451,9453,9455,9457],{"class":70,"line":77},[68,9440,4892],{"class":331},[68,9442,4895],{"class":331},[68,9444,4525],{"class":338},[68,9446,343],{"class":342},[68,9448,332],{"class":331},[68,9450,2657],{"class":342},[68,9452,4534],{"class":346},[68,9454,1869],{"class":342},[68,9456,1617],{"class":331},[68,9458,1620],{"class":342},[68,9460,9461,9463,9466,9468],{"class":70,"line":83},[68,9462,376],{"class":331},[68,9464,9465],{"class":353}," cf",[68,9467,382],{"class":331},[68,9469,9470],{"class":342}," event.context.cloudflare\n",[68,9472,9473,9475,9478,9480],{"class":70,"line":89},[68,9474,376],{"class":331},[68,9476,9477],{"class":353}," cacheKey",[68,9479,382],{"class":331},[68,9481,9482],{"class":409}," 'products:all'\n",[68,9484,9485],{"class":70,"line":95},[68,9486,123],{"emptyLinePlaceholder":122},[68,9488,9489],{"class":70,"line":101},[68,9490,9491],{"class":325}," // Try cache first\n",[68,9493,9494,9496,9498,9500,9502,9505,9507,9509,9511,9514,9517],{"class":70,"line":107},[68,9495,376],{"class":331},[68,9497,6831],{"class":353},[68,9499,382],{"class":331},[68,9501,385],{"class":331},[68,9503,9504],{"class":342}," cf.env.",[68,9506,9422],{"class":353},[68,9508,51],{"class":342},[68,9510,3169],{"class":338},[68,9512,9513],{"class":342},"(cacheKey, ",[68,9515,9516],{"class":409},"'json'",[68,9518,1702],{"class":342},[68,9520,9521,9523,9525,9527],{"class":70,"line":113},[68,9522,400],{"class":331},[68,9524,6855],{"class":342},[68,9526,6858],{"class":331},[68,9528,6861],{"class":342},[68,9530,9531],{"class":70,"line":119},[68,9532,123],{"emptyLinePlaceholder":122},[68,9534,9535],{"class":70,"line":126},[68,9536,9537],{"class":325}," // Fetch from your API\n",[68,9539,9540,9542,9545,9547,9549,9551,9553,9556],{"class":70,"line":132},[68,9541,376],{"class":331},[68,9543,9544],{"class":353}," products",[68,9546,382],{"class":331},[68,9548,385],{"class":331},[68,9550,7024],{"class":338},[68,9552,343],{"class":342},[68,9554,9555],{"class":409},"'https://api.yourdomain.com/products'",[68,9557,1702],{"class":342},[68,9559,9560],{"class":70,"line":2135},[68,9561,123],{"emptyLinePlaceholder":122},[68,9563,9564],{"class":70,"line":2141},[68,9565,9566],{"class":325}," // Cache for 5 minutes\n",[68,9568,9569,9571,9573,9575,9577,9580,9582,9585,9587,9590],{"class":70,"line":2437},[68,9570,385],{"class":331},[68,9572,9504],{"class":342},[68,9574,9422],{"class":353},[68,9576,51],{"class":342},[68,9578,9579],{"class":338},"put",[68,9581,9513],{"class":342},[68,9583,9584],{"class":353},"JSON",[68,9586,51],{"class":342},[68,9588,9589],{"class":338},"stringify",[68,9591,9592],{"class":342},"(products), {\n",[68,9594,9595,9598,9600],{"class":70,"line":2442},[68,9596,9597],{"class":342}," expirationTtl: ",[68,9599,6899],{"class":353},[68,9601,2751],{"class":342},[68,9603,9604],{"class":70,"line":2447},[68,9605,1859],{"class":342},[68,9607,9608],{"class":70,"line":2652},[68,9609,123],{"emptyLinePlaceholder":122},[68,9611,9612,9614],{"class":70,"line":2687},[68,9613,418],{"class":331},[68,9615,9616],{"class":342}," products\n",[68,9618,9619],{"class":70,"line":2692},[68,9620,2789],{"class":342},[20,9622,9623],{},"This pattern gives you API response caching at the edge with zero cold start. Requests that hit the KV cache return in under 10ms globally.",[15,9625,9627],{"id":9626},"wrangler-for-local-development","Wrangler for Local Development",[20,9629,9630],{},"To test Cloudflare-specific features locally, use Wrangler:",[59,9632,9634],{"className":1888,"code":9633,"language":1890,"meta":64,"style":64},"# Build your Nuxt app\nnpm run build\n\n# Serve locally with Cloudflare Workers runtime\nnpx wrangler pages dev .output/public\n",[37,9635,9636,9641,9651,9655,9660],{"__ignoreMap":64},[68,9637,9638],{"class":70,"line":71},[68,9639,9640],{"class":325},"# Build your Nuxt app\n",[68,9642,9643,9645,9648],{"class":70,"line":77},[68,9644,7311],{"class":338},[68,9646,9647],{"class":409}," run",[68,9649,9650],{"class":409}," build\n",[68,9652,9653],{"class":70,"line":83},[68,9654,123],{"emptyLinePlaceholder":122},[68,9656,9657],{"class":70,"line":89},[68,9658,9659],{"class":325},"# Serve locally with Cloudflare Workers runtime\n",[68,9661,9662,9665,9667,9669,9672],{"class":70,"line":95},[68,9663,9664],{"class":338},"npx",[68,9666,9205],{"class":409},[68,9668,6254],{"class":409},[68,9670,9671],{"class":409}," dev",[68,9673,9674],{"class":409}," .output/public\n",[20,9676,9677],{},"This runs your application in the actual Workers runtime, not Node.js, so you catch Workers-incompatible code before deploying. I run this as a final check before every production deployment when I have made changes to server routes.",[20,9679,9680],{},"Add a local KV namespace for development:",[59,9682,9686],{"className":9683,"code":9684,"language":9685,"meta":64,"style":64},"language-toml shiki shiki-themes github-dark","# wrangler.toml\n[[kv_namespaces]]\nbinding = \"CACHE\"\nid = \"your-kv-namespace-id\"\npreview_id = \"your-preview-kv-namespace-id\"\n","toml",[37,9687,9688,9693,9698,9703,9708],{"__ignoreMap":64},[68,9689,9690],{"class":70,"line":71},[68,9691,9692],{},"# wrangler.toml\n",[68,9694,9695],{"class":70,"line":77},[68,9696,9697],{},"[[kv_namespaces]]\n",[68,9699,9700],{"class":70,"line":83},[68,9701,9702],{},"binding = \"CACHE\"\n",[68,9704,9705],{"class":70,"line":89},[68,9706,9707],{},"id = \"your-kv-namespace-id\"\n",[68,9709,9710],{"class":70,"line":95},[68,9711,9712],{},"preview_id = \"your-preview-kv-namespace-id\"\n",[15,9714,9716],{"id":9715},"custom-domains","Custom Domains",[20,9718,9719],{},"Adding a custom domain to a Cloudflare Pages project is straightforward if your domain is managed by Cloudflare (which it should be — Cloudflare's DNS is excellent):",[9217,9721,9722,9725,9728],{},[519,9723,9724],{},"Settings > Custom domains",[519,9726,9727],{},"Enter your domain",[519,9729,9730],{},"Cloudflare automatically creates the DNS records",[20,9732,9733,9734,9737],{},"If your domain is with another registrar, add a CNAME record pointing to ",[37,9735,9736],{},"your-project.pages.dev",". SSL is automatic and included.",[15,9739,9741],{"id":9740},"preview-deployments","Preview Deployments",[20,9743,9744,9745,9748],{},"Every pull request automatically gets a preview deployment at a unique URL like ",[37,9746,9747],{},"your-branch.your-project.pages.dev",". This is one of the best features of Cloudflare Pages for team workflows — stakeholders can review changes before they hit production.",[20,9750,9751],{},"You can add comments to pull requests linking to the preview URL, or use Cloudflare's GitHub integration to post deployment status directly to PRs.",[15,9753,9755],{"id":9754},"performance-tuning-for-edge-deployment","Performance Tuning for Edge Deployment",[20,9757,9758],{},"A few patterns that improve edge performance:",[20,9760,9761,9764],{},[55,9762,9763],{},"Minimize cold start time."," Cloudflare Workers have no cold start in the traditional sense, but they do have a global JavaScript bundle size limit (1MB compressed for free tier, 5MB for paid). Keep your server-side code lean. Do not import large Node.js libraries.",[20,9766,9767],{},[55,9768,9769,9770,9773],{},"Use ",[37,9771,9772],{},"routeRules"," for route-level caching:",[59,9775,9777],{"className":316,"code":9776,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nrouteRules: {\n '/': { swr: 3600 }, // Cache homepage for 1 hour\n '/blog/**': { swr: 86400 }, // Cache blog posts for 24 hours\n '/api/**': { cors: true }, // API routes, no caching\n '/dashboard/**': { ssr: true }, // Always SSR, no cache\n}\n",[37,9778,9779,9783,9789,9806,9821,9836,9851],{"__ignoreMap":64},[68,9780,9781],{"class":70,"line":71},[68,9782,9151],{"class":325},[68,9784,9785,9787],{"class":70,"line":77},[68,9786,9772],{"class":338},[68,9788,7834],{"class":342},[68,9790,9791,9794,9797,9800,9803],{"class":70,"line":83},[68,9792,9793],{"class":409}," '/'",[68,9795,9796],{"class":342},": { swr: ",[68,9798,9799],{"class":353},"3600",[68,9801,9802],{"class":342}," }, ",[68,9804,9805],{"class":325},"// Cache homepage for 1 hour\n",[68,9807,9808,9811,9813,9816,9818],{"class":70,"line":89},[68,9809,9810],{"class":409}," '/blog/**'",[68,9812,9796],{"class":342},[68,9814,9815],{"class":353},"86400",[68,9817,9802],{"class":342},[68,9819,9820],{"class":325},"// Cache blog posts for 24 hours\n",[68,9822,9823,9826,9829,9831,9833],{"class":70,"line":95},[68,9824,9825],{"class":409}," '/api/**'",[68,9827,9828],{"class":342},": { cors: ",[68,9830,3619],{"class":353},[68,9832,9802],{"class":342},[68,9834,9835],{"class":325},"// API routes, no caching\n",[68,9837,9838,9841,9844,9846,9848],{"class":70,"line":101},[68,9839,9840],{"class":409}," '/dashboard/**'",[68,9842,9843],{"class":342},": { ssr: ",[68,9845,3619],{"class":353},[68,9847,9802],{"class":342},[68,9849,9850],{"class":325},"// Always SSR, no cache\n",[68,9852,9853],{"class":70,"line":107},[68,9854,447],{"class":342},[20,9856,9857,9860],{},[55,9858,9859],{},"Stream responses"," for large pages. Nuxt's streaming support allows the browser to start rendering before the server finishes generating the complete page.",[15,9862,9864],{"id":9863},"troubleshooting-common-issues","Troubleshooting Common Issues",[20,9866,9867,9870],{},[55,9868,9869],{},"\"Cannot find module\" errors at runtime:"," A Node.js module you are importing is not available in the Workers runtime. Check Cloudflare's Node.js compatibility docs and find a Workers-compatible alternative.",[20,9872,9873,9876],{},[55,9874,9875],{},"Environment variables undefined:"," Verify the variable name matches exactly (case-sensitive) and that you have deployed after adding the variable. Preview and Production environments have separate variable sets.",[20,9878,9879,9882,9883,9886],{},[55,9880,9881],{},"KV binding undefined:"," Make sure the binding name in the dashboard matches the property name you are accessing on ",[37,9884,9885],{},"cf.env",". The binding name is case-sensitive.",[20,9888,9889,9892,9893,9896],{},[55,9890,9891],{},"Build fails on Cloudflare:"," Run ",[37,9894,9895],{},"npm run build"," locally first to catch issues before they fail in CI. Cloudflare's build logs are detailed but debugging through the dashboard is slower than local iteration.",[20,9898,9899],{},"Cloudflare Pages is an excellent deployment target for Nuxt applications. The free tier is sufficient for most personal and small business sites, the edge performance is genuine (not marketing), and the integration with the rest of the Cloudflare ecosystem is increasingly powerful.",[509,9901],{},[20,9903,9904,9905,51],{},"Deploying a Nuxt application and running into issues with the infrastructure, or want help designing a deployment strategy that fits your team's workflow? I am happy to help — book a call at ",[502,9906,817],{"href":504,"rel":9907},[506],[509,9909],{},[15,9911,514],{"id":513},[516,9913,9914,9920,9924,9928],{},[519,9915,9916],{},[502,9917,9919],{"href":9918},"/blog/nuxt-deployment-vercel","Zero-Config Nuxt Deployment on Vercel: What to Know Before You Ship",[519,9921,9922],{},[502,9923,4402],{"href":4401},[519,9925,9926],{},[502,9927,4396],{"href":4395},[519,9929,9930],{},[502,9931,4408],{"href":4407},[544,9933,9934],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":64,"searchDepth":83,"depth":83,"links":9936},[9937,9938,9939,9940,9941,9942,9943,9944,9945,9946,9947],{"id":9117,"depth":77,"text":9118},{"id":9137,"depth":77,"text":9138},{"id":9211,"depth":77,"text":9212},{"id":9251,"depth":77,"text":9252},{"id":9404,"depth":77,"text":9405},{"id":9626,"depth":77,"text":9627},{"id":9715,"depth":77,"text":9716},{"id":9740,"depth":77,"text":9741},{"id":9754,"depth":77,"text":9755},{"id":9863,"depth":77,"text":9864},{"id":513,"depth":77,"text":514},"Step-by-step guide to deploying a Nuxt 3 or Nuxt 4 app to Cloudflare Pages with edge SSR, environment variables, KV storage, and custom domains.",[9950,9951],"Nuxt Cloudflare Pages","Nuxt deployment",{},{"title":4414,"description":9948},"blog/nuxt-cloudflare-deployment",[4438,9956,9957],"Cloudflare","Deployment","qkQPlXSHPjcr4BTsgjLnfHSj6-r9tQAZ6S0CmTdKHx4",{"id":9960,"title":4402,"author":9961,"body":9962,"category":557,"date":558,"description":11721,"extension":560,"featured":561,"image":562,"keywords":11722,"meta":11725,"navigation":122,"path":4401,"readTime":107,"seo":11726,"stem":11727,"tags":11728,"__hash__":11731},"blog/blog/nuxt-content-module-guide.md",{"name":9,"bio":10},{"type":12,"value":9963,"toc":11707},[9964,9967,9970,9974,9977,9980,9983,9986,9990,9993,10014,10020,10026,10029,10033,10036,10039,10137,10144,10308,10311,10315,10318,10751,10755,10762,11105,11110,11114,11117,11157,11163,11167,11170,11277,11280,11284,11290,11565,11569,11576,11643,11647,11654,11657,11660,11664,11671,11674,11676,11682,11684,11686,11704],[20,9965,9966],{},"I have built this exact thing more times than I can count: a content-driven site where the developer wants to write in Markdown, have it render beautifully, support full-text search, and not require a CMS subscription. Nuxt Content hits every one of those requirements, and the developer experience is genuinely pleasant once you understand how the pieces fit together.",[20,9968,9969],{},"This guide walks through building a production-ready blog with Nuxt Content from scratch. Not a toy example — the actual patterns I use on client sites.",[15,9971,9973],{"id":9972},"why-nuxt-content-over-a-headless-cms","Why Nuxt Content Over a Headless CMS",[20,9975,9976],{},"Before diving in, let me address the obvious question. When should you reach for Nuxt Content versus Contentful, Sanity, or another headless CMS?",[20,9978,9979],{},"Nuxt Content wins when the content authors are developers (or comfortable with Git), when you want zero runtime external dependencies, when content changes deploy with code, and when you need the flexibility to embed custom Vue components in your content. The files live in your repository, the build is self-contained, and there are no API rate limits or monthly subscription costs.",[20,9981,9982],{},"Headless CMS wins when non-technical editors need a visual interface, when content needs to be shared across multiple frontends, or when you need real-time preview workflows for a large editorial team.",[20,9984,9985],{},"For a developer portfolio, documentation site, or a small business blog where the developer manages content — Nuxt Content is the right call.",[15,9987,9989],{"id":9988},"initial-setup","Initial Setup",[20,9991,9992],{},"Install the module and create your content directory:",[59,9994,9996],{"className":1888,"code":9995,"language":1890,"meta":64,"style":64},"npx nuxi module add content\n",[37,9997,9998],{"__ignoreMap":64},[68,9999,10000,10002,10005,10008,10011],{"class":70,"line":71},[68,10001,9664],{"class":338},[68,10003,10004],{"class":409}," nuxi",[68,10006,10007],{"class":409}," module",[68,10009,10010],{"class":409}," add",[68,10012,10013],{"class":409}," content\n",[20,10015,10016,10017,10019],{},"Your ",[37,10018,7821],{}," will get the module added automatically. The content directory at the root of your project is where all your Markdown files live.",[59,10021,10024],{"className":10022,"code":10023,"language":4098},[4096],"content/\n blog/\n my-first-post.md\n building-with-nuxt.md\n pages/\n about.md\n",[37,10025,10023],{"__ignoreMap":64},[20,10027,10028],{},"The directory structure becomes your URL structure by default, which is clean and predictable.",[15,10030,10032],{"id":10031},"frontmatter-and-content-schema","Frontmatter and Content Schema",[20,10034,10035],{},"Every blog post should have consistent frontmatter. I define this as a Zod schema and validate it at build time to catch missing fields before they reach production.",[20,10037,10038],{},"Here is the frontmatter structure I use:",[59,10040,10044],{"className":10041,"code":10042,"language":10043,"meta":64,"style":64},"language-yaml shiki shiki-themes github-dark","---\ntitle: \"Your Post Title\"\ndescription: \"150-160 character meta description for SEO\"\ndate: 2026-03-03\ncategory: Engineering\nreadTime: 7\ntags:\n - Nuxt\n - Web Development\ndraft: false\n---\n","yaml",[37,10045,10046,10051,10061,10071,10081,10090,10100,10108,10116,10123,10133],{"__ignoreMap":64},[68,10047,10048],{"class":70,"line":71},[68,10049,10050],{"class":338},"---\n",[68,10052,10053,10056,10058],{"class":70,"line":77},[68,10054,10055],{"class":7771},"title",[68,10057,938],{"class":342},[68,10059,10060],{"class":409},"\"Your Post Title\"\n",[68,10062,10063,10066,10068],{"class":70,"line":83},[68,10064,10065],{"class":7771},"description",[68,10067,938],{"class":342},[68,10069,10070],{"class":409},"\"150-160 character meta description for SEO\"\n",[68,10072,10073,10076,10078],{"class":70,"line":89},[68,10074,10075],{"class":7771},"date",[68,10077,938],{"class":342},[68,10079,10080],{"class":353},"2026-03-03\n",[68,10082,10083,10085,10087],{"class":70,"line":95},[68,10084,626],{"class":7771},[68,10086,938],{"class":342},[68,10088,10089],{"class":409},"Engineering\n",[68,10091,10092,10095,10097],{"class":70,"line":101},[68,10093,10094],{"class":7771},"readTime",[68,10096,938],{"class":342},[68,10098,10099],{"class":353},"7\n",[68,10101,10102,10105],{"class":70,"line":107},[68,10103,10104],{"class":7771},"tags",[68,10106,10107],{"class":342},":\n",[68,10109,10110,10113],{"class":70,"line":113},[68,10111,10112],{"class":342}," - ",[68,10114,10115],{"class":409},"Nuxt\n",[68,10117,10118,10120],{"class":70,"line":119},[68,10119,10112],{"class":342},[68,10121,10122],{"class":409},"Web Development\n",[68,10124,10125,10128,10130],{"class":70,"line":126},[68,10126,10127],{"class":7771},"draft",[68,10129,938],{"class":342},[68,10131,10132],{"class":353},"false\n",[68,10134,10135],{"class":70,"line":132},[68,10136,10050],{"class":338},[20,10138,10139,10140,10143],{},"In ",[37,10141,10142],{},"content.config.ts",", you can define collections with typed schemas:",[59,10145,10147],{"className":316,"code":10146,"language":318,"meta":64,"style":64},"import { defineCollection, z } from '@nuxt/content'\n\nExport const collections = {\n blog: defineCollection({\n type: 'page',\n source: 'blog/**/*.md',\n schema: z.object({\n title: z.string(),\n description: z.string(),\n date: z.date(),\n category: z.string(),\n readTime: z.number(),\n tags: z.array(z.string()),\n draft: z.boolean().default(false),\n }),\n }),\n}\n",[37,10148,10149,10161,10165,10178,10188,10197,10207,10216,10225,10234,10243,10252,10262,10278,10296,10300,10304],{"__ignoreMap":64},[68,10150,10151,10153,10156,10158],{"class":70,"line":71},[68,10152,2037],{"class":331},[68,10154,10155],{"class":342}," { defineCollection, z } ",[68,10157,2043],{"class":331},[68,10159,10160],{"class":409}," '@nuxt/content'\n",[68,10162,10163],{"class":70,"line":77},[68,10164,123],{"emptyLinePlaceholder":122},[68,10166,10167,10169,10171,10174,10176],{"class":70,"line":83},[68,10168,4519],{"class":342},[68,10170,1991],{"class":331},[68,10172,10173],{"class":353}," collections",[68,10175,382],{"class":331},[68,10177,1620],{"class":342},[68,10179,10180,10183,10186],{"class":70,"line":89},[68,10181,10182],{"class":342}," blog: ",[68,10184,10185],{"class":338},"defineCollection",[68,10187,1789],{"class":342},[68,10189,10190,10192,10195],{"class":70,"line":95},[68,10191,8482],{"class":342},[68,10193,10194],{"class":409},"'page'",[68,10196,2751],{"class":342},[68,10198,10199,10202,10205],{"class":70,"line":101},[68,10200,10201],{"class":342}," source: ",[68,10203,10204],{"class":409},"'blog/**/*.md'",[68,10206,2751],{"class":342},[68,10208,10209,10212,10214],{"class":70,"line":107},[68,10210,10211],{"class":342}," schema: z.",[68,10213,5094],{"class":338},[68,10215,1789],{"class":342},[68,10217,10218,10221,10223],{"class":70,"line":113},[68,10219,10220],{"class":342}," title: z.",[68,10222,3569],{"class":338},[68,10224,4746],{"class":342},[68,10226,10227,10230,10232],{"class":70,"line":119},[68,10228,10229],{"class":342}," description: z.",[68,10231,3569],{"class":338},[68,10233,4746],{"class":342},[68,10235,10236,10239,10241],{"class":70,"line":126},[68,10237,10238],{"class":342}," date: z.",[68,10240,10075],{"class":338},[68,10242,4746],{"class":342},[68,10244,10245,10248,10250],{"class":70,"line":132},[68,10246,10247],{"class":342}," category: z.",[68,10249,3569],{"class":338},[68,10251,4746],{"class":342},[68,10253,10254,10257,10260],{"class":70,"line":2135},[68,10255,10256],{"class":342}," readTime: z.",[68,10258,10259],{"class":338},"number",[68,10261,4746],{"class":342},[68,10263,10264,10267,10270,10273,10275],{"class":70,"line":2141},[68,10265,10266],{"class":342}," tags: z.",[68,10268,10269],{"class":338},"array",[68,10271,10272],{"class":342},"(z.",[68,10274,3569],{"class":338},[68,10276,10277],{"class":342},"()),\n",[68,10279,10280,10283,10286,10288,10290,10292,10294],{"class":70,"line":2437},[68,10281,10282],{"class":342}," draft: z.",[68,10284,10285],{"class":338},"boolean",[68,10287,2773],{"class":342},[68,10289,4522],{"class":338},[68,10291,343],{"class":342},[68,10293,8495],{"class":353},[68,10295,1814],{"class":342},[68,10297,10298],{"class":70,"line":2442},[68,10299,4736],{"class":342},[68,10301,10302],{"class":70,"line":2447},[68,10303,4736],{"class":342},[68,10305,10306],{"class":70,"line":2652},[68,10307,447],{"class":342},[20,10309,10310],{},"This gives you full TypeScript inference when querying content. If a post is missing a required field, the build fails with a clear error message. That beats discovering a broken page in production.",[15,10312,10314],{"id":10313},"building-the-blog-list-page","Building the Blog List Page",[20,10316,10317],{},"The blog index page queries all posts, sorts them, and filters out drafts:",[59,10319,10321],{"className":7755,"code":10320,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nconst { data: posts } = await useAsyncData('blog-posts', () =>\n queryCollection('blog')\n .where('draft', '=', false)\n .order('date', 'DESC')\n .all()\n)\n\u003C/script>\n\n\u003Ctemplate>\n \u003Cdiv class=\"max-w-4xl mx-auto px-4 py-12\">\n \u003Ch1 class=\"text-4xl font-bold mb-8\">Writing\u003C/h1>\n \u003Cdiv class=\"space-y-8\">\n \u003Carticle v-for=\"post in posts\" :key=\"post._path\" class=\"border-b pb-8\">\n \u003Ctime class=\"text-sm text-gray-500\">\n {{ new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}\n \u003C/time>\n \u003Ch2 class=\"text-2xl font-semibold mt-2\">\n \u003CNuxtLink :to=\"post._path\" class=\"hover:text-blue-600 transition-colors\">\n {{ post.title }}\n \u003C/NuxtLink>\n \u003C/h2>\n \u003Cp class=\"text-gray-600 mt-2\">{{ post.description }}\u003C/p>\n \u003Cdiv class=\"flex gap-2 mt-3\">\n \u003Cspan v-for=\"tag in post.tags\" :key=\"tag\"\n class=\"text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded\">\n {{ tag }}\n \u003C/span>\n \u003C/div>\n \u003C/article>\n \u003C/div>\n \u003C/div>\n\u003C/template>\n",[37,10322,10323,10339,10370,10382,10405,10424,10432,10436,10444,10448,10457,10475,10496,10511,10543,10559,10564,10573,10588,10611,10616,10624,10632,10652,10667,10687,10698,10703,10711,10719,10727,10735,10743],{"__ignoreMap":64},[68,10324,10325,10327,10329,10331,10333,10335,10337],{"class":70,"line":71},[68,10326,365],{"class":342},[68,10328,7772],{"class":7771},[68,10330,6980],{"class":338},[68,10332,7777],{"class":338},[68,10334,1593],{"class":342},[68,10336,7782],{"class":409},[68,10338,7785],{"class":342},[68,10340,10341,10343,10345,10347,10349,10351,10353,10355,10357,10360,10362,10365,10367],{"class":70,"line":77},[68,10342,1991],{"class":331},[68,10344,1748],{"class":342},[68,10346,4157],{"class":346},[68,10348,938],{"class":342},[68,10350,4162],{"class":353},[68,10352,1769],{"class":342},[68,10354,1593],{"class":331},[68,10356,385],{"class":331},[68,10358,10359],{"class":338}," useAsyncData",[68,10361,343],{"class":342},[68,10363,10364],{"class":409},"'blog-posts'",[68,10366,3101],{"class":342},[68,10368,10369],{"class":331},"=>\n",[68,10371,10372,10375,10377,10380],{"class":70,"line":83},[68,10373,10374],{"class":338}," queryCollection",[68,10376,343],{"class":342},[68,10378,10379],{"class":409},"'blog'",[68,10381,1702],{"class":342},[68,10383,10384,10386,10389,10391,10394,10396,10399,10401,10403],{"class":70,"line":89},[68,10385,2389],{"class":342},[68,10387,10388],{"class":338},"where",[68,10390,343],{"class":342},[68,10392,10393],{"class":409},"'draft'",[68,10395,180],{"class":342},[68,10397,10398],{"class":409},"'='",[68,10400,180],{"class":342},[68,10402,8495],{"class":353},[68,10404,1702],{"class":342},[68,10406,10407,10409,10412,10414,10417,10419,10422],{"class":70,"line":95},[68,10408,2389],{"class":342},[68,10410,10411],{"class":338},"order",[68,10413,343],{"class":342},[68,10415,10416],{"class":409},"'date'",[68,10418,180],{"class":342},[68,10420,10421],{"class":409},"'DESC'",[68,10423,1702],{"class":342},[68,10425,10426,10428,10430],{"class":70,"line":101},[68,10427,2389],{"class":342},[68,10429,4653],{"class":338},[68,10431,1602],{"class":342},[68,10433,10434],{"class":70,"line":107},[68,10435,1702],{"class":342},[68,10437,10438,10440,10442],{"class":70,"line":113},[68,10439,7811],{"class":342},[68,10441,7772],{"class":7771},[68,10443,7785],{"class":342},[68,10445,10446],{"class":70,"line":119},[68,10447,123],{"emptyLinePlaceholder":122},[68,10449,10450,10452,10455],{"class":70,"line":126},[68,10451,365],{"class":342},[68,10453,10454],{"class":7771},"template",[68,10456,7785],{"class":342},[68,10458,10459,10462,10465,10468,10470,10473],{"class":70,"line":132},[68,10460,10461],{"class":342}," \u003C",[68,10463,10464],{"class":7771},"div",[68,10466,10467],{"class":338}," class",[68,10469,1593],{"class":342},[68,10471,10472],{"class":409},"\"max-w-4xl mx-auto px-4 py-12\"",[68,10474,7785],{"class":342},[68,10476,10477,10479,10482,10484,10486,10489,10492,10494],{"class":70,"line":2135},[68,10478,10461],{"class":342},[68,10480,10481],{"class":7771},"h1",[68,10483,10467],{"class":338},[68,10485,1593],{"class":342},[68,10487,10488],{"class":409},"\"text-4xl font-bold mb-8\"",[68,10490,10491],{"class":342},">Writing\u003C/",[68,10493,10481],{"class":7771},[68,10495,7785],{"class":342},[68,10497,10498,10500,10502,10504,10506,10509],{"class":70,"line":2141},[68,10499,10461],{"class":342},[68,10501,10464],{"class":7771},[68,10503,10467],{"class":338},[68,10505,1593],{"class":342},[68,10507,10508],{"class":409},"\"space-y-8\"",[68,10510,7785],{"class":342},[68,10512,10513,10515,10518,10521,10523,10526,10529,10531,10534,10536,10538,10541],{"class":70,"line":2437},[68,10514,10461],{"class":342},[68,10516,10517],{"class":7771},"article",[68,10519,10520],{"class":338}," v-for",[68,10522,1593],{"class":342},[68,10524,10525],{"class":409},"\"post in posts\"",[68,10527,10528],{"class":338}," :key",[68,10530,1593],{"class":342},[68,10532,10533],{"class":409},"\"post._path\"",[68,10535,10467],{"class":338},[68,10537,1593],{"class":342},[68,10539,10540],{"class":409},"\"border-b pb-8\"",[68,10542,7785],{"class":342},[68,10544,10545,10547,10550,10552,10554,10557],{"class":70,"line":2442},[68,10546,10461],{"class":342},[68,10548,10549],{"class":7771},"time",[68,10551,10467],{"class":338},[68,10553,1593],{"class":342},[68,10555,10556],{"class":409},"\"text-sm text-gray-500\"",[68,10558,7785],{"class":342},[68,10560,10561],{"class":70,"line":2447},[68,10562,10563],{"class":342}," {{ new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }}\n",[68,10565,10566,10569,10571],{"class":70,"line":2652},[68,10567,10568],{"class":342}," \u003C/",[68,10570,10549],{"class":7771},[68,10572,7785],{"class":342},[68,10574,10575,10577,10579,10581,10583,10586],{"class":70,"line":2687},[68,10576,10461],{"class":342},[68,10578,15],{"class":7771},[68,10580,10467],{"class":338},[68,10582,1593],{"class":342},[68,10584,10585],{"class":409},"\"text-2xl font-semibold mt-2\"",[68,10587,7785],{"class":342},[68,10589,10590,10592,10595,10598,10600,10602,10604,10606,10609],{"class":70,"line":2692},[68,10591,10461],{"class":342},[68,10593,10594],{"class":7771},"NuxtLink",[68,10596,10597],{"class":338}," :to",[68,10599,1593],{"class":342},[68,10601,10533],{"class":409},[68,10603,10467],{"class":338},[68,10605,1593],{"class":342},[68,10607,10608],{"class":409},"\"hover:text-blue-600 transition-colors\"",[68,10610,7785],{"class":342},[68,10612,10613],{"class":70,"line":2697},[68,10614,10615],{"class":342}," {{ post.title }}\n",[68,10617,10618,10620,10622],{"class":70,"line":3129},[68,10619,10568],{"class":342},[68,10621,10594],{"class":7771},[68,10623,7785],{"class":342},[68,10625,10626,10628,10630],{"class":70,"line":3134},[68,10627,10568],{"class":342},[68,10629,15],{"class":7771},[68,10631,7785],{"class":342},[68,10633,10634,10636,10638,10640,10642,10645,10648,10650],{"class":70,"line":4749},[68,10635,10461],{"class":342},[68,10637,20],{"class":7771},[68,10639,10467],{"class":338},[68,10641,1593],{"class":342},[68,10643,10644],{"class":409},"\"text-gray-600 mt-2\"",[68,10646,10647],{"class":342},">{{ post.description }}\u003C/",[68,10649,20],{"class":7771},[68,10651,7785],{"class":342},[68,10653,10654,10656,10658,10660,10662,10665],{"class":70,"line":4755},[68,10655,10461],{"class":342},[68,10657,10464],{"class":7771},[68,10659,10467],{"class":338},[68,10661,1593],{"class":342},[68,10663,10664],{"class":409},"\"flex gap-2 mt-3\"",[68,10666,7785],{"class":342},[68,10668,10669,10671,10673,10675,10677,10680,10682,10684],{"class":70,"line":4760},[68,10670,10461],{"class":342},[68,10672,68],{"class":7771},[68,10674,10520],{"class":338},[68,10676,1593],{"class":342},[68,10678,10679],{"class":409},"\"tag in post.tags\"",[68,10681,10528],{"class":338},[68,10683,1593],{"class":342},[68,10685,10686],{"class":409},"\"tag\"\n",[68,10688,10689,10691,10693,10696],{"class":70,"line":4767},[68,10690,10467],{"class":338},[68,10692,1593],{"class":342},[68,10694,10695],{"class":409},"\"text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded\"",[68,10697,7785],{"class":342},[68,10699,10700],{"class":70,"line":4773},[68,10701,10702],{"class":342}," {{ tag }}\n",[68,10704,10705,10707,10709],{"class":70,"line":4779},[68,10706,10568],{"class":342},[68,10708,68],{"class":7771},[68,10710,7785],{"class":342},[68,10712,10713,10715,10717],{"class":70,"line":4785},[68,10714,10568],{"class":342},[68,10716,10464],{"class":7771},[68,10718,7785],{"class":342},[68,10720,10721,10723,10725],{"class":70,"line":4791},[68,10722,10568],{"class":342},[68,10724,10517],{"class":7771},[68,10726,7785],{"class":342},[68,10728,10729,10731,10733],{"class":70,"line":4797},[68,10730,10568],{"class":342},[68,10732,10464],{"class":7771},[68,10734,7785],{"class":342},[68,10736,10737,10739,10741],{"class":70,"line":4814},[68,10738,10568],{"class":342},[68,10740,10464],{"class":7771},[68,10742,7785],{"class":342},[68,10744,10745,10747,10749],{"class":70,"line":4819},[68,10746,7811],{"class":342},[68,10748,10454],{"class":7771},[68,10750,7785],{"class":342},[15,10752,10754],{"id":10753},"the-post-detail-page","The Post Detail Page",[20,10756,10757,10758,10761],{},"Create ",[37,10759,10760],{},"pages/blog/[...slug].vue"," to handle individual post rendering:",[59,10763,10765],{"className":7755,"code":10764,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst { data: post } = await useAsyncData(`post-${route.path}`, () =>\n queryCollection('blog').path(route.path).first()\n)\n\nIf (!post.value) {\n throw createError({ statusCode: 404, statusMessage: 'Post not found' })\n}\n\nUseSeoMeta({\n title: post.value.title,\n description: post.value.description,\n ogTitle: post.value.title,\n ogDescription: post.value.description,\n})\n\u003C/script>\n\n\u003Ctemplate>\n \u003Carticle class=\"max-w-3xl mx-auto px-4 py-12\" v-if=\"post\">\n \u003Cheader class=\"mb-8\">\n \u003Ch1 class=\"text-4xl font-bold leading-tight\">{{ post.title }}\u003C/h1>\n \u003Cdiv class=\"flex items-center gap-4 mt-4 text-sm text-gray-500\">\n \u003Ctime>{{ new Date(post.date).toLocaleDateString() }}\u003C/time>\n \u003Cspan>{{ post.readTime }} min read\u003C/span>\n \u003C/div>\n \u003C/header>\n \u003CContentRenderer :value=\"post\" class=\"prose prose-lg max-w-none\" />\n \u003C/article>\n\u003C/template>\n",[37,10766,10767,10783,10795,10834,10854,10858,10862,10873,10890,10894,10898,10905,10910,10915,10920,10925,10929,10937,10941,10949,10972,10988,11008,11023,11036,11049,11057,11065,11089,11097],{"__ignoreMap":64},[68,10768,10769,10771,10773,10775,10777,10779,10781],{"class":70,"line":71},[68,10770,365],{"class":342},[68,10772,7772],{"class":7771},[68,10774,6980],{"class":338},[68,10776,7777],{"class":338},[68,10778,1593],{"class":342},[68,10780,7782],{"class":409},[68,10782,7785],{"class":342},[68,10784,10785,10787,10789,10791,10793],{"class":70,"line":77},[68,10786,1991],{"class":331},[68,10788,4271],{"class":353},[68,10790,382],{"class":331},[68,10792,4276],{"class":338},[68,10794,1602],{"class":342},[68,10796,10797,10799,10801,10803,10805,10808,10810,10812,10814,10816,10818,10821,10824,10826,10828,10830,10832],{"class":70,"line":83},[68,10798,1991],{"class":331},[68,10800,1748],{"class":342},[68,10802,4157],{"class":346},[68,10804,938],{"class":342},[68,10806,10807],{"class":353},"post",[68,10809,1769],{"class":342},[68,10811,1593],{"class":331},[68,10813,385],{"class":331},[68,10815,10359],{"class":338},[68,10817,343],{"class":342},[68,10819,10820],{"class":409},"`post-${",[68,10822,10823],{"class":342},"route",[68,10825,51],{"class":409},[68,10827,7737],{"class":342},[68,10829,2682],{"class":409},[68,10831,3101],{"class":342},[68,10833,10369],{"class":331},[68,10835,10836,10838,10840,10842,10844,10846,10849,10852],{"class":70,"line":89},[68,10837,10374],{"class":338},[68,10839,343],{"class":342},[68,10841,10379],{"class":409},[68,10843,5115],{"class":342},[68,10845,7737],{"class":338},[68,10847,10848],{"class":342},"(route.path).",[68,10850,10851],{"class":338},"first",[68,10853,1602],{"class":342},[68,10855,10856],{"class":70,"line":95},[68,10857,1702],{"class":342},[68,10859,10860],{"class":70,"line":101},[68,10861,123],{"emptyLinePlaceholder":122},[68,10863,10864,10866,10868,10870],{"class":70,"line":107},[68,10865,2890],{"class":338},[68,10867,2657],{"class":342},[68,10869,3428],{"class":331},[68,10871,10872],{"class":342},"post.value) {\n",[68,10874,10875,10877,10879,10881,10883,10885,10888],{"class":70,"line":113},[68,10876,5281],{"class":331},[68,10878,4842],{"class":338},[68,10880,6648],{"class":342},[68,10882,4852],{"class":353},[68,10884,6654],{"class":342},[68,10886,10887],{"class":409},"'Post not found'",[68,10889,1859],{"class":342},[68,10891,10892],{"class":70,"line":119},[68,10893,447],{"class":342},[68,10895,10896],{"class":70,"line":126},[68,10897,123],{"emptyLinePlaceholder":122},[68,10899,10900,10903],{"class":70,"line":132},[68,10901,10902],{"class":338},"UseSeoMeta",[68,10904,1789],{"class":342},[68,10906,10907],{"class":70,"line":2135},[68,10908,10909],{"class":342}," title: post.value.title,\n",[68,10911,10912],{"class":70,"line":2141},[68,10913,10914],{"class":342}," description: post.value.description,\n",[68,10916,10917],{"class":70,"line":2437},[68,10918,10919],{"class":342}," ogTitle: post.value.title,\n",[68,10921,10922],{"class":70,"line":2442},[68,10923,10924],{"class":342}," ogDescription: post.value.description,\n",[68,10926,10927],{"class":70,"line":2447},[68,10928,2789],{"class":342},[68,10930,10931,10933,10935],{"class":70,"line":2652},[68,10932,7811],{"class":342},[68,10934,7772],{"class":7771},[68,10936,7785],{"class":342},[68,10938,10939],{"class":70,"line":2687},[68,10940,123],{"emptyLinePlaceholder":122},[68,10942,10943,10945,10947],{"class":70,"line":2692},[68,10944,365],{"class":342},[68,10946,10454],{"class":7771},[68,10948,7785],{"class":342},[68,10950,10951,10953,10955,10957,10959,10962,10965,10967,10970],{"class":70,"line":2697},[68,10952,10461],{"class":342},[68,10954,10517],{"class":7771},[68,10956,10467],{"class":338},[68,10958,1593],{"class":342},[68,10960,10961],{"class":409},"\"max-w-3xl mx-auto px-4 py-12\"",[68,10963,10964],{"class":338}," v-if",[68,10966,1593],{"class":342},[68,10968,10969],{"class":409},"\"post\"",[68,10971,7785],{"class":342},[68,10973,10974,10976,10979,10981,10983,10986],{"class":70,"line":3129},[68,10975,10461],{"class":342},[68,10977,10978],{"class":7771},"header",[68,10980,10467],{"class":338},[68,10982,1593],{"class":342},[68,10984,10985],{"class":409},"\"mb-8\"",[68,10987,7785],{"class":342},[68,10989,10990,10992,10994,10996,10998,11001,11004,11006],{"class":70,"line":3134},[68,10991,10461],{"class":342},[68,10993,10481],{"class":7771},[68,10995,10467],{"class":338},[68,10997,1593],{"class":342},[68,10999,11000],{"class":409},"\"text-4xl font-bold leading-tight\"",[68,11002,11003],{"class":342},">{{ post.title }}\u003C/",[68,11005,10481],{"class":7771},[68,11007,7785],{"class":342},[68,11009,11010,11012,11014,11016,11018,11021],{"class":70,"line":4749},[68,11011,10461],{"class":342},[68,11013,10464],{"class":7771},[68,11015,10467],{"class":338},[68,11017,1593],{"class":342},[68,11019,11020],{"class":409},"\"flex items-center gap-4 mt-4 text-sm text-gray-500\"",[68,11022,7785],{"class":342},[68,11024,11025,11027,11029,11032,11034],{"class":70,"line":4755},[68,11026,10461],{"class":342},[68,11028,10549],{"class":7771},[68,11030,11031],{"class":342},">{{ new Date(post.date).toLocaleDateString() }}\u003C/",[68,11033,10549],{"class":7771},[68,11035,7785],{"class":342},[68,11037,11038,11040,11042,11045,11047],{"class":70,"line":4760},[68,11039,10461],{"class":342},[68,11041,68],{"class":7771},[68,11043,11044],{"class":342},">{{ post.readTime }} min read\u003C/",[68,11046,68],{"class":7771},[68,11048,7785],{"class":342},[68,11050,11051,11053,11055],{"class":70,"line":4767},[68,11052,10568],{"class":342},[68,11054,10464],{"class":7771},[68,11056,7785],{"class":342},[68,11058,11059,11061,11063],{"class":70,"line":4773},[68,11060,10568],{"class":342},[68,11062,10978],{"class":7771},[68,11064,7785],{"class":342},[68,11066,11067,11069,11072,11075,11077,11079,11081,11083,11086],{"class":70,"line":4779},[68,11068,10461],{"class":342},[68,11070,11071],{"class":7771},"ContentRenderer",[68,11073,11074],{"class":338}," :value",[68,11076,1593],{"class":342},[68,11078,10969],{"class":409},[68,11080,10467],{"class":338},[68,11082,1593],{"class":342},[68,11084,11085],{"class":409},"\"prose prose-lg max-w-none\"",[68,11087,11088],{"class":342}," />\n",[68,11090,11091,11093,11095],{"class":70,"line":4785},[68,11092,10568],{"class":342},[68,11094,10517],{"class":7771},[68,11096,7785],{"class":342},[68,11098,11099,11101,11103],{"class":70,"line":4791},[68,11100,7811],{"class":342},[68,11102,10454],{"class":7771},[68,11104,7785],{"class":342},[20,11106,4139,11107,11109],{},[37,11108,11071],{}," component handles the heavy lifting — it renders your Markdown to HTML, processes MDC syntax, and applies any prose styles you have configured.",[15,11111,11113],{"id":11112},"custom-vue-components-in-markdown","Custom Vue Components in Markdown",[20,11115,11116],{},"This is where Nuxt Content separates itself. You can use Vue components directly in your Markdown files using MDC (Markdown Components) syntax:",[59,11118,11122],{"className":11119,"code":11120,"language":11121,"meta":64,"style":64},"language-markdown shiki shiki-themes github-dark","This is regular markdown text.\n\n::alert{type=\"warning\"}\nThis renders a custom Alert component with type=\"warning\" prop.\n::\n\nHere is some inline text with a :badge[New Feature] component.\n","markdown",[37,11123,11124,11129,11133,11138,11143,11148,11152],{"__ignoreMap":64},[68,11125,11126],{"class":70,"line":71},[68,11127,11128],{},"This is regular markdown text.\n",[68,11130,11131],{"class":70,"line":77},[68,11132,123],{"emptyLinePlaceholder":122},[68,11134,11135],{"class":70,"line":83},[68,11136,11137],{},"::alert{type=\"warning\"}\n",[68,11139,11140],{"class":70,"line":89},[68,11141,11142],{},"This renders a custom Alert component with type=\"warning\" prop.\n",[68,11144,11145],{"class":70,"line":95},[68,11146,11147],{},"::\n",[68,11149,11150],{"class":70,"line":101},[68,11151,123],{"emptyLinePlaceholder":122},[68,11153,11154],{"class":70,"line":107},[68,11155,11156],{},"Here is some inline text with a :badge[New Feature] component.\n",[20,11158,10757,11159,11162],{},[37,11160,11161],{},"components/content/Alert.vue"," and it auto-imports into your content. This is powerful for documentation sites where you need callout boxes, code sandboxes, or interactive demos embedded in articles.",[15,11164,11166],{"id":11165},"full-text-search","Full-Text Search",[20,11168,11169],{},"Nuxt Content includes a built-in search feature powered by a local index — no Algolia required for most use cases:",[59,11171,11173],{"className":7755,"code":11172,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nconst search = ref('')\nconst { data: results } = await useAsyncData(\n `search-${search.value}`,\n () => searchContent(search.value),\n { watch: [search] }\n)\n\u003C/script>\n",[37,11174,11175,11191,11208,11231,11248,11260,11265,11269],{"__ignoreMap":64},[68,11176,11177,11179,11181,11183,11185,11187,11189],{"class":70,"line":71},[68,11178,365],{"class":342},[68,11180,7772],{"class":7771},[68,11182,6980],{"class":338},[68,11184,7777],{"class":338},[68,11186,1593],{"class":342},[68,11188,7782],{"class":409},[68,11190,7785],{"class":342},[68,11192,11193,11195,11198,11200,11202,11204,11206],{"class":70,"line":77},[68,11194,1991],{"class":331},[68,11196,11197],{"class":353}," search",[68,11199,382],{"class":331},[68,11201,8584],{"class":338},[68,11203,343],{"class":342},[68,11205,9368],{"class":409},[68,11207,1702],{"class":342},[68,11209,11210,11212,11214,11216,11218,11221,11223,11225,11227,11229],{"class":70,"line":83},[68,11211,1991],{"class":331},[68,11213,1748],{"class":342},[68,11215,4157],{"class":346},[68,11217,938],{"class":342},[68,11219,11220],{"class":353},"results",[68,11222,1769],{"class":342},[68,11224,1593],{"class":331},[68,11226,385],{"class":331},[68,11228,10359],{"class":338},[68,11230,2488],{"class":342},[68,11232,11233,11236,11239,11241,11244,11246],{"class":70,"line":89},[68,11234,11235],{"class":409}," `search-${",[68,11237,11238],{"class":342},"search",[68,11240,51],{"class":409},[68,11242,11243],{"class":342},"value",[68,11245,2682],{"class":409},[68,11247,2751],{"class":342},[68,11249,11250,11252,11254,11257],{"class":70,"line":95},[68,11251,6969],{"class":342},[68,11253,1617],{"class":331},[68,11255,11256],{"class":338}," searchContent",[68,11258,11259],{"class":342},"(search.value),\n",[68,11261,11262],{"class":70,"line":101},[68,11263,11264],{"class":342}," { watch: [search] }\n",[68,11266,11267],{"class":70,"line":107},[68,11268,1702],{"class":342},[68,11270,11271,11273,11275],{"class":70,"line":113},[68,11272,7811],{"class":342},[68,11274,7772],{"class":7771},[68,11276,7785],{"class":342},[20,11278,11279],{},"For larger sites, Nuxt Content integrates with Algolia DocSearch if you need more advanced ranking and filtering. But for a blog with under a few hundred posts, the built-in search is excellent and requires no external service.",[15,11281,11283],{"id":11282},"rss-feed","RSS Feed",[20,11285,11286,11287,350],{},"Every blog needs an RSS feed. Add a server route at ",[37,11288,11289],{},"server/routes/rss.xml.ts",[59,11291,11293],{"className":316,"code":11292,"language":318,"meta":64,"style":64},"import { serverQueryContent } from '#content/server'\nimport RSS from 'rss'\n\nExport default defineEventHandler(async (event) => {\n const feed = new RSS({\n title: 'James Ross Jr. — Writing',\n site_url: 'https://jamesrossjr.com',\n feed_url: 'https://jamesrossjr.com/rss.xml',\n })\n\n const posts = await serverQueryContent(event, 'blog')\n .where({ draft: false })\n .sort({ date: -1 })\n .find()\n\n for (const post of posts) {\n feed.item({\n title: post.title,\n url: `https://jamesrossjr.com${post._path}`,\n description: post.description,\n date: post.date,\n })\n }\n\n setHeader(event, 'Content-Type', 'text/xml')\n return feed.xml()\n})\n",[37,11294,11295,11307,11319,11323,11345,11361,11371,11381,11391,11395,11399,11419,11432,11448,11457,11461,11478,11487,11492,11511,11516,11521,11525,11529,11533,11550,11561],{"__ignoreMap":64},[68,11296,11297,11299,11302,11304],{"class":70,"line":71},[68,11298,2037],{"class":331},[68,11300,11301],{"class":342}," { serverQueryContent } ",[68,11303,2043],{"class":331},[68,11305,11306],{"class":409}," '#content/server'\n",[68,11308,11309,11311,11314,11316],{"class":70,"line":77},[68,11310,2037],{"class":331},[68,11312,11313],{"class":342}," RSS ",[68,11315,2043],{"class":331},[68,11317,11318],{"class":409}," 'rss'\n",[68,11320,11321],{"class":70,"line":83},[68,11322,123],{"emptyLinePlaceholder":122},[68,11324,11325,11327,11329,11331,11333,11335,11337,11339,11341,11343],{"class":70,"line":89},[68,11326,4519],{"class":342},[68,11328,4522],{"class":331},[68,11330,4525],{"class":338},[68,11332,343],{"class":342},[68,11334,332],{"class":331},[68,11336,2657],{"class":342},[68,11338,4534],{"class":346},[68,11340,1869],{"class":342},[68,11342,1617],{"class":331},[68,11344,1620],{"class":342},[68,11346,11347,11349,11352,11354,11356,11359],{"class":70,"line":95},[68,11348,376],{"class":331},[68,11350,11351],{"class":353}," feed",[68,11353,382],{"class":331},[68,11355,2551],{"class":331},[68,11357,11358],{"class":338}," RSS",[68,11360,1789],{"class":342},[68,11362,11363,11366,11369],{"class":70,"line":101},[68,11364,11365],{"class":342}," title: ",[68,11367,11368],{"class":409},"'James Ross Jr. — Writing'",[68,11370,2751],{"class":342},[68,11372,11373,11376,11379],{"class":70,"line":107},[68,11374,11375],{"class":342}," site_url: ",[68,11377,11378],{"class":409},"'https://jamesrossjr.com'",[68,11380,2751],{"class":342},[68,11382,11383,11386,11389],{"class":70,"line":113},[68,11384,11385],{"class":342}," feed_url: ",[68,11387,11388],{"class":409},"'https://jamesrossjr.com/rss.xml'",[68,11390,2751],{"class":342},[68,11392,11393],{"class":70,"line":119},[68,11394,1859],{"class":342},[68,11396,11397],{"class":70,"line":126},[68,11398,123],{"emptyLinePlaceholder":122},[68,11400,11401,11403,11406,11408,11410,11413,11415,11417],{"class":70,"line":132},[68,11402,376],{"class":331},[68,11404,11405],{"class":353}," posts",[68,11407,382],{"class":331},[68,11409,385],{"class":331},[68,11411,11412],{"class":338}," serverQueryContent",[68,11414,5012],{"class":342},[68,11416,10379],{"class":409},[68,11418,1702],{"class":342},[68,11420,11421,11423,11425,11428,11430],{"class":70,"line":2135},[68,11422,2389],{"class":342},[68,11424,10388],{"class":338},[68,11426,11427],{"class":342},"({ draft: ",[68,11429,8495],{"class":353},[68,11431,1859],{"class":342},[68,11433,11434,11436,11439,11442,11444,11446],{"class":70,"line":2141},[68,11435,2389],{"class":342},[68,11437,11438],{"class":338},"sort",[68,11440,11441],{"class":342},"({ date: ",[68,11443,1639],{"class":331},[68,11445,2764],{"class":353},[68,11447,1859],{"class":342},[68,11449,11450,11452,11455],{"class":70,"line":2437},[68,11451,2389],{"class":342},[68,11453,11454],{"class":338},"find",[68,11456,1602],{"class":342},[68,11458,11459],{"class":70,"line":2442},[68,11460,123],{"emptyLinePlaceholder":122},[68,11462,11463,11465,11467,11469,11472,11475],{"class":70,"line":2447},[68,11464,2951],{"class":331},[68,11466,2657],{"class":342},[68,11468,1991],{"class":331},[68,11470,11471],{"class":353}," post",[68,11473,11474],{"class":331}," of",[68,11476,11477],{"class":342}," posts) {\n",[68,11479,11480,11483,11485],{"class":70,"line":2652},[68,11481,11482],{"class":342}," feed.",[68,11484,3597],{"class":338},[68,11486,1789],{"class":342},[68,11488,11489],{"class":70,"line":2687},[68,11490,11491],{"class":342}," title: post.title,\n",[68,11493,11494,11497,11500,11502,11504,11507,11509],{"class":70,"line":2692},[68,11495,11496],{"class":342}," url: ",[68,11498,11499],{"class":409},"`https://jamesrossjr.com${",[68,11501,10807],{"class":342},[68,11503,51],{"class":409},[68,11505,11506],{"class":342},"_path",[68,11508,2682],{"class":409},[68,11510,2751],{"class":342},[68,11512,11513],{"class":70,"line":2697},[68,11514,11515],{"class":342}," description: post.description,\n",[68,11517,11518],{"class":70,"line":3129},[68,11519,11520],{"class":342}," date: post.date,\n",[68,11522,11523],{"class":70,"line":3134},[68,11524,1859],{"class":342},[68,11526,11527],{"class":70,"line":4749},[68,11528,429],{"class":342},[68,11530,11531],{"class":70,"line":4755},[68,11532,123],{"emptyLinePlaceholder":122},[68,11534,11535,11538,11540,11543,11545,11548],{"class":70,"line":4760},[68,11536,11537],{"class":338}," setHeader",[68,11539,5012],{"class":342},[68,11541,11542],{"class":409},"'Content-Type'",[68,11544,180],{"class":342},[68,11546,11547],{"class":409},"'text/xml'",[68,11549,1702],{"class":342},[68,11551,11552,11554,11556,11559],{"class":70,"line":4767},[68,11553,418],{"class":331},[68,11555,11482],{"class":342},[68,11557,11558],{"class":338},"xml",[68,11560,1602],{"class":342},[68,11562,11563],{"class":70,"line":4773},[68,11564,2789],{"class":342},[15,11566,11568],{"id":11567},"sitemap-integration","Sitemap Integration",[20,11570,11571,11572,11575],{},"Install ",[37,11573,11574],{},"@nuxtjs/sitemap"," and it will automatically discover your content routes and include them in the generated sitemap. Add content-specific configuration if you want to control change frequency or priority:",[59,11577,11579],{"className":316,"code":11578,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nsitemap: {\n sources: ['/api/__sitemap__/urls'],\n defaults: {\n changefreq: 'weekly',\n priority: 0.8,\n },\n}\n",[37,11580,11581,11585,11592,11604,11611,11623,11635,11639],{"__ignoreMap":64},[68,11582,11583],{"class":70,"line":71},[68,11584,9151],{"class":325},[68,11586,11587,11590],{"class":70,"line":77},[68,11588,11589],{"class":338},"sitemap",[68,11591,7834],{"class":342},[68,11593,11594,11597,11599,11602],{"class":70,"line":83},[68,11595,11596],{"class":338}," sources",[68,11598,7842],{"class":342},[68,11600,11601],{"class":409},"'/api/__sitemap__/urls'",[68,11603,6051],{"class":342},[68,11605,11606,11609],{"class":70,"line":89},[68,11607,11608],{"class":338}," defaults",[68,11610,7834],{"class":342},[68,11612,11613,11616,11618,11621],{"class":70,"line":95},[68,11614,11615],{"class":338}," changefreq",[68,11617,938],{"class":342},[68,11619,11620],{"class":409},"'weekly'",[68,11622,2751],{"class":342},[68,11624,11625,11628,11630,11633],{"class":70,"line":101},[68,11626,11627],{"class":338}," priority",[68,11629,938],{"class":342},[68,11631,11632],{"class":353},"0.8",[68,11634,2751],{"class":342},[68,11636,11637],{"class":70,"line":107},[68,11638,3893],{"class":342},[68,11640,11641],{"class":70,"line":113},[68,11642,447],{"class":342},[15,11644,11646],{"id":11645},"deployment-and-static-generation","Deployment and Static Generation",[20,11648,11649,11650,11653],{},"For a purely static blog, run ",[37,11651,11652],{},"nuxt generate",". Nuxt Content works perfectly with static generation — all your Markdown gets processed at build time and you get fully static HTML files.",[20,11655,11656],{},"For sites that need server-side rendering (dynamic content, user-specific data), deploy to a Node.js host or use Cloudflare Pages with SSR enabled. Nuxt Content works in both modes without any configuration changes.",[20,11658,11659],{},"I recommend static generation for most blogs. The result is a fast, SEO-optimized site that can be hosted on Cloudflare Pages for free, with no server to maintain.",[15,11661,11663],{"id":11662},"the-pattern-i-follow-on-every-content-site","The Pattern I Follow on Every Content Site",[20,11665,11666,11667,11670],{},"Structure your content directory early and be consistent with your frontmatter schema. Add TypeScript validation to your content collection at the start of the project, not as an afterthought. Build the RSS feed on day one — it is a 30-minute task that pays dividends in reach. Use prose Tailwind plugin for article typography. Keep components in ",[37,11668,11669],{},"components/content/"," so they auto-import into MDC.",[20,11672,11673],{},"Nuxt Content is a mature, well-designed tool. Once you understand the content collection API and how MDC works, you can build sophisticated content sites quickly. The combination of Git-based content, Vue components in Markdown, and static generation is genuinely powerful.",[509,11675],{},[20,11677,11678,11679,51],{},"Building a content site with Nuxt and want help with architecture, SEO configuration, or deployment? Book a call and we can work through the specifics together: ",[502,11680,817],{"href":504,"rel":11681},[506],[509,11683],{},[15,11685,514],{"id":513},[516,11687,11688,11692,11696,11700],{},[519,11689,11690],{},[502,11691,4408],{"href":4407},[519,11693,11694],{},[502,11695,4414],{"href":4413},[519,11697,11698],{},[502,11699,4444],{"href":7234},[519,11701,11702],{},[502,11703,4396],{"href":4395},[544,11705,11706],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":64,"searchDepth":83,"depth":83,"links":11708},[11709,11710,11711,11712,11713,11714,11715,11716,11717,11718,11719,11720],{"id":9972,"depth":77,"text":9973},{"id":9988,"depth":77,"text":9989},{"id":10031,"depth":77,"text":10032},{"id":10313,"depth":77,"text":10314},{"id":10753,"depth":77,"text":10754},{"id":11112,"depth":77,"text":11113},{"id":11165,"depth":77,"text":11166},{"id":11282,"depth":77,"text":11283},{"id":11567,"depth":77,"text":11568},{"id":11645,"depth":77,"text":11646},{"id":11662,"depth":77,"text":11663},{"id":513,"depth":77,"text":514},"Everything you need to set up a production-ready blog using the Nuxt Content module — from MDX files to full-text search and RSS feeds.",[11723,11724],"Nuxt content module","Nuxt blog",{},{"title":4402,"description":11721},"blog/nuxt-content-module-guide",[4438,11729,11730],"Nuxt Content","Blogging","oxgKzuAumOkfqM2L5J7x0JSpc3CYIJrt7sziD68ZFWI",{"id":11733,"title":9919,"author":11734,"body":11735,"category":557,"date":558,"description":12429,"extension":560,"featured":561,"image":562,"keywords":12430,"meta":12432,"navigation":122,"path":9918,"readTime":107,"seo":12433,"stem":12434,"tags":12435,"__hash__":12437},"blog/blog/nuxt-deployment-vercel.md",{"name":9,"bio":10},{"type":12,"value":11736,"toc":12417},[11737,11740,11743,11747,11750,11756,11771,11780,11788,11791,11793,11796,11799,11898,11921,11935,11939,11942,11948,11954,11957,12011,12018,12024,12028,12034,12114,12125,12127,12130,12144,12147,12149,12152,12166,12179,12182,12186,12189,12199,12209,12215,12219,12222,12225,12327,12339,12343,12353,12359,12369,12379,12382,12384,12390,12392,12394,12414],[20,11738,11739],{},"Vercel is one of the best deployment platforms for Nuxt applications. The integration is genuinely good — connect a repository, configure a few settings, and you have a globally distributed application with preview deployments, automatic SSL, and a CDN in front of everything. For teams that value deployment simplicity, it is hard to beat.",[20,11741,11742],{},"But \"zero-config\" is a bit of a marketing claim. There are things to understand before you ship, particularly around environment variables, edge vs serverless functions, and the behavior differences between local development and Vercel's production environment.",[15,11744,11746],{"id":11745},"setting-up-the-deployment","Setting Up the Deployment",[20,11748,11749],{},"Import your repository into Vercel and configure the build settings:",[20,11751,11752,11755],{},[55,11753,11754],{},"Framework Preset:"," Nuxt.js (Vercel detects this automatically for most projects)",[20,11757,11758,1370,11761,11763,11764,11767,11768,357],{},[55,11759,11760],{},"Build Command:",[37,11762,9895],{}," (or ",[37,11765,11766],{},"pnpm build"," / ",[37,11769,11770],{},"yarn build",[20,11772,11773,1370,11776,11779],{},[55,11774,11775],{},"Output Directory:",[37,11777,11778],{},".output"," (Nuxt's Nitro output — Vercel knows this)",[20,11781,11782,1370,11785],{},[55,11783,11784],{},"Install Command:",[37,11786,11787],{},"npm install",[20,11789,11790],{},"For most Nuxt projects, Vercel's auto-detection handles all of this correctly. The one setting worth verifying manually is the Node.js version. Go to Settings > General > Node.js Version and confirm it matches what you are using locally. Mismatched Node versions are a common source of deployment-only bugs.",[15,11792,9252],{"id":9251},[20,11794,11795],{},"Vercel separates environment variables into three environments: Production, Preview, and Development. Configure them in Settings > Environment Variables.",[20,11797,11798],{},"Nuxt runtime config maps to environment variables by convention:",[59,11800,11802],{"className":316,"code":11801,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nruntimeConfig: {\n // Private — server only (no prefix)\n databaseUrl: '', // Set via DATABASE_URL\n openaiKey: '', // Set via OPENAI_KEY\n\n // Public — available client and server\n public: {\n apiBase: '', // Set via NUXT_PUBLIC_API_BASE\n analyticsId: '', // Set via NUXT_PUBLIC_ANALYTICS_ID\n },\n},\n",[37,11803,11804,11808,11814,11819,11833,11847,11851,11856,11862,11875,11889,11893],{"__ignoreMap":64},[68,11805,11806],{"class":70,"line":71},[68,11807,9151],{"class":325},[68,11809,11810,11812],{"class":70,"line":77},[68,11811,9356],{"class":338},[68,11813,7834],{"class":342},[68,11815,11816],{"class":70,"line":83},[68,11817,11818],{"class":325}," // Private — server only (no prefix)\n",[68,11820,11821,11824,11826,11828,11830],{"class":70,"line":89},[68,11822,11823],{"class":338}," databaseUrl",[68,11825,938],{"class":342},[68,11827,9368],{"class":409},[68,11829,180],{"class":342},[68,11831,11832],{"class":325},"// Set via DATABASE_URL\n",[68,11834,11835,11838,11840,11842,11844],{"class":70,"line":95},[68,11836,11837],{"class":338}," openaiKey",[68,11839,938],{"class":342},[68,11841,9368],{"class":409},[68,11843,180],{"class":342},[68,11845,11846],{"class":325},"// Set via OPENAI_KEY\n",[68,11848,11849],{"class":70,"line":101},[68,11850,123],{"emptyLinePlaceholder":122},[68,11852,11853],{"class":70,"line":107},[68,11854,11855],{"class":325}," // Public — available client and server\n",[68,11857,11858,11860],{"class":70,"line":113},[68,11859,9378],{"class":338},[68,11861,7834],{"class":342},[68,11863,11864,11866,11868,11870,11872],{"class":70,"line":119},[68,11865,9385],{"class":338},[68,11867,938],{"class":342},[68,11869,9368],{"class":409},[68,11871,180],{"class":342},[68,11873,11874],{"class":325},"// Set via NUXT_PUBLIC_API_BASE\n",[68,11876,11877,11880,11882,11884,11886],{"class":70,"line":126},[68,11878,11879],{"class":338}," analyticsId",[68,11881,938],{"class":342},[68,11883,9368],{"class":409},[68,11885,180],{"class":342},[68,11887,11888],{"class":325},"// Set via NUXT_PUBLIC_ANALYTICS_ID\n",[68,11890,11891],{"class":70,"line":132},[68,11892,3893],{"class":342},[68,11894,11895],{"class":70,"line":2135},[68,11896,11897],{"class":342},"},\n",[20,11899,11900,11901,11904,11905,11907,11908,11911,11912,946,11915,11911,11918,51],{},"The naming convention matters. Nuxt automatically maps ",[37,11902,11903],{},"NUXT_"," prefixed variables to the corresponding ",[37,11906,9356],{}," key. ",[37,11909,11910],{},"NUXT_DATABASE_URL"," maps to ",[37,11913,11914],{},"runtimeConfig.databaseUrl",[37,11916,11917],{},"NUXT_PUBLIC_API_BASE",[37,11919,11920],{},"runtimeConfig.public.apiBase",[20,11922,11923,11926,11927,11930,11931,11934],{},[55,11924,11925],{},"Critical security note:"," Never put secret keys in ",[37,11928,11929],{},"runtimeConfig.public",". These values are embedded in the client JavaScript bundle and are readable by anyone who views your page source. Only put values in ",[37,11932,11933],{},"public"," that are safe to expose.",[15,11936,11938],{"id":11937},"edge-functions-vs-serverless-functions","Edge Functions vs Serverless Functions",[20,11940,11941],{},"Vercel offers two function runtimes for Nuxt: Edge Functions and Serverless Functions. The choice matters.",[20,11943,11944,11947],{},[55,11945,11946],{},"Serverless Functions"," are Node.js. They have the full Node.js API available, support larger bundles, and support the complete Nuxt/Nitro feature set. They have cold starts (a delay on the first request after a period of inactivity) but are otherwise highly compatible.",[20,11949,11950,11953],{},[55,11951,11952],{},"Edge Functions"," run on Vercel's Edge Network — V8-based, globally distributed, zero cold start. They are faster for most requests but have limitations: no Node.js APIs, 4MB bundle limit, limited filesystem access.",[20,11955,11956],{},"Configure which runtime Nuxt uses:",[59,11958,11960],{"className":316,"code":11959,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nnitro: {\n preset: 'vercel', // Serverless Functions (default)\n // OR\n preset: 'vercel-edge', // Edge Functions\n},\n",[37,11961,11962,11966,11973,11988,11993,12007],{"__ignoreMap":64},[68,11963,11964],{"class":70,"line":71},[68,11965,9151],{"class":325},[68,11967,11968,11971],{"class":70,"line":77},[68,11969,11970],{"class":338},"nitro",[68,11972,7834],{"class":342},[68,11974,11975,11978,11980,11983,11985],{"class":70,"line":83},[68,11976,11977],{"class":338}," preset",[68,11979,938],{"class":342},[68,11981,11982],{"class":409},"'vercel'",[68,11984,180],{"class":342},[68,11986,11987],{"class":325},"// Serverless Functions (default)\n",[68,11989,11990],{"class":70,"line":89},[68,11991,11992],{"class":325}," // OR\n",[68,11994,11995,11997,11999,12002,12004],{"class":70,"line":95},[68,11996,11977],{"class":338},[68,11998,938],{"class":342},[68,12000,12001],{"class":409},"'vercel-edge'",[68,12003,180],{"class":342},[68,12005,12006],{"class":325},"// Edge Functions\n",[68,12008,12009],{"class":70,"line":101},[68,12010,11897],{"class":342},[20,12012,12013,12014,12017],{},"I default to ",[37,12015,12016],{},"vercel"," (Serverless) unless I have a specific reason to use Edge. The compatibility is better, the debugging is easier, and cold starts are negligible for most application traffic patterns.",[20,12019,9769,12020,12023],{},[37,12021,12022],{},"vercel-edge"," when: cold starts are unacceptable for your use case (real-time applications, APIs with SLA requirements), and you have verified your dependencies are compatible with the Edge runtime.",[15,12025,12027],{"id":12026},"per-route-edge-configuration","Per-Route Edge Configuration",[20,12029,12030,12031,12033],{},"Nuxt's ",[37,12032,9772],{}," gives you fine-grained control over caching and function runtime per route:",[59,12035,12037],{"className":316,"code":12036,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nrouteRules: {\n '/': { prerender: true }, // Static — no function needed\n '/blog/**': { swr: 86400 }, // Cache for 24 hours\n '/api/**': { headers: { 'cache-control': 'no-store' } }, // No cache\n '/dashboard/**': { ssr: true }, // Always SSR\n},\n",[37,12038,12039,12043,12049,12063,12076,12097,12110],{"__ignoreMap":64},[68,12040,12041],{"class":70,"line":71},[68,12042,9151],{"class":325},[68,12044,12045,12047],{"class":70,"line":77},[68,12046,9772],{"class":338},[68,12048,7834],{"class":342},[68,12050,12051,12053,12056,12058,12060],{"class":70,"line":83},[68,12052,9793],{"class":409},[68,12054,12055],{"class":342},": { prerender: ",[68,12057,3619],{"class":353},[68,12059,9802],{"class":342},[68,12061,12062],{"class":325},"// Static — no function needed\n",[68,12064,12065,12067,12069,12071,12073],{"class":70,"line":89},[68,12066,9810],{"class":409},[68,12068,9796],{"class":342},[68,12070,9815],{"class":353},[68,12072,9802],{"class":342},[68,12074,12075],{"class":325},"// Cache for 24 hours\n",[68,12077,12078,12080,12083,12086,12088,12091,12094],{"class":70,"line":95},[68,12079,9825],{"class":409},[68,12081,12082],{"class":342},": { headers: { ",[68,12084,12085],{"class":409},"'cache-control'",[68,12087,938],{"class":342},[68,12089,12090],{"class":409},"'no-store'",[68,12092,12093],{"class":342}," } }, ",[68,12095,12096],{"class":325},"// No cache\n",[68,12098,12099,12101,12103,12105,12107],{"class":70,"line":101},[68,12100,9840],{"class":409},[68,12102,9843],{"class":342},[68,12104,3619],{"class":353},[68,12106,9802],{"class":342},[68,12108,12109],{"class":325},"// Always SSR\n",[68,12111,12112],{"class":70,"line":107},[68,12113,11897],{"class":342},[20,12115,12116,12117,12120,12121,12124],{},"These rules map to Vercel's configuration automatically. ",[37,12118,12119],{},"prerender: true"," generates static HTML at build time and serves it from Vercel's CDN. ",[37,12122,12123],{},"swr: N"," configures stale-while-revalidate caching. This is the configuration that makes Nuxt on Vercel genuinely fast for content-heavy applications.",[15,12126,9741],{"id":9740},[20,12128,12129],{},"Every pull request gets a preview URL. This is one of Vercel's best features for team workflows. By default, preview deployments use the same environment variables as production. Override this for environment-specific values:",[516,12131,12132,12135,12138],{},[519,12133,12134],{},"Go to Settings > Environment Variables",[519,12136,12137],{},"For each variable, choose which environments it applies to",[519,12139,12140,12141,12143],{},"Set ",[37,12142,11917],{}," to your staging API URL for Preview environments",[20,12145,12146],{},"Be careful with databases. If your Preview deployments connect to production data, a bad deploy could corrupt production. Use a dedicated staging database for Preview environments or use branch databases (Neon, PlanetScale, and Supabase all support this).",[15,12148,9716],{"id":9715},[20,12150,12151],{},"Add custom domains in Settings > Domains. Vercel handles SSL automatically via Let's Encrypt. Configure your DNS:",[20,12153,12154,12157,12158,12161,12162,12165],{},[55,12155,12156],{},"For apex domains (yourdomain.com):"," Add an ",[37,12159,12160],{},"A"," record pointing to Vercel's IP (",[37,12163,12164],{},"76.76.19.61",") or use Vercel's nameservers.",[20,12167,12168,12171,12172,12175,12176,51],{},[55,12169,12170],{},"For subdomains (app.yourdomain.com):"," Add a ",[37,12173,12174],{},"CNAME"," record pointing to ",[37,12177,12178],{},"cname.vercel-dns.com",[20,12180,12181],{},"Vercel's automatic SSL renewal means you never worry about certificate expiration. Set it up once and forget it.",[15,12183,12185],{"id":12184},"build-performance","Build Performance",[20,12187,12188],{},"For large Nuxt applications, builds on Vercel can be slow. Optimize them:",[20,12190,12191,12194,12195,12198],{},[55,12192,12193],{},"Enable build caching."," Vercel caches the ",[37,12196,12197],{},"node_modules"," directory between builds. Make sure you are using a package-lock.json, yarn.lock, or pnpm-lock.yaml so the cache can be validated.",[20,12200,12201,12204,12205,12208],{},[55,12202,12203],{},"Use incremental static regeneration."," Instead of pre-rendering every page at build time (which is slow), use ",[37,12206,12207],{},"swr"," rules to generate pages on demand and cache them.",[20,12210,12211,12214],{},[55,12212,12213],{},"Reduce unnecessary pre-rendering."," Only pre-render pages that are high-traffic and truly static. Dynamic pages with user-specific content should use SSR or SWR, not pre-rendering.",[15,12216,12218],{"id":12217},"monitoring-and-observability","Monitoring and Observability",[20,12220,12221],{},"Vercel's built-in Analytics provides real-user performance data (Core Web Vitals from real visitors, not just Lighthouse). Enable it in your project settings — it is worth the data.",[20,12223,12224],{},"For application monitoring, integrate with your observability stack:",[59,12226,12228],{"className":316,"code":12227,"language":318,"meta":64,"style":64},"// plugins/sentry.ts\nexport default defineNuxtPlugin((nuxtApp) => {\n const config = useRuntimeConfig()\n\n if (config.public.sentryDsn) {\n Sentry.init({\n dsn: config.public.sentryDsn,\n environment: process.env.VERCEL_ENV || 'development',\n release: process.env.VERCEL_GIT_COMMIT_SHA,\n })\n }\n})\n",[37,12229,12230,12235,12253,12265,12269,12276,12286,12291,12305,12315,12319,12323],{"__ignoreMap":64},[68,12231,12232],{"class":70,"line":71},[68,12233,12234],{"class":325},"// plugins/sentry.ts\n",[68,12236,12237,12239,12241,12243,12245,12247,12249,12251],{"class":70,"line":77},[68,12238,4892],{"class":331},[68,12240,4895],{"class":331},[68,12242,8890],{"class":338},[68,12244,2525],{"class":342},[68,12246,4203],{"class":346},[68,12248,1869],{"class":342},[68,12250,1617],{"class":331},[68,12252,1620],{"class":342},[68,12254,12255,12257,12259,12261,12263],{"class":70,"line":83},[68,12256,376],{"class":331},[68,12258,9305],{"class":353},[68,12260,382],{"class":331},[68,12262,9310],{"class":338},[68,12264,1602],{"class":342},[68,12266,12267],{"class":70,"line":89},[68,12268,123],{"emptyLinePlaceholder":122},[68,12270,12271,12273],{"class":70,"line":95},[68,12272,400],{"class":331},[68,12274,12275],{"class":342}," (config.public.sentryDsn) {\n",[68,12277,12278,12281,12284],{"class":70,"line":101},[68,12279,12280],{"class":342}," Sentry.",[68,12282,12283],{"class":338},"init",[68,12285,1789],{"class":342},[68,12287,12288],{"class":70,"line":107},[68,12289,12290],{"class":342}," dsn: config.public.sentryDsn,\n",[68,12292,12293,12296,12299,12301,12303],{"class":70,"line":113},[68,12294,12295],{"class":342}," environment: process.env.",[68,12297,12298],{"class":353},"VERCEL_ENV",[68,12300,3053],{"class":331},[68,12302,6023],{"class":409},[68,12304,2751],{"class":342},[68,12306,12307,12310,12313],{"class":70,"line":119},[68,12308,12309],{"class":342}," release: process.env.",[68,12311,12312],{"class":353},"VERCEL_GIT_COMMIT_SHA",[68,12314,2751],{"class":342},[68,12316,12317],{"class":70,"line":126},[68,12318,1859],{"class":342},[68,12320,12321],{"class":70,"line":132},[68,12322,429],{"class":342},[68,12324,12325],{"class":70,"line":2135},[68,12326,2789],{"class":342},[20,12328,12329,12330,12332,12333,180,12335,12338],{},"Vercel exposes several useful environment variables automatically: ",[37,12331,12298],{}," (production/preview/development), ",[37,12334,12312],{},[37,12336,12337],{},"VERCEL_GIT_COMMIT_REF"," (branch name). Use these for release tracking and environment detection.",[15,12340,12342],{"id":12341},"common-gotchas","Common Gotchas",[20,12344,12345,12348,12349,12352],{},[55,12346,12347],{},"Filesystem writes fail silently."," Serverless functions on Vercel have a read-only filesystem (except ",[37,12350,12351],{},"/tmp",", which is ephemeral). Any code that writes files to disk — log files, generated content — will fail silently or with permissions errors. Use a database or object storage (S3, Cloudflare R2) instead.",[20,12354,12355,12358],{},[55,12356,12357],{},"Cold starts on infrequently visited routes."," Serverless functions cold start after 15 minutes of inactivity. For applications where every route needs instant response, this is a problem. Solutions: edge functions (no cold start), keep-alive pings (a hack), or accept the cold start on infrequently visited pages.",[20,12360,12361,12364,12365,12368],{},[55,12362,12363],{},"Middleware and edge functions behave differently."," Nuxt middleware that accesses browser APIs (window, document, localStorage) will fail if it runs in an edge function context. Guard these with ",[37,12366,12367],{},"process.client"," checks.",[20,12370,12371,12374,12375,12378],{},[55,12372,12373],{},"Bundle size limits."," Serverless functions on Vercel have a 50MB compressed bundle limit. If you install many dependencies, check your bundle size. Use ",[37,12376,12377],{},"ANALYZE=true npm run build"," to find large dependencies.",[20,12380,12381],{},"Vercel and Nuxt together are an excellent combination for most web applications. The deployment workflow is genuinely good, the performance is solid, and the preview deployment feature alone is worth the platform lock-in for teams that collaborate on UI changes.",[509,12383],{},[20,12385,12386,12387,51],{},"Deploying a Nuxt application on Vercel and running into infrastructure or configuration questions? I am happy to help troubleshoot. Book a call: ",[502,12388,817],{"href":504,"rel":12389},[506],[509,12391],{},[15,12393,514],{"id":513},[516,12395,12396,12400,12406,12410],{},[519,12397,12398],{},[502,12399,4414],{"href":4413},[519,12401,12402],{},[502,12403,12405],{"href":12404},"/blog/zero-to-production-nuxt-vercel","Zero to Production: My Nuxt + Vercel Deployment Pipeline",[519,12407,12408],{},[502,12409,4396],{"href":4395},[519,12411,12412],{},[502,12413,4402],{"href":4401},[544,12415,12416],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":64,"searchDepth":83,"depth":83,"links":12418},[12419,12420,12421,12422,12423,12424,12425,12426,12427,12428],{"id":11745,"depth":77,"text":11746},{"id":9251,"depth":77,"text":9252},{"id":11937,"depth":77,"text":11938},{"id":12026,"depth":77,"text":12027},{"id":9740,"depth":77,"text":9741},{"id":9715,"depth":77,"text":9716},{"id":12184,"depth":77,"text":12185},{"id":12217,"depth":77,"text":12218},{"id":12341,"depth":77,"text":12342},{"id":513,"depth":77,"text":514},"A complete guide to deploying Nuxt on Vercel — from initial setup to environment variables, edge functions, preview deployments, and the gotchas that catch developers by surprise.",[12431,9951],"Nuxt Vercel deployment",{},{"title":9919,"description":12429},"blog/nuxt-deployment-vercel",[4438,12436,9957],"Vercel","HdqWl-ulf-0D0ZxrWIU_SXgv3Uan8qGzZ5s-iMzeynk",{"id":12439,"title":12440,"author":12441,"body":12442,"category":557,"date":558,"description":14040,"extension":560,"featured":561,"image":562,"keywords":14041,"meta":14044,"navigation":122,"path":14045,"readTime":107,"seo":14046,"stem":14047,"tags":14048,"__hash__":14050},"blog/blog/nuxt-image-optimization.md","Image Optimization in Nuxt: @nuxt/image and Beyond",{"name":9,"bio":10},{"type":12,"value":12443,"toc":14024},[12444,12447,12454,12458,12476,12481,12642,12646,12656,12741,12744,12772,12778,12851,12858,12862,12868,12950,12964,12968,12974,12980,13016,13022,13059,13065,13119,13125,13129,13134,13137,13296,13302,13306,13314,13390,13396,13448,13452,13455,13511,13517,13521,13524,13527,13599,13606,13682,13685,13689,13695,13780,13783,13827,13831,13843,13963,13967,13970,13981,13984,13987,13989,13995,13997,13999,14021],[20,12445,12446],{},"Images are the most common source of web performance problems and the most commonly ignored optimization opportunity. In most web applications, images account for 50-80% of the total page weight. Getting your image pipeline right is one of the highest-leverage performance investments you can make.",[20,12448,12449,12450,12453],{},"Nuxt makes this relatively easy with ",[37,12451,12452],{},"@nuxt/image",", but using it correctly requires understanding what it does, what it does not do, and when the defaults need to be overridden.",[15,12455,12457],{"id":12456},"installing-nuxtimage","Installing @nuxt/image",[59,12459,12461],{"className":1888,"code":12460,"language":1890,"meta":64,"style":64},"npx nuxi module add image\n",[37,12462,12463],{"__ignoreMap":64},[68,12464,12465,12467,12469,12471,12473],{"class":70,"line":71},[68,12466,9664],{"class":338},[68,12468,10004],{"class":409},[68,12470,10007],{"class":409},[68,12472,10010],{"class":409},[68,12474,12475],{"class":409}," image\n",[20,12477,12478,12479,350],{},"Configure the module in ",[37,12480,7821],{},[59,12482,12484],{"className":316,"code":12483,"language":318,"meta":64,"style":64},"image: {\n formats: ['avif', 'webp'],\n quality: 80,\n screens: {\n xs: 320,\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1280,\n xxl: 1536,\n },\n providers: {\n cloudflare: {\n baseURL: 'https://yourdomain.com',\n },\n },\n},\n",[37,12485,12486,12493,12509,12521,12528,12540,12552,12564,12576,12588,12600,12604,12611,12618,12630,12634,12638],{"__ignoreMap":64},[68,12487,12488,12491],{"class":70,"line":71},[68,12489,12490],{"class":338},"image",[68,12492,7834],{"class":342},[68,12494,12495,12498,12500,12503,12505,12507],{"class":70,"line":77},[68,12496,12497],{"class":338}," formats",[68,12499,7842],{"class":342},[68,12501,12502],{"class":409},"'avif'",[68,12504,180],{"class":342},[68,12506,2826],{"class":409},[68,12508,6051],{"class":342},[68,12510,12511,12514,12516,12519],{"class":70,"line":83},[68,12512,12513],{"class":338}," quality",[68,12515,938],{"class":342},[68,12517,12518],{"class":353},"80",[68,12520,2751],{"class":342},[68,12522,12523,12526],{"class":70,"line":89},[68,12524,12525],{"class":338}," screens",[68,12527,7834],{"class":342},[68,12529,12530,12533,12535,12538],{"class":70,"line":95},[68,12531,12532],{"class":338}," xs",[68,12534,938],{"class":342},[68,12536,12537],{"class":353},"320",[68,12539,2751],{"class":342},[68,12541,12542,12545,12547,12550],{"class":70,"line":101},[68,12543,12544],{"class":338}," sm",[68,12546,938],{"class":342},[68,12548,12549],{"class":353},"640",[68,12551,2751],{"class":342},[68,12553,12554,12557,12559,12562],{"class":70,"line":107},[68,12555,12556],{"class":338}," md",[68,12558,938],{"class":342},[68,12560,12561],{"class":353},"768",[68,12563,2751],{"class":342},[68,12565,12566,12569,12571,12574],{"class":70,"line":113},[68,12567,12568],{"class":338}," lg",[68,12570,938],{"class":342},[68,12572,12573],{"class":353},"1024",[68,12575,2751],{"class":342},[68,12577,12578,12581,12583,12586],{"class":70,"line":119},[68,12579,12580],{"class":338}," xl",[68,12582,938],{"class":342},[68,12584,12585],{"class":353},"1280",[68,12587,2751],{"class":342},[68,12589,12590,12593,12595,12598],{"class":70,"line":126},[68,12591,12592],{"class":338}," xxl",[68,12594,938],{"class":342},[68,12596,12597],{"class":353},"1536",[68,12599,2751],{"class":342},[68,12601,12602],{"class":70,"line":132},[68,12603,3893],{"class":342},[68,12605,12606,12609],{"class":70,"line":2135},[68,12607,12608],{"class":338}," providers",[68,12610,7834],{"class":342},[68,12612,12613,12616],{"class":70,"line":2141},[68,12614,12615],{"class":338}," cloudflare",[68,12617,7834],{"class":342},[68,12619,12620,12623,12625,12628],{"class":70,"line":2437},[68,12621,12622],{"class":338}," baseURL",[68,12624,938],{"class":342},[68,12626,12627],{"class":409},"'https://yourdomain.com'",[68,12629,2751],{"class":342},[68,12631,12632],{"class":70,"line":2442},[68,12633,3893],{"class":342},[68,12635,12636],{"class":70,"line":2447},[68,12637,3893],{"class":342},[68,12639,12640],{"class":70,"line":2652},[68,12641,11897],{"class":342},[15,12643,12645],{"id":12644},"the-nuxtimg-component","The NuxtImg Component",[20,12647,12648,12649,12652,12653,350],{},"Replace every ",[37,12650,12651],{},"\u003Cimg>"," tag in your application with ",[37,12654,12655],{},"\u003CNuxtImg>",[59,12657,12659],{"className":7755,"code":12658,"language":7757,"meta":64,"style":64},"\u003CNuxtImg\n src=\"/images/hero.jpg\"\n alt=\"Hero image description\"\n width=\"1200\"\n height=\"630\"\n format=\"webp\"\n quality=\"85\"\n loading=\"lazy\"\n/>\n",[37,12660,12661,12668,12678,12688,12698,12708,12718,12727,12736],{"__ignoreMap":64},[68,12662,12663,12665],{"class":70,"line":71},[68,12664,365],{"class":342},[68,12666,12667],{"class":7771},"NuxtImg\n",[68,12669,12670,12673,12675],{"class":70,"line":77},[68,12671,12672],{"class":338}," src",[68,12674,1593],{"class":342},[68,12676,12677],{"class":409},"\"/images/hero.jpg\"\n",[68,12679,12680,12683,12685],{"class":70,"line":83},[68,12681,12682],{"class":338}," alt",[68,12684,1593],{"class":342},[68,12686,12687],{"class":409},"\"Hero image description\"\n",[68,12689,12690,12693,12695],{"class":70,"line":89},[68,12691,12692],{"class":338}," width",[68,12694,1593],{"class":342},[68,12696,12697],{"class":409},"\"1200\"\n",[68,12699,12700,12703,12705],{"class":70,"line":95},[68,12701,12702],{"class":338}," height",[68,12704,1593],{"class":342},[68,12706,12707],{"class":409},"\"630\"\n",[68,12709,12710,12713,12715],{"class":70,"line":101},[68,12711,12712],{"class":338}," format",[68,12714,1593],{"class":342},[68,12716,12717],{"class":409},"\"webp\"\n",[68,12719,12720,12722,12724],{"class":70,"line":107},[68,12721,12513],{"class":338},[68,12723,1593],{"class":342},[68,12725,12726],{"class":409},"\"85\"\n",[68,12728,12729,12731,12733],{"class":70,"line":113},[68,12730,8579],{"class":338},[68,12732,1593],{"class":342},[68,12734,12735],{"class":409},"\"lazy\"\n",[68,12737,12738],{"class":70,"line":119},[68,12739,12740],{"class":342},"/>\n",[20,12742,12743],{},"The component handles several things automatically:",[516,12745,12746,12749,12752,12761],{},[519,12747,12748],{},"Converts to modern formats (WebP, AVIF) based on browser support",[519,12750,12751],{},"Generates multiple sizes for responsive serving",[519,12753,12754,12755,12757,12758,12760],{},"Adds proper ",[37,12756,2346],{}," and ",[37,12759,2351],{}," attributes to prevent layout shift",[519,12762,12763,12764,12767,12768,12771],{},"Adds ",[37,12765,12766],{},"loading=\"lazy\""," by default (you override to ",[37,12769,12770],{},"\"eager\""," for above-fold images)",[20,12773,12774,12775,350],{},"For responsive images that change size across breakpoints, use ",[37,12776,12777],{},"sizes",[59,12779,12781],{"className":7755,"code":12780,"language":7757,"meta":64,"style":64},"\u003CNuxtImg\n src=\"/images/product.jpg\"\n alt=\"Product name\"\n sizes=\"100vw sm:50vw md:400px\"\n :width=\"800\"\n :height=\"600\"\n/>\n",[37,12782,12783,12789,12798,12807,12817,12833,12847],{"__ignoreMap":64},[68,12784,12785,12787],{"class":70,"line":71},[68,12786,365],{"class":342},[68,12788,12667],{"class":7771},[68,12790,12791,12793,12795],{"class":70,"line":77},[68,12792,12672],{"class":338},[68,12794,1593],{"class":342},[68,12796,12797],{"class":409},"\"/images/product.jpg\"\n",[68,12799,12800,12802,12804],{"class":70,"line":83},[68,12801,12682],{"class":338},[68,12803,1593],{"class":342},[68,12805,12806],{"class":409},"\"Product name\"\n",[68,12808,12809,12812,12814],{"class":70,"line":89},[68,12810,12811],{"class":338}," sizes",[68,12813,1593],{"class":342},[68,12815,12816],{"class":409},"\"100vw sm:50vw md:400px\"\n",[68,12818,12819,12822,12824,12826,12828,12830],{"class":70,"line":95},[68,12820,12821],{"class":342}," :",[68,12823,2346],{"class":338},[68,12825,1593],{"class":342},[68,12827,634],{"class":409},[68,12829,2814],{"class":353},[68,12831,12832],{"class":409},"\"\n",[68,12834,12835,12837,12839,12841,12843,12845],{"class":70,"line":101},[68,12836,12821],{"class":342},[68,12838,2351],{"class":338},[68,12840,1593],{"class":342},[68,12842,634],{"class":409},[68,12844,2820],{"class":353},[68,12846,12832],{"class":409},[68,12848,12849],{"class":70,"line":107},[68,12850,12740],{"class":342},[20,12852,12853,12854,12857],{},"This generates a ",[37,12855,12856],{},"srcset"," attribute with multiple image sizes. The browser downloads only the appropriate size for the current viewport. A user on a 375px mobile screen downloads a 375px image instead of an 800px image. That difference in download size is the difference between acceptable and excellent mobile performance.",[15,12859,12861],{"id":12860},"the-nuxtpicture-component","The NuxtPicture Component",[20,12863,12864,12865,350],{},"When you need more control over format fallbacks or want to serve different images for different art direction needs, use ",[37,12866,12867],{},"\u003CNuxtPicture>",[59,12869,12871],{"className":7755,"code":12870,"language":7757,"meta":64,"style":64},"\u003CNuxtPicture\n src=\"/images/hero.jpg\"\n alt=\"Hero description\"\n :width=\"1200\"\n :height=\"630\"\n loading=\"eager\"\n fetchpriority=\"high\"\n/>\n",[37,12872,12873,12880,12888,12897,12912,12927,12936,12946],{"__ignoreMap":64},[68,12874,12875,12877],{"class":70,"line":71},[68,12876,365],{"class":342},[68,12878,12879],{"class":7771},"NuxtPicture\n",[68,12881,12882,12884,12886],{"class":70,"line":77},[68,12883,12672],{"class":338},[68,12885,1593],{"class":342},[68,12887,12677],{"class":409},[68,12889,12890,12892,12894],{"class":70,"line":83},[68,12891,12682],{"class":338},[68,12893,1593],{"class":342},[68,12895,12896],{"class":409},"\"Hero description\"\n",[68,12898,12899,12901,12903,12905,12907,12910],{"class":70,"line":89},[68,12900,12821],{"class":342},[68,12902,2346],{"class":338},[68,12904,1593],{"class":342},[68,12906,634],{"class":409},[68,12908,12909],{"class":353},"1200",[68,12911,12832],{"class":409},[68,12913,12914,12916,12918,12920,12922,12925],{"class":70,"line":95},[68,12915,12821],{"class":342},[68,12917,2351],{"class":338},[68,12919,1593],{"class":342},[68,12921,634],{"class":409},[68,12923,12924],{"class":353},"630",[68,12926,12832],{"class":409},[68,12928,12929,12931,12933],{"class":70,"line":101},[68,12930,8579],{"class":338},[68,12932,1593],{"class":342},[68,12934,12935],{"class":409},"\"eager\"\n",[68,12937,12938,12941,12943],{"class":70,"line":107},[68,12939,12940],{"class":338}," fetchpriority",[68,12942,1593],{"class":342},[68,12944,12945],{"class":409},"\"high\"\n",[68,12947,12948],{"class":70,"line":113},[68,12949,12740],{"class":342},[20,12951,12952,12955,12956,12959,12960,12963],{},[37,12953,12954],{},"NuxtPicture"," renders a ",[37,12957,12958],{},"\u003Cpicture>"," element with ",[37,12961,12962],{},"\u003Csource>"," tags for each format, with the original as a fallback. Older browsers that do not support WebP or AVIF fall back gracefully to the original format.",[15,12965,12967],{"id":12966},"provider-configuration-where-images-come-from","Provider Configuration: Where Images Come From",[20,12969,12970,12971,12973],{},"For production applications, you rarely want to serve images from your own server. Use a CDN or image transformation service. ",[37,12972,12452],{}," supports many providers out of the box:",[20,12975,12976,12979],{},[55,12977,12978],{},"Cloudflare Images"," is my preferred choice when already using Cloudflare:",[59,12981,12983],{"className":316,"code":12982,"language":318,"meta":64,"style":64},"image: {\n cloudflare: {\n baseURL: 'https://imagedelivery.net/your-account-hash',\n },\n},\n",[37,12984,12985,12991,12997,13008,13012],{"__ignoreMap":64},[68,12986,12987,12989],{"class":70,"line":71},[68,12988,12490],{"class":338},[68,12990,7834],{"class":342},[68,12992,12993,12995],{"class":70,"line":77},[68,12994,12615],{"class":338},[68,12996,7834],{"class":342},[68,12998,12999,13001,13003,13006],{"class":70,"line":83},[68,13000,12622],{"class":338},[68,13002,938],{"class":342},[68,13004,13005],{"class":409},"'https://imagedelivery.net/your-account-hash'",[68,13007,2751],{"class":342},[68,13009,13010],{"class":70,"line":89},[68,13011,3893],{"class":342},[68,13013,13014],{"class":70,"line":95},[68,13015,11897],{"class":342},[20,13017,13018,13021],{},[55,13019,13020],{},"Imgix"," for more advanced transformations:",[59,13023,13025],{"className":316,"code":13024,"language":318,"meta":64,"style":64},"image: {\n imgix: {\n baseURL: 'https://your-subdomain.imgix.net',\n },\n},\n",[37,13026,13027,13033,13040,13051,13055],{"__ignoreMap":64},[68,13028,13029,13031],{"class":70,"line":71},[68,13030,12490],{"class":338},[68,13032,7834],{"class":342},[68,13034,13035,13038],{"class":70,"line":77},[68,13036,13037],{"class":338}," imgix",[68,13039,7834],{"class":342},[68,13041,13042,13044,13046,13049],{"class":70,"line":83},[68,13043,12622],{"class":338},[68,13045,938],{"class":342},[68,13047,13048],{"class":409},"'https://your-subdomain.imgix.net'",[68,13050,2751],{"class":342},[68,13052,13053],{"class":70,"line":89},[68,13054,3893],{"class":342},[68,13056,13057],{"class":70,"line":95},[68,13058,11897],{"class":342},[20,13060,13061,13064],{},[55,13062,13063],{},"IPX"," (the built-in local provider) works for development and small-scale production when you do not have a CDN:",[59,13066,13068],{"className":316,"code":13067,"language":318,"meta":64,"style":64},"image: {\n ipx: {\n maxAge: 60 * 60 * 24 * 7, // 7 days cache\n },\n},\n",[37,13069,13070,13076,13083,13111,13115],{"__ignoreMap":64},[68,13071,13072,13074],{"class":70,"line":71},[68,13073,12490],{"class":338},[68,13075,7834],{"class":342},[68,13077,13078,13081],{"class":70,"line":77},[68,13079,13080],{"class":338}," ipx",[68,13082,7834],{"class":342},[68,13084,13085,13088,13090,13092,13094,13096,13098,13101,13103,13106,13108],{"class":70,"line":83},[68,13086,13087],{"class":338}," maxAge",[68,13089,938],{"class":342},[68,13091,6763],{"class":353},[68,13093,3520],{"class":331},[68,13095,3523],{"class":353},[68,13097,3520],{"class":331},[68,13099,13100],{"class":353}," 24",[68,13102,3520],{"class":331},[68,13104,13105],{"class":353}," 7",[68,13107,180],{"class":342},[68,13109,13110],{"class":325},"// 7 days cache\n",[68,13112,13113],{"class":70,"line":89},[68,13114,3893],{"class":342},[68,13116,13117],{"class":70,"line":95},[68,13118,11897],{"class":342},[20,13120,13121,13122,13124],{},"Switch providers without changing your template code — just update the ",[37,13123,7821],{}," provider configuration.",[15,13126,13128],{"id":13127},"critical-image-performance-patterns","Critical Image Performance Patterns",[13130,13131,13133],"h3",{"id":13132},"preload-hero-images","Preload Hero Images",[20,13135,13136],{},"The LCP element is often a hero image. Preload it to tell the browser to fetch it immediately:",[59,13138,13140],{"className":7755,"code":13139,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nuseHead({\n link: [\n {\n rel: 'preload',\n as: 'image',\n href: '/images/hero.webp',\n type: 'image/webp',\n },\n ],\n})\n\u003C/script>\n\n\u003CNuxtImg\n src=\"/images/hero.jpg\"\n loading=\"eager\"\n fetchpriority=\"high\"\n alt=\"Hero description\"\n width=\"1200\"\n height=\"630\"\n/>\n",[37,13141,13142,13158,13165,13170,13174,13184,13194,13204,13213,13217,13222,13226,13234,13238,13244,13252,13260,13268,13276,13284,13292],{"__ignoreMap":64},[68,13143,13144,13146,13148,13150,13152,13154,13156],{"class":70,"line":71},[68,13145,365],{"class":342},[68,13147,7772],{"class":7771},[68,13149,6980],{"class":338},[68,13151,7777],{"class":338},[68,13153,1593],{"class":342},[68,13155,7782],{"class":409},[68,13157,7785],{"class":342},[68,13159,13160,13163],{"class":70,"line":77},[68,13161,13162],{"class":338},"useHead",[68,13164,1789],{"class":342},[68,13166,13167],{"class":70,"line":83},[68,13168,13169],{"class":342}," link: [\n",[68,13171,13172],{"class":70,"line":89},[68,13173,1620],{"class":342},[68,13175,13176,13179,13182],{"class":70,"line":95},[68,13177,13178],{"class":342}," rel: ",[68,13180,13181],{"class":409},"'preload'",[68,13183,2751],{"class":342},[68,13185,13186,13189,13192],{"class":70,"line":101},[68,13187,13188],{"class":342}," as: ",[68,13190,13191],{"class":409},"'image'",[68,13193,2751],{"class":342},[68,13195,13196,13199,13202],{"class":70,"line":107},[68,13197,13198],{"class":342}," href: ",[68,13200,13201],{"class":409},"'/images/hero.webp'",[68,13203,2751],{"class":342},[68,13205,13206,13208,13211],{"class":70,"line":113},[68,13207,8482],{"class":342},[68,13209,13210],{"class":409},"'image/webp'",[68,13212,2751],{"class":342},[68,13214,13215],{"class":70,"line":119},[68,13216,3893],{"class":342},[68,13218,13219],{"class":70,"line":126},[68,13220,13221],{"class":342}," ],\n",[68,13223,13224],{"class":70,"line":132},[68,13225,2789],{"class":342},[68,13227,13228,13230,13232],{"class":70,"line":2135},[68,13229,7811],{"class":342},[68,13231,7772],{"class":7771},[68,13233,7785],{"class":342},[68,13235,13236],{"class":70,"line":2141},[68,13237,123],{"emptyLinePlaceholder":122},[68,13239,13240,13242],{"class":70,"line":2437},[68,13241,365],{"class":342},[68,13243,12667],{"class":7771},[68,13245,13246,13248,13250],{"class":70,"line":2442},[68,13247,12672],{"class":338},[68,13249,1593],{"class":342},[68,13251,12677],{"class":409},[68,13253,13254,13256,13258],{"class":70,"line":2447},[68,13255,8579],{"class":338},[68,13257,1593],{"class":342},[68,13259,12935],{"class":409},[68,13261,13262,13264,13266],{"class":70,"line":2652},[68,13263,12940],{"class":338},[68,13265,1593],{"class":342},[68,13267,12945],{"class":409},[68,13269,13270,13272,13274],{"class":70,"line":2687},[68,13271,12682],{"class":338},[68,13273,1593],{"class":342},[68,13275,12896],{"class":409},[68,13277,13278,13280,13282],{"class":70,"line":2692},[68,13279,12692],{"class":338},[68,13281,1593],{"class":342},[68,13283,12697],{"class":409},[68,13285,13286,13288,13290],{"class":70,"line":2697},[68,13287,12702],{"class":338},[68,13289,1593],{"class":342},[68,13291,12707],{"class":409},[68,13293,13294],{"class":70,"line":3129},[68,13295,12740],{"class":342},[20,13297,13298,13299,13301],{},"Never use ",[37,13300,12766],{}," on above-the-fold images. Lazy loading defers the image fetch, which is exactly wrong for your LCP element.",[13130,13303,13305],{"id":13304},"prevent-layout-shift","Prevent Layout Shift",[20,13307,13308,13309,12757,13311,13313],{},"Always provide ",[37,13310,2346],{},[37,13312,2351],{}," attributes. The browser uses these to reserve space before the image loads, preventing layout shift:",[59,13315,13317],{"className":7755,"code":13316,"language":7757,"meta":64,"style":64},"\u003C!-- WRONG: no dimensions, causes layout shift -->\n\u003CNuxtImg src=\"/product.jpg\" alt=\"Product\" />\n\n\u003C!-- CORRECT: dimensions prevent layout shift -->\n\u003CNuxtImg src=\"/product.jpg\" alt=\"Product\" width=\"400\" height=\"300\" />\n",[37,13318,13319,13324,13347,13351,13356],{"__ignoreMap":64},[68,13320,13321],{"class":70,"line":71},[68,13322,13323],{"class":325},"\u003C!-- WRONG: no dimensions, causes layout shift -->\n",[68,13325,13326,13328,13331,13333,13335,13338,13340,13342,13345],{"class":70,"line":77},[68,13327,365],{"class":342},[68,13329,13330],{"class":7771},"NuxtImg",[68,13332,12672],{"class":338},[68,13334,1593],{"class":342},[68,13336,13337],{"class":409},"\"/product.jpg\"",[68,13339,12682],{"class":338},[68,13341,1593],{"class":342},[68,13343,13344],{"class":409},"\"Product\"",[68,13346,11088],{"class":342},[68,13348,13349],{"class":70,"line":83},[68,13350,123],{"emptyLinePlaceholder":122},[68,13352,13353],{"class":70,"line":89},[68,13354,13355],{"class":325},"\u003C!-- CORRECT: dimensions prevent layout shift -->\n",[68,13357,13358,13360,13362,13364,13366,13368,13370,13372,13374,13376,13378,13381,13383,13385,13388],{"class":70,"line":95},[68,13359,365],{"class":342},[68,13361,13330],{"class":7771},[68,13363,12672],{"class":338},[68,13365,1593],{"class":342},[68,13367,13337],{"class":409},[68,13369,12682],{"class":338},[68,13371,1593],{"class":342},[68,13373,13344],{"class":409},[68,13375,12692],{"class":338},[68,13377,1593],{"class":342},[68,13379,13380],{"class":409},"\"400\"",[68,13382,12702],{"class":338},[68,13384,1593],{"class":342},[68,13386,13387],{"class":409},"\"300\"",[68,13389,11088],{"class":342},[20,13391,4139,13392,13395],{},[37,13393,13394],{},"aspect-ratio"," CSS property works as an alternative when you do not know the exact dimensions:",[59,13397,13399],{"className":7755,"code":13398,"language":7757,"meta":64,"style":64},"\u003Cdiv class=\"aspect-[4/3] overflow-hidden\">\n \u003CNuxtImg\n src=\"/product.jpg\"\n alt=\"Product\"\n class=\"w-full h-full object-cover\"\n />\n\u003C/div>\n",[37,13400,13401,13416,13421,13426,13431,13436,13440],{"__ignoreMap":64},[68,13402,13403,13405,13407,13409,13411,13414],{"class":70,"line":71},[68,13404,365],{"class":342},[68,13406,10464],{"class":7771},[68,13408,10467],{"class":338},[68,13410,1593],{"class":342},[68,13412,13413],{"class":409},"\"aspect-[4/3] overflow-hidden\"",[68,13415,7785],{"class":342},[68,13417,13418],{"class":70,"line":77},[68,13419,13420],{"class":342}," \u003CNuxtImg\n",[68,13422,13423],{"class":70,"line":83},[68,13424,13425],{"class":342}," src=\"/product.jpg\"\n",[68,13427,13428],{"class":70,"line":89},[68,13429,13430],{"class":342}," alt=\"Product\"\n",[68,13432,13433],{"class":70,"line":95},[68,13434,13435],{"class":342}," class=\"w-full h-full object-cover\"\n",[68,13437,13438],{"class":70,"line":101},[68,13439,11088],{"class":342},[68,13441,13442,13444,13446],{"class":70,"line":107},[68,13443,7811],{"class":342},[68,13445,10464],{"class":7771},[68,13447,7785],{"class":342},[13130,13449,13451],{"id":13450},"blur-placeholders","Blur Placeholders",[20,13453,13454],{},"For images below the fold, a blur placeholder improves perceived performance. The user sees a blurred low-quality version immediately while the full image loads:",[59,13456,13458],{"className":7755,"code":13457,"language":7757,"meta":64,"style":64},"\u003CNuxtImg\n src=\"/images/blog-post.jpg\"\n alt=\"Blog post image\"\n width=\"800\"\n height=\"450\"\n placeholder\n/>\n",[37,13459,13460,13466,13475,13484,13493,13502,13507],{"__ignoreMap":64},[68,13461,13462,13464],{"class":70,"line":71},[68,13463,365],{"class":342},[68,13465,12667],{"class":7771},[68,13467,13468,13470,13472],{"class":70,"line":77},[68,13469,12672],{"class":338},[68,13471,1593],{"class":342},[68,13473,13474],{"class":409},"\"/images/blog-post.jpg\"\n",[68,13476,13477,13479,13481],{"class":70,"line":83},[68,13478,12682],{"class":338},[68,13480,1593],{"class":342},[68,13482,13483],{"class":409},"\"Blog post image\"\n",[68,13485,13486,13488,13490],{"class":70,"line":89},[68,13487,12692],{"class":338},[68,13489,1593],{"class":342},[68,13491,13492],{"class":409},"\"800\"\n",[68,13494,13495,13497,13499],{"class":70,"line":95},[68,13496,12702],{"class":338},[68,13498,1593],{"class":342},[68,13500,13501],{"class":409},"\"450\"\n",[68,13503,13504],{"class":70,"line":101},[68,13505,13506],{"class":338}," placeholder\n",[68,13508,13509],{"class":70,"line":107},[68,13510,12740],{"class":342},[20,13512,4139,13513,13516],{},[37,13514,13515],{},"placeholder"," prop generates a tiny base64 image that shows while the full image loads. On a slow connection, this is a significant UX improvement — the user sees content rather than a blank space or jumping layout.",[15,13518,13520],{"id":13519},"handling-cms-and-external-images","Handling CMS and External Images",[20,13522,13523],{},"When images come from a CMS or user-generated content, you need a different approach. You cannot rely on local paths — image URLs come from API responses.",[20,13525,13526],{},"Configure a domain allowlist for external images:",[59,13528,13530],{"className":316,"code":13529,"language":318,"meta":64,"style":64},"image: {\n domains: ['images.contentful.com', 'uploads.yourapp.com'],\n remotePatterns: [\n {\n protocol: 'https',\n hostname: '**.yourdomain.com',\n },\n ],\n},\n",[37,13531,13532,13538,13555,13563,13567,13577,13587,13591,13595],{"__ignoreMap":64},[68,13533,13534,13536],{"class":70,"line":71},[68,13535,12490],{"class":338},[68,13537,7834],{"class":342},[68,13539,13540,13543,13545,13548,13550,13553],{"class":70,"line":77},[68,13541,13542],{"class":338}," domains",[68,13544,7842],{"class":342},[68,13546,13547],{"class":409},"'images.contentful.com'",[68,13549,180],{"class":342},[68,13551,13552],{"class":409},"'uploads.yourapp.com'",[68,13554,6051],{"class":342},[68,13556,13557,13560],{"class":70,"line":83},[68,13558,13559],{"class":338}," remotePatterns",[68,13561,13562],{"class":342},": [\n",[68,13564,13565],{"class":70,"line":89},[68,13566,1620],{"class":342},[68,13568,13569,13572,13575],{"class":70,"line":95},[68,13570,13571],{"class":342}," protocol: ",[68,13573,13574],{"class":409},"'https'",[68,13576,2751],{"class":342},[68,13578,13579,13582,13585],{"class":70,"line":101},[68,13580,13581],{"class":342}," hostname: ",[68,13583,13584],{"class":409},"'**.yourdomain.com'",[68,13586,2751],{"class":342},[68,13588,13589],{"class":70,"line":107},[68,13590,3893],{"class":342},[68,13592,13593],{"class":70,"line":113},[68,13594,13221],{"class":342},[68,13596,13597],{"class":70,"line":119},[68,13598,11897],{"class":342},[20,13600,13601,13602,13605],{},"Use the ",[37,13603,13604],{},"src"," attribute with external URLs normally:",[59,13607,13609],{"className":7755,"code":13608,"language":7757,"meta":64,"style":64},"\u003CNuxtImg\n :src=\"post.coverImage.url\"\n :alt=\"post.coverImage.alt\"\n :width=\"post.coverImage.width\"\n :height=\"post.coverImage.height\"\n/>\n",[37,13610,13611,13617,13632,13648,13663,13678],{"__ignoreMap":64},[68,13612,13613,13615],{"class":70,"line":71},[68,13614,365],{"class":342},[68,13616,12667],{"class":7771},[68,13618,13619,13621,13623,13625,13627,13630],{"class":70,"line":77},[68,13620,12821],{"class":342},[68,13622,13604],{"class":338},[68,13624,1593],{"class":342},[68,13626,634],{"class":409},[68,13628,13629],{"class":342},"post.coverImage.url",[68,13631,12832],{"class":409},[68,13633,13634,13636,13639,13641,13643,13646],{"class":70,"line":83},[68,13635,12821],{"class":342},[68,13637,13638],{"class":338},"alt",[68,13640,1593],{"class":342},[68,13642,634],{"class":409},[68,13644,13645],{"class":342},"post.coverImage.alt",[68,13647,12832],{"class":409},[68,13649,13650,13652,13654,13656,13658,13661],{"class":70,"line":89},[68,13651,12821],{"class":342},[68,13653,2346],{"class":338},[68,13655,1593],{"class":342},[68,13657,634],{"class":409},[68,13659,13660],{"class":342},"post.coverImage.width",[68,13662,12832],{"class":409},[68,13664,13665,13667,13669,13671,13673,13676],{"class":70,"line":95},[68,13666,12821],{"class":342},[68,13668,2351],{"class":338},[68,13670,1593],{"class":342},[68,13672,634],{"class":409},[68,13674,13675],{"class":342},"post.coverImage.height",[68,13677,12832],{"class":409},[68,13679,13680],{"class":70,"line":101},[68,13681,12740],{"class":342},[20,13683,13684],{},"If the CMS provides image width and height metadata (Contentful and Sanity both do), use those values. If not, define reasonable defaults and use CSS to constrain the display dimensions.",[15,13686,13688],{"id":13687},"svg-when-to-not-use-nuxtimage","SVG: When to Not Use @nuxt/image",[20,13690,13691,13692,13694],{},"For SVG files, skip ",[37,13693,12452],{},". SVGs are already vector format and do not benefit from format conversion or resizing. Import them directly:",[59,13696,13698],{"className":7755,"code":13697,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nimport LogoIcon from '~/assets/icons/logo.svg?component'\n\u003C/script>\n\n\u003Ctemplate>\n \u003CLogoIcon class=\"w-8 h-8 text-blue-600\" aria-hidden=\"true\" />\n\u003C/template>\n",[37,13699,13700,13716,13728,13736,13740,13748,13772],{"__ignoreMap":64},[68,13701,13702,13704,13706,13708,13710,13712,13714],{"class":70,"line":71},[68,13703,365],{"class":342},[68,13705,7772],{"class":7771},[68,13707,6980],{"class":338},[68,13709,7777],{"class":338},[68,13711,1593],{"class":342},[68,13713,7782],{"class":409},[68,13715,7785],{"class":342},[68,13717,13718,13720,13723,13725],{"class":70,"line":77},[68,13719,2037],{"class":331},[68,13721,13722],{"class":342}," LogoIcon ",[68,13724,2043],{"class":331},[68,13726,13727],{"class":409}," '~/assets/icons/logo.svg?component'\n",[68,13729,13730,13732,13734],{"class":70,"line":83},[68,13731,7811],{"class":342},[68,13733,7772],{"class":7771},[68,13735,7785],{"class":342},[68,13737,13738],{"class":70,"line":89},[68,13739,123],{"emptyLinePlaceholder":122},[68,13741,13742,13744,13746],{"class":70,"line":95},[68,13743,365],{"class":342},[68,13745,10454],{"class":7771},[68,13747,7785],{"class":342},[68,13749,13750,13752,13755,13757,13759,13762,13765,13767,13770],{"class":70,"line":101},[68,13751,10461],{"class":342},[68,13753,13754],{"class":7771},"LogoIcon",[68,13756,10467],{"class":338},[68,13758,1593],{"class":342},[68,13760,13761],{"class":409},"\"w-8 h-8 text-blue-600\"",[68,13763,13764],{"class":338}," aria-hidden",[68,13766,1593],{"class":342},[68,13768,13769],{"class":409},"\"true\"",[68,13771,11088],{"class":342},[68,13773,13774,13776,13778],{"class":70,"line":107},[68,13775,7811],{"class":342},[68,13777,10454],{"class":7771},[68,13779,7785],{"class":342},[20,13781,13782],{},"Or for decorative SVGs that do not need to be styled:",[59,13784,13788],{"className":13785,"code":13786,"language":13787,"meta":64,"style":64},"language-html shiki shiki-themes github-dark","\u003Cimg src=\"/logo.svg\" alt=\"Company logo\" width=\"120\" height=\"40\" />\n","html",[37,13789,13790],{"__ignoreMap":64},[68,13791,13792,13794,13797,13799,13801,13804,13806,13808,13811,13813,13815,13818,13820,13822,13825],{"class":70,"line":71},[68,13793,365],{"class":342},[68,13795,13796],{"class":7771},"img",[68,13798,12672],{"class":338},[68,13800,1593],{"class":342},[68,13802,13803],{"class":409},"\"/logo.svg\"",[68,13805,12682],{"class":338},[68,13807,1593],{"class":342},[68,13809,13810],{"class":409},"\"Company logo\"",[68,13812,12692],{"class":338},[68,13814,1593],{"class":342},[68,13816,13817],{"class":409},"\"120\"",[68,13819,12702],{"class":338},[68,13821,1593],{"class":342},[68,13823,13824],{"class":409},"\"40\"",[68,13826,11088],{"class":342},[15,13828,13830],{"id":13829},"background-images","Background Images",[20,13832,13833,13834,13836,13837,12959,13839,13842],{},"CSS background images bypass ",[37,13835,12452],{},". For performance-critical background images, either switch to an ",[37,13838,12651],{},[37,13840,13841],{},"object-fit: cover",", or use the CSS image-set function with WebP:",[59,13844,13848],{"className":13845,"code":13846,"language":13847,"meta":64,"style":64},"language-css shiki shiki-themes github-dark",".hero {\n background-image: image-set(\n url('/images/hero.avif') type('image/avif'),\n url('/images/hero.webp') type('image/webp'),\n url('/images/hero.jpg') type('image/jpeg')\n );\n background-size: cover;\n}\n","css",[37,13849,13850,13857,13869,13888,13903,13941,13946,13959],{"__ignoreMap":64},[68,13851,13852,13855],{"class":70,"line":71},[68,13853,13854],{"class":338},".hero",[68,13856,1620],{"class":342},[68,13858,13859,13862,13864,13867],{"class":70,"line":77},[68,13860,13861],{"class":353}," background-image",[68,13863,938],{"class":342},[68,13865,13866],{"class":353},"image-set",[68,13868,2488],{"class":342},[68,13870,13871,13873,13875,13878,13880,13883,13886],{"class":70,"line":83},[68,13872,5789],{"class":353},[68,13874,343],{"class":342},[68,13876,13877],{"class":409},"'/images/hero.avif'",[68,13879,1869],{"class":342},[68,13881,13882],{"class":346},"type(",[68,13884,13885],{"class":409},"'image/avif'",[68,13887,1814],{"class":342},[68,13889,13890,13892,13894,13896,13899,13901],{"class":70,"line":89},[68,13891,5789],{"class":353},[68,13893,343],{"class":342},[68,13895,13201],{"class":409},[68,13897,13898],{"class":342},") type(",[68,13900,13210],{"class":409},[68,13902,1814],{"class":342},[68,13904,13905,13907,13910,13913,13915,13918,13920,13923,13926,13928,13931,13933,13935,13938],{"class":70,"line":95},[68,13906,5789],{"class":353},[68,13908,13909],{"class":342},"('/",[68,13911,13912],{"class":353},"images",[68,13914,1803],{"class":342},[68,13916,13917],{"class":353},"hero",[68,13919,51],{"class":342},[68,13921,13922],{"class":353},"jpg",[68,13924,13925],{"class":342},"') ",[68,13927,8321],{"class":353},[68,13929,13930],{"class":342},"('",[68,13932,12490],{"class":353},[68,13934,1803],{"class":342},[68,13936,13937],{"class":353},"jpeg",[68,13939,13940],{"class":342},"')\n",[68,13942,13943],{"class":70,"line":101},[68,13944,13945],{"class":342}," );\n",[68,13947,13948,13951,13953,13956],{"class":70,"line":107},[68,13949,13950],{"class":353}," background-size",[68,13952,938],{"class":342},[68,13954,13955],{"class":353},"cover",[68,13957,13958],{"class":342},";\n",[68,13960,13961],{"class":70,"line":113},[68,13962,447],{"class":342},[15,13964,13966],{"id":13965},"measuring-impact","Measuring Impact",[20,13968,13969],{},"After implementing image optimization, measure the impact in Google PageSpeed Insights and the Network tab of Chrome DevTools. Look for:",[516,13971,13972,13975,13978],{},[519,13973,13974],{},"Total image payload before and after (should drop significantly)",[519,13976,13977],{},"LCP improvement (faster images = faster LCP)",[519,13979,13980],{},"CLS score (should be 0 after adding dimensions to all images)",[20,13982,13983],{},"On one client project, implementing these patterns reduced total page weight by 65%, improved LCP from 3.8 seconds to 1.4 seconds, and fixed a CLS score that was causing ranking suppression. The work took about a day. The SEO recovery took about three weeks of Google re-crawling.",[20,13985,13986],{},"Images are not glamorous work, but the returns are real and measurable.",[509,13988],{},[20,13990,13991,13992,51],{},"Want help auditing your Nuxt application's image performance or designing a CDN and image delivery strategy? Book a call: ",[502,13993,817],{"href":504,"rel":13994},[506],[509,13996],{},[15,13998,514],{"id":513},[516,14000,14001,14007,14013,14017],{},[519,14002,14003],{},[502,14004,14006],{"href":14005},"/blog/image-optimization-web","Image Optimization for the Web: Formats, Compression, and Lazy Loading",[519,14008,14009],{},[502,14010,14012],{"href":14011},"/blog/nuxt-performance-optimization","Nuxt Performance: From Good Lighthouse Scores to Great Ones",[519,14014,14015],{},[502,14016,4020],{"href":4019},[519,14018,14019],{},[502,14020,4396],{"href":4395},[544,14022,14023],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":64,"searchDepth":83,"depth":83,"links":14025},[14026,14027,14028,14029,14030,14035,14036,14037,14038,14039],{"id":12456,"depth":77,"text":12457},{"id":12644,"depth":77,"text":12645},{"id":12860,"depth":77,"text":12861},{"id":12966,"depth":77,"text":12967},{"id":13127,"depth":77,"text":13128,"children":14031},[14032,14033,14034],{"id":13132,"depth":83,"text":13133},{"id":13304,"depth":83,"text":13305},{"id":13450,"depth":83,"text":13451},{"id":13519,"depth":77,"text":13520},{"id":13687,"depth":77,"text":13688},{"id":13829,"depth":77,"text":13830},{"id":13965,"depth":77,"text":13966},{"id":513,"depth":77,"text":514},"A complete guide to image optimization in Nuxt — @nuxt/image setup, lazy loading, modern formats, responsive images, and improving Core Web Vitals with every image decision.",[14042,14043],"Nuxt image optimization","Nuxt performance",{},"/blog/nuxt-image-optimization",{"title":12440,"description":14040},"blog/nuxt-image-optimization",[4438,4062,14049],"Images","1u6yHYnmpXyt-kV2sD5Na-e6_nZN6BlyrpfDlVKL_kQ",{"id":14052,"title":14053,"author":14054,"body":14055,"category":557,"date":558,"description":16596,"extension":560,"featured":561,"image":562,"keywords":16597,"meta":16600,"navigation":122,"path":16601,"readTime":107,"seo":16602,"stem":16603,"tags":16604,"__hash__":16606},"blog/blog/nuxt-internationalization.md","i18n in Nuxt: Adding Multi-Language Support Without the Pain",{"name":9,"bio":10},{"type":12,"value":14056,"toc":16583},[14057,14060,14063,14067,14085,14088,14340,14358,14362,14369,14537,14689,14692,14696,14706,14947,14963,14967,14970,15319,15323,15326,15364,15398,15402,15409,15513,15516,15550,15571,15614,15618,15621,15650,15656,15777,15780,15815,15819,16145,16158,16162,16173,16176,16228,16231,16235,16238,16547,16550,16552,16558,16560,16562,16580],[20,14058,14059],{},"Internationalization gets a reputation for being painful to add after the fact. That reputation is earned — retrofitting an application with translation support is genuinely tedious. But building it in from the start with the right tools is not that difficult, and the module ecosystem for Nuxt makes it more approachable than most frameworks.",[20,14061,14062],{},"This article walks through everything you need for a production-quality multi-language Nuxt application: routing, translation files, locale detection, SEO, and the common edge cases.",[15,14064,14066],{"id":14065},"installing-nuxtjsi18n","Installing @nuxtjs/i18n",[59,14068,14070],{"className":1888,"code":14069,"language":1890,"meta":64,"style":64},"npx nuxi module add i18n\n",[37,14071,14072],{"__ignoreMap":64},[68,14073,14074,14076,14078,14080,14082],{"class":70,"line":71},[68,14075,9664],{"class":338},[68,14077,10004],{"class":409},[68,14079,10007],{"class":409},[68,14081,10010],{"class":409},[68,14083,14084],{"class":409}," i18n\n",[20,14086,14087],{},"Basic configuration:",[59,14089,14091],{"className":316,"code":14090,"language":318,"meta":64,"style":64},"// nuxt.config.ts\ni18n: {\n strategy: 'prefix_except_default',\n defaultLocale: 'en',\n locales: [\n { code: 'en', language: 'en-US', name: 'English', dir: 'ltr', file: 'en.json' },\n { code: 'es', language: 'es-ES', name: 'Español', dir: 'ltr', file: 'es.json' },\n { code: 'ar', language: 'ar-SA', name: 'العربية', dir: 'rtl', file: 'ar.json' },\n { code: 'de', language: 'de-DE', name: 'Deutsch', dir: 'ltr', file: 'de.json' },\n ],\n lazy: true,\n langDir: 'locales',\n detectBrowserLanguage: {\n useCookie: true,\n cookieKey: 'i18n_redirected',\n alwaysRedirect: false,\n fallbackLocale: 'en',\n },\n},\n",[37,14092,14093,14097,14104,14116,14128,14135,14168,14196,14225,14253,14257,14268,14280,14287,14298,14310,14321,14332,14336],{"__ignoreMap":64},[68,14094,14095],{"class":70,"line":71},[68,14096,9151],{"class":325},[68,14098,14099,14102],{"class":70,"line":77},[68,14100,14101],{"class":338},"i18n",[68,14103,7834],{"class":342},[68,14105,14106,14109,14111,14114],{"class":70,"line":83},[68,14107,14108],{"class":338}," strategy",[68,14110,938],{"class":342},[68,14112,14113],{"class":409},"'prefix_except_default'",[68,14115,2751],{"class":342},[68,14117,14118,14121,14123,14126],{"class":70,"line":89},[68,14119,14120],{"class":338}," defaultLocale",[68,14122,938],{"class":342},[68,14124,14125],{"class":409},"'en'",[68,14127,2751],{"class":342},[68,14129,14130,14133],{"class":70,"line":95},[68,14131,14132],{"class":338}," locales",[68,14134,13562],{"class":342},[68,14136,14137,14140,14142,14145,14148,14151,14154,14157,14160,14163,14166],{"class":70,"line":101},[68,14138,14139],{"class":342}," { code: ",[68,14141,14125],{"class":409},[68,14143,14144],{"class":342},", language: ",[68,14146,14147],{"class":409},"'en-US'",[68,14149,14150],{"class":342},", name: ",[68,14152,14153],{"class":409},"'English'",[68,14155,14156],{"class":342},", dir: ",[68,14158,14159],{"class":409},"'ltr'",[68,14161,14162],{"class":342},", file: ",[68,14164,14165],{"class":409},"'en.json'",[68,14167,3893],{"class":342},[68,14169,14170,14172,14175,14177,14180,14182,14185,14187,14189,14191,14194],{"class":70,"line":107},[68,14171,14139],{"class":342},[68,14173,14174],{"class":409},"'es'",[68,14176,14144],{"class":342},[68,14178,14179],{"class":409},"'es-ES'",[68,14181,14150],{"class":342},[68,14183,14184],{"class":409},"'Español'",[68,14186,14156],{"class":342},[68,14188,14159],{"class":409},[68,14190,14162],{"class":342},[68,14192,14193],{"class":409},"'es.json'",[68,14195,3893],{"class":342},[68,14197,14198,14200,14203,14205,14208,14210,14213,14215,14218,14220,14223],{"class":70,"line":113},[68,14199,14139],{"class":342},[68,14201,14202],{"class":409},"'ar'",[68,14204,14144],{"class":342},[68,14206,14207],{"class":409},"'ar-SA'",[68,14209,14150],{"class":342},[68,14211,14212],{"class":409},"'العربية'",[68,14214,14156],{"class":342},[68,14216,14217],{"class":409},"'rtl'",[68,14219,14162],{"class":342},[68,14221,14222],{"class":409},"'ar.json'",[68,14224,3893],{"class":342},[68,14226,14227,14229,14232,14234,14237,14239,14242,14244,14246,14248,14251],{"class":70,"line":119},[68,14228,14139],{"class":342},[68,14230,14231],{"class":409},"'de'",[68,14233,14144],{"class":342},[68,14235,14236],{"class":409},"'de-DE'",[68,14238,14150],{"class":342},[68,14240,14241],{"class":409},"'Deutsch'",[68,14243,14156],{"class":342},[68,14245,14159],{"class":409},[68,14247,14162],{"class":342},[68,14249,14250],{"class":409},"'de.json'",[68,14252,3893],{"class":342},[68,14254,14255],{"class":70,"line":126},[68,14256,13221],{"class":342},[68,14258,14259,14262,14264,14266],{"class":70,"line":132},[68,14260,14261],{"class":338}," lazy",[68,14263,938],{"class":342},[68,14265,3619],{"class":353},[68,14267,2751],{"class":342},[68,14269,14270,14273,14275,14278],{"class":70,"line":2135},[68,14271,14272],{"class":338}," langDir",[68,14274,938],{"class":342},[68,14276,14277],{"class":409},"'locales'",[68,14279,2751],{"class":342},[68,14281,14282,14285],{"class":70,"line":2141},[68,14283,14284],{"class":338}," detectBrowserLanguage",[68,14286,7834],{"class":342},[68,14288,14289,14292,14294,14296],{"class":70,"line":2437},[68,14290,14291],{"class":338}," useCookie",[68,14293,938],{"class":342},[68,14295,3619],{"class":353},[68,14297,2751],{"class":342},[68,14299,14300,14303,14305,14308],{"class":70,"line":2442},[68,14301,14302],{"class":338}," cookieKey",[68,14304,938],{"class":342},[68,14306,14307],{"class":409},"'i18n_redirected'",[68,14309,2751],{"class":342},[68,14311,14312,14315,14317,14319],{"class":70,"line":2447},[68,14313,14314],{"class":338}," alwaysRedirect",[68,14316,938],{"class":342},[68,14318,8495],{"class":353},[68,14320,2751],{"class":342},[68,14322,14323,14326,14328,14330],{"class":70,"line":2652},[68,14324,14325],{"class":338}," fallbackLocale",[68,14327,938],{"class":342},[68,14329,14125],{"class":409},[68,14331,2751],{"class":342},[68,14333,14334],{"class":70,"line":2687},[68,14335,3893],{"class":342},[68,14337,14338],{"class":70,"line":2692},[68,14339,11897],{"class":342},[20,14341,4139,14342,14345,14346,14349,14350,14353,14354,14357],{},[37,14343,14344],{},"strategy: 'prefix_except_default'"," setting gives you URLs like ",[37,14347,14348],{},"/products"," for English and ",[37,14351,14352],{},"/es/products"," for Spanish, ",[37,14355,14356],{},"/ar/products"," for Arabic. The default locale has no prefix, which is the most common and most SEO-friendly approach.",[15,14359,14361],{"id":14360},"translation-files","Translation Files",[20,14363,14364,14365,14368],{},"Create your translation files in the ",[37,14366,14367],{},"locales/"," directory:",[59,14370,14374],{"className":14371,"code":14372,"language":14373,"meta":64,"style":64},"language-json shiki shiki-themes github-dark","// locales/en.json\n{\n \"nav\": {\n \"home\": \"Home\",\n \"products\": \"Products\",\n \"about\": \"About\",\n \"contact\": \"Contact\"\n },\n \"hero\": {\n \"title\": \"Build Better Software\",\n \"subtitle\": \"Strategic systems architecture for complex problems.\",\n \"cta\": \"Work with me\"\n },\n \"errors\": {\n \"notFound\": \"Page not found\",\n \"notFoundMessage\": \"The page you are looking for does not exist.\",\n \"backHome\": \"Go back home\"\n }\n}\n","json",[37,14375,14376,14381,14386,14393,14405,14417,14429,14439,14443,14450,14462,14474,14484,14488,14495,14507,14519,14529,14533],{"__ignoreMap":64},[68,14377,14378],{"class":70,"line":71},[68,14379,14380],{"class":325},"// locales/en.json\n",[68,14382,14383],{"class":70,"line":77},[68,14384,14385],{"class":342},"{\n",[68,14387,14388,14391],{"class":70,"line":83},[68,14389,14390],{"class":353}," \"nav\"",[68,14392,7834],{"class":342},[68,14394,14395,14398,14400,14403],{"class":70,"line":89},[68,14396,14397],{"class":353}," \"home\"",[68,14399,938],{"class":342},[68,14401,14402],{"class":409},"\"Home\"",[68,14404,2751],{"class":342},[68,14406,14407,14410,14412,14415],{"class":70,"line":95},[68,14408,14409],{"class":353}," \"products\"",[68,14411,938],{"class":342},[68,14413,14414],{"class":409},"\"Products\"",[68,14416,2751],{"class":342},[68,14418,14419,14422,14424,14427],{"class":70,"line":101},[68,14420,14421],{"class":353}," \"about\"",[68,14423,938],{"class":342},[68,14425,14426],{"class":409},"\"About\"",[68,14428,2751],{"class":342},[68,14430,14431,14434,14436],{"class":70,"line":107},[68,14432,14433],{"class":353}," \"contact\"",[68,14435,938],{"class":342},[68,14437,14438],{"class":409},"\"Contact\"\n",[68,14440,14441],{"class":70,"line":113},[68,14442,3893],{"class":342},[68,14444,14445,14448],{"class":70,"line":119},[68,14446,14447],{"class":353}," \"hero\"",[68,14449,7834],{"class":342},[68,14451,14452,14455,14457,14460],{"class":70,"line":126},[68,14453,14454],{"class":353}," \"title\"",[68,14456,938],{"class":342},[68,14458,14459],{"class":409},"\"Build Better Software\"",[68,14461,2751],{"class":342},[68,14463,14464,14467,14469,14472],{"class":70,"line":132},[68,14465,14466],{"class":353}," \"subtitle\"",[68,14468,938],{"class":342},[68,14470,14471],{"class":409},"\"Strategic systems architecture for complex problems.\"",[68,14473,2751],{"class":342},[68,14475,14476,14479,14481],{"class":70,"line":2135},[68,14477,14478],{"class":353}," \"cta\"",[68,14480,938],{"class":342},[68,14482,14483],{"class":409},"\"Work with me\"\n",[68,14485,14486],{"class":70,"line":2141},[68,14487,3893],{"class":342},[68,14489,14490,14493],{"class":70,"line":2437},[68,14491,14492],{"class":353}," \"errors\"",[68,14494,7834],{"class":342},[68,14496,14497,14500,14502,14505],{"class":70,"line":2442},[68,14498,14499],{"class":353}," \"notFound\"",[68,14501,938],{"class":342},[68,14503,14504],{"class":409},"\"Page not found\"",[68,14506,2751],{"class":342},[68,14508,14509,14512,14514,14517],{"class":70,"line":2447},[68,14510,14511],{"class":353}," \"notFoundMessage\"",[68,14513,938],{"class":342},[68,14515,14516],{"class":409},"\"The page you are looking for does not exist.\"",[68,14518,2751],{"class":342},[68,14520,14521,14524,14526],{"class":70,"line":2652},[68,14522,14523],{"class":353}," \"backHome\"",[68,14525,938],{"class":342},[68,14527,14528],{"class":409},"\"Go back home\"\n",[68,14530,14531],{"class":70,"line":2687},[68,14532,429],{"class":342},[68,14534,14535],{"class":70,"line":2692},[68,14536,447],{"class":342},[59,14538,14540],{"className":14371,"code":14539,"language":14373,"meta":64,"style":64},"// locales/es.json\n{\n \"nav\": {\n \"home\": \"Inicio\",\n \"products\": \"Productos\",\n \"about\": \"Sobre nosotros\",\n \"contact\": \"Contacto\"\n },\n \"hero\": {\n \"title\": \"Construye Mejor Software\",\n \"subtitle\": \"Arquitectura de sistemas estratégica para problemas complejos.\",\n \"cta\": \"Trabajar conmigo\"\n },\n \"errors\": {\n \"notFound\": \"Página no encontrada\",\n \"notFoundMessage\": \"La página que busca no existe.\",\n \"backHome\": \"Volver al inicio\"\n }\n}\n",[37,14541,14542,14547,14551,14557,14568,14579,14590,14599,14603,14609,14620,14631,14640,14644,14650,14661,14672,14681,14685],{"__ignoreMap":64},[68,14543,14544],{"class":70,"line":71},[68,14545,14546],{"class":325},"// locales/es.json\n",[68,14548,14549],{"class":70,"line":77},[68,14550,14385],{"class":342},[68,14552,14553,14555],{"class":70,"line":83},[68,14554,14390],{"class":353},[68,14556,7834],{"class":342},[68,14558,14559,14561,14563,14566],{"class":70,"line":89},[68,14560,14397],{"class":353},[68,14562,938],{"class":342},[68,14564,14565],{"class":409},"\"Inicio\"",[68,14567,2751],{"class":342},[68,14569,14570,14572,14574,14577],{"class":70,"line":95},[68,14571,14409],{"class":353},[68,14573,938],{"class":342},[68,14575,14576],{"class":409},"\"Productos\"",[68,14578,2751],{"class":342},[68,14580,14581,14583,14585,14588],{"class":70,"line":101},[68,14582,14421],{"class":353},[68,14584,938],{"class":342},[68,14586,14587],{"class":409},"\"Sobre nosotros\"",[68,14589,2751],{"class":342},[68,14591,14592,14594,14596],{"class":70,"line":107},[68,14593,14433],{"class":353},[68,14595,938],{"class":342},[68,14597,14598],{"class":409},"\"Contacto\"\n",[68,14600,14601],{"class":70,"line":113},[68,14602,3893],{"class":342},[68,14604,14605,14607],{"class":70,"line":119},[68,14606,14447],{"class":353},[68,14608,7834],{"class":342},[68,14610,14611,14613,14615,14618],{"class":70,"line":126},[68,14612,14454],{"class":353},[68,14614,938],{"class":342},[68,14616,14617],{"class":409},"\"Construye Mejor Software\"",[68,14619,2751],{"class":342},[68,14621,14622,14624,14626,14629],{"class":70,"line":132},[68,14623,14466],{"class":353},[68,14625,938],{"class":342},[68,14627,14628],{"class":409},"\"Arquitectura de sistemas estratégica para problemas complejos.\"",[68,14630,2751],{"class":342},[68,14632,14633,14635,14637],{"class":70,"line":2135},[68,14634,14478],{"class":353},[68,14636,938],{"class":342},[68,14638,14639],{"class":409},"\"Trabajar conmigo\"\n",[68,14641,14642],{"class":70,"line":2141},[68,14643,3893],{"class":342},[68,14645,14646,14648],{"class":70,"line":2437},[68,14647,14492],{"class":353},[68,14649,7834],{"class":342},[68,14651,14652,14654,14656,14659],{"class":70,"line":2442},[68,14653,14499],{"class":353},[68,14655,938],{"class":342},[68,14657,14658],{"class":409},"\"Página no encontrada\"",[68,14660,2751],{"class":342},[68,14662,14663,14665,14667,14670],{"class":70,"line":2447},[68,14664,14511],{"class":353},[68,14666,938],{"class":342},[68,14668,14669],{"class":409},"\"La página que busca no existe.\"",[68,14671,2751],{"class":342},[68,14673,14674,14676,14678],{"class":70,"line":2652},[68,14675,14523],{"class":353},[68,14677,938],{"class":342},[68,14679,14680],{"class":409},"\"Volver al inicio\"\n",[68,14682,14683],{"class":70,"line":2687},[68,14684,429],{"class":342},[68,14686,14687],{"class":70,"line":2692},[68,14688,447],{"class":342},[20,14690,14691],{},"The nested structure keeps translations organized. Be consistent with nesting depth across your locale files — mismatched structures are a common source of bugs.",[15,14693,14695],{"id":14694},"using-translations-in-components","Using Translations in Components",[20,14697,4139,14698,14701,14702,14705],{},[37,14699,14700],{},"$t"," function and ",[37,14703,14704],{},"useI18n"," composable are your primary tools:",[59,14707,14709],{"className":7755,"code":14708,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nconst { t, locale, locales, setLocale } = useI18n()\n\n// Locale-aware formatting\nconst { n, d } = useI18n()\n\nConst formattedPrice = n(29.99, 'currency', locale.value)\nconst formattedDate = d(new Date(), 'long', locale.value)\n\u003C/script>\n\n\u003Ctemplate>\n \u003Cnav>\n \u003CNuxtLink :to=\"localePath('/')\">{{ t('nav.home') }}\u003C/NuxtLink>\n \u003CNuxtLink :to=\"localePath('/products')\">{{ t('nav.products') }}\u003C/NuxtLink>\n \u003C/nav>\n\n \u003Ch1>{{ t('hero.title') }}\u003C/h1>\n\u003C/template>\n",[37,14710,14711,14727,14760,14764,14769,14791,14795,14818,14845,14853,14857,14865,14874,14894,14914,14922,14926,14939],{"__ignoreMap":64},[68,14712,14713,14715,14717,14719,14721,14723,14725],{"class":70,"line":71},[68,14714,365],{"class":342},[68,14716,7772],{"class":7771},[68,14718,6980],{"class":338},[68,14720,7777],{"class":338},[68,14722,1593],{"class":342},[68,14724,7782],{"class":409},[68,14726,7785],{"class":342},[68,14728,14729,14731,14733,14736,14738,14741,14743,14746,14748,14751,14753,14755,14758],{"class":70,"line":77},[68,14730,1991],{"class":331},[68,14732,1748],{"class":342},[68,14734,14735],{"class":353},"t",[68,14737,180],{"class":342},[68,14739,14740],{"class":353},"locale",[68,14742,180],{"class":342},[68,14744,14745],{"class":353},"locales",[68,14747,180],{"class":342},[68,14749,14750],{"class":353},"setLocale",[68,14752,1769],{"class":342},[68,14754,1593],{"class":331},[68,14756,14757],{"class":338}," useI18n",[68,14759,1602],{"class":342},[68,14761,14762],{"class":70,"line":83},[68,14763,123],{"emptyLinePlaceholder":122},[68,14765,14766],{"class":70,"line":89},[68,14767,14768],{"class":325},"// Locale-aware formatting\n",[68,14770,14771,14773,14775,14778,14780,14783,14785,14787,14789],{"class":70,"line":95},[68,14772,1991],{"class":331},[68,14774,1748],{"class":342},[68,14776,14777],{"class":353},"n",[68,14779,180],{"class":342},[68,14781,14782],{"class":353},"d",[68,14784,1769],{"class":342},[68,14786,1593],{"class":331},[68,14788,14757],{"class":338},[68,14790,1602],{"class":342},[68,14792,14793],{"class":70,"line":101},[68,14794,123],{"emptyLinePlaceholder":122},[68,14796,14797,14800,14802,14805,14807,14810,14812,14815],{"class":70,"line":107},[68,14798,14799],{"class":342},"Const formattedPrice ",[68,14801,1593],{"class":331},[68,14803,14804],{"class":338}," n",[68,14806,343],{"class":342},[68,14808,14809],{"class":353},"29.99",[68,14811,180],{"class":342},[68,14813,14814],{"class":409},"'currency'",[68,14816,14817],{"class":342},", locale.value)\n",[68,14819,14820,14822,14825,14827,14830,14832,14834,14837,14840,14843],{"class":70,"line":113},[68,14821,1991],{"class":331},[68,14823,14824],{"class":353}," formattedDate",[68,14826,382],{"class":331},[68,14828,14829],{"class":338}," d",[68,14831,343],{"class":342},[68,14833,2669],{"class":331},[68,14835,14836],{"class":338}," Date",[68,14838,14839],{"class":342},"(), ",[68,14841,14842],{"class":409},"'long'",[68,14844,14817],{"class":342},[68,14846,14847,14849,14851],{"class":70,"line":119},[68,14848,7811],{"class":342},[68,14850,7772],{"class":7771},[68,14852,7785],{"class":342},[68,14854,14855],{"class":70,"line":126},[68,14856,123],{"emptyLinePlaceholder":122},[68,14858,14859,14861,14863],{"class":70,"line":132},[68,14860,365],{"class":342},[68,14862,10454],{"class":7771},[68,14864,7785],{"class":342},[68,14866,14867,14869,14872],{"class":70,"line":2135},[68,14868,10461],{"class":342},[68,14870,14871],{"class":7771},"nav",[68,14873,7785],{"class":342},[68,14875,14876,14878,14880,14882,14884,14887,14890,14892],{"class":70,"line":2141},[68,14877,10461],{"class":342},[68,14879,10594],{"class":7771},[68,14881,10597],{"class":338},[68,14883,1593],{"class":342},[68,14885,14886],{"class":409},"\"localePath('/')\"",[68,14888,14889],{"class":342},">{{ t('nav.home') }}\u003C/",[68,14891,10594],{"class":7771},[68,14893,7785],{"class":342},[68,14895,14896,14898,14900,14902,14904,14907,14910,14912],{"class":70,"line":2437},[68,14897,10461],{"class":342},[68,14899,10594],{"class":7771},[68,14901,10597],{"class":338},[68,14903,1593],{"class":342},[68,14905,14906],{"class":409},"\"localePath('/products')\"",[68,14908,14909],{"class":342},">{{ t('nav.products') }}\u003C/",[68,14911,10594],{"class":7771},[68,14913,7785],{"class":342},[68,14915,14916,14918,14920],{"class":70,"line":2442},[68,14917,10568],{"class":342},[68,14919,14871],{"class":7771},[68,14921,7785],{"class":342},[68,14923,14924],{"class":70,"line":2447},[68,14925,123],{"emptyLinePlaceholder":122},[68,14927,14928,14930,14932,14935,14937],{"class":70,"line":2652},[68,14929,10461],{"class":342},[68,14931,10481],{"class":7771},[68,14933,14934],{"class":342},">{{ t('hero.title') }}\u003C/",[68,14936,10481],{"class":7771},[68,14938,7785],{"class":342},[68,14940,14941,14943,14945],{"class":70,"line":2687},[68,14942,7811],{"class":342},[68,14944,10454],{"class":7771},[68,14946,7785],{"class":342},[20,14948,4139,14949,14952,14953,14956,14957,14959,14960,14962],{},[37,14950,14951],{},"localePath"," composable generates locale-aware paths. ",[37,14954,14955],{},"localePath('/products')"," returns ",[37,14958,14348],{}," when English is active and ",[37,14961,14352],{}," when Spanish is active.",[15,14964,14966],{"id":14965},"locale-aware-dates-numbers-and-currencies","Locale-Aware Dates, Numbers, and Currencies",[20,14968,14969],{},"Use the built-in formatters rather than manual formatting. They are locale-aware and consistent:",[59,14971,14973],{"className":316,"code":14972,"language":318,"meta":64,"style":64},"// nuxt.config.ts\ni18n: {\n numberFormats: {\n en: {\n currency: {\n style: 'currency',\n currency: 'USD',\n notation: 'standard',\n },\n decimal: {\n style: 'decimal',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n },\n },\n es: {\n currency: {\n style: 'currency',\n currency: 'EUR',\n },\n },\n },\n datetimeFormats: {\n en: {\n short: { year: 'numeric', month: 'short', day: 'numeric' },\n long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },\n },\n es: {\n short: { year: 'numeric', month: 'short', day: 'numeric' },\n long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },\n },\n },\n},\n",[37,14974,14975,14979,14985,14992,14999,15006,15017,15028,15040,15044,15051,15062,15074,15085,15089,15093,15100,15106,15116,15127,15131,15135,15139,15146,15152,15189,15229,15233,15239,15269,15307,15311,15315],{"__ignoreMap":64},[68,14976,14977],{"class":70,"line":71},[68,14978,9151],{"class":325},[68,14980,14981,14983],{"class":70,"line":77},[68,14982,14101],{"class":338},[68,14984,7834],{"class":342},[68,14986,14987,14990],{"class":70,"line":83},[68,14988,14989],{"class":338}," numberFormats",[68,14991,7834],{"class":342},[68,14993,14994,14997],{"class":70,"line":89},[68,14995,14996],{"class":338}," en",[68,14998,7834],{"class":342},[68,15000,15001,15004],{"class":70,"line":95},[68,15002,15003],{"class":338}," currency",[68,15005,7834],{"class":342},[68,15007,15008,15011,15013,15015],{"class":70,"line":101},[68,15009,15010],{"class":338}," style",[68,15012,938],{"class":342},[68,15014,14814],{"class":409},[68,15016,2751],{"class":342},[68,15018,15019,15021,15023,15026],{"class":70,"line":107},[68,15020,15003],{"class":338},[68,15022,938],{"class":342},[68,15024,15025],{"class":409},"'USD'",[68,15027,2751],{"class":342},[68,15029,15030,15033,15035,15038],{"class":70,"line":113},[68,15031,15032],{"class":338}," notation",[68,15034,938],{"class":342},[68,15036,15037],{"class":409},"'standard'",[68,15039,2751],{"class":342},[68,15041,15042],{"class":70,"line":119},[68,15043,3893],{"class":342},[68,15045,15046,15049],{"class":70,"line":126},[68,15047,15048],{"class":338}," decimal",[68,15050,7834],{"class":342},[68,15052,15053,15055,15057,15060],{"class":70,"line":132},[68,15054,15010],{"class":338},[68,15056,938],{"class":342},[68,15058,15059],{"class":409},"'decimal'",[68,15061,2751],{"class":342},[68,15063,15064,15067,15069,15072],{"class":70,"line":2135},[68,15065,15066],{"class":338}," minimumFractionDigits",[68,15068,938],{"class":342},[68,15070,15071],{"class":353},"2",[68,15073,2751],{"class":342},[68,15075,15076,15079,15081,15083],{"class":70,"line":2141},[68,15077,15078],{"class":338}," maximumFractionDigits",[68,15080,938],{"class":342},[68,15082,15071],{"class":353},[68,15084,2751],{"class":342},[68,15086,15087],{"class":70,"line":2437},[68,15088,3893],{"class":342},[68,15090,15091],{"class":70,"line":2442},[68,15092,3893],{"class":342},[68,15094,15095,15098],{"class":70,"line":2447},[68,15096,15097],{"class":338}," es",[68,15099,7834],{"class":342},[68,15101,15102,15104],{"class":70,"line":2652},[68,15103,15003],{"class":338},[68,15105,7834],{"class":342},[68,15107,15108,15110,15112,15114],{"class":70,"line":2687},[68,15109,15010],{"class":338},[68,15111,938],{"class":342},[68,15113,14814],{"class":409},[68,15115,2751],{"class":342},[68,15117,15118,15120,15122,15125],{"class":70,"line":2692},[68,15119,15003],{"class":338},[68,15121,938],{"class":342},[68,15123,15124],{"class":409},"'EUR'",[68,15126,2751],{"class":342},[68,15128,15129],{"class":70,"line":2697},[68,15130,3893],{"class":342},[68,15132,15133],{"class":70,"line":3129},[68,15134,3893],{"class":342},[68,15136,15137],{"class":70,"line":3134},[68,15138,3893],{"class":342},[68,15140,15141,15144],{"class":70,"line":4749},[68,15142,15143],{"class":338}," datetimeFormats",[68,15145,7834],{"class":342},[68,15147,15148,15150],{"class":70,"line":4755},[68,15149,14996],{"class":338},[68,15151,7834],{"class":342},[68,15153,15154,15157,15160,15163,15165,15168,15170,15173,15175,15178,15180,15183,15185,15187],{"class":70,"line":4760},[68,15155,15156],{"class":338}," short",[68,15158,15159],{"class":342},": { ",[68,15161,15162],{"class":338},"year",[68,15164,938],{"class":342},[68,15166,15167],{"class":409},"'numeric'",[68,15169,180],{"class":342},[68,15171,15172],{"class":338},"month",[68,15174,938],{"class":342},[68,15176,15177],{"class":409},"'short'",[68,15179,180],{"class":342},[68,15181,15182],{"class":338},"day",[68,15184,938],{"class":342},[68,15186,15167],{"class":409},[68,15188,3893],{"class":342},[68,15190,15191,15194,15196,15198,15200,15202,15204,15206,15208,15210,15212,15214,15216,15218,15220,15223,15225,15227],{"class":70,"line":4767},[68,15192,15193],{"class":338}," long",[68,15195,15159],{"class":342},[68,15197,15162],{"class":338},[68,15199,938],{"class":342},[68,15201,15167],{"class":409},[68,15203,180],{"class":342},[68,15205,15172],{"class":338},[68,15207,938],{"class":342},[68,15209,14842],{"class":409},[68,15211,180],{"class":342},[68,15213,15182],{"class":338},[68,15215,938],{"class":342},[68,15217,15167],{"class":409},[68,15219,180],{"class":342},[68,15221,15222],{"class":338},"weekday",[68,15224,938],{"class":342},[68,15226,14842],{"class":409},[68,15228,3893],{"class":342},[68,15230,15231],{"class":70,"line":4773},[68,15232,3893],{"class":342},[68,15234,15235,15237],{"class":70,"line":4779},[68,15236,15097],{"class":338},[68,15238,7834],{"class":342},[68,15240,15241,15243,15245,15247,15249,15251,15253,15255,15257,15259,15261,15263,15265,15267],{"class":70,"line":4785},[68,15242,15156],{"class":338},[68,15244,15159],{"class":342},[68,15246,15162],{"class":338},[68,15248,938],{"class":342},[68,15250,15167],{"class":409},[68,15252,180],{"class":342},[68,15254,15172],{"class":338},[68,15256,938],{"class":342},[68,15258,15177],{"class":409},[68,15260,180],{"class":342},[68,15262,15182],{"class":338},[68,15264,938],{"class":342},[68,15266,15167],{"class":409},[68,15268,3893],{"class":342},[68,15270,15271,15273,15275,15277,15279,15281,15283,15285,15287,15289,15291,15293,15295,15297,15299,15301,15303,15305],{"class":70,"line":4791},[68,15272,15193],{"class":338},[68,15274,15159],{"class":342},[68,15276,15162],{"class":338},[68,15278,938],{"class":342},[68,15280,15167],{"class":409},[68,15282,180],{"class":342},[68,15284,15172],{"class":338},[68,15286,938],{"class":342},[68,15288,14842],{"class":409},[68,15290,180],{"class":342},[68,15292,15182],{"class":338},[68,15294,938],{"class":342},[68,15296,15167],{"class":409},[68,15298,180],{"class":342},[68,15300,15222],{"class":338},[68,15302,938],{"class":342},[68,15304,14842],{"class":409},[68,15306,3893],{"class":342},[68,15308,15309],{"class":70,"line":4797},[68,15310,3893],{"class":342},[68,15312,15313],{"class":70,"line":4814},[68,15314,3893],{"class":342},[68,15316,15317],{"class":70,"line":4819},[68,15318,11897],{"class":342},[15,15320,15322],{"id":15321},"pluralization","Pluralization",[20,15324,15325],{},"Translation strings often need to vary based on count. Use the built-in plural handling:",[59,15327,15329],{"className":14371,"code":15328,"language":14373,"meta":64,"style":64},"// locales/en.json\n{\n \"cart\": {\n \"items\": \"No items | One item | {count} items\"\n }\n}\n",[37,15330,15331,15335,15339,15346,15356,15360],{"__ignoreMap":64},[68,15332,15333],{"class":70,"line":71},[68,15334,14380],{"class":325},[68,15336,15337],{"class":70,"line":77},[68,15338,14385],{"class":342},[68,15340,15341,15344],{"class":70,"line":83},[68,15342,15343],{"class":353}," \"cart\"",[68,15345,7834],{"class":342},[68,15347,15348,15351,15353],{"class":70,"line":89},[68,15349,15350],{"class":353}," \"items\"",[68,15352,938],{"class":342},[68,15354,15355],{"class":409},"\"No items | One item | {count} items\"\n",[68,15357,15358],{"class":70,"line":95},[68,15359,429],{"class":342},[68,15361,15362],{"class":70,"line":101},[68,15363,447],{"class":342},[59,15365,15367],{"className":7755,"code":15366,"language":7757,"meta":64,"style":64},"\u003Ctemplate>\n \u003Cspan>{{ $tc('cart.items', cartCount, { count: cartCount }) }}\u003C/span>\n\u003C/template>\n",[37,15368,15369,15377,15390],{"__ignoreMap":64},[68,15370,15371,15373,15375],{"class":70,"line":71},[68,15372,365],{"class":342},[68,15374,10454],{"class":7771},[68,15376,7785],{"class":342},[68,15378,15379,15381,15383,15386,15388],{"class":70,"line":77},[68,15380,10461],{"class":342},[68,15382,68],{"class":7771},[68,15384,15385],{"class":342},">{{ $tc('cart.items', cartCount, { count: cartCount }) }}\u003C/",[68,15387,68],{"class":7771},[68,15389,7785],{"class":342},[68,15391,15392,15394,15396],{"class":70,"line":83},[68,15393,7811],{"class":342},[68,15395,10454],{"class":7771},[68,15397,7785],{"class":342},[15,15399,15401],{"id":15400},"rtl-language-support","RTL Language Support",[20,15403,15404,15405,15408],{},"For Arabic, Hebrew, and other right-to-left languages, you need more than just translated text — the entire layout direction must flip. Configure the ",[37,15406,15407],{},"dir"," attribute per locale and apply it to the HTML element:",[59,15410,15412],{"className":316,"code":15411,"language":318,"meta":64,"style":64},"// plugins/i18n.ts\nexport default defineNuxtPlugin(() => {\n const { localeProperties } = useI18n()\n\n watch(localeProperties, (locale) => {\n document.documentElement.dir = locale.dir ?? 'ltr'\n document.documentElement.lang = locale.language ?? locale.code\n }, { immediate: true })\n})\n",[37,15413,15414,15419,15433,15450,15454,15470,15485,15500,15509],{"__ignoreMap":64},[68,15415,15416],{"class":70,"line":71},[68,15417,15418],{"class":325},"// plugins/i18n.ts\n",[68,15420,15421,15423,15425,15427,15429,15431],{"class":70,"line":77},[68,15422,4892],{"class":331},[68,15424,4895],{"class":331},[68,15426,8890],{"class":338},[68,15428,1614],{"class":342},[68,15430,1617],{"class":331},[68,15432,1620],{"class":342},[68,15434,15435,15437,15439,15442,15444,15446,15448],{"class":70,"line":83},[68,15436,376],{"class":331},[68,15438,1748],{"class":342},[68,15440,15441],{"class":353},"localeProperties",[68,15443,1769],{"class":342},[68,15445,1593],{"class":331},[68,15447,14757],{"class":338},[68,15449,1602],{"class":342},[68,15451,15452],{"class":70,"line":89},[68,15453,123],{"emptyLinePlaceholder":122},[68,15455,15456,15459,15462,15464,15466,15468],{"class":70,"line":95},[68,15457,15458],{"class":338}," watch",[68,15460,15461],{"class":342},"(localeProperties, (",[68,15463,14740],{"class":346},[68,15465,1869],{"class":342},[68,15467,1617],{"class":331},[68,15469,1620],{"class":342},[68,15471,15472,15475,15477,15480,15482],{"class":70,"line":101},[68,15473,15474],{"class":342}," document.documentElement.dir ",[68,15476,1593],{"class":331},[68,15478,15479],{"class":342}," locale.dir ",[68,15481,4213],{"class":331},[68,15483,15484],{"class":409}," 'ltr'\n",[68,15486,15487,15490,15492,15495,15497],{"class":70,"line":107},[68,15488,15489],{"class":342}," document.documentElement.lang ",[68,15491,1593],{"class":331},[68,15493,15494],{"class":342}," locale.language ",[68,15496,4213],{"class":331},[68,15498,15499],{"class":342}," locale.code\n",[68,15501,15502,15505,15507],{"class":70,"line":113},[68,15503,15504],{"class":342}," }, { immediate: ",[68,15506,3619],{"class":353},[68,15508,1859],{"class":342},[68,15510,15511],{"class":70,"line":119},[68,15512,2789],{"class":342},[20,15514,15515],{},"In your Tailwind config, enable RTL support:",[59,15517,15519],{"className":316,"code":15518,"language":318,"meta":64,"style":64},"// tailwind.config.ts\nplugins: [\n require('tailwindcss-rtl'),\n]\n",[37,15520,15521,15526,15533,15545],{"__ignoreMap":64},[68,15522,15523],{"class":70,"line":71},[68,15524,15525],{"class":325},"// tailwind.config.ts\n",[68,15527,15528,15531],{"class":70,"line":77},[68,15529,15530],{"class":338},"plugins",[68,15532,13562],{"class":342},[68,15534,15535,15538,15540,15543],{"class":70,"line":83},[68,15536,15537],{"class":338}," require",[68,15539,343],{"class":342},[68,15541,15542],{"class":409},"'tailwindcss-rtl'",[68,15544,1814],{"class":342},[68,15546,15547],{"class":70,"line":89},[68,15548,15549],{"class":342},"]\n",[20,15551,15552,15553,180,15556,180,15559,15562,15563,15566,15567,15570],{},"This adds RTL-aware utility classes: ",[37,15554,15555],{},"rtl:flex-row-reverse",[37,15557,15558],{},"rtl:mr-auto",[37,15560,15561],{},"rtl:text-right",". Use these instead of directional classes (",[37,15564,15565],{},"ml-4"," → ",[37,15568,15569],{},"ms-4"," for margin-start which flips in RTL):",[59,15572,15574],{"className":7755,"code":15573,"language":7757,"meta":64,"style":64},"\u003C!-- This flips correctly in RTL -->\n\u003Cdiv class=\"flex items-center gap-4\">\n \u003CIcon class=\"me-2\" />\n \u003Cspan>{{ label }}\u003C/span>\n\u003C/div>\n",[37,15575,15576,15581,15596,15601,15606],{"__ignoreMap":64},[68,15577,15578],{"class":70,"line":71},[68,15579,15580],{"class":325},"\u003C!-- This flips correctly in RTL -->\n",[68,15582,15583,15585,15587,15589,15591,15594],{"class":70,"line":77},[68,15584,365],{"class":342},[68,15586,10464],{"class":7771},[68,15588,10467],{"class":338},[68,15590,1593],{"class":342},[68,15592,15593],{"class":409},"\"flex items-center gap-4\"",[68,15595,7785],{"class":342},[68,15597,15598],{"class":70,"line":83},[68,15599,15600],{"class":342}," \u003CIcon class=\"me-2\" />\n",[68,15602,15603],{"class":70,"line":89},[68,15604,15605],{"class":342}," \u003Cspan>{{ label }}\u003C/span>\n",[68,15607,15608,15610,15612],{"class":70,"line":95},[68,15609,7811],{"class":342},[68,15611,10464],{"class":7771},[68,15613,7785],{"class":342},[15,15615,15617],{"id":15616},"seo-for-multi-language-sites","SEO for Multi-Language Sites",[20,15619,15620],{},"Multi-language SEO requires hreflang tags on every page. The module generates these automatically:",[59,15622,15624],{"className":316,"code":15623,"language":318,"meta":64,"style":64},"// nuxt.config.ts\ni18n: {\n // Automatically adds hreflang alternate links\n // to every page in the \u003Chead>\n},\n",[37,15625,15626,15630,15636,15641,15646],{"__ignoreMap":64},[68,15627,15628],{"class":70,"line":71},[68,15629,9151],{"class":325},[68,15631,15632,15634],{"class":70,"line":77},[68,15633,14101],{"class":338},[68,15635,7834],{"class":342},[68,15637,15638],{"class":70,"line":83},[68,15639,15640],{"class":325}," // Automatically adds hreflang alternate links\n",[68,15642,15643],{"class":70,"line":89},[68,15644,15645],{"class":325}," // to every page in the \u003Chead>\n",[68,15647,15648],{"class":70,"line":95},[68,15649,11897],{"class":342},[20,15651,15652,15653,350],{},"Verify the tags are present in your HTML source. Each page should have an alternate link for each supported locale plus ",[37,15654,15655],{},"x-default",[59,15657,15659],{"className":13785,"code":15658,"language":13787,"meta":64,"style":64},"\u003Clink rel=\"alternate\" hreflang=\"en\" href=\"https://yourdomain.com/products\" />\n\u003Clink rel=\"alternate\" hreflang=\"es\" href=\"https://yourdomain.com/es/products\" />\n\u003Clink rel=\"alternate\" hreflang=\"ar\" href=\"https://yourdomain.com/ar/products\" />\n\u003Clink rel=\"alternate\" hreflang=\"x-default\" href=\"https://yourdomain.com/products\" />\n",[37,15660,15661,15694,15722,15750],{"__ignoreMap":64},[68,15662,15663,15665,15668,15671,15673,15676,15679,15681,15684,15687,15689,15692],{"class":70,"line":71},[68,15664,365],{"class":342},[68,15666,15667],{"class":7771},"link",[68,15669,15670],{"class":338}," rel",[68,15672,1593],{"class":342},[68,15674,15675],{"class":409},"\"alternate\"",[68,15677,15678],{"class":338}," hreflang",[68,15680,1593],{"class":342},[68,15682,15683],{"class":409},"\"en\"",[68,15685,15686],{"class":338}," href",[68,15688,1593],{"class":342},[68,15690,15691],{"class":409},"\"https://yourdomain.com/products\"",[68,15693,11088],{"class":342},[68,15695,15696,15698,15700,15702,15704,15706,15708,15710,15713,15715,15717,15720],{"class":70,"line":77},[68,15697,365],{"class":342},[68,15699,15667],{"class":7771},[68,15701,15670],{"class":338},[68,15703,1593],{"class":342},[68,15705,15675],{"class":409},[68,15707,15678],{"class":338},[68,15709,1593],{"class":342},[68,15711,15712],{"class":409},"\"es\"",[68,15714,15686],{"class":338},[68,15716,1593],{"class":342},[68,15718,15719],{"class":409},"\"https://yourdomain.com/es/products\"",[68,15721,11088],{"class":342},[68,15723,15724,15726,15728,15730,15732,15734,15736,15738,15741,15743,15745,15748],{"class":70,"line":83},[68,15725,365],{"class":342},[68,15727,15667],{"class":7771},[68,15729,15670],{"class":338},[68,15731,1593],{"class":342},[68,15733,15675],{"class":409},[68,15735,15678],{"class":338},[68,15737,1593],{"class":342},[68,15739,15740],{"class":409},"\"ar\"",[68,15742,15686],{"class":338},[68,15744,1593],{"class":342},[68,15746,15747],{"class":409},"\"https://yourdomain.com/ar/products\"",[68,15749,11088],{"class":342},[68,15751,15752,15754,15756,15758,15760,15762,15764,15766,15769,15771,15773,15775],{"class":70,"line":89},[68,15753,365],{"class":342},[68,15755,15667],{"class":7771},[68,15757,15670],{"class":338},[68,15759,1593],{"class":342},[68,15761,15675],{"class":409},[68,15763,15678],{"class":338},[68,15765,1593],{"class":342},[68,15767,15768],{"class":409},"\"x-default\"",[68,15770,15686],{"class":338},[68,15772,1593],{"class":342},[68,15774,15691],{"class":409},[68,15776,11088],{"class":342},[20,15778,15779],{},"Update your sitemap configuration to include all locale URLs:",[59,15781,15783],{"className":316,"code":15782,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nsitemap: {\n // Include all locale variants in sitemap\n i18n: true,\n},\n",[37,15784,15785,15789,15795,15800,15811],{"__ignoreMap":64},[68,15786,15787],{"class":70,"line":71},[68,15788,9151],{"class":325},[68,15790,15791,15793],{"class":70,"line":77},[68,15792,11589],{"class":338},[68,15794,7834],{"class":342},[68,15796,15797],{"class":70,"line":83},[68,15798,15799],{"class":325}," // Include all locale variants in sitemap\n",[68,15801,15802,15805,15807,15809],{"class":70,"line":89},[68,15803,15804],{"class":338}," i18n",[68,15806,938],{"class":342},[68,15808,3619],{"class":353},[68,15810,2751],{"class":342},[68,15812,15813],{"class":70,"line":95},[68,15814,11897],{"class":342},[15,15816,15818],{"id":15817},"language-switcher-component","Language Switcher Component",[59,15820,15822],{"className":7755,"code":15821,"language":7757,"meta":64,"style":64},"\u003C!-- components/LanguageSwitcher.vue -->\n\u003Cscript setup lang=\"ts\">\nconst { locale, locales, setLocale } = useI18n()\nconst switchLocalePath = useSwitchLocalePath()\n\nConst availableLocales = computed(() =>\n locales.value.filter(l => l.code !== locale.value)\n)\n\u003C/script>\n\n\u003Ctemplate>\n \u003Cdiv class=\"relative\">\n \u003Cbutton\n class=\"flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100\"\n aria-haspopup=\"listbox\"\n :aria-label=\"`Current language: ${locale}`\"\n >\n \u003Cspan>{{ locale.toUpperCase() }}\u003C/span>\n \u003C/button>\n \u003Cul role=\"listbox\">\n \u003Cli\n v-for=\"l in availableLocales\"\n :key=\"l.code\"\n role=\"option\"\n >\n \u003CNuxtLink :to=\"switchLocalePath(l.code)\" class=\"block px-4 py-2 hover:bg-gray-100\">\n {{ l.name }}\n \u003C/NuxtLink>\n \u003C/li>\n \u003C/ul>\n \u003C/div>\n\u003C/template>\n",[37,15823,15824,15829,15845,15869,15883,15887,15900,15923,15927,15935,15939,15947,15962,15969,15978,15988,15998,16003,16016,16025,16040,16047,16056,16065,16074,16078,16100,16105,16113,16121,16129,16137],{"__ignoreMap":64},[68,15825,15826],{"class":70,"line":71},[68,15827,15828],{"class":325},"\u003C!-- components/LanguageSwitcher.vue -->\n",[68,15830,15831,15833,15835,15837,15839,15841,15843],{"class":70,"line":77},[68,15832,365],{"class":342},[68,15834,7772],{"class":7771},[68,15836,6980],{"class":338},[68,15838,7777],{"class":338},[68,15840,1593],{"class":342},[68,15842,7782],{"class":409},[68,15844,7785],{"class":342},[68,15846,15847,15849,15851,15853,15855,15857,15859,15861,15863,15865,15867],{"class":70,"line":83},[68,15848,1991],{"class":331},[68,15850,1748],{"class":342},[68,15852,14740],{"class":353},[68,15854,180],{"class":342},[68,15856,14745],{"class":353},[68,15858,180],{"class":342},[68,15860,14750],{"class":353},[68,15862,1769],{"class":342},[68,15864,1593],{"class":331},[68,15866,14757],{"class":338},[68,15868,1602],{"class":342},[68,15870,15871,15873,15876,15878,15881],{"class":70,"line":89},[68,15872,1991],{"class":331},[68,15874,15875],{"class":353}," switchLocalePath",[68,15877,382],{"class":331},[68,15879,15880],{"class":338}," useSwitchLocalePath",[68,15882,1602],{"class":342},[68,15884,15885],{"class":70,"line":95},[68,15886,123],{"emptyLinePlaceholder":122},[68,15888,15889,15892,15894,15896,15898],{"class":70,"line":101},[68,15890,15891],{"class":342},"Const availableLocales ",[68,15893,1593],{"class":331},[68,15895,8813],{"class":338},[68,15897,1614],{"class":342},[68,15899,10369],{"class":331},[68,15901,15902,15905,15908,15910,15913,15915,15918,15920],{"class":70,"line":107},[68,15903,15904],{"class":342}," locales.value.",[68,15906,15907],{"class":338},"filter",[68,15909,343],{"class":342},[68,15911,15912],{"class":346},"l",[68,15914,3600],{"class":331},[68,15916,15917],{"class":342}," l.code ",[68,15919,7713],{"class":331},[68,15921,15922],{"class":342}," locale.value)\n",[68,15924,15925],{"class":70,"line":113},[68,15926,1702],{"class":342},[68,15928,15929,15931,15933],{"class":70,"line":119},[68,15930,7811],{"class":342},[68,15932,7772],{"class":7771},[68,15934,7785],{"class":342},[68,15936,15937],{"class":70,"line":126},[68,15938,123],{"emptyLinePlaceholder":122},[68,15940,15941,15943,15945],{"class":70,"line":132},[68,15942,365],{"class":342},[68,15944,10454],{"class":7771},[68,15946,7785],{"class":342},[68,15948,15949,15951,15953,15955,15957,15960],{"class":70,"line":2135},[68,15950,10461],{"class":342},[68,15952,10464],{"class":7771},[68,15954,10467],{"class":338},[68,15956,1593],{"class":342},[68,15958,15959],{"class":409},"\"relative\"",[68,15961,7785],{"class":342},[68,15963,15964,15966],{"class":70,"line":2141},[68,15965,10461],{"class":342},[68,15967,15968],{"class":7771},"button\n",[68,15970,15971,15973,15975],{"class":70,"line":2437},[68,15972,10467],{"class":338},[68,15974,1593],{"class":342},[68,15976,15977],{"class":409},"\"flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100\"\n",[68,15979,15980,15983,15985],{"class":70,"line":2442},[68,15981,15982],{"class":338}," aria-haspopup",[68,15984,1593],{"class":342},[68,15986,15987],{"class":409},"\"listbox\"\n",[68,15989,15990,15993,15995],{"class":70,"line":2447},[68,15991,15992],{"class":338}," :aria-label",[68,15994,1593],{"class":342},[68,15996,15997],{"class":409},"\"`Current language: ${locale}`\"\n",[68,15999,16000],{"class":70,"line":2652},[68,16001,16002],{"class":342}," >\n",[68,16004,16005,16007,16009,16012,16014],{"class":70,"line":2687},[68,16006,10461],{"class":342},[68,16008,68],{"class":7771},[68,16010,16011],{"class":342},">{{ locale.toUpperCase() }}\u003C/",[68,16013,68],{"class":7771},[68,16015,7785],{"class":342},[68,16017,16018,16020,16023],{"class":70,"line":2692},[68,16019,10568],{"class":342},[68,16021,16022],{"class":7771},"button",[68,16024,7785],{"class":342},[68,16026,16027,16029,16031,16033,16035,16038],{"class":70,"line":2697},[68,16028,10461],{"class":342},[68,16030,516],{"class":7771},[68,16032,6154],{"class":338},[68,16034,1593],{"class":342},[68,16036,16037],{"class":409},"\"listbox\"",[68,16039,7785],{"class":342},[68,16041,16042,16044],{"class":70,"line":3129},[68,16043,10461],{"class":342},[68,16045,16046],{"class":7771},"li\n",[68,16048,16049,16051,16053],{"class":70,"line":3134},[68,16050,10520],{"class":338},[68,16052,1593],{"class":342},[68,16054,16055],{"class":409},"\"l in availableLocales\"\n",[68,16057,16058,16060,16062],{"class":70,"line":4749},[68,16059,10528],{"class":338},[68,16061,1593],{"class":342},[68,16063,16064],{"class":409},"\"l.code\"\n",[68,16066,16067,16069,16071],{"class":70,"line":4755},[68,16068,6154],{"class":338},[68,16070,1593],{"class":342},[68,16072,16073],{"class":409},"\"option\"\n",[68,16075,16076],{"class":70,"line":4760},[68,16077,16002],{"class":342},[68,16079,16080,16082,16084,16086,16088,16091,16093,16095,16098],{"class":70,"line":4767},[68,16081,10461],{"class":342},[68,16083,10594],{"class":7771},[68,16085,10597],{"class":338},[68,16087,1593],{"class":342},[68,16089,16090],{"class":409},"\"switchLocalePath(l.code)\"",[68,16092,10467],{"class":338},[68,16094,1593],{"class":342},[68,16096,16097],{"class":409},"\"block px-4 py-2 hover:bg-gray-100\"",[68,16099,7785],{"class":342},[68,16101,16102],{"class":70,"line":4773},[68,16103,16104],{"class":342}," {{ l.name }}\n",[68,16106,16107,16109,16111],{"class":70,"line":4779},[68,16108,10568],{"class":342},[68,16110,10594],{"class":7771},[68,16112,7785],{"class":342},[68,16114,16115,16117,16119],{"class":70,"line":4785},[68,16116,10568],{"class":342},[68,16118,519],{"class":7771},[68,16120,7785],{"class":342},[68,16122,16123,16125,16127],{"class":70,"line":4791},[68,16124,10568],{"class":342},[68,16126,516],{"class":7771},[68,16128,7785],{"class":342},[68,16130,16131,16133,16135],{"class":70,"line":4797},[68,16132,10568],{"class":342},[68,16134,10464],{"class":7771},[68,16136,7785],{"class":342},[68,16138,16139,16141,16143],{"class":70,"line":4814},[68,16140,7811],{"class":342},[68,16142,10454],{"class":7771},[68,16144,7785],{"class":342},[20,16146,4139,16147,16150,16151,16154,16155,51],{},[37,16148,16149],{},"switchLocalePath"," composable generates the equivalent URL in the target locale — if the user is on ",[37,16152,16153],{},"/es/products/widget",", switching to English produces ",[37,16156,16157],{},"/products/widget",[15,16159,16161],{"id":16160},"lazy-loading-locales","Lazy Loading Locales",[20,16163,16164,16165,16168,16169,16172],{},"For applications with many supported languages, lazy loading prevents downloading all translations upfront. The configuration we set earlier with ",[37,16166,16167],{},"lazy: true"," and individual ",[37,16170,16171],{},"file"," references handles this — only the active locale's translation file downloads.",[20,16174,16175],{},"Use split files for large applications:",[59,16177,16179],{"className":316,"code":16178,"language":318,"meta":64,"style":64},"locales: [\n {\n code: 'en',\n files: ['en/common.json', 'en/products.json', 'en/checkout.json'],\n },\n],\n",[37,16180,16181,16187,16191,16200,16220,16224],{"__ignoreMap":64},[68,16182,16183,16185],{"class":70,"line":71},[68,16184,14745],{"class":338},[68,16186,13562],{"class":342},[68,16188,16189],{"class":70,"line":77},[68,16190,1620],{"class":342},[68,16192,16193,16196,16198],{"class":70,"line":83},[68,16194,16195],{"class":342}," code: ",[68,16197,14125],{"class":409},[68,16199,2751],{"class":342},[68,16201,16202,16205,16208,16210,16213,16215,16218],{"class":70,"line":89},[68,16203,16204],{"class":342}," files: [",[68,16206,16207],{"class":409},"'en/common.json'",[68,16209,180],{"class":342},[68,16211,16212],{"class":409},"'en/products.json'",[68,16214,180],{"class":342},[68,16216,16217],{"class":409},"'en/checkout.json'",[68,16219,6051],{"class":342},[68,16221,16222],{"class":70,"line":95},[68,16223,3893],{"class":342},[68,16225,16226],{"class":70,"line":101},[68,16227,6051],{"class":342},[20,16229,16230],{},"Load namespace-specific translations only on the routes that need them, reducing the initial bundle for simpler pages.",[15,16232,16234],{"id":16233},"testing-translations","Testing Translations",[20,16236,16237],{},"Add a check in your CI to ensure all locale files have the same keys:",[59,16239,16241],{"className":316,"code":16240,"language":318,"meta":64,"style":64},"// scripts/check-translations.ts\nconst en = JSON.parse(readFileSync('locales/en.json', 'utf8'))\nconst es = JSON.parse(readFileSync('locales/es.json', 'utf8'))\n\nFunction getKeys(obj: object, prefix = ''): string[] {\n return Object.entries(obj).flatMap(([key, value]) =>\n typeof value === 'object'\n ? getKeys(value, `${prefix}${key}.`)\n : [`${prefix}${key}`]\n )\n}\n\nConst enKeys = new Set(getKeys(en))\nconst esKeys = new Set(getKeys(es))\n\nConst missing = [...enKeys].filter(k => !esKeys.has(k))\nif (missing.length) {\n console.error('Missing Spanish translations:', missing)\n process.exit(1)\n}\n",[37,16242,16243,16248,16277,16306,16310,16328,16358,16371,16396,16414,16419,16423,16427,16446,16466,16470,16504,16515,16530,16543],{"__ignoreMap":64},[68,16244,16245],{"class":70,"line":71},[68,16246,16247],{"class":325},"// scripts/check-translations.ts\n",[68,16249,16250,16252,16254,16256,16258,16260,16262,16264,16266,16268,16271,16273,16275],{"class":70,"line":77},[68,16251,1991],{"class":331},[68,16253,14996],{"class":353},[68,16255,382],{"class":331},[68,16257,1999],{"class":353},[68,16259,51],{"class":342},[68,16261,2004],{"class":338},[68,16263,343],{"class":342},[68,16265,2010],{"class":338},[68,16267,343],{"class":342},[68,16269,16270],{"class":409},"'locales/en.json'",[68,16272,180],{"class":342},[68,16274,2020],{"class":409},[68,16276,2023],{"class":342},[68,16278,16279,16281,16283,16285,16287,16289,16291,16293,16295,16297,16300,16302,16304],{"class":70,"line":83},[68,16280,1991],{"class":331},[68,16282,15097],{"class":353},[68,16284,382],{"class":331},[68,16286,1999],{"class":353},[68,16288,51],{"class":342},[68,16290,2004],{"class":338},[68,16292,343],{"class":342},[68,16294,2010],{"class":338},[68,16296,343],{"class":342},[68,16298,16299],{"class":409},"'locales/es.json'",[68,16301,180],{"class":342},[68,16303,2020],{"class":409},[68,16305,2023],{"class":342},[68,16307,16308],{"class":70,"line":89},[68,16309,123],{"emptyLinePlaceholder":122},[68,16311,16312,16314,16317,16320,16322,16325],{"class":70,"line":95},[68,16313,2482],{"class":342},[68,16315,16316],{"class":338},"getKeys",[68,16318,16319],{"class":342},"(obj: object, prefix ",[68,16321,1593],{"class":331},[68,16323,16324],{"class":409}," ''",[68,16326,16327],{"class":342},"): string[] {\n",[68,16329,16330,16332,16335,16338,16341,16344,16347,16349,16351,16353,16356],{"class":70,"line":101},[68,16331,418],{"class":331},[68,16333,16334],{"class":342}," Object.",[68,16336,16337],{"class":338},"entries",[68,16339,16340],{"class":342},"(obj).",[68,16342,16343],{"class":338},"flatMap",[68,16345,16346],{"class":342},"(([",[68,16348,3413],{"class":346},[68,16350,180],{"class":342},[68,16352,11243],{"class":346},[68,16354,16355],{"class":342},"]) ",[68,16357,10369],{"class":331},[68,16359,16360,16363,16366,16368],{"class":70,"line":107},[68,16361,16362],{"class":331}," typeof",[68,16364,16365],{"class":342}," value ",[68,16367,406],{"class":331},[68,16369,16370],{"class":409}," 'object'\n",[68,16372,16373,16375,16378,16381,16383,16386,16389,16391,16394],{"class":70,"line":113},[68,16374,6026],{"class":331},[68,16376,16377],{"class":338}," getKeys",[68,16379,16380],{"class":342},"(value, ",[68,16382,5854],{"class":409},[68,16384,16385],{"class":342},"prefix",[68,16387,16388],{"class":409},"}${",[68,16390,3413],{"class":342},[68,16392,16393],{"class":409},"}.`",[68,16395,1702],{"class":342},[68,16397,16398,16400,16402,16404,16406,16408,16410,16412],{"class":70,"line":119},[68,16399,12821],{"class":331},[68,16401,4631],{"class":342},[68,16403,5854],{"class":409},[68,16405,16385],{"class":342},[68,16407,16388],{"class":409},[68,16409,3413],{"class":342},[68,16411,2682],{"class":409},[68,16413,15549],{"class":342},[68,16415,16416],{"class":70,"line":126},[68,16417,16418],{"class":342}," )\n",[68,16420,16421],{"class":70,"line":132},[68,16422,447],{"class":342},[68,16424,16425],{"class":70,"line":2135},[68,16426,123],{"emptyLinePlaceholder":122},[68,16428,16429,16432,16434,16436,16439,16441,16443],{"class":70,"line":2141},[68,16430,16431],{"class":342},"Const enKeys ",[68,16433,1593],{"class":331},[68,16435,2551],{"class":331},[68,16437,16438],{"class":338}," Set",[68,16440,343],{"class":342},[68,16442,16316],{"class":338},[68,16444,16445],{"class":342},"(en))\n",[68,16447,16448,16450,16453,16455,16457,16459,16461,16463],{"class":70,"line":2437},[68,16449,1991],{"class":331},[68,16451,16452],{"class":353}," esKeys",[68,16454,382],{"class":331},[68,16456,2551],{"class":331},[68,16458,16438],{"class":338},[68,16460,343],{"class":342},[68,16462,16316],{"class":338},[68,16464,16465],{"class":342},"(es))\n",[68,16467,16468],{"class":70,"line":2442},[68,16469,123],{"emptyLinePlaceholder":122},[68,16471,16472,16475,16477,16479,16481,16484,16486,16488,16491,16493,16496,16499,16501],{"class":70,"line":2447},[68,16473,16474],{"class":342},"Const missing ",[68,16476,1593],{"class":331},[68,16478,4631],{"class":342},[68,16480,2570],{"class":331},[68,16482,16483],{"class":342},"enKeys].",[68,16485,15907],{"class":338},[68,16487,343],{"class":342},[68,16489,16490],{"class":346},"k",[68,16492,3600],{"class":331},[68,16494,16495],{"class":331}," !",[68,16497,16498],{"class":342},"esKeys.",[68,16500,3434],{"class":338},[68,16502,16503],{"class":342},"(k))\n",[68,16505,16506,16508,16511,16513],{"class":70,"line":2652},[68,16507,6852],{"class":331},[68,16509,16510],{"class":342}," (missing.",[68,16512,2776],{"class":353},[68,16514,413],{"class":342},[68,16516,16517,16519,16522,16524,16527],{"class":70,"line":2687},[68,16518,1685],{"class":342},[68,16520,16521],{"class":338},"error",[68,16523,343],{"class":342},[68,16525,16526],{"class":409},"'Missing Spanish translations:'",[68,16528,16529],{"class":342},", missing)\n",[68,16531,16532,16534,16537,16539,16541],{"class":70,"line":2692},[68,16533,1774],{"class":342},[68,16535,16536],{"class":338},"exit",[68,16538,343],{"class":342},[68,16540,2764],{"class":353},[68,16542,1702],{"class":342},[68,16544,16545],{"class":70,"line":2697},[68,16546,447],{"class":342},[20,16548,16549],{},"Internationalization is a commitment that requires coordination with translators, design, and QA. The technical implementation is the easy part. The harder part is the process of keeping translations updated as the application evolves. Establish that process early.",[509,16551],{},[20,16553,16554,16555,51],{},"Building a multi-language Nuxt application or need help with the i18n architecture? Book a call and let's design it together: ",[502,16556,817],{"href":504,"rel":16557},[506],[509,16559],{},[15,16561,514],{"id":513},[516,16563,16564,16568,16572,16576],{},[519,16565,16566],{},[502,16567,4408],{"href":4407},[519,16569,16570],{},[502,16571,4396],{"href":4395},[519,16573,16574],{},[502,16575,4402],{"href":4401},[519,16577,16578],{},[502,16579,4414],{"href":4413},[544,16581,16582],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":64,"searchDepth":83,"depth":83,"links":16584},[16585,16586,16587,16588,16589,16590,16591,16592,16593,16594,16595],{"id":14065,"depth":77,"text":14066},{"id":14360,"depth":77,"text":14361},{"id":14694,"depth":77,"text":14695},{"id":14965,"depth":77,"text":14966},{"id":15321,"depth":77,"text":15322},{"id":15400,"depth":77,"text":15401},{"id":15616,"depth":77,"text":15617},{"id":15817,"depth":77,"text":15818},{"id":16160,"depth":77,"text":16161},{"id":16233,"depth":77,"text":16234},{"id":513,"depth":77,"text":514},"A complete guide to internationalization in Nuxt with @nuxtjs/i18n — locale routing, translation files, lazy-loading locales, RTL support, and SEO for multi-language sites.",[16598,16599],"Nuxt i18n","Nuxt internationalization",{},"/blog/nuxt-internationalization",{"title":14053,"description":16596},"blog/nuxt-internationalization",[4438,14101,16605],"Internationalization","qotI2Q8AZElNLsbywuSLAgJRMLtc24NnnUW3ZT9g2hc",{"id":16608,"title":16609,"author":16610,"body":16611,"category":557,"date":558,"description":17933,"extension":560,"featured":561,"image":562,"keywords":17934,"meta":17937,"navigation":122,"path":17938,"readTime":107,"seo":17939,"stem":17940,"tags":17941,"__hash__":17943},"blog/blog/nuxt-middleware-guide.md","Nuxt Middleware and Plugins: The Difference and When to Use Each",{"name":9,"bio":10},{"type":12,"value":16612,"toc":17923},[16613,16616,16619,16623,16626,16639,16651,16657,16661,16664,16828,16833,16890,16897,16942,16945,16959,16961,16967,17141,17322,17325,17339,17342,17345,17440,17583,17586,17749,17763,17767,17770,17776,17782,17788,17794,17798,17804,17810,17823,17829,17833,17840,17846,17851,17887,17890,17892,17898,17900,17902,17920],[20,16614,16615],{},"Middleware and plugins in Nuxt serve different purposes, but the names obscure that difference for developers new to the framework. I have seen middleware used for things that belong in plugins, plugins used for things that belong in composables, and server middleware confused with route middleware. Getting this right leads to cleaner code and fewer subtle bugs.",[20,16617,16618],{},"Let me draw the lines clearly.",[15,16620,16622],{"id":16621},"three-types-of-middleware","Three Types of Middleware",[20,16624,16625],{},"Nuxt has three distinct middleware systems:",[20,16627,16628,16631,16632,16635,16636,51],{},[55,16629,16630],{},"Route middleware"," runs on client-side navigation between pages. It lives in the ",[37,16633,16634],{},"middleware/"," directory and uses ",[37,16637,16638],{},"defineNuxtRouteMiddleware",[20,16640,16641,16644,16645,16647,16648,51],{},[55,16642,16643],{},"Server middleware"," runs on every incoming HTTP request to the Nitro server. It lives in ",[37,16646,5607],{}," and uses ",[37,16649,16650],{},"defineEventHandler",[20,16652,16653,16656],{},[55,16654,16655],{},"Plugin middleware"," does not technically exist as a term, but people often reach for plugins when they want route middleware. I will clarify that distinction below.",[15,16658,16660],{"id":16659},"route-middleware","Route Middleware",[20,16662,16663],{},"Route middleware intercepts navigation between pages. Its purpose is to control whether a navigation happens — redirect, abort, or allow it.",[59,16665,16667],{"className":316,"code":16666,"language":318,"meta":64,"style":64},"// middleware/auth.ts\nexport default defineNuxtRouteMiddleware(async (to, from) => {\n const { data: session } = await useAuth()\n\n // User is not authenticated\n if (!session.value) {\n // Redirect to login with the intended destination\n return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)\n }\n\n // User lacks required role\n if (to.meta.requiredRole && session.value.user.role !== to.meta.requiredRole) {\n throw createError({ statusCode: 403, statusMessage: 'Forbidden' })\n }\n})\n",[37,16668,16669,16673,16699,16721,16725,16730,16741,16746,16774,16778,16782,16787,16804,16820,16824],{"__ignoreMap":64},[68,16670,16671],{"class":70,"line":71},[68,16672,7641],{"class":325},[68,16674,16675,16677,16679,16681,16683,16685,16687,16689,16691,16693,16695,16697],{"class":70,"line":77},[68,16676,4892],{"class":331},[68,16678,4895],{"class":331},[68,16680,7650],{"class":338},[68,16682,343],{"class":342},[68,16684,332],{"class":331},[68,16686,2657],{"class":342},[68,16688,7659],{"class":346},[68,16690,180],{"class":342},[68,16692,2043],{"class":346},[68,16694,1869],{"class":342},[68,16696,1617],{"class":331},[68,16698,1620],{"class":342},[68,16700,16701,16703,16705,16707,16709,16711,16713,16715,16717,16719],{"class":70,"line":83},[68,16702,376],{"class":331},[68,16704,1748],{"class":342},[68,16706,4157],{"class":346},[68,16708,938],{"class":342},[68,16710,7678],{"class":353},[68,16712,1769],{"class":342},[68,16714,1593],{"class":331},[68,16716,385],{"class":331},[68,16718,7687],{"class":338},[68,16720,1602],{"class":342},[68,16722,16723],{"class":70,"line":89},[68,16724,123],{"emptyLinePlaceholder":122},[68,16726,16727],{"class":70,"line":95},[68,16728,16729],{"class":325}," // User is not authenticated\n",[68,16731,16732,16734,16736,16738],{"class":70,"line":101},[68,16733,400],{"class":331},[68,16735,2657],{"class":342},[68,16737,3428],{"class":331},[68,16739,16740],{"class":342},"session.value) {\n",[68,16742,16743],{"class":70,"line":107},[68,16744,16745],{"class":325}," // Redirect to login with the intended destination\n",[68,16747,16748,16750,16752,16754,16756,16759,16761,16763,16765,16768,16770,16772],{"class":70,"line":113},[68,16749,418],{"class":331},[68,16751,7725],{"class":338},[68,16753,343],{"class":342},[68,16755,7730],{"class":409},[68,16757,16758],{"class":338},"encodeURIComponent",[68,16760,343],{"class":409},[68,16762,7659],{"class":342},[68,16764,51],{"class":409},[68,16766,16767],{"class":342},"fullPath",[68,16769,357],{"class":409},[68,16771,2682],{"class":409},[68,16773,1702],{"class":342},[68,16775,16776],{"class":70,"line":119},[68,16777,429],{"class":342},[68,16779,16780],{"class":70,"line":126},[68,16781,123],{"emptyLinePlaceholder":122},[68,16783,16784],{"class":70,"line":132},[68,16785,16786],{"class":325}," // User lacks required role\n",[68,16788,16789,16791,16794,16796,16799,16801],{"class":70,"line":2135},[68,16790,400],{"class":331},[68,16792,16793],{"class":342}," (to.meta.requiredRole ",[68,16795,7707],{"class":331},[68,16797,16798],{"class":342}," session.value.user.role ",[68,16800,7713],{"class":331},[68,16802,16803],{"class":342}," to.meta.requiredRole) {\n",[68,16805,16806,16808,16810,16812,16814,16816,16818],{"class":70,"line":2141},[68,16807,5281],{"class":331},[68,16809,4842],{"class":338},[68,16811,6648],{"class":342},[68,16813,8415],{"class":353},[68,16815,6654],{"class":342},[68,16817,8420],{"class":409},[68,16819,1859],{"class":342},[68,16821,16822],{"class":70,"line":2437},[68,16823,429],{"class":342},[68,16825,16826],{"class":70,"line":2442},[68,16827,2789],{"class":342},[20,16829,16830,16831,350],{},"Apply middleware to specific pages with ",[37,16832,7790],{},[59,16834,16836],{"className":7755,"code":16835,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\ndefinePageMeta({\n middleware: ['auth'],\n requiredRole: 'admin',\n})\n\u003C/script>\n",[37,16837,16838,16854,16860,16869,16878,16882],{"__ignoreMap":64},[68,16839,16840,16842,16844,16846,16848,16850,16852],{"class":70,"line":71},[68,16841,365],{"class":342},[68,16843,7772],{"class":7771},[68,16845,6980],{"class":338},[68,16847,7777],{"class":338},[68,16849,1593],{"class":342},[68,16851,7782],{"class":409},[68,16853,7785],{"class":342},[68,16855,16856,16858],{"class":70,"line":77},[68,16857,7790],{"class":338},[68,16859,1789],{"class":342},[68,16861,16862,16865,16867],{"class":70,"line":83},[68,16863,16864],{"class":342}," middleware: [",[68,16866,7800],{"class":409},[68,16868,6051],{"class":342},[68,16870,16871,16874,16876],{"class":70,"line":89},[68,16872,16873],{"class":342}," requiredRole: ",[68,16875,5178],{"class":409},[68,16877,2751],{"class":342},[68,16879,16880],{"class":70,"line":95},[68,16881,2789],{"class":342},[68,16883,16884,16886,16888],{"class":70,"line":101},[68,16885,7811],{"class":342},[68,16887,7772],{"class":7771},[68,16889,7785],{"class":342},[20,16891,16892,16893,16896],{},"Or make it global (runs on every navigation) by naming it with a ",[37,16894,16895],{},".global"," suffix:",[59,16898,16900],{"className":316,"code":16899,"language":318,"meta":64,"style":64},"// middleware/analytics.global.ts\nexport default defineNuxtRouteMiddleware((to) => {\n // Track every page view\n useTrackPageView(to.path)\n})\n",[37,16901,16902,16907,16925,16930,16938],{"__ignoreMap":64},[68,16903,16904],{"class":70,"line":71},[68,16905,16906],{"class":325},"// middleware/analytics.global.ts\n",[68,16908,16909,16911,16913,16915,16917,16919,16921,16923],{"class":70,"line":77},[68,16910,4892],{"class":331},[68,16912,4895],{"class":331},[68,16914,7650],{"class":338},[68,16916,2525],{"class":342},[68,16918,7659],{"class":346},[68,16920,1869],{"class":342},[68,16922,1617],{"class":331},[68,16924,1620],{"class":342},[68,16926,16927],{"class":70,"line":83},[68,16928,16929],{"class":325}," // Track every page view\n",[68,16931,16932,16935],{"class":70,"line":89},[68,16933,16934],{"class":338}," useTrackPageView",[68,16936,16937],{"class":342},"(to.path)\n",[68,16939,16940],{"class":70,"line":95},[68,16941,2789],{"class":342},[20,16943,16944],{},"Key points about route middleware:",[516,16946,16947,16950,16953,16956],{},[519,16948,16949],{},"It runs in the browser during client-side navigation",[519,16951,16952],{},"It runs on the server during SSR for the initial page request",[519,16954,16955],{},"It should not do heavy work — it blocks navigation until it completes",[519,16957,16958],{},"It cannot be used for server-only operations (database access, etc.) when running client-side",[15,16960,5601],{"id":5600},[20,16962,16963,16964,16966],{},"Server middleware runs on every request before your API routes handle it. It lives in ",[37,16965,5607],{}," and has access to the full H3 event object.",[59,16968,16970],{"className":316,"code":16969,"language":318,"meta":64,"style":64},"// server/middleware/logger.ts\nexport default defineEventHandler((event) => {\n const start = Date.now()\n const url = getRequestURL(event)\n const method = getMethod(event)\n\n event.node.res.on('finish', () => {\n const duration = Date.now() - start\n const status = event.node.res.statusCode\n console.log(`[${new Date().toISOString()}] ${method} ${url.pathname} ${status} ${duration}ms`)\n })\n})\n",[37,16971,16972,16976,16994,17008,17020,17034,17038,17054,17072,17084,17133,17137],{"__ignoreMap":64},[68,16973,16974],{"class":70,"line":71},[68,16975,5749],{"class":325},[68,16977,16978,16980,16982,16984,16986,16988,16990,16992],{"class":70,"line":77},[68,16979,4892],{"class":331},[68,16981,4895],{"class":331},[68,16983,4525],{"class":338},[68,16985,2525],{"class":342},[68,16987,4534],{"class":346},[68,16989,1869],{"class":342},[68,16991,1617],{"class":331},[68,16993,1620],{"class":342},[68,16995,16996,16998,17000,17002,17004,17006],{"class":70,"line":83},[68,16997,376],{"class":331},[68,16999,5774],{"class":353},[68,17001,382],{"class":331},[68,17003,1596],{"class":342},[68,17005,1599],{"class":338},[68,17007,1602],{"class":342},[68,17009,17010,17012,17014,17016,17018],{"class":70,"line":89},[68,17011,376],{"class":331},[68,17013,5789],{"class":353},[68,17015,382],{"class":331},[68,17017,5794],{"class":338},[68,17019,4555],{"class":342},[68,17021,17022,17024,17027,17029,17032],{"class":70,"line":95},[68,17023,376],{"class":331},[68,17025,17026],{"class":353}," method",[68,17028,382],{"class":331},[68,17030,17031],{"class":338}," getMethod",[68,17033,4555],{"class":342},[68,17035,17036],{"class":70,"line":101},[68,17037,123],{"emptyLinePlaceholder":122},[68,17039,17040,17042,17044,17046,17048,17050,17052],{"class":70,"line":107},[68,17041,5810],{"class":342},[68,17043,2595],{"class":338},[68,17045,343],{"class":342},[68,17047,5817],{"class":409},[68,17049,3101],{"class":342},[68,17051,1617],{"class":331},[68,17053,1620],{"class":342},[68,17055,17056,17058,17060,17062,17064,17066,17068,17070],{"class":70,"line":113},[68,17057,376],{"class":331},[68,17059,5830],{"class":353},[68,17061,382],{"class":331},[68,17063,1596],{"class":342},[68,17065,1599],{"class":338},[68,17067,1636],{"class":342},[68,17069,1639],{"class":331},[68,17071,5843],{"class":342},[68,17073,17074,17076,17079,17081],{"class":70,"line":119},[68,17075,376],{"class":331},[68,17077,17078],{"class":353}," status",[68,17080,382],{"class":331},[68,17082,17083],{"class":342}," event.node.res.statusCode\n",[68,17085,17086,17088,17090,17092,17095,17097,17099,17101,17104,17107,17110,17113,17115,17117,17119,17121,17123,17125,17127,17129,17131],{"class":70,"line":126},[68,17087,1685],{"class":342},[68,17089,1786],{"class":338},[68,17091,343],{"class":342},[68,17093,17094],{"class":409},"`[${",[68,17096,2669],{"class":331},[68,17098,14836],{"class":338},[68,17100,2773],{"class":409},[68,17102,17103],{"class":338},"toISOString",[68,17105,17106],{"class":409},"()",[68,17108,17109],{"class":409},"}] ${",[68,17111,17112],{"class":342},"method",[68,17114,5865],{"class":409},[68,17116,5868],{"class":342},[68,17118,51],{"class":409},[68,17120,5873],{"class":342},[68,17122,5865],{"class":409},[68,17124,952],{"class":342},[68,17126,5865],{"class":409},[68,17128,5895],{"class":342},[68,17130,1699],{"class":409},[68,17132,1702],{"class":342},[68,17134,17135],{"class":70,"line":132},[68,17136,1859],{"class":342},[68,17138,17139],{"class":70,"line":2135},[68,17140,2789],{"class":342},[59,17142,17144],{"className":316,"code":17143,"language":318,"meta":64,"style":64},"// server/middleware/cors.ts\nexport default defineEventHandler((event) => {\n const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com']\n const origin = getHeader(event, 'origin')\n\n if (origin && allowedOrigins.includes(origin)) {\n setResponseHeader(event, 'Access-Control-Allow-Origin', origin)\n }\n\n setResponseHeaders(event, {\n 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n })\n\n if (getMethod(event) === 'OPTIONS') {\n event.node.res.statusCode = 204\n return ''\n }\n})\n",[37,17145,17146,17150,17168,17188,17206,17210,17228,17241,17245,17249,17255,17265,17275,17279,17283,17299,17307,17314,17318],{"__ignoreMap":64},[68,17147,17148],{"class":70,"line":71},[68,17149,5618],{"class":325},[68,17151,17152,17154,17156,17158,17160,17162,17164,17166],{"class":70,"line":77},[68,17153,4892],{"class":331},[68,17155,4895],{"class":331},[68,17157,4525],{"class":338},[68,17159,2525],{"class":342},[68,17161,4534],{"class":346},[68,17163,1869],{"class":342},[68,17165,1617],{"class":331},[68,17167,1620],{"class":342},[68,17169,17170,17172,17175,17177,17179,17181,17183,17186],{"class":70,"line":83},[68,17171,376],{"class":331},[68,17173,17174],{"class":353}," allowedOrigins",[68,17176,382],{"class":331},[68,17178,4631],{"class":342},[68,17180,12627],{"class":409},[68,17182,180],{"class":342},[68,17184,17185],{"class":409},"'https://app.yourdomain.com'",[68,17187,15549],{"class":342},[68,17189,17190,17192,17195,17197,17199,17201,17204],{"class":70,"line":89},[68,17191,376],{"class":331},[68,17193,17194],{"class":353}," origin",[68,17196,382],{"class":331},[68,17198,5009],{"class":338},[68,17200,5012],{"class":342},[68,17202,17203],{"class":409},"'origin'",[68,17205,1702],{"class":342},[68,17207,17208],{"class":70,"line":95},[68,17209,123],{"emptyLinePlaceholder":122},[68,17211,17212,17214,17217,17219,17222,17225],{"class":70,"line":101},[68,17213,400],{"class":331},[68,17215,17216],{"class":342}," (origin ",[68,17218,7707],{"class":331},[68,17220,17221],{"class":342}," allowedOrigins.",[68,17223,17224],{"class":338},"includes",[68,17226,17227],{"class":342},"(origin)) {\n",[68,17229,17230,17233,17235,17238],{"class":70,"line":107},[68,17231,17232],{"class":338}," setResponseHeader",[68,17234,5012],{"class":342},[68,17236,17237],{"class":409},"'Access-Control-Allow-Origin'",[68,17239,17240],{"class":342},", origin)\n",[68,17242,17243],{"class":70,"line":113},[68,17244,429],{"class":342},[68,17246,17247],{"class":70,"line":119},[68,17248,123],{"emptyLinePlaceholder":122},[68,17250,17251,17253],{"class":70,"line":126},[68,17252,5641],{"class":338},[68,17254,5644],{"class":342},[68,17256,17257,17259,17261,17263],{"class":70,"line":132},[68,17258,5668],{"class":409},[68,17260,938],{"class":342},[68,17262,5673],{"class":409},[68,17264,2751],{"class":342},[68,17266,17267,17269,17271,17273],{"class":70,"line":2135},[68,17268,5680],{"class":409},[68,17270,938],{"class":342},[68,17272,5685],{"class":409},[68,17274,2751],{"class":342},[68,17276,17277],{"class":70,"line":2141},[68,17278,1859],{"class":342},[68,17280,17281],{"class":70,"line":2437},[68,17282,123],{"emptyLinePlaceholder":122},[68,17284,17285,17287,17289,17291,17293,17295,17297],{"class":70,"line":2442},[68,17286,400],{"class":331},[68,17288,2657],{"class":342},[68,17290,5704],{"class":338},[68,17292,5707],{"class":342},[68,17294,406],{"class":331},[68,17296,5712],{"class":409},[68,17298,413],{"class":342},[68,17300,17301,17303,17305],{"class":70,"line":2447},[68,17302,5719],{"class":342},[68,17304,1593],{"class":331},[68,17306,5724],{"class":353},[68,17308,17309,17311],{"class":70,"line":2652},[68,17310,418],{"class":331},[68,17312,17313],{"class":409}," ''\n",[68,17315,17316],{"class":70,"line":2687},[68,17317,429],{"class":342},[68,17319,17320],{"class":70,"line":2692},[68,17321,2789],{"class":342},[20,17323,17324],{},"Server middleware runs only on the server. It processes every request including API routes, static files, and SSR page renders. Use it for:",[516,17326,17327,17330,17333,17336],{},[519,17328,17329],{},"Logging all requests",[519,17331,17332],{},"CORS headers",[519,17334,17335],{},"Authentication token parsing (extracting the user from the token and attaching it to event context)",[519,17337,17338],{},"Request rate limiting at the infrastructure level",[15,17340,17341],{"id":15530},"Plugins",[20,17343,17344],{},"Plugins are for initialization — running code once when the Nuxt application starts, either on the server or client. They register third-party libraries, set up global error handlers, configure API clients, and extend Vue.",[59,17346,17348],{"className":316,"code":17347,"language":318,"meta":64,"style":64},"// plugins/analytics.client.ts\nexport default defineNuxtPlugin(() => {\n // .client.ts suffix: runs only in the browser\n // Perfect for browser-only third-party libraries\n\n window.analytics = Analytics({\n app: 'my-app',\n version: '1.0.0',\n plugins: [segmentPlugin({ writeKey: useRuntimeConfig().public.segmentKey })],\n })\n})\n",[37,17349,17350,17355,17369,17374,17379,17383,17395,17405,17415,17432,17436],{"__ignoreMap":64},[68,17351,17352],{"class":70,"line":71},[68,17353,17354],{"class":325},"// plugins/analytics.client.ts\n",[68,17356,17357,17359,17361,17363,17365,17367],{"class":70,"line":77},[68,17358,4892],{"class":331},[68,17360,4895],{"class":331},[68,17362,8890],{"class":338},[68,17364,1614],{"class":342},[68,17366,1617],{"class":331},[68,17368,1620],{"class":342},[68,17370,17371],{"class":70,"line":83},[68,17372,17373],{"class":325}," // .client.ts suffix: runs only in the browser\n",[68,17375,17376],{"class":70,"line":89},[68,17377,17378],{"class":325}," // Perfect for browser-only third-party libraries\n",[68,17380,17381],{"class":70,"line":95},[68,17382,123],{"emptyLinePlaceholder":122},[68,17384,17385,17388,17390,17393],{"class":70,"line":101},[68,17386,17387],{"class":342}," window.analytics ",[68,17389,1593],{"class":331},[68,17391,17392],{"class":338}," Analytics",[68,17394,1789],{"class":342},[68,17396,17397,17400,17403],{"class":70,"line":107},[68,17398,17399],{"class":342}," app: ",[68,17401,17402],{"class":409},"'my-app'",[68,17404,2751],{"class":342},[68,17406,17407,17410,17413],{"class":70,"line":113},[68,17408,17409],{"class":342}," version: ",[68,17411,17412],{"class":409},"'1.0.0'",[68,17414,2751],{"class":342},[68,17416,17417,17420,17423,17426,17429],{"class":70,"line":119},[68,17418,17419],{"class":342}," plugins: [",[68,17421,17422],{"class":338},"segmentPlugin",[68,17424,17425],{"class":342},"({ writeKey: ",[68,17427,17428],{"class":338},"useRuntimeConfig",[68,17430,17431],{"class":342},"().public.segmentKey })],\n",[68,17433,17434],{"class":70,"line":126},[68,17435,1859],{"class":342},[68,17437,17438],{"class":70,"line":132},[68,17439,2789],{"class":342},[59,17441,17443],{"className":316,"code":17442,"language":318,"meta":64,"style":64},"// plugins/sentry.ts\n// No suffix: runs on both server and client\nimport * as Sentry from '@sentry/vue'\n\nExport default defineNuxtPlugin((nuxtApp) => {\n const config = useRuntimeConfig()\n\n Sentry.init({\n app: nuxtApp.vueApp,\n dsn: config.public.sentryDsn,\n environment: config.public.environment,\n integrations: [\n new Sentry.BrowserTracing({\n routingInstrumentation: Sentry.vueRouterInstrumentation(nuxtApp.$router),\n }),\n ],\n tracesSampleRate: 0.1,\n })\n})\n",[37,17444,17445,17449,17454,17470,17474,17492,17504,17508,17516,17521,17525,17530,17535,17546,17557,17561,17565,17575,17579],{"__ignoreMap":64},[68,17446,17447],{"class":70,"line":71},[68,17448,12234],{"class":325},[68,17450,17451],{"class":70,"line":77},[68,17452,17453],{"class":325},"// No suffix: runs on both server and client\n",[68,17455,17456,17458,17460,17462,17465,17467],{"class":70,"line":83},[68,17457,2037],{"class":331},[68,17459,3520],{"class":353},[68,17461,5958],{"class":331},[68,17463,17464],{"class":342}," Sentry ",[68,17466,2043],{"class":331},[68,17468,17469],{"class":409}," '@sentry/vue'\n",[68,17471,17472],{"class":70,"line":89},[68,17473,123],{"emptyLinePlaceholder":122},[68,17475,17476,17478,17480,17482,17484,17486,17488,17490],{"class":70,"line":95},[68,17477,4519],{"class":342},[68,17479,4522],{"class":331},[68,17481,8890],{"class":338},[68,17483,2525],{"class":342},[68,17485,4203],{"class":346},[68,17487,1869],{"class":342},[68,17489,1617],{"class":331},[68,17491,1620],{"class":342},[68,17493,17494,17496,17498,17500,17502],{"class":70,"line":101},[68,17495,376],{"class":331},[68,17497,9305],{"class":353},[68,17499,382],{"class":331},[68,17501,9310],{"class":338},[68,17503,1602],{"class":342},[68,17505,17506],{"class":70,"line":107},[68,17507,123],{"emptyLinePlaceholder":122},[68,17509,17510,17512,17514],{"class":70,"line":113},[68,17511,12280],{"class":342},[68,17513,12283],{"class":338},[68,17515,1789],{"class":342},[68,17517,17518],{"class":70,"line":119},[68,17519,17520],{"class":342}," app: nuxtApp.vueApp,\n",[68,17522,17523],{"class":70,"line":126},[68,17524,12290],{"class":342},[68,17526,17527],{"class":70,"line":132},[68,17528,17529],{"class":342}," environment: config.public.environment,\n",[68,17531,17532],{"class":70,"line":2135},[68,17533,17534],{"class":342}," integrations: [\n",[68,17536,17537,17539,17541,17544],{"class":70,"line":2141},[68,17538,2551],{"class":331},[68,17540,12280],{"class":342},[68,17542,17543],{"class":338},"BrowserTracing",[68,17545,1789],{"class":342},[68,17547,17548,17551,17554],{"class":70,"line":2437},[68,17549,17550],{"class":342}," routingInstrumentation: Sentry.",[68,17552,17553],{"class":338},"vueRouterInstrumentation",[68,17555,17556],{"class":342},"(nuxtApp.$router),\n",[68,17558,17559],{"class":70,"line":2442},[68,17560,4736],{"class":342},[68,17562,17563],{"class":70,"line":2447},[68,17564,13221],{"class":342},[68,17566,17567,17570,17573],{"class":70,"line":2652},[68,17568,17569],{"class":342}," tracesSampleRate: ",[68,17571,17572],{"class":353},"0.1",[68,17574,2751],{"class":342},[68,17576,17577],{"class":70,"line":2687},[68,17578,1859],{"class":342},[68,17580,17581],{"class":70,"line":2692},[68,17582,2789],{"class":342},[20,17584,17585],{},"Plugins can provide helpers through the Nuxt app context:",[59,17587,17589],{"className":316,"code":17588,"language":318,"meta":64,"style":64},"// plugins/api.ts\nexport default defineNuxtPlugin(() => {\n const config = useRuntimeConfig()\n\n const api = $fetch.create({\n baseURL: config.public.apiBase,\n headers: {\n 'Accept': 'application/json',\n },\n async onResponseError({ response }) {\n if (response.status === 401) {\n await navigateTo('/login')\n }\n },\n })\n\n return {\n provide: {\n api,\n },\n }\n})\n",[37,17590,17591,17596,17610,17622,17626,17641,17646,17651,17663,17667,17681,17693,17705,17709,17713,17717,17721,17727,17732,17737,17741,17745],{"__ignoreMap":64},[68,17592,17593],{"class":70,"line":71},[68,17594,17595],{"class":325},"// plugins/api.ts\n",[68,17597,17598,17600,17602,17604,17606,17608],{"class":70,"line":77},[68,17599,4892],{"class":331},[68,17601,4895],{"class":331},[68,17603,8890],{"class":338},[68,17605,1614],{"class":342},[68,17607,1617],{"class":331},[68,17609,1620],{"class":342},[68,17611,17612,17614,17616,17618,17620],{"class":70,"line":83},[68,17613,376],{"class":331},[68,17615,9305],{"class":353},[68,17617,382],{"class":331},[68,17619,9310],{"class":338},[68,17621,1602],{"class":342},[68,17623,17624],{"class":70,"line":89},[68,17625,123],{"emptyLinePlaceholder":122},[68,17627,17628,17630,17633,17635,17637,17639],{"class":70,"line":95},[68,17629,376],{"class":331},[68,17631,17632],{"class":353}," api",[68,17634,382],{"class":331},[68,17636,8901],{"class":342},[68,17638,8904],{"class":338},[68,17640,1789],{"class":342},[68,17642,17643],{"class":70,"line":101},[68,17644,17645],{"class":342}," baseURL: config.public.apiBase,\n",[68,17647,17648],{"class":70,"line":107},[68,17649,17650],{"class":342}," headers: {\n",[68,17652,17653,17656,17658,17661],{"class":70,"line":113},[68,17654,17655],{"class":409}," 'Accept'",[68,17657,938],{"class":342},[68,17659,17660],{"class":409},"'application/json'",[68,17662,2751],{"class":342},[68,17664,17665],{"class":70,"line":119},[68,17666,3893],{"class":342},[68,17668,17669,17671,17673,17676,17678],{"class":70,"line":126},[68,17670,6709],{"class":331},[68,17672,8911],{"class":338},[68,17674,17675],{"class":342},"({ ",[68,17677,8921],{"class":346},[68,17679,17680],{"class":342}," }) {\n",[68,17682,17683,17685,17687,17689,17691],{"class":70,"line":132},[68,17684,400],{"class":331},[68,17686,8934],{"class":342},[68,17688,406],{"class":331},[68,17690,8939],{"class":353},[68,17692,413],{"class":342},[68,17694,17695,17697,17699,17701,17703],{"class":70,"line":2135},[68,17696,385],{"class":331},[68,17698,7725],{"class":338},[68,17700,343],{"class":342},[68,17702,8791],{"class":409},[68,17704,1702],{"class":342},[68,17706,17707],{"class":70,"line":2141},[68,17708,429],{"class":342},[68,17710,17711],{"class":70,"line":2437},[68,17712,3893],{"class":342},[68,17714,17715],{"class":70,"line":2442},[68,17716,1859],{"class":342},[68,17718,17719],{"class":70,"line":2447},[68,17720,123],{"emptyLinePlaceholder":122},[68,17722,17723,17725],{"class":70,"line":2652},[68,17724,418],{"class":331},[68,17726,1620],{"class":342},[68,17728,17729],{"class":70,"line":2687},[68,17730,17731],{"class":342}," provide: {\n",[68,17733,17734],{"class":70,"line":2692},[68,17735,17736],{"class":342}," api,\n",[68,17738,17739],{"class":70,"line":2697},[68,17740,3893],{"class":342},[68,17742,17743],{"class":70,"line":3129},[68,17744,429],{"class":342},[68,17746,17747],{"class":70,"line":3134},[68,17748,2789],{"class":342},[20,17750,17751,17752,17755,17756,17759,17760,51],{},"Anything provided through ",[37,17753,17754],{},"return { provide: { ... } }"," becomes available as ",[37,17757,17758],{},"useNuxtApp().$api"," or directly in templates as ",[37,17761,17762],{},"$api",[15,17764,17766],{"id":17765},"the-decision-tree","The Decision Tree",[20,17768,17769],{},"When you need to add some behavior to your Nuxt app, ask these questions:",[20,17771,17772,17775],{},[55,17773,17774],{},"Does it need to intercept page navigation?","\nUse route middleware. Guard authenticated routes, redirect by role, track page views.",[20,17777,17778,17781],{},[55,17779,17780],{},"Does it need to process every HTTP request on the server?","\nUse server middleware. Logging, CORS, auth token extraction.",[20,17783,17784,17787],{},[55,17785,17786],{},"Does it need to run once at startup to set something up?","\nUse a plugin. Initialize analytics, configure a global API client, register a third-party library.",[20,17789,17790,17793],{},[55,17791,17792],{},"Is it logic that components share?","\nUse a composable. Shared state, shared behavior, reusable reactive patterns.",[15,17795,17797],{"id":17796},"common-mistakes","Common Mistakes",[20,17799,17800,17803],{},[55,17801,17802],{},"Using a plugin to protect routes."," Plugins run once at startup, not on every navigation. You cannot redirect users in a plugin based on authentication state. Use route middleware for that.",[20,17805,17806,17809],{},[55,17807,17808],{},"Using route middleware for API security."," Route middleware can be bypassed by making direct API calls. Never use it as your only authentication check on API data. Protect API routes with server middleware or per-route authentication checks.",[20,17811,17812,17815,17816,17819,17820,17822],{},[55,17813,17814],{},"Using server middleware for client-only operations."," Server middleware runs on the server, always. If you try to access ",[37,17817,17818],{},"window"," or ",[37,17821,7280],{}," in server middleware, it will throw an error.",[20,17824,17825,17828],{},[55,17826,17827],{},"Making route middleware async when it does not need to be."," Every async middleware adds latency to navigation. Only await things you actually need before deciding whether to allow the navigation.",[15,17830,17832],{"id":17831},"plugin-execution-order","Plugin Execution Order",[20,17834,17835,17836,17839],{},"Plugins execute in the order they are listed in the ",[37,17837,17838],{},"plugins/"," directory (alphabetically). When order matters, prefix filenames with numbers:",[59,17841,17844],{"className":17842,"code":17843,"language":4098},[4096],"plugins/\n 01.pinia.ts ← First\n 02.sentry.ts ← Second (uses pinia state)\n 03.analytics.ts ← Third\n",[37,17845,17843],{"__ignoreMap":64},[20,17847,17848,17849,350],{},"Or specify order explicitly in ",[37,17850,7821],{},[59,17852,17854],{"className":316,"code":17853,"language":318,"meta":64,"style":64},"plugins: [\n '~/plugins/01.pinia.ts',\n '~/plugins/02.sentry.ts',\n '~/plugins/03.analytics.ts',\n]\n",[37,17855,17856,17862,17869,17876,17883],{"__ignoreMap":64},[68,17857,17858,17860],{"class":70,"line":71},[68,17859,15530],{"class":338},[68,17861,13562],{"class":342},[68,17863,17864,17867],{"class":70,"line":77},[68,17865,17866],{"class":409}," '~/plugins/01.pinia.ts'",[68,17868,2751],{"class":342},[68,17870,17871,17874],{"class":70,"line":83},[68,17872,17873],{"class":409}," '~/plugins/02.sentry.ts'",[68,17875,2751],{"class":342},[68,17877,17878,17881],{"class":70,"line":89},[68,17879,17880],{"class":409}," '~/plugins/03.analytics.ts'",[68,17882,2751],{"class":342},[68,17884,17885],{"class":70,"line":95},[68,17886,15549],{"class":342},[20,17888,17889],{},"Middleware, plugins, and composables each have a clear purpose in Nuxt's architecture. The framework is opinionated about where logic goes, and following those opinions pays back in clarity and maintainability.",[509,17891],{},[20,17893,17894,17895,51],{},"If you are working through a Nuxt architecture question or want a review of your middleware and plugin setup, I am happy to help. Book a call at ",[502,17896,817],{"href":504,"rel":17897},[506],[509,17899],{},[15,17901,514],{"id":513},[516,17903,17904,17908,17912,17916],{},[519,17905,17906],{},[502,17907,4396],{"href":4395},[519,17909,17910],{},[502,17911,4402],{"href":4401},[519,17913,17914],{},[502,17915,4408],{"href":4407},[519,17917,17918],{},[502,17919,4414],{"href":4413},[544,17921,17922],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":64,"searchDepth":83,"depth":83,"links":17924},[17925,17926,17927,17928,17929,17930,17931,17932],{"id":16621,"depth":77,"text":16622},{"id":16659,"depth":77,"text":16660},{"id":5600,"depth":77,"text":5601},{"id":15530,"depth":77,"text":17341},{"id":17765,"depth":77,"text":17766},{"id":17796,"depth":77,"text":17797},{"id":17831,"depth":77,"text":17832},{"id":513,"depth":77,"text":514},"A clear breakdown of Nuxt route middleware vs server middleware vs plugins — what each does, when to use which, and patterns for authentication, logging, and initialization.",[17935,17936],"Nuxt middleware","Nuxt plugins",{},"/blog/nuxt-middleware-guide",{"title":16609,"description":17933},"blog/nuxt-middleware-guide",[4438,17942,17341],"Middleware","6r-7_lzg2TcP4ykum1Pof0xjFilnAEG38RS2itEVVeY",{"id":17945,"title":14012,"author":17946,"body":17947,"category":557,"date":558,"description":19222,"extension":560,"featured":561,"image":562,"keywords":19223,"meta":19225,"navigation":122,"path":14011,"readTime":107,"seo":19226,"stem":19227,"tags":19228,"__hash__":19230},"blog/blog/nuxt-performance-optimization.md",{"name":9,"bio":10},{"type":12,"value":17948,"toc":19208},[17949,17952,17955,17959,17962,17968,17974,17980,17983,17986,17990,17993,18010,18094,18100,18111,18118,18122,18129,18191,18194,18293,18297,18300,18413,18416,18420,18423,18516,18526,18605,18609,18612,18715,18722,18725,18729,18736,18739,18789,18792,18796,18799,18830,18833,18961,18965,18968,19032,19037,19041,19054,19137,19140,19144,19147,19150,19170,19173,19175,19181,19183,19185,19205],[20,17950,17951],{},"A Lighthouse score of 80 is table stakes. Most Nuxt applications hit that without much effort because the framework's defaults are sensible. Getting to 95+ requires deliberate choices about what JavaScript ships to the browser, when it executes, and how aggressively you cache at every layer.",[20,17953,17954],{},"I have tuned the performance of enough production Nuxt applications that I have a consistent set of techniques that move the needle. These are not theoretical — they are patterns I apply on client projects and measure the impact of.",[15,17956,17958],{"id":17957},"understand-your-baseline-first","Understand Your Baseline First",[20,17960,17961],{},"Before optimizing anything, understand what you are optimizing. Run a Lighthouse audit in Chrome DevTools with throttling enabled (simulates a mobile 4G connection). Look at the three numbers that actually matter:",[20,17963,17964,17967],{},[55,17965,17966],{},"LCP (Largest Contentful Paint):"," When does the main content appear? Target: under 2.5 seconds.",[20,17969,17970,17973],{},[55,17971,17972],{},"INP (Interaction to Next Paint):"," How quickly does the page respond to input? Target: under 200ms.",[20,17975,17976,17979],{},[55,17977,17978],{},"CLS (Cumulative Layout Shift):"," How much does the layout shift unexpectedly? Target: under 0.1.",[20,17981,17982],{},"Then open the Network tab and the Coverage tab. The Network tab shows you exactly what is being downloaded and how large each file is. The Coverage tab shows you how much of that downloaded JavaScript is actually executed.",[20,17984,17985],{},"The Coverage tab is often a revelation. On unoptimized applications, I routinely see 40-60% of downloaded JavaScript going unused on any given page. That is waste you can eliminate.",[15,17987,17989],{"id":17988},"bundle-analysis","Bundle Analysis",[20,17991,17992],{},"Install the bundle analyzer:",[59,17994,17996],{"className":1888,"code":17995,"language":1890,"meta":64,"style":64},"npm install --save-dev rollup-plugin-visualizer\n",[37,17997,17998],{"__ignoreMap":64},[68,17999,18000,18002,18004,18007],{"class":70,"line":71},[68,18001,7311],{"class":338},[68,18003,7314],{"class":409},[68,18005,18006],{"class":353}," --save-dev",[68,18008,18009],{"class":409}," rollup-plugin-visualizer\n",[59,18011,18013],{"className":316,"code":18012,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nvite: {\n plugins: [\n process.env.ANALYZE && visualizer({\n open: true,\n gzipSize: true,\n brotliSize: true,\n }),\n ].filter(Boolean),\n},\n",[37,18014,18015,18019,18026,18033,18049,18058,18067,18076,18080,18090],{"__ignoreMap":64},[68,18016,18017],{"class":70,"line":71},[68,18018,9151],{"class":325},[68,18020,18021,18024],{"class":70,"line":77},[68,18022,18023],{"class":338},"vite",[68,18025,7834],{"class":342},[68,18027,18028,18031],{"class":70,"line":83},[68,18029,18030],{"class":338}," plugins",[68,18032,13562],{"class":342},[68,18034,18035,18038,18041,18044,18047],{"class":70,"line":89},[68,18036,18037],{"class":342}," process.env.",[68,18039,18040],{"class":353},"ANALYZE",[68,18042,18043],{"class":331}," &&",[68,18045,18046],{"class":338}," visualizer",[68,18048,1789],{"class":342},[68,18050,18051,18054,18056],{"class":70,"line":95},[68,18052,18053],{"class":342}," open: ",[68,18055,3619],{"class":353},[68,18057,2751],{"class":342},[68,18059,18060,18063,18065],{"class":70,"line":101},[68,18061,18062],{"class":342}," gzipSize: ",[68,18064,3619],{"class":353},[68,18066,2751],{"class":342},[68,18068,18069,18072,18074],{"class":70,"line":107},[68,18070,18071],{"class":342}," brotliSize: ",[68,18073,3619],{"class":353},[68,18075,2751],{"class":342},[68,18077,18078],{"class":70,"line":113},[68,18079,4736],{"class":342},[68,18081,18082,18085,18087],{"class":70,"line":119},[68,18083,18084],{"class":342}," ].",[68,18086,15907],{"class":338},[68,18088,18089],{"class":342},"(Boolean),\n",[68,18091,18092],{"class":70,"line":126},[68,18093,11897],{"class":342},[20,18095,18096,18097,18099],{},"Run ",[37,18098,12377],{}," to generate an interactive treemap of your bundle. Look for:",[516,18101,18102,18105,18108],{},[519,18103,18104],{},"Large libraries that could be replaced with smaller alternatives",[519,18106,18107],{},"Libraries that are imported but barely used",[519,18109,18110],{},"Duplicate dependencies being bundled multiple times",[20,18112,18113,18114,18117],{},"Common findings: ",[37,18115,18116],{},"lodash"," imported in full instead of individual functions, large chart libraries included for a single chart on one page, moment.js instead of date-fns.",[15,18119,18121],{"id":18120},"lazy-loading-routes","Lazy Loading Routes",[20,18123,18124,18125,18128],{},"Nuxt lazy-loads routes by default — each page becomes its own JavaScript chunk. But components imported directly are included in the current chunk. Prefix large components with ",[37,18126,18127],{},"Lazy"," to defer them:",[59,18130,18132],{"className":7755,"code":18131,"language":7757,"meta":64,"style":64},"\u003C!-- Direct import: included in the current page bundle -->\n\u003CHeavyChart :data=\"chartData\" />\n\n\u003C!-- Lazy import: fetched only when the component renders -->\n\u003CLazyHeavyChart :data=\"chartData\" />\n",[37,18133,18134,18139,18161,18165,18170],{"__ignoreMap":64},[68,18135,18136],{"class":70,"line":71},[68,18137,18138],{"class":325},"\u003C!-- Direct import: included in the current page bundle -->\n",[68,18140,18141,18143,18146,18148,18150,18152,18154,18157,18159],{"class":70,"line":77},[68,18142,365],{"class":342},[68,18144,18145],{"class":7771},"HeavyChart",[68,18147,12821],{"class":342},[68,18149,4157],{"class":338},[68,18151,1593],{"class":342},[68,18153,634],{"class":409},[68,18155,18156],{"class":342},"chartData",[68,18158,634],{"class":409},[68,18160,11088],{"class":342},[68,18162,18163],{"class":70,"line":83},[68,18164,123],{"emptyLinePlaceholder":122},[68,18166,18167],{"class":70,"line":89},[68,18168,18169],{"class":325},"\u003C!-- Lazy import: fetched only when the component renders -->\n",[68,18171,18172,18174,18177,18179,18181,18183,18185,18187,18189],{"class":70,"line":95},[68,18173,365],{"class":342},[68,18175,18176],{"class":7771},"LazyHeavyChart",[68,18178,12821],{"class":342},[68,18180,4157],{"class":338},[68,18182,1593],{"class":342},[68,18184,634],{"class":409},[68,18186,18156],{"class":342},[68,18188,634],{"class":409},[68,18190,11088],{"class":342},[20,18192,18193],{},"For components that might not render at all (error states, empty states, modals), lazy loading is especially valuable:",[59,18195,18197],{"className":7755,"code":18196,"language":7757,"meta":64,"style":64},"\u003CLazyErrorBoundary v-if=\"hasError\" :error=\"error\" />\n\u003CLazyEmptyState v-else-if=\"items.length === 0\" />\n\u003CLazyConfirmModal v-if=\"showConfirm\" @confirm=\"handleConfirm\" />\n",[37,18198,18199,18231,18258],{"__ignoreMap":64},[68,18200,18201,18203,18206,18208,18210,18212,18215,18217,18219,18221,18223,18225,18227,18229],{"class":70,"line":71},[68,18202,365],{"class":342},[68,18204,18205],{"class":7771},"LazyErrorBoundary",[68,18207,10964],{"class":331},[68,18209,1593],{"class":342},[68,18211,634],{"class":409},[68,18213,18214],{"class":342},"hasError",[68,18216,634],{"class":409},[68,18218,12821],{"class":342},[68,18220,16521],{"class":338},[68,18222,1593],{"class":342},[68,18224,634],{"class":409},[68,18226,16521],{"class":342},[68,18228,634],{"class":409},[68,18230,11088],{"class":342},[68,18232,18233,18235,18238,18241,18243,18245,18248,18250,18252,18254,18256],{"class":70,"line":77},[68,18234,365],{"class":342},[68,18236,18237],{"class":7771},"LazyEmptyState",[68,18239,18240],{"class":331}," v-else-if",[68,18242,1593],{"class":342},[68,18244,634],{"class":409},[68,18246,18247],{"class":342},"items.",[68,18249,2776],{"class":353},[68,18251,6020],{"class":331},[68,18253,2963],{"class":353},[68,18255,634],{"class":409},[68,18257,11088],{"class":342},[68,18259,18260,18262,18265,18267,18269,18271,18274,18276,18279,18282,18284,18286,18289,18291],{"class":70,"line":83},[68,18261,365],{"class":342},[68,18263,18264],{"class":7771},"LazyConfirmModal",[68,18266,10964],{"class":331},[68,18268,1593],{"class":342},[68,18270,634],{"class":409},[68,18272,18273],{"class":342},"showConfirm",[68,18275,634],{"class":409},[68,18277,18278],{"class":342}," @",[68,18280,18281],{"class":338},"confirm",[68,18283,1593],{"class":342},[68,18285,634],{"class":409},[68,18287,18288],{"class":342},"handleConfirm",[68,18290,634],{"class":409},[68,18292,11088],{"class":342},[15,18294,18296],{"id":18295},"granular-code-splitting","Granular Code Splitting",[20,18298,18299],{},"For large features that are only used by some users, split them into separate chunks:",[59,18301,18303],{"className":316,"code":18302,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nvite: {\n build: {\n rollupOptions: {\n output: {\n manualChunks: {\n 'charts': ['chart.js', 'vue-chartjs'],\n 'editor': ['@tiptap/core', '@tiptap/vue-3'],\n 'maps': ['leaflet', '@vue-leaflet/vue-leaflet'],\n },\n },\n },\n },\n},\n",[37,18304,18305,18309,18315,18322,18329,18336,18343,18360,18376,18393,18397,18401,18405,18409],{"__ignoreMap":64},[68,18306,18307],{"class":70,"line":71},[68,18308,9151],{"class":325},[68,18310,18311,18313],{"class":70,"line":77},[68,18312,18023],{"class":338},[68,18314,7834],{"class":342},[68,18316,18317,18320],{"class":70,"line":83},[68,18318,18319],{"class":338}," build",[68,18321,7834],{"class":342},[68,18323,18324,18327],{"class":70,"line":89},[68,18325,18326],{"class":338}," rollupOptions",[68,18328,7834],{"class":342},[68,18330,18331,18334],{"class":70,"line":95},[68,18332,18333],{"class":338}," output",[68,18335,7834],{"class":342},[68,18337,18338,18341],{"class":70,"line":101},[68,18339,18340],{"class":338}," manualChunks",[68,18342,7834],{"class":342},[68,18344,18345,18348,18350,18353,18355,18358],{"class":70,"line":107},[68,18346,18347],{"class":409}," 'charts'",[68,18349,7842],{"class":342},[68,18351,18352],{"class":409},"'chart.js'",[68,18354,180],{"class":342},[68,18356,18357],{"class":409},"'vue-chartjs'",[68,18359,6051],{"class":342},[68,18361,18362,18364,18366,18369,18371,18374],{"class":70,"line":113},[68,18363,6164],{"class":409},[68,18365,7842],{"class":342},[68,18367,18368],{"class":409},"'@tiptap/core'",[68,18370,180],{"class":342},[68,18372,18373],{"class":409},"'@tiptap/vue-3'",[68,18375,6051],{"class":342},[68,18377,18378,18381,18383,18386,18388,18391],{"class":70,"line":119},[68,18379,18380],{"class":409}," 'maps'",[68,18382,7842],{"class":342},[68,18384,18385],{"class":409},"'leaflet'",[68,18387,180],{"class":342},[68,18389,18390],{"class":409},"'@vue-leaflet/vue-leaflet'",[68,18392,6051],{"class":342},[68,18394,18395],{"class":70,"line":126},[68,18396,3893],{"class":342},[68,18398,18399],{"class":70,"line":132},[68,18400,3893],{"class":342},[68,18402,18403],{"class":70,"line":2135},[68,18404,3893],{"class":342},[68,18406,18407],{"class":70,"line":2141},[68,18408,3893],{"class":342},[68,18410,18411],{"class":70,"line":2437},[68,18412,11897],{"class":342},[20,18414,18415],{},"These chunks only download when a component that uses them renders for the first time. A user who never opens the map view never downloads the maps bundle.",[15,18417,18419],{"id":18418},"defer-non-critical-javascript","Defer Non-Critical JavaScript",[20,18421,18422],{},"Third-party scripts — analytics, chat widgets, marketing pixels — should never block page rendering:",[59,18424,18426],{"className":7755,"code":18425,"language":7757,"meta":64,"style":64},"\u003C!-- plugins/analytics.client.ts -->\n\u003Cscript setup lang=\"ts\">\nonMounted(() => {\n // Delay until after the page is interactive\n requestIdleCallback(() => {\n // Load analytics\n window.dataLayer = window.dataLayer || []\n // ... Google Analytics initialization\n })\n})\n\u003C/script>\n",[37,18427,18428,18433,18449,18460,18465,18476,18481,18495,18500,18504,18508],{"__ignoreMap":64},[68,18429,18430],{"class":70,"line":71},[68,18431,18432],{"class":325},"\u003C!-- plugins/analytics.client.ts -->\n",[68,18434,18435,18437,18439,18441,18443,18445,18447],{"class":70,"line":77},[68,18436,365],{"class":342},[68,18438,7772],{"class":7771},[68,18440,6980],{"class":338},[68,18442,7777],{"class":338},[68,18444,1593],{"class":342},[68,18446,7782],{"class":409},[68,18448,7785],{"class":342},[68,18450,18451,18454,18456,18458],{"class":70,"line":83},[68,18452,18453],{"class":338},"onMounted",[68,18455,1614],{"class":342},[68,18457,1617],{"class":331},[68,18459,1620],{"class":342},[68,18461,18462],{"class":70,"line":89},[68,18463,18464],{"class":325}," // Delay until after the page is interactive\n",[68,18466,18467,18470,18472,18474],{"class":70,"line":95},[68,18468,18469],{"class":338}," requestIdleCallback",[68,18471,1614],{"class":342},[68,18473,1617],{"class":331},[68,18475,1620],{"class":342},[68,18477,18478],{"class":70,"line":101},[68,18479,18480],{"class":325}," // Load analytics\n",[68,18482,18483,18486,18488,18490,18492],{"class":70,"line":107},[68,18484,18485],{"class":342}," window.dataLayer ",[68,18487,1593],{"class":331},[68,18489,18485],{"class":342},[68,18491,6466],{"class":331},[68,18493,18494],{"class":342}," []\n",[68,18496,18497],{"class":70,"line":113},[68,18498,18499],{"class":325}," // ... Google Analytics initialization\n",[68,18501,18502],{"class":70,"line":119},[68,18503,1859],{"class":342},[68,18505,18506],{"class":70,"line":126},[68,18507,2789],{"class":342},[68,18509,18510,18512,18514],{"class":70,"line":132},[68,18511,7811],{"class":342},[68,18513,7772],{"class":7771},[68,18515,7785],{"class":342},[20,18517,4139,18518,18521,18522,18525],{},[37,18519,18520],{},"\u003CScriptGoogleAnalytics>"," and similar components from ",[37,18523,18524],{},"@nuxt/scripts"," handle this pattern with a clean API and a Partytown integration for true off-main-thread execution.",[59,18527,18529],{"className":316,"code":18528,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nscripts: {\n registry: {\n googleAnalytics: {\n id: 'G-XXXXXXXXXX',\n scriptOptions: {\n trigger: 'idle', // Load after page is idle\n },\n },\n },\n},\n",[37,18530,18531,18535,18542,18549,18556,18567,18574,18589,18593,18597,18601],{"__ignoreMap":64},[68,18532,18533],{"class":70,"line":71},[68,18534,9151],{"class":325},[68,18536,18537,18540],{"class":70,"line":77},[68,18538,18539],{"class":338},"scripts",[68,18541,7834],{"class":342},[68,18543,18544,18547],{"class":70,"line":83},[68,18545,18546],{"class":338}," registry",[68,18548,7834],{"class":342},[68,18550,18551,18554],{"class":70,"line":89},[68,18552,18553],{"class":338}," googleAnalytics",[68,18555,7834],{"class":342},[68,18557,18558,18560,18562,18565],{"class":70,"line":95},[68,18559,6126],{"class":338},[68,18561,938],{"class":342},[68,18563,18564],{"class":409},"'G-XXXXXXXXXX'",[68,18566,2751],{"class":342},[68,18568,18569,18572],{"class":70,"line":101},[68,18570,18571],{"class":338}," scriptOptions",[68,18573,7834],{"class":342},[68,18575,18576,18579,18581,18584,18586],{"class":70,"line":107},[68,18577,18578],{"class":338}," trigger",[68,18580,938],{"class":342},[68,18582,18583],{"class":409},"'idle'",[68,18585,180],{"class":342},[68,18587,18588],{"class":325},"// Load after page is idle\n",[68,18590,18591],{"class":70,"line":113},[68,18592,3893],{"class":342},[68,18594,18595],{"class":70,"line":119},[68,18596,3893],{"class":342},[68,18598,18599],{"class":70,"line":126},[68,18600,3893],{"class":342},[68,18602,18603],{"class":70,"line":132},[68,18604,11897],{"class":342},[15,18606,18608],{"id":18607},"server-component-islands","Server Component Islands",[20,18610,18611],{},"For pages with mostly static content and a few interactive islands, use Nuxt's server components to reduce hydration cost:",[59,18613,18615],{"className":7755,"code":18614,"language":7757,"meta":64,"style":64},"\u003C!-- components/StaticArticle.server.vue -->\n\u003C!-- This renders on the server and ships NO JavaScript to the client -->\n\u003Ctemplate>\n \u003Carticle class=\"prose\">\n \u003Ch1>{{ title }}\u003C/h1>\n \u003Cdiv v-html=\"content\" />\n \u003CAuthorCard :author=\"author\" />\n \u003C/article>\n\u003C/template>\n",[37,18616,18617,18622,18627,18635,18650,18663,18682,18699,18707],{"__ignoreMap":64},[68,18618,18619],{"class":70,"line":71},[68,18620,18621],{"class":325},"\u003C!-- components/StaticArticle.server.vue -->\n",[68,18623,18624],{"class":70,"line":77},[68,18625,18626],{"class":325},"\u003C!-- This renders on the server and ships NO JavaScript to the client -->\n",[68,18628,18629,18631,18633],{"class":70,"line":83},[68,18630,365],{"class":342},[68,18632,10454],{"class":7771},[68,18634,7785],{"class":342},[68,18636,18637,18639,18641,18643,18645,18648],{"class":70,"line":89},[68,18638,10461],{"class":342},[68,18640,10517],{"class":7771},[68,18642,10467],{"class":338},[68,18644,1593],{"class":342},[68,18646,18647],{"class":409},"\"prose\"",[68,18649,7785],{"class":342},[68,18651,18652,18654,18656,18659,18661],{"class":70,"line":95},[68,18653,10461],{"class":342},[68,18655,10481],{"class":7771},[68,18657,18658],{"class":342},">{{ title }}\u003C/",[68,18660,10481],{"class":7771},[68,18662,7785],{"class":342},[68,18664,18665,18667,18669,18672,18674,18677,18680],{"class":70,"line":101},[68,18666,10461],{"class":342},[68,18668,10464],{"class":7771},[68,18670,18671],{"class":338}," v-html",[68,18673,1593],{"class":342},[68,18675,18676],{"class":409},"\"content\"",[68,18678,1809],{"class":18679},"s6RL2",[68,18681,7785],{"class":342},[68,18683,18684,18686,18689,18692,18694,18697],{"class":70,"line":107},[68,18685,10461],{"class":342},[68,18687,18688],{"class":7771},"AuthorCard",[68,18690,18691],{"class":338}," :author",[68,18693,1593],{"class":342},[68,18695,18696],{"class":409},"\"author\"",[68,18698,11088],{"class":342},[68,18700,18701,18703,18705],{"class":70,"line":113},[68,18702,10568],{"class":342},[68,18704,10517],{"class":7771},[68,18706,7785],{"class":342},[68,18708,18709,18711,18713],{"class":70,"line":119},[68,18710,7811],{"class":342},[68,18712,10454],{"class":7771},[68,18714,7785],{"class":342},[20,18716,18717,18718,18721],{},"Components in ",[37,18719,18720],{},".server.vue"," files are rendered on the server and sent as HTML. They do not ship JavaScript to the client, do not hydrate, and cannot have client-side interactivity. For static content sections of a page, this is a significant bundle size reduction.",[20,18723,18724],{},"Interactive elements on the same page use regular components and hydrate normally.",[15,18726,18728],{"id":18727},"payload-hydration","Payload Hydration",[20,18730,18731,18732,12757,18734,51],{},"When Nuxt SSR fetches data on the server, it includes the data in the HTML as a serialized payload. This allows the client to read the data directly without re-fetching it during hydration. This works automatically with ",[37,18733,4126],{},[37,18735,4129],{},[20,18737,18738],{},"Make sure you are using consistent keys so deduplication works:",[59,18740,18742],{"className":316,"code":18741,"language":318,"meta":64,"style":64},"// This key ensures the same data is not fetched twice\nconst { data } = await useAsyncData('homepage-posts', () =>\n $fetch('/api/posts?featured=true')\n)\n",[37,18743,18744,18749,18774,18785],{"__ignoreMap":64},[68,18745,18746],{"class":70,"line":71},[68,18747,18748],{"class":325},"// This key ensures the same data is not fetched twice\n",[68,18750,18751,18753,18755,18757,18759,18761,18763,18765,18767,18770,18772],{"class":70,"line":77},[68,18752,1991],{"class":331},[68,18754,1748],{"class":342},[68,18756,4157],{"class":353},[68,18758,1769],{"class":342},[68,18760,1593],{"class":331},[68,18762,385],{"class":331},[68,18764,10359],{"class":338},[68,18766,343],{"class":342},[68,18768,18769],{"class":409},"'homepage-posts'",[68,18771,3101],{"class":342},[68,18773,10369],{"class":331},[68,18775,18776,18778,18780,18783],{"class":70,"line":83},[68,18777,7024],{"class":338},[68,18779,343],{"class":342},[68,18781,18782],{"class":409},"'/api/posts?featured=true'",[68,18784,1702],{"class":342},[68,18786,18787],{"class":70,"line":89},[68,18788,1702],{"class":342},[20,18790,18791],{},"Without a stable key, the same API call might happen on the server, be included in the payload, and then fire again on the client — doubling your API load and slowing hydration.",[15,18793,18795],{"id":18794},"prefetching-for-perceived-performance","Prefetching for Perceived Performance",[20,18797,18798],{},"Make navigation feel instant by prefetching pages before the user clicks:",[59,18800,18802],{"className":316,"code":18801,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nexperimental: {\n payloadExtraction: true,\n},\n",[37,18803,18804,18808,18815,18826],{"__ignoreMap":64},[68,18805,18806],{"class":70,"line":71},[68,18807,9151],{"class":325},[68,18809,18810,18813],{"class":70,"line":77},[68,18811,18812],{"class":338},"experimental",[68,18814,7834],{"class":342},[68,18816,18817,18820,18822,18824],{"class":70,"line":83},[68,18818,18819],{"class":338}," payloadExtraction",[68,18821,938],{"class":342},[68,18823,3619],{"class":353},[68,18825,2751],{"class":342},[68,18827,18828],{"class":70,"line":89},[68,18829,11897],{"class":342},[20,18831,18832],{},"Nuxt prefetches page payloads when links enter the viewport by default. For pages you know users will navigate to, you can prefetch manually:",[59,18834,18836],{"className":7755,"code":18835,"language":7757,"meta":64,"style":64},"\u003Cscript setup lang=\"ts\">\nconst router = useRouter()\n\n// Prefetch when the cursor enters the button\nfunction prefetch() {\n router.prefetch('/dashboard')\n}\n\u003C/script>\n\n\u003Ctemplate>\n \u003CNuxtLink to=\"/dashboard\" @mouseenter=\"prefetch\">Dashboard\u003C/NuxtLink>\n\u003C/template>\n",[37,18837,18838,18854,18868,18872,18877,18886,18900,18904,18912,18916,18924,18953],{"__ignoreMap":64},[68,18839,18840,18842,18844,18846,18848,18850,18852],{"class":70,"line":71},[68,18841,365],{"class":342},[68,18843,7772],{"class":7771},[68,18845,6980],{"class":338},[68,18847,7777],{"class":338},[68,18849,1593],{"class":342},[68,18851,7782],{"class":409},[68,18853,7785],{"class":342},[68,18855,18856,18858,18861,18863,18866],{"class":70,"line":77},[68,18857,1991],{"class":331},[68,18859,18860],{"class":353}," router",[68,18862,382],{"class":331},[68,18864,18865],{"class":338}," useRouter",[68,18867,1602],{"class":342},[68,18869,18870],{"class":70,"line":83},[68,18871,123],{"emptyLinePlaceholder":122},[68,18873,18874],{"class":70,"line":89},[68,18875,18876],{"class":325},"// Prefetch when the cursor enters the button\n",[68,18878,18879,18881,18884],{"class":70,"line":95},[68,18880,2082],{"class":331},[68,18882,18883],{"class":338}," prefetch",[68,18885,2332],{"class":342},[68,18887,18888,18891,18894,18896,18898],{"class":70,"line":101},[68,18889,18890],{"class":342}," router.",[68,18892,18893],{"class":338},"prefetch",[68,18895,343],{"class":342},[68,18897,8694],{"class":409},[68,18899,1702],{"class":342},[68,18901,18902],{"class":70,"line":107},[68,18903,447],{"class":342},[68,18905,18906,18908,18910],{"class":70,"line":113},[68,18907,7811],{"class":342},[68,18909,7772],{"class":7771},[68,18911,7785],{"class":342},[68,18913,18914],{"class":70,"line":119},[68,18915,123],{"emptyLinePlaceholder":122},[68,18917,18918,18920,18922],{"class":70,"line":126},[68,18919,365],{"class":342},[68,18921,10454],{"class":7771},[68,18923,7785],{"class":342},[68,18925,18926,18928,18930,18933,18935,18938,18941,18943,18946,18949,18951],{"class":70,"line":132},[68,18927,10461],{"class":342},[68,18929,10594],{"class":7771},[68,18931,18932],{"class":338}," to",[68,18934,1593],{"class":342},[68,18936,18937],{"class":409},"\"/dashboard\"",[68,18939,18940],{"class":338}," @mouseenter",[68,18942,1593],{"class":342},[68,18944,18945],{"class":409},"\"prefetch\"",[68,18947,18948],{"class":342},">Dashboard\u003C/",[68,18950,10594],{"class":7771},[68,18952,7785],{"class":342},[68,18954,18955,18957,18959],{"class":70,"line":2135},[68,18956,7811],{"class":342},[68,18958,10454],{"class":7771},[68,18960,7785],{"class":342},[15,18962,18964],{"id":18963},"edge-caching-with-route-rules","Edge Caching With Route Rules",[20,18966,18967],{},"Nuxt's route rules let you configure caching per route:",[59,18969,18971],{"className":316,"code":18970,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nrouteRules: {\n '/': { swr: 3600 },\n '/blog/**': { swr: 86400 },\n '/api/products': { cache: { maxAge: 300 } },\n '/api/user/**': { cache: false },\n}\n",[37,18972,18973,18977,18983,18993,19003,19016,19028],{"__ignoreMap":64},[68,18974,18975],{"class":70,"line":71},[68,18976,9151],{"class":325},[68,18978,18979,18981],{"class":70,"line":77},[68,18980,9772],{"class":338},[68,18982,7834],{"class":342},[68,18984,18985,18987,18989,18991],{"class":70,"line":83},[68,18986,9793],{"class":409},[68,18988,9796],{"class":342},[68,18990,9799],{"class":353},[68,18992,3893],{"class":342},[68,18994,18995,18997,18999,19001],{"class":70,"line":89},[68,18996,9810],{"class":409},[68,18998,9796],{"class":342},[68,19000,9815],{"class":353},[68,19002,3893],{"class":342},[68,19004,19005,19008,19011,19013],{"class":70,"line":95},[68,19006,19007],{"class":409}," '/api/products'",[68,19009,19010],{"class":342},": { cache: { maxAge: ",[68,19012,6899],{"class":353},[68,19014,19015],{"class":342}," } },\n",[68,19017,19018,19021,19024,19026],{"class":70,"line":101},[68,19019,19020],{"class":409}," '/api/user/**'",[68,19022,19023],{"class":342},": { cache: ",[68,19025,8495],{"class":353},[68,19027,3893],{"class":342},[68,19029,19030],{"class":70,"line":107},[68,19031,447],{"class":342},[20,19033,4139,19034,19036],{},[37,19035,12207],{}," (stale-while-revalidate) value means users see cached content immediately, and the new version generates in the background. This is the pattern that makes the perceived performance of ISR match static generation.",[15,19038,19040],{"id":19039},"font-optimization","Font Optimization",[20,19042,19043,19044,12757,19047,19050,19051,350],{},"Web fonts are a common performance killer. ",[37,19045,19046],{},"@nuxtjs/google-fonts",[37,19048,19049],{},"@nuxt/fonts"," handle this correctly — they download fonts, serve them from your own domain, and use ",[37,19052,19053],{},"font-display: swap",[59,19055,19057],{"className":316,"code":19056,"language":318,"meta":64,"style":64},"// nuxt.config.ts\nfonts: {\n families: [\n { name: 'Inter', weights: [400, 500, 600, 700] },\n ],\n defaults: {\n preload: true,\n },\n},\n",[37,19058,19059,19063,19070,19077,19108,19112,19118,19129,19133],{"__ignoreMap":64},[68,19060,19061],{"class":70,"line":71},[68,19062,9151],{"class":325},[68,19064,19065,19068],{"class":70,"line":77},[68,19066,19067],{"class":338},"fonts",[68,19069,7834],{"class":342},[68,19071,19072,19075],{"class":70,"line":83},[68,19073,19074],{"class":338}," families",[68,19076,13562],{"class":342},[68,19078,19079,19082,19085,19088,19091,19093,19096,19098,19100,19102,19105],{"class":70,"line":89},[68,19080,19081],{"class":342}," { name: ",[68,19083,19084],{"class":409},"'Inter'",[68,19086,19087],{"class":342},", weights: [",[68,19089,19090],{"class":353},"400",[68,19092,180],{"class":342},[68,19094,19095],{"class":353},"500",[68,19097,180],{"class":342},[68,19099,2820],{"class":353},[68,19101,180],{"class":342},[68,19103,19104],{"class":353},"700",[68,19106,19107],{"class":342},"] },\n",[68,19109,19110],{"class":70,"line":95},[68,19111,13221],{"class":342},[68,19113,19114,19116],{"class":70,"line":101},[68,19115,11608],{"class":338},[68,19117,7834],{"class":342},[68,19119,19120,19123,19125,19127],{"class":70,"line":107},[68,19121,19122],{"class":338}," preload",[68,19124,938],{"class":342},[68,19126,3619],{"class":353},[68,19128,2751],{"class":342},[68,19130,19131],{"class":70,"line":113},[68,19132,3893],{"class":342},[68,19134,19135],{"class":70,"line":119},[68,19136,11897],{"class":342},[20,19138,19139],{},"Preload only the font weights you actually use. Preloading unused weights is a net negative — it adds HTTP requests without improving any visible metric.",[15,19141,19143],{"id":19142},"measuring-after-each-change","Measuring After Each Change",[20,19145,19146],{},"Performance optimization without measurement is guesswork. After each change, run a Lighthouse audit in an incognito window (to avoid extension interference) and record the scores. Track the Network tab payload sizes.",[20,19148,19149],{},"The changes that make the biggest difference in my experience, in rough order:",[9217,19151,19152,19155,19158,19161,19164,19167],{},[519,19153,19154],{},"Image optimization with correct sizing and modern formats (often 40-60% size reduction)",[519,19156,19157],{},"Deferring third-party scripts to idle",[519,19159,19160],{},"Lazy loading large below-the-fold components",[519,19162,19163],{},"Removing unused JavaScript dependencies",[519,19165,19166],{},"Font preloading with correct weights",[519,19168,19169],{},"Edge caching for public content",[20,19171,19172],{},"None of these are magic tricks. They are disciplined application of known techniques. The results are real and measurable, and they compound — a site that does all of these well does not have a 95 Lighthouse score, it has a 98.",[509,19174],{},[20,19176,19177,19178,51],{},"If you want a performance audit of your Nuxt application or help designing an optimization strategy, I can run through it methodically. Book a call: ",[502,19179,817],{"href":504,"rel":19180},[506],[509,19182],{},[15,19184,514],{"id":513},[516,19186,19187,19191,19197,19201],{},[519,19188,19189],{},[502,19190,12440],{"href":14045},[519,19192,19193],{},[502,19194,19196],{"href":19195},"/blog/database-query-performance","Database Query Performance: Finding and Fixing the Slow Ones",[519,19198,19199],{},[502,19200,4020],{"href":4019},[519,19202,19203],{},[502,19204,4396],{"href":4395},[544,19206,19207],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s6RL2, html code.shiki .s6RL2{--shiki-default:#FDAEB7;--shiki-default-font-style:italic}",{"title":64,"searchDepth":83,"depth":83,"links":19209},[19210,19211,19212,19213,19214,19215,19216,19217,19218,19219,19220,19221],{"id":17957,"depth":77,"text":17958},{"id":17988,"depth":77,"text":17989},{"id":18120,"depth":77,"text":18121},{"id":18295,"depth":77,"text":18296},{"id":18418,"depth":77,"text":18419},{"id":18607,"depth":77,"text":18608},{"id":18727,"depth":77,"text":18728},{"id":18794,"depth":77,"text":18795},{"id":18963,"depth":77,"text":18964},{"id":19039,"depth":77,"text":19040},{"id":19142,"depth":77,"text":19143},{"id":513,"depth":77,"text":514},"Advanced Nuxt performance techniques — code splitting, lazy hydration, bundle analysis, prefetching, edge caching, and the optimizations that move Lighthouse from 80 to 98.",[14043,19224],"Nuxt optimization",{},{"title":14012,"description":19222},"blog/nuxt-performance-optimization",[4438,4062,19229],"Web Vitals","X8sn_aG1-aVw4WKf0Zr1ZJD1lDHNnwXFWqMAbGnuzbs",[19232,19234,19235,19236,19237,19238,19239,19240,19241,19242,19243,19244,19245,19246,19247,19248,19249,19250,19251,19252,19253,19254,19255,19256,19257,19258,19259,19260,19261,19262,19263,19264,19265,19266,19267,19268,19269,19270,19271,19272,19273,19274,19275,19276,19277,19278,19279,19280,19281,19282,19284,19285,19286,19287,19288,19289,19290,19291,19292,19293,19294,19295,19296,19297,19298,19299,19300,19301,19302,19303,19304,19305,19306,19307,19308,19309,19310,19311,19312,19313,19314,19315,19317,19318,19319,19320,19321,19322,19323,19324,19325,19326,19327,19328,19329,19330,19331,19332,19333,19334,19335,19336,19337,19338,19339,19340,19341,19342,19343,19344,19345,19346,19347,19348,19349,19350,19351,19352,19353,19354,19355,19356,19357,19358,19359,19360,19361,19362,19363,19364,19365,19366,19367,19368,19369,19370,19371,19372,19373,19374,19375,19376,19377,19378,19379,19380,19381,19382,19383,19384,19385,19386,19387,19388,19389,19390,19391,19392,19393,19394,19395,19396,19397,19398,19399,19400,19401,19402,19403,19404,19405,19406,19407,19408,19409,19410,19411,19412,19413,19414,19415,19416,19417,19418,19419,19420,19421,19422,19423,19424,19425,19426,19427,19428,19429,19430,19431,19432,19433,19434,19435,19436,19437,19438,19439,19440,19441,19442,19443,19444,19445,19446,19447,19448,19449,19450,19451,19452,19453,19454,19455,19456,19457,19458,19459,19460,19461,19462,19463,19464,19465,19466,19467,19468,19469,19470,19471,19472,19473,19474,19475,19476,19477,19478,19479,19480,19481,19482,19483,19484,19485,19486,19487,19488,19489,19490,19491,19492,19493,19494,19495,19496,19497,19498,19499,19500,19501,19502,19503,19504,19505,19506,19507,19508,19509,19510,19511,19512,19513,19514,19515,19516,19517,19518,19519,19520,19521,19522,19523,19524,19525,19526,19527,19528,19529,19530,19531,19532,19533,19534,19535,19536,19537,19538,19539,19540,19541,19542,19543,19544,19545,19546,19547,19548,19549,19550,19551,19552,19553,19554,19555,19556,19557,19558,19559,19560,19561,19562,19563,19564,19565,19566,19567,19568,19569,19570,19571,19572,19573,19574,19575,19576,19577,19578,19579,19580,19581,19582,19583,19584,19585,19586,19587,19588,19589,19590,19591,19592,19593,19594,19595,19596,19597,19598,19599,19600,19601,19602,19603,19604,19605,19606,19607,19608,19609,19610,19611,19612,19613,19614,19615,19616,19617,19618,19619,19620,19621,19622,19623,19624,19625,19626,19627,19628,19629,19630,19631,19632,19633,19634,19635,19636,19637,19638,19639,19640,19641,19642,19643,19644,19645,19646,19647,19648,19649,19650,19651,19652,19653,19654,19655,19656,19657,19658,19659,19660,19661,19662,19663,19664,19665,19666,19667,19668,19669,19670,19671,19672,19673,19674,19675,19676,19677,19678,19679,19680,19681,19682,19683,19684,19685,19686,19687,19688,19689,19690,19691,19692,19693,19694,19695,19696,19697,19698,19699,19700,19701,19702,19703,19704,19705,19707,19708,19709,19710,19711,19712,19713,19714,19715,19716,19717,19718,19719,19720,19721,19722,19723,19724,19725,19726,19727,19728,19729,19730,19731,19732,19733,19734,19735,19736,19737,19738,19739,19740,19741,19742,19743,19744,19745,19746,19747,19748,19749,19750,19751,19752,19753,19754,19755,19756,19757,19758,19759,19760,19761,19762,19763,19764,19765,19766,19767,19768,19769,19770,19771,19772,19773,19774,19775,19776,19777,19778,19779,19780,19781,19782,19783,19784,19785,19786,19787,19788,19789,19790,19791,19792,19793,19794,19795,19796,19797,19798,19799,19800,19801,19802,19803,19804,19805,19806,19807,19808,19809,19810,19811,19812,19813,19814,19815,19816,19817,19818,19819,19820,19821,19822,19823,19824,19825,19826,19827,19828,19829,19830,19831,19832,19833,19834,19835,19836,19837,19838,19839,19840,19841,19842,19843,19844,19845,19846,19847,19848,19849,19850,19851,19852,19853,19854,19855,19856,19857,19858,19859,19860,19861,19862,19863,19864,19865,19866,19867,19868,19869,19870,19871,19872,19873,19874,19875],{"category":19233},"Frontend",{"category":1532},{"category":1150},{"category":557},{"category":858},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1150},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":571},{"category":557},{"category":557},{"category":571},{"category":557},{"category":557},{"category":9102},{"category":9102},{"category":858},{"category":858},{"category":1532},{"category":9102},{"category":1532},{"category":571},{"category":9102},{"category":557},{"category":858},{"category":19283},"DevOps",{"category":1150},{"category":1532},{"category":557},{"category":571},{"category":557},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":557},{"category":571},{"category":557},{"category":557},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":19283},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":557},{"category":19316},"Career",{"category":1150},{"category":1150},{"category":858},{"category":571},{"category":858},{"category":557},{"category":557},{"category":858},{"category":557},{"category":571},{"category":557},{"category":19283},{"category":19283},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1150},{"category":571},{"category":858},{"category":19283},{"category":19283},{"category":19283},{"category":1532},{"category":557},{"category":557},{"category":1532},{"category":19233},{"category":1150},{"category":19283},{"category":19283},{"category":9102},{"category":19283},{"category":858},{"category":1150},{"category":1532},{"category":557},{"category":1532},{"category":571},{"category":1532},{"category":571},{"category":9102},{"category":1532},{"category":1532},{"category":557},{"category":858},{"category":557},{"category":19233},{"category":557},{"category":557},{"category":557},{"category":557},{"category":858},{"category":858},{"category":1532},{"category":19233},{"category":9102},{"category":571},{"category":9102},{"category":19233},{"category":557},{"category":557},{"category":19283},{"category":557},{"category":557},{"category":571},{"category":557},{"category":19283},{"category":557},{"category":557},{"category":1532},{"category":1532},{"category":9102},{"category":571},{"category":571},{"category":19316},{"category":19316},{"category":19316},{"category":858},{"category":557},{"category":19283},{"category":571},{"category":1532},{"category":1532},{"category":19283},{"category":571},{"category":571},{"category":19233},{"category":557},{"category":1532},{"category":1532},{"category":557},{"category":1532},{"category":19283},{"category":19283},{"category":1532},{"category":9102},{"category":1532},{"category":571},{"category":9102},{"category":571},{"category":557},{"category":571},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":571},{"category":557},{"category":557},{"category":9102},{"category":557},{"category":19283},{"category":19283},{"category":858},{"category":557},{"category":557},{"category":557},{"category":571},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":571},{"category":571},{"category":571},{"category":557},{"category":1532},{"category":1532},{"category":1532},{"category":19283},{"category":858},{"category":1532},{"category":1532},{"category":557},{"category":1532},{"category":557},{"category":19233},{"category":1532},{"category":858},{"category":858},{"category":557},{"category":557},{"category":1150},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":557},{"category":19283},{"category":19283},{"category":19283},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":1532},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":858},{"category":858},{"category":1532},{"category":557},{"category":19233},{"category":571},{"category":19316},{"category":1532},{"category":1532},{"category":9102},{"category":557},{"category":1532},{"category":1532},{"category":19283},{"category":1532},{"category":19233},{"category":19283},{"category":19283},{"category":9102},{"category":557},{"category":557},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":19316},{"category":1532},{"category":571},{"category":557},{"category":557},{"category":1532},{"category":19283},{"category":1532},{"category":1532},{"category":1532},{"category":19233},{"category":1532},{"category":1532},{"category":557},{"category":1532},{"category":557},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":1150},{"category":1150},{"category":557},{"category":1532},{"category":19283},{"category":19283},{"category":1532},{"category":557},{"category":1532},{"category":1532},{"category":1150},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":557},{"category":9102},{"category":557},{"category":557},{"category":19233},{"category":557},{"category":19233},{"category":19233},{"category":9102},{"category":571},{"category":557},{"category":571},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":557},{"category":858},{"category":557},{"category":557},{"category":1532},{"category":571},{"category":1150},{"category":1150},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":858},{"category":557},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":19233},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":557},{"category":571},{"category":557},{"category":557},{"category":557},{"category":571},{"category":1532},{"category":858},{"category":1150},{"category":1532},{"category":858},{"category":9102},{"category":1532},{"category":9102},{"category":557},{"category":19283},{"category":1532},{"category":1532},{"category":557},{"category":1532},{"category":571},{"category":1532},{"category":1532},{"category":557},{"category":858},{"category":557},{"category":557},{"category":557},{"category":557},{"category":858},{"category":557},{"category":557},{"category":858},{"category":19283},{"category":557},{"category":1150},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":1532},{"category":1532},{"category":1532},{"category":1150},{"category":557},{"category":557},{"category":571},{"category":19233},{"category":557},{"category":1532},{"category":557},{"category":571},{"category":858},{"category":858},{"category":19233},{"category":19233},{"category":1532},{"category":858},{"category":9102},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":557},{"category":557},{"category":571},{"category":557},{"category":557},{"category":557},{"category":19706},"Programming",{"category":557},{"category":557},{"category":571},{"category":571},{"category":557},{"category":557},{"category":858},{"category":9102},{"category":557},{"category":858},{"category":557},{"category":557},{"category":557},{"category":557},{"category":19283},{"category":571},{"category":858},{"category":858},{"category":557},{"category":557},{"category":858},{"category":557},{"category":9102},{"category":858},{"category":557},{"category":557},{"category":571},{"category":571},{"category":1532},{"category":858},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":19233},{"category":1532},{"category":19283},{"category":9102},{"category":9102},{"category":9102},{"category":9102},{"category":9102},{"category":9102},{"category":1532},{"category":557},{"category":19283},{"category":571},{"category":19283},{"category":571},{"category":557},{"category":19233},{"category":1532},{"category":571},{"category":19233},{"category":1532},{"category":1532},{"category":1532},{"category":571},{"category":571},{"category":571},{"category":858},{"category":858},{"category":858},{"category":571},{"category":571},{"category":858},{"category":858},{"category":858},{"category":1532},{"category":9102},{"category":557},{"category":19283},{"category":557},{"category":1532},{"category":858},{"category":858},{"category":1532},{"category":1532},{"category":571},{"category":557},{"category":571},{"category":571},{"category":571},{"category":19233},{"category":557},{"category":1532},{"category":1532},{"category":858},{"category":858},{"category":571},{"category":557},{"category":19316},{"category":571},{"category":19316},{"category":858},{"category":1532},{"category":571},{"category":1532},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":1532},{"category":1150},{"category":1150},{"category":19283},{"category":1532},{"category":1532},{"category":1532},{"category":1532},{"category":557},{"category":557},{"category":19233},{"category":557},{"category":9102},{"category":571},{"category":19233},{"category":19233},{"category":557},{"category":557},{"category":19233},{"category":19233},{"category":19233},{"category":9102},{"category":557},{"category":557},{"category":858},{"category":557},{"category":571},{"category":1532},{"category":1532},{"category":571},{"category":1532},{"category":1532},{"category":571},{"category":1532},{"category":557},{"category":1532},{"category":9102},{"category":1532},{"category":1532},{"category":1532},{"category":19283},{"category":19283},{"category":9102},1772951194529]