[{"data":1,"prerenderedAt":4172},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-23":4,"blog-paginated-cats":3528},640,[5,156,320,519,732,911,1059,1173,1297,1445,1569,1665,2891,3104,3339],{"id":6,"title":7,"author":8,"body":11,"category":137,"date":138,"description":139,"extension":140,"featured":141,"image":142,"keywords":143,"meta":146,"navigation":147,"path":125,"readTime":148,"seo":149,"stem":150,"tags":151,"__hash__":155},"blog/blog/api-documentation-guide.md","API Documentation That Developers Love",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":128},"minimark",[14,19,23,26,29,32,36,39,46,52,58,60,64,67,79,86,93,95,99,102,105,108,111,120],[15,16,18],"h2",{"id":17},"the-cost-of-bad-api-documentation","The Cost of Bad API Documentation",[20,21,22],"p",{},"Every API with poor documentation generates a hidden tax. Developers integrating with your API spend hours reading source code, experimenting with endpoints, and asking questions in support channels that could be answered by a well-written docs page. Multiply that by every developer who integrates with your API, and the cumulative cost is staggering.",[20,24,25],{},"Stripe is the canonical example of documentation done right, and it's not a coincidence that they're one of the most widely adopted payment APIs. Developers choose tools they can learn quickly and use confidently. When two APIs offer similar functionality, the one with better documentation wins almost every time — because documentation quality is a proxy for engineering quality in the developer's mind.",[20,27,28],{},"The good news is that writing excellent API documentation doesn't require a technical writing team or expensive tooling. It requires understanding what developers need at each stage of their integration journey and providing exactly that.",[30,31],"hr",{},[15,33,35],{"id":34},"the-three-layers-of-api-documentation","The Three Layers of API Documentation",[20,37,38],{},"Effective API documentation serves three distinct needs, and most documentation fails because it tries to serve them all with a single format.",[20,40,41,45],{},[42,43,44],"strong",{},"Getting started guides"," are for developers who have just decided to use your API and need to make their first successful request. This layer should be ruthlessly focused on time-to-first-success. Show them how to authenticate, make a basic request, and see a response — in under five minutes. Every concept that isn't essential to that first request belongs in a later layer. Include a complete, runnable code example. Not a pseudocode snippet, not a curl command with placeholder values — a real example they can copy, paste, and run.",[20,47,48,51],{},[42,49,50],{},"Conceptual guides"," explain the mental model behind your API. How do resources relate to each other? What's the lifecycle of an order, a subscription, or a webhook event? What are the common workflows? This layer answers \"how should I think about this?\" rather than \"what does this endpoint accept?\" Developers who understand the conceptual model make fewer mistakes, ask fewer support questions, and build more solid integrations.",[20,53,54,57],{},[42,55,56],{},"Reference documentation"," is the comprehensive, endpoint-by-endpoint specification. Every endpoint, every parameter, every response field, every error code. This is the layer most people think of when they hear \"API documentation,\" and it's the easiest to generate from code annotations. But reference docs without the other two layers are like a dictionary without a grammar guide — technically complete but practically insufficient.",[30,59],{},[15,61,63],{"id":62},"writing-effective-reference-documentation","Writing Effective Reference Documentation",[20,65,66],{},"Each endpoint's reference entry should follow a consistent structure: a one-sentence description of what the endpoint does, the HTTP method and path, authentication requirements, request parameters with types and descriptions, a complete request example, a complete response example, and a list of possible error responses.",[20,68,69,70,74,75,78],{},"The most common mistake in reference documentation is omitting realistic examples. A parameter described as ",[71,72,73],"code",{},"status (string)"," tells the developer almost nothing. A parameter described as ",[71,76,77],{},"status (string) — Filter by order status. Possible values: pending, processing, shipped, delivered, cancelled"," tells them everything they need. Be specific about allowed values, formats, and constraints.",[20,80,81,82,85],{},"Document your error responses as thoroughly as your success responses. Developers spend more time debugging errors than celebrating successes, and the quality of your error documentation directly determines how quickly they recover from mistakes. For each error code, explain what triggered it and how to fix it. \"400 Bad Request\" is useless. \"400: The ",[71,83,84],{},"email"," field must be a valid email address\" is actionable.",[20,87,88,89,92],{},"Include response field descriptions, not just example values. An example response showing ",[71,90,91],{},"\"type\": \"premium\""," doesn't tell the developer whether \"premium\" is one of two options or one of twenty. Describe every field with its type, possible values, and any conditional logic that determines its presence.",[30,94],{},[15,96,98],{"id":97},"keeping-documentation-accurate","Keeping Documentation Accurate",[20,100,101],{},"The most dangerous documentation is documentation that's almost right. A developer who follows slightly outdated docs will build an integration that mostly works but fails in subtle ways — which are harder to debug than complete failures because the developer trusts the documentation.",[20,103,104],{},"Generate reference documentation from code when possible. OpenAPI specifications, GraphQL schema introspection, and similar tools ensure that the documentation reflects the actual API behavior. Manual documentation inevitably drifts from reality.",[20,106,107],{},"Include documentation updates in your definition of done for API changes. A pull request that modifies an endpoint without updating the corresponding documentation should not pass code review. This is a cultural norm that needs to be established explicitly — it won't happen on its own.",[20,109,110],{},"Test your documentation. Not just proofreading — actually run the code examples and verify that they produce the described output. Automated documentation testing, where example requests are executed against a test environment as part of CI, is the gold standard. The effort to set this up pays for itself quickly in reduced support burden.",[20,112,113,114,119],{},"Version your documentation alongside your API. When developers reference your docs, they need to see the documentation for the API version they're using, not the latest version that may have changed parameters or behavior. This is straightforward with ",[115,116,118],"a",{"href":117},"/blog/software-documentation-best-practices","good documentation infrastructure"," but requires planning from the start.",[20,121,122,123,127],{},"The best API documentation I've worked with shares a common quality: it respects the developer's time. Every page answers a specific question, every example works when copied, every error is explained with a resolution path. Achieving this level of quality requires the same engineering discipline you apply to the API itself — because to the developers who use your API, ",[115,124,126],{"href":125},"/blog/api-documentation-guide","the documentation is the product",".",{"title":129,"searchDepth":130,"depth":130,"links":131},"",3,[132,134,135,136],{"id":17,"depth":133,"text":18},2,{"id":34,"depth":133,"text":35},{"id":62,"depth":133,"text":63},{"id":97,"depth":133,"text":98},"Engineering","2025-12-18","How to write API documentation that developers actually want to read. Practical patterns for reference docs, guides, and examples that reduce support burden.","md",false,null,[144,145],"API documentation best practices","writing API docs",{},true,7,{"title":7,"description":139},"blog/api-documentation-guide",[152,153,154],"API Documentation","Developer Experience","Technical Writing","yAAfdK_mt6-G3olzVfn-Qse2zvphATyeRSkqnZD_yKs",{"id":157,"title":158,"author":159,"body":160,"category":302,"date":138,"description":303,"extension":140,"featured":141,"image":142,"keywords":304,"meta":310,"navigation":147,"path":311,"readTime":148,"seo":312,"stem":313,"tags":314,"__hash__":319},"blog/blog/balnagown-castle-ross-clan.md","Balnagown Castle: Seat of the Clan Ross Chiefs",{"name":9,"bio":10},{"type":12,"value":161,"toc":293},[162,166,174,177,181,194,197,200,204,207,210,213,217,220,228,231,235,238,246,250,253,256,264,267,269,273],[15,163,165],{"id":164},"the-castle-in-easter-ross","The Castle in Easter Ross",[20,167,168,169,173],{},"On the southern shore of the Cromarty Firth, where the flat farmland of Easter Ross gives way to gently rising ground, stands Balnagown Castle -- the ancestral seat of the chiefs of Clan Ross for over four hundred years. The castle's name comes from the Gaelic ",[170,171,172],"em",{},"Baile na Gobhainn",", \"settlement of the smith,\" a name that predates the castle itself and hints at a community that was already old when the first stone walls were raised.",[20,175,176],{},"Balnagown is not one of Scotland's famous showpiece castles. It lacks the dramatic clifftop setting of Dunnottar or the picturesque island position of Eilean Donan. But for the history of Clan Ross and the broader story of the Highland clans, Balnagown matters more than most -- because it was the physical anchor of the Ross identity for centuries, and its loss to the family was a turning point in the clan's history.",[15,178,180],{"id":179},"the-early-castle","The Early Castle",[20,182,183,184,188,189,193],{},"The earliest castle at Balnagown was likely built in the fourteenth century, during the period when the ",[115,185,187],{"href":186},"/blog/earls-of-ross-medieval","Earls of Ross"," held the earldom and the Clan Ross chiefs were consolidating their hold on the territory of ",[115,190,192],{"href":191},"/blog/ross-shire-geography-history","Ross-shire",". The original structure was a tower house -- the standard form of Highland chief's residence, combining domestic accommodation with defensive capability.",[20,195,196],{},"The tower house at Balnagown would have been a relatively modest structure by the standards of the great Scottish earldoms, but it served its purpose: a visible statement of territorial authority, a defensible residence for the chief and his immediate household, and a focal point for the clan's political and social life.",[20,198,199],{},"The castle was expanded and modified repeatedly over the centuries. By the sixteenth and seventeenth centuries, Balnagown had grown from a simple tower house into a more substantial complex, with additional wings, domestic buildings, and the agricultural infrastructure of a working estate.",[15,201,203],{"id":202},"the-ross-chiefs-at-balnagown","The Ross Chiefs at Balnagown",[20,205,206],{},"The chiefs of Clan Ross held Balnagown from the medieval period until 1672 -- a tenure of approximately four centuries. During this time, the castle was the center of Ross clan governance, the place where the chief administered justice, received rents, hosted allies, and planned military campaigns.",[20,208,209],{},"The relationship between the Ross chiefs and their Balnagown seat was not always peaceful. The clan experienced internal disputes, contested successions, and the general turbulence of Highland politics. The Rosses fought with neighboring clans -- particularly the Mackays and Mackenzies -- and the castle periodically served its defensive function.",[20,211,212],{},"One of the most significant chiefs associated with Balnagown was Alexander Ross, the last Ross chief to hold the castle in a period of relative prosperity. By the seventeenth century, however, the Ross chiefs -- like many Highland families -- were burdened with debt and facing the economic pressures of a changing Scotland.",[15,214,216],{"id":215},"the-loss-of-balnagown","The Loss of Balnagown",[20,218,219],{},"In 1672, the Balnagown estate passed out of direct Ross family control -- a loss that represented one of the most significant ruptures in the clan's history. The debts accumulated by successive chiefs, combined with the broader economic difficulties facing Highland landowners in the post-Civil War period, made the estate financially untenable.",[20,221,222,223,227],{},"The sale of Balnagown severed the physical connection between the Ross chiefs and the territorial base that had defined their identity for four centuries. Unlike some Highland clans that maintained their ancestral seats through the modern period, the Rosses lost theirs before the most traumatic chapters of Highland history -- the Jacobite risings and the ",[115,224,226],{"href":225},"/blog/highland-clearances-clan-ross-diaspora","Highland Clearances"," -- had even begun.",[20,229,230],{},"Subsequent owners of Balnagown included various non-Ross families who modified and expanded the castle according to the fashions of their respective eras. The estate changed hands multiple times over the following centuries.",[15,232,234],{"id":233},"the-castle-today","The Castle Today",[20,236,237],{},"Balnagown Castle still stands in Easter Ross, now a private residence that has been through several restorations. The structure visible today is a composite of medieval, early modern, and Victorian elements -- a palimpsest of architectural styles reflecting six centuries of continuous occupation and modification.",[20,239,240,241,245],{},"The castle is not generally open to the public, though it has occasionally been available for private hire. For members of the Ross clan diaspora visiting the Highlands, Balnagown remains a site of genealogical and emotional significance -- the place where the clan's chiefs lived and governed during the centuries when the ",[115,242,244],{"href":243},"/blog/scottish-clan-system-explained","clan system"," was the organizing structure of Highland society.",[15,247,249],{"id":248},"balnagown-and-clan-identity","Balnagown and Clan Identity",[20,251,252],{},"The loss of Balnagown in 1672 had consequences that extended far beyond the real estate transaction. In the Highland clan system, the chief's seat was not merely a residence -- it was the symbolic center of the clan's identity. The castle was where the clan gathered, where disputes were settled, where the chief's authority was visibly exercised.",[20,254,255],{},"When the Rosses lost Balnagown, they lost the physical anchor of that identity. The chiefs continued to hold their position through clan tradition, but without the territorial base that gave the chieftainship its material reality. By the time of the Clearances, the disjunction between the Ross chiefs and the Ross lands was already a century and a half old.",[20,257,258,259,263],{},"For the ",[115,260,262],{"href":261},"/blog/clan-ross-in-america","Ross diaspora"," -- the descendants of families cleared from Ross-shire in the nineteenth century -- Balnagown represents both a connection and a disconnection. It is the ancestral seat, the place the name comes from, the castle the chiefs built. But it is also the castle the family lost, centuries before the people themselves were lost to emigration and displacement.",[20,265,266],{},"The stones still stand in Easter Ross, bearing the memory of the smiths who named the place and the chiefs who built upon it.",[30,268],{},[15,270,272],{"id":271},"related-articles","Related Articles",[274,275,276,282,287],"ul",{},[277,278,279],"li",{},[115,280,281],{"href":191},"Ross-shire: The Land That Shaped a Clan",[277,283,284],{},[115,285,286],{"href":186},"The Earls of Ross: Power and Politics in Medieval Scotland",[277,288,289],{},[115,290,292],{"href":291},"/blog/ross-surname-origin-meaning","The Ross Surname: Scottish Origins, Meaning, and Where the Name Came From",{"title":129,"searchDepth":130,"depth":130,"links":294},[295,296,297,298,299,300,301],{"id":164,"depth":133,"text":165},{"id":179,"depth":133,"text":180},{"id":202,"depth":133,"text":203},{"id":215,"depth":133,"text":216},{"id":233,"depth":133,"text":234},{"id":248,"depth":133,"text":249},{"id":271,"depth":133,"text":272},"Heritage","Balnagown Castle in Easter Ross was the ancestral seat of the Clan Ross chiefs for over four centuries. Here is the history of the castle, the family who built it, and what happened when they lost it.",[305,306,307,308,309],"balnagown castle","balnagown castle history","clan ross castle","ross clan seat","balnagown ross-shire",{},"/blog/balnagown-castle-ross-clan",{"title":158,"description":303},"blog/balnagown-castle-ross-clan",[315,316,317,192,318],"Balnagown Castle","Clan Ross","Scottish Castles","Scottish History","cGateDB4P6lyUALEzclEoilgI7xd38b98Azacaddi_w",{"id":321,"title":322,"author":323,"body":324,"category":137,"date":138,"description":504,"extension":140,"featured":141,"image":142,"keywords":505,"meta":509,"navigation":147,"path":510,"readTime":148,"seo":511,"stem":512,"tags":513,"__hash__":518},"blog/blog/erp-reporting-best-practices.md","ERP Reporting: Building Reports That Actually Drive Decisions",{"name":9,"bio":10},{"type":12,"value":325,"toc":497},[326,330,333,336,339,341,345,348,354,357,360,366,369,377,379,383,386,392,398,404,410,421,423,427,433,439,445,451,457,466,468,472],[15,327,329],{"id":328},"the-reporting-problem-in-erp-systems","The Reporting Problem in ERP Systems",[20,331,332],{},"ERP systems generate enormous amounts of data. Every transaction, every status change, every approval is recorded. The problem isn't data availability — it's that most ERP reporting surfaces this data in ways that don't help people make decisions.",[20,334,335],{},"The typical ERP report is a tabular data dump: 47 columns, 10,000 rows, exported to Excel. The user who requested it knows what they're looking for and will spend 30 minutes filtering, pivoting, and formatting the data to extract the insight they need. Tomorrow they'll do it again with slightly different parameters.",[20,337,338],{},"Effective ERP reporting starts from the other direction: what decision does this report support? A production manager needs to know which orders are at risk of missing their delivery date. A CFO needs to know whether receivables are aging faster than last quarter. A warehouse manager needs to know which items are approaching reorder points. Each of these is a specific question with a specific answer, not a generic data dump.",[30,340],{},[15,342,344],{"id":343},"report-architecture-operational-vs-analytical","Report Architecture: Operational vs. Analytical",[20,346,347],{},"ERP reporting serves two fundamentally different purposes that require different architectures.",[20,349,350,353],{},[42,351,352],{},"Operational reports"," answer questions about what's happening now. What orders are open? Which invoices are overdue? What's the current inventory level for this product? These reports query transactional data in real-time or near-real-time and are used by front-line workers and managers to make immediate decisions.",[20,355,356],{},"Operational reports should query the ERP's transactional database — or, if query performance is a concern, a read replica. The data needs to be current because the decisions are immediate: a warehouse worker looking at a pick list needs to see the orders that exist right now, not an hour ago.",[20,358,359],{},"The performance trap with operational reports is query complexity. A report that joins six tables with aggregate functions across millions of rows will lock the database and degrade the application's transactional performance. Either use a read replica to isolate reporting queries from transactional workload, or pre-compute frequently accessed aggregations with materialized views that refresh on a schedule.",[20,361,362,365],{},[42,363,364],{},"Analytical reports"," answer questions about trends, patterns, and performance over time. How have sales trended this quarter compared to last year? Which products have the highest return rates? Which vendors consistently deliver late? These reports look at historical data and derive insights that inform strategy.",[20,367,368],{},"Analytical reports should query a data warehouse or a dedicated reporting schema, not the transactional database. The warehouse stores historical data in a structure optimized for analytical queries — star schemas or wide denormalized tables — that performs well for the aggregation, grouping, and time-series analysis that analytical queries require.",[20,370,371,372,376],{},"The pipeline that moves data from the ERP's transactional database to the reporting warehouse is a ",[115,373,375],{"href":374},"/blog/enterprise-data-pipeline","data pipeline"," concern, and getting it right is essential for trustworthy analytics.",[30,378],{},[15,380,382],{"id":381},"building-reports-that-people-actually-use","Building Reports That People Actually Use",[20,384,385],{},"A report that goes unused is waste. Here are the patterns I've seen that separate useful reports from shelfware.",[20,387,388,391],{},[42,389,390],{},"Start with the decision, not the data."," Talk to the person who will use the report. What question are they trying to answer? What action will they take based on the answer? Design the report to answer that specific question clearly. A well-designed exception report — \"these 12 orders are at risk\" — is more valuable than a comprehensive report showing all 500 orders with their statuses.",[20,393,394,397],{},[42,395,396],{},"Exception-based reporting"," highlights the items that need attention rather than showing everything. Instead of a report showing all 2,000 invoices, show the 47 invoices that are more than 30 days past due, sorted by amount. The report becomes a to-do list rather than a data exploration exercise.",[20,399,400,403],{},[42,401,402],{},"Scheduled delivery"," pushes reports to users rather than requiring them to pull. A daily email with the key operational metrics and exception counts — sent at 7 AM so it's waiting when the manager starts their day — is more useful than a dashboard they have to remember to check. For the details behind the summary numbers, link back to the full report in the application.",[20,405,406,409],{},[42,407,408],{},"Drill-down capability"," connects summary numbers to underlying detail. When the CFO sees that receivables have increased 15% this month, they should be able to click through to see which customers and which invoices are driving the increase. This requires that reports are linked — the summary report contains references to the detail report, parameterized for the specific segment the user clicked.",[20,411,412,415,416,420],{},[42,413,414],{},"Self-service for power users."," Some users need the ability to create their own reports or modify existing ones. A report builder that lets users select fields, filters, groupings, and sort orders — constrained to the data they have permission to see — reduces the backlog of reporting requests to the development team. This is effectively a ",[115,417,419],{"href":418},"/blog/enterprise-form-builder","form builder"," for queries, and many of the same architectural patterns apply.",[30,422],{},[15,424,426],{"id":425},"technical-implementation-patterns","Technical Implementation Patterns",[20,428,429,432],{},[42,430,431],{},"Parameterized queries"," with user-supplied filters should always use parameterized SQL to prevent injection. This sounds obvious but ERP reporting is one of the areas where ad-hoc query construction is most common because users need flexible filtering. Every user-supplied value goes through a parameter, never string concatenation.",[20,434,435,438],{},[42,436,437],{},"Query performance budgets"," prevent reports from monopolizing database resources. Set a maximum query execution time — 30 seconds for interactive reports, 5 minutes for scheduled reports — and kill queries that exceed it. A report that takes 10 minutes to generate needs optimization, not patience.",[20,440,441,444],{},[42,442,443],{},"Caching report results"," is valuable for reports that are expensive to generate and don't need to be real-time. A financial summary report that runs for 2 minutes can be cached and served to multiple users without re-executing the query. Invalidate the cache when the underlying data changes or on a schedule.",[20,446,447,450],{},[42,448,449],{},"Export formats"," matter for enterprise users. PDF for formatted printable reports. Excel for data that users need to further analyze. CSV for data that feeds into other systems. The export should match the report's visual formatting — column headers, groupings, subtotals — not dump raw query results.",[20,452,453,456],{},[42,454,455],{},"Access control"," for reports must respect the ERP's data permissions. A regional manager should only see data for their region, even in a report that technically queries all regions. Row-level security applied at the query level ensures that reports automatically respect the user's data access scope.",[20,458,459,460],{},"If you're building reporting for your ERP system, ",[115,461,465],{"href":462,"rel":463},"https://calendly.com/jamesrossjr",[464],"nofollow","let's discuss the architecture that will serve your organization best.",[30,467],{},[15,469,471],{"id":470},"keep-reading","Keep Reading",[274,473,474,480,485,491],{},[277,475,476],{},[115,477,479],{"href":478},"/blog/custom-erp-development-guide","Custom ERP Development: What It Actually Takes",[277,481,482],{},[115,483,484],{"href":374},"Enterprise Data Pipeline Architecture",[277,486,487],{},[115,488,490],{"href":489},"/blog/database-indexing-strategies","Database Indexing Strategies That Actually Improve Performance",[277,492,493],{},[115,494,496],{"href":495},"/blog/erp-data-analytics","ERP Data Analytics: Turning Transactions Into Insights",{"title":129,"searchDepth":130,"depth":130,"links":498},[499,500,501,502,503],{"id":328,"depth":133,"text":329},{"id":343,"depth":133,"text":344},{"id":381,"depth":133,"text":382},{"id":425,"depth":133,"text":426},{"id":470,"depth":133,"text":471},"Most ERP reports are data dumps that nobody reads. Here's how to build reporting that surfaces the information decision-makers actually need, when they need it.",[506,507,508],"ERP reporting best practices","enterprise reporting architecture","business intelligence ERP",{},"/blog/erp-reporting-best-practices",{"title":322,"description":504},"blog/erp-reporting-best-practices",[514,515,516,517],"ERP","Reporting","Analytics","Enterprise Software","02Ts7Q6xTQ0vBN33VAaLBmADgUGQXg4hdGoDaXHGDtk",{"id":520,"title":521,"author":522,"body":523,"category":719,"date":138,"description":720,"extension":140,"featured":141,"image":142,"keywords":721,"meta":724,"navigation":147,"path":725,"readTime":148,"seo":726,"stem":727,"tags":728,"__hash__":731},"blog/blog/saas-tenant-isolation.md","Tenant Isolation in SaaS: Security and Performance",{"name":9,"bio":10},{"type":12,"value":524,"toc":711},[525,529,532,535,542,544,548,551,564,567,573,579,582,584,588,591,597,603,609,615,618,620,624,627,633,639,645,651,659,661,665,668,674,680,686,689,691,693],[15,526,528],{"id":527},"isolation-is-the-foundation-of-trust","Isolation Is the Foundation of Trust",[20,530,531],{},"In a multi-tenant SaaS application, every customer trusts that their data is invisible to every other customer. They trust that one tenant's heavy workload doesn't degrade their experience. They trust that a security vulnerability exploited in one tenant's context doesn't expose their data.",[20,533,534],{},"Tenant isolation is the set of architectural decisions that makes those trust assumptions real. It operates at multiple layers — data, compute, network, and application — and the level of isolation at each layer involves tradeoffs between security, performance, cost, and operational complexity.",[20,536,537,538,127],{},"Getting isolation wrong has consequences that range from embarrassing (one tenant sees another's data in a UI glitch) to catastrophic (a data breach exposing all tenants simultaneously). The architecture decisions you make here are among the most consequential in a ",[115,539,541],{"href":540},"/blog/multi-tenant-architecture","multi-tenant system",[30,543],{},[15,545,547],{"id":546},"data-isolation-patterns","Data Isolation Patterns",[20,549,550],{},"Data isolation is the most critical dimension. A failure here is a data breach, full stop.",[20,552,553,556,557,560,561,563],{},[42,554,555],{},"Row-level isolation"," stores all tenants' data in shared tables with a ",[71,558,559],{},"tenant_id"," column on every row. Queries filter by ",[71,562,559],{}," to ensure each tenant only sees their own data. This is operationally simple but relies on every query including the tenant filter. A single missed filter in a single query exposes data across tenants.",[20,565,566],{},"The mitigation is PostgreSQL's Row-Level Security (RLS). RLS policies enforce tenant filtering at the database level, regardless of what the application query does. You set the tenant context on the database session, and RLS ensures that every query — including ad hoc queries from admin tools — respects tenant boundaries. This converts a class of application bugs from \"data exposure\" to \"empty result set,\" which is a dramatically better failure mode.",[20,568,569,572],{},[42,570,571],{},"Schema-per-tenant"," gives each tenant their own database schema within a shared database. Data isolation is enforced by the schema boundary — a query in one schema physically cannot access tables in another. Migrations are more complex because they must be applied to every schema, but data isolation is structural rather than policy-based.",[20,574,575,578],{},[42,576,577],{},"Database-per-tenant"," provides the strongest isolation. Each tenant has a completely separate database instance. There's no mechanism by which a query in one tenant's database can access another tenant's data, even if the application has a bug. The cost is operational complexity — managing connections, running migrations, and monitoring performance across potentially hundreds of databases.",[20,580,581],{},"The right choice depends on your customer profile. B2C SaaS with thousands of small tenants typically uses row-level isolation with RLS. B2B SaaS with dozens of enterprise tenants who have strict compliance requirements often uses schema-per-tenant or database-per-tenant.",[30,583],{},[15,585,587],{"id":586},"compute-and-performance-isolation","Compute and Performance Isolation",[20,589,590],{},"Data isolation prevents cross-tenant data access. Compute isolation prevents cross-tenant performance interference — the \"noisy neighbor\" problem.",[20,592,593,596],{},[42,594,595],{},"Shared compute"," is the default in most SaaS architectures. All tenants share the same application servers and database. This is cost-efficient but means a single tenant running an expensive report or triggering a bulk import can degrade performance for everyone.",[20,598,599,602],{},[42,600,601],{},"Resource limits"," are the minimum viable compute isolation. Rate limiting per tenant, query timeout limits, and background job queue prioritization prevent any single tenant from consuming disproportionate resources. These don't provide true isolation — they limit the blast radius of resource consumption.",[20,604,605,608],{},[42,606,607],{},"Compute partitioning"," assigns dedicated resources to specific tenants. An enterprise tenant might get their own application server pool or their own database read replica. This provides genuine performance isolation but increases infrastructure cost and operational complexity.",[20,610,611,614],{},[42,612,613],{},"Queue isolation"," ensures that one tenant's bulk operations don't block another tenant's time-sensitive jobs. Use separate queues or queue priorities for different tenants, or at minimum separate queues for different job types so that a bulk data import doesn't delay email delivery.",[20,616,617],{},"The practical approach is to start with shared compute and resource limits, then offer dedicated resources as a premium tier for enterprise customers who need performance guarantees. This aligns cost with revenue — the customers who need isolation are the ones paying enough to fund the additional infrastructure.",[30,619],{},[15,621,623],{"id":622},"application-level-isolation","Application-Level Isolation",[20,625,626],{},"Even with strong data and compute isolation, application-level concerns can leak between tenants.",[20,628,629,632],{},[42,630,631],{},"Session isolation"," ensures that a user authenticated in one tenant's context cannot access another tenant's resources. In applications where users can belong to multiple tenants (common in B2B SaaS), the session must track the current tenant context and enforce it on every request. Switching tenants should require an explicit action, not just changing a URL parameter.",[20,634,635,638],{},[42,636,637],{},"File storage isolation"," is frequently overlooked. If tenants upload files, those files must be stored with tenant-scoped access controls. A file URL that's guessable or sequential allows one tenant to access another's files. Use signed URLs with short expiration times, and verify tenant context when generating them.",[20,640,641,644],{},[42,642,643],{},"Cache isolation"," means cache keys must include the tenant identifier. A cache entry for \"dashboard_summary\" without a tenant prefix returns the wrong tenant's data to the next requester. This is a subtle bug that may not be caught in development (where there's typically only one tenant) and surfaces in production as a data exposure incident.",[20,646,647,650],{},[42,648,649],{},"Search index isolation"," applies if you're using a search engine like Elasticsearch. Queries must filter by tenant, and the index structure should support efficient tenant-scoped queries. A search query that returns results from the wrong tenant is functionally identical to a data breach.",[20,652,653,654,658],{},"For a deeper look at the ",[115,655,657],{"href":656},"/blog/saas-security-guide","security architecture"," that wraps around tenant isolation, including authentication, encryption, and network segmentation, my security guide covers the broader context.",[30,660],{},[15,662,664],{"id":663},"testing-isolation","Testing Isolation",[20,666,667],{},"Tenant isolation must be tested explicitly. It's not sufficient to test that features work correctly for a single tenant — you must test that features work correctly in the presence of multiple tenants and that no data leaks between them.",[20,669,670,673],{},[42,671,672],{},"Multi-tenant integration tests"," create two tenants, populate both with data, and verify that operations in one tenant's context never return or modify the other tenant's data. These tests should cover every data access path, including search, reporting, file access, and API endpoints.",[20,675,676,679],{},[42,677,678],{},"Penetration testing"," should specifically target tenant boundaries. Can a user in tenant A craft a request that accesses tenant B's data? Can they manipulate request parameters, cookies, or headers to switch tenant context? These tests should be part of your regular security assessment.",[20,681,682,685],{},[42,683,684],{},"Chaos testing"," for noisy neighbor scenarios validates compute isolation. Simulate heavy load from one tenant and verify that other tenants' performance remains within acceptable bounds.",[20,687,688],{},"Tenant isolation is not a feature you build once and forget. It's a property of your system that must be verified continuously as the codebase evolves. Every new feature, every new query, every new API endpoint is a potential isolation boundary violation if not designed with multi-tenancy in mind.",[30,690],{},[15,692,471],{"id":470},[274,694,695,700,705],{},[277,696,697],{},[115,698,699],{"href":540},"Multi-Tenant Architecture: Patterns for Building Software That Serves Many Clients",[277,701,702],{},[115,703,704],{"href":656},"SaaS Security Guide: Protecting Multi-Tenant Applications",[277,706,707],{},[115,708,710],{"href":709},"/blog/saas-compliance-soc2","SOC 2 Compliance for SaaS: What Developers Need to Know",{"title":129,"searchDepth":130,"depth":130,"links":712},[713,714,715,716,717,718],{"id":527,"depth":133,"text":528},{"id":546,"depth":133,"text":547},{"id":586,"depth":133,"text":587},{"id":622,"depth":133,"text":623},{"id":663,"depth":133,"text":664},{"id":470,"depth":133,"text":471},"Security","Tenant isolation determines whether a bug, a performance spike, or a security vulnerability in one tenant's environment can affect another. Here's how to get it right.",[722,723],"SaaS tenant isolation","multi-tenant security",{},"/blog/saas-tenant-isolation",{"title":521,"description":720},"blog/saas-tenant-isolation",[729,719,730],"SaaS","Multi-Tenancy","wjfdwUfsi0OdVij39f83LNfbzhS8CuMDCAnWQOcszBI",{"id":733,"title":734,"author":735,"body":736,"category":898,"date":138,"description":899,"extension":140,"featured":141,"image":142,"keywords":900,"meta":903,"navigation":147,"path":904,"readTime":148,"seo":905,"stem":906,"tags":907,"__hash__":910},"blog/blog/single-page-app-vs-multi-page.md","SPA vs MPA: Choosing the Right Rendering Strategy",{"name":9,"bio":10},{"type":12,"value":737,"toc":891},[738,742,745,748,751,754,762,764,768,771,777,783,793,796,798,802,805,811,821,832,835,837,841,844,847,850,853,861,863,867,870,876,882,888],[15,739,741],{"id":740},"the-rendering-strategy-shapes-everything","The Rendering Strategy Shapes Everything",[20,743,744],{},"When you decide between a single-page application (SPA) and a multi-page application (MPA), you are not choosing a technology — you are choosing how HTML reaches the browser, and that decision cascades into SEO capability, performance characteristics, infrastructure requirements, and user experience patterns.",[20,746,747],{},"An MPA generates HTML on the server for each page request. The browser navigates between pages with full page loads — the server sends complete HTML, and the browser replaces everything. This is how the web worked for its first 20 years, and it remains the default model for most server-rendered frameworks.",[20,749,750],{},"An SPA loads a single HTML shell and uses JavaScript to render all content client-side. After the initial load, navigation happens without full page reloads — JavaScript intercepts link clicks, fetches data from APIs, and updates the DOM. The result feels like a native application: instant navigation, smooth transitions, persistent UI state across views.",[20,752,753],{},"Both approaches are valid. The problem is not that either is wrong — it is that each creates tradeoffs that proponents tend to minimize. SPAs sacrifice initial load performance and SEO for smooth in-app navigation. MPAs sacrifice navigation fluidity for simpler infrastructure and guaranteed search engine compatibility.",[20,755,756,757,761],{},"The modern answer is usually neither pure SPA nor pure MPA, but a hybrid that renders the initial page on the server (MPA behavior) and handles subsequent navigation on the client (SPA behavior). Frameworks like ",[115,758,760],{"href":759},"/blog/nuxt-performance-optimization","Nuxt"," and Next.js implement this hybrid model by default.",[30,763],{},[15,765,767],{"id":766},"when-spas-make-sense","When SPAs Make Sense",[20,769,770],{},"SPAs are the right choice when the application lives behind authentication, when SEO is irrelevant, and when the user experience benefits from persistent state and instant navigation.",[20,772,773,776],{},[42,774,775],{},"Dashboards and admin panels."," Users log in and interact with data-dense interfaces for extended sessions. Navigation between views should be instantaneous because users switch between sections frequently. Loading a full page on every navigation would be noticeably slower and lose UI state (scroll position, filter selections, open panels).",[20,778,779,782],{},[42,780,781],{},"Collaborative tools."," Applications like document editors, project management tools, and design tools maintain complex client-side state that would be expensive to reconstruct on every page load. Real-time updates (other users' cursors, live edits, notifications) require persistent WebSocket connections that full page navigations would break.",[20,784,785,788,789,792],{},[42,786,787],{},"Internal business applications."," ",[115,790,791],{"href":478},"Custom ERP systems",", CRM interfaces, and workflow tools used by employees do not need SEO, do not need to support sharing via URL in most cases, and benefit from the desktop-application feel that SPAs provide.",[20,794,795],{},"The SPA tradeoffs you accept: a larger initial JavaScript payload (typically 200-500KB compressed for a React or Vue app), a blank page until JavaScript downloads and executes (mitigated by loading screens), no server-side HTML for search engines (irrelevant for authenticated apps), and the need for a separate API backend since the SPA is a static client application.",[30,797],{},[15,799,801],{"id":800},"when-mpas-make-sense","When MPAs Make Sense",[20,803,804],{},"MPAs are the right choice when content discoverability is essential, when initial load performance must be optimal, and when the application serves primarily as an information delivery system.",[20,806,807,810],{},[42,808,809],{},"Content websites."," Blogs, news sites, documentation, and marketing pages exist to be found through search engines and loaded directly via shared URLs. Each page must deliver complete HTML for search engine crawlers. Server-rendered MPAs guarantee this by generating HTML on the server before sending it to the browser.",[20,812,813,816,817,127],{},[42,814,815],{},"E-commerce."," Product pages need SEO visibility, fast initial loads (every 100ms of delay reduces conversion), and shareable URLs. An MPA that server-renders each product page ensures search engines index products properly and users get fast page loads regardless of device or JavaScript support. Dynamic features like cart management and checkout can layer on as ",[115,818,820],{"href":819},"/blog/e-commerce-web-development","client-side enhancements",[20,822,823,826,827,831],{},[42,824,825],{},"Landing pages and campaign sites."," Performance and SEO are the only priorities. JavaScript complexity is unwanted overhead. A server-rendered or statically generated MPA produces the ",[115,828,830],{"href":829},"/blog/landing-page-optimization","fastest possible page loads"," with the smallest possible bundle size.",[20,833,834],{},"The MPA tradeoffs: full page reloads between navigations (the browser discards everything and rebuilds), UI state is lost on navigation (form contents, scroll position, open menus), and the navigation experience feels less fluid than SPAs. These tradeoffs are acceptable when users typically consume one page in depth rather than navigating rapidly between many pages.",[30,836],{},[15,838,840],{"id":839},"the-hybrid-model","The Hybrid Model",[20,842,843],{},"Modern full-stack frameworks have largely resolved the SPA vs MPA debate by offering hybrid rendering. The initial page load is server-rendered (MPA behavior — complete HTML for SEO and fast initial paint), and subsequent navigations are handled client-side (SPA behavior — instant transitions without full page reloads).",[20,845,846],{},"This hybrid is called \"universal rendering\" or \"isomorphic rendering.\" The same component code runs on both server and client. The server renders HTML for the first request, ships JavaScript to the browser, and the framework \"hydrates\" the server-rendered HTML — attaching event listeners and enabling interactivity without re-rendering everything.",[20,848,849],{},"Nuxt implements this through its default rendering mode. A visitor's first page load gets server-rendered HTML (fast, SEO-friendly). Clicking a link triggers client-side navigation (instant, no page reload). The user gets MPA benefits on entry and SPA benefits on navigation.",[20,851,852],{},"The hybrid model introduces its own complexity. Hydration — the process of making server-rendered HTML interactive — has a performance cost. The browser must download the framework JavaScript, parse it, execute it, and reconcile it with the existing DOM. During hydration, the page may appear interactive but not respond to clicks (the \"uncanny valley\" of web performance). Frameworks are addressing this with techniques like selective hydration, islands architecture, and streaming SSR.",[20,854,855,856,860],{},"For ",[115,857,859],{"href":858},"/blog/choosing-right-web-framework","choosing the right framework",", evaluate whether you need the hybrid model and how well each framework implements it. If your application is entirely behind authentication with no SEO needs, a pure SPA is simpler and avoids hydration complexity. If your application is entirely content-driven with minimal interactivity, static generation or server rendering without the SPA layer is simpler. The hybrid model is most valuable for applications that span both worlds — public-facing content pages that need SEO alongside authenticated, interactive features.",[30,862],{},[15,864,866],{"id":865},"making-the-decision","Making the Decision",[20,868,869],{},"The decision framework is practical. Answer three questions:",[20,871,872,875],{},[42,873,874],{},"Does this application need search engine visibility?"," If yes, you need server-rendered or statically generated HTML. Pure SPAs are disqualified unless you add pre-rendering, which adds complexity equivalent to just using SSR.",[20,877,878,881],{},[42,879,880],{},"Do users navigate frequently between views in a single session?"," If yes, client-side navigation provides a meaningfully better experience. Full page reloads on every click in a data-heavy dashboard are a noticeable degradation.",[20,883,884,887],{},[42,885,886],{},"Is the application primarily content consumption or content creation?"," Content consumption (reading articles, browsing products) favors server rendering because each page is a self-contained unit. Content creation (editing documents, managing data) favors SPA because the interface maintains state across many interactions.",[20,889,890],{},"If the answers point in the same direction, the choice is clear. If they conflict — you need SEO and frequent navigation and interactive features — the hybrid model is your answer. The frameworks that implement hybrid rendering well have made the SPA vs MPA debate largely academic for new projects. Start with universal rendering and optimize from there based on what your users and your data tell you.",{"title":129,"searchDepth":130,"depth":130,"links":892},[893,894,895,896,897],{"id":740,"depth":133,"text":741},{"id":766,"depth":133,"text":767},{"id":800,"depth":133,"text":801},{"id":839,"depth":133,"text":840},{"id":865,"depth":133,"text":866},"Architecture","Single-page apps and multi-page apps solve different problems. Here's how to choose the rendering strategy that matches your application's actual requirements.",[901,902],"SPA vs MPA comparison","single page app vs multi page app",{},"/blog/single-page-app-vs-multi-page",{"title":734,"description":899},"blog/single-page-app-vs-multi-page",[898,908,909],"SPA","Rendering","g-t762fE6wQqLUhc2PwPoUalq_X08ZPGBdNsZpZg4UM",{"id":912,"title":913,"author":914,"body":915,"category":302,"date":1042,"description":1043,"extension":140,"featured":141,"image":142,"keywords":1044,"meta":1048,"navigation":147,"path":1049,"readTime":1050,"seo":1051,"stem":1052,"tags":1053,"__hash__":1058},"blog/blog/ancient-irish-mythology.md","Ancient Irish Mythology: The Cycles That Shaped a Culture",{"name":9,"bio":10},{"type":12,"value":916,"toc":1036},[917,921,924,944,959,970,976,980,988,996,999,1003,1006,1014,1017,1021,1028],[15,918,920],{"id":919},"four-cycles-of-story","Four Cycles of Story",[20,922,923],{},"Irish mythology is organized into four great cycles, each focused on a different period and cast of characters. These are not arbitrary divisions — they represent different layers of cultural memory, from the cosmic to the historical, preserved through oral tradition and eventually written down by Christian monks in the medieval period.",[20,925,926,927,930,931,935,936,939,940,127],{},"The ",[42,928,929],{},"Mythological Cycle"," deals with the earliest inhabitants of Ireland, including the ",[115,932,934],{"href":933},"/blog/tuatha-de-danann-mythology","Tuatha De Danann"," — the divine race who ruled Ireland before the arrival of the Gaels. These stories describe the creation and shaping of the Irish landscape, the battles between supernatural races, and the retreat of the old gods into the ",[170,937,938],{},"sidhe"," (fairy mounds) after their defeat by the ",[115,941,943],{"href":942},"/blog/sons-of-mil-milesian-invasion-ireland","Sons of Mil",[20,945,926,946,949,950,953,954,958],{},[42,947,948],{},"Ulster Cycle"," centers on the heroes of the Ulaid (Ulster), particularly Cu Chulainn, the greatest warrior in Irish mythology. The central narrative is the ",[170,951,952],{},"Tain Bo Cuailnge"," (The Cattle Raid of Cooley), an epic that describes the invasion of Ulster by Queen Medb of Connacht and Cu Chulainn's single-handed defense of his province. The Ulster Cycle is set in the Iron Age and preserves details about ",[115,955,957],{"href":956},"/blog/highland-warrior-culture","warrior culture",", chariot warfare, and social customs that may reflect genuine pre-Christian practice.",[20,960,926,961,964,965,969],{},[42,962,963],{},"Fenian Cycle"," (or Ossianic Cycle) follows Fionn mac Cumhaill and the Fianna, a band of wandering warriors who serve the High King. These stories are more romantic and adventure-oriented than the Ulster Cycle, and they spread beyond Ireland into Scotland, where Fionn (Fingal) became a central figure in ",[115,966,968],{"href":967},"/blog/scottish-gaelic-language-history","Scottish Gaelic"," tradition. James Macpherson's controversial \"Ossian\" poems of the 1760s brought the Fenian Cycle to a European audience and helped ignite the Romantic movement.",[20,971,926,972,975],{},[42,973,974],{},"Historical Cycle"," (or Cycles of the Kings) narrates the deeds of legendary and semi-historical Irish kings, bridging mythology and recorded history.",[15,977,979],{"id":978},"what-the-myths-encode","What the Myths Encode",[20,981,982,983,987],{},"Reading Irish mythology as entertainment misses the point. These stories encoded legal principles, genealogical claims, cosmological beliefs, and political arguments. The ",[115,984,986],{"href":985},"/blog/lebor-gabala-erenn-book-of-invasions","Lebor Gabala Erenn"," — the Book of Invasions — is not a history in the modern sense, but it served as a charter myth for Gaelic Ireland, establishing who had the right to rule and why.",[20,989,990,991,995],{},"The story of ",[115,992,994],{"href":993},"/blog/fenius-farsaid-tower-of-babel-gaelic","Fenius Farsaid at the Tower of Babel",", which claims that the Gaelic language was deliberately constructed from the best elements of all the languages confused at Babel, is obviously not historical. But it makes a powerful cultural claim: that Gaelic is not merely one language among many but a perfected synthesis, and that the Gaels are not merely one people among many but a chosen lineage with a special destiny.",[20,997,998],{},"Similarly, the mythology's treatment of sovereignty — the concept that legitimate kingship requires the consent of the land itself, personified as a goddess — reflects genuine pre-Christian political theology. The king's ritual marriage to the land was not just a metaphor. It was an ideological framework that governed the selection and evaluation of rulers for centuries.",[15,1000,1002],{"id":1001},"monks-and-manuscripts","Monks and Manuscripts",[20,1004,1005],{},"The paradox of Irish mythology is that it was preserved almost entirely by Christian monks. The stories are pagan in origin — they describe pre-Christian gods, rituals, and worldviews — but they were written down in monasteries during the early medieval period, often by scribes who added Christian glosses or apologetic commentary.",[20,1007,1008,1009,1013],{},"This creates interpretive challenges. When a manuscript describes the Tuatha De Danann, is it preserving a genuine pre-Christian tradition or filtering it through a Christian lens? The answer is usually both. The monks were literate products of ",[115,1010,1012],{"href":1011},"/blog/celtic-christianity-scotland","Celtic Christianity",", trained in Latin scholarship but also heirs to a Gaelic oral tradition that they valued and wished to preserve. They Christianized what they could and simply recorded what they could not.",[20,1015,1016],{},"The result is a body of literature that is simultaneously pagan and Christian, mythological and historical, fantastical and deeply grounded in the Irish landscape. Every hill, river, and plain in Ireland has a story attached to it in the mythology — a narrative archaeology that makes the land itself a text.",[15,1018,1020],{"id":1019},"mythology-and-identity","Mythology and Identity",[20,1022,1023,1024,1027],{},"Irish mythology is not an antiquarian curiosity. It shaped — and continues to shape — how Irish and Gaelic-speaking Scots understand their place in the world. The mythological connection between Ireland and Scotland, established through the Dal Riata migration and the shared Gaelic literary tradition, means that stories like the ",[115,1025,1026],{"href":942},"Milesian invasion"," and the deeds of Fionn mac Cumhaill belong to both cultures.",[20,1029,1030,1031,1035],{},"For those tracing their ancestry through ",[115,1032,1034],{"href":1033},"/blog/what-is-genetic-genealogy","genetic genealogy",", the mythology offers a parallel narrative — not a scientific one, but a cultural one that records how the Gaelic peoples understood their own origins long before DNA testing was possible. The myths got the trajectory right, even when the details were fantastical: people came from the east, through multiple waves, and the current inhabitants are the inheritors of all who came before.",{"title":129,"searchDepth":130,"depth":130,"links":1037},[1038,1039,1040,1041],{"id":919,"depth":133,"text":920},{"id":978,"depth":133,"text":979},{"id":1001,"depth":133,"text":1002},{"id":1019,"depth":133,"text":1020},"2025-12-15","Irish mythology is not fairy tales. It is a sophisticated literary tradition preserved by monks, encoding centuries of cultural memory in story form.",[1045,1046,1047],"ancient irish mythology","irish mythological cycles","irish mythology overview",{},"/blog/ancient-irish-mythology",6,{"title":913,"description":1043},"blog/ancient-irish-mythology",[1054,1055,1056,1057],"Irish Mythology","Celtic Culture","Gaelic Literature","Ancient Ireland","cHxg6md0Lhc6Qkr_Q_PxehCBs5NPAa0DPitUer8uurc",{"id":1060,"title":1061,"author":1062,"body":1063,"category":137,"date":1042,"description":1158,"extension":140,"featured":141,"image":142,"keywords":1159,"meta":1163,"navigation":147,"path":1164,"readTime":148,"seo":1165,"stem":1166,"tags":1167,"__hash__":1172},"blog/blog/bastionglass-quoting-engine.md","Building a Quoting Engine for the Auto Glass Industry",{"name":9,"bio":10},{"type":12,"value":1064,"toc":1152},[1065,1069,1072,1075,1078,1086,1090,1093,1101,1104,1112,1116,1119,1122,1125,1128,1131,1135,1138,1141,1144],[15,1066,1068],{"id":1067},"why-auto-glass-quoting-is-harder-than-it-looks","Why Auto Glass Quoting Is Harder Than It Looks",[20,1070,1071],{},"From the outside, quoting an auto glass job seems straightforward: look up the part, add labor, give the customer a number. In practice, it involves dozens of variables that interact in non-obvious ways.",[20,1073,1074],{},"The glass itself varies by vehicle year, make, model, and trim. A 2022 Toyota Camry LE has a different windshield than a 2022 Toyota Camry XSE because the XSE has a heads-up display that requires a specific glass type with a coating layer. That is one vehicle model with two completely different parts, different costs, and different installation procedures.",[20,1076,1077],{},"Then there is the insurance dimension. Insurance-paid jobs and cash-pay jobs have different pricing structures. Insurance companies negotiate rates with glass providers, and those rates vary by insurer, by region, and by the specific glass type. A shop needs to quote accurately for both channels, and the margins are different enough that getting it wrong on a few jobs per week can meaningfully impact profitability.",[20,1079,1080,1085],{},[115,1081,1084],{"href":1082,"rel":1083},"https://bastionglass.com",[464],"BastionGlass","'s quoting engine needed to handle this complexity while being fast enough for Chris to quote a customer on the phone without putting them on hold.",[15,1087,1089],{"id":1088},"vehicle-and-parts-resolution","Vehicle and Parts Resolution",[20,1091,1092],{},"The quoting process starts with vehicle identification. The customer provides year, make, model, and sometimes trim. The system resolves this to a specific vehicle configuration, which determines which glass parts are compatible.",[20,1094,1095,1096,1100],{},"We built the vehicle resolution as a cascading lookup. The database contains a normalized vehicle catalog — makes, models, years, and trims as separate related tables. When a user selects a make, the model dropdown filters to models available for that make. Year and trim further narrow the options. This is the same pattern used in the ",[115,1097,1099],{"href":1098},"/blog/auto-glass-customer-intake-system","customer intake form",", ensuring consistency between the website-facing form and the internal quoting tool.",[20,1102,1103],{},"Once the vehicle is identified, the system queries the parts catalog for compatible glass options. Each vehicle configuration maps to one or more part numbers, each with different characteristics — OEM, OEM-equivalent, and aftermarket options with different price points, quality ratings, and availability.",[20,1105,1106,1107,1111],{},"The parts catalog is a shared resource across all BastionGlass tenants, but pricing overlays are tenant-specific. Each shop can set their own markup percentages, preferred suppliers, and default glass types. The quoting engine merges the shared catalog data with the ",[115,1108,1110],{"href":1109},"/blog/bastionglass-multi-tenant-strategy","tenant-specific configuration"," to produce prices that reflect the individual shop's business model.",[15,1113,1115],{"id":1114},"the-pricing-calculation","The Pricing Calculation",[20,1117,1118],{},"The quote calculation combines several cost components. Glass cost is the base — the wholesale price of the part from the shop's supplier, plus the shop's markup. Labor cost is calculated based on the job type and complexity. A standard windshield replacement has a base labor rate, but certain vehicles require additional labor for recalibration of advanced driver-assistance systems (ADAS), removal of rain sensors, or special adhesive requirements.",[20,1120,1121],{},"Material costs cover adhesives, primers, and other consumables. These are relatively small per job but add up and should be accounted for in the quote rather than absorbed as overhead.",[20,1123,1124],{},"For insurance-paid jobs, the calculation changes. Insurance companies pay based on their own rate schedules, which may differ from the shop's retail pricing. The quoting engine maintains rate tables for major insurers and can generate both a customer-facing quote and an insurance claim amount from the same job record. When the insurance reimbursement differs from the shop's standard rate, the system calculates the customer's out-of-pocket amount automatically.",[20,1126,1127],{},"The entire calculation runs in TypeScript with the business rules encoded as pure functions. Given a vehicle configuration, a part selection, a job type, and a payment method, the pricing function returns a deterministic quote with a full breakdown of each cost component. No side effects, no database calls in the calculation itself — all the data is loaded before the calculation runs, and the function operates on plain objects.",[20,1129,1130],{},"This design made the quoting logic straightforward to test. Unit tests cover the matrix of vehicle types, part selections, and payment methods with known expected results. When insurance rate tables change, we update the test expectations alongside the data and verify that existing quotes are not affected.",[15,1132,1134],{"id":1133},"speed-and-user-experience","Speed and User Experience",[20,1136,1137],{},"A quoting engine is only useful if it is fast. Chris needs to quote a customer while they are on the phone, which means the entire flow — vehicle selection, parts lookup, price calculation — needs to complete in seconds, not minutes.",[20,1139,1140],{},"The primary optimization was preloading. When a user starts a new quote, the most common vehicle configurations and their associated parts are loaded in a single query and cached in memory. The cascading dropdowns filter this preloaded data on the client side rather than making round trips to the server for each selection. The price calculation runs entirely on the server but returns in under 200 milliseconds because all the input data is already resolved.",[20,1142,1143],{},"For less common vehicles that are not in the preloaded set, the system falls back to a server query that typically returns in under 500 milliseconds. The user experience is slightly slower but still well within the tolerance of a phone conversation.",[20,1145,1146,1147,1151],{},"The quoting engine also supports saved quotes that can be sent to customers via email or text with a link to approve and schedule. This turned the quote from a verbal number on a phone call into a documented, trackable artifact that feeds into the ",[115,1148,1150],{"href":1149},"/blog/bastionglass-dispatch-scheduling","scheduling and dispatch system"," when the customer approves.",{"title":129,"searchDepth":130,"depth":130,"links":1153},[1154,1155,1156,1157],{"id":1067,"depth":133,"text":1068},{"id":1088,"depth":133,"text":1089},{"id":1114,"depth":133,"text":1115},{"id":1133,"depth":133,"text":1134},"How I built BastionGlass's quoting engine — vehicle lookups, parts catalogs, labor calculations, and insurance pricing that produce accurate quotes in seconds.",[1160,1161,1162],"auto glass quoting system","service quoting engine","field service pricing software",{},"/blog/bastionglass-quoting-engine",{"title":1061,"description":1158},"blog/bastionglass-quoting-engine",[514,1168,1169,1170,1171],"Auto Glass","Business Logic","TypeScript","Pricing","NYTqQP_sqEkWxdP4FjkbJbxxCnT52S9oiW6LAhjlgAA",{"id":1174,"title":1175,"author":1176,"body":1177,"category":302,"date":1042,"description":1277,"extension":140,"featured":141,"image":142,"keywords":1278,"meta":1285,"navigation":147,"path":1286,"readTime":1287,"seo":1288,"stem":1289,"tags":1290,"__hash__":1296},"blog/blog/celtic-britain-before-romans.md","Celtic Britain Before the Romans",{"name":9,"bio":10},{"type":12,"value":1178,"toc":1270},[1179,1183,1186,1209,1213,1216,1219,1222,1226,1229,1241,1244,1248,1251,1254,1257,1261,1264,1267],[15,1180,1182],{"id":1181},"the-island-before-conquest","The Island Before Conquest",[20,1184,1185],{},"When Julius Caesar made his first expedition to Britain in 55 BC, he did not find a wilderness inhabited by barbarians. He found a densely populated island with a sophisticated Iron Age civilization, powerful tribal kingdoms, extensive trade networks reaching the continent, and a religious establishment -- the druids -- whose influence extended across the Celtic world. Roman conquest would transform Britain, but the civilization the Romans encountered was already centuries old and deeply rooted.",[20,1187,1188,1189,1193,1194,1198,1199,1203,1204,1208],{},"Celtic culture had been established in Britain for at least five hundred years before Caesar's arrival, brought by migrations and cultural exchanges across the English Channel during the ",[115,1190,1192],{"href":1191},"/blog/hallstatt-culture-celtic-origins","Hallstatt"," and ",[115,1195,1197],{"href":1196},"/blog/la-tene-celtic-civilization","La Tene"," periods. The British Celts spoke Brittonic languages -- ancestors of Welsh, Cornish, and Breton -- that belonged to the P-Celtic branch of the ",[115,1200,1202],{"href":1201},"/blog/celtic-languages-family-tree","Celtic language family",". They shared material culture, artistic traditions, and religious practices with their cousins on the continent, particularly the ",[115,1205,1207],{"href":1206},"/blog/gauls-celtic-france","Gauls"," across the Channel.",[15,1210,1212],{"id":1211},"tribes-and-territories","Tribes and Territories",[20,1214,1215],{},"Pre-Roman Britain was divided among dozens of tribes, each controlling a defined territory with its own leadership, economy, and political identity. The major tribes included the Catuvellauni and Trinovantes of southeastern England, the Iceni of East Anglia, the Brigantes of northern England (the largest tribal territory in Britain), the Silures and Ordovices of Wales, and the Dumnonii of the southwest.",[20,1217,1218],{},"These were not primitive bands. The larger tribal territories were proto-states with complex political structures. The Catuvellauni, under their king Cunobelinus in the early first century AD, controlled a territory that functioned as a kingdom with a capital at Camulodunum (modern Colchester), minted its own coinage, and maintained diplomatic relations with Rome. Cunobelinus was called \"King of the Britons\" by the Roman writer Suetonius, though his authority was not recognized by all British tribes.",[20,1220,1221],{},"The hillfort was the defining architectural feature of pre-Roman Celtic Britain. Maiden Castle in Dorset, one of the largest hillforts in Europe, covered an area of 47 acres and was defended by multiple concentric ramparts and ditches. Danebury in Hampshire was occupied continuously for over four centuries and has produced some of the most detailed archaeological evidence for Iron Age British life. These were not just military installations -- they were economic centers, storage facilities, and gathering places for the surrounding population.",[15,1223,1225],{"id":1224},"economy-and-trade","Economy and Trade",[20,1227,1228],{},"The British economy was based on mixed farming, supplemented by metalworking, textile production, and trade. British agriculture was productive enough to generate surpluses that supported a substantial population -- estimates range from one to two million people in the late Iron Age, comparable to the population of Roman Britain.",[20,1230,1231,1232,1236,1237,1240],{},"Trade connections with the continent were extensive. British tin from Cornwall had been traded into the Mediterranean since the ",[115,1233,1235],{"href":1234},"/blog/bronze-age-collapse-europe","Bronze Age",", and by the Iron Age, Britain exported grain, cattle, iron, hides, hunting dogs, and slaves to ",[115,1238,1239],{"href":1206},"Gaul"," and beyond. In return, British elites imported Mediterranean wine, bronze vessels, glass, and other luxury goods. The Hengistbury Head trading settlement on the Dorset coast functioned as a major port for cross-Channel commerce.",[20,1242,1243],{},"Coinage provides evidence of political and economic sophistication. British coins, initially inspired by Macedonian gold staters that reached Britain through continental trade, developed into distinctive regional styles. They bear the names of rulers and tribal identities, providing the earliest written evidence for British Celtic personal names and political organization.",[15,1245,1247],{"id":1246},"the-druids","The Druids",[20,1249,1250],{},"Britain, and particularly the island of Anglesey (Mona), was regarded as the heartland of druidic learning. Caesar reported that aspiring druids from Gaul traveled to Britain to complete their training, suggesting that the British druidic tradition was considered the most authoritative in the Celtic world.",[20,1252,1253],{},"The druids served as priests, judges, teachers, and political advisers. They presided over religious ceremonies, adjudicated disputes between tribes, and maintained the oral traditions that preserved Celtic law, history, and cosmology. Their refusal to write down their teachings -- though they used Greek and later Latin script for mundane purposes -- means that druidic knowledge died with the institution.",[20,1255,1256],{},"The Roman destruction of the druidic center on Anglesey in AD 60, described by Tacitus in vivid detail, was a deliberate act of cultural suppression. The Romans understood that the druids were a unifying force in Celtic resistance, and their elimination was a strategic priority.",[15,1258,1260],{"id":1259},"the-roman-arrival-and-beyond","The Roman Arrival and Beyond",[20,1262,1263],{},"Caesar's expeditions of 55 and 54 BC were reconnaissance in force rather than conquest. The full Roman invasion under Claudius in AD 43 began a process of conquest that took decades and was never fully completed. The northern frontier, eventually marked by Hadrian's Wall, represented the limit of effective Roman control. Beyond it, Celtic and Pictish societies continued largely unaffected by Rome.",[20,1265,1266],{},"Even within Roman Britain, Celtic culture persisted at the popular level. Rural communities maintained Celtic religious practices, Celtic art styles influenced Romano-British material culture, and the Brittonic language survived alongside Latin throughout the Roman period. When Roman authority collapsed in the early fifth century, the cultural substrate that re-emerged was recognizably Celtic.",[20,1268,1269],{},"The pre-Roman Celtic heritage of Britain is the foundation on which everything else was built -- Roman, Anglo-Saxon, Norman, and modern. Understanding it requires moving beyond the Roman sources, which described the Britons through the lens of conquest, and recognizing that the island Caesar invaded was not waiting to be civilized. It already was.",{"title":129,"searchDepth":130,"depth":130,"links":1271},[1272,1273,1274,1275,1276],{"id":1181,"depth":133,"text":1182},{"id":1211,"depth":133,"text":1212},{"id":1224,"depth":133,"text":1225},{"id":1246,"depth":133,"text":1247},{"id":1259,"depth":133,"text":1260},"Before the legions arrived, Britain was a Celtic island of powerful tribes, hillforts, druidic religion, and long-distance trade. The pre-Roman British Celts built a complex civilization that Rome struggled to subdue and never fully controlled.",[1279,1280,1281,1282,1283,1284],"celtic britain before romans","pre-roman britain","iron age britain","british celtic tribes","druids britain","celtic iron age",{},"/blog/celtic-britain-before-romans",9,{"title":1175,"description":1277},"blog/celtic-britain-before-romans",[1291,1292,1293,1294,1295],"Celtic Britain","Pre-Roman Britain","Iron Age Britain","British Celts","Druids","VsoEaOcgPywJ4wub321_oM76cX-hTDQbksAkbKJ7NxA",{"id":1298,"title":1299,"author":1300,"body":1301,"category":1431,"date":1042,"description":1432,"extension":140,"featured":141,"image":142,"keywords":1433,"meta":1436,"navigation":147,"path":1437,"readTime":148,"seo":1438,"stem":1439,"tags":1440,"__hash__":1444},"blog/blog/outsourcing-vs-inhouse-development.md","Outsourcing vs In-House Development: The Honest Trade-Offs",{"name":9,"bio":10},{"type":12,"value":1302,"toc":1425},[1303,1307,1310,1313,1317,1320,1326,1332,1338,1341,1345,1348,1354,1365,1371,1373,1376,1379,1382,1390,1392,1395,1401,1407,1413,1419,1422],[1304,1305,1299],"h1",{"id":1306},"outsourcing-vs-in-house-development-the-honest-trade-offs",[20,1308,1309],{},"The outsourcing versus in-house debate generates strong opinions on both sides, and most of those opinions are colored by bad experiences. Someone who hired a cheap offshore team that delivered unusable code will swear outsourcing never works. Someone who spent eighteen months hiring an in-house team before shipping a single feature will swear in-house development is too slow.",[20,1311,1312],{},"Both are wrong because both are generalizing from specific failures that had specific, avoidable causes. The truth is that both models work well in specific circumstances and fail in specific circumstances. The decision should be analytical, not ideological.",[15,1314,1316],{"id":1315},"when-in-house-development-makes-sense","When In-House Development Makes Sense",[20,1318,1319],{},"In-house development is the right choice when your software is your core competitive advantage, when you need deep institutional knowledge built over years, or when the speed of iteration between business decisions and technical execution is critical.",[20,1321,1322,1325],{},[42,1323,1324],{},"Software is your product."," If you are a SaaS company, your application is what you sell. The people who build it need to understand your market deeply, iterate quickly based on customer feedback, and maintain a codebase over many years. In-house engineers develop domain expertise that outsourced teams cannot replicate because they do not live with the product daily.",[20,1327,1328,1331],{},[42,1329,1330],{},"Tight feedback loops are required."," When the business team needs to discuss a technical trade-off at 2 PM and have a decision implemented by end of day, physical and organizational proximity matters. In-house teams can have hallway conversations, join impromptu meetings, and adjust priorities in real time. This speed of coordination is difficult to achieve with external teams, especially across time zones.",[20,1333,1334,1337],{},[42,1335,1336],{},"Intellectual property is sensitive."," Some applications involve proprietary algorithms, trade secrets, or competitive advantages that you do not want external parties to have access to. While NDAs and contractual protections help, keeping sensitive development in-house reduces the surface area for IP exposure.",[20,1339,1340],{},"The cost of in-house development is significant. Fully loaded cost for a mid-level developer — salary, benefits, equipment, office space, management overhead — runs $150,000 to $250,000 annually in most US markets. A four-person engineering team costs $600,000 to $1,000,000 per year. This is a fixed cost regardless of whether the team has productive work every week.",[15,1342,1344],{"id":1343},"when-outsourcing-makes-sense","When Outsourcing Makes Sense",[20,1346,1347],{},"Outsourcing is the right choice when you need specialized expertise for a defined period, when you want to test a concept before building an in-house team, or when the development work is well-defined and separable from your core business.",[20,1349,1350,1353],{},[42,1351,1352],{},"Specialized expertise you do not need permanently."," A mobile app rewrite, a data migration, a security audit, or a performance optimization project requires specific skills for a defined period. Hiring full-time for skills you need for three months is wasteful. An outsourced team with that specific expertise delivers better results faster than an in-house generalist learning on the job.",[20,1355,1356,1359,1360,1364],{},[42,1357,1358],{},"Validating a product concept."," Before investing in a full-time engineering team, you may need an ",[115,1361,1363],{"href":1362},"/blog/mvp-development-guide","MVP"," to validate product-market fit. An outsourced team can build a functional prototype in weeks rather than the months it takes to recruit an in-house team. If the concept is validated, you build the in-house team. If not, you have spent far less than you would have on full-time hires.",[20,1366,1367,1370],{},[42,1368,1369],{},"Non-core development."," Internal tools, marketing websites, integrations between existing systems, and other work that is necessary but not differentiated is well-suited to outsourcing. The work is well-defined, the quality bar is clear, and the ongoing iteration requirements are low.",[15,1372,840],{"id":839},[20,1374,1375],{},"Most mature organizations use a hybrid model. Core product development is in-house. Specialized projects, overflow capacity, and non-core work are outsourced. This captures the benefits of both models while mitigating the weaknesses of each.",[20,1377,1378],{},"The key to making a hybrid model work is clear boundaries. Define which systems, features, and codebases are in-house responsibilities and which are outsourced. Mixing in-house and outsourced developers on the same codebase without clear ownership creates confusion, blame-shifting, and quality inconsistency.",[20,1380,1381],{},"Establish standards that apply to both teams. Code review processes, testing requirements, deployment procedures, and documentation standards should be identical regardless of who writes the code. If outsourced code enters your repository without meeting the same quality bar as in-house code, it will create technical debt that the in-house team has to maintain.",[20,1383,1384,1385,1389],{},"For guidance on selecting the right external partner, the ",[115,1386,1388],{"href":1387},"/blog/hiring-software-development-company","hiring a development company guide"," covers the vetting process in detail.",[15,1391,866],{"id":865},[20,1393,1394],{},"A practical framework for the decision considers four factors.",[20,1396,1397,1400],{},[42,1398,1399],{},"Duration."," Is this a project (months) or an ongoing function (years)? Projects favor outsourcing. Ongoing functions favor in-house.",[20,1402,1403,1406],{},[42,1404,1405],{},"Complexity and ambiguity."," Well-defined requirements with clear acceptance criteria are easier to outsource. Ambiguous requirements that require iterative discovery favor in-house teams that can adapt quickly.",[20,1408,1409,1412],{},[42,1410,1411],{},"Strategic importance."," Work that is core to your competitive advantage should be in-house. Work that is necessary but undifferentiated can be outsourced without strategic risk.",[20,1414,1415,1418],{},[42,1416,1417],{},"Budget constraints."," In-house teams are a fixed cost. Outsourced teams are a variable cost. If your budget is unpredictable — common in early-stage companies — variable costs provide more flexibility.",[20,1420,1421],{},"Map your specific projects against these dimensions. The answer will rarely be all in-house or all outsourced. It will be a portfolio of decisions where each project is matched to the model that fits its characteristics.",[20,1423,1424],{},"The companies that fail at outsourcing typically fail at one of three things: selecting the wrong partner, defining requirements poorly, or managing the engagement passively. The companies that fail at in-house development typically fail at hiring, retaining talent, or managing the team effectively. The model is not the problem. The execution is.",{"title":129,"searchDepth":130,"depth":130,"links":1426},[1427,1428,1429,1430],{"id":1315,"depth":133,"text":1316},{"id":1343,"depth":133,"text":1344},{"id":839,"depth":133,"text":840},{"id":865,"depth":133,"text":866},"Business","Neither outsourcing nor in-house development is universally better. Here's a framework for making the decision based on your actual situation, not ideology.",[1434,1435],"outsourcing vs in-house development","software outsourcing guide",{},"/blog/outsourcing-vs-inhouse-development",{"title":1299,"description":1432},"blog/outsourcing-vs-inhouse-development",[1441,1442,1443],"Outsourcing","Software Development","Business Strategy","e4kfs9QBh6ttuEBZhQGPIkk8aTwSs9oiHI5phvwQk1w",{"id":1446,"title":1447,"author":1448,"body":1449,"category":137,"date":1042,"description":1556,"extension":140,"featured":141,"image":142,"keywords":1557,"meta":1560,"navigation":147,"path":1561,"readTime":148,"seo":1562,"stem":1563,"tags":1564,"__hash__":1568},"blog/blog/react-native-vs-flutter.md","React Native vs Flutter: A Developer's Honest Comparison",{"name":9,"bio":10},{"type":12,"value":1450,"toc":1550},[1451,1454,1457,1461,1464,1467,1470,1474,1477,1480,1488,1492,1495,1498,1501,1509,1513,1516,1527,1533,1544,1547],[20,1452,1453],{},"I have shipped production apps with both React Native and Flutter. When clients ask me which one to pick, my answer is always the same: it depends on your team, your timeline, and what you are actually building. That is not a cop-out. These are genuinely different tools with different strengths.",[20,1455,1456],{},"Here is what I have learned building real products with both frameworks, stripped of the hype.",[15,1458,1460],{"id":1459},"developer-experience-and-language","Developer Experience and Language",[20,1462,1463],{},"React Native uses JavaScript and TypeScript. If your team already builds web apps, the transition is relatively smooth. You can reuse utilities, state management patterns, and even some business logic between your web and mobile codebases. The mental model of components and props carries over directly.",[20,1465,1466],{},"Flutter uses Dart, a language most developers have not used before. Dart is well-designed and the tooling is excellent, but it means your team has a learning curve. The upside is that Dart was purpose-built for UI development. Hot reload in Flutter is fast and reliable, and the widget composition model is consistent in a way that React Native's bridge between JS and native views sometimes is not.",[20,1468,1469],{},"In practice, I find that teams with strong TypeScript backgrounds ship faster with React Native in the first three months. Teams starting fresh without strong JS opinions sometimes prefer Flutter's more opinionated structure. Neither choice is wrong.",[15,1471,1473],{"id":1472},"performance-in-the-real-world","Performance in the Real World",[20,1475,1476],{},"The \"which is faster\" debate generates more heat than light. For the vast majority of apps — forms, lists, navigation, API calls — both frameworks perform well enough that users cannot tell the difference.",[20,1478,1479],{},"Where differences show up is in graphics-heavy applications. Flutter renders everything through its own Skia engine, which means consistent 60fps animations and custom drawing. React Native relies on native platform views, which is great for standard UI but can struggle with complex custom animations on the bridge.",[20,1481,1482,1483,1487],{},"For apps that are mostly data display, CRUD operations, and standard navigation, performance is a wash. For apps with heavy custom animation, game-like interfaces, or complex drawing, Flutter has an edge. I have written about ",[115,1484,1486],{"href":1485},"/blog/mobile-app-performance-optimization","optimizing mobile performance"," in more depth, and the framework choice matters less than people think compared to how you handle data fetching and rendering.",[15,1489,1491],{"id":1490},"ecosystem-and-third-party-libraries","Ecosystem and Third-Party Libraries",[20,1493,1494],{},"React Native has a larger ecosystem. The npm registry has packages for nearly every integration you need — payments, maps, analytics, push notifications. However, quality varies wildly. Some packages are abandoned, some have poor TypeScript support, and some break on new OS versions.",[20,1496,1497],{},"Flutter's pub.dev ecosystem is smaller but more consistently maintained. The first-party packages from Google cover most common needs well. When you need a native integration that does not exist, writing platform channels in Dart is more straightforward than writing a React Native bridge module.",[20,1499,1500],{},"One area where React Native clearly wins is web code sharing. If you are building a web app alongside your mobile app and want to share logic, React Native with tools like Expo gives you a path to do that. Flutter for web exists but is not yet at the same maturity level for production web applications.",[20,1502,1503,1504,1508],{},"When planning your ",[115,1505,1507],{"href":1506},"/blog/mobile-app-development-guide","mobile app development approach",", the ecosystem question is really about what specific integrations your product needs. Check that the libraries exist and are maintained before committing.",[15,1510,1512],{"id":1511},"which-should-you-pick","Which Should You Pick",[20,1514,1515],{},"Here is my decision framework after building with both:",[20,1517,1518,1521,1522,1526],{},[42,1519,1520],{},"Choose React Native when"," your team already knows TypeScript, you want to share code with a web app, you are building a content-driven or data-driven app with standard UI patterns, or you need access to a larger hiring pool. The ",[115,1523,1525],{"href":1524},"/blog/expo-react-native-guide","Expo ecosystem"," has matured to the point where most of the rough edges of React Native are smoothed over.",[20,1528,1529,1532],{},[42,1530,1531],{},"Choose Flutter when"," you are starting a team from scratch and want strong opinions built in, your app has heavy custom UI or animations, you want pixel-perfect consistency across iOS and Android without platform-specific styling work, or you are building something visually distinctive.",[20,1534,1535,1538,1539,1543],{},[42,1536,1537],{},"Choose neither"," when you need deep platform integration (health sensors, AR, low-level Bluetooth) — go native. Or when your \"app\" is really just a mobile-friendly website — consider a ",[115,1540,1542],{"href":1541},"/blog/progressive-web-apps-guide","progressive web app"," instead.",[20,1545,1546],{},"The honest truth is that both frameworks can build excellent production apps. I have seen terrible apps built with both and great apps built with both. The framework matters less than your architecture decisions, your testing discipline, and how well you handle the inevitable platform-specific edge cases.",[20,1548,1549],{},"Pick the one that fits your team. Ship something. Iterate. That matters more than the framework debate.",{"title":129,"searchDepth":130,"depth":130,"links":1551},[1552,1553,1554,1555],{"id":1459,"depth":133,"text":1460},{"id":1472,"depth":133,"text":1473},{"id":1490,"depth":133,"text":1491},{"id":1511,"depth":133,"text":1512},"An experienced developer's honest comparison of React Native and Flutter for production mobile apps — performance, ecosystem, hiring, and which to pick for your project.",[1558,1559],"React Native vs Flutter","mobile app framework comparison",{},"/blog/react-native-vs-flutter",{"title":1447,"description":1556},"blog/react-native-vs-flutter",[1565,1566,1567],"React Native","Flutter","Mobile Development","-j4DHMXDJArgezuce56f1bzLFyFqLf2kJMNCD0EmIZA",{"id":1570,"title":1571,"author":1572,"body":1573,"category":302,"date":1042,"description":1647,"extension":140,"featured":141,"image":142,"keywords":1648,"meta":1654,"navigation":147,"path":1655,"readTime":148,"seo":1656,"stem":1657,"tags":1658,"__hash__":1664},"blog/blog/tartan-day-celebration.md","Tartan Day: Celebrating Scottish Heritage in America",{"name":9,"bio":10},{"type":12,"value":1574,"toc":1641},[1575,1579,1582,1585,1588,1592,1595,1598,1605,1608,1612,1618,1621,1624,1628,1635,1638],[15,1576,1578],{"id":1577},"the-date-and-its-significance","The Date and Its Significance",[20,1580,1581],{},"April 6th was not chosen at random. The date marks the anniversary of the Declaration of Arbroath, signed in 1320, a letter sent by the Scottish nobility to Pope John XXII asserting Scotland's independence from England. The Declaration is one of the most significant documents in Scottish history, and its language about the sovereignty of the people and the right to resist tyranny has long been cited as an influence on the American Declaration of Independence.",[20,1583,1584],{},"The connection between the two declarations is debated by historians. There is no direct documentary evidence that Thomas Jefferson read the Declaration of Arbroath, though the philosophical tradition of popular sovereignty that it represents was certainly part of the intellectual heritage available to the American founders. What is beyond dispute is that Scots and Scots-Irish immigrants played an outsized role in the American Revolution and in the founding of the American republic, and that the principles articulated at Arbroath resonate with the principles articulated at Philadelphia.",[20,1586,1587],{},"Tartan Day was first established in Canada in the 1980s, and the concept spread to the United States in the 1990s. The U.S. Senate passed Resolution 155 in 1998, declaring April 6th as National Tartan Day. The resolution noted that almost half of the signers of the Declaration of Independence were of Scottish descent, that the first speaker of the House was a Scot, and that Scottish-Americans had contributed to every aspect of American life.",[15,1589,1591],{"id":1590},"how-tartan-day-is-celebrated","How Tartan Day Is Celebrated",[20,1593,1594],{},"The centerpiece of Tartan Day in the United States is the New York City Tartan Day Parade, which marches up Sixth Avenue every April. The parade draws pipe bands, clan societies, Scottish dance groups, and Scottish heritage organizations from across the country. It is the largest celebration of Scottish culture in the United States, and its route through Midtown Manhattan gives Scottish heritage a visibility that it rarely achieves in everyday American life.",[20,1596,1597],{},"The parade is complemented by a week of Scottish cultural events in New York, including concerts, ceilidh dances, whisky tastings, lectures, and receptions. Similar events take place in cities with significant Scottish-American populations, including Washington, D.C., Philadelphia, Charleston, and cities throughout the Southeast, where Scots-Irish settlement was historically dense.",[20,1599,1600,1604],{},[115,1601,1603],{"href":1602},"/blog/clan-societies-membership","Clan societies"," are central to Tartan Day celebrations across the country. Many societies organize local events, from formal dinners to casual pub nights, that bring members together on or around April 6th. Highland games organizations often tie their spring events to the Tartan Day calendar, extending the celebration beyond a single day.",[20,1606,1607],{},"The celebrations are not exclusively backward-looking. Tartan Day has become an occasion for Scottish-American organizations to highlight contemporary Scottish culture: modern Scottish literature, film, music, and innovation. Scotland's trade and investment agencies use the week to promote business ties between Scotland and the United States, recognizing that cultural affinity can drive economic partnership.",[15,1609,1611],{"id":1610},"the-scottish-american-story","The Scottish-American Story",[20,1613,1614,1615,1617],{},"Tartan Day exists because the Scottish contribution to American life is genuinely substantial, even if it is not always recognized. Scottish and Scots-Irish immigrants arrived in North America in waves from the early eighteenth century onward, driven by economic hardship, political repression, and, most dramatically, the ",[115,1616,226],{"href":225}," that displaced tens of thousands of families from their ancestral lands.",[20,1619,1620],{},"These immigrants shaped American life in ways that are easy to overlook because they became so thoroughly woven into the fabric of the country. Scottish settlers founded Princeton, built the Appalachian frontier culture, established the Presbyterian churches that became a defining institution of American life, and contributed disproportionately to the professions of law, medicine, engineering, and education.",[20,1622,1623],{},"The Scots-Irish, predominantly Presbyterian settlers from Ulster who had originally migrated from the Scottish Lowlands to northern Ireland, were arguably the most culturally influential immigrant group in American history. Their settlement patterns, from Pennsylvania through the Shenandoah Valley and into the southern backcountry, created a regional culture that profoundly shaped American politics, music, religion, and attitudes toward authority and independence.",[15,1625,1627],{"id":1626},"beyond-the-parade","Beyond the Parade",[20,1629,1630,1631,1634],{},"For many Scottish-Americans, Tartan Day is the entry point to deeper engagement. A person who watches the parade one year might join a ",[115,1632,1633],{"href":1602},"clan society"," the next, attend Highland games the year after, and eventually make the journey to Scotland.",[20,1636,1637],{},"The holiday also creates space for reflection on the more complex aspects of the Scottish-American story. Scottish immigrants participated in the displacement of Native Americans, and many were slaveholders. A mature engagement with Scottish heritage requires reckoning with the full story.",[20,1639,1640],{},"But the core impulse behind Tartan Day is genuinely worth celebrating. The people who crossed an ocean carried more than their belongings. They carried language, music, stories, values, and a stubborn attachment to the idea that people have the right to govern themselves. Those are gifts worth remembering, and April 6th is a good day to do it.",{"title":129,"searchDepth":130,"depth":130,"links":1642},[1643,1644,1645,1646],{"id":1577,"depth":133,"text":1578},{"id":1590,"depth":133,"text":1591},{"id":1610,"depth":133,"text":1611},{"id":1626,"depth":133,"text":1627},"Every April 6th, Tartan Day celebrates the Scottish contribution to American life. From its origins in the 1980s to the New York City parade, here's the story of America's Scottish holiday.",[1649,1650,1651,1652,1653],"tartan day april 6","tartan day celebration","scottish heritage america","tartan day history","new york tartan day parade",{},"/blog/tartan-day-celebration",{"title":1571,"description":1647},"blog/tartan-day-celebration",[1659,1660,1661,1662,1663],"Tartan Day","Scottish Heritage","Scottish-American","American Holidays","Scottish Diaspora","PB_Niqwv-uAxBCVXJ3ryJl7RAeSx94tDCV--viwCl7o",{"id":1666,"title":1667,"author":1668,"body":1669,"category":2872,"date":1042,"description":2873,"extension":140,"featured":141,"image":142,"keywords":2874,"meta":2880,"navigation":147,"path":2881,"readTime":1287,"seo":2882,"stem":2883,"tags":2884,"__hash__":2890},"blog/blog/zero-to-production-nuxt-vercel.md","Zero to Production: My Nuxt + Vercel Deployment Pipeline",{"name":9,"bio":10},{"type":12,"value":1670,"toc":2858},[1671,1675,1678,1716,1719,1721,1728,1731,1743,2088,2105,2107,2114,2122,2129,2132,2147,2150,2153,2167,2169,2173,2179,2348,2356,2362,2368,2479,2490,2492,2496,2505,2587,2594,2597,2680,2690,2692,2696,2699,2717,2723,2739,2741,2745,2753,2756,2758,2762,2768,2778,2787,2789,2793,2796,2818,2821,2824,2826,2828,2854],[15,1672,1674],{"id":1673},"the-setup","The Setup",[20,1676,1677],{},"This post documents the production deployment configuration for a Nuxt 4 app on Vercel — specifically this portfolio. The setup involves:",[274,1679,1680,1692,1701,1707],{},[277,1681,1682,1685,1686,1193,1689],{},[42,1683,1684],{},"Nuxt 4"," with ",[71,1687,1688],{},"ssr: true",[71,1690,1691],{},"preset: 'vercel'",[277,1693,1694,1700],{},[42,1695,1696,1699],{},[71,1697,1698],{},"@nuxt/content"," v3"," using SQLite for content indexing",[277,1702,1703,1706],{},[42,1704,1705],{},"Selective prerendering"," — some routes prerendered at build time, others SSR at request time",[277,1708,1709,1712,1713],{},[42,1710,1711],{},"GitHub → Vercel"," for automatic deploys on push to ",[71,1714,1715],{},"main",[20,1717,1718],{},"I'll cover what works, what doesn't, and the non-obvious configurations that took me time to figure out.",[30,1720],{},[15,1722,926,1724,1727],{"id":1723},"the-verceljson-configuration",[71,1725,1726],{},"vercel.json"," Configuration",[20,1729,1730],{},"Start here. Vercel auto-detects Nuxt, but you need explicit configuration for:",[1732,1733,1734,1737,1740],"ol",{},[277,1735,1736],{},"The build command (because of the native module issue — more on this below)",[277,1738,1739],{},"Security headers",[277,1741,1742],{},"Permanent redirects",[1744,1745,1749],"pre",{"className":1746,"code":1747,"language":1748,"meta":129,"style":129},"language-json shiki shiki-themes github-dark","{\n \"framework\": \"nuxtjs\",\n \"installCommand\": \"pnpm install\",\n \"buildCommand\": \"pnpm rebuild better-sqlite3 && pnpm run build\",\n \"outputDirectory\": \".output/public\",\n \"redirects\": [\n {\n \"source\": \"/(.*)\",\n \"has\": [{ \"type\": \"host\", \"value\": \"jamesrossjr.com\" }],\n \"destination\": \"https://www.jamesrossjr.com/$1\",\n \"permanent\": true\n }\n ],\n \"headers\": [\n {\n \"source\": \"/(.*)\",\n \"headers\": [\n { \"key\": \"X-Frame-Options\", \"value\": \"DENY\" },\n { \"key\": \"X-Content-Type-Options\", \"value\": \"nosniff\" },\n { \"key\": \"Referrer-Policy\", \"value\": \"strict-origin-when-cross-origin\" }\n ]\n },\n {\n \"source\": \"/_nuxt/(.*)\",\n \"headers\": [\n { \"key\": \"Cache-Control\", \"value\": \"public, max-age=31536000, immutable\" }\n ]\n }\n ]\n}\n","json",[71,1750,1751,1760,1776,1788,1801,1814,1822,1827,1840,1870,1883,1894,1900,1906,1914,1919,1930,1937,1963,1986,2009,2015,2020,2025,2037,2044,2067,2072,2077,2082],{"__ignoreMap":129},[1752,1753,1756],"span",{"class":1754,"line":1755},"line",1,[1752,1757,1759],{"class":1758},"s95oV","{\n",[1752,1761,1762,1766,1769,1773],{"class":1754,"line":133},[1752,1763,1765],{"class":1764},"sDLfK"," \"framework\"",[1752,1767,1768],{"class":1758},": ",[1752,1770,1772],{"class":1771},"sU2Wk","\"nuxtjs\"",[1752,1774,1775],{"class":1758},",\n",[1752,1777,1778,1781,1783,1786],{"class":1754,"line":130},[1752,1779,1780],{"class":1764}," \"installCommand\"",[1752,1782,1768],{"class":1758},[1752,1784,1785],{"class":1771},"\"pnpm install\"",[1752,1787,1775],{"class":1758},[1752,1789,1791,1794,1796,1799],{"class":1754,"line":1790},4,[1752,1792,1793],{"class":1764}," \"buildCommand\"",[1752,1795,1768],{"class":1758},[1752,1797,1798],{"class":1771},"\"pnpm rebuild better-sqlite3 && pnpm run build\"",[1752,1800,1775],{"class":1758},[1752,1802,1804,1807,1809,1812],{"class":1754,"line":1803},5,[1752,1805,1806],{"class":1764}," \"outputDirectory\"",[1752,1808,1768],{"class":1758},[1752,1810,1811],{"class":1771},"\".output/public\"",[1752,1813,1775],{"class":1758},[1752,1815,1816,1819],{"class":1754,"line":1050},[1752,1817,1818],{"class":1764}," \"redirects\"",[1752,1820,1821],{"class":1758},": [\n",[1752,1823,1824],{"class":1754,"line":148},[1752,1825,1826],{"class":1758}," {\n",[1752,1828,1830,1833,1835,1838],{"class":1754,"line":1829},8,[1752,1831,1832],{"class":1764}," \"source\"",[1752,1834,1768],{"class":1758},[1752,1836,1837],{"class":1771},"\"/(.*)\"",[1752,1839,1775],{"class":1758},[1752,1841,1842,1845,1848,1851,1853,1856,1859,1862,1864,1867],{"class":1754,"line":1287},[1752,1843,1844],{"class":1764}," \"has\"",[1752,1846,1847],{"class":1758},": [{ ",[1752,1849,1850],{"class":1764},"\"type\"",[1752,1852,1768],{"class":1758},[1752,1854,1855],{"class":1771},"\"host\"",[1752,1857,1858],{"class":1758},", ",[1752,1860,1861],{"class":1764},"\"value\"",[1752,1863,1768],{"class":1758},[1752,1865,1866],{"class":1771},"\"jamesrossjr.com\"",[1752,1868,1869],{"class":1758}," }],\n",[1752,1871,1873,1876,1878,1881],{"class":1754,"line":1872},10,[1752,1874,1875],{"class":1764}," \"destination\"",[1752,1877,1768],{"class":1758},[1752,1879,1880],{"class":1771},"\"https://www.jamesrossjr.com/$1\"",[1752,1882,1775],{"class":1758},[1752,1884,1886,1889,1891],{"class":1754,"line":1885},11,[1752,1887,1888],{"class":1764}," \"permanent\"",[1752,1890,1768],{"class":1758},[1752,1892,1893],{"class":1764},"true\n",[1752,1895,1897],{"class":1754,"line":1896},12,[1752,1898,1899],{"class":1758}," }\n",[1752,1901,1903],{"class":1754,"line":1902},13,[1752,1904,1905],{"class":1758}," ],\n",[1752,1907,1909,1912],{"class":1754,"line":1908},14,[1752,1910,1911],{"class":1764}," \"headers\"",[1752,1913,1821],{"class":1758},[1752,1915,1917],{"class":1754,"line":1916},15,[1752,1918,1826],{"class":1758},[1752,1920,1922,1924,1926,1928],{"class":1754,"line":1921},16,[1752,1923,1832],{"class":1764},[1752,1925,1768],{"class":1758},[1752,1927,1837],{"class":1771},[1752,1929,1775],{"class":1758},[1752,1931,1933,1935],{"class":1754,"line":1932},17,[1752,1934,1911],{"class":1764},[1752,1936,1821],{"class":1758},[1752,1938,1940,1943,1946,1948,1951,1953,1955,1957,1960],{"class":1754,"line":1939},18,[1752,1941,1942],{"class":1758}," { ",[1752,1944,1945],{"class":1764},"\"key\"",[1752,1947,1768],{"class":1758},[1752,1949,1950],{"class":1771},"\"X-Frame-Options\"",[1752,1952,1858],{"class":1758},[1752,1954,1861],{"class":1764},[1752,1956,1768],{"class":1758},[1752,1958,1959],{"class":1771},"\"DENY\"",[1752,1961,1962],{"class":1758}," },\n",[1752,1964,1966,1968,1970,1972,1975,1977,1979,1981,1984],{"class":1754,"line":1965},19,[1752,1967,1942],{"class":1758},[1752,1969,1945],{"class":1764},[1752,1971,1768],{"class":1758},[1752,1973,1974],{"class":1771},"\"X-Content-Type-Options\"",[1752,1976,1858],{"class":1758},[1752,1978,1861],{"class":1764},[1752,1980,1768],{"class":1758},[1752,1982,1983],{"class":1771},"\"nosniff\"",[1752,1985,1962],{"class":1758},[1752,1987,1989,1991,1993,1995,1998,2000,2002,2004,2007],{"class":1754,"line":1988},20,[1752,1990,1942],{"class":1758},[1752,1992,1945],{"class":1764},[1752,1994,1768],{"class":1758},[1752,1996,1997],{"class":1771},"\"Referrer-Policy\"",[1752,1999,1858],{"class":1758},[1752,2001,1861],{"class":1764},[1752,2003,1768],{"class":1758},[1752,2005,2006],{"class":1771},"\"strict-origin-when-cross-origin\"",[1752,2008,1899],{"class":1758},[1752,2010,2012],{"class":1754,"line":2011},21,[1752,2013,2014],{"class":1758}," ]\n",[1752,2016,2018],{"class":1754,"line":2017},22,[1752,2019,1962],{"class":1758},[1752,2021,2023],{"class":1754,"line":2022},23,[1752,2024,1826],{"class":1758},[1752,2026,2028,2030,2032,2035],{"class":1754,"line":2027},24,[1752,2029,1832],{"class":1764},[1752,2031,1768],{"class":1758},[1752,2033,2034],{"class":1771},"\"/_nuxt/(.*)\"",[1752,2036,1775],{"class":1758},[1752,2038,2040,2042],{"class":1754,"line":2039},25,[1752,2041,1911],{"class":1764},[1752,2043,1821],{"class":1758},[1752,2045,2047,2049,2051,2053,2056,2058,2060,2062,2065],{"class":1754,"line":2046},26,[1752,2048,1942],{"class":1758},[1752,2050,1945],{"class":1764},[1752,2052,1768],{"class":1758},[1752,2054,2055],{"class":1771},"\"Cache-Control\"",[1752,2057,1858],{"class":1758},[1752,2059,1861],{"class":1764},[1752,2061,1768],{"class":1758},[1752,2063,2064],{"class":1771},"\"public, max-age=31536000, immutable\"",[1752,2066,1899],{"class":1758},[1752,2068,2070],{"class":1754,"line":2069},27,[1752,2071,2014],{"class":1758},[1752,2073,2075],{"class":1754,"line":2074},28,[1752,2076,1899],{"class":1758},[1752,2078,2080],{"class":1754,"line":2079},29,[1752,2081,2014],{"class":1758},[1752,2083,2085],{"class":1754,"line":2084},30,[1752,2086,2087],{"class":1758},"}\n",[20,2089,926,2090,2093,2094,2097,2098,2101,2102,127],{},[71,2091,2092],{},"outputDirectory"," matters — Nuxt 4 with the Vercel preset outputs to ",[71,2095,2096],{},".output/public",", not ",[71,2099,2100],{},"dist"," or ",[71,2103,2104],{},".nuxt/dist/client",[30,2106],{},[15,2108,926,2110,2113],{"id":2109},"the-better-sqlite3-problem",[71,2111,2112],{},"better-sqlite3"," Problem",[20,2115,2116,2118,2119,2121],{},[71,2117,1698],{}," v3 uses SQLite to index your content at build time. The SQLite driver is ",[71,2120,2112],{},", a native Node.js module that compiles against the platform's architecture.",[20,2123,2124,2125,2128],{},"On Vercel, the build runs on Linux x64. If you're developing on Apple Silicon (M1/M2/M3), your local ",[71,2126,2127],{},"node_modules/better-sqlite3"," was compiled for ARM64. When Vercel pulls your installed dependencies, the binary is wrong for their infrastructure.",[20,2130,2131],{},"The fix is simple: rebuild the native module during the Vercel build step.",[1744,2133,2135],{"className":1746,"code":2134,"language":1748,"meta":129,"style":129},"\"buildCommand\": \"pnpm rebuild better-sqlite3 && pnpm run build\"\n",[71,2136,2137],{"__ignoreMap":129},[1752,2138,2139,2142,2144],{"class":1754,"line":1755},[1752,2140,2141],{"class":1771},"\"buildCommand\"",[1752,2143,1768],{"class":1758},[1752,2145,2146],{"class":1771},"\"pnpm rebuild better-sqlite3 && pnpm run build\"\n",[20,2148,2149],{},"Without this, you get a cryptic error during deployment that mentions failed to load native addon or similar. This burned me for a few hours before I understood what was happening.",[20,2151,2152],{},"If you're using npm instead of pnpm:",[1744,2154,2156],{"className":1746,"code":2155,"language":1748,"meta":129,"style":129},"\"buildCommand\": \"npm rebuild better-sqlite3 && npm run build\"\n",[71,2157,2158],{"__ignoreMap":129},[1752,2159,2160,2162,2164],{"class":1754,"line":1755},[1752,2161,2141],{"class":1771},[1752,2163,1768],{"class":1758},[1752,2165,2166],{"class":1771},"\"npm rebuild better-sqlite3 && npm run build\"\n",[30,2168],{},[15,2170,2172],{"id":2171},"nuxt-config-ssr-selective-prerender","Nuxt Config: SSR + Selective Prerender",[20,2174,926,2175,2178],{},[71,2176,2177],{},"nuxt.config.ts"," nitro configuration controls what gets prerendered vs SSR'd:",[1744,2180,2184],{"className":2181,"code":2182,"language":2183,"meta":129,"style":129},"language-ts shiki shiki-themes github-dark","nitro: {\n preset: 'vercel',\n prerender: {\n crawlLinks: true,\n routes: [\n '/',\n '/blog',\n '/services',\n '/services/architecture-consulting',\n '/services/digital-transformation',\n '/services/web-development',\n '/services/software-development',\n '/portfolio',\n '/portfolio/bastionglass',\n '/portfolio/myautoglassrehab',\n ...blogSlugs.map(slug => `/blog/${slug}`)\n ]\n }\n}\n","ts",[71,2185,2186,2195,2207,2214,2226,2233,2240,2247,2254,2261,2268,2275,2282,2289,2296,2303,2336,2340,2344],{"__ignoreMap":129},[1752,2187,2188,2192],{"class":1754,"line":1755},[1752,2189,2191],{"class":2190},"svObZ","nitro",[1752,2193,2194],{"class":1758},": {\n",[1752,2196,2197,2200,2202,2205],{"class":1754,"line":133},[1752,2198,2199],{"class":2190}," preset",[1752,2201,1768],{"class":1758},[1752,2203,2204],{"class":1771},"'vercel'",[1752,2206,1775],{"class":1758},[1752,2208,2209,2212],{"class":1754,"line":130},[1752,2210,2211],{"class":2190}," prerender",[1752,2213,2194],{"class":1758},[1752,2215,2216,2219,2221,2224],{"class":1754,"line":1790},[1752,2217,2218],{"class":2190}," crawlLinks",[1752,2220,1768],{"class":1758},[1752,2222,2223],{"class":1764},"true",[1752,2225,1775],{"class":1758},[1752,2227,2228,2231],{"class":1754,"line":1803},[1752,2229,2230],{"class":2190}," routes",[1752,2232,1821],{"class":1758},[1752,2234,2235,2238],{"class":1754,"line":1050},[1752,2236,2237],{"class":1771}," '/'",[1752,2239,1775],{"class":1758},[1752,2241,2242,2245],{"class":1754,"line":148},[1752,2243,2244],{"class":1771}," '/blog'",[1752,2246,1775],{"class":1758},[1752,2248,2249,2252],{"class":1754,"line":1829},[1752,2250,2251],{"class":1771}," '/services'",[1752,2253,1775],{"class":1758},[1752,2255,2256,2259],{"class":1754,"line":1287},[1752,2257,2258],{"class":1771}," '/services/architecture-consulting'",[1752,2260,1775],{"class":1758},[1752,2262,2263,2266],{"class":1754,"line":1872},[1752,2264,2265],{"class":1771}," '/services/digital-transformation'",[1752,2267,1775],{"class":1758},[1752,2269,2270,2273],{"class":1754,"line":1885},[1752,2271,2272],{"class":1771}," '/services/web-development'",[1752,2274,1775],{"class":1758},[1752,2276,2277,2280],{"class":1754,"line":1896},[1752,2278,2279],{"class":1771}," '/services/software-development'",[1752,2281,1775],{"class":1758},[1752,2283,2284,2287],{"class":1754,"line":1902},[1752,2285,2286],{"class":1771}," '/portfolio'",[1752,2288,1775],{"class":1758},[1752,2290,2291,2294],{"class":1754,"line":1908},[1752,2292,2293],{"class":1771}," '/portfolio/bastionglass'",[1752,2295,1775],{"class":1758},[1752,2297,2298,2301],{"class":1754,"line":1916},[1752,2299,2300],{"class":1771}," '/portfolio/myautoglassrehab'",[1752,2302,1775],{"class":1758},[1752,2304,2305,2309,2312,2315,2318,2322,2325,2328,2330,2333],{"class":1754,"line":1921},[1752,2306,2308],{"class":2307},"snl16"," ...",[1752,2310,2311],{"class":1758},"blogSlugs.",[1752,2313,2314],{"class":2190},"map",[1752,2316,2317],{"class":1758},"(",[1752,2319,2321],{"class":2320},"s9osk","slug",[1752,2323,2324],{"class":2307}," =>",[1752,2326,2327],{"class":1771}," `/blog/${",[1752,2329,2321],{"class":1758},[1752,2331,2332],{"class":1771},"}`",[1752,2334,2335],{"class":1758},")\n",[1752,2337,2338],{"class":1754,"line":1932},[1752,2339,2014],{"class":1758},[1752,2341,2342],{"class":1754,"line":1939},[1752,2343,1899],{"class":1758},[1752,2345,2346],{"class":1754,"line":1965},[1752,2347,2087],{"class":1758},[20,2349,2350,2355],{},[42,2351,2352],{},[71,2353,2354],{},"crawlLinks: true"," tells Nuxt to follow links found in prerendered pages and prerender those too. This catches any routes you forgot to explicitly list.",[20,2357,2358,2361],{},[42,2359,2360],{},"Why prerender at all?"," For a portfolio/blog, content changes rarely. Prerendered pages load from Vercel's CDN with zero server compute. For blog posts specifically, prerendering means crawlers get fully-rendered HTML — which is the whole point of fixing the SSR issue.",[20,2363,2364,2367],{},[42,2365,2366],{},"The blog slug array"," is dynamically built at config time:",[1744,2369,2371],{"className":2181,"code":2370,"language":2183,"meta":129,"style":129},"import { readdirSync } from 'fs'\nimport { join, basename } from 'path'\n\nConst blogSlugs = readdirSync(join(__dirname, 'content/blog'))\n .filter(f => f.endsWith('.md'))\n .map(f => basename(f, '.md'))\n",[71,2372,2373,2387,2399,2404,2429,2457],{"__ignoreMap":129},[1752,2374,2375,2378,2381,2384],{"class":1754,"line":1755},[1752,2376,2377],{"class":2307},"import",[1752,2379,2380],{"class":1758}," { readdirSync } ",[1752,2382,2383],{"class":2307},"from",[1752,2385,2386],{"class":1771}," 'fs'\n",[1752,2388,2389,2391,2394,2396],{"class":1754,"line":133},[1752,2390,2377],{"class":2307},[1752,2392,2393],{"class":1758}," { join, basename } ",[1752,2395,2383],{"class":2307},[1752,2397,2398],{"class":1771}," 'path'\n",[1752,2400,2401],{"class":1754,"line":130},[1752,2402,2403],{"emptyLinePlaceholder":147},"\n",[1752,2405,2406,2409,2412,2415,2417,2420,2423,2426],{"class":1754,"line":1790},[1752,2407,2408],{"class":1758},"Const blogSlugs ",[1752,2410,2411],{"class":2307},"=",[1752,2413,2414],{"class":2190}," readdirSync",[1752,2416,2317],{"class":1758},[1752,2418,2419],{"class":2190},"join",[1752,2421,2422],{"class":1758},"(__dirname, ",[1752,2424,2425],{"class":1771},"'content/blog'",[1752,2427,2428],{"class":1758},"))\n",[1752,2430,2431,2434,2437,2439,2442,2444,2447,2450,2452,2455],{"class":1754,"line":1803},[1752,2432,2433],{"class":1758}," .",[1752,2435,2436],{"class":2190},"filter",[1752,2438,2317],{"class":1758},[1752,2440,2441],{"class":2320},"f",[1752,2443,2324],{"class":2307},[1752,2445,2446],{"class":1758}," f.",[1752,2448,2449],{"class":2190},"endsWith",[1752,2451,2317],{"class":1758},[1752,2453,2454],{"class":1771},"'.md'",[1752,2456,2428],{"class":1758},[1752,2458,2459,2461,2463,2465,2467,2469,2472,2475,2477],{"class":1754,"line":1050},[1752,2460,2433],{"class":1758},[1752,2462,2314],{"class":2190},[1752,2464,2317],{"class":1758},[1752,2466,2441],{"class":2320},[1752,2468,2324],{"class":2307},[1752,2470,2471],{"class":2190}," basename",[1752,2473,2474],{"class":1758},"(f, ",[1752,2476,2454],{"class":1771},[1752,2478,2428],{"class":1758},[20,2480,2481,2482,2485,2486,2489],{},"This means every ",[71,2483,2484],{},".md"," file in ",[71,2487,2488],{},"content/blog/"," is automatically added to the prerender list. No manual maintenance.",[30,2491],{},[15,2493,2495],{"id":2494},"content-module-configuration","Content Module Configuration",[20,2497,2498,2500,2501,2504],{},[71,2499,1698],{}," v3 requires a ",[71,2502,2503],{},"content.config.ts"," at the project root:",[1744,2506,2508],{"className":2181,"code":2507,"language":2183,"meta":129,"style":129},"import { defineCollection, defineContentConfig } from '@nuxt/content'\n\nExport default defineContentConfig({\n collections: {\n blog: defineCollection({\n type: 'page',\n source: 'blog/*.md'\n })\n }\n})\n",[71,2509,2510,2522,2526,2540,2545,2555,2565,2573,2578,2582],{"__ignoreMap":129},[1752,2511,2512,2514,2517,2519],{"class":1754,"line":1755},[1752,2513,2377],{"class":2307},[1752,2515,2516],{"class":1758}," { defineCollection, defineContentConfig } ",[1752,2518,2383],{"class":2307},[1752,2520,2521],{"class":1771}," '@nuxt/content'\n",[1752,2523,2524],{"class":1754,"line":133},[1752,2525,2403],{"emptyLinePlaceholder":147},[1752,2527,2528,2531,2534,2537],{"class":1754,"line":130},[1752,2529,2530],{"class":1758},"Export ",[1752,2532,2533],{"class":2307},"default",[1752,2535,2536],{"class":2190}," defineContentConfig",[1752,2538,2539],{"class":1758},"({\n",[1752,2541,2542],{"class":1754,"line":1790},[1752,2543,2544],{"class":1758}," collections: {\n",[1752,2546,2547,2550,2553],{"class":1754,"line":1803},[1752,2548,2549],{"class":1758}," blog: ",[1752,2551,2552],{"class":2190},"defineCollection",[1752,2554,2539],{"class":1758},[1752,2556,2557,2560,2563],{"class":1754,"line":1050},[1752,2558,2559],{"class":1758}," type: ",[1752,2561,2562],{"class":1771},"'page'",[1752,2564,1775],{"class":1758},[1752,2566,2567,2570],{"class":1754,"line":148},[1752,2568,2569],{"class":1758}," source: ",[1752,2571,2572],{"class":1771},"'blog/*.md'\n",[1752,2574,2575],{"class":1754,"line":1829},[1752,2576,2577],{"class":1758}," })\n",[1752,2579,2580],{"class":1754,"line":1287},[1752,2581,1899],{"class":1758},[1752,2583,2584],{"class":1754,"line":1872},[1752,2585,2586],{"class":1758},"})\n",[20,2588,2589,2590,2593],{},"Without this file, the module doesn't know your content structure and ",[71,2591,2592],{},"queryCollection()"," returns nothing. This is the most common gotcha — the documentation mentions it, but not prominently enough.",[20,2595,2596],{},"In the page component:",[1744,2598,2600],{"className":2181,"code":2599,"language":2183,"meta":129,"style":129},"const { data: article } = await useAsyncData(`blog-${slug}`, () =>\n queryCollection('blog').path(`/blog/${slug}`).first()\n)\n",[71,2601,2602,2643,2676],{"__ignoreMap":129},[1752,2603,2604,2607,2609,2612,2614,2617,2620,2622,2625,2628,2630,2633,2635,2637,2640],{"class":1754,"line":1755},[1752,2605,2606],{"class":2307},"const",[1752,2608,1942],{"class":1758},[1752,2610,2611],{"class":2320},"data",[1752,2613,1768],{"class":1758},[1752,2615,2616],{"class":1764},"article",[1752,2618,2619],{"class":1758}," } ",[1752,2621,2411],{"class":2307},[1752,2623,2624],{"class":2307}," await",[1752,2626,2627],{"class":2190}," useAsyncData",[1752,2629,2317],{"class":1758},[1752,2631,2632],{"class":1771},"`blog-${",[1752,2634,2321],{"class":1758},[1752,2636,2332],{"class":1771},[1752,2638,2639],{"class":1758},", () ",[1752,2641,2642],{"class":2307},"=>\n",[1752,2644,2645,2648,2650,2653,2656,2659,2661,2664,2666,2668,2670,2673],{"class":1754,"line":133},[1752,2646,2647],{"class":2190}," queryCollection",[1752,2649,2317],{"class":1758},[1752,2651,2652],{"class":1771},"'blog'",[1752,2654,2655],{"class":1758},").",[1752,2657,2658],{"class":2190},"path",[1752,2660,2317],{"class":1758},[1752,2662,2663],{"class":1771},"`/blog/${",[1752,2665,2321],{"class":1758},[1752,2667,2332],{"class":1771},[1752,2669,2655],{"class":1758},[1752,2671,2672],{"class":2190},"first",[1752,2674,2675],{"class":1758},"()\n",[1752,2677,2678],{"class":1754,"line":130},[1752,2679,2335],{"class":1758},[20,2681,926,2682,2685,2686,2689],{},[71,2683,2684],{},"await useAsyncData"," at the top level of ",[71,2687,2688],{},"\u003Cscript setup>"," ensures the data is available during SSR — no blank pages for crawlers.",[30,2691],{},[15,2693,2695],{"id":2694},"environment-variables","Environment Variables",[20,2697,2698],{},"Vercel's environment variable management is solid. A few things to get right:",[20,2700,2701,2706,2707,2710,2711,2716],{},[42,2702,2703],{},[71,2704,2705],{},"NUXT_PUBLIC_SITE_URL"," or the equivalent runtime config key must be set to ",[71,2708,2709],{},"https://www.jamesrossjr.com"," (with www). Without this, the sitemap module generates URLs pointing to the wrong host, and internal links might reference non-",[115,2712,2715],{"href":2713,"rel":2714},"http://www",[464],"www",". Set this in Vercel's dashboard under Project Settings → Environment Variables.",[20,2718,2719,2722],{},[42,2720,2721],{},"Preview vs Production."," Vercel creates a preview deployment for every push and pull request. If your app has environment-specific behavior (analytics, feature flags, API endpoints), separate your vars by environment. The Vercel dashboard lets you scope vars to Production, Preview, or Development.",[20,2724,2725,2730,2731,2734,2735,2738],{},[42,2726,2727,2728],{},"Don't put secrets in ",[71,2729,1726],{}," or anywhere committed to git. Use the dashboard or ",[71,2732,2733],{},"vercel env pull"," to sync to ",[71,2736,2737],{},".env.local"," for local development.",[30,2740],{},[15,2742,2744],{"id":2743},"the-deployment-flow","The Deployment Flow",[1744,2746,2751],{"className":2747,"code":2749,"language":2750},[2748],"language-text","git push origin main\n → GitHub webhook triggers Vercel build\n → pnpm install\n → pnpm rebuild better-sqlite3\n → nuxt build\n → @nuxt/content indexes markdown files into SQLite\n → Nitro generates prerendered HTML for listed routes\n → Bundled output written to .output/\n → Vercel deploys .output/ to edge network\n → Production URL updated\n","text",[71,2752,2749],{"__ignoreMap":129},[20,2754,2755],{},"The whole pipeline takes 35–50 seconds for this project. Most of that is the Nuxt build — prerendering 20+ routes adds meaningful time compared to a pure SSR build.",[30,2757],{},[15,2759,2761],{"id":2760},"what-id-do-differently","What I'd Do Differently",[20,2763,2764,2767],{},[42,2765,2766],{},"Use Vercel Analytics from day one."," It's free, installs in two lines, and gives you real performance data. I added it late and missed early baseline data.",[20,2769,2770,2777],{},[42,2771,2772,2773,2776],{},"Set the ",[71,2774,2775],{},"SITE_URL"," environment variable before first deploy."," The sitemap and robots configuration depend on it, and the default fallback (often localhost or a Vercel preview URL) will end up cached in search engines if you don't set it immediately.",[20,2779,2780,2783,2784,2786],{},[42,2781,2782],{},"Consider a separate branch for blog-only content updates."," Right now, adding a blog post requires a full rebuild. Since ",[71,2785,2354],{}," and prerendering are involved, this is slower than a simple file write. For a higher-volume blog, you'd want either ISR (Incremental Static Regeneration) or a CMS with webhook-triggered redeploys.",[30,2788],{},[15,2790,2792],{"id":2791},"the-result","The Result",[20,2794,2795],{},"The deployment pipeline is:",[274,2797,2798,2804,2807,2810,2815],{},[277,2799,2800,2801,2803],{},"Fully automated — push to ",[71,2802,1715],{},", it ships",[277,2805,2806],{},"Fast — 35–50 second builds",[277,2808,2809],{},"SEO-correct — prerendered HTML for crawlers",[277,2811,2812,2813],{},"Secure — content security headers in ",[71,2814,1726],{},[277,2816,2817],{},"www-canonical — permanent 301 redirect from non-www",[20,2819,2820],{},"The only manual step is setting environment variables once through the Vercel dashboard. Everything else is config as code, committed to the repo.",[20,2822,2823],{},"That's the goal: a deployment pipeline you don't have to think about, because you built it right once.",[30,2825],{},[15,2827,471],{"id":470},[274,2829,2830,2836,2842,2848],{},[277,2831,2832],{},[115,2833,2835],{"href":2834},"/blog/vercel-deployment-best-practices","Vercel Deployment Best Practices: Shipping With Confidence",[277,2837,2838],{},[115,2839,2841],{"href":2840},"/blog/continuous-deployment-guide","Continuous Deployment: From Code Push to Production in Minutes",[277,2843,2844],{},[115,2845,2847],{"href":2846},"/blog/production-monitoring-guide","Production Monitoring: The Metrics That Actually Tell You Something Is Wrong",[277,2849,2850],{},[115,2851,2853],{"href":2852},"/blog/docker-for-developers-guide","Docker for Developers: From Zero to Production Containers",[2855,2856,2857],"style",{},"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 .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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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":129,"searchDepth":130,"depth":130,"links":2859},[2860,2861,2863,2865,2866,2867,2868,2869,2870,2871],{"id":1673,"depth":133,"text":1674},{"id":1723,"depth":133,"text":2862},"The vercel.json Configuration",{"id":2109,"depth":133,"text":2864},"The better-sqlite3 Problem",{"id":2171,"depth":133,"text":2172},{"id":2494,"depth":133,"text":2495},{"id":2694,"depth":133,"text":2695},{"id":2743,"depth":133,"text":2744},{"id":2760,"depth":133,"text":2761},{"id":2791,"depth":133,"text":2792},{"id":470,"depth":133,"text":471},"DevOps","The actual configuration, gotchas, and decisions behind deploying a Nuxt 4 app with SSR, prerendering, and a content module to Vercel — including the sqlite issue that cost me an afternoon.",[2875,2876,2877,2878,2879],"nuxt vercel deployment","nuxt production deployment","vercel nuxt tutorial","deploy nuxt to vercel","nuxt deployment pipeline",{},"/blog/zero-to-production-nuxt-vercel",{"title":1667,"description":2873},"blog/zero-to-production-nuxt-vercel",[2885,760,2886,2887,2872,2888,2889],"Vercel","Deployment","CI/CD","SSR","Production","tqT-YV1XezGpR42V_UxQXh1n6k9S4FyKumipNoBkkmw",{"id":2892,"title":2893,"author":2894,"body":2895,"category":302,"date":3084,"description":3085,"extension":140,"featured":141,"image":142,"keywords":3086,"meta":3093,"navigation":147,"path":3094,"readTime":1829,"seo":3095,"stem":3096,"tags":3097,"__hash__":3103},"blog/blog/viking-dna-british-isles.md","Viking DNA in the British Isles: The Genetic Evidence",{"name":9,"bio":10},{"type":12,"value":2896,"toc":3075},[2897,2901,2904,2907,2915,2919,2927,2945,2948,2956,2960,2963,2976,2979,2982,2986,2989,2992,3000,3003,3007,3010,3013,3016,3020,3023,3043,3050,3053,3055,3057],[15,2898,2900],{"id":2899},"raiders-settlers-ancestors","Raiders, Settlers, Ancestors",[20,2902,2903],{},"The first recorded Viking raid on the British Isles struck the monastery of Lindisfarne in 793 AD. Over the next three centuries, Norse raiders, traders, and settlers reshaped Britain and Ireland — politically, linguistically, and culturally. They established the Danelaw across eastern England, founded the Kingdom of Dublin, colonized Orkney and Shetland, and controlled the Western Isles of Scotland for centuries.",[20,2905,2906],{},"The historical and archaeological evidence for this Norse presence is extensive. But a different question remained unanswered until the advent of modern genetic analysis: how much of the British and Irish gene pool is actually Norse? Did the Vikings fundamentally alter the genetic makeup of the islands, or was their impact primarily cultural and political, layered over a population that remained genetically Celtic and Anglo-Saxon?",[20,2908,2909,2910,2914],{},"The answer, as revealed by both ",[115,2911,2913],{"href":2912},"/blog/ancient-dna-revolution","ancient DNA"," and modern population studies, is: it depends entirely on where you look.",[15,2916,2918],{"id":2917},"the-northern-isles-deep-norse-replacement","The Northern Isles: Deep Norse Replacement",[20,2920,2921,2922,2926],{},"Orkney and Shetland received the most intensive Norse settlement, and their genetic profiles reflect this. The ",[115,2923,2925],{"href":2924},"/blog/scottish-dna-project-findings","Scottish DNA Project"," and academic studies have found that:",[274,2928,2929,2936,2942],{},[277,2930,2931,2932,2935],{},"In ",[42,2933,2934],{},"Shetland",", approximately 40-50% of Y-chromosomes belong to Scandinavian-associated haplogroups, primarily I1 and R1a-M420",[277,2937,2931,2938,2941],{},[42,2939,2940],{},"Orkney",", the proportion is approximately 30-40%",[277,2943,2944],{},"Mitochondrial DNA shows a more mixed picture, with substantial pre-Norse maternal lineages surviving alongside Norse ones",[20,2946,2947],{},"These figures represent the most significant Norse genetic contribution anywhere in the British Isles. The Northern Isles were not just raided — they were colonized. Norse settlers established farming communities, imposed Norse law and language (Norn, a Norse dialect, was spoken in Orkney and Shetland into the eighteenth century), and genetically transformed the population.",[20,2949,2950,2951,2955],{},"Yet even in the Northern Isles, the replacement was not total. Pre-Norse Y-chromosomes — primarily R1b-L21 haplogroups associated with ",[115,2952,2954],{"href":2953},"/blog/celtic-dna-modern-populations","the pre-existing Celtic/Pictish population"," — survive at significant frequencies. The Norse settlement was substantial, but it absorbed rather than eliminated the indigenous population.",[15,2957,2959],{"id":2958},"the-western-isles-and-mainland-scotland","The Western Isles and Mainland Scotland",[20,2961,2962],{},"The Western Isles — Lewis, Harris, the Uists, Skye — were under Norse political control from the ninth century until the Treaty of Perth in 1266. The genetic impact is measurable but less dramatic than in Orkney and Shetland:",[274,2964,2965,2968],{},[277,2966,2967],{},"Norse Y-chromosomes appear at approximately 15-25% frequency in the Western Isles",[277,2969,2970,2971,2975],{},"The dominant genetic signal remains ",[115,2972,2974],{"href":2973},"/blog/r1b-l21-atlantic-celtic-haplogroup","R1b-L21",", the Atlantic Celtic marker",[20,2977,2978],{},"The difference between the Northern and Western Isles reflects different settlement patterns. In Orkney and Shetland, which are geographically closer to Norway and had smaller pre-existing populations, Norse settlers arrived in sufficient numbers to become the majority population. In the Western Isles, Norse settlement overlaid a larger and more established Gaelic-speaking population, resulting in a significant but minority Norse genetic contribution.",[20,2980,2981],{},"On mainland Scotland, Norse Y-chromosomes are present at low frequencies — typically under 10% — concentrated in coastal areas of the north and west where Norse influence was strongest. The interior Highlands show minimal Norse genetic input, consistent with historical evidence that Norse settlement was primarily a coastal phenomenon.",[15,2983,2985],{"id":2984},"the-danelaw-englands-norse-east","The Danelaw: England's Norse East",[20,2987,2988],{},"Eastern England — the territory of the historic Danelaw — received significant Danish Viking settlement from the late ninth century onward. Place-name evidence is abundant: hundreds of towns ending in -by (farmstead), -thorpe (village), and -thwaite (clearing) testify to dense Scandinavian settlement across Yorkshire, Lincolnshire, Norfolk, and the East Midlands.",[20,2990,2991],{},"The genetic evidence, however, suggests that the demographic impact of the Danelaw was more modest than the place-name evidence implies:",[274,2993,2994,2997],{},[277,2995,2996],{},"Modern Y-chromosome studies show elevated Scandinavian haplogroup frequencies in the Danelaw region compared to western England, but the differences are relatively small — on the order of 5-15 percentage points",[277,2998,2999],{},"The \"People of the British Isles\" project found that genetic differences between Danelaw and non-Danelaw England exist but are subtle compared to the more dramatic contrasts between England and the Celtic fringe",[20,3001,3002],{},"The 2020 paper by Margaryan and colleagues, which sequenced ancient DNA from over 400 Viking Age individuals across Europe, provided direct evidence that many \"Viking\" burials in England contained individuals with significant local British ancestry — suggesting rapid cultural assimilation of the local population into Norse cultural practices. The genetic boundary between \"Viking\" and \"Anglo-Saxon\" was more porous than either group's material culture would suggest.",[15,3004,3006],{"id":3005},"ireland-norse-founders-genetic-minorities","Ireland: Norse Founders, Genetic Minorities",[20,3008,3009],{},"The Norse impact on Ireland followed yet another pattern. Vikings established major urban centers — Dublin, Waterford, Wexford, Cork, Limerick — beginning in the ninth century. These were significant trading and political centers, and the Norse-Irish (Hiberno-Norse) population played an important role in Irish history for several centuries.",[20,3011,3012],{},"But genetically, the Norse contribution to Ireland as a whole was small. Ireland's Y-chromosome profile remains overwhelmingly R1b-L21, with Scandinavian haplogroups appearing at very low frequencies — typically under 5% in most regions. The Norse presence in Ireland was concentrated in a few urban centers, and the rural population — which constituted the vast majority — remained genetically Gaelic.",[20,3014,3015],{},"The Hiberno-Norse communities did leave a genetic legacy, but it was geographically restricted. Elevated Norse haplogroup frequencies have been detected in the immediate vicinity of the medieval Norse towns, particularly along the eastern and southern coasts. Outside these areas, the Norse genetic signal is negligible.",[15,3017,3019],{"id":3018},"what-the-dna-tells-us-about-viking-settlement","What the DNA Tells Us About Viking Settlement",[20,3021,3022],{},"The overall picture that emerges from genetic studies of Viking settlement in the British Isles is one of regional variation rather than uniform transformation. The Vikings were not a single demographic wave that washed over the islands equally. They were many separate migrations, each following its own pattern:",[274,3024,3025,3031,3037],{},[277,3026,3027,3030],{},[42,3028,3029],{},"Colonization"," in the Northern Isles, producing deep genetic transformation",[277,3032,3033,3036],{},[42,3034,3035],{},"Political dominance with significant settlement"," in the Western Isles and the Danelaw, producing measurable but minority Norse genetic contributions",[277,3038,3039,3042],{},[42,3040,3041],{},"Urban enclaves"," in Ireland, producing geographically concentrated but nationally minimal genetic impact",[20,3044,3045,3046,127],{},"The genetic evidence also reveals asymmetries in the Norse settlement process. Y-chromosomes (patrilineal markers) consistently show higher Norse frequencies than mitochondrial DNA (matrilineal markers) in the same populations. This pattern suggests that Norse settlement was male-biased: Norse men settled in larger numbers than Norse women, and they married local women from the pre-existing Celtic or ",[115,3047,3049],{"href":3048},"/blog/anglo-saxon-dna-england","Anglo-Saxon populations",[20,3051,3052],{},"For anyone carrying a Y-DNA haplogroup like I1 or R1a-M420 with British Isles ancestry, the Viking Age is the most likely window in which that Scandinavian patriline entered the islands. For the majority of people with British and Irish ancestry, however, the genetic core remains what it was before the first longship appeared on the horizon: Atlantic Celtic, R1b-L21, rooted in the Bronze Age rather than the Viking Age.",[30,3054],{},[15,3056,272],{"id":271},[274,3058,3059,3064,3069],{},[277,3060,3061],{},[115,3062,3063],{"href":2924},"The Scottish DNA Project: What We've Learned",[277,3065,3066],{},[115,3067,3068],{"href":3048},"Anglo-Saxon DNA: How Much of England Is Really Germanic?",[277,3070,3071],{},[115,3072,3074],{"href":3073},"/blog/norman-conquest-genetic-impact","The Norman Conquest: Genetic Impact on Britain",{"title":129,"searchDepth":130,"depth":130,"links":3076},[3077,3078,3079,3080,3081,3082,3083],{"id":2899,"depth":133,"text":2900},{"id":2917,"depth":133,"text":2918},{"id":2958,"depth":133,"text":2959},{"id":2984,"depth":133,"text":2985},{"id":3005,"depth":133,"text":3006},{"id":3018,"depth":133,"text":3019},{"id":271,"depth":133,"text":272},"2025-12-12","The Viking Age transformed the political map of the British Isles, but how much did it transform the gene pool? Ancient and modern DNA studies reveal a complex picture of Norse genetic impact — significant in some regions, surprisingly modest in others.",[3087,3088,3089,3090,3091,3092],"viking dna british isles","norse genetic impact","viking ancestry dna","scandinavian dna england","viking settlement genetics","norse dna scotland",{},"/blog/viking-dna-british-isles",{"title":2893,"description":3085},"blog/viking-dna-british-isles",[3098,3099,3100,3101,3102],"Viking DNA","Norse Genetics","British Isles","Population Genetics","Ancient DNA","bj-ZP0Dl_mVyhGvrMKB7kax2igHToEez9-_6vI14Kf0",{"id":3105,"title":3106,"author":3107,"body":3108,"category":3325,"date":3326,"description":3327,"extension":140,"featured":141,"image":142,"keywords":3328,"meta":3331,"navigation":147,"path":3332,"readTime":148,"seo":3333,"stem":3334,"tags":3335,"__hash__":3338},"blog/blog/data-visualization-web.md","Data Visualization for the Web: Charts, Graphs, and Dashboards",{"name":9,"bio":10},{"type":12,"value":3109,"toc":3319},[3110,3113,3116,3120,3123,3129,3135,3141,3147,3153,3156,3160,3163,3169,3175,3181,3193,3197,3200,3203,3214,3281,3289,3293,3296,3299,3302,3310,3313,3316],[20,3111,3112],{},"Data visualization on the web has a contradiction at its core. The best visualizations are the simplest ones — a well-designed bar chart communicates more than a 3D rotating scatter plot with particle effects. But the tools available make complexity easy and simplicity surprisingly hard to get right. Choosing the right chart type, the right library, and the right level of interactivity determines whether your dashboard informs decisions or just looks impressive in screenshots.",[20,3114,3115],{},"I have built dashboards for SaaS products and internal tools. The patterns that work are more about restraint than technical sophistication.",[15,3117,3119],{"id":3118},"choosing-the-right-chart-type","Choosing the Right Chart Type",[20,3121,3122],{},"This is where most dashboards go wrong. The chart type should be determined by the question the user is trying to answer, not by what looks interesting. A few principles that eliminate most bad choices:",[20,3124,3125,3128],{},[42,3126,3127],{},"Comparing values across categories"," — use a bar chart. Horizontal bars if the category labels are long. Vertical bars if the categories have a natural order like months or quarters.",[20,3130,3131,3134],{},[42,3132,3133],{},"Showing trends over time"," — use a line chart. Multiple lines for comparison, but limit it to four or five series before it becomes unreadable. Area charts work when you want to emphasize volume rather than trajectory.",[20,3136,3137,3140],{},[42,3138,3139],{},"Showing part-to-whole relationships"," — use a stacked bar chart or a treemap. Pie charts are acceptable for two or three segments. Beyond that, humans cannot accurately compare angles, and the chart fails its purpose.",[20,3142,3143,3146],{},[42,3144,3145],{},"Showing distribution"," — use a histogram or box plot. These are underused in web applications because they require a moment of learning, but they communicate far more than summary statistics alone.",[20,3148,3149,3152],{},[42,3150,3151],{},"Showing correlation"," — use a scatter plot. Add a trend line if the relationship is the point.",[20,3154,3155],{},"The chart type decision should be made during product design, not during implementation. If you are choosing chart types while writing code, the requirements are underspecified.",[15,3157,3159],{"id":3158},"library-selection","Library Selection",[20,3161,3162],{},"The JavaScript charting landscape has three tiers, and picking the right tier matters more than picking the right library within a tier.",[20,3164,3165,3168],{},[42,3166,3167],{},"High-level declarative libraries"," — Chart.js, Apache ECharts, Recharts. You provide data and configuration, the library renders the chart. These cover 90% of dashboard needs with minimal code. Chart.js is lightest, ECharts is most feature-rich, Recharts is best for React applications.",[20,3170,3171,3174],{},[42,3172,3173],{},"Low-level rendering libraries"," — D3.js. You describe the visual encoding programmatically. D3 gives you complete control but requires significantly more code for standard charts. Use D3 when you need a custom visualization that does not map to any standard chart type.",[20,3176,3177,3180],{},[42,3178,3179],{},"Canvas/WebGL libraries"," — for large datasets (tens of thousands of points), SVG-based libraries hit performance walls. Libraries like uPlot or Plotly with WebGL rendering handle these cases. If your scatter plot has 50,000 points, you need canvas rendering.",[20,3182,3183,3184,3187,3188,3192],{},"For Vue applications, I typically reach for Chart.js with the ",[71,3185,3186],{},"vue-chartjs"," wrapper for standard charts. The wrapper provides reactive updates when data changes, which integrates cleanly with Vue's reactivity system and ",[115,3189,3191],{"href":3190},"/blog/pinia-state-management-guide","Pinia stores"," that often serve as the data source for dashboard state.",[15,3194,3196],{"id":3195},"performance-with-large-datasets","Performance With Large Datasets",[20,3198,3199],{},"Dashboard performance problems almost always come from one of three sources: too many DOM elements, too-frequent re-renders, or too much data transferred from the server.",[20,3201,3202],{},"SVG charts create a DOM element for every data point. A line chart with 10,000 points creates 10,000 SVG elements. The browser slows down well before that limit. The solutions are data aggregation (show hourly averages instead of per-minute values) or switching to canvas rendering.",[20,3204,3205,3206,3209,3210,3213],{},"Re-renders happen when the chart data or options object changes reference. In Vue, this means wrapping your chart data in a ",[71,3207,3208],{},"shallowRef"," rather than a ",[71,3211,3212],{},"ref"," to avoid triggering deep reactivity tracking on every internal property. Update the data by replacing the entire object rather than mutating individual values.",[1744,3215,3217],{"className":2181,"code":3216,"language":2183,"meta":129,"style":129},"const chartData = shallowRef({\n labels: [],\n datasets: [{ data: [] }],\n})\n\n// Update by replacing, not mutating\nchartData.value = {\n labels: newLabels,\n datasets: [{ data: newValues }],\n}\n",[71,3218,3219,3234,3239,3244,3248,3252,3258,3267,3272,3277],{"__ignoreMap":129},[1752,3220,3221,3223,3226,3229,3232],{"class":1754,"line":1755},[1752,3222,2606],{"class":2307},[1752,3224,3225],{"class":1764}," chartData",[1752,3227,3228],{"class":2307}," =",[1752,3230,3231],{"class":2190}," shallowRef",[1752,3233,2539],{"class":1758},[1752,3235,3236],{"class":1754,"line":133},[1752,3237,3238],{"class":1758}," labels: [],\n",[1752,3240,3241],{"class":1754,"line":130},[1752,3242,3243],{"class":1758}," datasets: [{ data: [] }],\n",[1752,3245,3246],{"class":1754,"line":1790},[1752,3247,2586],{"class":1758},[1752,3249,3250],{"class":1754,"line":1803},[1752,3251,2403],{"emptyLinePlaceholder":147},[1752,3253,3254],{"class":1754,"line":1050},[1752,3255,3257],{"class":3256},"sAwPA","// Update by replacing, not mutating\n",[1752,3259,3260,3263,3265],{"class":1754,"line":148},[1752,3261,3262],{"class":1758},"chartData.value ",[1752,3264,2411],{"class":2307},[1752,3266,1826],{"class":1758},[1752,3268,3269],{"class":1754,"line":1829},[1752,3270,3271],{"class":1758}," labels: newLabels,\n",[1752,3273,3274],{"class":1754,"line":1287},[1752,3275,3276],{"class":1758}," datasets: [{ data: newValues }],\n",[1752,3278,3279],{"class":1754,"line":1872},[1752,3280,2087],{"class":1758},[20,3282,3283,3284,3288],{},"Server-side, aggregate data in your API rather than sending raw records to the frontend. A dashboard that fetches 100,000 rows and processes them in JavaScript is doing the database's job less efficiently. Write the aggregation query, send the summary, render the chart. This aligns with the ",[115,3285,3287],{"href":3286},"/blog/frontend-performance-guide","performance principles"," that apply to any data-heavy frontend.",[15,3290,3292],{"id":3291},"dashboard-layout-and-interaction-design","Dashboard Layout and Interaction Design",[20,3294,3295],{},"A dashboard is not a collection of charts — it is an answer to a set of questions. Arrange charts by information priority. The most important metric belongs in the top-left position (for left-to-right reading cultures). Supporting detail goes below and to the right.",[20,3297,3298],{},"Use consistent color encoding across all charts. If \"revenue\" is blue in the bar chart, it must be blue in the line chart and blue in the summary card. Inconsistent color forces the user to re-learn the encoding for each chart, which destroys the scanning speed that makes dashboards useful.",[20,3300,3301],{},"Filters should be global by default. When a user selects a date range, every chart on the dashboard should update. Chart-specific filters are acceptable for drill-down views but should not be the primary interaction model.",[20,3303,3304,3305,3309],{},"Loading states matter more on dashboards than on typical pages because users expect near-instant updates when changing filters. Use ",[115,3306,3308],{"href":3307},"/blog/skeleton-loading-patterns","skeleton loading patterns"," that match the chart dimensions so the layout does not shift when data arrives. A shimmer effect on a chart-sized placeholder communicates that data is loading better than a spinner does.",[20,3311,3312],{},"Tooltips are the primary interaction mechanism for charts. They should appear on hover without delay, contain the exact values being visualized, and disappear when the cursor moves away. Never require a click to see values — hover-to-reveal matches the exploratory behavior dashboard users exhibit. Keep tooltips formatted consistently: label, value, and optional comparison to the previous period.",[20,3314,3315],{},"The best dashboards I have built started as wireframes on paper — boxes with chart type labels and the questions they answer. The code was the easy part. Getting the information architecture right was the real work.",[2855,3317,3318],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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":129,"searchDepth":130,"depth":130,"links":3320},[3321,3322,3323,3324],{"id":3118,"depth":133,"text":3119},{"id":3158,"depth":133,"text":3159},{"id":3195,"depth":133,"text":3196},{"id":3291,"depth":133,"text":3292},"Frontend","2025-12-11","Build effective data visualizations for web applications — choosing the right chart types, library selection, performance with large datasets, and dashboard design.",[3329,3330],"web data visualization","JavaScript charting libraries",{},"/blog/data-visualization-web",{"title":3106,"description":3327},"blog/data-visualization-web",[3336,3337,3325],"Data Visualization","JavaScript","ZpIi6JC9dGed5mtn3BnPqgOa1rg5c0fDIZjGCGt6Vs4",{"id":3340,"title":3341,"author":3342,"body":3343,"category":898,"date":3326,"description":3514,"extension":140,"featured":141,"image":142,"keywords":3515,"meta":3519,"navigation":147,"path":3520,"readTime":1829,"seo":3521,"stem":3522,"tags":3523,"__hash__":3527},"blog/blog/modular-monolith-architecture.md","Modular Monolith: The Architecture Nobody Talks About",{"name":9,"bio":10},{"type":12,"value":3344,"toc":3507},[3345,3349,3352,3355,3358,3360,3364,3367,3373,3379,3385,3391,3393,3397,3400,3406,3412,3423,3429,3435,3437,3441,3444,3450,3461,3467,3470,3472,3479,3481,3483],[15,3346,3348],{"id":3347},"the-false-binary","The False Binary",[20,3350,3351],{},"The architecture conversation in software development is dominated by a false binary: monolith or microservices. Monoliths are portrayed as the legacy approach — everything tangled together, impossible to scale, destined for a painful rewrite. Microservices are portrayed as the modern approach — independently deployable, infinitely scalable, the architecture of successful companies.",[20,3353,3354],{},"The reality is messier. Most teams that adopt microservices early end up with a distributed monolith — services that cannot be deployed independently because they share databases, have synchronous call chains, and require coordinated releases. All the operational complexity of distribution with none of the organizational benefits.",[20,3356,3357],{},"The modular monolith sits between these extremes and is the right choice far more often than it gets credit for. It is a single deployable unit — one process, one database — but internally organized into well-defined modules with explicit boundaries, clear interfaces, and enforced separation. Each module owns its domain logic, its data, and its public API. Modules communicate through defined interfaces, not by reaching into each other's internals.",[30,3359],{},[15,3361,3363],{"id":3362},"what-makes-a-monolith-modular","What Makes a Monolith \"Modular\"",[20,3365,3366],{},"A modular monolith is not just a monolith with folders. The difference is enforcement of boundaries.",[20,3368,3369,3372],{},[42,3370,3371],{},"Each module has a public interface."," Other modules can only interact with it through this interface — typically a set of exported functions or a service class. The module's internal classes, database queries, and implementation details are not accessible to other modules. In TypeScript, this means careful use of barrel exports (index.ts files) that expose only the public API and keeping everything else module-private.",[20,3374,3375,3378],{},[42,3376,3377],{},"Each module owns its data."," Even though all modules share a database, each module has its own set of tables (or schema) that only it queries directly. If the orders module needs customer data, it calls the customers module's public interface — it does not run a SQL query against the customers table. This establishes the data ownership pattern that would make extracting a module into a separate service straightforward later.",[20,3380,3381,3384],{},[42,3382,3383],{},"Module boundaries are enforced."," Convention-based boundaries (\"please don't import from the internals directory\") erode over time. Effective modular monoliths enforce boundaries through build tooling, linting rules, or architectural fitness functions that fail the build when a module violates another module's boundary. Without enforcement, a modular monolith decays into a regular monolith within a year.",[20,3386,3387,3390],{},[42,3388,3389],{},"Modules communicate through defined patterns."," Direct function calls for synchronous operations. An internal event bus for asynchronous operations where the caller does not need a response. These communication patterns mirror what would happen across service boundaries, making future extraction possible without redesigning the interaction model.",[30,3392],{},[15,3394,3396],{"id":3395},"why-this-works-for-most-teams","Why This Works for Most Teams",[20,3398,3399],{},"The modular monolith delivers many of the benefits attributed to microservices while avoiding their costs.",[20,3401,3402,3405],{},[42,3403,3404],{},"Independent development."," Teams can work on different modules without stepping on each other, as long as they respect the module interfaces. This is the organizational benefit that actually matters — not independent deployment, but independent development. Most teams deploy on a shared schedule anyway.",[20,3407,3408,3411],{},[42,3409,3410],{},"Simple operations."," One deployment artifact. One database to back up, monitor, and maintain. One log stream to search. No service mesh, no distributed tracing, no inter-service authentication. For teams without dedicated platform engineering, this reduction in operational burden is significant.",[20,3413,3414,3417,3418,3422],{},[42,3415,3416],{},"Strong consistency."," Because all modules share a database, cross-module operations can use database transactions. Creating an order and decrementing inventory can be atomic. No ",[115,3419,3421],{"href":3420},"/blog/saga-pattern-distributed-transactions","sagas",", no eventual consistency, no compensating transactions. This is a genuine advantage for domains where consistency matters more than independence.",[20,3424,3425,3428],{},[42,3426,3427],{},"Easy refactoring."," Renaming a function that is used across module boundaries is a single refactor operation with the compiler verifying correctness. In a microservices architecture, the same rename requires coordinated changes across multiple repositories, API versioning, and careful deployment sequencing.",[20,3430,3431,3434],{},[42,3432,3433],{},"Extraction path."," A well-structured modular monolith can be partially extracted into services later if organizational or scaling needs demand it. The module boundaries become service boundaries. The internal event bus becomes an external message broker. The public interface becomes an API. The extraction is incremental and driven by actual need rather than speculative architecture.",[30,3436],{},[15,3438,3440],{"id":3439},"when-to-choose-something-else","When to Choose Something Else",[20,3442,3443],{},"The modular monolith has genuine limitations. Being honest about them prevents the same architectural overreach that microservices suffer from.",[20,3445,3446,3449],{},[42,3447,3448],{},"Scaling individual modules independently is hard."," If one module needs 10x more compute than the others, you scale the entire monolith to meet that one module's needs. If this happens frequently — if your workloads have genuinely different scaling profiles — extracting the resource-intensive module into a separate service makes sense. But most applications do not have this problem.",[20,3451,3452,3455,3456,3460],{},[42,3453,3454],{},"Polyglot technology is off the table."," All modules share the same runtime, language, and framework. If one team wants to use Python for machine learning and another wants TypeScript for the API, a monolith cannot accommodate both. ",[115,3457,3459],{"href":3458},"/blog/microservices-vs-monolith","Microservices"," genuinely solve this.",[20,3462,3463,3466],{},[42,3464,3465],{},"Very large teams hit coordination limits."," When you have 50 developers working on a single codebase, even with clean module boundaries, build times, merge conflicts, and release coordination become real friction. Organizations at this scale usually benefit from service extraction — not because microservices are architecturally superior, but because they reduce coordination costs across large teams.",[20,3468,3469],{},"For the majority of projects — startups, small-to-medium teams, internal business applications, SaaS products before product-market fit — the modular monolith is the architecture that delivers the most value with the least complexity. It is not a stepping stone to microservices. It is a legitimate destination that many systems should stay at permanently.",[30,3471],{},[20,3473,3474,3475],{},"If you are starting a new project or evaluating your current architecture and want guidance on structuring a system that fits your team and scale, ",[115,3476,3478],{"href":462,"rel":3477},[464],"let's talk.",[30,3480],{},[15,3482,471],{"id":470},[274,3484,3485,3490,3496,3502],{},[277,3486,3487],{},[115,3488,3489],{"href":3458},"Microservices vs. Monolith: Choosing the Right Architecture",[277,3491,3492],{},[115,3493,3495],{"href":3494},"/blog/clean-architecture-guide","Clean Architecture in Practice",[277,3497,3498],{},[115,3499,3501],{"href":3500},"/blog/domain-driven-design-guide","Domain-Driven Design: A Practical Guide",[277,3503,3504],{},[115,3505,3506],{"href":3420},"The Saga Pattern: Managing Distributed Transactions",{"title":129,"searchDepth":130,"depth":130,"links":3508},[3509,3510,3511,3512,3513],{"id":3347,"depth":133,"text":3348},{"id":3362,"depth":133,"text":3363},{"id":3395,"depth":133,"text":3396},{"id":3439,"depth":133,"text":3440},{"id":470,"depth":133,"text":471},"Microservices get the conference talks. Monoliths get the criticism. The modular monolith quietly solves most of the problems both create.",[3516,3517,3518],"modular monolith architecture","modular monolith vs microservices","monolith architecture patterns",{},"/blog/modular-monolith-architecture",{"title":3341,"description":3514},"blog/modular-monolith-architecture",[3524,3525,3526],"Software Architecture","System Design","Modular Architecture","eeLchntXR36445v29lfXS6-htZ96lyCztDqRl1G2_Ss",[3529,3530,3531,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573,3574,3575,3576,3577,3578,3579,3580,3581,3582,3583,3584,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645,3646,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679,3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695,3696,3697,3698,3699,3700,3701,3702,3703,3704,3705,3706,3707,3708,3709,3710,3711,3712,3713,3714,3715,3716,3717,3718,3719,3720,3721,3722,3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910,3911,3912,3913,3914,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4067,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4159,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4170,4171],{"category":3325},{"category":302},{"category":3532},"AI",{"category":137},{"category":1431},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":3532},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":898},{"category":898},{"category":137},{"category":137},{"category":898},{"category":137},{"category":137},{"category":719},{"category":719},{"category":1431},{"category":1431},{"category":302},{"category":719},{"category":302},{"category":898},{"category":719},{"category":137},{"category":1431},{"category":2872},{"category":3532},{"category":302},{"category":137},{"category":898},{"category":137},{"category":302},{"category":302},{"category":302},{"category":898},{"category":137},{"category":898},{"category":137},{"category":137},{"category":898},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":2872},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":137},{"category":3612},"Career",{"category":3532},{"category":3532},{"category":1431},{"category":898},{"category":1431},{"category":137},{"category":137},{"category":1431},{"category":137},{"category":898},{"category":137},{"category":2872},{"category":2872},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":898},{"category":898},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":3532},{"category":898},{"category":1431},{"category":2872},{"category":2872},{"category":2872},{"category":302},{"category":137},{"category":137},{"category":302},{"category":3325},{"category":3532},{"category":2872},{"category":2872},{"category":719},{"category":2872},{"category":1431},{"category":3532},{"category":302},{"category":137},{"category":302},{"category":898},{"category":302},{"category":898},{"category":719},{"category":302},{"category":302},{"category":137},{"category":1431},{"category":137},{"category":3325},{"category":137},{"category":137},{"category":137},{"category":137},{"category":1431},{"category":1431},{"category":302},{"category":3325},{"category":719},{"category":898},{"category":719},{"category":3325},{"category":137},{"category":137},{"category":2872},{"category":137},{"category":137},{"category":898},{"category":137},{"category":2872},{"category":137},{"category":137},{"category":302},{"category":302},{"category":719},{"category":898},{"category":898},{"category":3612},{"category":3612},{"category":3612},{"category":1431},{"category":137},{"category":2872},{"category":898},{"category":302},{"category":302},{"category":2872},{"category":898},{"category":898},{"category":3325},{"category":137},{"category":302},{"category":302},{"category":137},{"category":302},{"category":2872},{"category":2872},{"category":302},{"category":719},{"category":302},{"category":898},{"category":719},{"category":898},{"category":137},{"category":898},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":898},{"category":137},{"category":137},{"category":719},{"category":137},{"category":2872},{"category":2872},{"category":1431},{"category":137},{"category":137},{"category":137},{"category":898},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":898},{"category":898},{"category":898},{"category":137},{"category":302},{"category":302},{"category":302},{"category":2872},{"category":1431},{"category":302},{"category":302},{"category":137},{"category":302},{"category":137},{"category":3325},{"category":302},{"category":1431},{"category":1431},{"category":137},{"category":137},{"category":3532},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":137},{"category":2872},{"category":2872},{"category":2872},{"category":898},{"category":302},{"category":302},{"category":302},{"category":302},{"category":898},{"category":302},{"category":898},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":1431},{"category":1431},{"category":302},{"category":137},{"category":3325},{"category":898},{"category":3612},{"category":302},{"category":302},{"category":719},{"category":137},{"category":302},{"category":302},{"category":2872},{"category":302},{"category":3325},{"category":2872},{"category":2872},{"category":719},{"category":137},{"category":137},{"category":898},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":3612},{"category":302},{"category":898},{"category":137},{"category":137},{"category":302},{"category":2872},{"category":302},{"category":302},{"category":302},{"category":3325},{"category":302},{"category":302},{"category":137},{"category":302},{"category":137},{"category":898},{"category":302},{"category":302},{"category":302},{"category":3532},{"category":3532},{"category":137},{"category":302},{"category":2872},{"category":2872},{"category":302},{"category":137},{"category":302},{"category":302},{"category":3532},{"category":302},{"category":302},{"category":302},{"category":898},{"category":302},{"category":302},{"category":302},{"category":137},{"category":137},{"category":137},{"category":719},{"category":137},{"category":137},{"category":3325},{"category":137},{"category":3325},{"category":3325},{"category":719},{"category":898},{"category":137},{"category":898},{"category":302},{"category":302},{"category":137},{"category":137},{"category":137},{"category":1431},{"category":137},{"category":137},{"category":302},{"category":898},{"category":3532},{"category":3532},{"category":302},{"category":302},{"category":302},{"category":302},{"category":1431},{"category":137},{"category":302},{"category":302},{"category":137},{"category":137},{"category":3325},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":137},{"category":898},{"category":137},{"category":137},{"category":137},{"category":898},{"category":302},{"category":1431},{"category":3532},{"category":302},{"category":1431},{"category":719},{"category":302},{"category":719},{"category":137},{"category":2872},{"category":302},{"category":302},{"category":137},{"category":302},{"category":898},{"category":302},{"category":302},{"category":137},{"category":1431},{"category":137},{"category":137},{"category":137},{"category":137},{"category":1431},{"category":137},{"category":137},{"category":1431},{"category":2872},{"category":137},{"category":3532},{"category":302},{"category":302},{"category":137},{"category":137},{"category":302},{"category":302},{"category":302},{"category":3532},{"category":137},{"category":137},{"category":898},{"category":3325},{"category":137},{"category":302},{"category":137},{"category":898},{"category":1431},{"category":1431},{"category":3325},{"category":3325},{"category":302},{"category":1431},{"category":719},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":898},{"category":137},{"category":137},{"category":898},{"category":137},{"category":137},{"category":137},{"category":4002},"Programming",{"category":137},{"category":137},{"category":898},{"category":898},{"category":137},{"category":137},{"category":1431},{"category":719},{"category":137},{"category":1431},{"category":137},{"category":137},{"category":137},{"category":137},{"category":2872},{"category":898},{"category":1431},{"category":1431},{"category":137},{"category":137},{"category":1431},{"category":137},{"category":719},{"category":1431},{"category":137},{"category":137},{"category":898},{"category":898},{"category":302},{"category":1431},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":302},{"category":3325},{"category":302},{"category":2872},{"category":719},{"category":719},{"category":719},{"category":719},{"category":719},{"category":719},{"category":302},{"category":137},{"category":2872},{"category":898},{"category":2872},{"category":898},{"category":137},{"category":3325},{"category":302},{"category":898},{"category":3325},{"category":302},{"category":302},{"category":302},{"category":898},{"category":898},{"category":898},{"category":1431},{"category":1431},{"category":1431},{"category":898},{"category":898},{"category":1431},{"category":1431},{"category":1431},{"category":302},{"category":719},{"category":137},{"category":2872},{"category":137},{"category":302},{"category":1431},{"category":1431},{"category":302},{"category":302},{"category":898},{"category":137},{"category":898},{"category":898},{"category":898},{"category":3325},{"category":137},{"category":302},{"category":302},{"category":1431},{"category":1431},{"category":898},{"category":137},{"category":3612},{"category":898},{"category":3612},{"category":1431},{"category":302},{"category":898},{"category":302},{"category":302},{"category":302},{"category":137},{"category":137},{"category":302},{"category":3532},{"category":3532},{"category":2872},{"category":302},{"category":302},{"category":302},{"category":302},{"category":137},{"category":137},{"category":3325},{"category":137},{"category":719},{"category":898},{"category":3325},{"category":3325},{"category":137},{"category":137},{"category":3325},{"category":3325},{"category":3325},{"category":719},{"category":137},{"category":137},{"category":1431},{"category":137},{"category":898},{"category":302},{"category":302},{"category":898},{"category":302},{"category":302},{"category":898},{"category":302},{"category":137},{"category":302},{"category":719},{"category":302},{"category":302},{"category":302},{"category":2872},{"category":2872},{"category":719},1772951194597]