[{"data":1,"prerenderedAt":4647},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-18":4,"blog-paginated-cats":4003},640,[5,169,378,582,759,942,1076,1292,1458,1576,2187,2912,3025,3232,3778],{"id":6,"title":7,"author":8,"body":11,"category":147,"date":148,"description":149,"extension":150,"featured":151,"image":152,"keywords":153,"meta":157,"navigation":158,"path":159,"readTime":160,"seo":161,"stem":162,"tags":163,"__hash__":168},"blog/blog/portfolio-seo-strategy-developer.md","SEO Strategy for a Developer Portfolio: What Actually Works",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":137},"minimark",[14,19,23,26,30,39,42,45,67,71,74,77,80,88,91,95,98,101,104,107,111,114,117],[15,16,18],"h2",{"id":17},"most-developer-seo-advice-is-wrong","Most Developer SEO Advice Is Wrong",[20,21,22],"p",{},"Search \"developer portfolio SEO\" and you will find the same generic advice recycled across dozens of blog posts: add meta descriptions, use semantic HTML, get some backlinks, write a few blog posts. This advice is technically correct and practically useless. It describes the minimum viable effort for SEO, not a strategy that produces meaningful results.",[20,24,25],{},"The gap between \"technically optimized\" and \"generates consistent organic traffic\" is enormous. My portfolio site ranks for hundreds of search queries, generates qualified leads, and grows its organic footprint month over month. That did not happen from adding alt text to images. It happened from treating SEO as a product strategy, not a checklist.",[15,27,29],{"id":28},"content-is-the-strategy-not-a-tactic","Content Is the Strategy, Not a Tactic",[20,31,32,33,38],{},"The core of portfolio SEO is content volume and quality. Not five blog posts — ",[34,35,37],"a",{"href":36},"/blog/portfolio-site-400-blog-articles","hundreds of articles"," covering the topics potential clients search for. This is the fundamental insight that most developer portfolios miss: search engines cannot rank you for topics you have not written about.",[20,40,41],{},"A developer who specializes in multi-tenant SaaS architecture but has no content about multi-tenant systems will not appear in search results when a founder searches for \"multi-tenant SaaS development.\" The founder will find someone else's content, build trust with that author, and hire them instead.",[20,43,44],{},"Content strategy for a developer portfolio is the same as content strategy for any business: identify what your target audience searches for, create content that answers their questions better than existing results, and do it consistently across enough topics to build topical authority.",[20,46,47,48,52,53,57,58,52,62,66],{},"The topic selection process matters. I focus on topics that sit at the intersection of my technical expertise and my clients' research queries. Articles about ",[34,49,51],{"href":50},"/blog/prisma-orm-guide","Prisma ORM"," or ",[34,54,56],{"href":55},"/blog/building-rest-apis-typescript","building REST APIs"," attract developers, some of whom are CTOs or tech leads evaluating contractors. Articles about ",[34,59,61],{"href":60},"/blog/custom-erp-development-guide","custom ERP development",[34,63,65],{"href":64},"/blog/saas-development-guide","SaaS development"," attract business owners and founders directly.",[15,68,70],{"id":69},"technical-seo-that-matters","Technical SEO That Matters",[20,72,73],{},"With the content strategy established, the technical SEO provides the foundation that allows the content to rank. Here is what actually moves the needle for a developer portfolio:",[20,75,76],{},"Server-side rendering is the single most impactful technical decision. Search engine crawlers can execute JavaScript, but they prefer pre-rendered HTML. Nuxt 3 delivers fully rendered pages on the first request, which means every article is indexable immediately without requiring the crawler to execute client-side JavaScript. For a content-heavy site, this is non-negotiable.",[20,78,79],{},"Structured data helps search engines understand the content. Each article includes Article schema markup with the author, publication date, and description. The portfolio page includes Person schema with professional information. The services pages include Service schema. These do not directly improve rankings, but they improve how the site appears in search results — rich snippets, author attribution, and knowledge panel eligibility.",[20,81,82,83,87],{},"Page speed matters, but there are diminishing returns. Getting from a 50 Lighthouse score to a 90 produces meaningful ranking improvements. Getting from a 90 to a 100 is vanity. I focus on the fundamentals: fast server response times, optimized images, minimal JavaScript, and no render-blocking resources. The ",[34,84,86],{"href":85},"/blog/core-web-vitals-optimization","Core Web Vitals"," pass consistently, and that is sufficient.",[20,89,90],{},"Internal linking is the most underrated technical SEO factor. Each article links to two or three related articles, creating a network of connections that helps search engines understand the topical relationships across the site. This distributes page authority from high-traffic articles to lower-traffic ones and helps new articles get indexed and ranked faster.",[15,92,94],{"id":93},"what-does-not-work","What Does Not Work",[20,96,97],{},"Some SEO tactics that are commonly recommended do not produce meaningful results for developer portfolios.",[20,99,100],{},"Social media sharing. Tweeting articles does not improve search rankings. Social signals are not a ranking factor, and the traffic from social media posts is transient — it spikes for a day and then disappears. I share articles when it makes sense for audience building, but I do not expect it to impact SEO.",[20,102,103],{},"Backlink outreach. Sending emails asking other sites to link to your content has an extremely low success rate and consumes time that could be spent writing articles. The backlinks that matter most come naturally — someone reads your article, finds it useful, and links to it from their own content. The best way to earn backlinks is to write articles that are genuinely useful, not to ask for them.",[20,105,106],{},"Over-optimizing for specific keywords. Stuffing an article with a target keyword phrase makes the content worse and does not improve rankings. Modern search engines understand topic relevance through semantic analysis, not keyword density. I write articles about topics, not for keywords. The keywords come naturally from writing clearly about the subject.",[15,108,110],{"id":109},"the-long-game","The Long Game",[20,112,113],{},"Portfolio SEO is a compound investment. Each article adds marginal traffic, but the cumulative effect grows over time as the site's domain authority increases and the internal linking network strengthens. An article published today may receive minimal traffic for the first three months, then gradually increase as it ages and accumulates signals.",[20,115,116],{},"This means portfolio SEO is a poor strategy for developers who need clients next week. It is an excellent strategy for developers who want a sustainable, growing lead generation channel that works independently of platforms, marketplaces, and referral relationships. The content is an asset that appreciates — unlike paid advertising, which stops producing the moment you stop paying.",[20,118,119,120,126,127,131,132,136],{},"The portfolio at ",[34,121,125],{"href":122,"rel":123},"https://www.jamesrossjr.com",[124],"nofollow","jamesrossjr.com"," is both a demonstration of what I build and an application of ",[34,128,130],{"href":129},"/blog/nuxt-seo-optimization","the same SEO principles"," I apply to client projects like ",[34,133,135],{"href":134},"/blog/myautoglassrehab-seo-strategy","MyAutoGlassRehab",". The strategy is not secret — it is just work that most developers do not invest in consistently enough to see results.",{"title":138,"searchDepth":139,"depth":139,"links":140},"",3,[141,143,144,145,146],{"id":17,"depth":142,"text":18},2,{"id":28,"depth":142,"text":29},{"id":69,"depth":142,"text":70},{"id":93,"depth":142,"text":94},{"id":109,"depth":142,"text":110},"Business","2026-02-08","The SEO strategy I use for jamesrossjr.com — what drives traffic, what is a waste of time, and how a developer portfolio can compete for organic search visibility.","md",false,null,[154,155,156],"developer portfolio seo strategy","developer website seo","portfolio seo for developers",{},true,"/blog/portfolio-seo-strategy-developer",7,{"title":7,"description":149},"blog/portfolio-seo-strategy-developer",[164,165,166,167],"SEO","Developer Portfolio","Content Marketing","Business Strategy","pQYeLdLmcl8JLRSv5ujKwN5UePTuo1SqYmKY7NMSEr4",{"id":170,"title":171,"author":172,"body":173,"category":359,"date":148,"description":360,"extension":150,"featured":151,"image":152,"keywords":361,"meta":367,"navigation":158,"path":368,"readTime":160,"seo":369,"stem":370,"tags":371,"__hash__":377},"blog/blog/triangulation-dna-matches.md","Triangulation: Confirming DNA Matches with Shared Segments",{"name":9,"bio":10},{"type":12,"value":174,"toc":351},[175,179,182,195,203,207,210,213,216,238,245,249,252,255,261,267,273,279,285,289,292,295,306,310,318,321,324,327,330,334],[15,176,178],{"id":177},"beyond-the-match-list","Beyond the Match List",[20,180,181],{},"When you take an autosomal DNA test, the results include a list of genetic matches — other tested individuals who share measurable segments of DNA with you. A typical match list contains hundreds or thousands of names, each with a number representing the total amount of shared DNA in centimorgans (cM).",[20,183,184,185,189,190,194],{},"But a match list alone does not tell you how you are related to each person. Two people might share 90 cM of DNA and be second cousins, or they might share 90 cM because of ",[34,186,188],{"href":187},"/blog/endogamy-dna-challenges","endogamy"," in their respective populations, making them more distantly related than the raw number suggests. A single match is a data point. It becomes evidence only when confirmed by additional matches — a process called ",[191,192,193],"strong",{},"triangulation",".",[20,196,197,198,202],{},"Triangulation is the most reliable method in ",[34,199,201],{"href":200},"/blog/what-is-genetic-genealogy","genetic genealogy"," for confirming that a DNA match represents a genuine shared ancestor rather than a statistical artifact or a coincidence of population-level genetic similarity.",[15,204,206],{"id":205},"how-triangulation-works","How Triangulation Works",[20,208,209],{},"The principle is straightforward. If three people — call them A, B, and C — all share the same segment of DNA on the same chromosome, in the same position, then that segment almost certainly came from a single common ancestor. The shared segment has been passed down through different lines of descent to each person, and its presence in all three confirms that the ancestral connection is real.",[20,211,212],{},"This works because of how DNA inheritance operates. When your parents' DNA recombines to create the chromosomes you carry, specific segments from specific ancestors are preserved intact. Your second cousin might have inherited the same segment from the same great-grandparent that you inherited — through a different child of that great-grandparent. A third person who also inherited that segment from the same great-grandparent completes the triangle.",[20,214,215],{},"The requirements for a valid triangulation are specific:",[217,218,219,226,232],"ul",{},[220,221,222,225],"li",{},[191,223,224],{},"All three people must share overlapping DNA"," on the same chromosome, at the same position",[220,227,228,231],{},[191,229,230],{},"The shared segment must be large enough"," to be genealogically meaningful (typically above 7 cM to avoid false matches from identical-by-state segments)",[220,233,234,237],{},[191,235,236],{},"Each person must match each of the other two"," on that segment (A matches B, B matches C, and A matches C)",[20,239,240,241,244],{},"If all three conditions are met, the three individuals form a ",[191,242,243],{},"triangulation group"," — a set of people who almost certainly share a common ancestor from whom the segment was inherited.",[15,246,248],{"id":247},"practical-application-using-the-chromosome-browser","Practical Application: Using the Chromosome Browser",[20,250,251],{},"Several DNA testing platforms provide chromosome browsers that allow you to visualize where on each chromosome you share DNA with your matches. GEDmatch, FamilyTreeDNA, and 23andMe all offer some form of this tool (AncestryDNA does not provide a chromosome browser, which is a significant limitation for triangulation work).",[20,253,254],{},"The process works as follows:",[20,256,257,260],{},[191,258,259],{},"Step 1: Identify a match of interest."," Start with someone you share a meaningful amount of DNA with — say, 60 to 150 cM, suggesting a second to fourth cousin relationship.",[20,262,263,266],{},[191,264,265],{},"Step 2: Examine the shared segments."," Using the chromosome browser, identify which chromosome(s) you share DNA on and the exact positions (start and end points) of the shared segments.",[20,268,269,272],{},[191,270,271],{},"Step 3: Look for other matches who share the same segment."," Check whether any of your other DNA matches also share DNA with you on the same chromosome in an overlapping position.",[20,274,275,278],{},[191,276,277],{},"Step 4: Verify the triangle."," Confirm that your two matches also match each other on that same segment. If they do, you have a triangulation group. If they match you but not each other, the shared DNA may have come from different ancestors (one from your paternal side, one from your maternal side) and is not a valid triangulation.",[20,280,281,284],{},[191,282,283],{},"Step 5: Research the genealogy."," Once a triangulation group is established, examine the documented family trees of all members. Look for a common ancestral couple. If one group member has a well-documented tree that intersects with another member's tree at a specific ancestor, that ancestor is likely the source of the shared segment — and therefore your ancestor as well.",[15,286,288],{"id":287},"what-triangulation-can-and-cannot-prove","What Triangulation Can and Cannot Prove",[20,290,291],{},"Triangulation is strong evidence but not absolute proof. It confirms that a group of people inherited a specific DNA segment from a shared ancestor. It does not, by itself, identify who that ancestor was — that requires documentary genealogy.",[20,293,294],{},"The method is most powerful when combined with family tree research. A triangulation group where all members can trace their documented ancestry back to the same couple provides strong corroboration that the documented connection is also the genetic one. A triangulation group where no common ancestor can be identified in documented records suggests that the connection exists beyond the reach of paper records — potentially pointing to a previously unknown branch of the family.",[20,296,297,298,301,302,305],{},"Triangulation is also limited by segment size. Very small shared segments (below approximately 7 cM) can appear to be shared due to ",[191,299,300],{},"identical by state"," (IBS) rather than ",[191,303,304],{},"identical by descent"," (IBD). IBS segments are stretches of DNA that happen to be the same in two people by coincidence — because those particular base pairs are common in the broader population — rather than because they were inherited from a recent common ancestor. Triangulation with very small segments can produce false positives, which is why most genetic genealogists set a minimum segment size threshold.",[15,307,309],{"id":308},"triangulation-for-adoptees-and-unknown-parentage","Triangulation for Adoptees and Unknown Parentage",[20,311,312,313,317],{},"For ",[34,314,316],{"href":315},"/blog/genetic-genealogy-adoptees","adoptees searching for biological family",", triangulation is an essential tool. Without a known family tree to anchor matches, an adoptee must build the family tree from DNA outward. Triangulation groups provide the scaffolding for this reverse-engineering process.",[20,319,320],{},"By identifying triangulation groups and researching the trees of group members, an adoptee can work backward to identify ancestral couples — great-grandparents or second-great-grandparents — and then trace their descendants forward to identify potential parents. This process is labor-intensive but has produced thousands of successful identifications since autosomal DNA testing became widely available.",[20,322,323],{},"The method works because biology is consistent even when records are not. A sealed adoption file can hide a name, but it cannot erase the DNA segments that pass from parent to child. Those segments persist, and when relatives test, the triangulation reveals the connections that documents conceal.",[20,325,326],{},"Triangulation turns a list of anonymous matches into a structured argument about shared ancestry. It is the difference between \"you share DNA with 1,400 people\" and \"these seven people all share the same segment on chromosome 12, and three of them descend from William and Margaret Thompson of County Antrim.\" The first is data. The second is genealogy.",[328,329],"hr",{},[15,331,333],{"id":332},"related-articles","Related Articles",[217,335,336,341,346],{},[220,337,338],{},[34,339,340],{"href":200},"What Is Genetic Genealogy? A Beginner's Guide",[220,342,343],{},[34,344,345],{"href":315},"Genetic Genealogy for Adoptees: Finding Biological Family",[220,347,348],{},[34,349,350],{"href":187},"Endogamy and DNA: When Everyone Is Related",{"title":138,"searchDepth":139,"depth":139,"links":352},[353,354,355,356,357,358],{"id":177,"depth":142,"text":178},{"id":205,"depth":142,"text":206},{"id":247,"depth":142,"text":248},{"id":287,"depth":142,"text":288},{"id":308,"depth":142,"text":309},{"id":332,"depth":142,"text":333},"Heritage","Triangulation is the process of confirming genetic relationships by identifying DNA segments shared among three or more people. Here's how it works, why it matters, and how to apply it to your own match list.",[362,363,364,365,243,366],"dna triangulation","triangulation genetic genealogy","shared dna segments","confirming dna matches","chromosome browser",{},"/blog/triangulation-dna-matches",{"title":171,"description":360},"blog/triangulation-dna-matches",[372,373,374,375,376],"Triangulation","DNA Matching","Genetic Genealogy","Autosomal DNA","Shared Segments","34QyX8G6Z0AWOzZYVWWaUtd5yofo18Oi4LGVmMcWYIQ",{"id":379,"title":380,"author":381,"body":382,"category":568,"date":569,"description":570,"extension":150,"featured":151,"image":152,"keywords":571,"meta":574,"navigation":158,"path":575,"readTime":160,"seo":576,"stem":577,"tags":578,"__hash__":581},"blog/blog/saas-integration-marketplace.md","Building a SaaS Integration Marketplace",{"name":9,"bio":10},{"type":12,"value":383,"toc":560},[384,388,391,394,397,400,402,406,409,415,421,436,442,445,447,451,454,460,466,477,483,485,489,492,498,504,510,518,520,524,527,530,533,536,538,542],[15,385,387],{"id":386},"why-integrations-become-a-platform-play","Why Integrations Become a Platform Play",[20,389,390],{},"Every SaaS product eventually faces the same request from customers: \"Can you integrate with X?\" The first few integrations are built custom — a Slack notification here, a Salesforce sync there. Each one is a feature, built by your team, maintained by your team.",[20,392,393],{},"This approach doesn't scale. By the time you have 10 custom integrations, each with its own data mapping logic, authentication handling, error recovery, and sync scheduling, you're spending a meaningful percentage of engineering time maintaining integrations instead of building product.",[20,395,396],{},"An integration marketplace is the architectural answer to this scaling problem. It provides a framework that defines how integrations connect to your product, standardizes the patterns that every integration follows, and eventually enables third-party developers to build integrations without your team's involvement.",[20,398,399],{},"Building a marketplace is a significant investment, but for SaaS products that serve business customers, integrations are often the difference between being a standalone tool and being an essential part of the customer's workflow. The more deeply your product integrates with the customer's other tools, the higher the switching cost and the lower the churn.",[328,401],{},[15,403,405],{"id":404},"the-integration-framework-architecture","The Integration Framework Architecture",[20,407,408],{},"The marketplace needs a framework that abstracts the common patterns so that building a new integration is about writing the connector logic, not rebuilding infrastructure.",[20,410,411,414],{},[191,412,413],{},"A standardized connector interface"," defines the contract that every integration must implement. This includes authentication (how to connect to the external service), configuration (what settings the user needs to provide), data mapping (how external data maps to your domain model), sync operations (what data flows in which direction), and webhook handlers (how to receive events from the external service).",[20,416,417,420],{},[191,418,419],{},"An authentication abstraction"," handles OAuth flows, API key management, and token refresh. Most integrations authenticate via OAuth 2.0, and the flow is nearly identical across services — redirect the user, exchange the code for a token, store the token securely, and refresh it before expiration. This logic should be implemented once in the framework, not reimplemented for every integration.",[20,422,423,426,427,431,432,435],{},[191,424,425],{},"A data mapping layer"," transforms external data structures into your internal domain model and vice versa. Each integration defines its mappings declaratively — \"the Salesforce Contact's ",[428,429,430],"code",{},"Email"," field maps to our User's ",[428,433,434],{},"emailAddress"," field.\" The framework handles the transformation, validation, and conflict resolution.",[20,437,438,441],{},[191,439,440],{},"A sync engine"," orchestrates data synchronization between your product and external services. It handles scheduling (run every 15 minutes), incremental sync (only process records changed since the last sync), conflict detection (what happens when the same record is modified in both systems), and failure recovery (retry failed records without re-processing successful ones).",[20,443,444],{},"This framework is what transforms integration building from a multi-week engineering effort to a multi-day effort. Each new integration implements the connector interface and defines its data mappings. The framework handles everything else.",[328,446],{},[15,448,450],{"id":449},"marketplace-ux-and-tenant-configuration","Marketplace UX and Tenant Configuration",[20,452,453],{},"The user-facing marketplace needs to be simple enough that customers can set up integrations without engineering support.",[20,455,456,459],{},[191,457,458],{},"A marketplace catalog"," presents available integrations with descriptions, categories, and status indicators. Each integration listing should clearly communicate what it does, what data it syncs, and what permissions it requires. Screenshots or diagrams showing the data flow help customers understand what they're enabling.",[20,461,462,465],{},[191,463,464],{},"Installation and configuration"," follows a consistent flow across all integrations. Connect the external account (OAuth or API key), configure the sync settings (which data to sync, in which direction, how often), map any fields that require custom mapping, and activate the integration. The framework ensures this flow is consistent so customers don't need to learn a new process for each integration.",[20,467,468,471,472,476],{},[191,469,470],{},"Tenant-level isolation"," is critical. Each tenant's integration connections, credentials, and sync state must be completely isolated. A sync failure for one tenant must not affect another tenant's integration. Credentials stored for one tenant must be inaccessible to any other tenant. This extends the ",[34,473,475],{"href":474},"/blog/saas-tenant-isolation","tenant isolation principles"," into the integration layer.",[20,478,479,482],{},[191,480,481],{},"Monitoring and status"," should be visible to the customer. A dashboard showing sync status, last sync time, records processed, and any errors gives customers confidence that their integrations are working. When something breaks, they should see a clear error message with guidance on how to fix it, not silence.",[328,484],{},[15,486,488],{"id":487},"event-architecture-and-webhooks","Event Architecture and Webhooks",[20,490,491],{},"The integration marketplace depends on a solid event architecture. When data changes in your product, integrations that care about that data need to be notified. When data changes in an external service, your product needs to receive and process that notification.",[20,493,494,497],{},[191,495,496],{},"Outbound events"," from your product are delivered to integrations via an internal event bus. When a record is created, updated, or deleted, the event bus notifies all active integrations that have subscribed to that event type. Each integration's event handler determines whether the event is relevant and what sync action to take.",[20,499,500,503],{},[191,501,502],{},"Inbound webhooks"," from external services need secure, reliable handling. Each integration registers a webhook endpoint that validates the incoming request (checking signatures or authentication headers), parses the payload, and queues a sync operation. Webhook handlers should be fast — acknowledge receipt immediately and process the payload asynchronously.",[20,505,506,509],{},[191,507,508],{},"Idempotency"," is essential for both directions. External services may deliver the same webhook multiple times. Your event bus may emit duplicate events during failure recovery. Every sync operation must handle duplicates gracefully, using external identifiers or content hashes to detect and skip records that have already been processed.",[20,511,512,513,517],{},"Building the ",[34,514,516],{"href":515},"/blog/api-design-best-practices","API architecture"," that supports integration events, webhook registration, and data access is a foundational concern. The integration marketplace's reliability depends entirely on the quality of the underlying API infrastructure.",[328,519],{},[15,521,523],{"id":522},"third-party-developer-experience","Third-Party Developer Experience",[20,525,526],{},"The long-term vision for an integration marketplace is enabling third-party developers to build integrations, which multiplies your integration catalog without multiplying your engineering team.",[20,528,529],{},"This requires clear documentation (API reference, integration framework guide, example integrations), a developer portal (registration, API key management, testing sandbox), a review process (security review, quality checks before publication), and a distribution mechanism (how customers discover and install third-party integrations).",[20,531,532],{},"The developer experience is a product in itself, and it deserves the same attention to usability and documentation that your customer-facing product receives. A poor developer experience results in few third-party integrations, which defeats the purpose of building the marketplace infrastructure.",[20,534,535],{},"Start with first-party integrations built on the framework, prove that the framework works, and then open it to third-party developers once you're confident in the architecture. Launching a developer program on an unstable framework creates frustration that's hard to recover from.",[328,537],{},[15,539,541],{"id":540},"keep-reading","Keep Reading",[217,543,544,549,554],{},[220,545,546],{},[34,547,548],{"href":515},"API Design Best Practices: Building APIs That Last",[220,550,551],{},[34,552,553],{"href":474},"Tenant Isolation in SaaS: Security and Performance",[220,555,556],{},[34,557,559],{"href":558},"/blog/enterprise-integration-patterns","Enterprise Integration Patterns for Modern Systems",{"title":138,"searchDepth":139,"depth":139,"links":561},[562,563,564,565,566,567],{"id":386,"depth":142,"text":387},{"id":404,"depth":142,"text":405},{"id":449,"depth":142,"text":450},{"id":487,"depth":142,"text":488},{"id":522,"depth":142,"text":523},{"id":540,"depth":142,"text":541},"Architecture","2026-02-07","An integration marketplace turns your SaaS into a platform. Here's the architecture behind building one that scales without creating a maintenance nightmare.",[572,573],"SaaS integration marketplace","integration platform architecture",{},"/blog/saas-integration-marketplace",{"title":380,"description":570},"blog/saas-integration-marketplace",[579,580,568],"SaaS","Integrations","6TKDVIoxZ7wAYdWNumgqeW2CSs07iHAsyTvvQHc5F_I",{"id":583,"title":584,"author":585,"body":586,"category":359,"date":739,"description":740,"extension":150,"featured":151,"image":152,"keywords":741,"meta":748,"navigation":158,"path":749,"readTime":750,"seo":751,"stem":752,"tags":753,"__hash__":758},"blog/blog/brendan-navigator-voyage.md","Saint Brendan the Navigator: Celtic Voyage to the Unknown",{"name":9,"bio":10},{"type":12,"value":587,"toc":732},[588,592,600,612,623,627,632,635,641,645,657,668,675,679,693,702,714,718,724],[15,589,591],{"id":590},"the-monk-who-sailed-west","The Monk Who Sailed West",[20,593,594,595,599],{},"Brendan of Clonfert, born around 484 AD in County Kerry, was one of the most remarkable figures of the early Irish church. A contemporary of ",[34,596,598],{"href":597},"/blog/columba-iona-missionary","Saint Columba",", Brendan founded monasteries across Ireland, including the great establishment at Clonfert in County Galway. But it was not his monastic foundations that made him famous across medieval Europe. It was his voyage.",[20,601,602,603,607,608,611],{},"The ",[604,605,606],"em",{},"Navigatio Sancti Brendani Abbatis"," -- The Voyage of Saint Brendan the Abbot -- became one of the most widely read texts of the Middle Ages, translated into virtually every European vernacular and distributed from Iceland to Italy. It describes a seven-year voyage in which Brendan and a crew of monks sailed into the Atlantic in a leather-hulled boat called a ",[604,609,610],{},"currach",", encountering islands, sea monsters, crystal pillars, and ultimately reaching a \"Promised Land of the Saints\" far to the west before returning home.",[20,613,614,615,618,619,622],{},"The text is clearly a literary composition, shaped by biblical typology, classical voyage narratives, and the Irish tradition of ",[604,616,617],{},"immram"," (wonder voyage). But beneath its fantastical surface, the ",[604,620,621],{},"Navigatio"," may preserve genuine geographical knowledge -- descriptions of real places encountered by Irish monks who sailed further into the Atlantic than any European before or after them for centuries.",[15,624,626],{"id":625},"the-voyage-narrative","The Voyage Narrative",[20,628,602,629,631],{},[604,630,621],{}," follows a liturgical structure. Brendan and his monks sail from island to island, and their landfalls correspond to the major feasts of the Christian calendar. Each Easter they celebrate mass on the back of a great whale named Jasconius -- a motif that is obviously legendary but may encode knowledge of whale behavior in the North Atlantic, where large whales surface and remain motionless for extended periods.",[20,633,634],{},"Several of the islands described in the text have been tentatively identified with real locations. The \"Island of Smiths,\" where the monks are pelted with lumps of burning slag, may describe volcanic activity in Iceland. The \"Crystal Pillar\" floating in the sea has been interpreted as an iceberg. The \"Island of Grapes\" could be a reference to wild grapes found in North America -- the same grapes that would give Vinland its name when Norse explorers reached the continent five centuries later.",[20,636,637,638,640],{},"The \"Promised Land of the Saints,\" Brendan's ultimate destination, is described as a vast mainland with rivers, forests, and abundant fruit. Brendan and his monks explore it for forty days before being told by an angel that it is not yet time for this land to be revealed to the world. If the ",[604,639,621],{}," preserves even a kernel of genuine geographical tradition, this mainland could represent North America, making Brendan the first European to reach the New World -- nearly a thousand years before Columbus.",[15,642,644],{"id":643},"the-tim-severin-experiment","The Tim Severin Experiment",[20,646,647,648,650,651,653,654,656],{},"In 1976, the British explorer Tim Severin set out to test whether the voyage described in the ",[604,649,621],{}," was physically possible. He built a ",[604,652,610],{}," using the same materials specified in the text -- ox hides stretched over a wooden frame and waterproofed with animal fat -- and sailed it from Ireland to Newfoundland via the Hebrides, the Faroe Islands, and Iceland, following the island-hopping route that the ",[604,655,621],{}," implies.",[20,658,659,660,663,664,667],{},"The voyage took two sailing seasons, and Severin successfully reached North America in his leather boat. He did not prove that Brendan made the voyage, but he demonstrated that it was technically feasible with sixth-century materials and technology. His account, published as ",[604,661,662],{},"The Brendan Voyage",", showed that the ",[604,665,666],{},"Navigatio's"," descriptions of sea conditions, wildlife, and landfalls were consistent with the actual experience of sailing the North Atlantic in a small open boat.",[20,669,670,671,674],{},"The experiment also confirmed that Irish monks had the seamanship skills to reach the Faroes and Iceland. Archaeological evidence -- including the presence of Irish-style stone crosses and the Norse word ",[604,672,673],{},"papar"," (meaning monks) in Icelandic and Faroese place names -- supports the idea that Irish monks reached both island groups before the Norse, possibly as early as the sixth century.",[15,676,678],{"id":677},"brendan-in-celtic-tradition","Brendan in Celtic Tradition",[20,680,681,682,684,685,688,689,692],{},"Brendan's voyage belongs to a genre of Irish literature called the ",[604,683,617],{},", or wonder voyage, which includes other texts like the ",[604,686,687],{},"Immram Brain"," (Voyage of Bran) and the ",[604,690,691],{},"Immram Mael Duin",". These narratives share common elements: a hero sails west into the Atlantic, visits a series of extraordinary islands, and encounters beings and phenomena that blur the line between the natural and the supernatural.",[20,694,602,695,697,698,701],{},[604,696,617],{}," tradition reflects the distinctive Atlantic orientation of Irish culture. Unlike Mediterranean civilizations, which looked inward toward a known sea, the Irish faced an ocean of unknown extent and unknowable depth. The west was the direction of mystery, the location of the otherworld (",[604,699,700],{},"Tir na nOg",", the Land of Youth), and the horizon beyond which anything might exist. Brendan's voyage is the Christian transformation of this older Celtic fascination with the western ocean.",[20,703,704,705,708,709,713],{},"The monastic context is also essential. Brendan's voyage is explicitly an act of ",[604,706,707],{},"peregrinatio"," -- the voluntary exile that Irish monks undertook as spiritual discipline. Like the monks of ",[34,710,712],{"href":711},"/blog/skellig-michael-monastic-life","Skellig Michael",", Brendan sought God at the edge of the world. His willingness to sail into the unknown was not recklessness but faith expressed through action.",[15,715,717],{"id":716},"legacy","Legacy",[20,719,720,721,723],{},"Whether Brendan reached North America is ultimately less important than what his story reveals about the civilization that produced it. The early Irish church was a maritime culture embedded in an Atlantic world. Its monks built boats, navigated by stars, and sailed to the most remote islands they could find. They carried with them a literary and scholarly tradition that could produce a text like the ",[604,722,621],{}," -- a work that combines theological allegory, geographical observation, and narrative art into a seamless whole.",[20,725,726,727,731],{},"For those tracing ",[34,728,730],{"href":729},"/blog/highland-clearances-clan-ross-diaspora","Celtic heritage"," through the Irish and Scottish diaspora, Brendan is a fitting ancestor figure. The impulse that drove him west -- curiosity, faith, the refusal to accept the horizon as a limit -- is the same impulse that would later drive millions of Irish and Scots across the Atlantic to build new lives in a world that their medieval ancestors may have been the first Europeans to glimpse.",{"title":138,"searchDepth":139,"depth":139,"links":733},[734,735,736,737,738],{"id":590,"depth":142,"text":591},{"id":625,"depth":142,"text":626},{"id":643,"depth":142,"text":644},{"id":677,"depth":142,"text":678},{"id":716,"depth":142,"text":717},"2026-02-05","In the sixth century, an Irish monk named Brendan reportedly sailed into the Atlantic and discovered lands beyond the horizon. The Navigatio Sancti Brendani became one of the most popular texts of the Middle Ages and may preserve real geographical knowledge within its fantastical narrative.",[742,743,744,745,746,747],"saint brendan navigator","brendan voyage","navigatio sancti brendani","irish monks atlantic","brendan america","celtic voyage history",{},"/blog/brendan-navigator-voyage",9,{"title":584,"description":740},"blog/brendan-navigator-voyage",[754,755,756,757,621],"Saint Brendan","Irish Monks","Atlantic Voyage","Celtic Christianity","tumkxNBnoP7mKoEmYDvzfBvApd4n2aaTHNfPyaIl69Q",{"id":760,"title":761,"author":762,"body":763,"category":927,"date":739,"description":928,"extension":150,"featured":151,"image":152,"keywords":929,"meta":933,"navigation":158,"path":934,"readTime":160,"seo":935,"stem":936,"tags":937,"__hash__":941},"blog/blog/conversational-ai-design.md","Designing Conversational AI Experiences That Feel Natural",{"name":9,"bio":10},{"type":12,"value":764,"toc":920},[765,769,772,775,778,780,784,787,790,793,811,814,816,820,823,829,835,841,847,849,853,856,862,868,874,881,883,891,893,895],[15,766,768],{"id":767},"technology-is-not-the-hard-part","Technology Is Not the Hard Part",[20,770,771],{},"The technology to power conversational AI is widely available. LLMs generate fluent, contextually appropriate responses. Speech-to-text and text-to-speech handle voice interfaces. NLU systems parse intent and entities with reasonable accuracy. The API calls work.",[20,773,774],{},"What separates good conversational AI from bad conversational AI is design. Not visual design — there is no UI to design in the traditional sense — but interaction design: how the conversation flows, how the system handles ambiguity, how it recovers from misunderstandings, what it says and when. These design decisions determine whether users find the experience helpful or infuriating.",[20,776,777],{},"Most frustrating chatbot experiences are not technology failures. They are design failures: the system does not set expectations, does not handle unexpected inputs gracefully, does not remember context, and does not know when to hand off to a human. These are solvable problems.",[328,779],{},[15,781,783],{"id":782},"setting-the-right-expectations","Setting the Right Expectations",[20,785,786],{},"The most important design decision happens in the first message.",[20,788,789],{},"A conversational AI that opens with \"How can I help you?\" and nothing else sets the expectation that it can help with anything. When it cannot — and no system can help with everything — the user feels misled. The experience goes from \"this is helpful\" to \"this is useless\" at the first failure.",[20,791,792],{},"Effective opening messages scope the conversation: \"I can help you with order status, returns, and product questions. What can I help with today?\" This tells the user what the system is good at, which sets realistic expectations and guides the user toward queries the system can handle well. It also implicitly communicates that other topics may not be supported, reducing the frequency of out-of-scope queries.",[20,794,795,796,800,801,800,804,800,807,810],{},"For more complex systems that handle many domains, providing starting suggestions — clickable quick replies or suggested questions — guides users while demonstrating capability. \"Here are some things I can help with: ",[797,798,799],"span",{},"Check order status"," ",[797,802,803],{},"Start a return",[797,805,806],{},"Product recommendations",[797,808,809],{},"Shipping info","\" gives the user concrete options while leaving the free-text input available for users who prefer to type.",[20,812,813],{},"The key principle: never claim more capability than you deliver. Users forgive limited capability if it is clearly communicated. They do not forgive capability claims that prove false.",[328,815],{},[15,817,819],{"id":818},"conversation-flow-design","Conversation Flow Design",[20,821,822],{},"Natural conversations have structure, even if that structure is not visible. Designing conversational AI means making that structure explicit.",[20,824,825,828],{},[191,826,827],{},"Slot filling with grace."," Many conversational tasks require collecting specific information: an order number, a product name, a date range. The rigid approach asks for each piece of information in sequence: \"What is your order number?\" then \"Which item?\" then \"What is the issue?\" The natural approach allows users to provide information in any order and in any combination: \"I want to return the blue shirt from order 4521\" provides three pieces of information in one message. The system should extract all three rather than ignoring two and asking for them sequentially.",[20,830,831,834],{},[191,832,833],{},"Context persistence."," If a user says \"I ordered a laptop last week\" and then asks \"when will it arrive?\" the system must connect \"it\" to \"the laptop ordered last week.\" This referential resolution requires maintaining conversation state — tracking entities mentioned earlier and resolving pronouns and references against that state. Without it, every message feels like a new conversation.",[20,836,837,840],{},[191,838,839],{},"Clarification without interrogation."," When the user's input is ambiguous, the system should ask for clarification. But clarification questions should be specific and offer options: \"I found two recent orders — one from March 3 for running shoes and one from March 5 for a jacket. Which one are you asking about?\" is better than \"Can you clarify which order you mean?\" The first helps the user respond quickly. The second puts the burden of disambiguation entirely on the user.",[20,842,843,846],{},[191,844,845],{},"Graceful failure."," The system will encounter inputs it cannot handle. The design for these moments matters more than the design for the happy path. Good failure responses: acknowledge the limitation, explain what the system can do, and offer an alternative path (rephrase, try a different topic, connect with a human). Bad failure responses: \"I didn't understand that. Please try again.\" — which tells the user nothing about why it failed or what to do differently.",[328,848],{},[15,850,852],{"id":851},"voice-specific-design","Voice-Specific Design",[20,854,855],{},"Voice interfaces introduce constraints that text-based chat does not have.",[20,857,858,861],{},[191,859,860],{},"Brevity matters more."," Reading a paragraph on screen takes seconds. Listening to a paragraph takes 30 seconds and the user cannot skim. Voice responses should be concise — answer the question directly, then offer to provide more detail if needed. \"Your order shipped yesterday and should arrive Friday. Want the tracking number?\" is better than a full paragraph about shipping carriers and delivery windows.",[20,863,864,867],{},[191,865,866],{},"Confirmation is critical."," In text, the user can see what they typed and correct mistakes before sending. In voice, the system's interpretation of speech may be wrong. For any action with consequences (placing an order, canceling a subscription), the system must read back its understanding and confirm: \"Just to confirm — you would like to cancel your Premium plan, effective immediately. Is that right?\"",[20,869,870,873],{},[191,871,872],{},"Navigation is invisible."," Text interfaces can show menus, buttons, and links. Voice interfaces cannot. The user must remember the options or the system must repeat them. Keep option lists short (three or fewer) and memorable. For complex workflows, use progressive disclosure: offer the first decision, then the next, rather than presenting the full decision tree upfront.",[20,875,602,876,880],{},[34,877,879],{"href":878},"/blog/ai-chatbot-development-guide","technical architecture for conversational AI"," — LLM selection, retrieval systems, integration with business data — is important. But the design layer that sits on top of that architecture determines whether users find the experience helpful enough to use again. Technology provides the capability. Design provides the experience.",[328,882],{},[20,884,885,886],{},"If you are building a conversational AI experience and want to design it for genuine user satisfaction, ",[34,887,890],{"href":888,"rel":889},"https://calendly.com/jamesrossjr",[124],"let's talk.",[328,892],{},[15,894,541],{"id":540},[217,896,897,902,908,914],{},[220,898,899],{},[34,900,901],{"href":878},"Building AI Chatbots That Actually Help Customers",[220,903,904],{},[34,905,907],{"href":906},"/blog/building-chatbots-for-business","Building Chatbots for Business: A Practical Guide",[220,909,910],{},[34,911,913],{"href":912},"/blog/natural-language-processing-apps","NLP in Production Applications: Practical Patterns",[220,915,916],{},[34,917,919],{"href":918},"/blog/prompt-engineering-for-developers","Prompt Engineering for Developers",{"title":138,"searchDepth":139,"depth":139,"links":921},[922,923,924,925,926],{"id":767,"depth":142,"text":768},{"id":782,"depth":142,"text":783},{"id":818,"depth":142,"text":819},{"id":851,"depth":142,"text":852},{"id":540,"depth":142,"text":541},"AI","The difference between a frustrating chatbot and a helpful assistant is design, not technology. Here are the design patterns that make conversational AI work.",[930,931,932],"conversational ai design","chatbot ux design","conversational interface patterns",{},"/blog/conversational-ai-design",{"title":761,"description":928},"blog/conversational-ai-design",[938,939,940],"Conversational AI","UX Design","AI for Business","OZAK6gIXJ9Itaswspr14J1bW6HzaJsSWpb2GRx0K4tU",{"id":943,"title":944,"author":945,"body":946,"category":359,"date":739,"description":1060,"extension":150,"featured":151,"image":152,"keywords":1061,"meta":1065,"navigation":158,"path":1066,"readTime":1067,"seo":1068,"stem":1069,"tags":1070,"__hash__":1075},"blog/blog/iron-age-celtic-europe.md","Iron Age Celtic Europe: La Tene and Hallstatt Cultures",{"name":9,"bio":10},{"type":12,"value":947,"toc":1054},[948,952,967,970,988,992,995,998,1001,1009,1013,1021,1024,1027,1031,1039,1042],[15,949,951],{"id":950},"before-celtic-meant-anything","Before \"Celtic\" Meant Anything",[20,953,954,955,958,959,962,963,966],{},"The word \"Celtic\" is used so loosely today that it is worth pausing to consider what it originally meant. The Greeks called the peoples of central and western Europe ",[604,956,957],{},"Keltoi",". The Romans called them ",[604,960,961],{},"Galli"," (Gauls) or ",[604,964,965],{},"Celtae",". Neither term referred to a unified nation or a single ethnic group. They were umbrella labels for a vast, diverse population that shared broadly similar languages, art styles, and social structures across a territory stretching from Anatolia to Ireland.",[20,968,969],{},"The archaeological cultures that define this world are Hallstatt and La Tene, named for sites in Austria and Switzerland respectively. Together, they span roughly a thousand years — from about 800 BC to the Roman conquests — and they represent the material evidence for what we call \"Celtic\" civilization.",[20,971,972,973,977,978,982,983,987],{},"Understanding these cultures is essential for understanding the ancestry of the ",[34,974,976],{"href":975},"/blog/scottish-clan-system-explained","Scottish clans",", the origins of the ",[34,979,981],{"href":980},"/blog/scottish-gaelic-language-history","Gaelic languages",", and the deep roots of the ",[34,984,986],{"href":985},"/blog/r1b-l21-atlantic-celtic-haplogroup","R1b-L21 populations"," that dominate the genetics of the British Isles.",[15,989,991],{"id":990},"hallstatt-salt-iron-and-hierarchy","Hallstatt: Salt, Iron, and Hierarchy",[20,993,994],{},"The Hallstatt culture (c. 800-450 BC) takes its name from a salt-mining settlement in the Austrian Alps, where over a thousand graves were excavated in the 19th century. The salt mines had been operating since the Bronze Age, and the wealth they generated funded an elite culture of remarkable sophistication.",[20,996,997],{},"Hallstatt burials reveal a hierarchical society. Wealthy individuals were interred with elaborate grave goods — bronze vessels, iron swords, gold jewelry, and four-wheeled wagons. The most spectacular finds include the Hochdorf burial in Germany, where a Celtic prince was laid on a bronze couch surrounded by drinking horns, a cauldron, and a gold-covered dagger.",[20,999,1000],{},"The Hallstatt economy was based on salt, iron, and long-distance trade. Mediterranean goods — Greek pottery, Etruscan bronzeware, wine amphorae — appear in Hallstatt elite burials, indicating trade networks that connected the Celtic heartland to the classical world. In return, the Celts exported salt, metals, furs, and slaves.",[20,1002,1003,1004,1008],{},"The social structure was dominated by a warrior aristocracy — chiefs and princes who controlled trade routes, commanded labor, and displayed their status through conspicuous consumption. The Hallstatt period laid the foundations for the class structure that would characterize Celtic society throughout the Iron Age: an elite warrior class, a priestly/learned class (the ancestors of the ",[34,1005,1007],{"href":1006},"/blog/druid-tradition-history","druids","), and a producing class of farmers and craftsmen.",[15,1010,1012],{"id":1011},"la-tene-art-expansion-and-conflict","La Tene: Art, Expansion, and Conflict",[20,1014,1015,1016,1020],{},"Around 450 BC, Hallstatt culture was replaced — or evolved into — the La Tene culture, which represents the full flowering of Iron Age Celtic civilization. La Tene art abandoned the geometric patterns of Hallstatt in favor of the flowing, curvilinear designs that define ",[34,1017,1019],{"href":1018},"/blog/celtic-art-symbolism","Celtic art",": abstract plant motifs, animal transformations, and the sinuous, asymmetric compositions that would eventually evolve into the knotwork of the Insular manuscripts.",[20,1022,1023],{},"La Tene was also an age of expansion. Celtic-speaking peoples spread across Europe — into the Iberian Peninsula, the British Isles, the Po Valley, the Balkans, and even Anatolia (where the Galatians preserved a Celtic language into the Roman period). This was not a coordinated invasion but a series of migrations, military adventures, and population movements driven by demographic pressure, climate change, and the search for new land.",[20,1025,1026],{},"The sack of Rome by the Gauls under Brennus in 390 BC was the most dramatic event of this expansion — a trauma that shaped Roman attitudes toward the Celts for centuries. The Celtic attack on Delphi in 279 BC demonstrated that the expansionary impulse extended into the Greek world as well.",[15,1028,1030],{"id":1029},"the-atlantic-fringe","The Atlantic Fringe",[20,1032,1033,1034,1038],{},"For the story of the British Isles, the most important aspect of La Tene culture is what happened at its western edge. The ",[34,1035,1037],{"href":1036},"/blog/bell-beaker-conquest-ireland-britain","Bell Beaker populations"," who had settled Britain and Ireland in the Bronze Age were already genetically and (probably) linguistically Celtic before the La Tene culture emerged. The La Tene influence reached the British Isles not through mass migration but through trade, elite exchange, and cultural diffusion.",[20,1040,1041],{},"This distinction matters because it undermines the old model of \"Celtic invasions\" of Britain. The peoples who built the hill forts, forged the iron swords, and created the La Tene-influenced art of Iron Age Britain were not newcomers from the continent. They were the descendants of populations that had been in the islands for two thousand years, adopting new styles and technologies from their continental cousins while maintaining a deep genetic continuity.",[20,1043,602,1044,1048,1049,1053],{},[34,1045,1047],{"href":1046},"/blog/pictish-kingdoms-scotland","Picts",", the Britons, and the Irish of the Iron Age were all products of this Atlantic Celtic world — connected to the continent by trade and cultural exchange but rooted in a local population that traced its ancestry to the Bronze Age and beyond. When Gaelic speakers later crossed from Ireland to Scotland via ",[34,1050,1052],{"href":1051},"/blog/dal-riata-irish-kingdom-created-scotland","Dal Riata",", they were not introducing Celtic culture to a non-Celtic land. They were bringing one version of Celtic culture to a territory that already had its own.",{"title":138,"searchDepth":139,"depth":139,"links":1055},[1056,1057,1058,1059],{"id":950,"depth":142,"text":951},{"id":990,"depth":142,"text":991},{"id":1011,"depth":142,"text":1012},{"id":1029,"depth":142,"text":1030},"The Hallstatt and La Tene cultures defined Celtic Europe for a thousand years. Their art, warfare, and trade networks shaped the continent before Rome.",[1062,1063,1064],"iron age celtic europe","la tene culture","hallstatt culture celts",{},"/blog/iron-age-celtic-europe",6,{"title":944,"description":1060},"blog/iron-age-celtic-europe",[1071,1072,1073,1074],"Iron Age","Celtic Europe","La Tene","Hallstatt","Nz5r8WwM9i498TvUjOOOVsJMugbnSsKAJSsWcmMA_I8",{"id":1077,"title":1078,"author":1079,"body":1080,"category":359,"date":739,"description":1275,"extension":150,"featured":151,"image":152,"keywords":1276,"meta":1282,"navigation":158,"path":1283,"readTime":160,"seo":1284,"stem":1285,"tags":1286,"__hash__":1291},"blog/blog/scottish-diaspora-world.md","The Scottish Diaspora: How Scotland Seeded the World",{"name":9,"bio":10},{"type":12,"value":1081,"toc":1265},[1082,1086,1089,1096,1099,1103,1106,1112,1122,1128,1132,1135,1141,1150,1156,1160,1163,1169,1175,1181,1185,1188,1192,1195,1201,1207,1217,1223,1227,1235,1242,1244,1246],[15,1083,1085],{"id":1084},"a-small-country-with-a-long-reach","A Small Country With a Long Reach",[20,1087,1088],{},"Scotland's population has never exceeded six million. Yet an estimated forty to fifty million people worldwide claim Scottish descent. The disproportion tells a story: for over three centuries, Scotland exported its people at a rate that few nations of its size have matched.",[20,1090,1091,1092,1095],{},"The Scottish diaspora was not a single movement but a series of overlapping migrations driven by different forces at different times -- religious persecution, economic hardship, the ",[34,1093,1094],{"href":729},"Highland Clearances",", the pull of colonial opportunity, and the systematic displacement of a Gaelic-speaking rural population by an industrializing economy that had no place for them.",[20,1097,1098],{},"The result is a global network of Scottish-descended communities stretching from Cape Breton to Dunedin, from Appalachia to the Australian outback. Each community carries fragments of the culture that was displaced -- sometimes preserved more faithfully than in Scotland itself.",[15,1100,1102],{"id":1101},"canada-new-scotland","Canada: New Scotland",[20,1104,1105],{},"No country received more Highland Scottish emigrants than Canada. Nova Scotia -- literally \"New Scotland\" -- was the primary destination from the late eighteenth century onward.",[20,1107,1108,1111],{},[191,1109,1110],{},"Cape Breton Island"," received particularly large numbers of Gaelic-speaking Highlanders, creating a community where Scottish Gaelic was spoken as a community language until the late twentieth century. The Gaelic College of Celtic Arts and Crafts in St. Ann's Bay, founded in 1938, continues to maintain Highland cultural traditions. Cape Breton fiddle music -- a direct descendant of Highland musical traditions -- is recognized as one of the most vital living folk music traditions in North America.",[20,1113,1114,1117,1118,1121],{},[191,1115,1116],{},"Prince Edward Island"," and ",[191,1119,1120],{},"Ontario"," also received significant Scottish settlement. The counties of eastern Ontario -- Glengarry, Stormont, Dundas -- were settled by Highland Scots in sufficient numbers that Gaelic was the dominant language of some townships into the late nineteenth century.",[20,1123,1124,1127],{},[191,1125,1126],{},"The Red River Settlement"," in Manitoba, established in 1812 by the Earl of Selkirk, was specifically designed to receive displaced Highland families. The settlement endured extraordinary hardships but survived, contributing to the foundation of modern Manitoba.",[15,1129,1131],{"id":1130},"the-united-states","The United States",[20,1133,1134],{},"Scottish emigration to America began well before the Clearances and continued long after. The Scottish imprint on American culture is deep but often invisible, having been absorbed into the broader fabric of American identity.",[20,1136,1137,1140],{},[191,1138,1139],{},"The colonial period."," Highland Scots settled the Cape Fear Valley of North Carolina from the 1730s onward. These communities were substantial enough that Gaelic was spoken in North Carolina into the early nineteenth century. Many Highland settlers supported the Loyalist cause during the American Revolution and subsequently relocated to Canada.",[20,1142,1143,1149],{},[191,1144,602,1145,194],{},[34,1146,1148],{"href":1147},"/blog/scots-irish-appalachia","Scots-Irish"," The largest Scottish-descended migration to America was not directly from Scotland but through Ireland. The Ulster Scots -- Lowland Scots who had been planted in northern Ireland in the seventeenth century -- emigrated to the American colonies in enormous numbers during the eighteenth century, settling heavily in the Appalachian backcountry.",[20,1151,1152,1155],{},[191,1153,1154],{},"The nineteenth century."," Post-Clearance emigration, the California Gold Rush, and the general pull of American opportunity brought Scottish emigrants to every region of the United States. Scottish surnames are concentrated in the American South and Midwest, reflecting the settlement patterns of the eighteenth and nineteenth centuries.",[15,1157,1159],{"id":1158},"australia-and-new-zealand","Australia and New Zealand",[20,1161,1162],{},"Scottish emigrants played a disproportionate role in the settlement of Australia and New Zealand, particularly from the 1830s onward.",[20,1164,1165,1168],{},[191,1166,1167],{},"Victoria"," received large numbers of Scottish settlers during the 1850s gold rush. The Highland society of Victoria, established in 1856, is one of the oldest Scottish cultural organizations in the Southern Hemisphere.",[20,1170,1171,1174],{},[191,1172,1173],{},"Otago"," in New Zealand was founded in 1848 as a specifically Scottish settlement, organized by the Free Church of Scotland. The city of Dunedin -- its name the Gaelic form of Edinburgh -- was planned as a New Edinburgh in the South Pacific. The Scottish character of Otago persisted well into the twentieth century.",[20,1176,1177,1180],{},[191,1178,1179],{},"Tasmania and South Australia"," also received Highland emigrants, some cleared from their lands, others transported as convicts for offences related to the resistance against eviction.",[15,1182,1184],{"id":1183},"southern-africa","Southern Africa",[20,1186,1187],{},"Scottish missionaries, soldiers, and settlers left a significant mark on southern Africa. The Scottish missionary tradition -- centered on education and Presbyterianism -- established institutions across southern and eastern Africa that still operate today. David Livingstone, the most famous Scottish explorer of the Victorian era, is only the most prominent example of a broad pattern of Scottish engagement with Africa.",[15,1189,1191],{"id":1190},"what-they-carried","What They Carried",[20,1193,1194],{},"The Scottish diaspora communities preserved cultural elements that sometimes survived longer abroad than at home:",[20,1196,1197,1200],{},[191,1198,1199],{},"Language."," Scottish Gaelic survived as a community language in Cape Breton until the late twentieth century, generations after it had retreated to the western Highlands in Scotland itself.",[20,1202,1203,1206],{},[191,1204,1205],{},"Music."," The fiddle and bagpipe traditions of the Highlands were carried to every diaspora community and evolved independently. Cape Breton fiddle, Appalachian fiddling, and the pipe band tradition all represent distinct diaspora developments of Highland musical culture.",[20,1208,1209,1212,1213,1216],{},[191,1210,1211],{},"Clan identity."," Highland games, ",[34,1214,1215],{"href":975},"clan associations",", and tartan societies flourished in the diaspora, often with more enthusiasm and organization than in Scotland. The diaspora kept clan identity alive through two centuries of assimilation pressure.",[20,1218,1219,1222],{},[191,1220,1221],{},"Education."," The Scottish tradition of parish education -- the democratic belief that every child should be literate -- was exported with the emigrants and contributed to the educational infrastructure of every country they settled in. The disproportionate Scottish contribution to the founding of universities, schools, and libraries across the English-speaking world is one of the most consistent patterns in diaspora history.",[15,1224,1226],{"id":1225},"tracing-the-diaspora","Tracing the Diaspora",[20,1228,1229,1230,1234],{},"For anyone with Scottish ancestry, the diaspora pattern determines which records to search and where. A ",[34,1231,1233],{"href":1232},"/blog/ross-surname-origin-meaning","Ross"," who emigrated to Nova Scotia in 1803 left a different paper trail than a Ross who reached North Carolina in 1750 or a Ross who landed in Melbourne in 1855.",[20,1236,1237,1238,1241],{},"The starting point is always the Scottish end: the parish, the estate, the port of departure. From there, ship records, immigration records, and colonial censuses track the passage to the new world. And beneath all the paper, the ",[34,1239,1240],{"href":200},"DNA"," carries its own record -- connecting a Ross in Texas to a Ross in Nova Scotia to a Ross in Easter Ross through an unbroken chain of Y-chromosome inheritance.",[328,1243],{},[15,1245,333],{"id":332},[217,1247,1248,1253,1259],{},[220,1249,1250],{},[34,1251,1252],{"href":729},"The Highland Clearances and Clan Ross: How a People Were Scattered",[220,1254,1255],{},[34,1256,1258],{"href":1257},"/blog/scottish-immigration-america","Scottish Immigration to America: Waves and Patterns",[220,1260,1261],{},[34,1262,1264],{"href":1263},"/blog/clan-ross-in-america","Clan Ross in America: Tracing the Diaspora",{"title":138,"searchDepth":139,"depth":139,"links":1266},[1267,1268,1269,1270,1271,1272,1273,1274],{"id":1084,"depth":142,"text":1085},{"id":1101,"depth":142,"text":1102},{"id":1130,"depth":142,"text":1131},{"id":1158,"depth":142,"text":1159},{"id":1183,"depth":142,"text":1184},{"id":1190,"depth":142,"text":1191},{"id":1225,"depth":142,"text":1226},{"id":332,"depth":142,"text":333},"From the Highland Clearances to the empire's far reaches, Scottish emigrants built communities on every continent. Here is the story of the Scottish diaspora -- where they went, what they carried with them, and the cultural legacy they planted across the globe.",[1277,1278,1279,1280,1281],"scottish diaspora","scottish emigration history","scots around the world","scottish settlers america","scottish heritage diaspora",{},"/blog/scottish-diaspora-world",{"title":1078,"description":1275},"blog/scottish-diaspora-world",[1287,1288,1094,1289,1290],"Scottish Diaspora","Scottish Emigration","Nova Scotia","Scottish Heritage","jPK9iTXRiFsMo-ahGtn8S2V4YazQmYYnWOQo49rUOUQ",{"id":1293,"title":1294,"author":1295,"body":1296,"category":147,"date":739,"description":1445,"extension":150,"featured":151,"image":152,"keywords":1446,"meta":1449,"navigation":158,"path":1450,"readTime":160,"seo":1451,"stem":1452,"tags":1453,"__hash__":1457},"blog/blog/software-development-contracts.md","Software Development Contracts: What to Include",{"name":9,"bio":10},{"type":12,"value":1297,"toc":1438},[1298,1302,1305,1308,1312,1315,1318,1321,1329,1333,1336,1342,1348,1354,1358,1361,1367,1370,1376,1382,1386,1396,1402,1408,1412,1418,1429,1435],[1299,1300,1294],"h1",{"id":1301},"software-development-contracts-what-to-include",[20,1303,1304],{},"Every software development engagement should be governed by a written contract. I have seen projects where a handshake and a vague email thread were the only agreements, and every one of those projects ended in a dispute — about scope, about payment, about who owns the code, about what \"done\" means.",[20,1306,1307],{},"A good contract is not adversarial. It is a shared understanding between two parties about what will be done, how it will be done, what it costs, and what happens when things do not go as planned. Both the client and the developer benefit from clarity, because ambiguity always favors the party who is willing to litigate.",[15,1309,1311],{"id":1310},"scope-of-work","Scope of Work",[20,1313,1314],{},"The scope defines what the developer will build. This is the section that prevents scope creep and mismatched expectations, and it is where most contract disputes originate.",[20,1316,1317],{},"Write the scope in terms of deliverables, not activities. \"The developer will build an e-commerce checkout flow supporting Stripe payments, guest checkout, and order confirmation emails\" is a deliverable. \"The developer will work on the e-commerce features\" is an activity description that leaves too much room for disagreement about what is included.",[20,1319,1320],{},"Include acceptance criteria for each deliverable. How will both parties agree that a deliverable is complete? Acceptance criteria should be specific and testable: \"Users can complete a purchase with a test credit card, receive an order confirmation email within sixty seconds, and see the order in their order history.\" Without acceptance criteria, \"complete\" is a matter of opinion.",[20,1322,1323,1324,1328],{},"Define the change order process. Requirements will change — that is normal in software development. The contract should specify how changes are proposed, evaluated for impact, priced, and approved. A simple change order process includes a written description of the change, an estimate of additional time and cost, and written approval before work begins. For strategies on managing scope changes, the ",[34,1325,1327],{"href":1326},"/blog/scope-creep-prevention","scope creep prevention guide"," covers the operational side.",[15,1330,1332],{"id":1331},"intellectual-property","Intellectual Property",[20,1334,1335],{},"Intellectual property ownership is the most important clause in a software development contract. Without explicit assignment, the developer may retain ownership of code they write — even if you paid for it. IP law varies by jurisdiction, but the safest approach is an explicit assignment clause.",[20,1337,1338,1341],{},[191,1339,1340],{},"Work product assignment."," The contract should state that all code, documentation, designs, and other work product created during the engagement are assigned to the client upon payment. This includes source code, database schemas, API designs, and any other deliverables.",[20,1343,1344,1347],{},[191,1345,1346],{},"Pre-existing IP."," Developers often use frameworks, libraries, and tools they created before the engagement. The contract should allow the developer to retain ownership of pre-existing IP while granting the client a perpetual, royalty-free license to use it as part of the delivered work product. List any significant pre-existing IP components explicitly.",[20,1349,1350,1353],{},[191,1351,1352],{},"Open-source components."," Custom software almost always includes open-source libraries. The contract should require the developer to disclose all open-source components and their licenses. Some open-source licenses (like GPL) have copyleft provisions that could affect the client's ability to use the software as proprietary. Require disclosure so there are no surprises.",[15,1355,1357],{"id":1356},"payment-terms","Payment Terms",[20,1359,1360],{},"Payment structure should align incentives and protect both parties from disproportionate risk.",[20,1362,1363,1366],{},[191,1364,1365],{},"Milestone-based payments"," are the most balanced structure. The project is divided into phases, each with defined deliverables and a payment amount. The client pays as value is delivered. The developer receives revenue as they complete work. Neither party has excessive exposure.",[20,1368,1369],{},"A typical structure: 20% upfront (to cover project initialization and demonstrate client commitment), 30% at first major milestone, 30% at second major milestone, 20% at final delivery and acceptance. Adjust the percentages based on project duration and risk profile.",[20,1371,1372,1375],{},[191,1373,1374],{},"Payment timing."," Specify when payment is due — net 15 or net 30 from invoice date is standard. Include provisions for late payment — interest charges, suspension of work, or both. A developer who continues working while invoices go unpaid for months is absorbing financial risk that the contract should address.",[20,1377,1378,1381],{},[191,1379,1380],{},"Kill fee."," If the client terminates the project early, what happens? The developer has allocated capacity and possibly turned down other work. A kill fee — typically payment for work completed plus 10-25% of the remaining contract value — compensates the developer for the disruption. Without a kill fee, clients can terminate without consequence, and developers bear all the risk of cancellation.",[15,1383,1385],{"id":1384},"warranties-and-liability","Warranties and Liability",[20,1387,1388,1391,1392,194],{},[191,1389,1390],{},"Warranty period."," The developer should warrant that the delivered software will function in accordance with the acceptance criteria for a defined period after delivery — typically 30 to 90 days. During the warranty period, the developer fixes bugs at no additional cost. After the warranty period, bug fixes are billed separately, typically under a ",[34,1393,1395],{"href":1394},"/blog/software-maintenance-planning","maintenance agreement",[20,1397,1398,1401],{},[191,1399,1400],{},"Limitation of liability."," Both parties should agree to limit liability to the total contract value. Without a limitation clause, a developer could theoretically be liable for consequential damages — lost revenue, lost customers, lost opportunities — that far exceed what they were paid. This is disproportionate risk that makes the engagement untenable for the developer.",[20,1403,1404,1407],{},[191,1405,1406],{},"Indemnification."," The developer should indemnify the client against third-party IP claims — if someone claims the delivered code infringes their patent or copyright, the developer is responsible. The client should indemnify the developer against claims arising from the client's use of the software — if the software is used in a way that harms a third party, that is the client's responsibility.",[15,1409,1411],{"id":1410},"confidentiality-and-non-compete","Confidentiality and Non-Compete",[20,1413,1414,1417],{},[191,1415,1416],{},"Confidentiality."," Both parties will share sensitive information during the engagement — business plans, customer data, proprietary processes, source code. A mutual confidentiality clause requires both parties to protect the other's confidential information and limits its use to the purposes of the engagement.",[20,1419,1420,1423,1424,1428],{},[191,1421,1422],{},"Non-compete clauses"," should be narrow and reasonable. A clause preventing the developer from building any software in the client's industry for two years is unreasonable and likely unenforceable. A clause preventing the developer from building a directly competing product using the client's proprietary business logic for twelve months is more reasonable. For guidance on how ",[34,1425,1427],{"href":1426},"/blog/pricing-software-projects","pricing models"," affect contract structures, that guide covers the financial framework.",[20,1430,1431,1434],{},[191,1432,1433],{},"Portfolio rights."," Developers often want to reference completed projects in their portfolio. The contract should specify whether this is permitted and what information can be shared. A clause that allows the developer to mention the client by name and describe the project at a high level, without sharing proprietary details, is a reasonable middle ground.",[20,1436,1437],{},"Every clause in a software development contract exists because someone, somewhere, had a dispute about exactly that issue. The time you invest in a thorough contract saves multiples of that time in prevented misunderstandings, avoided disputes, and preserved relationships.",{"title":138,"searchDepth":139,"depth":139,"links":1439},[1440,1441,1442,1443,1444],{"id":1310,"depth":142,"text":1311},{"id":1331,"depth":142,"text":1332},{"id":1356,"depth":142,"text":1357},{"id":1384,"depth":142,"text":1385},{"id":1410,"depth":142,"text":1411},"A good contract protects both parties and prevents disputes. Here's what every software development contract should cover — from IP to payment to liability.",[1447,1448],"software development contract","software development agreement",{},"/blog/software-development-contracts",{"title":1294,"description":1445},"blog/software-development-contracts",[1454,1455,1456],"Contracts","Legal","Project Management","qXRqg421MwaRv6lGLNRRv944VHQks5-xVixGT4rnNYw",{"id":1459,"title":1460,"author":1461,"body":1462,"category":147,"date":739,"description":1564,"extension":150,"featured":151,"image":152,"keywords":1565,"meta":1568,"navigation":158,"path":1569,"readTime":160,"seo":1570,"stem":1571,"tags":1572,"__hash__":1575},"blog/blog/software-licensing-guide.md","Software Licensing: Choosing the Right Model",{"name":9,"bio":10},{"type":12,"value":1463,"toc":1558},[1464,1468,1471,1474,1477,1479,1483,1489,1495,1501,1507,1518,1520,1524,1527,1530,1536,1538,1542,1545,1548,1551],[15,1465,1467],{"id":1466},"licensing-defines-your-business-not-just-your-legal-terms","Licensing Defines Your Business, Not Just Your Legal Terms",[20,1469,1470],{},"Most developers think about licensing as a legal formality — something to attach to the repository before shipping. In reality, your licensing model determines how you make money, how customers perceive your value, how you compete, and how your business scales. It's a business strategy decision that happens to have legal expression.",[20,1472,1473],{},"Choosing the wrong licensing model creates friction that compounds over time. A SaaS model for software that enterprises want to run on-premises loses those deals entirely. A perpetual license for a product that requires continuous infrastructure costs puts you on a treadmill of constant new-customer acquisition. A per-seat model for a tool that's most valuable when adopted across an entire organization creates incentives for customers to limit adoption.",[20,1475,1476],{},"The licensing model should align with how your customers derive value from the software. When the pricing mechanism mirrors the value mechanism, sales conversations become simpler, customers feel the pricing is fair, and revenue grows naturally with customer success.",[328,1478],{},[15,1480,1482],{"id":1481},"the-models-that-dominate-today","The Models That Dominate Today",[20,1484,1485,1488],{},[191,1486,1487],{},"SaaS subscription"," is the default for cloud-delivered software and for good reason. Recurring revenue is predictable, updates are automatic, and the model aligns vendor and customer incentives — the vendor is motivated to keep the product valuable because cancellation is easy. Monthly or annual billing, tiered by features or usage, is the most common structure. The challenge is churn: every customer is one bad month away from cancellation, which makes customer success and retention as important as acquisition.",[20,1490,1491,1494],{},[191,1492,1493],{},"Usage-based pricing"," charges customers based on how much they use the product — API calls, data processed, compute minutes, messages sent. This model scales naturally with customer success: as they grow, you grow. It also has the lowest barrier to entry because new customers can start small and pay little. The downside is revenue unpredictability. Usage can drop during seasonal slowdowns, customer budget cuts, or competitive displacement. Stripe, AWS, and Twilio have proven this model at scale, but it requires infrastructure for metering and billing that adds engineering complexity.",[20,1496,1497,1500],{},[191,1498,1499],{},"Perpetual licensing"," — a one-time purchase with optional maintenance agreements — has declined significantly but remains appropriate for software that runs on-premises in regulated or air-gapped environments. Healthcare, defense, and manufacturing clients often require this model. The business challenge is the constant need for new customers, since existing customers aren't generating recurring revenue. Maintenance agreements (typically 15-20% of license cost annually) provide some recurring income but don't match SaaS-level predictability.",[20,1502,1503,1506],{},[191,1504,1505],{},"Freemium"," offers a free tier with limited functionality and charges for premium features. This is a customer acquisition strategy as much as a pricing model. The free tier needs to provide genuine value — enough that users become dependent on the product — while reserving enough premium value that the upgrade is compelling. The conversion rate from free to paid is typically 2-5%, which means you need a large free user base to generate significant revenue. This model works best when the marginal cost of free users is near zero and when free usage creates network effects or viral distribution.",[20,1508,1509,1512,1513,1517],{},[191,1510,1511],{},"Open source with commercial offerings"," deserves its own consideration. I've explored this in depth in my article on ",[34,1514,1516],{"href":1515},"/blog/open-source-business-strategy","open source as a business strategy",". The licensing implications here are nuanced — your choice of open source license (MIT, Apache, AGPL, SSPL) directly affects what competitors and cloud providers can do with your code.",[328,1519],{},[15,1521,1523],{"id":1522},"matching-the-model-to-your-market","Matching the Model to Your Market",[20,1525,1526],{},"Enterprise software buyers evaluate licensing models differently than individual developers or small businesses. Enterprise procurement teams prefer annual contracts because they align with budget cycles. They want volume discounts for large deployments. They expect dedicated support tiers. And they often have specific requirements around data residency, uptime SLAs, and compliance that influence which licensing models are even feasible.",[20,1528,1529],{},"Developer tools and small-business software benefit from self-serve purchasing with low initial commitment. Monthly subscriptions, usage-based pricing, or freemium models work because the buyer has authority to make the decision without a procurement process. Speed from discovery to payment should be measured in minutes, not weeks.",[20,1531,1532,1533,1535],{},"Vertical SaaS — software built for a specific industry — often supports premium pricing because the alternatives are either generic tools that don't fit or legacy systems that are expensive to replace. If your ",[34,1534,61],{"href":60}," serves a niche market, you can price based on the value delivered rather than the cost of alternatives.",[328,1537],{},[15,1539,1541],{"id":1540},"evolving-your-model-over-time","Evolving Your Model Over Time",[20,1543,1544],{},"Your licensing model isn't permanent. As your market position changes, your customer base shifts, and your product capabilities expand, your licensing model should evolve too.",[20,1546,1547],{},"The most common evolution is from simple to hybrid. A SaaS product that starts with flat-rate pricing adds a usage-based component as customers' usage patterns diverge. A freemium product adds an enterprise tier with annual contracts. A usage-based product adds committed-use discounts for customers who want budget predictability.",[20,1549,1550],{},"When changing pricing models for existing customers, transparency and gradual transition matter enormously. Grandfather existing customers at their current terms for a reasonable period. Communicate the change early and explain the reasoning. Provide tools or dashboards that help customers understand how the new pricing affects them.",[20,1552,1553,1554,1557],{},"The licensing decision is inseparable from the ",[34,1555,1556],{"href":64},"broader business strategy",". Revenue model, go-to-market strategy, competitive positioning, and customer success approach all interlock. Choose a licensing model that serves the business you're building, not just the software you've built.",{"title":138,"searchDepth":139,"depth":139,"links":1559},[1560,1561,1562,1563],{"id":1466,"depth":142,"text":1467},{"id":1481,"depth":142,"text":1482},{"id":1522,"depth":142,"text":1523},{"id":1540,"depth":142,"text":1541},"A practical guide to software licensing models for developers and businesses. SaaS, perpetual, open source, freemium, and usage-based licensing compared clearly.",[1566,1567],"software licensing models","software licensing guide",{},"/blog/software-licensing-guide",{"title":1460,"description":1564},"blog/software-licensing-guide",[1573,1574,579],"Software Licensing","Business Models","MLHHP_PkMKjDs9zhYaRaCA24W7ppDktzkdFuWD4y1NU",{"id":1577,"title":1578,"author":1579,"body":1580,"category":2174,"date":739,"description":2175,"extension":150,"featured":151,"image":152,"keywords":2176,"meta":2179,"navigation":158,"path":2180,"readTime":160,"seo":2181,"stem":2182,"tags":2183,"__hash__":2186},"blog/blog/web-forms-best-practices.md","Form Design Patterns That Improve Conversion Rates",{"name":9,"bio":10},{"type":12,"value":1581,"toc":2167},[1582,1586,1589,1592,1595,1597,1601,1609,1612,1731,1745,1762,1765,1767,1771,1774,1780,1786,1797,1803,1955,1973,1975,1979,1982,1985,1988,1995,1998,2000,2004,2007,2010,2157,2160,2163],[15,1583,1585],{"id":1584},"forms-are-where-users-give-up","Forms Are Where Users Give Up",[20,1587,1588],{},"Every web application has forms. Sign-up forms, checkout forms, contact forms, search forms, settings forms. Forms are the primary mechanism through which users provide data to your application, and they are the most common point of abandonment. The average form abandonment rate across industries is 67%, meaning two-thirds of users who start filling out a form never submit it.",[20,1590,1591],{},"That number is not inevitable — it reflects bad form design. Forms that ask for too much information, provide confusing validation, use poor input types, or create anxiety about what happens after submission drive users away. The engineering choices behind a form matter as much as the visual design: input type selection, validation timing, error message clarity, and submission feedback all affect whether a user completes the form or gives up.",[20,1593,1594],{},"Good form engineering is invisible. The user fills out fields, each field works as expected, errors are clear and helpful, and submission provides immediate feedback. Bad form engineering is very visible — unexpected input formatting, error messages that appear after submission rather than inline, dropdowns for data better served by text inputs, and submission states that leave users wondering if their click registered.",[328,1596],{},[15,1598,1600],{"id":1599},"input-design-that-reduces-friction","Input Design That Reduces Friction",[20,1602,1603,1604,1608],{},"Every form field creates friction. The most impactful optimization is removing fields entirely. Do you need the user's phone number on a newsletter signup form? Do you need their company name on a contact form? Each additional field reduces completion rates by approximately 5-10%. The ",[34,1605,1607],{"href":1606},"/blog/landing-page-optimization","landing page principle"," applies: the form should contain the minimum fields required to accomplish its purpose.",[20,1610,1611],{},"For the fields that remain, use the correct HTML input type. This is not just semantics — the input type determines which keyboard appears on mobile devices, which browser autocomplete suggestions appear, and which built-in validation applies.",[1613,1614,1618],"pre",{"className":1615,"code":1616,"language":1617,"meta":138,"style":138},"language-html shiki shiki-themes github-dark","\u003Cinput type=\"email\" inputmode=\"email\" autocomplete=\"email\" />\n\u003Cinput type=\"tel\" inputmode=\"tel\" autocomplete=\"tel\" />\n\u003Cinput type=\"url\" inputmode=\"url\" />\n\u003Cinput type=\"number\" inputmode=\"numeric\" />\n","html",[428,1619,1620,1660,1687,1708],{"__ignoreMap":138},[797,1621,1624,1628,1632,1636,1639,1643,1646,1648,1650,1653,1655,1657],{"class":1622,"line":1623},"line",1,[797,1625,1627],{"class":1626},"s95oV","\u003C",[797,1629,1631],{"class":1630},"s4JwU","input",[797,1633,1635],{"class":1634},"svObZ"," type",[797,1637,1638],{"class":1626},"=",[797,1640,1642],{"class":1641},"sU2Wk","\"email\"",[797,1644,1645],{"class":1634}," inputmode",[797,1647,1638],{"class":1626},[797,1649,1642],{"class":1641},[797,1651,1652],{"class":1634}," autocomplete",[797,1654,1638],{"class":1626},[797,1656,1642],{"class":1641},[797,1658,1659],{"class":1626}," />\n",[797,1661,1662,1664,1666,1668,1670,1673,1675,1677,1679,1681,1683,1685],{"class":1622,"line":142},[797,1663,1627],{"class":1626},[797,1665,1631],{"class":1630},[797,1667,1635],{"class":1634},[797,1669,1638],{"class":1626},[797,1671,1672],{"class":1641},"\"tel\"",[797,1674,1645],{"class":1634},[797,1676,1638],{"class":1626},[797,1678,1672],{"class":1641},[797,1680,1652],{"class":1634},[797,1682,1638],{"class":1626},[797,1684,1672],{"class":1641},[797,1686,1659],{"class":1626},[797,1688,1689,1691,1693,1695,1697,1700,1702,1704,1706],{"class":1622,"line":139},[797,1690,1627],{"class":1626},[797,1692,1631],{"class":1630},[797,1694,1635],{"class":1634},[797,1696,1638],{"class":1626},[797,1698,1699],{"class":1641},"\"url\"",[797,1701,1645],{"class":1634},[797,1703,1638],{"class":1626},[797,1705,1699],{"class":1641},[797,1707,1659],{"class":1626},[797,1709,1711,1713,1715,1717,1719,1722,1724,1726,1729],{"class":1622,"line":1710},4,[797,1712,1627],{"class":1626},[797,1714,1631],{"class":1630},[797,1716,1635],{"class":1634},[797,1718,1638],{"class":1626},[797,1720,1721],{"class":1641},"\"number\"",[797,1723,1645],{"class":1634},[797,1725,1638],{"class":1626},[797,1727,1728],{"class":1641},"\"numeric\"",[797,1730,1659],{"class":1626},[20,1732,602,1733,1736,1737,1740,1741,1744],{},[428,1734,1735],{},"inputmode"," attribute gives additional control over the mobile keyboard. ",[428,1738,1739],{},"inputmode=\"numeric\""," shows a number pad without the spinner controls that ",[428,1742,1743],{},"type=\"number\""," adds. This is ideal for inputs like credit card numbers, verification codes, and ZIP codes that are numeric but not mathematical quantities.",[20,1746,1747,1750,1751,1754,1755,1754,1758,1761],{},[428,1748,1749],{},"autocomplete"," attributes let the browser fill in stored information automatically. Properly labeled autocomplete fields — ",[428,1752,1753],{},"autocomplete=\"given-name\"",", ",[428,1756,1757],{},"autocomplete=\"address-line1\"",[428,1759,1760],{},"autocomplete=\"cc-number\""," — reduce form completion time dramatically. Users who can auto-fill a checkout form in 3 seconds instead of 90 seconds are far more likely to complete the purchase.",[20,1763,1764],{},"Use radio buttons or segmented controls for 2-4 options. Use select dropdowns for 5-15 options. Use searchable autocomplete inputs for more than 15 options. Never use a dropdown for two options (yes/no, male/female) — that requires three interactions (click to open, scroll to option, click to select) for something that should be a single click.",[328,1766],{},[15,1768,1770],{"id":1769},"validation-that-helps-instead-of-punishes","Validation That Helps Instead of Punishes",[20,1772,1773],{},"Form validation is where the gap between good and bad user experience is widest. Bad validation punishes users for mistakes. Good validation prevents mistakes and helps users fix them.",[20,1775,1776,1779],{},[191,1777,1778],{},"Validate on blur, not on change."," Showing \"Invalid email\" while the user is mid-keystroke typing \"jane@exam\" is hostile. Wait until the user moves focus away from the field (the blur event) before validating. At that point, they have indicated they are finished with the field, and validation feedback is useful.",[20,1781,1782,1785],{},[191,1783,1784],{},"The exception: validate on change after an error."," Once a field has been flagged as invalid, switch to validating on each keystroke so the user sees their fix take effect in real time. This is the pattern that Zod-based validation libraries like VeeValidate and React Hook Form implement well.",[20,1787,1788,1791,1792,1796],{},[191,1789,1790],{},"Error messages must be specific and actionable."," \"Invalid input\" tells the user nothing. \"Please enter an email address (e.g., ",[34,1793,1795],{"href":1794},"mailto:name@example.com","name@example.com",")\" tells them exactly what is expected. \"Password must be at least 8 characters\" is better than \"Password too short.\" Include the requirement in the message, not just the failure.",[20,1798,1799,1802],{},[191,1800,1801],{},"Position error messages directly below the field."," Users scan forms top to bottom. An error message at the top of the form (\"Please fix the errors below\") forces the user to hunt for the problem. An error message directly below the problematic field is immediately visible in context.",[1613,1804,1806],{"className":1615,"code":1805,"language":1617,"meta":138,"style":138},"\u003Cdiv class=\"field-group\">\n \u003Clabel for=\"email\">Email address\u003C/label>\n \u003Cinput\n id=\"email\"\n type=\"email\"\n aria-describedby=\"email-error\"\n aria-invalid=\"true\"\n />\n \u003Cp id=\"email-error\" class=\"error\" role=\"alert\">\n Please enter a valid email address.\n \u003C/p>\n\u003C/div>\n",[428,1807,1808,1826,1848,1855,1865,1874,1884,1894,1899,1929,1935,1945],{"__ignoreMap":138},[797,1809,1810,1812,1815,1818,1820,1823],{"class":1622,"line":1623},[797,1811,1627],{"class":1626},[797,1813,1814],{"class":1630},"div",[797,1816,1817],{"class":1634}," class",[797,1819,1638],{"class":1626},[797,1821,1822],{"class":1641},"\"field-group\"",[797,1824,1825],{"class":1626},">\n",[797,1827,1828,1831,1834,1837,1839,1841,1844,1846],{"class":1622,"line":142},[797,1829,1830],{"class":1626}," \u003C",[797,1832,1833],{"class":1630},"label",[797,1835,1836],{"class":1634}," for",[797,1838,1638],{"class":1626},[797,1840,1642],{"class":1641},[797,1842,1843],{"class":1626},">Email address\u003C/",[797,1845,1833],{"class":1630},[797,1847,1825],{"class":1626},[797,1849,1850,1852],{"class":1622,"line":139},[797,1851,1830],{"class":1626},[797,1853,1854],{"class":1630},"input\n",[797,1856,1857,1860,1862],{"class":1622,"line":1710},[797,1858,1859],{"class":1634}," id",[797,1861,1638],{"class":1626},[797,1863,1864],{"class":1641},"\"email\"\n",[797,1866,1868,1870,1872],{"class":1622,"line":1867},5,[797,1869,1635],{"class":1634},[797,1871,1638],{"class":1626},[797,1873,1864],{"class":1641},[797,1875,1876,1879,1881],{"class":1622,"line":1067},[797,1877,1878],{"class":1634}," aria-describedby",[797,1880,1638],{"class":1626},[797,1882,1883],{"class":1641},"\"email-error\"\n",[797,1885,1886,1889,1891],{"class":1622,"line":160},[797,1887,1888],{"class":1634}," aria-invalid",[797,1890,1638],{"class":1626},[797,1892,1893],{"class":1641},"\"true\"\n",[797,1895,1897],{"class":1622,"line":1896},8,[797,1898,1659],{"class":1626},[797,1900,1901,1903,1905,1907,1909,1912,1914,1916,1919,1922,1924,1927],{"class":1622,"line":750},[797,1902,1830],{"class":1626},[797,1904,20],{"class":1630},[797,1906,1859],{"class":1634},[797,1908,1638],{"class":1626},[797,1910,1911],{"class":1641},"\"email-error\"",[797,1913,1817],{"class":1634},[797,1915,1638],{"class":1626},[797,1917,1918],{"class":1641},"\"error\"",[797,1920,1921],{"class":1634}," role",[797,1923,1638],{"class":1626},[797,1925,1926],{"class":1641},"\"alert\"",[797,1928,1825],{"class":1626},[797,1930,1932],{"class":1622,"line":1931},10,[797,1933,1934],{"class":1626}," Please enter a valid email address.\n",[797,1936,1938,1941,1943],{"class":1622,"line":1937},11,[797,1939,1940],{"class":1626}," \u003C/",[797,1942,20],{"class":1630},[797,1944,1825],{"class":1626},[797,1946,1948,1951,1953],{"class":1622,"line":1947},12,[797,1949,1950],{"class":1626},"\u003C/",[797,1952,1814],{"class":1630},[797,1954,1825],{"class":1626},[20,1956,602,1957,1117,1960,1963,1964,1968,1969,1972],{},[428,1958,1959],{},"aria-describedby",[428,1961,1962],{},"aria-invalid"," attributes ensure ",[34,1965,1967],{"href":1966},"/blog/web-accessibility-wcag-compliance","screen reader users"," receive the same validation feedback as sighted users. The ",[428,1970,1971],{},"role=\"alert\""," attribute announces the error immediately when it appears.",[328,1974],{},[15,1976,1978],{"id":1977},"multi-step-forms-and-progressive-disclosure","Multi-Step Forms and Progressive Disclosure",[20,1980,1981],{},"Long forms should be broken into logical steps. A checkout form with 15 fields is intimidating. The same 15 fields split into three steps — shipping information, payment details, order review — feels manageable because the user only sees 5 fields at a time.",[20,1983,1984],{},"Display a progress indicator that shows the current step, total steps, and completion percentage. This reduces anxiety by making the scope of the form visible. Users who can see they are on \"Step 2 of 3\" are more likely to continue than users who cannot tell how many more fields await them.",[20,1986,1987],{},"Each step should feel complete in itself. The shipping step collects all shipping fields. The payment step collects all payment fields. Do not split logically related fields across steps — putting first name on step 1 and last name on step 2 creates confusion.",[20,1989,1990,1991,1994],{},"Preserve state between steps. If the user navigates back to a previous step, their data should still be there. If they accidentally close the browser tab, consider saving their progress to ",[428,1992,1993],{},"localStorage"," or the server so they can resume. Cart abandonment recovery in e-commerce depends on this — sending a \"complete your purchase\" email only works if the user can return to where they left off.",[20,1996,1997],{},"Validate each step before allowing progression to the next. Do not let users advance past a step with missing required fields and then show errors at the end. Surface validation errors at the step level, where the user can fix them in context.",[328,1999],{},[15,2001,2003],{"id":2002},"submission-feedback-and-error-recovery","Submission Feedback and Error Recovery",[20,2005,2006],{},"The submit button is the most critical interactive element on the form. It should communicate three states clearly: ready to submit, submitting, and submitted.",[20,2008,2009],{},"On click, immediately disable the button and show a loading indicator. This prevents double submissions and provides visual confirmation that the click registered. If the submission takes more than a few hundred milliseconds, the loading state prevents the user from wondering if the form is broken.",[1613,2011,2015],{"className":2012,"code":2013,"language":2014,"meta":138,"style":138},"language-javascript shiki shiki-themes github-dark","const isSubmitting = ref(false);\n\nAsync function handleSubmit(data) {\n isSubmitting.value = true;\n try {\n await submitForm(data);\n showSuccess();\n } catch (error) {\n showError(error.message);\n } finally {\n isSubmitting.value = false;\n }\n}\n","javascript",[428,2016,2017,2042,2047,2067,2080,2088,2099,2107,2118,2126,2135,2146,2151],{"__ignoreMap":138},[797,2018,2019,2023,2027,2030,2033,2036,2039],{"class":1622,"line":1623},[797,2020,2022],{"class":2021},"snl16","const",[797,2024,2026],{"class":2025},"sDLfK"," isSubmitting",[797,2028,2029],{"class":2021}," =",[797,2031,2032],{"class":1634}," ref",[797,2034,2035],{"class":1626},"(",[797,2037,2038],{"class":2025},"false",[797,2040,2041],{"class":1626},");\n",[797,2043,2044],{"class":1622,"line":142},[797,2045,2046],{"emptyLinePlaceholder":158},"\n",[797,2048,2049,2052,2055,2058,2060,2064],{"class":1622,"line":139},[797,2050,2051],{"class":1626},"Async ",[797,2053,2054],{"class":2021},"function",[797,2056,2057],{"class":1634}," handleSubmit",[797,2059,2035],{"class":1626},[797,2061,2063],{"class":2062},"s9osk","data",[797,2065,2066],{"class":1626},") {\n",[797,2068,2069,2072,2074,2077],{"class":1622,"line":1710},[797,2070,2071],{"class":1626}," isSubmitting.value ",[797,2073,1638],{"class":2021},[797,2075,2076],{"class":2025}," true",[797,2078,2079],{"class":1626},";\n",[797,2081,2082,2085],{"class":1622,"line":1867},[797,2083,2084],{"class":2021}," try",[797,2086,2087],{"class":1626}," {\n",[797,2089,2090,2093,2096],{"class":1622,"line":1067},[797,2091,2092],{"class":2021}," await",[797,2094,2095],{"class":1634}," submitForm",[797,2097,2098],{"class":1626},"(data);\n",[797,2100,2101,2104],{"class":1622,"line":160},[797,2102,2103],{"class":1634}," showSuccess",[797,2105,2106],{"class":1626},"();\n",[797,2108,2109,2112,2115],{"class":1622,"line":1896},[797,2110,2111],{"class":1626}," } ",[797,2113,2114],{"class":2021},"catch",[797,2116,2117],{"class":1626}," (error) {\n",[797,2119,2120,2123],{"class":1622,"line":750},[797,2121,2122],{"class":1634}," showError",[797,2124,2125],{"class":1626},"(error.message);\n",[797,2127,2128,2130,2133],{"class":1622,"line":1931},[797,2129,2111],{"class":1626},[797,2131,2132],{"class":2021},"finally",[797,2134,2087],{"class":1626},[797,2136,2137,2139,2141,2144],{"class":1622,"line":1937},[797,2138,2071],{"class":1626},[797,2140,1638],{"class":2021},[797,2142,2143],{"class":2025}," false",[797,2145,2079],{"class":1626},[797,2147,2148],{"class":1622,"line":1947},[797,2149,2150],{"class":1626}," }\n",[797,2152,2154],{"class":1622,"line":2153},13,[797,2155,2156],{"class":1626},"}\n",[20,2158,2159],{},"On success, provide clear confirmation. For a contact form, show \"Message sent. We'll respond within 24 hours.\" For a checkout, show \"Order confirmed. Check your email for details.\" Be specific about what happens next to reduce post-submission anxiety.",[20,2161,2162],{},"On error, distinguish between field-level errors (validation issues the user can fix) and system-level errors (server failures the user cannot fix). For field errors, scroll to the first error and focus on the errored field. For system errors, show a clear message that the form was not submitted and provide a retry option that preserves all entered data. Never clear a form on a failed submission — forcing users to re-enter 12 fields because the server returned a 500 error is the fastest way to guarantee they will never return.",[2164,2165,2166],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":138,"searchDepth":139,"depth":139,"links":2168},[2169,2170,2171,2172,2173],{"id":1584,"depth":142,"text":1585},{"id":1599,"depth":142,"text":1600},{"id":1769,"depth":142,"text":1770},{"id":1977,"depth":142,"text":1978},{"id":2002,"depth":142,"text":2003},"Frontend","Form design is where UX and engineering intersect directly with business metrics. Here are the patterns that reduce abandonment and increase completion rates.",[2177,2178],"web forms best practices","form design conversion",{},"/blog/web-forms-best-practices",{"title":1578,"description":2175},"blog/web-forms-best-practices",[2184,2185,2174],"Forms","UX","gZvDC0ypxrjqlQgzJqRdDThHhRuFTMSOcgsU_X0hmbI",{"id":2188,"title":2189,"author":2190,"body":2191,"category":2174,"date":2895,"description":2896,"extension":150,"featured":151,"image":152,"keywords":2897,"meta":2903,"navigation":158,"path":2904,"readTime":160,"seo":2905,"stem":2906,"tags":2907,"__hash__":2911},"blog/blog/horizontal-scroll-ux-when-it-works.md","Horizontal Scroll UX: When It Works and When It Doesn't",{"name":9,"bio":10},{"type":12,"value":2192,"toc":2876},[2193,2197,2200,2203,2205,2209,2214,2217,2220,2224,2227,2230,2234,2237,2241,2244,2246,2250,2254,2257,2260,2264,2267,2271,2274,2285,2288,2355,2358,2362,2365,2368,2370,2374,2381,2765,2768,2797,2799,2803,2809,2822,2828,2830,2834,2837,2840,2843,2845,2847,2873],[15,2194,2196],{"id":2195},"how-i-got-here","How I Got Here",[20,2198,2199],{},"This portfolio uses a horizontal scroll layout. Seven sections — Home, About, Portfolio, Blog, Services, FAQ, Contact — arranged side by side, navigated by scrolling, swiping, arrow keys, or clicking dots at the bottom of the screen.",[20,2201,2202],{},"It was a deliberate design decision, and I've thought hard about whether it was the right one. This post documents that thinking.",[328,2204],{},[15,2206,2208],{"id":2207},"when-horizontal-scroll-works","When Horizontal Scroll Works",[2210,2211,2213],"h3",{"id":2212},"storytelling-and-linear-narratives","Storytelling and linear narratives",[20,2215,2216],{},"Horizontal scroll forces sequential consumption. You can't skip to section 5 without passing through sections 1–4 (without using the nav dots). For a portfolio, that's a feature: the story of who I am, what I've built, how I work, and how to reach me unfolds in order.",[20,2218,2219],{},"This is also why horizontal scroll works well for product landing pages, onboarding flows, and interactive presentations. When there's a narrative arc and you want the user to follow it, the layout enforces that.",[2210,2221,2223],{"id":2222},"short-distinct-content-panels","Short, distinct content panels",[20,2225,2226],{},"Each section of this portfolio is exactly one viewport. Horizontal scroll breaks down when individual panels have varying heights or long-form content that needs vertical scrolling itself. When every section fits one screen, the mechanic is clean.",[20,2228,2229],{},"If you're thinking about horizontal scroll for a project, ask: can each section be self-contained in one viewport? If not, you'll be fighting the layout.",[2210,2231,2233],{"id":2232},"desktop-first-experiences","Desktop-first experiences",[20,2235,2236],{},"On a wide monitor, horizontal space is abundant and often underused. Horizontal scroll reclaims that space and gives the layout a cinematic quality. On mobile, it's a different story (more on that below).",[2210,2238,2240],{"id":2239},"when-you-want-to-stand-out","When you want to stand out",[20,2242,2243],{},"Most sites scroll vertically. Horizontal scroll is immediately distinctive. For a portfolio specifically, a memorable layout IS part of the product — you're showcasing what you can build, and this layout demonstrates that you can build something unusual.",[328,2245],{},[15,2247,2249],{"id":2248},"when-horizontal-scroll-fights-the-user","When Horizontal Scroll Fights the User",[2210,2251,2253],{"id":2252},"long-form-content","Long-form content",[20,2255,2256],{},"Blog posts, documentation, articles — anything that requires extended reading — should scroll vertically. Readers scan down, not sideways. Forcing horizontal on reading content creates cognitive friction.",[20,2258,2259],{},"This is why the blog posts and service pages on this site break out of the horizontal layout entirely. The home page is horizontal; the subpages are standard vertical scroll.",[2210,2261,2263],{"id":2262},"content-of-unknown-or-variable-length","Content of unknown or variable length",[20,2265,2266],{},"Horizontal scroll requires knowing your content dimensions at design time. If you're building a CMS-driven page where content length varies, each section may overflow its panel unpredictably. You either clip the content or break the mechanic.",[2210,2268,2270],{"id":2269},"accessibility","Accessibility",[20,2272,2273],{},"This is the most serious concern. Horizontal scroll:",[217,2275,2276,2279,2282],{},[220,2277,2278],{},"Disrupts keyboard navigation for screen reader users who expect Tab to move through focusable elements, not trigger section changes",[220,2280,2281],{},"Fights the browser's default scroll behavior",[220,2283,2284],{},"Requires explicit handling for all input methods: mouse wheel, trackpad, keyboard, touch swipe, and nav buttons",[20,2286,2287],{},"If you build horizontal scroll, you must handle all five input methods well. This portfolio handles:",[217,2289,2290,2300,2312,2332,2345],{},[220,2291,2292,2295,2296,2299],{},[191,2293,2294],{},"Mouse wheel",": Intercepts ",[428,2297,2298],{},"WheelEvent",", translates vertical scroll to section change",[220,2301,2302,2305,2306,2309,2310],{},[191,2303,2304],{},"Trackpad two-finger swipe",": Detected via ",[428,2307,2308],{},"deltaX"," on ",[428,2311,2298],{},[220,2313,2314,2317,2318,2321,2322,2325,2326,2321,2329],{},[191,2315,2316],{},"Keyboard",": ",[428,2319,2320],{},"ArrowLeft","/",[428,2323,2324],{},"ArrowRight"," mapped to ",[428,2327,2328],{},"scrollPrev",[428,2330,2331],{},"scrollNext",[220,2333,2334,2317,2337,2340,2341,2344],{},[191,2335,2336],{},"Touch swipe",[428,2338,2339],{},"touchstart"," + ",[428,2342,2343],{},"touchend"," delta calculation",[220,2346,2347,2350,2351,2354],{},[191,2348,2349],{},"Nav dots",": Direct ",[428,2352,2353],{},"scrollTo"," with smooth behavior",[20,2356,2357],{},"If any of these break, some percentage of users can't navigate. Build it well or don't build it.",[2210,2359,2361],{"id":2360},"mobile","Mobile",[20,2363,2364],{},"On mobile, the native scroll direction is vertical. Users swipe up to scroll — this is a deeply ingrained mental model. Horizontal swipe works on mobile, but it competes with browser back/forward gestures, especially on iOS.",[20,2366,2367],{},"My implementation uses a 50px minimum swipe distance and checks whether the swipe is primarily horizontal before intercepting it. This prevents accidental section changes when users are trying to scroll within a section.",[328,2369],{},[15,2371,2373],{"id":2372},"the-implementation","The Implementation",[20,2375,2376,2377,2380],{},"The core mechanic in ",[428,2378,2379],{},"layouts/horizontal.vue",":",[1613,2382,2386],{"className":2383,"code":2384,"language":2385,"meta":138,"style":138},"language-ts shiki shiki-themes github-dark","const handleWheel = (e: WheelEvent) => {\n if (isScrolling) return\n\n // Allow natural scroll inside scrollable child elements\n const scrollableParent = (e.target as HTMLElement)\n .closest('.overflow-y-auto')\n if (scrollableParent) {\n // Check if we're at the boundary before capturing the event\n const { scrollTop, scrollHeight, clientHeight } = scrollableParent\n const atTop = scrollTop === 0\n const atBottom = Math.abs(scrollHeight - clientHeight - scrollTop) \u003C 1\n if (e.deltaY > 0 && !atBottom) return\n if (e.deltaY \u003C 0 && !atTop) return\n }\n\n e.preventDefault()\n\n const delta = Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX\n if (Math.abs(delta) > 30) {\n isScrolling = true\n delta > 0 ? scrollNext() : scrollPrev()\n setTimeout(() => { isScrolling = false }, 600)\n }\n}\n","ts",[428,2387,2388,2416,2427,2431,2437,2459,2474,2481,2486,2513,2531,2565,2589,2608,2613,2618,2630,2635,2672,2692,2703,2729,2755,2760],{"__ignoreMap":138},[797,2389,2390,2392,2395,2397,2400,2403,2405,2408,2411,2414],{"class":1622,"line":1623},[797,2391,2022],{"class":2021},[797,2393,2394],{"class":1634}," handleWheel",[797,2396,2029],{"class":2021},[797,2398,2399],{"class":1626}," (",[797,2401,2402],{"class":2062},"e",[797,2404,2380],{"class":2021},[797,2406,2407],{"class":1634}," WheelEvent",[797,2409,2410],{"class":1626},") ",[797,2412,2413],{"class":2021},"=>",[797,2415,2087],{"class":1626},[797,2417,2418,2421,2424],{"class":1622,"line":142},[797,2419,2420],{"class":2021}," if",[797,2422,2423],{"class":1626}," (isScrolling) ",[797,2425,2426],{"class":2021},"return\n",[797,2428,2429],{"class":1622,"line":139},[797,2430,2046],{"emptyLinePlaceholder":158},[797,2432,2433],{"class":1622,"line":1710},[797,2434,2436],{"class":2435},"sAwPA"," // Allow natural scroll inside scrollable child elements\n",[797,2438,2439,2442,2445,2447,2450,2453,2456],{"class":1622,"line":1867},[797,2440,2441],{"class":2021}," const",[797,2443,2444],{"class":2025}," scrollableParent",[797,2446,2029],{"class":2021},[797,2448,2449],{"class":1626}," (e.target ",[797,2451,2452],{"class":2021},"as",[797,2454,2455],{"class":1634}," HTMLElement",[797,2457,2458],{"class":1626},")\n",[797,2460,2461,2464,2467,2469,2472],{"class":1622,"line":1067},[797,2462,2463],{"class":1626}," .",[797,2465,2466],{"class":1634},"closest",[797,2468,2035],{"class":1626},[797,2470,2471],{"class":1641},"'.overflow-y-auto'",[797,2473,2458],{"class":1626},[797,2475,2476,2478],{"class":1622,"line":160},[797,2477,2420],{"class":2021},[797,2479,2480],{"class":1626}," (scrollableParent) {\n",[797,2482,2483],{"class":1622,"line":1896},[797,2484,2485],{"class":2435}," // Check if we're at the boundary before capturing the event\n",[797,2487,2488,2490,2493,2496,2498,2501,2503,2506,2508,2510],{"class":1622,"line":750},[797,2489,2441],{"class":2021},[797,2491,2492],{"class":1626}," { ",[797,2494,2495],{"class":2025},"scrollTop",[797,2497,1754],{"class":1626},[797,2499,2500],{"class":2025},"scrollHeight",[797,2502,1754],{"class":1626},[797,2504,2505],{"class":2025},"clientHeight",[797,2507,2111],{"class":1626},[797,2509,1638],{"class":2021},[797,2511,2512],{"class":1626}," scrollableParent\n",[797,2514,2515,2517,2520,2522,2525,2528],{"class":1622,"line":1931},[797,2516,2441],{"class":2021},[797,2518,2519],{"class":2025}," atTop",[797,2521,2029],{"class":2021},[797,2523,2524],{"class":1626}," scrollTop ",[797,2526,2527],{"class":2021},"===",[797,2529,2530],{"class":2025}," 0\n",[797,2532,2533,2535,2538,2540,2543,2546,2549,2552,2555,2557,2560,2562],{"class":1622,"line":1937},[797,2534,2441],{"class":2021},[797,2536,2537],{"class":2025}," atBottom",[797,2539,2029],{"class":2021},[797,2541,2542],{"class":1626}," Math.",[797,2544,2545],{"class":1634},"abs",[797,2547,2548],{"class":1626},"(scrollHeight ",[797,2550,2551],{"class":2021},"-",[797,2553,2554],{"class":1626}," clientHeight ",[797,2556,2551],{"class":2021},[797,2558,2559],{"class":1626}," scrollTop) ",[797,2561,1627],{"class":2021},[797,2563,2564],{"class":2025}," 1\n",[797,2566,2567,2569,2572,2575,2578,2581,2584,2587],{"class":1622,"line":1947},[797,2568,2420],{"class":2021},[797,2570,2571],{"class":1626}," (e.deltaY ",[797,2573,2574],{"class":2021},">",[797,2576,2577],{"class":2025}," 0",[797,2579,2580],{"class":2021}," &&",[797,2582,2583],{"class":2021}," !",[797,2585,2586],{"class":1626},"atBottom) ",[797,2588,2426],{"class":2021},[797,2590,2591,2593,2595,2597,2599,2601,2603,2606],{"class":1622,"line":2153},[797,2592,2420],{"class":2021},[797,2594,2571],{"class":1626},[797,2596,1627],{"class":2021},[797,2598,2577],{"class":2025},[797,2600,2580],{"class":2021},[797,2602,2583],{"class":2021},[797,2604,2605],{"class":1626},"atTop) ",[797,2607,2426],{"class":2021},[797,2609,2611],{"class":1622,"line":2610},14,[797,2612,2150],{"class":1626},[797,2614,2616],{"class":1622,"line":2615},15,[797,2617,2046],{"emptyLinePlaceholder":158},[797,2619,2621,2624,2627],{"class":1622,"line":2620},16,[797,2622,2623],{"class":1626}," e.",[797,2625,2626],{"class":1634},"preventDefault",[797,2628,2629],{"class":1626},"()\n",[797,2631,2633],{"class":1622,"line":2632},17,[797,2634,2046],{"emptyLinePlaceholder":158},[797,2636,2638,2640,2643,2645,2647,2649,2652,2654,2656,2658,2661,2664,2667,2669],{"class":1622,"line":2637},18,[797,2639,2441],{"class":2021},[797,2641,2642],{"class":2025}," delta",[797,2644,2029],{"class":2021},[797,2646,2542],{"class":1626},[797,2648,2545],{"class":1634},[797,2650,2651],{"class":1626},"(e.deltaY) ",[797,2653,2574],{"class":2021},[797,2655,2542],{"class":1626},[797,2657,2545],{"class":1634},[797,2659,2660],{"class":1626},"(e.deltaX) ",[797,2662,2663],{"class":2021},"?",[797,2665,2666],{"class":1626}," e.deltaY ",[797,2668,2380],{"class":2021},[797,2670,2671],{"class":1626}," e.deltaX\n",[797,2673,2675,2677,2680,2682,2685,2687,2690],{"class":1622,"line":2674},19,[797,2676,2420],{"class":2021},[797,2678,2679],{"class":1626}," (Math.",[797,2681,2545],{"class":1634},[797,2683,2684],{"class":1626},"(delta) ",[797,2686,2574],{"class":2021},[797,2688,2689],{"class":2025}," 30",[797,2691,2066],{"class":1626},[797,2693,2695,2698,2700],{"class":1622,"line":2694},20,[797,2696,2697],{"class":1626}," isScrolling ",[797,2699,1638],{"class":2021},[797,2701,2702],{"class":2025}," true\n",[797,2704,2706,2709,2711,2713,2716,2719,2722,2724,2727],{"class":1622,"line":2705},21,[797,2707,2708],{"class":1626}," delta ",[797,2710,2574],{"class":2021},[797,2712,2577],{"class":2025},[797,2714,2715],{"class":2021}," ?",[797,2717,2718],{"class":1634}," scrollNext",[797,2720,2721],{"class":1626},"() ",[797,2723,2380],{"class":2021},[797,2725,2726],{"class":1634}," scrollPrev",[797,2728,2629],{"class":1626},[797,2730,2732,2735,2738,2740,2743,2745,2747,2750,2753],{"class":1622,"line":2731},22,[797,2733,2734],{"class":1634}," setTimeout",[797,2736,2737],{"class":1626},"(() ",[797,2739,2413],{"class":2021},[797,2741,2742],{"class":1626}," { isScrolling ",[797,2744,1638],{"class":2021},[797,2746,2143],{"class":2025},[797,2748,2749],{"class":1626}," }, ",[797,2751,2752],{"class":2025},"600",[797,2754,2458],{"class":1626},[797,2756,2758],{"class":1622,"line":2757},23,[797,2759,2150],{"class":1626},[797,2761,2763],{"class":1622,"line":2762},24,[797,2764,2156],{"class":1626},[20,2766,2767],{},"Three things matter here:",[2769,2770,2771,2781,2787],"ol",{},[220,2772,2773,2780],{},[191,2774,2775,2776,2779],{},"Debounce with ",[428,2777,2778],{},"isScrolling"," flag."," Without this, a single wheel event fires dozens of times and skips multiple sections.",[220,2782,2783,2786],{},[191,2784,2785],{},"Boundary detection for nested scrollable elements."," If a section has its own scrollable area (like a tall services list), vertical scroll should work inside it. Only when you hit the top or bottom should the horizontal navigation take over.",[220,2788,2789,2792,2793,2796],{},[191,2790,2791],{},"The 600ms timeout."," This matches the CSS ",[428,2794,2795],{},"scroll-behavior: smooth"," animation duration. Too short and sections can be skipped; too long and the layout feels sluggish.",[328,2798],{},[15,2800,2802],{"id":2801},"what-id-do-differently","What I'd Do Differently",[20,2804,2805,2808],{},[191,2806,2807],{},"Add a \"scroll to explore\" indicator on mobile."," Users on mobile who haven't interacted with horizontal layouts before need a cue. An animated swipe hint on first load would reduce the initial confusion.",[20,2810,2811,2814,2815,1754,2818,2821],{},[191,2812,2813],{},"Make sections individually deep-linkable."," Right now, all deep links go to the home page and you navigate from there. Adding URL hash updates (",[428,2816,2817],{},"#about",[428,2819,2820],{},"#portfolio",", etc.) as sections change would improve shareability and allow direct linking.",[20,2823,2824,2827],{},[191,2825,2826],{},"Test with real users earlier."," I built the mechanic first and refined it based on personal use. A few sessions with people who'd never seen the layout would have surfaced the mobile UX issues faster.",[328,2829],{},[15,2831,2833],{"id":2832},"the-verdict","The Verdict",[20,2835,2836],{},"Horizontal scroll works well for this portfolio. The content is panel-sized, the narrative is linear, and the layout differentiates the site from standard template portfolios.",[20,2838,2839],{},"It would be the wrong choice for a blog, a documentation site, a SaaS dashboard, or anything where users need to scan, search, or read long-form content.",[20,2841,2842],{},"The pattern isn't good or bad — it's situational. The mistake isn't using it; it's using it without understanding why.",[328,2844],{},[15,2846,541],{"id":540},[217,2848,2849,2855,2861,2867],{},[220,2850,2851],{},[34,2852,2854],{"href":2853},"/blog/ai-powered-code-review","AI-Powered Code Review: What Works, What Doesn't, and How I Use It",[220,2856,2857],{},[34,2858,2860],{"href":2859},"/blog/cdn-configuration-guide","CDN Configuration: Making Your Static Assets Load Instantly Everywhere",[220,2862,2863],{},[34,2864,2866],{"href":2865},"/blog/cloudflare-pages-guide","Cloudflare Pages: The Fastest Way to Deploy Your Frontend",[220,2868,2869],{},[34,2870,2872],{"href":2871},"/blog/frontend-performance-guide","Frontend Performance: The Metrics That Matter and How to Hit Them",[2164,2874,2875],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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);}",{"title":138,"searchDepth":139,"depth":139,"links":2877},[2878,2879,2885,2891,2892,2893,2894],{"id":2195,"depth":142,"text":2196},{"id":2207,"depth":142,"text":2208,"children":2880},[2881,2882,2883,2884],{"id":2212,"depth":139,"text":2213},{"id":2222,"depth":139,"text":2223},{"id":2232,"depth":139,"text":2233},{"id":2239,"depth":139,"text":2240},{"id":2248,"depth":142,"text":2249,"children":2886},[2887,2888,2889,2890],{"id":2252,"depth":139,"text":2253},{"id":2262,"depth":139,"text":2263},{"id":2269,"depth":139,"text":2270},{"id":2360,"depth":139,"text":2361},{"id":2372,"depth":142,"text":2373},{"id":2801,"depth":142,"text":2802},{"id":2832,"depth":142,"text":2833},{"id":540,"depth":142,"text":541},"2026-02-03","I built a horizontal-scrolling portfolio and learned exactly when this pattern enhances the experience and when it fights the user. Here's the honest breakdown.",[2898,2899,2900,2901,2902],"horizontal scroll UX","horizontal scrolling design","scroll UX patterns","web design scroll","horizontal carousel design",{},"/blog/horizontal-scroll-ux-when-it-works",{"title":2189,"description":2896},"blog/horizontal-scroll-ux-when-it-works",[2185,2908,2909,2174,2910,2270],"CSS","JavaScript","Portfolio Design","T0_h9YgKf7HAdSc1Talk2cBOQ5motevme_0vz76FyJ0",{"id":2913,"title":2914,"author":2915,"body":2916,"category":359,"date":3010,"description":3011,"extension":150,"featured":151,"image":152,"keywords":3012,"meta":3016,"navigation":158,"path":1006,"readTime":1867,"seo":3017,"stem":3018,"tags":3019,"__hash__":3024},"blog/blog/druid-tradition-history.md","The Druids: What We Actually Know",{"name":9,"bio":10},{"type":12,"value":2917,"toc":3004},[2918,2922,2925,2928,2931,2935,2938,2941,2962,2965,2969,2972,2975,2978,2982,2988,2996],[15,2919,2921],{"id":2920},"the-problem-of-sources","The Problem of Sources",[20,2923,2924],{},"Everything we think we know about druids is filtered through hostile or distant observers. The classical sources — Caesar, Strabo, Pliny, Diodorus Siculus — were Romans writing about people they were conquering. The Irish sources — the medieval law texts, the mythological cycles, the hagiographies — were written by Christians centuries after druidic practice had been suppressed or absorbed.",[20,2926,2927],{},"No druid ever wrote a text explaining their own beliefs. This absence is not accidental. The druids famously refused to commit their knowledge to writing, despite being literate in Greek and Latin. Caesar reported that druidic training lasted up to twenty years and was conducted entirely through memorization. The knowledge was oral, esoteric, and deliberately restricted.",[20,2929,2930],{},"This means that every statement about what druids \"believed\" or \"practiced\" is a reconstruction based on outsider accounts, archaeological inference, and comparison with related traditions. Some of those reconstructions are well-supported. Others are speculation dressed in confidence. The honest approach is to distinguish between what the sources actually say and what modern interpreters wish they said.",[15,2932,2934],{"id":2933},"what-the-sources-agree-on","What the Sources Agree On",[20,2936,2937],{},"Across the classical and Irish sources, a consistent picture emerges. Druids were the learned class of Celtic society — not a priesthood in the narrow sense but an intellectual elite whose functions encompassed religion, law, education, medicine, astronomy, and political counsel.",[20,2939,2940],{},"Caesar's account, written in the 1st century BC, describes druids as the arbiters of all disputes, both public and private. They determined what was lawful and what was not. They could ban individuals from religious ceremonies — effectively excommunicating them from society. They gathered annually at a sacred site in the territory of the Carnutes (central Gaul) to settle legal disputes and elect a chief druid.",[20,2942,2943,2944,2947,2948,2951,2952,2956,2957,2961],{},"The Irish sources largely confirm this picture. The ",[604,2945,2946],{},"drui"," (druid) appears alongside the ",[604,2949,2950],{},"fili"," (poet) and the ",[34,2953,2955],{"href":2954},"/blog/brehon-law-ancient-ireland","brehon"," (jurist) as one of the three branches of the learned class. The Irish druids served as royal advisors, performed divination, regulated the ",[34,2958,2960],{"href":2959},"/blog/celtic-calendar-festivals","Celtic calendar",", and presided over religious rituals. Their authority derived not from military force but from knowledge — and from the social consensus that knowledge conferred the right to judge and advise.",[20,2963,2964],{},"The parallel with other Indo-European priestly classes is clear. The druids occupied a similar social position to the Brahmins of Hindu society — a comparison that scholars from Georges Dumezil onward have explored in detail. Both were a hereditary learned class whose authority rested on mastery of sacred knowledge, and both occupied the highest tier in a tripartite social division (priest, warrior, producer) that characterizes Indo-European societies from India to Ireland.",[15,2966,2968],{"id":2967},"the-sacred-and-the-violent","The Sacred and the Violent",[20,2970,2971],{},"The most controversial aspect of druidic practice is human sacrifice. Caesar reports it. Strabo reports it. Lucan describes burning victims in wicker constructions. The question is whether to believe them.",[20,2973,2974],{},"The archaeological evidence is ambiguous. Lindow Man — a preserved body found in a Cheshire peat bog — shows signs of ritual killing (strangling, throat-cutting, and drowning in a sequence that may reflect a triple death ritual). Similar bog bodies have been found across the Celtic and Germanic world. Whether these represent druidic sacrifice, criminal execution, or something else entirely is debated.",[20,2976,2977],{},"The classical sources had political reasons to emphasize Celtic barbarity — it justified conquest. But dismissing all reports of sacrifice as propaganda ignores That ritual killing was practiced across the ancient world, including by the Romans themselves (who buried live victims in the Forum as late as 226 BC). The most balanced reading is that druids likely performed ritual killings on specific occasions, but that Roman sources exaggerated the practice for rhetorical purposes.",[15,2979,2981],{"id":2980},"after-the-druids","After the Druids",[20,2983,2984,2985,194],{},"The Roman conquest of Gaul and Britain targeted druids specifically. Suetonius Paulinus's attack on Anglesey (Mona) in 60 AD, which destroyed the druidic center there, was a deliberate strike against the intellectual infrastructure of British Celtic resistance. In Ireland, which Rome never conquered, the druidic tradition survived longer but was gradually absorbed by ",[34,2986,757],{"href":2987},"/blog/celtic-christianity-scotland",[20,2989,2990,2991,2995],{},"The absorption was not total destruction. Many scholars believe that the Irish ",[34,2992,2994],{"href":2993},"/blog/applecross-obeolans-monks-dynasty","monastic tradition"," inherited structural elements from the druidic schools — the emphasis on oral learning, the long training periods, the high social status of the scholar. The brehons, who maintained the legal tradition through the medieval period, may represent a direct continuation of one branch of druidic expertise.",[20,2997,2998,2999,3003],{},"The druids did not vanish overnight. They were transformed — their functions redistributed among Christian clerics, secular jurists, and hereditary poets. The knowledge they carried, insofar as it survived at all, was absorbed into the ",[34,3000,3002],{"href":3001},"/blog/ancient-irish-mythology","mythological"," and legal traditions that monks later wrote down. What was lost, permanently, was whatever the druids considered too sacred to commit to writing — the core of their tradition, carried in memory for centuries and extinguished when the last trained memory died.",{"title":138,"searchDepth":139,"depth":139,"links":3005},[3006,3007,3008,3009],{"id":2920,"depth":142,"text":2921},{"id":2933,"depth":142,"text":2934},{"id":2967,"depth":142,"text":2968},{"id":2980,"depth":142,"text":2981},"2026-02-01","Druids were not wizards in robes. They were the intellectual class of Celtic society — jurists, astronomers, theologians, and political advisors.",[3013,3014,3015],"druid tradition history","who were the druids","celtic druids",{},{"title":2914,"description":3011},"blog/druid-tradition-history",[3020,3021,3022,3023],"Druids","Celtic Religion","Ancient History","Celtic Society","_6mVLzD1psvqk0JFQlGKdlP2Jcma6tqGxPFLCywcusI",{"id":3026,"title":3027,"author":3028,"body":3029,"category":359,"date":3010,"description":3214,"extension":150,"featured":151,"image":152,"keywords":3215,"meta":3221,"navigation":158,"path":3222,"readTime":160,"seo":3223,"stem":3224,"tags":3225,"__hash__":3231},"blog/blog/land-records-property-research.md","Land Records: Finding Ancestors Through Property",{"name":9,"bio":10},{"type":12,"value":3030,"toc":3206},[3031,3035,3038,3050,3053,3057,3063,3066,3072,3078,3082,3085,3091,3097,3103,3109,3113,3119,3125,3134,3140,3151,3155,3158,3164,3171,3180,3183,3185,3187],[15,3032,3034],{"id":3033},"following-the-land","Following the Land",[20,3036,3037],{},"In agricultural societies, land was wealth. It was identity. It was the reason families stayed in one place for generations or moved across oceans to find new ground. For genealogists, land records are among the most revealing sources available -- and among the most overlooked.",[20,3039,3040,3041,1117,3045,3049],{},"While researchers routinely search ",[34,3042,3044],{"href":3043},"/blog/census-records-genealogy","census records",[34,3046,3048],{"href":3047},"/blog/parish-registers-family-history","parish registers",", land records often go unchecked. This is a mistake. Deeds, grants, surveys, tax lists, and probate records connected to property can reveal family relationships, establish dates and places, and document the economic circumstances of a family in ways that no other source can match.",[20,3051,3052],{},"The reason is structural. Land had to be legally transferred. When a father divided his farm among his sons, a deed was recorded. When a widow inherited her husband's property, the probate court documented it. When a family bought land in a new settlement, the purchase was registered. Each of these transactions left a paper trail, and those trails survive in remarkable quantity.",[15,3054,3056],{"id":3055},"types-of-land-records","Types of Land Records",[20,3058,3059,3062],{},[191,3060,3061],{},"Land grants"," are the original disposition of public land to private owners. In colonial America, grants were issued by the colonial government (or by proprietors like William Penn). After independence, the federal General Land Office managed the sale and grant of public domain land through a series of systems: military bounty warrants (land granted to veterans), cash sales, credit sales, and homestead entries.",[20,3064,3065],{},"The Bureau of Land Management's General Land Office Records website (glorecords.blm.gov) provides free access to federal land patents -- the documents recording the first transfer of public land to private ownership. These patents cover public land states (roughly, everything west of the original thirteen colonies plus a few eastern states).",[20,3067,3068,3071],{},[191,3069,3070],{},"Deeds"," record subsequent transfers of property between private parties. They are recorded at the county level -- at the county courthouse or county recorder's office -- and typically include the names of the buyer and seller, the property description, the purchase price, and the date. Many deeds also include the signatures of witnesses and the wife's release of dower rights (which confirms the seller's marital status and spouse's name).",[20,3073,3074,3077],{},[191,3075,3076],{},"Tax records"," -- property tax assessments, quit-rent rolls, tithe records -- list property owners and the value of their holdings. They are particularly valuable for filling gaps where other records do not survive. In Virginia, where many county courthouse records were destroyed during the Civil War, tax records are sometimes the only source that documents a family's presence in a specific county.",[15,3079,3081],{"id":3080},"what-land-records-reveal","What Land Records Reveal",[20,3083,3084],{},"Land records are not just property documents. They are genealogical documents, because land transfers often involve family relationships.",[20,3086,3087,3090],{},[191,3088,3089],{},"Father-to-son transfers"," are common, especially in the eighteenth and nineteenth centuries. A father deeding land to a son -- often for a nominal sum (\"for the consideration of natural love and affection and the sum of one dollar\") -- establishes the parent-child relationship directly.",[20,3092,3093,3096],{},[191,3094,3095],{},"Inheritance divisions"," documented in probate records or partition deeds list heirs by name and relationship. When a landowner died intestate (without a will), the court divided the property among the legal heirs, creating a document that names children, grandchildren, and sometimes in-laws.",[20,3098,3099,3102],{},[191,3100,3101],{},"Dower releases"," reveal marriages. When a married man sold land, his wife was required to release her dower interest (her legal right to one-third of the property). The dower release names the wife and confirms the marriage. In some cases, dower releases are the only evidence of a specific marriage.",[20,3104,3105,3108],{},[191,3106,3107],{},"Neighbors and witnesses"," in deeds are often relatives. In rural communities, adjacent landowners were frequently family members, and the witnesses to a deed were typically people known to both buyer and seller -- often brothers, in-laws, or cousins.",[15,3110,3112],{"id":3111},"how-to-search-land-records","How to Search Land Records",[20,3114,3115,3118],{},[191,3116,3117],{},"County courthouses"," are the primary repositories for deeds and related records. Most counties maintain grantor/grantee indexes -- alphabetical indexes of property sellers (grantors) and buyers (grantees). Search both indexes: your ancestor may appear as a buyer in one transaction and a seller in another.",[20,3120,3121,3124],{},[191,3122,3123],{},"FamilySearch.org"," has digitized deed books from many US counties, particularly in the eastern states. The images are often browsable even when no index exists.",[20,3126,3127,1117,3130,3133],{},[191,3128,3129],{},"Ancestry.com",[191,3131,3132],{},"Fold3.com"," have collections of land records, including military bounty land warrants and homestead records.",[20,3135,3136,3139],{},[191,3137,3138],{},"State archives"," hold records that predate county formation or that were transferred from county custody. Colonial-era land records are typically at the state level.",[20,3141,3142,3145,3146,3150],{},[191,3143,3144],{},"The National Archives"," holds federal land records, including General Land Office files, homestead applications, and ",[34,3147,3149],{"href":3148},"/blog/military-records-genealogy","military bounty land warrants",". Homestead applications can be particularly informative: they include the applicant's name, age, citizenship status, family composition, and a description of improvements made to the land.",[15,3152,3154],{"id":3153},"land-records-in-scotland-and-ireland","Land Records in Scotland and Ireland",[20,3156,3157],{},"For researchers with Scottish ancestry, land records take a different form. Scotland's system of land registration -- the Register of Sasines, maintained from 1617 onward -- records every transfer of land in Scotland. The Sasines are held at the National Records of Scotland and are partially indexed.",[20,3159,602,3160,3163],{},[191,3161,3162],{},"Valuation Rolls"," (from 1855 onward) list every property in Scotland with its owner, tenant, and rateable value. They serve a similar function to census records for locating families and are available through ScotlandsPeople.",[20,3165,3166,3167,3170],{},"In Ireland, the ",[191,3168,3169],{},"Griffith's Valuation"," (1847-1864) is a comprehensive survey of every property in Ireland, listing the occupier, the landlord, and the property's value. It is the nearest thing to a census for the pre-Famine and Famine period and is freely searchable online at askaboutireland.ie.",[20,3172,602,3173,3176,3177,3179],{},[191,3174,3175],{},"Tithe Applotment Books"," (1823-1837) list landholders liable for tithes and predate Griffith's Valuation, providing an earlier snapshot of who held what land. Both sources are invaluable for Irish research, where the destruction of ",[34,3178,3044],{"href":3043}," has left enormous gaps.",[20,3181,3182],{},"Land was the foundation of pre-industrial society. It determined where people lived, what they did, and how they were connected to each other. The records of that land -- deeds, grants, tax lists, valuations -- are the documentary traces of those connections, waiting in courthouse basements and digital archives for the researcher patient enough to find them.",[328,3184],{},[15,3186,333],{"id":332},[217,3188,3189,3194,3200],{},[220,3190,3191],{},[34,3192,3193],{"href":3043},"Census Records: Snapshots of Your Ancestors' Lives",[220,3195,3196],{},[34,3197,3199],{"href":3198},"/blog/genealogy-medieval-records","Medieval Records and Genealogy: What Survives and Where to Find It",[220,3201,3202],{},[34,3203,3205],{"href":3204},"/blog/family-history-documentary-research","Documentary Research: Building a Family History from Primary Sources",{"title":138,"searchDepth":139,"depth":139,"links":3207},[3208,3209,3210,3211,3212,3213],{"id":3033,"depth":142,"text":3034},{"id":3055,"depth":142,"text":3056},{"id":3080,"depth":142,"text":3081},{"id":3111,"depth":142,"text":3112},{"id":3153,"depth":142,"text":3154},{"id":332,"depth":142,"text":333},"Land records are among the most underused sources in genealogy. Deeds, grants, surveys, and tax lists place ancestors in specific locations, reveal family relationships, and document the transfer of wealth across generations.",[3216,3217,3218,3219,3220],"land records genealogy","deed research ancestors","property records family history","land grants genealogy","tax records ancestors",{},"/blog/land-records-property-research",{"title":3027,"description":3214},"blog/land-records-property-research",[3226,3227,3228,3229,3230],"Land Records","Genealogy Research","Property Records","Family History","Deeds Research","HwtA4DMvUrdNxzVLX0VDGSwy9phdRkIx7rMK3dftRvY",{"id":3233,"title":3234,"author":3235,"body":3236,"category":3765,"date":3010,"description":3766,"extension":150,"featured":151,"image":152,"keywords":3767,"meta":3770,"navigation":158,"path":3771,"readTime":1896,"seo":3772,"stem":3773,"tags":3774,"__hash__":3777},"blog/blog/log-aggregation-architecture.md","Log Aggregation Architecture for Distributed Systems",{"name":9,"bio":10},{"type":12,"value":3237,"toc":3759},[3238,3245,3248,3252,3255,3385,3391,3394,3518,3526,3530,3533,3539,3545,3548,3551,3555,3562,3672,3691,3709,3717,3721,3724,3730,3736,3742,3750,3753,3756],[20,3239,3240,3241,3244],{},"When your application runs on a single server, logs are simple — they are in a file, you ",[428,3242,3243],{},"tail"," it, you find the problem. When your application runs across ten services on fifty containers, logs are scattered. The request that failed touched four services, and the relevant log lines are in four different containers that might have been replaced since the error occurred. Without aggregation, debugging distributed systems is archaeology — piecing together fragments from dig sites you may no longer have access to.",[20,3246,3247],{},"Log aggregation collects logs from every service and container into a centralized, searchable system. The architecture of that system determines whether you can find the needle in the haystack within minutes or spend hours correlating timestamps across terminals.",[15,3249,3251],{"id":3250},"the-collection-layer","The Collection Layer",[20,3253,3254],{},"Log collection starts at the source. Each application writes structured logs — JSON, not free-form text — with consistent fields that make searching possible:",[1613,3256,3258],{"className":2383,"code":3257,"language":2385,"meta":138,"style":138},"const logger = createLogger({\n format: 'json',\n defaultMeta: {\n service: 'api-gateway',\n version: process.env.APP_VERSION,\n environment: process.env.NODE_ENV,\n },\n})\n\nLogger.info('Request processed', {\n requestId: req.id,\n method: req.method,\n path: req.path,\n statusCode: res.statusCode,\n duration: elapsed,\n userId: req.user?.id,\n})\n",[428,3259,3260,3275,3286,3291,3301,3311,3321,3326,3331,3335,3351,3356,3361,3366,3371,3376,3381],{"__ignoreMap":138},[797,3261,3262,3264,3267,3269,3272],{"class":1622,"line":1623},[797,3263,2022],{"class":2021},[797,3265,3266],{"class":2025}," logger",[797,3268,2029],{"class":2021},[797,3270,3271],{"class":1634}," createLogger",[797,3273,3274],{"class":1626},"({\n",[797,3276,3277,3280,3283],{"class":1622,"line":142},[797,3278,3279],{"class":1626}," format: ",[797,3281,3282],{"class":1641},"'json'",[797,3284,3285],{"class":1626},",\n",[797,3287,3288],{"class":1622,"line":139},[797,3289,3290],{"class":1626}," defaultMeta: {\n",[797,3292,3293,3296,3299],{"class":1622,"line":1710},[797,3294,3295],{"class":1626}," service: ",[797,3297,3298],{"class":1641},"'api-gateway'",[797,3300,3285],{"class":1626},[797,3302,3303,3306,3309],{"class":1622,"line":1867},[797,3304,3305],{"class":1626}," version: process.env.",[797,3307,3308],{"class":2025},"APP_VERSION",[797,3310,3285],{"class":1626},[797,3312,3313,3316,3319],{"class":1622,"line":1067},[797,3314,3315],{"class":1626}," environment: process.env.",[797,3317,3318],{"class":2025},"NODE_ENV",[797,3320,3285],{"class":1626},[797,3322,3323],{"class":1622,"line":160},[797,3324,3325],{"class":1626}," },\n",[797,3327,3328],{"class":1622,"line":1896},[797,3329,3330],{"class":1626},"})\n",[797,3332,3333],{"class":1622,"line":750},[797,3334,2046],{"emptyLinePlaceholder":158},[797,3336,3337,3340,3343,3345,3348],{"class":1622,"line":1931},[797,3338,3339],{"class":1626},"Logger.",[797,3341,3342],{"class":1634},"info",[797,3344,2035],{"class":1626},[797,3346,3347],{"class":1641},"'Request processed'",[797,3349,3350],{"class":1626},", {\n",[797,3352,3353],{"class":1622,"line":1937},[797,3354,3355],{"class":1626}," requestId: req.id,\n",[797,3357,3358],{"class":1622,"line":1947},[797,3359,3360],{"class":1626}," method: req.method,\n",[797,3362,3363],{"class":1622,"line":2153},[797,3364,3365],{"class":1626}," path: req.path,\n",[797,3367,3368],{"class":1622,"line":2610},[797,3369,3370],{"class":1626}," statusCode: res.statusCode,\n",[797,3372,3373],{"class":1622,"line":2615},[797,3374,3375],{"class":1626}," duration: elapsed,\n",[797,3377,3378],{"class":1622,"line":2620},[797,3379,3380],{"class":1626}," userId: req.user?.id,\n",[797,3382,3383],{"class":1622,"line":2632},[797,3384,3330],{"class":1626},[20,3386,602,3387,3390],{},[428,3388,3389],{},"requestId"," is the most important field for distributed tracing. When a request enters your system, assign it a unique ID and propagate that ID through every service it touches. Searching for a request ID returns every log line from every service related to that request — this is the difference between \"I can debug this\" and \"I have no idea what happened.\"",[20,3392,3393],{},"Collection agents run on each host or as sidecar containers. Fluentd, Fluent Bit, and the OpenTelemetry Collector are the standard choices. They read logs from stdout (for containers), files (for traditional deployments), or direct API submission, then forward them to the aggregation layer.",[1613,3395,3399],{"className":3396,"code":3397,"language":3398,"meta":138,"style":138},"language-yaml shiki shiki-themes github-dark","# Fluent Bit configuration for Kubernetes\n[INPUT]\n Name tail\n Path /var/log/containers/*.log\n Parser docker\n Tag kube.*\n Refresh_Interval 5\n\n[FILTER]\n Name kubernetes\n Match kube.*\n Merge_Log On\n K8S-Logging.Parser On\n\n[OUTPUT]\n Name es\n Match *\n Host elasticsearch\n Port 9200\n Index logs\n Type _doc\n","yaml",[428,3400,3401,3406,3417,3422,3427,3432,3437,3442,3446,3455,3460,3465,3470,3475,3479,3488,3493,3498,3503,3508,3513],{"__ignoreMap":138},[797,3402,3403],{"class":1622,"line":1623},[797,3404,3405],{"class":2435},"# Fluent Bit configuration for Kubernetes\n",[797,3407,3408,3411,3414],{"class":1622,"line":142},[797,3409,3410],{"class":1626},"[",[797,3412,3413],{"class":1641},"INPUT",[797,3415,3416],{"class":1626},"]\n",[797,3418,3419],{"class":1622,"line":139},[797,3420,3421],{"class":1641}," Name tail\n",[797,3423,3424],{"class":1622,"line":1710},[797,3425,3426],{"class":1641}," Path /var/log/containers/*.log\n",[797,3428,3429],{"class":1622,"line":1867},[797,3430,3431],{"class":1641}," Parser docker\n",[797,3433,3434],{"class":1622,"line":1067},[797,3435,3436],{"class":1641}," Tag kube.*\n",[797,3438,3439],{"class":1622,"line":160},[797,3440,3441],{"class":1641}," Refresh_Interval 5\n",[797,3443,3444],{"class":1622,"line":1896},[797,3445,2046],{"emptyLinePlaceholder":158},[797,3447,3448,3450,3453],{"class":1622,"line":750},[797,3449,3410],{"class":1626},[797,3451,3452],{"class":1641},"FILTER",[797,3454,3416],{"class":1626},[797,3456,3457],{"class":1622,"line":1931},[797,3458,3459],{"class":1641}," Name kubernetes\n",[797,3461,3462],{"class":1622,"line":1937},[797,3463,3464],{"class":1641}," Match kube.*\n",[797,3466,3467],{"class":1622,"line":1947},[797,3468,3469],{"class":1641}," Merge_Log On\n",[797,3471,3472],{"class":1622,"line":2153},[797,3473,3474],{"class":1641}," K8S-Logging.Parser On\n",[797,3476,3477],{"class":1622,"line":2610},[797,3478,2046],{"emptyLinePlaceholder":158},[797,3480,3481,3483,3486],{"class":1622,"line":2615},[797,3482,3410],{"class":1626},[797,3484,3485],{"class":1641},"OUTPUT",[797,3487,3416],{"class":1626},[797,3489,3490],{"class":1622,"line":2620},[797,3491,3492],{"class":1641}," Name es\n",[797,3494,3495],{"class":1622,"line":2632},[797,3496,3497],{"class":1641}," Match *\n",[797,3499,3500],{"class":1622,"line":2637},[797,3501,3502],{"class":1641}," Host elasticsearch\n",[797,3504,3505],{"class":1622,"line":2674},[797,3506,3507],{"class":1641}," Port 9200\n",[797,3509,3510],{"class":1622,"line":2694},[797,3511,3512],{"class":1641}," Index logs\n",[797,3514,3515],{"class":1622,"line":2705},[797,3516,3517],{"class":1641}," Type _doc\n",[20,3519,3520,3521,3525],{},"Fluent Bit is lighter than Fluentd and handles the collection-and-forwarding role well for most deployments. If you need complex log transformation or routing, Fluentd's plugin ecosystem is broader. The OpenTelemetry Collector merges logs with traces and metrics into a single pipeline, which simplifies the ",[34,3522,3524],{"href":3523},"/blog/infrastructure-monitoring","infrastructure monitoring"," stack.",[15,3527,3529],{"id":3528},"storage-and-indexing","Storage and Indexing",[20,3531,3532],{},"The aggregation backend stores logs and makes them searchable. The two dominant approaches are:",[20,3534,3535,3538],{},[191,3536,3537],{},"Elasticsearch (or OpenSearch)"," — full-text search engine that indexes log fields for fast querying. Elasticsearch handles billions of log lines and returns results in seconds. The operational complexity is its downside — managing cluster health, shard allocation, index lifecycle, and storage costs requires ongoing attention.",[20,3540,3541,3544],{},[191,3542,3543],{},"Loki"," — a newer approach from Grafana Labs that stores log lines as compressed chunks and indexes only the metadata labels (service name, environment, pod name). Queries that filter by labels are fast; queries that search within log text are slower. Loki is dramatically cheaper to operate than Elasticsearch because it does not build full-text indexes.",[20,3546,3547],{},"For most teams, Loki provides the right balance. You search by service, time range, and severity level 90% of the time — these are label queries that Loki handles well. The 10% of cases where you need full-text search are slower but still functional.",[20,3549,3550],{},"Retention policies matter for cost. Storing every log line forever is expensive and unnecessary. A common approach: keep the last 7 days at full resolution, aggregate to summary metrics for 30 days, and archive to cold storage for compliance needs. Define the retention policy before you have a storage cost crisis, not after.",[15,3552,3554],{"id":3553},"structured-logging-standards","Structured Logging Standards",[20,3556,3557,3558,3561],{},"The value of aggregated logs depends entirely on their structure. Unstructured log lines like ",[428,3559,3560],{},"\"User 12345 logged in at 2025-09-15\""," are human-readable but machine-hostile. Structured logs with consistent field names enable filtering, aggregation, and alerting:",[1613,3563,3567],{"className":3564,"code":3565,"language":3566,"meta":138,"style":138},"language-json shiki shiki-themes github-dark","{\n \"timestamp\": \"2025-09-15T14:30:00Z\",\n \"level\": \"info\",\n \"service\": \"auth\",\n \"message\": \"User authenticated\",\n \"userId\": \"12345\",\n \"method\": \"password\",\n \"duration\": 142,\n \"requestId\": \"req_abc123\"\n}\n","json",[428,3568,3569,3574,3586,3598,3610,3622,3634,3646,3658,3668],{"__ignoreMap":138},[797,3570,3571],{"class":1622,"line":1623},[797,3572,3573],{"class":1626},"{\n",[797,3575,3576,3579,3581,3584],{"class":1622,"line":142},[797,3577,3578],{"class":2025}," \"timestamp\"",[797,3580,2317],{"class":1626},[797,3582,3583],{"class":1641},"\"2025-09-15T14:30:00Z\"",[797,3585,3285],{"class":1626},[797,3587,3588,3591,3593,3596],{"class":1622,"line":139},[797,3589,3590],{"class":2025}," \"level\"",[797,3592,2317],{"class":1626},[797,3594,3595],{"class":1641},"\"info\"",[797,3597,3285],{"class":1626},[797,3599,3600,3603,3605,3608],{"class":1622,"line":1710},[797,3601,3602],{"class":2025}," \"service\"",[797,3604,2317],{"class":1626},[797,3606,3607],{"class":1641},"\"auth\"",[797,3609,3285],{"class":1626},[797,3611,3612,3615,3617,3620],{"class":1622,"line":1867},[797,3613,3614],{"class":2025}," \"message\"",[797,3616,2317],{"class":1626},[797,3618,3619],{"class":1641},"\"User authenticated\"",[797,3621,3285],{"class":1626},[797,3623,3624,3627,3629,3632],{"class":1622,"line":1067},[797,3625,3626],{"class":2025}," \"userId\"",[797,3628,2317],{"class":1626},[797,3630,3631],{"class":1641},"\"12345\"",[797,3633,3285],{"class":1626},[797,3635,3636,3639,3641,3644],{"class":1622,"line":160},[797,3637,3638],{"class":2025}," \"method\"",[797,3640,2317],{"class":1626},[797,3642,3643],{"class":1641},"\"password\"",[797,3645,3285],{"class":1626},[797,3647,3648,3651,3653,3656],{"class":1622,"line":1896},[797,3649,3650],{"class":2025}," \"duration\"",[797,3652,2317],{"class":1626},[797,3654,3655],{"class":2025},"142",[797,3657,3285],{"class":1626},[797,3659,3660,3663,3665],{"class":1622,"line":750},[797,3661,3662],{"class":2025}," \"requestId\"",[797,3664,2317],{"class":1626},[797,3666,3667],{"class":1641},"\"req_abc123\"\n",[797,3669,3670],{"class":1622,"line":1931},[797,3671,2156],{"class":1626},[20,3673,3674,3675,1754,3678,1754,3681,1754,3684,3687,3688,3690],{},"Establish a logging standard across all services. At minimum, every log line should include: ",[428,3676,3677],{},"timestamp",[428,3679,3680],{},"level",[428,3682,3683],{},"service",[428,3685,3686],{},"message",", and ",[428,3689,3389],{},". Beyond that, each service adds domain-specific fields relevant to its operations.",[20,3692,3693,3694,3697,3698,3701,3702,3704,3705,3708],{},"Log levels should be consistent and meaningful. ",[428,3695,3696],{},"error"," means something failed and needs attention. ",[428,3699,3700],{},"warn"," means something unexpected happened but was handled. ",[428,3703,3342],{}," means a significant business or operational event occurred. ",[428,3706,3707],{},"debug"," is disabled in production unless you are actively investigating an issue.",[20,3710,3711,3712,3716],{},"Do not log sensitive data. User passwords, API keys, credit card numbers, and personal information should never appear in logs. This is a security requirement and often a legal requirement under GDPR or HIPAA. Implement a log sanitizer that strips known sensitive fields before logs leave the application, and review log output during code review. The ",[34,3713,3715],{"href":3714},"/blog/environment-variables-guide","environment variable discipline"," that keeps secrets out of code should extend to keeping them out of logs.",[15,3718,3720],{"id":3719},"dashboards-and-alerts","Dashboards and Alerts",[20,3722,3723],{},"Aggregated logs are raw material. Dashboards transform them into operational awareness. The minimum set of log-based dashboards:",[20,3725,3726,3729],{},[191,3727,3728],{},"Error rate by service"," — a time series showing error log volume per service. This is your primary alert source. A sudden increase in errors from any service triggers an investigation.",[20,3731,3732,3735],{},[191,3733,3734],{},"Latency distribution"," — if you log request duration, plot the p50, p95, and p99 over time. Latency regressions often appear in p99 before they affect p50, giving you early warning.",[20,3737,3738,3741],{},[191,3739,3740],{},"Top errors"," — group error logs by message (or error code) and show the most frequent. This identifies recurring issues and helps prioritize fixes.",[1613,3743,3748],{"className":3744,"code":3746,"language":3747},[3745],"language-text","# Loki query: error rate by service over 5 minutes\nsum by (service) (rate({level=\"error\"} [5m]))\n","text",[428,3749,3746],{"__ignoreMap":138},[20,3751,3752],{},"Alerts should fire on meaningful thresholds, not on individual log lines. \"Error rate exceeds 5% for 3 consecutive minutes\" is actionable. \"An error log was written\" is not — every production system produces some errors. Set alert thresholds based on historical baselines and adjust them as you learn what is normal for your system.",[20,3754,3755],{},"Connect your log aggregation to your incident response process. When an alert fires, the responder should be able to click through from the alert to the relevant logs, filtered to the time window and service in question. Every click between the alert and the root cause adds response time. The goal is a single click from \"something is wrong\" to \"here are the logs that explain what.\"",[2164,3757,3758],{},"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":138,"searchDepth":139,"depth":139,"links":3760},[3761,3762,3763,3764],{"id":3250,"depth":142,"text":3251},{"id":3528,"depth":142,"text":3529},{"id":3553,"depth":142,"text":3554},{"id":3719,"depth":142,"text":3720},"DevOps","Design a log aggregation system for distributed applications — collection, transport, storage, indexing, and building dashboards that help you find problems fast.",[3768,3769],"log aggregation architecture","distributed systems logging",{},"/blog/log-aggregation-architecture",{"title":3234,"description":3766},"blog/log-aggregation-architecture",[3775,3776,3765],"Observability","Infrastructure","IyC6gPcjCSnq_R0H3cnMTqkKIn1vvxdo1vhtUEa8Fa0",{"id":3779,"title":3780,"author":3781,"body":3782,"category":3990,"date":3010,"description":3991,"extension":150,"featured":151,"image":152,"keywords":3992,"meta":3995,"navigation":158,"path":3996,"readTime":160,"seo":3997,"stem":3998,"tags":3999,"__hash__":4002},"blog/blog/multi-tenant-database-design.md","Multi-Tenant Database Design: Isolation Strategies",{"name":9,"bio":10},{"type":12,"value":3783,"toc":3984},[3784,3787,3790,3794,3801,3804,3811,3856,3859,3862,3884,3891,3902,3906,3909,3912,3915,3918,3921,3925,3928,3931,3934,3937,3940,3944,3947,3953,3959,3965,3973,3981],[20,3785,3786],{},"Multi-tenant database design is the foundation of every SaaS product. How you isolate tenant data affects performance, security, operational complexity, and your ability to offer different service tiers. Getting this right early prevents painful migrations when you have real customers with real data.",[20,3788,3789],{},"I have implemented all three major isolation strategies across different products. Here is when each one works and how to implement them reliably.",[15,3791,3793],{"id":3792},"shared-database-with-tenant-column","Shared Database with Tenant Column",[20,3795,3796,3797,3800],{},"The simplest and most common approach adds a ",[428,3798,3799],{},"tenant_id"," column to every table that holds tenant-specific data. All tenants share the same tables, and queries filter by tenant ID.",[20,3802,3803],{},"This works well for most SaaS products because it minimizes operational overhead. You run one database, execute one set of migrations, and maintain one connection pool. Database resources are shared efficiently — a small tenant using minimal storage does not waste dedicated resources.",[20,3805,3806,3807,3810],{},"The implementation in ",[34,3808,3809],{"href":50},"Prisma"," looks like this:",[1613,3812,3816],{"className":3813,"code":3814,"language":3815,"meta":138,"style":138},"language-prisma shiki shiki-themes github-dark","model Project {\n id String @id @default(cuid())\n tenantId String\n name String\n tenant Tenant @relation(fields: [tenantId], references: [id])\n\n @@index([tenantId])\n}\n","prisma",[428,3817,3818,3823,3828,3833,3838,3843,3847,3852],{"__ignoreMap":138},[797,3819,3820],{"class":1622,"line":1623},[797,3821,3822],{},"model Project {\n",[797,3824,3825],{"class":1622,"line":142},[797,3826,3827],{}," id String @id @default(cuid())\n",[797,3829,3830],{"class":1622,"line":139},[797,3831,3832],{}," tenantId String\n",[797,3834,3835],{"class":1622,"line":1710},[797,3836,3837],{}," name String\n",[797,3839,3840],{"class":1622,"line":1867},[797,3841,3842],{}," tenant Tenant @relation(fields: [tenantId], references: [id])\n",[797,3844,3845],{"class":1622,"line":1067},[797,3846,2046],{"emptyLinePlaceholder":158},[797,3848,3849],{"class":1622,"line":160},[797,3850,3851],{}," @@index([tenantId])\n",[797,3853,3854],{"class":1622,"line":1896},[797,3855,2156],{},[20,3857,3858],{},"The critical requirement is ensuring every query includes the tenant filter. A single unfiltered query leaks data across tenants — the most severe bug category in multi-tenant systems. Enforce this at the ORM level with middleware that automatically applies tenant scoping, and add defense in depth with PostgreSQL's Row-Level Security (RLS).",[20,3860,3861],{},"RLS policies operate at the database level, independent of your application code:",[1613,3863,3867],{"className":3864,"code":3865,"language":3866,"meta":138,"style":138},"language-sql shiki shiki-themes github-dark","ALTER TABLE projects ENABLE ROW LEVEL SECURITY;\nCREATE POLICY tenant_isolation ON projects\n USING (tenant_id = current_setting('app.current_tenant')::text);\n","sql",[428,3868,3869,3874,3879],{"__ignoreMap":138},[797,3870,3871],{"class":1622,"line":1623},[797,3872,3873],{},"ALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n",[797,3875,3876],{"class":1622,"line":142},[797,3877,3878],{},"CREATE POLICY tenant_isolation ON projects\n",[797,3880,3881],{"class":1622,"line":139},[797,3882,3883],{}," USING (tenant_id = current_setting('app.current_tenant')::text);\n",[20,3885,3886,3887,3890],{},"Set the ",[428,3888,3889],{},"app.current_tenant"," session variable at the start of each database connection, and PostgreSQL enforces isolation regardless of what your application code does. This catches the bugs that application-level middleware misses.",[20,3892,3893,3894,3896,3897,3901],{},"Index design matters more in shared tables. Every tenant-scoped query needs a composite index starting with ",[428,3895,3799],{},". Without it, the database scans the entire table to find one tenant's data. As your table grows to millions of rows across thousands of tenants, missing indexes cause cascading performance problems. Follow the ",[34,3898,3900],{"href":3899},"/blog/database-indexing-strategies","database indexing strategies"," that make shared-table multi-tenancy performant.",[15,3903,3905],{"id":3904},"schema-per-tenant","Schema-Per-Tenant",[20,3907,3908],{},"Schema isolation creates a separate database schema for each tenant within the same database server. Each schema has its own copy of every table, and tenants are isolated by the schema boundary.",[20,3910,3911],{},"This approach offers stronger isolation than shared tables. A query in tenant A's schema cannot accidentally access tenant B's data because the tables exist in different namespaces. It also allows schema customization per tenant — an enterprise customer can have additional columns or tables in their schema without affecting other tenants.",[20,3913,3914],{},"The operational trade-off is migration complexity. When you add a column or create a table, you run the migration across every schema. With 50 tenants this is manageable. With 5,000 tenants, migration deployment becomes a significant operation that needs automation, error handling, and the ability to roll back individual schemas that fail.",[20,3916,3917],{},"Connection management also changes. You need to set the search path or specify the schema for each database connection. In a connection pool, this means either maintaining separate pools per tenant (expensive in connection count) or dynamically switching schemas per request (which requires careful pool management to avoid leaking schema state between requests).",[20,3919,3920],{},"I recommend schema-per-tenant when you have dozens to low hundreds of tenants with compliance or customization needs that shared tables cannot satisfy. Healthcare and financial services clients often require this level of isolation for regulatory compliance.",[15,3922,3924],{"id":3923},"database-per-tenant","Database-Per-Tenant",[20,3926,3927],{},"The strongest isolation strategy gives each tenant their own database instance. Data is physically separated, and there is zero risk of cross-tenant access at the database level.",[20,3929,3930],{},"This is the right choice for enterprise SaaS products where tenants demand data residency (their data must live in a specific geographic region), complete isolation for compliance, independent backup and restore capabilities, or the ability to scale their database independently.",[20,3932,3933],{},"The operational cost is significant. You manage hundreds or thousands of database instances. Each needs monitoring, backup configuration, security patching, and connection management. Provisioning a new tenant means creating a new database, running migrations, configuring backups, and setting up monitoring — all automated, because doing this manually is not sustainable.",[20,3935,3936],{},"Infrastructure-as-code tools like Terraform or Pulumi help manage database fleet provisioning. Build a tenant provisioning pipeline that creates the database, runs migrations, seeds initial data, configures DNS, and registers the tenant in your routing layer — all triggered by a single API call or dashboard action.",[20,3938,3939],{},"Connection routing becomes a first-class concern. Your application needs to resolve the correct database connection for each incoming request based on the tenant identifier. Implement a connection registry that maps tenant IDs to connection strings, and cache the mapping to avoid a lookup on every request.",[15,3941,3943],{"id":3942},"choosing-your-strategy","Choosing Your Strategy",[20,3945,3946],{},"The decision framework is straightforward:",[20,3948,3949,3952],{},[191,3950,3951],{},"Start with shared database and tenant column"," if you are building a SaaS product from scratch, expect hundreds to thousands of tenants, and your tenants have similar data structures. This covers most B2B SaaS products and virtually all B2C products.",[20,3954,3955,3958],{},[191,3956,3957],{},"Move to schema-per-tenant"," when specific tenants need compliance-level isolation, you need per-tenant schema customization, or shared-table performance degrades for your largest tenants. You can migrate individual tenants from shared tables to dedicated schemas without a system-wide migration.",[20,3960,3961,3964],{},[191,3962,3963],{},"Use database-per-tenant"," for enterprise SaaS where tenants pay enough to justify the operational cost, data residency requirements mandate geographic separation, or tenants need independent backup and recovery. This is common in healthcare, finance, and government SaaS.",[20,3966,3967,3968,3972],{},"Many successful SaaS products use a hybrid approach. Small and medium tenants share a database with row-level security. Enterprise tenants get dedicated schemas or databases. This lets you serve the long tail efficiently while meeting enterprise requirements. The ",[34,3969,3971],{"href":3970},"/blog/saas-architecture-patterns","SaaS architecture patterns"," that support growth typically include this kind of tiered isolation.",[20,3974,3975,3976,3980],{},"Whichever strategy you choose, test your isolation guarantees. Write integration tests that attempt cross-tenant data access and verify they fail. Run these tests in CI on every deployment. A data isolation regression is not something you want to discover from a customer report. Build the ",[34,3977,3979],{"href":3978},"/blog/multi-tenant-architecture","multi-tenant architecture"," with defense in depth — application middleware, database policies, and automated testing all working together.",[2164,3982,3983],{},"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":138,"searchDepth":139,"depth":139,"links":3985},[3986,3987,3988,3989],{"id":3792,"depth":142,"text":3793},{"id":3904,"depth":142,"text":3905},{"id":3923,"depth":142,"text":3924},{"id":3942,"depth":142,"text":3943},"Engineering","Multi-tenant database design strategies — shared tables, schema-per-tenant, database-per-tenant, row-level security, and choosing the right isolation level.",[3993,3994],"multi-tenant database design","tenant isolation strategies",{},"/blog/multi-tenant-database-design",{"title":3780,"description":3991},"blog/multi-tenant-database-design",[4000,4001,579],"Multi-Tenancy","Database Design","Y0_tRVxx-TA7mQD_f3OG6ejd3kd103J2czDnQcHPV0k",[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,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,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,4172,4173,4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,4185,4186,4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197,4198,4199,4200,4201,4202,4203,4204,4205,4206,4207,4208,4209,4210,4211,4212,4213,4214,4215,4216,4217,4218,4219,4220,4221,4222,4223,4224,4225,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4238,4239,4240,4241,4242,4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,4332,4333,4334,4335,4336,4337,4338,4339,4340,4341,4342,4343,4344,4345,4346,4347,4348,4349,4350,4351,4352,4353,4354,4355,4356,4357,4358,4359,4360,4361,4362,4363,4364,4365,4366,4367,4368,4369,4370,4371,4372,4373,4374,4375,4376,4377,4378,4379,4380,4381,4382,4383,4384,4385,4386,4387,4388,4389,4390,4391,4392,4393,4394,4395,4396,4397,4398,4399,4400,4401,4402,4403,4404,4405,4406,4407,4408,4409,4410,4411,4412,4413,4414,4415,4416,4417,4418,4419,4420,4421,4422,4423,4424,4425,4426,4427,4428,4429,4430,4431,4432,4433,4434,4435,4436,4437,4438,4439,4440,4441,4442,4443,4444,4445,4446,4447,4448,4449,4450,4451,4452,4453,4454,4455,4456,4457,4458,4459,4460,4461,4462,4463,4464,4465,4466,4467,4468,4469,4470,4471,4472,4473,4474,4475,4476,4478,4479,4480,4481,4482,4483,4484,4485,4486,4487,4488,4489,4490,4491,4492,4493,4494,4495,4496,4497,4498,4499,4500,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4532,4533,4534,4535,4536,4537,4538,4539,4540,4541,4542,4543,4544,4545,4546,4547,4548,4549,4550,4551,4552,4553,4554,4555,4556,4557,4558,4559,4560,4561,4562,4563,4564,4565,4566,4567,4568,4569,4570,4571,4572,4573,4574,4575,4576,4577,4578,4579,4580,4581,4582,4583,4584,4585,4586,4587,4588,4589,4590,4591,4592,4593,4594,4595,4596,4597,4598,4599,4600,4601,4602,4603,4604,4605,4606,4607,4608,4609,4610,4611,4612,4613,4614,4615,4616,4617,4618,4619,4620,4621,4622,4623,4624,4625,4626,4627,4628,4629,4630,4631,4632,4633,4634,4635,4636,4637,4638,4639,4640,4641,4642,4643,4644,4645,4646],{"category":2174},{"category":359},{"category":927},{"category":3990},{"category":147},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":927},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":568},{"category":568},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":4043},"Security",{"category":4043},{"category":147},{"category":147},{"category":359},{"category":4043},{"category":359},{"category":568},{"category":4043},{"category":3990},{"category":147},{"category":3765},{"category":927},{"category":359},{"category":3990},{"category":568},{"category":3990},{"category":359},{"category":359},{"category":359},{"category":568},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":568},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":3765},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":3990},{"category":4087},"Career",{"category":927},{"category":927},{"category":147},{"category":568},{"category":147},{"category":3990},{"category":3990},{"category":147},{"category":3990},{"category":568},{"category":3990},{"category":3765},{"category":3765},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":568},{"category":568},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":927},{"category":568},{"category":147},{"category":3765},{"category":3765},{"category":3765},{"category":359},{"category":3990},{"category":3990},{"category":359},{"category":2174},{"category":927},{"category":3765},{"category":3765},{"category":4043},{"category":3765},{"category":147},{"category":927},{"category":359},{"category":3990},{"category":359},{"category":568},{"category":359},{"category":568},{"category":4043},{"category":359},{"category":359},{"category":3990},{"category":147},{"category":3990},{"category":2174},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":147},{"category":147},{"category":359},{"category":2174},{"category":4043},{"category":568},{"category":4043},{"category":2174},{"category":3990},{"category":3990},{"category":3765},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3765},{"category":3990},{"category":3990},{"category":359},{"category":359},{"category":4043},{"category":568},{"category":568},{"category":4087},{"category":4087},{"category":4087},{"category":147},{"category":3990},{"category":3765},{"category":568},{"category":359},{"category":359},{"category":3765},{"category":568},{"category":568},{"category":2174},{"category":3990},{"category":359},{"category":359},{"category":3990},{"category":359},{"category":3765},{"category":3765},{"category":359},{"category":4043},{"category":359},{"category":568},{"category":4043},{"category":568},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":4043},{"category":3990},{"category":3765},{"category":3765},{"category":147},{"category":3990},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":568},{"category":568},{"category":568},{"category":3990},{"category":359},{"category":359},{"category":359},{"category":3765},{"category":147},{"category":359},{"category":359},{"category":3990},{"category":359},{"category":3990},{"category":2174},{"category":359},{"category":147},{"category":147},{"category":3990},{"category":3990},{"category":927},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":3990},{"category":3765},{"category":3765},{"category":3765},{"category":568},{"category":359},{"category":359},{"category":359},{"category":359},{"category":568},{"category":359},{"category":568},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":147},{"category":147},{"category":359},{"category":3990},{"category":2174},{"category":568},{"category":4087},{"category":359},{"category":359},{"category":4043},{"category":3990},{"category":359},{"category":359},{"category":3765},{"category":359},{"category":2174},{"category":3765},{"category":3765},{"category":4043},{"category":3990},{"category":3990},{"category":568},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":4087},{"category":359},{"category":568},{"category":3990},{"category":3990},{"category":359},{"category":3765},{"category":359},{"category":359},{"category":359},{"category":2174},{"category":359},{"category":359},{"category":3990},{"category":359},{"category":3990},{"category":568},{"category":359},{"category":359},{"category":359},{"category":927},{"category":927},{"category":3990},{"category":359},{"category":3765},{"category":3765},{"category":359},{"category":3990},{"category":359},{"category":359},{"category":927},{"category":359},{"category":359},{"category":359},{"category":568},{"category":359},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":3990},{"category":4043},{"category":3990},{"category":3990},{"category":2174},{"category":3990},{"category":2174},{"category":2174},{"category":4043},{"category":568},{"category":3990},{"category":568},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":3990},{"category":147},{"category":3990},{"category":3990},{"category":359},{"category":568},{"category":927},{"category":927},{"category":359},{"category":359},{"category":359},{"category":359},{"category":147},{"category":3990},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":2174},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":3990},{"category":568},{"category":359},{"category":147},{"category":927},{"category":359},{"category":147},{"category":4043},{"category":359},{"category":4043},{"category":3990},{"category":3765},{"category":359},{"category":359},{"category":3990},{"category":359},{"category":568},{"category":359},{"category":359},{"category":3990},{"category":147},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":147},{"category":3990},{"category":3990},{"category":147},{"category":3765},{"category":3990},{"category":927},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":359},{"category":359},{"category":359},{"category":927},{"category":3990},{"category":3990},{"category":568},{"category":2174},{"category":3990},{"category":359},{"category":3990},{"category":568},{"category":147},{"category":147},{"category":2174},{"category":2174},{"category":359},{"category":147},{"category":4043},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":568},{"category":3990},{"category":3990},{"category":568},{"category":3990},{"category":3990},{"category":3990},{"category":4477},"Programming",{"category":3990},{"category":3990},{"category":568},{"category":568},{"category":3990},{"category":3990},{"category":147},{"category":4043},{"category":3990},{"category":147},{"category":3990},{"category":3990},{"category":3990},{"category":3990},{"category":3765},{"category":568},{"category":147},{"category":147},{"category":3990},{"category":3990},{"category":147},{"category":3990},{"category":4043},{"category":147},{"category":3990},{"category":3990},{"category":568},{"category":568},{"category":359},{"category":147},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":359},{"category":2174},{"category":359},{"category":3765},{"category":4043},{"category":4043},{"category":4043},{"category":4043},{"category":4043},{"category":4043},{"category":359},{"category":3990},{"category":3765},{"category":568},{"category":3765},{"category":568},{"category":3990},{"category":2174},{"category":359},{"category":568},{"category":2174},{"category":359},{"category":359},{"category":359},{"category":568},{"category":568},{"category":568},{"category":147},{"category":147},{"category":147},{"category":568},{"category":568},{"category":147},{"category":147},{"category":147},{"category":359},{"category":4043},{"category":3990},{"category":3765},{"category":3990},{"category":359},{"category":147},{"category":147},{"category":359},{"category":359},{"category":568},{"category":3990},{"category":568},{"category":568},{"category":568},{"category":2174},{"category":3990},{"category":359},{"category":359},{"category":147},{"category":147},{"category":568},{"category":3990},{"category":4087},{"category":568},{"category":4087},{"category":147},{"category":359},{"category":568},{"category":359},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":359},{"category":927},{"category":927},{"category":3765},{"category":359},{"category":359},{"category":359},{"category":359},{"category":3990},{"category":3990},{"category":2174},{"category":3990},{"category":4043},{"category":568},{"category":2174},{"category":2174},{"category":3990},{"category":3990},{"category":2174},{"category":2174},{"category":2174},{"category":4043},{"category":3990},{"category":3990},{"category":147},{"category":3990},{"category":568},{"category":359},{"category":359},{"category":568},{"category":359},{"category":359},{"category":568},{"category":359},{"category":3990},{"category":359},{"category":4043},{"category":359},{"category":359},{"category":359},{"category":3765},{"category":3765},{"category":4043},1772951194571]