[{"data":1,"prerenderedAt":4355},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-35":4,"blog-paginated-cats":3710},640,[5,404,778,976,1104,1204,1315,1447,1627,1736,2638,2822,2929,3428,3593],{"id":6,"title":7,"author":8,"body":11,"category":386,"date":387,"description":388,"extension":389,"featured":390,"image":391,"keywords":392,"meta":395,"navigation":186,"path":396,"readTime":183,"seo":397,"stem":398,"tags":399,"__hash__":403},"blog/blog/identity-access-management.md","Identity and Access Management for Modern Applications",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":380},"minimark",[14,18,22,25,30,33,36,39,42,56,60,63,70,73,79,85,341,344,348,351,354,357,360,364,367,370,373,376],[15,16,7],"h1",{"id":17},"identity-and-access-management-for-modern-applications",[19,20,21],"p",{},"Identity and access management is the discipline of ensuring that the right people have the right access to the right resources at the right time. That sentence sounds simple. Implementing it well across a modern application with multiple services, third-party integrations, and diverse user roles is anything but.",[19,23,24],{},"I have built IAM systems for SaaS platforms where a single misconfigured permission could expose one tenant's data to another. The stakes are high, and the margin for error is zero. Here is how to approach it.",[26,27,29],"h2",{"id":28},"authentication-vs-authorization-getting-the-distinction-right","Authentication vs Authorization: Getting the Distinction Right",[19,31,32],{},"These terms are used interchangeably in casual conversation, but they are fundamentally different concerns that should be implemented in separate layers.",[19,34,35],{},"Authentication answers \"who are you?\" It verifies that a user or service is who they claim to be. Passwords, multi-factor tokens, biometrics, API keys, and certificates are all authentication mechanisms. The output of authentication is a verified identity — a user ID, a service account name, a device identifier.",[19,37,38],{},"Authorization answers \"what are you allowed to do?\" Given a verified identity, what resources can they access, what operations can they perform, and under what conditions? The output of authorization is an access decision — allow or deny, with specific scope.",[19,40,41],{},"Keeping these concerns separate is not just a design nicety. It is a security requirement. Your authentication system should not need to know about your permission model. Your authorization system should not need to know how the identity was verified. This separation means you can change authentication methods — adding passkeys, migrating identity providers — without touching your authorization logic.",[19,43,44,45,50,51,55],{},"For a detailed treatment of authentication implementation, see the ",[46,47,49],"a",{"href":48},"/blog/authentication-security-guide","authentication security guide",". For token-based approaches, the ",[46,52,54],{"href":53},"/blog/jwt-authentication-guide","JWT authentication guide"," covers the specifics.",[26,57,59],{"id":58},"access-control-models","Access Control Models",[19,61,62],{},"There are several models for organizing authorization decisions, each with different trade-offs.",[19,64,65,69],{},[66,67,68],"strong",{},"Role-Based Access Control (RBAC)"," assigns permissions to roles, then assigns roles to users. An \"editor\" role can create and modify content. An \"admin\" role can do everything an editor can plus manage users and settings. This is the most common model because it is easy to understand and implement. It works well when your permission structure is relatively flat and does not change frequently.",[19,71,72],{},"The limitation of RBAC is role explosion. As your application grows, the number of distinct permission combinations increases, and you end up with dozens of highly specific roles — \"billing-admin-east-region\" or \"read-only-api-staging\" — that are difficult to manage and audit.",[19,74,75,78],{},[66,76,77],{},"Attribute-Based Access Control (ABAC)"," evaluates access based on attributes of the user, the resource, the action, and the environment. Instead of assigning a role that grants access to \"east region billing data,\" you define a policy: users with department=billing and region=east can read billing resources where resource.region=east. This is more flexible than RBAC but harder to reason about and debug.",[19,80,81,84],{},[66,82,83],{},"Relationship-Based Access Control (ReBAC)"," defines access based on the relationship between the user and the resource. Google Zanzibar pioneered this model for Google Drive, Docs, and other products. A user can edit a document because they are the owner, or because they belong to a group that was granted edit access, or because the document is in a folder where they have edit permissions. The relationship graph determines access.",[86,87,92],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":91},"language-typescript shiki shiki-themes github-dark","// RBAC: simple but inflexible\nconst permissions = {\n admin: [\"read\", \"write\", \"delete\", \"manage-users\"],\n editor: [\"read\", \"write\"],\n viewer: [\"read\"],\n};\n\n// ABAC: flexible but complex\nfunction evaluateAccess(\n user: User,\n resource: Resource,\n action: string\n): boolean {\n return policies.some(\n (policy) =>\n policy.action === action &&\n policy.userCondition(user) &&\n policy.resourceCondition(resource)\n );\n}\n","typescript","",[93,94,95,104,122,151,165,175,181,188,194,207,223,236,247,260,274,289,304,318,329,335],"code",{"__ignoreMap":91},[96,97,100],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sAwPA","// RBAC: simple but inflexible\n",[96,105,107,111,115,118],{"class":98,"line":106},2,[96,108,110],{"class":109},"snl16","const",[96,112,114],{"class":113},"sDLfK"," permissions",[96,116,117],{"class":109}," =",[96,119,121],{"class":120},"s95oV"," {\n",[96,123,125,128,132,135,138,140,143,145,148],{"class":98,"line":124},3,[96,126,127],{"class":120}," admin: [",[96,129,131],{"class":130},"sU2Wk","\"read\"",[96,133,134],{"class":120},", ",[96,136,137],{"class":130},"\"write\"",[96,139,134],{"class":120},[96,141,142],{"class":130},"\"delete\"",[96,144,134],{"class":120},[96,146,147],{"class":130},"\"manage-users\"",[96,149,150],{"class":120},"],\n",[96,152,154,157,159,161,163],{"class":98,"line":153},4,[96,155,156],{"class":120}," editor: [",[96,158,131],{"class":130},[96,160,134],{"class":120},[96,162,137],{"class":130},[96,164,150],{"class":120},[96,166,168,171,173],{"class":98,"line":167},5,[96,169,170],{"class":120}," viewer: [",[96,172,131],{"class":130},[96,174,150],{"class":120},[96,176,178],{"class":98,"line":177},6,[96,179,180],{"class":120},"};\n",[96,182,184],{"class":98,"line":183},7,[96,185,187],{"emptyLinePlaceholder":186},true,"\n",[96,189,191],{"class":98,"line":190},8,[96,192,193],{"class":102},"// ABAC: flexible but complex\n",[96,195,197,200,204],{"class":98,"line":196},9,[96,198,199],{"class":109},"function",[96,201,203],{"class":202},"svObZ"," evaluateAccess",[96,205,206],{"class":120},"(\n",[96,208,210,214,217,220],{"class":98,"line":209},10,[96,211,213],{"class":212},"s9osk"," user",[96,215,216],{"class":109},":",[96,218,219],{"class":202}," User",[96,221,222],{"class":120},",\n",[96,224,226,229,231,234],{"class":98,"line":225},11,[96,227,228],{"class":212}," resource",[96,230,216],{"class":109},[96,232,233],{"class":202}," Resource",[96,235,222],{"class":120},[96,237,239,242,244],{"class":98,"line":238},12,[96,240,241],{"class":212}," action",[96,243,216],{"class":109},[96,245,246],{"class":113}," string\n",[96,248,250,253,255,258],{"class":98,"line":249},13,[96,251,252],{"class":120},")",[96,254,216],{"class":109},[96,256,257],{"class":113}," boolean",[96,259,121],{"class":120},[96,261,263,266,269,272],{"class":98,"line":262},14,[96,264,265],{"class":109}," return",[96,267,268],{"class":120}," policies.",[96,270,271],{"class":202},"some",[96,273,206],{"class":120},[96,275,277,280,283,286],{"class":98,"line":276},15,[96,278,279],{"class":120}," (",[96,281,282],{"class":212},"policy",[96,284,285],{"class":120},") ",[96,287,288],{"class":109},"=>\n",[96,290,292,295,298,301],{"class":98,"line":291},16,[96,293,294],{"class":120}," policy.action ",[96,296,297],{"class":109},"===",[96,299,300],{"class":120}," action ",[96,302,303],{"class":109},"&&\n",[96,305,307,310,313,316],{"class":98,"line":306},17,[96,308,309],{"class":120}," policy.",[96,311,312],{"class":202},"userCondition",[96,314,315],{"class":120},"(user) ",[96,317,303],{"class":109},[96,319,321,323,326],{"class":98,"line":320},18,[96,322,309],{"class":120},[96,324,325],{"class":202},"resourceCondition",[96,327,328],{"class":120},"(resource)\n",[96,330,332],{"class":98,"line":331},19,[96,333,334],{"class":120}," );\n",[96,336,338],{"class":98,"line":337},20,[96,339,340],{"class":120},"}\n",[19,342,343],{},"For most applications, start with RBAC and add attribute-based policies only where RBAC breaks down. This gives you simplicity where it works and flexibility where you need it.",[26,345,347],{"id":346},"multi-tenancy-and-tenant-isolation","Multi-Tenancy and Tenant Isolation",[19,349,350],{},"In SaaS applications, IAM must enforce tenant isolation — ensuring that users in one organization can never access data belonging to another. This is the most critical security property in a multi-tenant system, and it cannot be achieved through authorization alone.",[19,352,353],{},"Tenant isolation should be enforced at multiple layers. At the application layer, every database query should include a tenant filter. At the API layer, every request should be scoped to the authenticated user's tenant. At the data layer, consider row-level security policies in your database that enforce tenant boundaries regardless of what the application code does.",[19,355,356],{},"The defense-in-depth approach matters here because a single bug in a single query — a missing WHERE clause — can expose cross-tenant data. If your only protection is application-level filtering, that one bug is a breach. If you also have database-level row security, the database rejects the query even when the application code is wrong.",[19,358,359],{},"Test tenant isolation explicitly. Write tests that authenticate as user A in tenant 1 and attempt to access resources belonging to tenant 2. These tests should exist for every endpoint that returns tenant-scoped data. They should fail loudly and never be skipped.",[26,361,363],{"id":362},"lifecycle-management","Lifecycle Management",[19,365,366],{},"Identities have a lifecycle: provisioning, modification, and deprovisioning. The security of your IAM system depends as much on deprovisioning as on provisioning. A former employee whose access was never revoked is one of the most common attack vectors in enterprise breaches.",[19,368,369],{},"Automate provisioning and deprovisioning through your identity provider. When HR terminates an employee in the HR system, that event should propagate to your identity provider, which revokes all access tokens and disables the account. Manual deprovisioning processes are unreliable — they depend on someone remembering to revoke access from every system the employee used.",[19,371,372],{},"Implement access reviews on a regular cadence. Quarterly reviews where managers confirm that their team members' access is appropriate catch permissions that accumulated over time but are no longer needed. An engineer who temporarily needed production database access six months ago should not still have it.",[19,374,375],{},"Monitor for anomalous access patterns. A service account that normally makes a hundred API calls per hour suddenly making ten thousand is suspicious. A user who normally accesses resources in one region accessing resources in five regions is suspicious. These patterns may be legitimate, but they deserve investigation. Building IAM well means treating it as an ongoing operational concern, not a one-time implementation.",[377,378,379],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .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":91,"searchDepth":124,"depth":124,"links":381},[382,383,384,385],{"id":28,"depth":106,"text":29},{"id":58,"depth":106,"text":59},{"id":346,"depth":106,"text":347},{"id":362,"depth":106,"text":363},"Security","2025-08-28","IAM is where authentication meets authorization. Here's how to design identity systems that scale with your application without becoming a security liability.","md",false,null,[393,394],"identity access management","IAM modern applications",{},"/blog/identity-access-management",{"title":7,"description":388},"blog/identity-access-management",[400,401,402],"Identity Management","Access Control","Authentication","iQrFMl9Mst0NjWv3ca3Xw7bhmsHufHDSk_kDPG03RB4",{"id":405,"title":406,"author":407,"body":408,"category":759,"date":387,"description":760,"extension":389,"featured":390,"image":391,"keywords":761,"meta":767,"navigation":186,"path":768,"readTime":183,"seo":769,"stem":770,"tags":771,"__hash__":777},"blog/blog/scottish-surnames-origins.md","Scottish Surnames: What Your Name Reveals About Your Ancestors",{"name":9,"bio":10},{"type":12,"value":409,"toc":746},[410,414,417,420,424,427,432,448,451,454,458,461,481,487,497,503,513,517,520,526,532,538,544,550,556,560,563,572,582,591,600,609,613,616,622,628,634,640,643,647,650,656,666,672,681,685,688,699,705,711,719,722,726],[26,411,413],{"id":412},"your-name-is-a-document","Your Name Is a Document",[19,415,416],{},"A Scottish surname is not merely a label. It is a compressed historical document -- encoding, in a few syllables, information about your ancestors' language, location, occupation, parentage, or physical characteristics. The surname traditions of Scotland are among the most complex in Europe, reflecting the layered linguistic history of a country where Gaelic, Brythonic Celtic, Norse, Scots, and English have each left their mark on the naming conventions.",[19,418,419],{},"Understanding the structure of Scottish surnames is the first step in any genealogical investigation. Before you search parish records or order a DNA test, your name itself may tell you where to look.",[26,421,423],{"id":422},"the-four-types-of-scottish-surnames","The Four Types of Scottish Surnames",[19,425,426],{},"Scottish surnames fall into four broad categories, each representing a different naming convention:",[428,429,431],"h3",{"id":430},"patronymic-names-macmc","Patronymic Names (Mac/Mc)",[19,433,434,435,439,440,443,444,447],{},"The most distinctively Scottish surnames are the patronymics -- names beginning with ",[436,437,438],"em",{},"Mac"," or ",[436,441,442],{},"Mc",", from the Gaelic word ",[436,445,446],{},"mac"," meaning \"son.\" MacDonald means \"son of Donald.\" MacLeod means \"son of Leod.\" MacKenzie means \"son of Coinneach\" (Kenneth).",[19,449,450],{},"In the original Gaelic system, patronymics were fluid -- they changed with each generation. A man named Domhnall mac Alasdair mhic Iain (Donald, son of Alasdair, son of John) would have a son named Seumas mac Dhomhnaill (James, son of Donald). The surname changed with each father.",[19,452,453],{},"The freezing of patronymics into fixed hereditary surnames occurred gradually, beginning in the lowlands in the twelfth and thirteenth centuries and not becoming universal in the Highlands until the sixteenth or seventeenth century. When the name froze, it captured a specific ancestor -- the Donald, the Kenneth, the Leod whose name would be carried forward by all subsequent generations.",[428,455,457],{"id":456},"territorial-and-clan-names","Territorial and Clan Names",[19,459,460],{},"Some Scottish surnames derive from territorial designations rather than parentage. These names indicate where a family held land or which clan they belonged to.",[19,462,463,466,467,470,471,475,476,480],{},[66,464,465],{},"Ross"," -- from the Gaelic ",[436,468,469],{},"ros",", meaning \"headland\" or \"promontory,\" referring to the ",[46,472,474],{"href":473},"/blog/ross-shire-geography-history","Ross peninsula"," in the northern Highlands. The ",[46,477,479],{"href":478},"/blog/ross-surname-origin-meaning","Ross surname"," is territorial: it identifies the family with the land rather than with a single ancestor.",[19,482,483,486],{},[66,484,485],{},"Murray"," -- from Moray, the province in northeastern Scotland.",[19,488,489,492,493,496],{},[66,490,491],{},"Sutherland"," -- from the Norse ",[436,494,495],{},"sudhrland",", \"southern land\" (the Norse considered it south of their Caithness and Orkney territories).",[19,498,499,502],{},[66,500,501],{},"Forbes"," -- from the place Forbes in Aberdeenshire.",[19,504,505,508,509,512],{},[66,506,507],{},"Grant"," -- possibly from the Norman French ",[436,510,511],{},"grand"," (large), but adopted as a territorial identifier in Strathspey.",[428,514,516],{"id":515},"occupational-names","Occupational Names",[19,518,519],{},"Some Scottish surnames derive from trades and occupations:",[19,521,522,525],{},[66,523,524],{},"Baxter"," -- a baker (from the Scots word for baker).",[19,527,528,531],{},[66,529,530],{},"Fletcher"," -- an arrow-maker.",[19,533,534,537],{},[66,535,536],{},"MacIntyre"," (Mac an t-Saoir) -- \"son of the carpenter.\"",[19,539,540,543],{},[66,541,542],{},"MacPherson"," (Mac a' Phearsain) -- \"son of the parson.\"",[19,545,546,549],{},[66,547,548],{},"MacNab"," (Mac an Aba) -- \"son of the abbot.\"",[19,551,552,555],{},[66,553,554],{},"MacTaggart"," (Mac an t-Sagairt) -- \"son of the priest.\" This is the epithet of Fearchar, the first Earl of Ross, whose descendants became the chiefs of Clan Ross.",[428,557,559],{"id":558},"descriptive-names","Descriptive Names",[19,561,562],{},"The final category includes names derived from physical characteristics or personal qualities:",[19,564,565,466,568,571],{},[66,566,567],{},"Campbell",[436,569,570],{},"cam beul",", \"crooked mouth.\"",[19,573,574,577,578,581],{},[66,575,576],{},"Cameron"," -- from ",[436,579,580],{},"cam shron",", \"crooked nose.\"",[19,583,584,466,587,590],{},[66,585,586],{},"Boyd",[436,588,589],{},"buidhe",", \"yellow\" or \"fair-haired.\"",[19,592,593,466,596,599],{},[66,594,595],{},"Duff",[436,597,598],{},"dubh",", \"dark\" or \"black.\"",[19,601,602,466,605,608],{},[66,603,604],{},"Bain",[436,606,607],{},"ban",", \"white, fair.\"",[26,610,612],{"id":611},"the-norse-layer","The Norse Layer",[19,614,615],{},"In areas of Scotland settled by Norse speakers -- the Northern Isles, the Western Isles, Caithness, and parts of the northwest Highlands -- surnames preserve Norse naming conventions:",[19,617,618,621],{},[66,619,620],{},"MacLeod"," -- from the Norse personal name Ljot.",[19,623,624,627],{},[66,625,626],{},"MacAulay"," -- possibly from the Norse Olaf.",[19,629,630,633],{},[66,631,632],{},"Gunn"," -- from the Norse personal name Gunni.",[19,635,636,639],{},[66,637,638],{},"MacIver"," -- from the Norse Ivar.",[19,641,642],{},"The Norse layer in Scottish surnames reflects the Viking Age settlement of Scotland's northern and western fringes, which left a permanent linguistic mark on the naming traditions of those regions.",[26,644,646],{"id":645},"the-lowland-scots-and-anglo-norman-layer","The Lowland Scots and Anglo-Norman Layer",[19,648,649],{},"In lowland Scotland, many surnames reflect the Anglo-Norman and Scots-speaking culture that dominated from the twelfth century onward:",[19,651,652,655],{},[66,653,654],{},"Bruce"," -- from the Norman place name Brix.",[19,657,658,661,662,665],{},[66,659,660],{},"Wallace"," -- from the Old French ",[436,663,664],{},"waleis",", meaning \"Welsh\" or \"foreign\" (i.e., Brythonic-speaking, from the perspective of the Scots-speaking lowlanders).",[19,667,668,671],{},[66,669,670],{},"Stewart / Stuart"," -- from the office of High Steward of Scotland.",[19,673,674,466,677,680],{},[66,675,676],{},"Douglas",[436,678,679],{},"dubh glas",", \"dark water,\" but adopted as a lowland surname.",[26,682,684],{"id":683},"what-your-surname-cannot-tell-you","What Your Surname Cannot Tell You",[19,686,687],{},"A Scottish surname provides a starting point, not a complete genealogy. Several caveats apply:",[19,689,690,693,694,698],{},[66,691,692],{},"Septs and adopted names."," The ",[46,695,697],{"href":696},"/blog/scottish-clan-system-explained","Scottish clan system"," was not purely genealogical. Smaller families (septs) attached themselves to larger clans for protection and adopted or were assigned the clan surname. A man named Ross in the eighteenth century may have been a genealogical descendant of the Ross chiefs, or he may have been a member of a sept family that adopted the Ross name for practical reasons.",[19,700,701,704],{},[66,702,703],{},"Anglicization."," Many Gaelic surnames were Anglicized -- sometimes translated, sometimes phonetically approximated -- during the seventeenth through nineteenth centuries. The original Gaelic form may reveal information that the Anglicized version obscures.",[19,706,707,710],{},[66,708,709],{},"Freezing point."," The generation at which a patronymic name froze into a hereditary surname varies. Two unrelated families may carry the same Mac-surname simply because their respective ancestors both happened to have a father named Donald when the name froze.",[19,712,713,714,718],{},"For deeper resolution, ",[46,715,717],{"href":716},"/blog/what-is-genetic-genealogy","genetic genealogy"," -- particularly Y-chromosome testing -- can determine whether two men sharing a Scottish surname are actually related in the paternal line, or whether their shared name reflects separate adoption of the same patronymic.",[720,721],"hr",{},[26,723,725],{"id":724},"related-articles","Related Articles",[727,728,729,735,740],"ul",{},[730,731,732],"li",{},[46,733,734],{"href":478},"The Ross Surname: Scottish Origins, Meaning, and Where the Name Came From",[730,736,737],{},[46,738,739],{"href":696},"The Scottish Clan System Explained",[730,741,742],{},[46,743,745],{"href":744},"/blog/place-names-celtic-history","Reading the Landscape: Celtic Place Names and Hidden History",{"title":91,"searchDepth":124,"depth":124,"links":747},[748,749,755,756,757,758],{"id":412,"depth":106,"text":413},{"id":422,"depth":106,"text":423,"children":750},[751,752,753,754],{"id":430,"depth":124,"text":431},{"id":456,"depth":124,"text":457},{"id":515,"depth":124,"text":516},{"id":558,"depth":124,"text":559},{"id":611,"depth":106,"text":612},{"id":645,"depth":106,"text":646},{"id":683,"depth":106,"text":684},{"id":724,"depth":106,"text":725},"Heritage","Scottish surnames encode centuries of history -- from Gaelic patronymics to Norse nicknames to territorial clan names. Here is how to decode what your Scottish surname tells you about your family's origins, occupation, and place in the clan system.",[762,763,764,765,766],"scottish surnames origins","scottish surname meanings","mac mc scottish names","clan names scotland","gaelic surname origins",{},"/blog/scottish-surnames-origins",{"title":406,"description":760},"blog/scottish-surnames-origins",[772,773,774,775,776],"Scottish Surnames","Scottish History","Clan System","Genealogy","Gaelic Names","jEoNWoGKGeTLkzImJllL7qTJvLZn1zO9TDAqAZjcxuI",{"id":779,"title":780,"author":781,"body":782,"category":759,"date":387,"description":957,"extension":389,"featured":390,"image":391,"keywords":958,"meta":965,"navigation":186,"path":966,"readTime":183,"seo":967,"stem":968,"tags":969,"__hash__":975},"blog/blog/snp-mutations-explained.md","SNP Mutations: The Genetic Markers That Track Ancestry",{"name":9,"bio":10},{"type":12,"value":783,"toc":949},[784,788,791,798,801,805,813,816,852,855,858,862,865,871,877,880,883,887,890,893,896,899,903,910,913,924,927,929,931],[26,785,787],{"id":786},"one-letter-one-moment-permanent-record","One Letter, One Moment, Permanent Record",[19,789,790],{},"Your genome is a string of roughly 3.2 billion characters, written in an alphabet of four letters: A, T, C, and G. Every time a cell divides, this entire sequence is copied. The copying machinery is remarkably accurate — but not perfect. Occasionally, a single letter is copied incorrectly. An A becomes a G. A T becomes a C. One letter, one position, one moment of imperfection.",[19,792,793,794,797],{},"This is a ",[66,795,796],{},"SNP"," — a Single Nucleotide Polymorphism (pronounced \"snip\"). It is the smallest possible change in a genome: one base pair, altered. And yet these tiny errors are the foundation of nearly everything we know about human ancestry, migration, and deep genealogy.",[19,799,800],{},"Once a SNP occurs in a reproductive cell and is passed to a child, it becomes a permanent part of that child's genome — and the genomes of all their descendants. It cannot be reversed. It cannot be undone by subsequent mutations at other positions. It is, in effect, a timestamp and a signature: a mark that says \"this lineage diverged from its relatives Now.\"",[26,802,804],{"id":803},"how-snps-define-haplogroups","How SNPs Define Haplogroups",[19,806,807,808,812],{},"The ",[46,809,811],{"href":810},"/blog/y-dna-haplogroups-explained","Y-DNA haplogroup system"," is built entirely on SNPs. Each branch point in the haplogroup tree corresponds to a SNP that occurred in a single man at a specific moment in the past. All of his male-line descendants carry that SNP. No one else does.",[19,814,815],{},"Consider the chain of SNPs that defines haplogroup R1b-L21:",[727,817,818,824,830,836,842],{},[730,819,820,823],{},[66,821,822],{},"M207"," occurred roughly 28,000 years ago, defining haplogroup R",[730,825,826,829],{},[66,827,828],{},"M343"," occurred roughly 22,000 years ago, defining R1b",[730,831,832,835],{},[66,833,834],{},"M269"," occurred roughly 6,000–7,000 years ago, defining the Western European branch",[730,837,838,841],{},[66,839,840],{},"P312"," occurred roughly 4,500 years ago, during the Bell Beaker expansion",[730,843,844,847,848],{},[66,845,846],{},"L21"," occurred roughly 4,000 years ago, defining the ",[46,849,851],{"href":850},"/blog/r1b-l21-atlantic-celtic-haplogroup","Atlantic Celtic branch",[19,853,854],{},"Each SNP is nested within the previous one. If you carry L21, you necessarily also carry P312, M269, M343, and M207 — because L21 occurred in a man who already carried all those earlier mutations. The SNP chain is cumulative and irreversible.",[19,856,857],{},"This nesting structure is what allows geneticists to build a tree. The tree is not a guess or an interpretation — it is a direct reading of accumulated mutations. Two men who share a SNP share a common patrilineal ancestor in whom that SNP first occurred. The more recent the shared SNP, the more recently they diverged.",[26,859,861],{"id":860},"snps-versus-strs-two-different-clocks","SNPs Versus STRs: Two Different Clocks",[19,863,864],{},"Genetic genealogy uses two types of Y-chromosome markers, and understanding the difference is essential for interpreting test results.",[19,866,867,870],{},[66,868,869],{},"SNPs"," (Single Nucleotide Polymorphisms) are permanent single-letter changes. They occur rarely — roughly once every 80 to 145 years on the Y-chromosome — and they do not reverse. SNPs are the gold standard for placing a man on the haplogroup tree and determining his deep ancestral lineage.",[19,872,873,876],{},[66,874,875],{},"STRs"," (Short Tandem Repeats) are regions where a short sequence of DNA is repeated multiple times. The number of repeats can increase or decrease from one generation to the next. STRs mutate much faster than SNPs, which makes them useful for distinguishing between closely related lineages — men who share the same SNP haplogroup but diverged within the last several hundred years.",[19,878,879],{},"Think of SNPs as chapter headings and STRs as page numbers. SNPs tell you which chapter of the human story your patriline belongs to. STRs tell you which page within that chapter — how closely you are related to other men in the same haplogroup.",[19,881,882],{},"The FamilyTreeDNA Big Y-700 test sequences both: it reads hundreds of thousands of SNP positions to assign your terminal haplogroup, and it measures 700+ STR markers to estimate genetic distance from other tested men. The combination provides both deep ancestry placement and recent-genealogy matching.",[26,884,886],{"id":885},"how-scientists-date-snp-mutations","How Scientists Date SNP Mutations",[19,888,889],{},"Because SNPs accumulate at a roughly constant rate — the so-called \"molecular clock\" — geneticists can estimate when a particular mutation occurred by counting the number of SNPs that have accumulated since then.",[19,891,892],{},"The method works like this: if two men share a common ancestor defined by SNP X, and one man carries five additional SNPs that the other does not (and vice versa), then roughly ten SNP mutations have occurred since their common ancestor. If the average rate is one SNP per 80–145 years, their common ancestor lived roughly 800 to 1,450 years ago.",[19,894,895],{},"This is a simplification — the actual statistical methods are more sophisticated, using Bayesian analysis and calibration against known historical dates — but the principle is straightforward. SNPs are a clock. Count them, calibrate the rate, and you can date the divergence of any two lineages.",[19,897,898],{},"The molecular clock is what allows researchers to assign dates to haplogroup branches. When we say R1b-L21 arose approximately 4,000 years ago, that estimate comes from counting the SNPs that have accumulated in L21's descendant branches and running the clock backward. The dates are approximate — the confidence intervals can span several centuries — but they are anchored in measurable physical evidence rather than historical speculation.",[26,900,902],{"id":901},"what-your-snp-results-mean-for-genealogy","What Your SNP Results Mean for Genealogy",[19,904,905,906,909],{},"When you receive Y-DNA results from a test like FamilyTreeDNA's Big Y-700, the most important piece of information is your ",[66,907,908],{},"terminal SNP"," — the most recent, most specific SNP you carry. This is your finest-resolution placement on the haplogroup tree.",[19,911,912],{},"A terminal SNP like FGC11134 (a subclade within R1b-L21) places you on a specific branch that diverged from other L21 branches at a datable point in time. Men who share your terminal SNP are your closest patrilineal relatives within the haplogroup tree. Men who share an upstream SNP but not your terminal SNP diverged from your line further back.",[19,914,915,916,918,919,923],{},"The practical application for ",[46,917,717],{"href":716}," is direct. By joining a ",[46,920,922],{"href":921},"/blog/dna-surname-projects","DNA surname project"," and comparing terminal SNPs with other tested men, you can determine whether men who share your surname also share your patrilineal ancestry — or whether the surname arose independently in multiple unrelated families.",[19,925,926],{},"SNPs are the most reliable markers in genetic genealogy because they are effectively permanent. STRs can mutate back and forth, creating ambiguity. SNPs do not. A shared SNP is a shared ancestor — no interpretation required.",[720,928],{},[26,930,725],{"id":724},[727,932,933,938,943],{},[730,934,935],{},[46,936,937],{"href":810},"Y-DNA Haplogroups Explained: The Paternal Lineage Map",[730,939,940],{},[46,941,942],{"href":716},"What Is Genetic Genealogy? A Beginner's Guide",[730,944,945],{},[46,946,948],{"href":947},"/blog/haplogroup-migration-maps","Haplogroup Migration Maps: Visualizing Human Movement",{"title":91,"searchDepth":124,"depth":124,"links":950},[951,952,953,954,955,956],{"id":786,"depth":106,"text":787},{"id":803,"depth":106,"text":804},{"id":860,"depth":106,"text":861},{"id":885,"depth":106,"text":886},{"id":901,"depth":106,"text":902},{"id":724,"depth":106,"text":725},"SNP mutations are single-letter changes in DNA that accumulate over generations and allow scientists to trace ancestry across thousands of years. Here's what they are, how they work, and why they matter for genetic genealogy.",[959,960,961,962,963,964],"snp mutation explained","single nucleotide polymorphism","snp genetic genealogy","how snp mutations work","dna markers ancestry","snp vs str markers",{},"/blog/snp-mutations-explained",{"title":780,"description":957},"blog/snp-mutations-explained",[970,971,972,973,974],"SNP Mutations","Genetic Genealogy","DNA Science","Haplogroups","Y-Chromosome","JfSz0W_2WUUIY-XVS17ppqjFM8zCkmYeyRuA5WujI9o",{"id":977,"title":978,"author":979,"body":980,"category":1090,"date":387,"description":1091,"extension":389,"featured":390,"image":391,"keywords":1092,"meta":1095,"navigation":186,"path":1096,"readTime":183,"seo":1097,"stem":1098,"tags":1099,"__hash__":1103},"blog/blog/technical-writing-developers.md","Technical Writing for Developers: Communicate Complex Ideas Clearly",{"name":9,"bio":10},{"type":12,"value":981,"toc":1084},[982,986,989,992,995,997,1001,1004,1010,1013,1019,1022,1024,1028,1031,1038,1046,1049,1051,1055,1058,1069,1075,1081],[26,983,985],{"id":984},"writing-is-the-multiplier-most-developers-ignore","Writing Is the Multiplier Most Developers Ignore",[19,987,988],{},"Every developer communicates in writing constantly — commit messages, pull request descriptions, documentation, Slack messages, emails, design proposals, bug reports. The quality of that writing directly affects how effectively they collaborate with their team, how quickly new contributors get productive, and how well stakeholders understand technical decisions.",[19,990,991],{},"Yet most developers treat writing as a chore, something to rush through on the way to the real work of coding. This is backwards. Code that no one understands is code that no one maintains. A brilliant architecture that can't be explained is an architecture that gets replaced when you leave. A bug fix with a one-word commit message is a mystery to the person who encounters the same area of code six months from now.",[19,993,994],{},"The developers I've worked with who advanced fastest in their careers — regardless of whether that meant senior engineer, architect, or CTO — were consistently the best writers on their teams. Not because writing ability was rewarded explicitly, but because clear communication builds trust, reduces misunderstandings, and makes everything move faster.",[720,996],{},[26,998,1000],{"id":999},"the-two-rules-of-technical-writing","The Two Rules of Technical Writing",[19,1002,1003],{},"Every technical writing problem can be solved by applying two principles.",[19,1005,1006,1009],{},[66,1007,1008],{},"Know your audience."," The same concept, explained three different ways, is appropriate for three different audiences. An explanation of database indexing for a fellow backend developer focuses on B-tree structure and query planning. For a frontend developer, it focuses on which queries will be fast and which will be slow. For a product manager, it focuses on the performance impact users will experience. None of these explanations is better or worse — they're optimized for different readers.",[19,1011,1012],{},"Before writing anything, ask: who will read this, what do they already know, and what do they need to know? The answers determine your vocabulary, your level of abstraction, and your examples. Writing \"use a distributed cache layer to mitigate latency degradation\" when you could write \"add Redis to make database queries faster\" doesn't demonstrate expertise. It demonstrates unawareness of the reader.",[19,1014,1015,1018],{},[66,1016,1017],{},"Structure before detail."," Readers need the big picture before the specifics. Start with why something matters, then what it is, then how it works. This isn't just a writing convention — it reflects how humans process information. We anchor new details to existing understanding, so you need to establish the anchor before providing the details.",[19,1020,1021],{},"In practice, this means your design document leads with the problem and the recommended solution, not with a detailed analysis of every option considered. It means your README starts with what the project does and how to use it, not with the installation prerequisites. The details matter, but they belong after the reader understands why they should care.",[720,1023],{},[26,1025,1027],{"id":1026},"writing-better-documentation","Writing Better Documentation",[19,1029,1030],{},"Good documentation has a clear scope. It answers a specific question or enables a specific task. Bad documentation tries to be comprehensive and ends up being unusable. A README that covers everything about a project at length is less useful than one that covers the three things a new developer needs in their first hour.",[19,1032,1033,1034,1037],{},"Write task-oriented documentation. Instead of describing what a system does, describe how to accomplish specific goals with it. \"The authentication module supports JWT-based sessions with configurable expiration\" is a description. \"To add authentication to a new route, wrap it with the ",[93,1035,1036],{},"requireAuth"," middleware\" is a task. Users come to documentation with a job to do. Help them do it.",[19,1039,1040,1041,1045],{},"Include examples liberally. A code example is worth paragraphs of explanation. When you document an API endpoint, show a complete request and response. When you document a function, show it being called with realistic parameters. When you describe a configuration option, show a complete configuration file with that option highlighted. I've written about ",[46,1042,1044],{"href":1043},"/blog/software-documentation-best-practices","documentation best practices"," in more detail, but the single most impactful improvement you can make is adding more examples.",[19,1047,1048],{},"Keep documentation close to the code it describes. Documentation in a wiki, separate from the codebase, is documentation that will be outdated within a month. Documentation in a markdown file next to the source code gets updated when the code changes, because it's visible during code review. Proximity is the best enforcement mechanism for documentation accuracy.",[720,1050],{},[26,1052,1054],{"id":1053},"writing-that-advances-your-career","Writing That Advances Your Career",[19,1056,1057],{},"Beyond documentation, three types of writing create disproportionate career leverage.",[19,1059,1060,1063,1064,1068],{},[66,1061,1062],{},"Design proposals"," that clearly articulate a problem, evaluate options with honest trade-offs, and recommend a specific approach demonstrate architectural thinking. Writing a thorough design document is one of the most direct paths to being perceived as a senior engineer, because it's the artifact that shows you think beyond individual tasks. The ",[46,1065,1067],{"href":1066},"/blog/software-architect-skills","skills that define a software architect"," are largely demonstrated through written artifacts.",[19,1070,1071,1074],{},[66,1072,1073],{},"Technical blog posts"," establish external credibility and create a public record of your expertise. When a hiring manager googles your name and finds thoughtful technical content, you've already passed a filter that most candidates haven't. The compounding value of a technical blog is substantial — articles you write today generate credibility for years.",[19,1076,1077,1080],{},[66,1078,1079],{},"Incident reports and postmortems"," are underrated career accelerators. The ability to analyze a production incident, identify root causes, and communicate findings clearly to both technical and non-technical stakeholders is a rare and valued skill. Engineers who write excellent postmortems become trusted with more responsibility, because leadership sees them as people who learn from failures and prevent recurrence.",[19,1082,1083],{},"Every piece of writing is practice. Commit messages, code comments, PR descriptions — treat each one as an opportunity to communicate clearly. The cumulative effect of thousands of small writing improvements over a career is enormous, and unlike technical skills that become outdated, clear communication never becomes obsolete.",{"title":91,"searchDepth":124,"depth":124,"links":1085},[1086,1087,1088,1089],{"id":984,"depth":106,"text":985},{"id":999,"depth":106,"text":1000},{"id":1026,"depth":106,"text":1027},{"id":1053,"depth":106,"text":1054},"Career","How developers can improve their technical writing. Practical techniques for documentation, blog posts, proposals, and architectural documents that people read.",[1093,1094],"technical writing for developers","developer communication skills",{},"/blog/technical-writing-developers",{"title":978,"description":1091},"blog/technical-writing-developers",[1100,1101,1102],"Technical Writing","Communication","Career Development","VjDrPCT14Pn0yo_qittMedEZOOy6axlpgiM3FcEHPDo",{"id":1105,"title":1106,"author":1107,"body":1108,"category":759,"date":1186,"description":1187,"extension":389,"featured":390,"image":391,"keywords":1188,"meta":1194,"navigation":186,"path":1195,"readTime":183,"seo":1196,"stem":1197,"tags":1198,"__hash__":1203},"blog/blog/scottish-dance-traditions.md","Scottish Dance Traditions: From Reel to Ceilidh",{"name":9,"bio":10},{"type":12,"value":1109,"toc":1180},[1110,1114,1117,1125,1128,1132,1135,1138,1146,1150,1153,1156,1159,1163,1166,1169,1172],[26,1111,1113],{"id":1112},"dance-in-scottish-life","Dance in Scottish Life",[19,1115,1116],{},"Dance has been central to Scottish social life for as long as we have records to document it. It is mentioned in medieval accounts of Highland gatherings, described by travelers visiting the Highlands and Lowlands alike, and depicted in the visual art of every period. More than mere recreation, dance in Scotland served social functions: it brought communities together, marked celebrations and seasonal transitions, provided courtship opportunities, and demonstrated the physical prowess and grace that were valued in Highland culture.",[19,1118,1119,1120,1124],{},"The Scottish dance tradition is not a single thing. It encompasses at least three distinct but overlapping forms: Highland dancing, Scottish country dancing, and ceilidh dancing. Each has its own history, its own rules, and its own relationship to the broader culture. Together, they represent one of the most complete surviving dance traditions in Europe, practiced by tens of thousands of people in Scotland and across the global ",[46,1121,1123],{"href":1122},"/blog/scottish-clans-modern-gatherings","Scottish diaspora",".",[19,1126,1127],{},"The music that drives Scottish dance is inseparable from the dance itself. Reels, strathspeys, jigs, and hornpipes are musical forms defined by their dance rhythms, and the relationship between musician and dancer is collaborative rather than one-directional. The piper or fiddler sets the tempo, the dancers respond, and the energy of the room shapes the performance of both. This symbiosis between music and movement is one of the defining characteristics of Scottish cultural expression.",[26,1129,1131],{"id":1130},"highland-dancing","Highland Dancing",[19,1133,1134],{},"Highland dancing is the most visually dramatic form. It is a solo competitive dance form, performed to bagpipe music, characterized by precise footwork, elevated posture, and controlled athleticism. The dancer performs complex steps on a small area of ground, often on the balls of the feet, with the upper body held erect and the arms raised in positions that vary with the specific dance.",[19,1136,1137],{},"Several traditional dances have origin stories connecting them to specific moments. The Sword Dance is said to have been performed over crossed swords after battle. The Highland Fling is associated with celebrations after a deer hunt. The Seann Triubhas is said to commemorate the repeal of the ban on Highland dress after Culloden.",[19,1139,1140,1141,1145],{},"These dances were formalized through the competitive framework of the ",[46,1142,1144],{"href":1143},"/blog/highland-games-history","Highland games",". The Scottish Official Board of Highland Dancing, established in 1950, codified the rules and created a grading system that governs competition worldwide. Today, Highland dancing is performed competitively from Canada to New Zealand, and the standard is extraordinarily high.",[26,1147,1149],{"id":1148},"scottish-country-dancing","Scottish Country Dancing",[19,1151,1152],{},"Scottish country dancing, or SCD, is the social dance form that evolved in the Scottish Lowlands from the seventeenth century onward. It is danced in sets, typically of three or four couples standing in two lines, and the choreography involves a complex vocabulary of figures: rights and lefts, set and turn, poussette, reel of three, and dozens more. The dances are performed to reels, jigs, and strathspeys, and the tempo ranges from brisk to stately.",[19,1154,1155],{},"The Royal Scottish Country Dance Society, founded in 1923, has been the custodian of the tradition for a century. The Society collected, preserved, and published dances, established teaching standards, and built an international network of branches and affiliated groups. Today, RSCDS has more than 150 branches in over 50 countries, making Scottish country dancing one of the most widely practiced folk dance traditions in the world.",[19,1157,1158],{},"SCD is more structured than ceilidh dancing, but beginners are welcome at most classes, and the tradition of walking through each dance before performing it at tempo makes it accessible. Mastery requires years of practice, and the best dancers combine precision with musicality and an awareness of the other dancers that creates a collective experience greater than the sum of its parts.",[26,1160,1162],{"id":1161},"the-ceilidh","The Ceilidh",[19,1164,1165],{},"The ceilidh, from the Gaelic word for a social gathering or visit, is the most informal and most popular form of Scottish dance. A ceilidh dance typically features a live band, a caller who explains each dance before it begins, and a room full of people of all ages and skill levels dancing together with more enthusiasm than technique.",[19,1167,1168],{},"Ceilidh dances are simpler than Scottish country dances, and they are designed to be learned on the spot. The Strip the Willow, the Gay Gordons, the Dashing White Sergeant, and the Canadian Barn Dance are staples of the ceilidh repertoire, and their basic structures can be grasped in seconds. The pleasure of the ceilidh lies not in technical excellence but in communal energy: the music driving the room, the caller's instructions cutting through the noise, the controlled chaos of couples spinning and weaving through the figures.",[19,1170,1171],{},"The ceilidh has become the default format for celebrations across Scotland. Weddings, Burns Night suppers, Hogmanay parties, and community fundraisers all feature ceilidh dancing as their social centerpiece. The form is supremely democratic: it requires no training, no special clothing, and no prior experience. All it requires is a willingness to move, to follow the caller, and to laugh when things go wrong.",[19,1173,1174,1175,1179],{},"In the diaspora, ceilidh dances serve as gathering points for Scottish communities and as gateways to deeper engagement with Scottish culture. A person who attends a ceilidh at a ",[46,1176,1178],{"href":1177},"/blog/tartan-day-celebration","Tartan Day celebration"," may discover an interest in Scottish music, join a country dance class, or attend a Highland games. The ceilidh's accessibility makes it the broadest point of entry into the Scottish dance tradition, and its irrepressible energy makes it one of the most effective ambassadors for Scottish culture in the world.",{"title":91,"searchDepth":124,"depth":124,"links":1181},[1182,1183,1184,1185],{"id":1112,"depth":106,"text":1113},{"id":1130,"depth":106,"text":1131},{"id":1148,"depth":106,"text":1149},{"id":1161,"depth":106,"text":1162},"2025-08-25","Scottish dance traditions range from the formal precision of Highland dancing to the communal joy of the ceilidh. Here's the history, the forms, and why these traditions continue to thrive.",[1189,1190,1191,1192,1193],"scottish dance traditions","highland dancing history","ceilidh dancing","scottish country dancing","scottish reel",{},"/blog/scottish-dance-traditions",{"title":1106,"description":1187},"blog/scottish-dance-traditions",[1199,1131,1200,1201,1202],"Scottish Dance","Ceilidh","Scottish Culture","Traditional Dance","A4Z5wtMce9vCBzaIZp_CQIz57rpha51qkae45MOrAfo",{"id":1205,"title":1206,"author":1207,"body":1209,"category":759,"date":1186,"description":1298,"extension":389,"featured":390,"image":391,"keywords":1299,"meta":1305,"navigation":186,"path":1306,"readTime":183,"seo":1307,"stem":1308,"tags":1309,"__hash__":1314},"blog/blog/william-wallace-real-history.md","William Wallace: The Real History Behind the Legend",{"name":9,"bio":1208},"Author of The Forge of Tongues — 22,000 Years of Migration, Mutation, and Memory",{"type":12,"value":1210,"toc":1292},[1211,1215,1226,1229,1237,1241,1244,1247,1250,1254,1257,1260,1268,1272,1275,1278,1284],[26,1212,1214],{"id":1213},"the-man-before-the-monument","The Man Before the Monument",[19,1216,1217,1218,1221,1222,1225],{},"Almost everything most people think they know about William Wallace comes from two sources: Blind Harry's epic poem ",[436,1219,1220],{},"The Wallace",", written around 1477 — nearly two centuries after the events it describes — and the 1995 film ",[436,1223,1224],{},"Braveheart",", which took considerable liberties with even Blind Harry's already embellished account. The real Wallace is harder to find, buried under layers of legend, propaganda, and national myth.",[19,1227,1228],{},"What we know is this: William Wallace was born around 1270, probably in Elderslie near Paisley, into a minor gentry family. His father was a knight, but far below the great magnates like Bruce, Comyn, or Balliol. Wallace was not a Highland chief or a peasant rebel. He was a member of the lesser nobility, educated enough to read Latin and French, trained in arms as befitted his class.",[19,1230,1231,1232,1236],{},"The circumstances that turned this minor knight into a national figure were created by Edward I of England. In 1296, Edward invaded Scotland, deposed King John Balliol, and imposed direct English rule. He stripped the country of its symbols of sovereignty — including the ",[46,1233,1235],{"href":1234},"/blog/stone-of-destiny-history","Stone of Destiny"," — and installed English administrators across the kingdom. Scotland was to be treated not as a conquered nation but as a province, absorbed into the English crown.",[26,1238,1240],{"id":1239},"stirling-bridge","Stirling Bridge",[19,1242,1243],{},"In 1297, Scotland erupted in a series of local revolts against English occupation. Wallace's rising began with the killing of the English sheriff of Lanark. He became the leader of a growing guerrilla campaign in the Lowlands and by September had joined forces with Andrew de Moray, who had been leading his own campaign in the Highlands. Together, they faced the English army at Stirling Bridge on September 11, 1297.",[19,1245,1246],{},"The battle was a masterpiece of tactical opportunism. The English army, led by John de Warenne, Earl of Surrey, attempted to cross the narrow wooden bridge over the River Forth. Wallace and Moray waited until roughly half the English force had crossed, then attacked, cutting the army in two. The English troops on the Scottish side of the bridge were trapped on a loop of the river with marshland at their backs. They were slaughtered. The bridge collapsed under the weight of soldiers trying to retreat. The English treasurer, Hugh de Cressingham, was killed, and according to Scottish tradition, Wallace had his skin made into a sword belt.",[19,1248,1249],{},"Stirling Bridge was a devastating English defeat, achieved not by numerical superiority but by intelligence, terrain, and timing. It proved that the English could be beaten, and it made Wallace the most important military figure in Scotland. He was knighted and named Guardian of Scotland — the acting head of state — in the name of the absent King John Balliol.",[26,1251,1253],{"id":1252},"falkirk-and-the-fall","Falkirk and the Fall",[19,1255,1256],{},"Wallace's time as Guardian lasted less than a year. In July 1298, Edward I returned to Scotland in person, leading a massive army. The two forces met at Falkirk on July 22, 1298.",[19,1258,1259],{},"At Falkirk, Wallace adopted a defensive formation — schiltrons, tight circles of spearmen with weapons pointing outward. The formation was effective against cavalry, but Edward had Welsh longbowmen. The archers poured arrows into the stationary schiltrons from a distance the spearmen could not reach. Once the formations broke, the English cavalry rode through. The Scottish army was destroyed.",[19,1261,1262,1263,1267],{},"Wallace survived but resigned the Guardianship. The great Scottish nobles had never fully accepted his authority, and the defeat gave them reason to withdraw support. For the next seven years, Wallace disappeared from the record. He may have traveled to France seeking diplomatic support. What is certain is that he refused to submit to English rule. While other leaders — including ",[46,1264,1266],{"href":1265},"/blog/robert-the-bruce-legacy","Robert Bruce"," — periodically swore fealty to Edward, Wallace never submitted.",[26,1269,1271],{"id":1270},"the-execution","The Execution",[19,1273,1274],{},"Wallace was captured near Glasgow on August 5, 1305, betrayed by a Scottish knight named John de Menteith. He was taken to London, where he was tried at Westminster Hall on August 23, 1305. The trial was a formality. Wallace was charged with treason — a charge he denied on the grounds that he had never sworn allegiance to the English king and therefore could not be a traitor.",[19,1276,1277],{},"The distinction was legally sound but politically irrelevant. Wallace was found guilty and sentenced to the full penalty for treason: hanged, drawn, and quartered. He was dragged through London behind a horse, hanged until nearly dead, disemboweled while alive, then beheaded and cut into four pieces. His head was placed on London Bridge. His limbs were sent to Newcastle, Berwick, Stirling, and Perth.",[19,1279,1280,1281,1283],{},"The brutality was intended to end the Scottish resistance. It had the opposite effect. Within seven months, ",[46,1282,1266],{"href":1265}," had murdered his rival John Comyn and seized the Scottish throne, beginning the campaign that would culminate at Bannockburn and ultimately secure the independence that Wallace had fought for.",[19,1285,1286,1287,1291],{},"Wallace's legacy is not a single battle or a political achievement. It is the demonstration that resistance to occupation is possible even when the odds are overwhelming, even when the political establishment has capitulated, even when the price of defiance is death. The ",[46,1288,1290],{"href":1289},"/blog/declaration-of-arbroath","Declaration of Arbroath",", written fifteen years after Wallace's execution, articulated in words the principle that Wallace had articulated in action: that freedom is not negotiable, and that no honest man surrenders it willingly.",{"title":91,"searchDepth":124,"depth":124,"links":1293},[1294,1295,1296,1297],{"id":1213,"depth":106,"text":1214},{"id":1239,"depth":106,"text":1240},{"id":1252,"depth":106,"text":1253},{"id":1270,"depth":106,"text":1271},"Before Mel Gibson, before the myths, there was a minor Scottish knight who led a popular uprising against English occupation and was executed for it. The real William Wallace is more interesting than the legend — and his story is far more brutal.",[1300,1301,1302,1303,1304],"william wallace real history","william wallace scotland","stirling bridge battle","scottish independence wars","wallace guardian of scotland",{},"/blog/william-wallace-real-history",{"title":1206,"description":1298},"blog/william-wallace-real-history",[1310,1311,1312,1240,1313],"William Wallace","Scottish Independence","Wars of Independence","Medieval Scotland","_WK6PIZj92Mh8TxkbhprqTsultIRoTim6jHRf_eyG1w",{"id":1316,"title":1317,"author":1318,"body":1319,"category":1432,"date":1433,"description":1434,"extension":389,"featured":390,"image":391,"keywords":1435,"meta":1438,"navigation":186,"path":1439,"readTime":183,"seo":1440,"stem":1441,"tags":1442,"__hash__":1446},"blog/blog/cross-platform-app-development.md","Cross-Platform App Development: The Real Cost of Write Once",{"name":9,"bio":10},{"type":12,"value":1320,"toc":1426},[1321,1324,1327,1331,1334,1337,1345,1348,1352,1355,1366,1372,1378,1384,1388,1391,1394,1402,1405,1408,1412,1415,1423],[19,1322,1323],{},"\"Write once, run anywhere\" is the oldest promise in cross-platform development. It has never been fully true, and in 2026, it still is not. But it is closer to reality than ever before, and the trade-offs are increasingly worth it for most applications.",[19,1325,1326],{},"The question is not whether cross-platform works. It does. The question is what it actually costs compared to what you save.",[26,1328,1330],{"id":1329},"what-you-actually-share","What You Actually Share",[19,1332,1333],{},"When teams adopt a cross-platform framework, they expect to share 100% of their code across iOS and Android. In practice, well-structured cross-platform apps share between 70% and 90% of their code. That remaining 10-30% is where the real cost hides.",[19,1335,1336],{},"The shared code covers business logic, API integration, state management, navigation structure, and most UI components. This is genuine value. Writing your authentication flow once instead of twice, your data models once instead of twice, your form validation once instead of twice — that adds up to significant time savings.",[19,1338,1339,1340,1344],{},"The platform-specific code covers permissions handling (which differs between iOS and Android in meaningful ways), native module integrations, platform-specific UI adjustments, and device capability checks. Some frameworks handle these differences better than others. The ",[46,1341,1343],{"href":1342},"/blog/react-native-vs-flutter","React Native vs Flutter comparison"," matters here because each framework bridges the platform gap differently.",[19,1346,1347],{},"The architectural decision you make at the start — how you structure your shared code versus your platform-specific code — determines whether cross-platform saves you 40% of development time or 10%. I use a layered approach: pure business logic at the bottom (fully shared), platform abstraction in the middle (mostly shared), and UI adaptation at the top (partially shared).",[26,1349,1351],{"id":1350},"the-hidden-costs-nobody-mentions","The Hidden Costs Nobody Mentions",[19,1353,1354],{},"The marketing materials for cross-platform frameworks show the happy path. Here is what they skip.",[19,1356,1357,1360,1361,1365],{},[66,1358,1359],{},"Debugging is harder."," When something breaks in a cross-platform app, the bug might be in your code, in the framework, in the bridge between your code and native APIs, or in the native layer itself. Stack traces cross these boundaries in unhelpful ways. A bug that manifests on Android might have its root cause in shared code that happens to work fine on iOS due to timing differences. This makes your ",[46,1362,1364],{"href":1363},"/blog/mobile-app-testing-strategy","testing strategy"," more important, not less.",[19,1367,1368,1371],{},[66,1369,1370],{},"Upgrades are non-trivial."," When Apple releases a new iOS version or Google ships a new Android API level, cross-platform frameworks need to update their bridges. You are on their timeline, not Apple's or Google's. If a framework update introduces breaking changes (which happens), you are doing upgrade work that native developers do not face.",[19,1373,1374,1377],{},[66,1375,1376],{},"Performance edge cases exist."," For standard UI — lists, forms, navigation — performance is fine. But cross-platform adds overhead for gesture handling, animation interpolation, and rapid state updates. If your app hits these edges, you spend time optimizing things that would be straightforward in native code.",[19,1379,1380,1383],{},[66,1381,1382],{},"Hiring is more specific."," You need developers who understand both the cross-platform framework and the underlying native platforms. A React developer who has never thought about iOS memory management or Android lifecycle will struggle with cross-platform mobile development. The pool of experienced cross-platform developers is growing but still smaller than native developers.",[26,1385,1387],{"id":1386},"when-cross-platform-pays-off","When Cross-Platform Pays Off",[19,1389,1390],{},"Despite the hidden costs, cross-platform development is the right choice for a wide range of applications. It clearly pays off when:",[19,1392,1393],{},"Your app is data-driven with standard UI patterns. CRUD apps, dashboards, marketplaces, social features, content apps — these are the sweet spot. The UI is standard enough that cross-platform components handle it well, and you benefit hugely from shared business logic.",[19,1395,1396,1397,1401],{},"Your team is small. If you have 2-4 developers, maintaining two native codebases is impractical. Cross-platform lets a small team cover both platforms competently. This is especially true for ",[46,1398,1400],{"href":1399},"/blog/mvp-development-guide","MVP development"," where speed to market matters more than platform-perfect polish.",[19,1403,1404],{},"Your product is evolving rapidly. When you are iterating on features weekly, making every change in one codebase instead of two means you iterate twice as fast. That speed advantage compounds over months.",[19,1406,1407],{},"You want web and mobile code sharing. If you are also building a web app, frameworks like React Native with Expo let you share significant code across web and mobile. This is a genuine architectural advantage for products that need to be everywhere.",[26,1409,1411],{"id":1410},"making-the-architecture-work","Making the Architecture Work",[19,1413,1414],{},"If you go cross-platform, invest in the architecture that makes it sustainable. Separate your business logic into platform-agnostic modules. Use dependency injection for platform-specific capabilities. Write platform-specific code in clearly defined boundary layers, not scattered through your components.",[19,1416,1417,1418,1422],{},"Build your ",[46,1419,1421],{"href":1420},"/blog/multi-tenant-architecture","multi-tenant architecture"," or backend services as platform-agnostic APIs from the start. The cleaner your API contract, the less platform-specific work your mobile code needs to do.",[19,1424,1425],{},"Cross-platform is not free. But it is often the most pragmatic choice for teams building real products with real constraints. Understand the costs going in, structure your code to minimize them, and you will ship faster than maintaining two native apps — without the quality compromises that gave cross-platform a bad reputation a decade ago.",{"title":91,"searchDepth":124,"depth":124,"links":1427},[1428,1429,1430,1431],{"id":1329,"depth":106,"text":1330},{"id":1350,"depth":106,"text":1351},{"id":1386,"depth":106,"text":1387},{"id":1410,"depth":106,"text":1411},"Architecture","2025-08-22","Cross-platform app development promises write once, run anywhere. Here is what that actually costs in practice — the trade-offs, hidden work, and when it pays off.",[1436,1437],"cross-platform app development","write once run anywhere mobile",{},"/blog/cross-platform-app-development",{"title":1317,"description":1434},"blog/cross-platform-app-development",[1443,1444,1445],"Cross-Platform","Mobile Development","Software Architecture","X9SgJbea5Y68WO3Ar0PrJFgnlbss0DIpLhjfWMjtE_8",{"id":1448,"title":1449,"author":1450,"body":1451,"category":1432,"date":1433,"description":1613,"extension":389,"featured":390,"image":391,"keywords":1614,"meta":1618,"navigation":186,"path":1619,"readTime":183,"seo":1620,"stem":1621,"tags":1622,"__hash__":1626},"blog/blog/database-per-service-pattern.md","Database Per Service: Isolating Data in Distributed Systems",{"name":9,"bio":10},{"type":12,"value":1452,"toc":1606},[1453,1457,1460,1463,1466,1468,1472,1475,1478,1484,1490,1501,1507,1509,1513,1516,1522,1533,1539,1545,1547,1551,1554,1562,1565,1567,1576,1578,1582],[26,1454,1456],{"id":1455},"why-shared-databases-break-down","Why Shared Databases Break Down",[19,1458,1459],{},"When two services share a database, they share a schema. When they share a schema, changes to one service's data model risk breaking the other. A seemingly harmless column rename in the orders table cascades into the inventory service. An index added to improve billing performance degrades shipping queries. A schema migration requires coordinating deployments across every service that touches the database.",[19,1461,1462],{},"This coupling defeats the core promise of service-oriented architecture: independent deployability. If you cannot deploy one service without coordinating with three others, you do not have independent services. You have a distributed monolith — all the operational complexity of microservices with none of the organizational benefits.",[19,1464,1465],{},"The database-per-service pattern eliminates this coupling by giving each service exclusive ownership of its data store. No other service reads from or writes to that store directly. All cross-service data access happens through the service's API.",[720,1467],{},[26,1469,1471],{"id":1470},"what-database-per-service-actually-looks-like","What Database Per Service Actually Looks Like",[19,1473,1474],{},"The pattern is simple in principle: each service owns a database (or schema, or set of tables) that only it can access. Other services that need that data request it through the owning service's API.",[19,1476,1477],{},"In practice, this means a few concrete things:",[19,1479,1480,1483],{},[66,1481,1482],{},"Each service has its own connection credentials."," The orders service cannot connect to the inventory database even if someone wanted it to. This is enforced at the infrastructure level, not just by convention.",[19,1485,1486,1489],{},[66,1487,1488],{},"Each service manages its own migrations."," The orders service's schema evolves on its own timeline. It does not wait for the billing service to be ready for a migration. This is what makes independent deployment possible.",[19,1491,1492,1495,1496,1500],{},[66,1493,1494],{},"Cross-service queries go through APIs."," If the reporting dashboard needs order data and customer data, it calls the orders API and the customers API. It does not run a SQL join across two databases. This is where the pattern introduces friction — and where complementary patterns like ",[46,1497,1499],{"href":1498},"/blog/cqrs-event-sourcing-explained","CQRS"," become important.",[19,1502,1503,1506],{},[66,1504,1505],{},"Services can use different database technologies."," The search service might use Elasticsearch. The user profile service might use PostgreSQL. The session service might use Redis. Each service picks the storage technology that fits its access patterns rather than conforming to a single shared database choice.",[720,1508],{},[26,1510,1512],{"id":1511},"the-hard-parts","The Hard Parts",[19,1514,1515],{},"Database per service solves the coupling problem but introduces new ones. Being honest about these trade-offs is essential before adopting the pattern.",[19,1517,1518,1521],{},[66,1519,1520],{},"Cross-service queries are harder."," A SQL join across two tables in one database takes milliseconds. The equivalent across two services requires two API calls, client-side joining, and careful handling of partial failures. For reporting and analytics, this overhead is often unacceptable, which is why most systems that adopt database per service also adopt a separate read-optimized store for cross-cutting queries.",[19,1523,1524,1527,1528,1532],{},[66,1525,1526],{},"Distributed transactions are gone."," When the orders service and the inventory service each have their own database, you cannot wrap both updates in a single transaction. If the order is created but the inventory decrement fails, you have an inconsistency. The ",[46,1529,1531],{"href":1530},"/blog/saga-pattern-distributed-transactions","saga pattern"," exists specifically to manage this — replacing ACID transactions with a sequence of local transactions and compensating actions.",[19,1534,1535,1538],{},[66,1536,1537],{},"Data duplication is inevitable."," Services often need reference data from other services. The orders service needs the customer name for order confirmation emails. Rather than calling the customers API on every email send, the orders service typically stores a local copy of the customer name. This duplication must be kept in sync, usually through events. The trade-off is operational complexity for runtime independence.",[19,1540,1541,1544],{},[66,1542,1543],{},"Operational overhead increases."," More databases means more backups, more monitoring, more capacity planning. This is manageable with good infrastructure automation but non-trivial for small teams. If you are running three services, you can probably handle three databases. If you are running thirty, you need mature platform tooling.",[720,1546],{},[26,1548,1550],{"id":1549},"when-to-adopt-it","When to Adopt It",[19,1552,1553],{},"The database-per-service pattern makes the most sense when you have genuinely independent teams working on genuinely independent services that deploy on independent schedules. If your organization has these characteristics, shared databases will become a coordination bottleneck and the pattern pays for itself in reduced deployment friction.",[19,1555,1556,1557,1561],{},"It makes less sense when a small team owns all the services. If the same three developers maintain the orders service, the inventory service, and the billing service, and they deploy together anyway, database isolation adds complexity without organizational benefit. A ",[46,1558,1560],{"href":1559},"/blog/microservices-vs-monolith","modular monolith"," with clear module boundaries and separate schemas within a single database gives you the logical separation without the operational overhead.",[19,1563,1564],{},"The honest assessment: database per service is a pattern for organizations that have outgrown shared databases, not a starting point for new projects. Start with a well-structured single database. When schema coupling starts blocking independent teams, extract services along with their data. The pattern works best when adopted incrementally — one service at a time — rather than as a big-bang migration.",[720,1566],{},[19,1568,1569,1570],{},"If you are evaluating whether your system needs database isolation or help designing the data architecture for a service-oriented system, ",[46,1571,1575],{"href":1572,"rel":1573},"https://calendly.com/jamesrossjr",[1574],"nofollow","let's talk.",[720,1577],{},[26,1579,1581],{"id":1580},"keep-reading","Keep Reading",[727,1583,1584,1589,1594,1600],{},[730,1585,1586],{},[46,1587,1588],{"href":1559},"Microservices vs. Monolith: Choosing the Right Architecture",[730,1590,1591],{},[46,1592,1593],{"href":1498},"CQRS and Event Sourcing Explained",[730,1595,1596],{},[46,1597,1599],{"href":1598},"/blog/distributed-systems-fundamentals","Distributed Systems Fundamentals",[730,1601,1602],{},[46,1603,1605],{"href":1604},"/blog/database-indexing-strategies","Database Indexing Strategies for Production Systems",{"title":91,"searchDepth":124,"depth":124,"links":1607},[1608,1609,1610,1611,1612],{"id":1455,"depth":106,"text":1456},{"id":1470,"depth":106,"text":1471},{"id":1511,"depth":106,"text":1512},{"id":1549,"depth":106,"text":1550},{"id":1580,"depth":106,"text":1581},"Sharing a database between services seems practical until it isn't. Here's how the database-per-service pattern works and when to adopt it.",[1615,1616,1617],"database per service pattern","microservices database isolation","distributed data management",{},"/blog/database-per-service-pattern",{"title":1449,"description":1613},"blog/database-per-service-pattern",[1623,1624,1625],"Distributed Systems","Microservices","Database Architecture","MbXhlkSZNTVwgY-GiXF_3zW72-1MONIueEcAh52otZA",{"id":1628,"title":1629,"author":1630,"body":1631,"category":759,"date":1433,"description":1718,"extension":389,"featured":390,"image":391,"keywords":1719,"meta":1725,"navigation":186,"path":1726,"readTime":183,"seo":1727,"stem":1728,"tags":1729,"__hash__":1735},"blog/blog/morrigan-war-goddess.md","The Morrigan: Celtic Goddess of War and Fate",{"name":9,"bio":10},{"type":12,"value":1632,"toc":1712},[1633,1637,1648,1655,1659,1666,1669,1676,1680,1683,1686,1694,1698,1706,1709],[26,1634,1636],{"id":1635},"the-name-and-the-nature","The Name and the Nature",[19,1638,1639,1640,1643,1644,1647],{},"The Morrigan is not a simple deity. She does not fit neatly into the categories that later mythographers tried to impose on Celtic religion. She is a war goddess, yes, but she is also a sovereignty figure, a prophetess, a shape-shifter, and a force of territorial power that transcends any single narrative. Her name is usually interpreted as \"Phantom Queen\" or \"Great Queen\" -- from the Old Irish ",[436,1641,1642],{},"mor"," (great or phantom) and ",[436,1645,1646],{},"rigan"," (queen) -- and both translations capture something essential about her character. She is both terrifyingly real and impossibly elusive.",[19,1649,1650,1651,1124],{},"In the Irish mythological texts, the Morrigan appears sometimes as a single figure and sometimes as a collective of three sisters: the Morrigan, Badb, and Macha. This triadic structure is characteristic of Celtic divine figures, where the number three carried deep symbolic weight. Whether she is one goddess with three aspects or three distinct beings who share a title is a question the texts deliberately refuse to resolve. The ambiguity is the point. The Morrigan operates at the boundary between categories -- life and death, victory and defeat, the human world and the ",[46,1652,1654],{"href":1653},"/blog/celtic-otherworld-beliefs","Celtic Otherworld",[26,1656,1658],{"id":1657},"the-morrigan-in-battle","The Morrigan in Battle",[19,1660,1661,1662,1665],{},"Her most famous appearances occur in the context of war. In the ",[436,1663,1664],{},"Cath Maige Tuired"," -- the Battle of Moytura -- the Morrigan fights alongside the Tuatha De Danann against the Fomorians, the monstrous race that threatened to dominate Ireland. Before the battle, she meets the Dagda at a ford on the river Unius at Samhain, and they couple in a ritual union that is explicitly tied to sovereignty and the fertility of the land. This is not a romantic encounter. It is a transaction of power. The Morrigan pledges to fight for the Tuatha De Danann, and in return, the cosmic order is maintained.",[19,1667,1668],{},"During the battle itself, she appears in multiple forms. She chants incantations from the sidhe mounds. She drives the Fomorians into confusion. After the victory, she delivers a prophecy that describes the end of the world -- a vision of a time when summers will bear no grain, cows will give no milk, and the bonds of kinship will dissolve. This apocalyptic coda is remarkable. Even in the moment of triumph, the Morrigan speaks of inevitable decay. She does not celebrate victory. She announces what comes after it.",[19,1670,1671,1672,1675],{},"In the Ulster Cycle, she appears to Cu Chulainn before and during the ",[436,1673,1674],{},"Tain Bo Cuailnge",". She offers him her love and her aid. He refuses her, either not recognizing who she is or not caring. The refusal is catastrophic. She attacks him during his combat at the ford, appearing as an eel that trips him, a wolf that stampedes cattle into his path, and a hornless red heifer that leads a charge against him. Cu Chulainn wounds her in each form. Later, she appears as an old woman milking a cow with three teats, and Cu Chulainn blesses her three times, healing her three wounds without realizing what he has done.",[26,1677,1679],{"id":1678},"shape-shifting-and-sovereignty","Shape-Shifting and Sovereignty",[19,1681,1682],{},"The Morrigan's shape-shifting is not mere spectacle. Each form she takes carries meaning. The crow or raven -- the form most commonly associated with her -- is a battlefield scavenger, the creature that arrives after the killing is done. When the Morrigan appears as a crow perched on a standing stone, she is not just watching. She is claiming the dead. She is marking the transition from life to aftermath.",[19,1684,1685],{},"Her forms also connect her to the land itself. The heifer, the wolf, the eel -- these are creatures of the Irish landscape, tied to the rivers, pastures, and wild places that define the territory. The Morrigan's power is inseparable from the physical geography of Ireland. This connects her to the broader Celtic concept of sovereignty, in which the legitimate ruler of a territory must be \"married\" to the land itself, often through union with a goddess figure.",[19,1687,1688,1689,1693],{},"This sovereignty aspect explains why her ",[46,1690,1692],{"href":1691},"/blog/celtic-art-symbolism","mythological significance"," extends far beyond the battlefield. She does not merely preside over war. She presides over the legitimacy of power. A king who rules justly is under her protection. A king who rules unjustly will find her standing against him, often in the form of a washerwoman at a ford, cleaning the bloodstained armor of the man who is about to die.",[26,1695,1697],{"id":1696},"the-morrigan-after-paganism","The Morrigan After Paganism",[19,1699,1700,1701,1705],{},"The Christianization of Ireland did not erase the Morrigan so much as reframe her. The medieval monks who transcribed the mythological cycles treated her with a mixture of fascination and unease. She was too central to the stories to be removed, but too pagan to be celebrated. In some later texts, she is diminished into a fairy woman or a banshee -- a wailing spirit who foretells death but no longer commands it. The ",[46,1702,1704],{"href":1703},"/blog/scottish-gaelic-language-history","transition from pagan goddess to folklore figure"," tracks the broader transformation of Celtic religion under Christian influence.",[19,1707,1708],{},"But even in her diminished forms, the core of the Morrigan persists. The banshee still foretells death. The crow on the battlefield still signifies the boundary between the living and the dead. The washerwoman at the ford still appears in Scottish Highland folklore centuries after anyone consciously worshipped the Morrigan by name.",[19,1710,1711],{},"What makes the Morrigan compelling is her refusal to be comforting. She does not protect heroes from death. She tells them when death is coming. She does not guarantee victory. She determines who deserves it. In a mythological tradition full of gods who feast and fight and boast, the Morrigan stands apart as the figure who sees the full arc of events -- the battle, the aftermath, and the long silence that follows. She is the goddess of what war actually costs, and the texts never let the reader forget that cost, even when the right side wins.",{"title":91,"searchDepth":124,"depth":124,"links":1713},[1714,1715,1716,1717],{"id":1635,"depth":106,"text":1636},{"id":1657,"depth":106,"text":1658},{"id":1678,"depth":106,"text":1679},{"id":1696,"depth":106,"text":1697},"The Morrigan is one of the most complex figures in Irish mythology -- a shape-shifting goddess of war, sovereignty, and prophecy who appears at the hinge points of every major conflict in the mythological cycle.",[1720,1721,1722,1723,1724],"the morrigan celtic goddess","morrigan irish mythology","celtic war goddess","morrigan shape shifter","tuatha de danann gods",{},"/blog/morrigan-war-goddess",{"title":1629,"description":1718},"blog/morrigan-war-goddess",[1730,1731,1732,1733,1734],"Irish Mythology","Celtic Deities","The Morrigan","War Goddess","Tuatha De Danann","l7xql1_jJpk_8aU5A3Y9i6bXSMRfvu-S3ebdHRCV1wo",{"id":1737,"title":1738,"author":1739,"body":1740,"category":2624,"date":1433,"description":2625,"extension":389,"featured":390,"image":391,"keywords":2626,"meta":2629,"navigation":186,"path":2630,"readTime":190,"seo":2631,"stem":2632,"tags":2633,"__hash__":2637},"blog/blog/nuxt-3-module-development.md","Building Custom Nuxt 3 Modules: From Concept to Published Package",{"name":9,"bio":10},{"type":12,"value":1741,"toc":2618},[1742,1745,1748,1752,1755,1892,1902,1926,1934,1938,1941,2097,2111,2121,2129,2133,2136,2408,2411,2418,2422,2429,2578,2589,2600,2607,2615],[19,1743,1744],{},"Nuxt modules are the primary extension mechanism for the framework, and they are more accessible to build than most developers realize. If you have ever copied the same composable, plugin, or server middleware across multiple Nuxt projects, you have a module waiting to be extracted. The module system gives you hooks into the build process, auto-imports, runtime configuration, and server route injection — all through a clean, well-documented API.",[19,1746,1747],{},"I have built several internal modules for projects where repeating setup across applications became a maintenance problem. Here is what I learned about doing it well.",[26,1749,1751],{"id":1750},"the-module-anatomy","The Module Anatomy",[19,1753,1754],{},"A Nuxt module is a function that runs at build time. It receives the Nuxt instance and can modify configuration, add plugins, register composables, inject components, and hook into the build lifecycle. The simplest module looks like this:",[86,1756,1760],{"className":1757,"code":1758,"language":1759,"meta":91,"style":91},"language-ts shiki shiki-themes github-dark","import { defineNuxtModule } from '@nuxt/kit'\n\nExport default defineNuxtModule({\n meta: {\n name: 'my-module',\n configKey: 'myModule',\n },\n defaults: {\n enabled: true,\n },\n setup(options, nuxt) {\n if (!options.enabled) return\n // Module logic here\n },\n})\n","ts",[93,1761,1762,1776,1780,1794,1799,1809,1819,1824,1829,1839,1843,1862,1878,1883,1887],{"__ignoreMap":91},[96,1763,1764,1767,1770,1773],{"class":98,"line":99},[96,1765,1766],{"class":109},"import",[96,1768,1769],{"class":120}," { defineNuxtModule } ",[96,1771,1772],{"class":109},"from",[96,1774,1775],{"class":130}," '@nuxt/kit'\n",[96,1777,1778],{"class":98,"line":106},[96,1779,187],{"emptyLinePlaceholder":186},[96,1781,1782,1785,1788,1791],{"class":98,"line":124},[96,1783,1784],{"class":120},"Export ",[96,1786,1787],{"class":109},"default",[96,1789,1790],{"class":202}," defineNuxtModule",[96,1792,1793],{"class":120},"({\n",[96,1795,1796],{"class":98,"line":153},[96,1797,1798],{"class":120}," meta: {\n",[96,1800,1801,1804,1807],{"class":98,"line":167},[96,1802,1803],{"class":120}," name: ",[96,1805,1806],{"class":130},"'my-module'",[96,1808,222],{"class":120},[96,1810,1811,1814,1817],{"class":98,"line":177},[96,1812,1813],{"class":120}," configKey: ",[96,1815,1816],{"class":130},"'myModule'",[96,1818,222],{"class":120},[96,1820,1821],{"class":98,"line":183},[96,1822,1823],{"class":120}," },\n",[96,1825,1826],{"class":98,"line":190},[96,1827,1828],{"class":120}," defaults: {\n",[96,1830,1831,1834,1837],{"class":98,"line":196},[96,1832,1833],{"class":120}," enabled: ",[96,1835,1836],{"class":113},"true",[96,1838,222],{"class":120},[96,1840,1841],{"class":98,"line":209},[96,1842,1823],{"class":120},[96,1844,1845,1848,1851,1854,1856,1859],{"class":98,"line":225},[96,1846,1847],{"class":202}," setup",[96,1849,1850],{"class":120},"(",[96,1852,1853],{"class":212},"options",[96,1855,134],{"class":120},[96,1857,1858],{"class":212},"nuxt",[96,1860,1861],{"class":120},") {\n",[96,1863,1864,1867,1869,1872,1875],{"class":98,"line":238},[96,1865,1866],{"class":109}," if",[96,1868,279],{"class":120},[96,1870,1871],{"class":109},"!",[96,1873,1874],{"class":120},"options.enabled) ",[96,1876,1877],{"class":109},"return\n",[96,1879,1880],{"class":98,"line":249},[96,1881,1882],{"class":102}," // Module logic here\n",[96,1884,1885],{"class":98,"line":262},[96,1886,1823],{"class":120},[96,1888,1889],{"class":98,"line":276},[96,1890,1891],{"class":120},"})\n",[19,1893,807,1894,1897,1898,1901],{},[93,1895,1896],{},"defineNuxtModule"," wrapper from ",[93,1899,1900],{},"@nuxt/kit"," handles boilerplate — deduplication, option merging, compatibility checks. Always use it. Writing a raw module function works but misses safety features you will want later.",[19,1903,807,1904,1906,1907,134,1910,134,1913,134,1916,134,1919,134,1922,1925],{},[93,1905,1900],{}," package is your primary tool. It provides utilities for everything a module needs: ",[93,1908,1909],{},"addPlugin",[93,1911,1912],{},"addImports",[93,1914,1915],{},"addComponent",[93,1917,1918],{},"addServerHandler",[93,1920,1921],{},"createResolver",[93,1923,1924],{},"addTemplate",". These functions are stable across Nuxt versions and handle edge cases you would otherwise discover in production.",[19,1927,1928,1929,1933],{},"Understanding how Nuxt modules fit into the broader ",[46,1930,1932],{"href":1931},"/blog/nuxt-content-module-guide","Nuxt architecture"," helps you decide where your module should hook in and what lifecycle events matter for your use case.",[26,1935,1937],{"id":1936},"auto-imports-and-composables","Auto-Imports and Composables",[19,1939,1940],{},"One of the most common reasons to build a module is providing composables that auto-import across the consuming application. The pattern is straightforward:",[86,1942,1944],{"className":1757,"code":1943,"language":1759,"meta":91,"style":91},"import { defineNuxtModule, addImports, createResolver } from '@nuxt/kit'\n\nExport default defineNuxtModule({\n meta: { name: 'analytics-module', configKey: 'analytics' },\n setup(options, nuxt) {\n const { resolve } = createResolver(import.meta.url)\n\n addImports([\n { name: 'useAnalytics', from: resolve('./runtime/composables/useAnalytics') },\n { name: 'usePageView', from: resolve('./runtime/composables/usePageView') },\n ])\n },\n})\n",[93,1945,1946,1957,1961,1971,1987,2001,2033,2037,2045,2066,2084,2089,2093],{"__ignoreMap":91},[96,1947,1948,1950,1953,1955],{"class":98,"line":99},[96,1949,1766],{"class":109},[96,1951,1952],{"class":120}," { defineNuxtModule, addImports, createResolver } ",[96,1954,1772],{"class":109},[96,1956,1775],{"class":130},[96,1958,1959],{"class":98,"line":106},[96,1960,187],{"emptyLinePlaceholder":186},[96,1962,1963,1965,1967,1969],{"class":98,"line":124},[96,1964,1784],{"class":120},[96,1966,1787],{"class":109},[96,1968,1790],{"class":202},[96,1970,1793],{"class":120},[96,1972,1973,1976,1979,1982,1985],{"class":98,"line":153},[96,1974,1975],{"class":120}," meta: { name: ",[96,1977,1978],{"class":130},"'analytics-module'",[96,1980,1981],{"class":120},", configKey: ",[96,1983,1984],{"class":130},"'analytics'",[96,1986,1823],{"class":120},[96,1988,1989,1991,1993,1995,1997,1999],{"class":98,"line":167},[96,1990,1847],{"class":202},[96,1992,1850],{"class":120},[96,1994,1853],{"class":212},[96,1996,134],{"class":120},[96,1998,1858],{"class":212},[96,2000,1861],{"class":120},[96,2002,2003,2006,2009,2012,2015,2018,2021,2023,2025,2027,2030],{"class":98,"line":177},[96,2004,2005],{"class":109}," const",[96,2007,2008],{"class":120}," { ",[96,2010,2011],{"class":113},"resolve",[96,2013,2014],{"class":120}," } ",[96,2016,2017],{"class":109},"=",[96,2019,2020],{"class":202}," createResolver",[96,2022,1850],{"class":120},[96,2024,1766],{"class":109},[96,2026,1124],{"class":120},[96,2028,2029],{"class":113},"meta",[96,2031,2032],{"class":120},".url)\n",[96,2034,2035],{"class":98,"line":183},[96,2036,187],{"emptyLinePlaceholder":186},[96,2038,2039,2042],{"class":98,"line":190},[96,2040,2041],{"class":202}," addImports",[96,2043,2044],{"class":120},"([\n",[96,2046,2047,2050,2053,2056,2058,2060,2063],{"class":98,"line":196},[96,2048,2049],{"class":120}," { name: ",[96,2051,2052],{"class":130},"'useAnalytics'",[96,2054,2055],{"class":120},", from: ",[96,2057,2011],{"class":202},[96,2059,1850],{"class":120},[96,2061,2062],{"class":130},"'./runtime/composables/useAnalytics'",[96,2064,2065],{"class":120},") },\n",[96,2067,2068,2070,2073,2075,2077,2079,2082],{"class":98,"line":209},[96,2069,2049],{"class":120},[96,2071,2072],{"class":130},"'usePageView'",[96,2074,2055],{"class":120},[96,2076,2011],{"class":202},[96,2078,1850],{"class":120},[96,2080,2081],{"class":130},"'./runtime/composables/usePageView'",[96,2083,2065],{"class":120},[96,2085,2086],{"class":98,"line":225},[96,2087,2088],{"class":120}," ])\n",[96,2090,2091],{"class":98,"line":238},[96,2092,1823],{"class":120},[96,2094,2095],{"class":98,"line":249},[96,2096,1891],{"class":120},[19,2098,2099,2100,2103,2104,2107,2108,1124],{},"The consuming application can then use ",[93,2101,2102],{},"useAnalytics()"," anywhere without importing it. This matches the developer experience of built-in Nuxt composables like ",[93,2105,2106],{},"useFetch"," and ",[93,2109,2110],{},"useRoute",[19,2112,2113,2114,2117,2118,2120],{},"The runtime directory is important. Code in the module root runs at build time only. Code in ",[93,2115,2116],{},"runtime/"," runs in the browser and server at request time. Composables, plugins, and components belong in ",[93,2119,2116],{},". Build-time logic — adding routes, modifying webpack config, generating files — belongs in the module setup function.",[19,2122,2123,2124,2128],{},"Type safety matters here. Export your composable types from the module's entry point so consuming projects get full IntelliSense. The ",[46,2125,2127],{"href":2126},"/blog/nuxt-typescript-guide","TypeScript patterns for Nuxt"," apply directly to module development, especially around generic composable signatures.",[26,2130,2132],{"id":2131},"hooks-and-the-build-lifecycle","Hooks and the Build Lifecycle",[19,2134,2135],{},"Nuxt exposes dozens of hooks that modules can tap into. The most useful ones for module development are:",[86,2137,2139],{"className":1757,"code":2138,"language":1759,"meta":91,"style":91},"setup(options, nuxt) {\n // Modify Nuxt config before build starts\n nuxt.hook('modules:done', () => {\n // All modules have loaded — safe to check for peer modules\n })\n\n nuxt.hook('components:dirs', (dirs) => {\n // Register additional component directories\n dirs.push({ path: resolve('./runtime/components') })\n })\n\n nuxt.hook('nitro:config', (nitroConfig) => {\n // Modify server configuration\n nitroConfig.alias = nitroConfig.alias || {}\n nitroConfig.alias['#analytics'] = resolve('./runtime/server/utils')\n })\n\n nuxt.hook('pages:extend', (pages) => {\n // Add or modify routes\n pages.push({\n name: 'analytics-dashboard',\n path: '/admin/analytics',\n file: resolve('./runtime/pages/dashboard.vue'),\n })\n })\n}\n",[93,2140,2141,2149,2154,2175,2180,2185,2189,2212,2217,2238,2242,2246,2268,2273,2288,2312,2316,2320,2342,2347,2356,2366,2377,2393,2398,2403],{"__ignoreMap":91},[96,2142,2143,2146],{"class":98,"line":99},[96,2144,2145],{"class":202},"setup",[96,2147,2148],{"class":120},"(options, nuxt) {\n",[96,2150,2151],{"class":98,"line":106},[96,2152,2153],{"class":102}," // Modify Nuxt config before build starts\n",[96,2155,2156,2159,2162,2164,2167,2170,2173],{"class":98,"line":124},[96,2157,2158],{"class":120}," nuxt.",[96,2160,2161],{"class":202},"hook",[96,2163,1850],{"class":120},[96,2165,2166],{"class":130},"'modules:done'",[96,2168,2169],{"class":120},", () ",[96,2171,2172],{"class":109},"=>",[96,2174,121],{"class":120},[96,2176,2177],{"class":98,"line":153},[96,2178,2179],{"class":102}," // All modules have loaded — safe to check for peer modules\n",[96,2181,2182],{"class":98,"line":167},[96,2183,2184],{"class":120}," })\n",[96,2186,2187],{"class":98,"line":177},[96,2188,187],{"emptyLinePlaceholder":186},[96,2190,2191,2193,2195,2197,2200,2203,2206,2208,2210],{"class":98,"line":183},[96,2192,2158],{"class":120},[96,2194,2161],{"class":202},[96,2196,1850],{"class":120},[96,2198,2199],{"class":130},"'components:dirs'",[96,2201,2202],{"class":120},", (",[96,2204,2205],{"class":212},"dirs",[96,2207,285],{"class":120},[96,2209,2172],{"class":109},[96,2211,121],{"class":120},[96,2213,2214],{"class":98,"line":190},[96,2215,2216],{"class":102}," // Register additional component directories\n",[96,2218,2219,2222,2225,2228,2230,2232,2235],{"class":98,"line":196},[96,2220,2221],{"class":120}," dirs.",[96,2223,2224],{"class":202},"push",[96,2226,2227],{"class":120},"({ path: ",[96,2229,2011],{"class":202},[96,2231,1850],{"class":120},[96,2233,2234],{"class":130},"'./runtime/components'",[96,2236,2237],{"class":120},") })\n",[96,2239,2240],{"class":98,"line":209},[96,2241,2184],{"class":120},[96,2243,2244],{"class":98,"line":225},[96,2245,187],{"emptyLinePlaceholder":186},[96,2247,2248,2250,2252,2254,2257,2259,2262,2264,2266],{"class":98,"line":238},[96,2249,2158],{"class":120},[96,2251,2161],{"class":202},[96,2253,1850],{"class":120},[96,2255,2256],{"class":130},"'nitro:config'",[96,2258,2202],{"class":120},[96,2260,2261],{"class":212},"nitroConfig",[96,2263,285],{"class":120},[96,2265,2172],{"class":109},[96,2267,121],{"class":120},[96,2269,2270],{"class":98,"line":249},[96,2271,2272],{"class":102}," // Modify server configuration\n",[96,2274,2275,2278,2280,2282,2285],{"class":98,"line":262},[96,2276,2277],{"class":120}," nitroConfig.alias ",[96,2279,2017],{"class":109},[96,2281,2277],{"class":120},[96,2283,2284],{"class":109},"||",[96,2286,2287],{"class":120}," {}\n",[96,2289,2290,2293,2296,2299,2301,2304,2306,2309],{"class":98,"line":276},[96,2291,2292],{"class":120}," nitroConfig.alias[",[96,2294,2295],{"class":130},"'#analytics'",[96,2297,2298],{"class":120},"] ",[96,2300,2017],{"class":109},[96,2302,2303],{"class":202}," resolve",[96,2305,1850],{"class":120},[96,2307,2308],{"class":130},"'./runtime/server/utils'",[96,2310,2311],{"class":120},")\n",[96,2313,2314],{"class":98,"line":291},[96,2315,2184],{"class":120},[96,2317,2318],{"class":98,"line":306},[96,2319,187],{"emptyLinePlaceholder":186},[96,2321,2322,2324,2326,2328,2331,2333,2336,2338,2340],{"class":98,"line":320},[96,2323,2158],{"class":120},[96,2325,2161],{"class":202},[96,2327,1850],{"class":120},[96,2329,2330],{"class":130},"'pages:extend'",[96,2332,2202],{"class":120},[96,2334,2335],{"class":212},"pages",[96,2337,285],{"class":120},[96,2339,2172],{"class":109},[96,2341,121],{"class":120},[96,2343,2344],{"class":98,"line":331},[96,2345,2346],{"class":102}," // Add or modify routes\n",[96,2348,2349,2352,2354],{"class":98,"line":337},[96,2350,2351],{"class":120}," pages.",[96,2353,2224],{"class":202},[96,2355,1793],{"class":120},[96,2357,2359,2361,2364],{"class":98,"line":2358},21,[96,2360,1803],{"class":120},[96,2362,2363],{"class":130},"'analytics-dashboard'",[96,2365,222],{"class":120},[96,2367,2369,2372,2375],{"class":98,"line":2368},22,[96,2370,2371],{"class":120}," path: ",[96,2373,2374],{"class":130},"'/admin/analytics'",[96,2376,222],{"class":120},[96,2378,2380,2383,2385,2387,2390],{"class":98,"line":2379},23,[96,2381,2382],{"class":120}," file: ",[96,2384,2011],{"class":202},[96,2386,1850],{"class":120},[96,2388,2389],{"class":130},"'./runtime/pages/dashboard.vue'",[96,2391,2392],{"class":120},"),\n",[96,2394,2396],{"class":98,"line":2395},24,[96,2397,2184],{"class":120},[96,2399,2401],{"class":98,"line":2400},25,[96,2402,2184],{"class":120},[96,2404,2406],{"class":98,"line":2405},26,[96,2407,340],{"class":120},[19,2409,2410],{},"The hook system is what makes modules genuinely powerful rather than just a packaging convention. You can modify almost any aspect of the application at build time — routes, middleware, server handlers, rendering configuration, head defaults.",[19,2412,2413,2414,2417],{},"A pattern I use frequently is conditional feature activation based on what other modules are installed. If your module integrates with authentication, check whether an auth module is present in the ",[93,2415,2416],{},"modules:done"," hook rather than requiring it as a hard dependency. This makes modules composable rather than monolithic.",[26,2419,2421],{"id":2420},"testing-and-publishing","Testing and Publishing",[19,2423,2424,2425,2428],{},"Testing a Nuxt module requires a test fixture — a minimal Nuxt application that uses the module. The ",[93,2426,2427],{},"@nuxt/test-utils"," package provides utilities for this:",[86,2430,2432],{"className":1757,"code":2431,"language":1759,"meta":91,"style":91},"import { describe, it, expect } from 'vitest'\nimport { setup, $fetch } from '@nuxt/test-utils'\n\nDescribe('analytics module', async () => {\n await setup({\n rootDir: './test/fixture',\n })\n\n it('injects analytics script', async () => {\n const html = await $fetch('/')\n expect(html).toContain('analytics.js')\n })\n})\n",[93,2433,2434,2446,2458,2462,2484,2493,2503,2507,2511,2531,2552,2570,2574],{"__ignoreMap":91},[96,2435,2436,2438,2441,2443],{"class":98,"line":99},[96,2437,1766],{"class":109},[96,2439,2440],{"class":120}," { describe, it, expect } ",[96,2442,1772],{"class":109},[96,2444,2445],{"class":130}," 'vitest'\n",[96,2447,2448,2450,2453,2455],{"class":98,"line":106},[96,2449,1766],{"class":109},[96,2451,2452],{"class":120}," { setup, $fetch } ",[96,2454,1772],{"class":109},[96,2456,2457],{"class":130}," '@nuxt/test-utils'\n",[96,2459,2460],{"class":98,"line":124},[96,2461,187],{"emptyLinePlaceholder":186},[96,2463,2464,2467,2469,2472,2474,2477,2480,2482],{"class":98,"line":153},[96,2465,2466],{"class":202},"Describe",[96,2468,1850],{"class":120},[96,2470,2471],{"class":130},"'analytics module'",[96,2473,134],{"class":120},[96,2475,2476],{"class":109},"async",[96,2478,2479],{"class":120}," () ",[96,2481,2172],{"class":109},[96,2483,121],{"class":120},[96,2485,2486,2489,2491],{"class":98,"line":167},[96,2487,2488],{"class":109}," await",[96,2490,1847],{"class":202},[96,2492,1793],{"class":120},[96,2494,2495,2498,2501],{"class":98,"line":177},[96,2496,2497],{"class":120}," rootDir: ",[96,2499,2500],{"class":130},"'./test/fixture'",[96,2502,222],{"class":120},[96,2504,2505],{"class":98,"line":183},[96,2506,2184],{"class":120},[96,2508,2509],{"class":98,"line":190},[96,2510,187],{"emptyLinePlaceholder":186},[96,2512,2513,2516,2518,2521,2523,2525,2527,2529],{"class":98,"line":196},[96,2514,2515],{"class":202}," it",[96,2517,1850],{"class":120},[96,2519,2520],{"class":130},"'injects analytics script'",[96,2522,134],{"class":120},[96,2524,2476],{"class":109},[96,2526,2479],{"class":120},[96,2528,2172],{"class":109},[96,2530,121],{"class":120},[96,2532,2533,2535,2538,2540,2542,2545,2547,2550],{"class":98,"line":209},[96,2534,2005],{"class":109},[96,2536,2537],{"class":113}," html",[96,2539,117],{"class":109},[96,2541,2488],{"class":109},[96,2543,2544],{"class":202}," $fetch",[96,2546,1850],{"class":120},[96,2548,2549],{"class":130},"'/'",[96,2551,2311],{"class":120},[96,2553,2554,2557,2560,2563,2565,2568],{"class":98,"line":225},[96,2555,2556],{"class":202}," expect",[96,2558,2559],{"class":120},"(html).",[96,2561,2562],{"class":202},"toContain",[96,2564,1850],{"class":120},[96,2566,2567],{"class":130},"'analytics.js'",[96,2569,2311],{"class":120},[96,2571,2572],{"class":98,"line":238},[96,2573,2184],{"class":120},[96,2575,2576],{"class":98,"line":249},[96,2577,1891],{"class":120},[19,2579,2580,2581,2584,2585,2588],{},"The test fixture is a real Nuxt app in your module's ",[93,2582,2583],{},"test/fixture/"," directory with a ",[93,2586,2587],{},"nuxt.config.ts"," that registers your module. This integration-level testing catches issues that unit tests miss — timing problems, hook ordering, conflicts with other modules.",[19,2590,2591,2592,2595,2596,2599],{},"For publishing, the standard approach is to use ",[93,2593,2594],{},"unbuild"," for the build step. It handles dual CJS/ESM output and preserves the directory structure modules need. Your ",[93,2597,2598],{},"package.json"," should export the module entry point and the runtime directory separately so that tree-shaking works correctly in consuming applications.",[19,2601,2602,2603,2606],{},"Before publishing, test your module in a real project using ",[93,2604,2605],{},"npm link"," or a file dependency. The build-time versus runtime boundary creates subtle issues that only surface when the module is consumed as a package rather than developed locally. I have found issues with path resolution, missing runtime files, and type declaration problems that were invisible during development.",[19,2608,2609,2610,2614],{},"Building modules is one of the most effective ways to reduce duplication across projects and enforce patterns consistently. The initial investment pays back quickly if you maintain more than one Nuxt application — and the skills transfer directly to understanding how the modules you depend on actually work, which is valuable when you need to debug or extend them. For related patterns around ",[46,2611,2613],{"href":2612},"/blog/nuxt-middleware-guide","middleware",", the same module hooks let you inject route guards programmatically.",[377,2616,2617],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .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":91,"searchDepth":124,"depth":124,"links":2619},[2620,2621,2622,2623],{"id":1750,"depth":106,"text":1751},{"id":1936,"depth":106,"text":1937},{"id":2131,"depth":106,"text":2132},{"id":2420,"depth":106,"text":2421},"Frontend","Learn how to build custom Nuxt 3 modules that extend framework functionality — hooks, runtime plugins, auto-imports, and publishing to npm.",[2627,2628],"Nuxt 3 module development","building Nuxt modules",{},"/blog/nuxt-3-module-development",{"title":1738,"description":2625},"blog/nuxt-3-module-development",[2634,2635,2636],"Nuxt","Vue","TypeScript","0KhkIOfqMl6vWdKmOEsFm8-H7gUOzvdU0ptKJbXLxe4",{"id":2639,"title":2640,"author":2641,"body":2642,"category":2808,"date":1433,"description":2809,"extension":389,"featured":390,"image":391,"keywords":2810,"meta":2813,"navigation":186,"path":2814,"readTime":183,"seo":2815,"stem":2816,"tags":2817,"__hash__":2821},"blog/blog/saas-trial-to-paid-conversion.md","Converting SaaS Trials to Paid: The Technical Playbook",{"name":9,"bio":10},{"type":12,"value":2643,"toc":2800},[2644,2648,2651,2654,2657,2659,2663,2666,2677,2683,2689,2692,2694,2698,2701,2707,2713,2719,2727,2729,2733,2736,2742,2748,2754,2765,2767,2771,2774,2777,2779,2781],[26,2645,2647],{"id":2646},"conversion-is-an-engineering-problem","Conversion Is an Engineering Problem",[19,2649,2650],{},"Most SaaS companies treat trial-to-paid conversion as a marketing and sales problem. They optimize email sequences, adjust trial lengths, and A/B test pricing pages. These tactics matter, but they're surface-level interventions on a problem that's fundamentally architectural.",[19,2652,2653],{},"The real question is whether your trial experience delivers enough value, fast enough, for a user to conclude that they need to keep using your product. That's a product engineering challenge. It depends on how quickly users reach their first meaningful outcome, how much friction exists between signup and value, and whether your application makes it obvious what they should do next.",[19,2655,2656],{},"I've worked on several SaaS products where conversion rates improved dramatically not from marketing changes but from engineering changes — reducing onboarding steps, preloading sample data, and removing feature gates that prevented trial users from experiencing the core value proposition.",[720,2658],{},[26,2660,2662],{"id":2661},"the-activation-framework","The Activation Framework",[19,2664,2665],{},"Activation is the moment a trial user first experiences real value from your product. Every SaaS product has a different activation event, but it always involves the user completing a meaningful action — not just signing up, not just clicking around, but doing something that demonstrates the product's value.",[19,2667,2668,2671,2672,2676],{},[66,2669,2670],{},"Define your activation metric."," For a project management tool, it might be \"created a project and added a team member.\" For an analytics product, it might be \"connected a data source and viewed a report.\" For a ",[46,2673,2675],{"href":2674},"/blog/stripe-subscription-billing","billing platform",", it might be \"created a subscription plan and attached it to a customer.\" The metric should represent the minimum amount of usage that correlates with conversion.",[19,2678,2679,2682],{},[66,2680,2681],{},"Measure time to activation."," How long does it take the average trial user to reach the activation event? This is the number that engineering can directly influence. Every step between signup and activation is a potential drop-off point, and every drop-off point is a conversion leak.",[19,2684,2685,2688],{},[66,2686,2687],{},"Remove obstacles to activation."," This is where engineering effort has the highest leverage. If your product requires data import before it's useful, offer sample data so users can explore immediately. If configuration is required, provide sensible defaults. If integrations are needed, prioritize the three most common ones and make them one-click.",[19,2690,2691],{},"The technical implementation of this framework requires an event system that tracks user behavior in granular detail. Emit events for every meaningful action, aggregate them into a per-user activation score, and use that score to trigger targeted interventions — in-app guidance, email nudges, or proactive support outreach.",[720,2693],{},[26,2695,2697],{"id":2696},"engineering-the-trial-experience","Engineering the Trial Experience",[19,2699,2700],{},"The trial experience is a distinct product surface that deserves its own engineering attention. It's not just the regular product with a time limit attached.",[19,2702,2703,2706],{},[66,2704,2705],{},"Progressive feature exposure"," works better than giving trial users access to everything at once. Show them the core features first, and introduce advanced features as they demonstrate proficiency with the basics. This prevents overwhelm and creates a natural learning curve that builds investment in the product.",[19,2708,2709,2712],{},[66,2710,2711],{},"Contextual onboarding"," replaces the \"guided tour\" pattern that most users skip. Instead of a modal walkthrough on first login, show relevant guidance at the moment the user encounters a feature for the first time. Tooltips that appear when a user hovers over an unfamiliar UI element. Empty states that explain what belongs in each section and provide a clear call to action to populate it.",[19,2714,2715,2718],{},[66,2716,2717],{},"Trial limitations should be strategic, not punitive."," Limiting the number of users, the volume of data, or access to advanced features is reasonable. Limiting core functionality so severely that the user can't experience the product's value defeats the purpose of the trial entirely. The goal is to let them feel the value and then make them want more of it.",[19,2720,2721,2722,2726],{},"Building this requires a solid ",[46,2723,2725],{"href":2724},"/blog/role-based-access-control-guide","role-based access control system"," that can differentiate between trial users, paying users, and different plan tiers — and enforce those differences at the API level, not just the UI level.",[720,2728],{},[26,2730,2732],{"id":2731},"the-technical-infrastructure-behind-conversion","The Technical Infrastructure Behind Conversion",[19,2734,2735],{},"Several systems work together to drive trial conversion, and most of them are invisible to the user.",[19,2737,2738,2741],{},[66,2739,2740],{},"An event pipeline"," captures every meaningful user action and feeds it into analytics, email automation, and in-app messaging systems. This is the foundation — without reliable event tracking, you're flying blind on what trial users are actually doing.",[19,2743,2744,2747],{},[66,2745,2746],{},"Automated email sequences"," triggered by behavior, not just time. \"You signed up 3 days ago and haven't connected a data source\" is dramatically more effective than \"Day 3 of your trial — check out these features.\" The behavioral triggers come from your event pipeline, which means the email system and the product need to share a common event vocabulary.",[19,2749,2750,2753],{},[66,2751,2752],{},"In-app messaging"," for users who are active but stuck. If a user has logged in five times but hasn't completed the activation event, something is blocking them. A well-timed in-app message offering help or pointing to the next step can unblock them without requiring them to open a support ticket.",[19,2755,2756,2759,2760,2764],{},[66,2757,2758],{},"Usage-based trial extensions"," for users who are actively using the product but haven't converted. Automatically extending a trial for someone who logged in every day of their trial and is clearly getting value is almost always the right business decision. It's a simple rule in your ",[46,2761,2763],{"href":2762},"/blog/subscription-management-architecture","subscription management system"," but it requires the event data to implement.",[720,2766],{},[26,2768,2770],{"id":2769},"measuring-what-matters","Measuring What Matters",[19,2772,2773],{},"The conversion funnel for a SaaS trial has specific, measurable stages: signup, first login, activation event, repeated usage, and conversion. Instrument each stage, measure the drop-off between them, and focus engineering effort on the largest drop-offs.",[19,2775,2776],{},"The data will almost always show that the biggest leak is between signup and activation. Most trial users never reach the point where they experience real value. That's not a marketing failure — it's a product engineering opportunity. Solve it, and conversion follows.",[720,2778],{},[26,2780,1581],{"id":1580},[727,2782,2783,2788,2794],{},[730,2784,2785],{},[46,2786,2787],{"href":2674},"Stripe Subscription Billing: A Developer's Complete Guide",[730,2789,2790],{},[46,2791,2793],{"href":2792},"/blog/saas-pricing-models","SaaS Pricing Models: Technical Architecture Behind the Strategy",[730,2795,2796],{},[46,2797,2799],{"href":2798},"/blog/saas-churn-reduction","Reducing SaaS Churn with Better Product Engineering",{"title":91,"searchDepth":124,"depth":124,"links":2801},[2802,2803,2804,2805,2806,2807],{"id":2646,"depth":106,"text":2647},{"id":2661,"depth":106,"text":2662},{"id":2696,"depth":106,"text":2697},{"id":2731,"depth":106,"text":2732},{"id":2769,"depth":106,"text":2770},{"id":1580,"depth":106,"text":1581},"Business","Trial-to-paid conversion isn't just a marketing problem. The technical decisions behind your trial experience determine whether users ever see enough value to pay.",[2811,2812],"SaaS trial conversion","trial to paid optimization",{},"/blog/saas-trial-to-paid-conversion",{"title":2640,"description":2809},"blog/saas-trial-to-paid-conversion",[2818,2819,2820],"SaaS","Product Engineering","Conversion","QESQWUjGwOGBj1dXsPXE_4P7HOCTqn45IIUUdsbe9jE",{"id":2823,"title":2824,"author":2825,"body":2826,"category":759,"date":2914,"description":2915,"extension":389,"featured":390,"image":391,"keywords":2916,"meta":2920,"navigation":186,"path":2921,"readTime":167,"seo":2922,"stem":2923,"tags":2924,"__hash__":2928},"blog/blog/celtic-christianity-scotland.md","Celtic Christianity in Scotland: Monks, Manuscripts, and Missions",{"name":9,"bio":10},{"type":12,"value":2827,"toc":2908},[2828,2832,2835,2843,2850,2854,2857,2860,2863,2867,2875,2883,2895,2899,2902,2905],[26,2829,2831],{"id":2830},"christianity-at-the-edge-of-the-world","Christianity at the Edge of the World",[19,2833,2834],{},"Christianity arrived in Scotland not as a top-down imperial project but as a grassroots movement carried by monks. The earliest documented mission is that of Ninian, who established a church at Whithorn in Galloway around 397 AD — before the Roman legions had even fully withdrawn from Britain. Ninian's mission targeted the southern Picts and the Britons of Strathclyde, working at the very edge of the post-Roman world.",[19,2836,2837,2838,2842],{},"But the figure who defined Celtic Christianity in Scotland was Columba. An Irish prince of the Ui Neill dynasty — a lineage later mythologized through figures like ",[46,2839,2841],{"href":2840},"/blog/niall-of-the-nine-hostages-ross-connection","Niall of the Nine Hostages"," — Columba left Ireland in 563 AD and established a monastery on the island of Iona, off the west coast of Scotland. Whether he left as a penitent exile or a deliberate missionary is debated. The impact.",[19,2844,2845,2846,1124],{},"Iona became the most important center of Christian learning in the British Isles. From that tiny island, monks launched missions to the Picts, to Northumbria, and across the North Sea. The Book of Kells — arguably the greatest masterpiece of medieval European art — was likely begun on Iona before being taken to Ireland for safety during ",[46,2847,2849],{"href":2848},"/blog/viking-age-scotland","Viking raids",[26,2851,2853],{"id":2852},"what-made-celtic-christianity-different","What Made Celtic Christianity Different",[19,2855,2856],{},"Celtic Christianity was not a separate religion from Roman Christianity, but it had distinctive characteristics that brought it into conflict with Rome. The differences were organizational, liturgical, and aesthetic.",[19,2858,2859],{},"Organizationally, Celtic Christianity was monastic rather than episcopal. Power resided in abbots and monasteries, not in bishops and dioceses. A monastery like Iona functioned as a self-contained community — part university, part farm, part scriptorium, part mission base. The abbot held authority over a network of daughter houses, creating a structure that resembled a clan more than a bureaucracy.",[19,2861,2862],{},"The most famous dispute was over the dating of Easter. Celtic churches used an older computational method that frequently produced a different date than Rome's. The Synod of Whitby in 664 settled the question in favor of Roman practice in Northumbria, and gradually the Celtic churches fell into line. But the tonsure — Celtic monks shaved the front of the head rather than the crown — persisted as a visible symbol of difference for generations longer.",[26,2864,2866],{"id":2865},"applecross-and-the-monastic-network","Applecross and the Monastic Network",[19,2868,2869,2870,2874],{},"While Iona dominates the narrative, it was one node in a vast monastic network. ",[46,2871,2873],{"href":2872},"/blog/applecross-obeolans-monks-dynasty","Applecross",", founded by Maelrubha in 673 AD on the remote west coast of Ross-shire, served as a mission center for the northern Highlands. Maelrubha was an Irishman from Bangor, and his monastery connected the Ross territory to the wider world of Gaelic Christianity.",[19,2876,2877,2878,2882],{},"The monastic communities were not isolated hermitages. They were centers of literacy, craft, agriculture, and diplomacy. Monks maintained genealogies, recorded legal proceedings, and preserved the oral traditions that would later be compiled into works like the ",[46,2879,2881],{"href":2880},"/blog/lebor-gabala-erenn-book-of-invasions","Lebor Gabala Erenn",". They also served practical functions — offering hospitality to travelers, providing medical care, and mediating disputes between chiefs.",[19,2884,2885,2886,2889,2890,2894],{},"The connection between these monasteries and the later ",[46,2887,2888],{"href":696},"clan system"," is direct. Many clan founders were descendants of monastic families. Fearchar mac an t-Sagairt — the founder of ",[46,2891,2893],{"href":2892},"/blog/clan-ross-origins-history","Clan Ross"," — was literally \"Son of the Priest,\" a title that likely indicated descent from a hereditary monastic lineage at Applecross.",[26,2896,2898],{"id":2897},"legacy-in-stone-and-story","Legacy in Stone and Story",[19,2900,2901],{},"The physical legacy of Celtic Christianity survives in high crosses, carved stones, and the ruins of monastic settlements scattered across Scotland's western seaboard and islands. The theological legacy is harder to trace — Rome eventually absorbed the Celtic churches completely — but the cultural legacy endures.",[19,2903,2904],{},"Celtic Christianity's emphasis on nature, on the spiritual significance of wild places, and on the monastic life as a form of spiritual athletics left a deep mark on Scottish and Irish culture. The hermit's cell on a storm-battered island, the illuminated manuscript produced in a cold scriptorium, the long sea voyage as spiritual pilgrimage — these images continue to resonate because they feel so different from the institutional Christianity that eventually replaced them.",[19,2906,2907],{},"The monks who built Iona and Applecross were not romantics. They were practical men operating in a violent, uncertain world. But they created something that outlasted the political structures of their time and continues to shape how we imagine the relationship between faith, learning, and the natural world.",{"title":91,"searchDepth":124,"depth":124,"links":2909},[2910,2911,2912,2913],{"id":2830,"depth":106,"text":2831},{"id":2852,"depth":106,"text":2853},{"id":2865,"depth":106,"text":2866},{"id":2897,"depth":106,"text":2898},"2025-08-20","Before Rome standardized the faith, Celtic monks built a Christian tradition rooted in monasticism, scholarship, and the wild edges of the Atlantic world.",[2917,2918,2919],"celtic christianity scotland","iona monastery history","celtic monks scotland",{},"/blog/celtic-christianity-scotland",{"title":2824,"description":2915},"blog/celtic-christianity-scotland",[2925,773,2926,2927],"Celtic Christianity","Early Medieval","Monasticism","ar0OTxLbzIzzGsF-i0MkfjBxuDd9LyK8iNp6NZFC180",{"id":2930,"title":2931,"author":2932,"body":2933,"category":2624,"date":3414,"description":3415,"extension":389,"featured":390,"image":391,"keywords":3416,"meta":3419,"navigation":186,"path":3420,"readTime":177,"seo":3421,"stem":3422,"tags":3423,"__hash__":3427},"blog/blog/dark-mode-implementation.md","Implementing Dark Mode Properly in Modern Web Apps",{"name":9,"bio":10},{"type":12,"value":2934,"toc":3408},[2935,2939,2942,2945,2955,3114,3121,3123,3127,3130,3136,3180,3183,3196,3206,3317,3320,3322,3326,3337,3345,3352,3361,3371,3373,3377,3380,3383,3391,3402,3405],[26,2936,2938],{"id":2937},"dark-mode-is-a-design-system-problem","Dark Mode Is a Design System Problem",[19,2940,2941],{},"Adding dark mode to an existing application sounds simple — swap white backgrounds for dark ones, change text to light colors, and ship it. In practice, dark mode touches every visual element in your application: backgrounds, text, borders, shadows, images, icons, form controls, charts, syntax highlighting, third-party embeds, and user-generated content. Treating it as a quick toggle produces a theme that is technically dark but visually broken.",[19,2943,2944],{},"Dark mode is a design system problem because it requires a parallel color system. Every color in your application needs a dark mode equivalent that maintains visual hierarchy, sufficient contrast, and brand consistency. A primary blue that looks great on white may be invisible on dark gray. A subtle light gray border that separates content sections on a white background becomes invisible on a dark background. Shadows that create depth on light backgrounds look unnatural on dark ones.",[19,2946,2947,2948,134,2951,2954],{},"The approach that works is building your color system on semantic design tokens from the start. Rather than referencing specific colors in your components (",[93,2949,2950],{},"background: #ffffff",[93,2952,2953],{},"color: #1a1a2e","), reference tokens that map to different values per theme:",[86,2956,2960],{"className":2957,"code":2958,"language":2959,"meta":91,"style":91},"language-css shiki shiki-themes github-dark",":root {\n --color-bg-primary: #ffffff;\n --color-bg-secondary: #f5f5f5;\n --color-text-primary: #1a1a2e;\n --color-text-secondary: #6b7280;\n --color-border: #e5e7eb;\n}\n\n[data-theme=\"dark\"] {\n --color-bg-primary: #0f172a;\n --color-bg-secondary: #1e293b;\n --color-text-primary: #f1f5f9;\n --color-text-secondary: #94a3b8;\n --color-border: #334155;\n}\n","css",[93,2961,2962,2969,2983,2995,3007,3019,3031,3035,3039,3055,3066,3077,3088,3099,3110],{"__ignoreMap":91},[96,2963,2964,2967],{"class":98,"line":99},[96,2965,2966],{"class":202},":root",[96,2968,121],{"class":120},[96,2970,2971,2974,2977,2980],{"class":98,"line":106},[96,2972,2973],{"class":212}," --color-bg-primary",[96,2975,2976],{"class":120},": ",[96,2978,2979],{"class":113},"#ffffff",[96,2981,2982],{"class":120},";\n",[96,2984,2985,2988,2990,2993],{"class":98,"line":124},[96,2986,2987],{"class":212}," --color-bg-secondary",[96,2989,2976],{"class":120},[96,2991,2992],{"class":113},"#f5f5f5",[96,2994,2982],{"class":120},[96,2996,2997,3000,3002,3005],{"class":98,"line":153},[96,2998,2999],{"class":212}," --color-text-primary",[96,3001,2976],{"class":120},[96,3003,3004],{"class":113},"#1a1a2e",[96,3006,2982],{"class":120},[96,3008,3009,3012,3014,3017],{"class":98,"line":167},[96,3010,3011],{"class":212}," --color-text-secondary",[96,3013,2976],{"class":120},[96,3015,3016],{"class":113},"#6b7280",[96,3018,2982],{"class":120},[96,3020,3021,3024,3026,3029],{"class":98,"line":177},[96,3022,3023],{"class":212}," --color-border",[96,3025,2976],{"class":120},[96,3027,3028],{"class":113},"#e5e7eb",[96,3030,2982],{"class":120},[96,3032,3033],{"class":98,"line":183},[96,3034,340],{"class":120},[96,3036,3037],{"class":98,"line":190},[96,3038,187],{"emptyLinePlaceholder":186},[96,3040,3041,3044,3047,3049,3052],{"class":98,"line":196},[96,3042,3043],{"class":120},"[",[96,3045,3046],{"class":202},"data-theme",[96,3048,2017],{"class":109},[96,3050,3051],{"class":130},"\"dark\"",[96,3053,3054],{"class":120},"] {\n",[96,3056,3057,3059,3061,3064],{"class":98,"line":209},[96,3058,2973],{"class":212},[96,3060,2976],{"class":120},[96,3062,3063],{"class":113},"#0f172a",[96,3065,2982],{"class":120},[96,3067,3068,3070,3072,3075],{"class":98,"line":225},[96,3069,2987],{"class":212},[96,3071,2976],{"class":120},[96,3073,3074],{"class":113},"#1e293b",[96,3076,2982],{"class":120},[96,3078,3079,3081,3083,3086],{"class":98,"line":238},[96,3080,2999],{"class":212},[96,3082,2976],{"class":120},[96,3084,3085],{"class":113},"#f1f5f9",[96,3087,2982],{"class":120},[96,3089,3090,3092,3094,3097],{"class":98,"line":249},[96,3091,3011],{"class":212},[96,3093,2976],{"class":120},[96,3095,3096],{"class":113},"#94a3b8",[96,3098,2982],{"class":120},[96,3100,3101,3103,3105,3108],{"class":98,"line":262},[96,3102,3023],{"class":212},[96,3104,2976],{"class":120},[96,3106,3107],{"class":113},"#334155",[96,3109,2982],{"class":120},[96,3111,3112],{"class":98,"line":276},[96,3113,340],{"class":120},[19,3115,3116,3117,3120],{},"Components reference the tokens, and the theme swap changes the token values. This is exactly how Tailwind CSS handles dark mode with its ",[93,3118,3119],{},"dark:"," variant prefix — each utility class can have a dark mode counterpart, and the theme is controlled by a class or attribute on the root element.",[720,3122],{},[26,3124,3126],{"id":3125},"respecting-user-preferences","Respecting User Preferences",[19,3128,3129],{},"Users express their theme preference in two places: their operating system settings and your application's theme toggle. A proper implementation respects both and lets the explicit choice override the system default.",[19,3131,807,3132,3135],{},[93,3133,3134],{},"prefers-color-scheme"," media query detects the OS preference:",[86,3137,3139],{"className":2957,"code":3138,"language":2959,"meta":91,"style":91},"@media (prefers-color-scheme: dark) {\n :root {\n --color-bg-primary: #0f172a;\n /* dark values */\n }\n}\n",[93,3140,3141,3149,3156,3166,3171,3176],{"__ignoreMap":91},[96,3142,3143,3146],{"class":98,"line":99},[96,3144,3145],{"class":109},"@media",[96,3147,3148],{"class":120}," (prefers-color-scheme: dark) {\n",[96,3150,3151,3154],{"class":98,"line":106},[96,3152,3153],{"class":202}," :root",[96,3155,121],{"class":120},[96,3157,3158,3160,3162,3164],{"class":98,"line":124},[96,3159,2973],{"class":212},[96,3161,2976],{"class":120},[96,3163,3063],{"class":113},[96,3165,2982],{"class":120},[96,3167,3168],{"class":98,"line":153},[96,3169,3170],{"class":102}," /* dark values */\n",[96,3172,3173],{"class":98,"line":167},[96,3174,3175],{"class":120}," }\n",[96,3177,3178],{"class":98,"line":177},[96,3179,340],{"class":120},[19,3181,3182],{},"But media queries alone are insufficient because users need a way to override the system preference within your application. The standard pattern is a three-state toggle: light, dark, and system (auto). When set to system, follow the OS preference. When set to light or dark, apply that theme regardless of the OS setting.",[19,3184,3185,3186,3189,3190,3192,3193,3195],{},"Store the user's explicit preference in ",[93,3187,3188],{},"localStorage",". On page load, check ",[93,3191,3188],{}," first — if a preference exists, apply it immediately. If no preference exists, fall back to the system preference via ",[93,3194,3134],{},". This logic must run before the page renders to prevent a flash of the wrong theme (FOWT).",[19,3197,3198,3199,3201,3202,3205],{},"For Nuxt or other SSR frameworks, this creates a hydration challenge. The server does not know the user's theme preference, so it renders either light or dark by default. When the client hydrates and reads ",[93,3200,3188],{},", the theme may switch, causing a visible flash. The solution is a small inline script in the ",[93,3203,3204],{},"\u003Chead>"," that sets the theme attribute before the body renders:",[86,3207,3211],{"className":3208,"code":3209,"language":3210,"meta":91,"style":91},"language-html shiki shiki-themes github-dark","\u003Cscript>\n const theme = localStorage.getItem('theme');\n if (theme === 'dark' || (!theme && matchMedia('(prefers-color-scheme: dark)').matches)) {\n document.documentElement.setAttribute('data-theme', 'dark');\n }\n\u003C/script>\n","html",[93,3212,3213,3225,3248,3284,3304,3308],{"__ignoreMap":91},[96,3214,3215,3218,3222],{"class":98,"line":99},[96,3216,3217],{"class":120},"\u003C",[96,3219,3221],{"class":3220},"s4JwU","script",[96,3223,3224],{"class":120},">\n",[96,3226,3227,3229,3232,3234,3237,3240,3242,3245],{"class":98,"line":106},[96,3228,2005],{"class":109},[96,3230,3231],{"class":113}," theme",[96,3233,117],{"class":109},[96,3235,3236],{"class":120}," localStorage.",[96,3238,3239],{"class":202},"getItem",[96,3241,1850],{"class":120},[96,3243,3244],{"class":130},"'theme'",[96,3246,3247],{"class":120},");\n",[96,3249,3250,3252,3255,3257,3260,3263,3265,3267,3270,3273,3276,3278,3281],{"class":98,"line":124},[96,3251,1866],{"class":109},[96,3253,3254],{"class":120}," (theme ",[96,3256,297],{"class":109},[96,3258,3259],{"class":130}," 'dark'",[96,3261,3262],{"class":109}," ||",[96,3264,279],{"class":120},[96,3266,1871],{"class":109},[96,3268,3269],{"class":120},"theme ",[96,3271,3272],{"class":109},"&&",[96,3274,3275],{"class":202}," matchMedia",[96,3277,1850],{"class":120},[96,3279,3280],{"class":130},"'(prefers-color-scheme: dark)'",[96,3282,3283],{"class":120},").matches)) {\n",[96,3285,3286,3289,3292,3294,3297,3299,3302],{"class":98,"line":153},[96,3287,3288],{"class":120}," document.documentElement.",[96,3290,3291],{"class":202},"setAttribute",[96,3293,1850],{"class":120},[96,3295,3296],{"class":130},"'data-theme'",[96,3298,134],{"class":120},[96,3300,3301],{"class":130},"'dark'",[96,3303,3247],{"class":120},[96,3305,3306],{"class":98,"line":167},[96,3307,3175],{"class":120},[96,3309,3310,3313,3315],{"class":98,"line":177},[96,3311,3312],{"class":120},"\u003C/",[96,3314,3221],{"class":3220},[96,3316,3224],{"class":120},[19,3318,3319],{},"This script runs synchronously before any rendering, preventing the flash entirely.",[720,3321],{},[26,3323,3325],{"id":3324},"colors-contrast-and-common-mistakes","Colors, Contrast, and Common Mistakes",[19,3327,3328,3329,3332,3333,3336],{},"The most common dark mode mistake is using pure black (",[93,3330,3331],{},"#000000",") as the background. Pure black creates maximum contrast with white text, which causes eye strain during extended reading. Material Design's dark theme guidelines recommend dark gray (",[93,3334,3335],{},"#121212"," or similar) as the surface color, with lighter grays for elevated surfaces. The slight lightness difference creates a visual hierarchy through elevation without the harshness of pure black.",[19,3338,3339,3340,3344],{},"Contrast requirements do not change between themes. ",[46,3341,3343],{"href":3342},"/blog/web-accessibility-wcag-compliance","WCAG AA requires 4.5:1"," contrast for normal text and 3:1 for large text in both light and dark modes. Test every text color against its dark background. Common failures include secondary text that passes contrast on white but fails on dark gray, and link colors that are distinguishable in light mode but blend into the dark background.",[19,3346,3347,3348,3351],{},"Images and media require special attention. Photos generally look fine in dark mode, but images with white or transparent backgrounds — logos, diagrams, screenshots — appear as bright rectangles in a dark interface. Solutions include providing dark-mode variants of key images, adding subtle borders or background padding to images in dark mode, or using ",[93,3349,3350],{},"mix-blend-mode"," to blend images with the dark background.",[19,3353,3354,3355,3358,3359,1124],{},"Icons that use ",[93,3356,3357],{},"currentColor"," automatically adapt to the theme. Icons with hardcoded colors do not. If your icon system uses SVGs with fixed fill colors, you will need dark mode variants or need to refactor them to use ",[93,3360,3357],{},[19,3362,3363,3364,3367,3368,3370],{},"Shadows need rethinking entirely. Drop shadows that create depth on light backgrounds become invisible against dark backgrounds. In dark mode, use lighter background colors for elevated elements (cards, modals, dropdowns) rather than shadows. A card with ",[93,3365,3366],{},"background: #1e293b"," on a ",[93,3369,3063],{}," page creates visual hierarchy through contrast, not shadow.",[720,3372],{},[26,3374,3376],{"id":3375},"testing-dark-mode-thoroughly","Testing Dark Mode Thoroughly",[19,3378,3379],{},"Dark mode testing is tedious but essential because dark mode issues are often invisible to developers working in light mode. The problems only appear when you actually use the dark theme continuously.",[19,3381,3382],{},"Start by using your own application in dark mode for an entire day. You will immediately find elements that were missed — a white background on a third-party embed, a chart with hardcoded light-mode colors, a tooltip with no dark variant, form inputs with unreadable placeholder text.",[19,3384,3385,3386,3390],{},"Automated visual regression testing catches many dark mode regressions. Tools like Playwright can take screenshots in both themes and compare them against baselines. Configure your ",[46,3387,3389],{"href":3388},"/blog/continuous-deployment-guide","testing pipeline"," to run visual tests in both light and dark mode so that changes to one theme do not inadvertently break the other.",[19,3392,3393,3394,3397,3398,3401],{},"Check transition behavior. When users switch themes, should the change be instant or animated? A subtle transition (",[93,3395,3396],{},"transition: background-color 0.2s, color 0.2s",") on key elements prevents the jarring flash of a hard switch. But applying transition to all elements via ",[93,3399,3400],{},"* { transition: all 0.2s }"," causes performance issues and unintended animation of elements that should not transition.",[19,3403,3404],{},"Test the theme across all pages, not just the homepage. Internal pages, settings screens, error pages, loading states, and empty states are commonly forgotten. Every screen in your application should be auditable in both themes before shipping.",[377,3406,3407],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":91,"searchDepth":124,"depth":124,"links":3409},[3410,3411,3412,3413],{"id":2937,"depth":106,"text":2938},{"id":3125,"depth":106,"text":3126},{"id":3324,"depth":106,"text":3325},{"id":3375,"depth":106,"text":3376},"2025-08-19","Dark mode is more than inverting colors. Here's how to implement dark mode that looks good, respects user preferences, and doesn't introduce accessibility issues.",[3417,3418],"dark mode implementation","dark mode web development",{},"/blog/dark-mode-implementation",{"title":2931,"description":3415},"blog/dark-mode-implementation",[3424,3425,3426],"Dark Mode","CSS","Accessibility","IsThnNtA9CwjzlHAT4pEwXIRQaX2c_yTEHDhSq1Ul7c",{"id":3429,"title":3430,"author":3431,"body":3432,"category":759,"date":3575,"description":3576,"extension":389,"featured":390,"image":391,"keywords":3577,"meta":3583,"navigation":186,"path":3584,"readTime":183,"seo":3585,"stem":3586,"tags":3587,"__hash__":3592},"blog/blog/scottish-english-dialect-history.md","Scots English: The Dialect with Its Own Literature",{"name":9,"bio":10},{"type":12,"value":3433,"toc":3568},[3434,3438,3441,3444,3452,3455,3459,3470,3473,3476,3480,3483,3486,3493,3497,3500,3531,3538,3546,3548,3550],[26,3435,3437],{"id":3436},"a-language-or-a-dialect","A Language or a Dialect?",[19,3439,3440],{},"The question of whether Scots is a language or a dialect of English has generated more heat than light for centuries. The linguistic answer is simple: there is no objective line between a language and a dialect. The distinction is political, not scientific. As the old saying goes, a language is a dialect with an army and a navy.",[19,3442,3443],{},"What is not debatable is that Scots has a continuous literary tradition from the fourteenth century, a distinct grammar and phonology, and a vocabulary that includes thousands of words not found in standard English. It was the language of the Scottish court, the Scottish parliament, and the Scottish church before the Union of the Crowns in 1603 began the long process of anglicization. It has its own Bible translation, its own poetry tradition, and its own prose literature.",[19,3445,3446,3447,3451],{},"Scots descends from the Northumbrian dialect of Old English, brought to southeastern Scotland by Anglo-Saxon settlers in the seventh century. It developed independently from the dialects that became standard English, accumulating Norse loanwords from Viking settlement, ",[46,3448,3450],{"href":3449},"/blog/celtic-loanwords-english","Gaelic influences"," from the Highland border, French borrowings from the Auld Alliance with France, and Dutch and Flemish vocabulary from centuries of North Sea trade.",[19,3453,3454],{},"By the fifteenth century, the language was called \"Scottis\" by its own speakers, distinguishing it from the \"Inglis\" spoken south of the border. It was a court language, an administrative language, and a literary language of considerable sophistication.",[26,3456,3458],{"id":3457},"the-golden-age-and-the-decline","The Golden Age and the Decline",[19,3460,3461,3462,3465,3466,3469],{},"The golden age of Scots literature runs from roughly 1370 to 1560. John Barbour's ",[436,3463,3464],{},"The Brus"," (1375) is a verse chronicle of Robert the Bruce's wars of independence, written in a Scots that is distinct from the contemporary English of Chaucer. Robert Henryson, William Dunbar, and Gavin Douglas produced poetry in Scots that stands comparison with anything written in English in the same period. Douglas's translation of Virgil's ",[436,3467,3468],{},"Aeneid"," into Scots (1513) was the first complete translation of a major classical work into any form of English.",[19,3471,3472],{},"The decline began with the Union of the Crowns in 1603, when James VI of Scotland became James I of England and moved his court to London. The Scottish court had been the primary patron of Scots literature. With the court gone, the prestige language shifted to English. The Scottish Reformation reinforced the shift: the Geneva Bible, printed in English, became the standard text of the Scottish kirk, and English became the language of religion and education.",[19,3474,3475],{},"By the eighteenth century, Scots was increasingly confined to speech, while English dominated writing. The Scottish Enlightenment was conducted almost entirely in English. David Hume, Adam Smith, and the other luminaries of Edinburgh wrote standard English, sometimes anxiously checking their work for \"Scotticisms\" that might betray their origins.",[26,3477,3479],{"id":3478},"burns-and-the-literary-revival","Burns and the Literary Revival",[19,3481,3482],{},"Robert Burns changed the equation. His poetry, published from 1786 onward, demonstrated that Scots could be a vehicle for literature of the highest order. \"Tam o' Shanter,\" \"To a Mouse,\" \"A Man's a Man for A' That\" -- these are not dialect curiosities. They are masterpieces of world literature, written in a language that Burns knew from the fields and kitchens of Ayrshire.",[19,3484,3485],{},"Burns did not write in pure Scots. His language is a continuum, shifting between broad Scots and standard English within a single poem, sometimes within a single line. This code-switching is itself a Scots literary technique -- the ability to modulate register, to draw humor or pathos from the gap between the two varieties.",[19,3487,3488,3489,3492],{},"After Burns, the tradition continued through the nineteenth century in the work of writers like James Hogg and the \"Kailyard\" school, and was revived in the twentieth century by Hugh MacDiarmid, whose Scots poetry deliberately drew on archaic and dialectal vocabulary to create a literary language capable of addressing modern subjects. MacDiarmid's ",[436,3490,3491],{},"A Drunk Man Looks at the Thistle"," (1926) is arguably the most ambitious long poem written in any variety of English in the twentieth century.",[26,3494,3496],{"id":3495},"scots-today","Scots Today",[19,3498,3499],{},"Modern Scots exists on a spectrum. At one end is broad Scots -- the traditional dialect of rural Aberdeenshire, the Borders, or Ayrshire -- which can be genuinely difficult for speakers of standard English to understand. At the other end is Scottish Standard English, which differs from English English mainly in accent and a scattering of vocabulary items. Most Scottish speakers occupy a position somewhere along this continuum, shifting toward Scots in informal contexts and toward standard English in formal ones.",[19,3501,3502,3503,3506,3507,3510,3511,3514,3515,3518,3519,3522,3523,3526,3527,3530],{},"The 2011 Scottish Census found that 1.5 million people in Scotland reported speaking Scots -- roughly 30 percent of the population. The number is contentious. Some argue it undercounts speakers who do not recognize their speech as \"Scots.\" Others argue it overcounts by including speakers of Scottish Standard English who are not really speaking Scots in any meaningful sense. That Scots vocabulary, pronunciation, and grammar continue to shape the English spoken in Scotland. Words like ",[436,3504,3505],{},"wee"," (small), ",[436,3508,3509],{},"braw"," (fine, handsome), ",[436,3512,3513],{},"dreich"," (wet and cold), ",[436,3516,3517],{},"ken"," (know), ",[436,3520,3521],{},"aye"," (yes, always), ",[436,3524,3525],{},"blether"," (talk), and ",[436,3528,3529],{},"scunner"," (annoy, disgust) are used daily by millions of people who may or may not think of themselves as Scots speakers.",[19,3532,3533,3534,3537],{},"The literary tradition continues too. Irvine Welsh's ",[436,3535,3536],{},"Trainspotting"," (1993) is written in a phonetic Edinburgh Scots that brought the language to a global audience. James Kelman, Tom Leonard, and Kathleen Jamie have all written significant work in or influenced by Scots.",[19,3539,3540,3541,3545],{},"Scots is not a relic. It is not a failed version of English. It is a parallel development from the same ",[46,3542,3544],{"href":3543},"/blog/grimms-law-sound-changes","Germanic root",", shaped by different contacts, different histories, and a different national experience. It has its own past, its own literature, and its own future -- however that future unfolds.",[720,3547],{},[26,3549,725],{"id":724},[727,3551,3552,3557,3562],{},[730,3553,3554],{},[46,3555,3556],{"href":3449},"Celtic Loanwords in English: The Words That Survived",[730,3558,3559],{},[46,3560,3561],{"href":3543},"Grimm's Law: How Sound Changes Reveal Language History",[730,3563,3564],{},[46,3565,3567],{"href":3566},"/blog/highland-clearances-clan-ross-diaspora","The Highland Clearances and Clan Ross Diaspora",{"title":91,"searchDepth":124,"depth":124,"links":3569},[3570,3571,3572,3573,3574],{"id":3436,"depth":106,"text":3437},{"id":3457,"depth":106,"text":3458},{"id":3478,"depth":106,"text":3479},{"id":3495,"depth":106,"text":3496},{"id":724,"depth":106,"text":725},"2025-08-16","Scots is not slang, not bad English, and not a failed attempt at received pronunciation. It is a distinct linguistic variety with its own grammar, vocabulary, and a literary tradition stretching back to the fourteenth century.",[3578,3579,3580,3581,3582],"scots english dialect","scots language history","scottish english","scots literature","robert burns language",{},"/blog/scottish-english-dialect-history",{"title":3430,"description":3576},"blog/scottish-english-dialect-history",[3588,773,3589,3590,3591],"Scots Language","Language History","English Dialects","Scottish Literature","2CzCHVBRNmNmNdSbcDewrHou3EboVQVbl3LlwFU4OE8",{"id":3594,"title":3595,"author":3596,"body":3597,"category":759,"date":3695,"description":3696,"extension":389,"featured":390,"image":391,"keywords":3697,"meta":3701,"navigation":186,"path":3702,"readTime":167,"seo":3703,"stem":3704,"tags":3705,"__hash__":3709},"blog/blog/autosomal-dna-ethnicity-estimates.md","Autosomal DNA and Ethnicity Estimates: Accuracy and Limits",{"name":9,"bio":10},{"type":12,"value":3598,"toc":3689},[3599,3603,3606,3618,3621,3625,3628,3635,3638,3641,3645,3648,3659,3662,3665,3669,3676,3679,3686],[26,3600,3602],{"id":3601},"the-pie-chart-problem","The Pie Chart Problem",[19,3604,3605],{},"When you take a consumer DNA test from AncestryDNA, 23andMe, or MyHeritage, the first thing you see is an ethnicity estimate — a colorful pie chart or map showing your genetic origins broken down by region. These estimates are intuitively appealing. They feel definitive. But they are statistical approximations, and understanding their limitations is critical for anyone using DNA data seriously.",[19,3607,3608,3609,3612,3613,3617],{},"Autosomal DNA is the DNA you inherit from both parents — roughly 50% from your mother and 50% from your father. Unlike ",[46,3610,3611],{"href":810},"Y-DNA"," (paternal only) or ",[46,3614,3616],{"href":3615},"/blog/mitochondrial-dna-maternal-ancestry","mtDNA"," (maternal only), autosomal DNA reflects your full ancestry. But it has a built-in limitation: recombination. Each generation, your autosomal DNA is shuffled, and segments from distant ancestors are progressively lost. Beyond about 6-7 generations (roughly 200 years), autosomal DNA cannot reliably identify individual ancestors.",[19,3619,3620],{},"The ethnicity estimate works by comparing your autosomal DNA to reference panels — collections of DNA from modern people with documented ancestry in specific regions. An algorithm calculates which combination of reference populations best explains your DNA. The result is the percentage breakdown you see on the screen.",[26,3622,3624],{"id":3623},"why-estimates-differ-between-companies","Why Estimates Differ Between Companies",[19,3626,3627],{},"If you test with AncestryDNA and 23andMe, your ethnicity estimates will differ — sometimes substantially. This is not because one company is right and the other is wrong. It is because they use different reference panels, different algorithms, and different regional categories.",[19,3629,3630,3631,3634],{},"One company might label a segment of your DNA as \"Scottish\" while another calls the same segment \"Irish\" or \"British.\" The genetic difference between these populations is genuinely small — the ",[46,3632,3633],{"href":850},"R1b-L21 populations"," of Atlantic Europe share deep common ancestry, and the boundaries between national populations are blurry in genetic terms.",[19,3636,3637],{},"Reference panels are also biased toward well-sampled populations. Regions with many testers (northwestern Europe, for example) have more refined categories than regions with fewer testers. An estimate of \"78% Northwest European\" might be all the algorithm can say if the reference panels for that region are not granular enough to distinguish between sub-populations.",[19,3639,3640],{},"Companies regularly update their algorithms and reference panels, which is why your ethnicity estimate can change without you submitting new DNA. Each update refines the model, but the refinements sometimes produce results that feel less accurate to the user — a common source of frustration.",[26,3642,3644],{"id":3643},"what-the-percentages-do-and-do-not-mean","What the Percentages Do and Do Not Mean",[19,3646,3647],{},"A result saying \"42% Irish\" does not mean that exactly 42% of your ancestors were from Ireland. It means that 42% of your autosomal DNA most closely matches the DNA of the modern reference panel labeled \"Irish.\" This is a statistical statement, not a historical one.",[19,3649,3650,3651,134,3655,3658],{},"Several factors complicate the picture. First, genetic similarity does not equal shared nationality. The Irish reference panel includes people who have been in Ireland for generations, but the DNA they carry arrived through multiple waves of migration — Mesolithic hunter-gatherers, Neolithic farmers, ",[46,3652,3654],{"href":3653},"/blog/bell-beaker-conquest-ireland-britain","Bell Beaker migrants",[46,3656,3657],{"href":2848},"Viking settlers",", Norman invaders, and English and Scottish colonists. Your \"Irish\" DNA might reflect any of these layers.",[19,3660,3661],{},"Second, small percentages (under 5%) are often noise — statistical artifacts of the algorithm rather than real ancestral contributions. A result showing 3% Finnish or 2% West African might be real, or it might be an artifact of how the algorithm handles ambiguous DNA segments. Most companies acknowledge this with confidence ranges, but users often ignore the ranges and focus on the point estimates.",[19,3663,3664],{},"Third, autosomal DNA cannot distinguish between different ancestors who came from the same region. If both your maternal and paternal lines have Irish ancestry, the test cannot separate them. It simply reports the total percentage of your DNA that matches the Irish reference panel.",[26,3666,3668],{"id":3667},"when-autosomal-dna-is-most-useful","When Autosomal DNA Is Most Useful",[19,3670,3671,3672,1124],{},"Despite its limitations for ethnicity estimation, autosomal DNA is extraordinarily powerful for two purposes: relative matching and ",[46,3673,3675],{"href":3674},"/blog/genetic-genealogy-brick-walls","breaking through genealogical brick walls",[19,3677,3678],{},"Relative matching works because close relatives share large, identifiable segments of autosomal DNA. The databases maintained by testing companies can identify your biological relatives — from close family to distant cousins — based on the amount and pattern of shared DNA. This is the most practically useful feature of autosomal testing and has reunited adoptees with biological families, confirmed or refuted family legends, and identified previously unknown relationships.",[19,3680,3681,3682,3685],{},"For genealogical research, autosomal DNA matches combined with family tree analysis can identify common ancestors and confirm documentary research. When paper trails run cold — as they often do for ",[46,3683,3684],{"href":3566},"Highland Scots"," and Irish families before civil registration — DNA matches can provide evidence that no document can.",[19,3687,3688],{},"The ethnicity estimate is the flashiest feature. The relative matching is the most useful. Understanding the difference is the key to getting real value from autosomal DNA testing.",{"title":91,"searchDepth":124,"depth":124,"links":3690},[3691,3692,3693,3694],{"id":3601,"depth":106,"text":3602},{"id":3623,"depth":106,"text":3624},{"id":3643,"depth":106,"text":3644},{"id":3667,"depth":106,"text":3668},"2025-08-15","Ethnicity estimate pie charts are the most popular DNA test result and the most misunderstood. Here is what they actually measure and where they fall short.",[3698,3699,3700],"autosomal dna ethnicity estimates","dna ethnicity accuracy","ancestry dna ethnicity",{},"/blog/autosomal-dna-ethnicity-estimates",{"title":3595,"description":3696},"blog/autosomal-dna-ethnicity-estimates",[3706,3707,971,3708],"Autosomal DNA","Ethnicity Estimates","DNA Testing","VDEgp7ufmrBgvgYQX8b5cSv6MTJm1bg0fSrlSsewH4g",[3711,3712,3713,3715,3717,3718,3719,3720,3721,3722,3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759,3760,3761,3762,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910,3911,3912,3913,3914,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4067,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4159,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4170,4171,4172,4173,4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,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],{"category":2624},{"category":759},{"category":3714},"AI",{"category":3716},"Engineering",{"category":2808},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":3714},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":1432},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":386},{"category":386},{"category":2808},{"category":2808},{"category":759},{"category":386},{"category":759},{"category":1432},{"category":386},{"category":3716},{"category":2808},{"category":3763},"DevOps",{"category":3714},{"category":759},{"category":3716},{"category":1432},{"category":3716},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":3763},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":3716},{"category":1090},{"category":3714},{"category":3714},{"category":2808},{"category":1432},{"category":2808},{"category":3716},{"category":3716},{"category":2808},{"category":3716},{"category":1432},{"category":3716},{"category":3763},{"category":3763},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":3714},{"category":1432},{"category":2808},{"category":3763},{"category":3763},{"category":3763},{"category":759},{"category":3716},{"category":3716},{"category":759},{"category":2624},{"category":3714},{"category":3763},{"category":3763},{"category":386},{"category":3763},{"category":2808},{"category":3714},{"category":759},{"category":3716},{"category":759},{"category":1432},{"category":759},{"category":1432},{"category":386},{"category":759},{"category":759},{"category":3716},{"category":2808},{"category":3716},{"category":2624},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":2808},{"category":2808},{"category":759},{"category":2624},{"category":386},{"category":1432},{"category":386},{"category":2624},{"category":3716},{"category":3716},{"category":3763},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3763},{"category":3716},{"category":3716},{"category":759},{"category":759},{"category":386},{"category":1432},{"category":1432},{"category":1090},{"category":1090},{"category":1090},{"category":2808},{"category":3716},{"category":3763},{"category":1432},{"category":759},{"category":759},{"category":3763},{"category":1432},{"category":1432},{"category":2624},{"category":3716},{"category":759},{"category":759},{"category":3716},{"category":759},{"category":3763},{"category":3763},{"category":759},{"category":386},{"category":759},{"category":1432},{"category":386},{"category":1432},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":386},{"category":3716},{"category":3763},{"category":3763},{"category":2808},{"category":3716},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":1432},{"category":1432},{"category":1432},{"category":3716},{"category":759},{"category":759},{"category":759},{"category":3763},{"category":2808},{"category":759},{"category":759},{"category":3716},{"category":759},{"category":3716},{"category":2624},{"category":759},{"category":2808},{"category":2808},{"category":3716},{"category":3716},{"category":3714},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":3716},{"category":3763},{"category":3763},{"category":3763},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":759},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":2808},{"category":2808},{"category":759},{"category":3716},{"category":2624},{"category":1432},{"category":1090},{"category":759},{"category":759},{"category":386},{"category":3716},{"category":759},{"category":759},{"category":3763},{"category":759},{"category":2624},{"category":3763},{"category":3763},{"category":386},{"category":3716},{"category":3716},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":1090},{"category":759},{"category":1432},{"category":3716},{"category":3716},{"category":759},{"category":3763},{"category":759},{"category":759},{"category":759},{"category":2624},{"category":759},{"category":759},{"category":3716},{"category":759},{"category":3716},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":3714},{"category":3714},{"category":3716},{"category":759},{"category":3763},{"category":3763},{"category":759},{"category":3716},{"category":759},{"category":759},{"category":3714},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":3716},{"category":386},{"category":3716},{"category":3716},{"category":2624},{"category":3716},{"category":2624},{"category":2624},{"category":386},{"category":1432},{"category":3716},{"category":1432},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":3716},{"category":2808},{"category":3716},{"category":3716},{"category":759},{"category":1432},{"category":3714},{"category":3714},{"category":759},{"category":759},{"category":759},{"category":759},{"category":2808},{"category":3716},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":2624},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":3716},{"category":1432},{"category":759},{"category":2808},{"category":3714},{"category":759},{"category":2808},{"category":386},{"category":759},{"category":386},{"category":3716},{"category":3763},{"category":759},{"category":759},{"category":3716},{"category":759},{"category":1432},{"category":759},{"category":759},{"category":3716},{"category":2808},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":2808},{"category":3716},{"category":3716},{"category":2808},{"category":3763},{"category":3716},{"category":3714},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":759},{"category":759},{"category":759},{"category":3714},{"category":3716},{"category":3716},{"category":1432},{"category":2624},{"category":3716},{"category":759},{"category":3716},{"category":1432},{"category":2808},{"category":2808},{"category":2624},{"category":2624},{"category":759},{"category":2808},{"category":386},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":3716},{"category":3716},{"category":1432},{"category":3716},{"category":3716},{"category":3716},{"category":4185},"Programming",{"category":3716},{"category":3716},{"category":1432},{"category":1432},{"category":3716},{"category":3716},{"category":2808},{"category":386},{"category":3716},{"category":2808},{"category":3716},{"category":3716},{"category":3716},{"category":3716},{"category":3763},{"category":1432},{"category":2808},{"category":2808},{"category":3716},{"category":3716},{"category":2808},{"category":3716},{"category":386},{"category":2808},{"category":3716},{"category":3716},{"category":1432},{"category":1432},{"category":759},{"category":2808},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":759},{"category":2624},{"category":759},{"category":3763},{"category":386},{"category":386},{"category":386},{"category":386},{"category":386},{"category":386},{"category":759},{"category":3716},{"category":3763},{"category":1432},{"category":3763},{"category":1432},{"category":3716},{"category":2624},{"category":759},{"category":1432},{"category":2624},{"category":759},{"category":759},{"category":759},{"category":1432},{"category":1432},{"category":1432},{"category":2808},{"category":2808},{"category":2808},{"category":1432},{"category":1432},{"category":2808},{"category":2808},{"category":2808},{"category":759},{"category":386},{"category":3716},{"category":3763},{"category":3716},{"category":759},{"category":2808},{"category":2808},{"category":759},{"category":759},{"category":1432},{"category":3716},{"category":1432},{"category":1432},{"category":1432},{"category":2624},{"category":3716},{"category":759},{"category":759},{"category":2808},{"category":2808},{"category":1432},{"category":3716},{"category":1090},{"category":1432},{"category":1090},{"category":2808},{"category":759},{"category":1432},{"category":759},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":759},{"category":3714},{"category":3714},{"category":3763},{"category":759},{"category":759},{"category":759},{"category":759},{"category":3716},{"category":3716},{"category":2624},{"category":3716},{"category":386},{"category":1432},{"category":2624},{"category":2624},{"category":3716},{"category":3716},{"category":2624},{"category":2624},{"category":2624},{"category":386},{"category":3716},{"category":3716},{"category":2808},{"category":3716},{"category":1432},{"category":759},{"category":759},{"category":1432},{"category":759},{"category":759},{"category":1432},{"category":759},{"category":3716},{"category":759},{"category":386},{"category":759},{"category":759},{"category":759},{"category":3763},{"category":3763},{"category":386},1772951194679]