[{"data":1,"prerenderedAt":3561},["ShallowReactive",2],{"blog-paginated-count":3,"blog-paginated-27":4,"blog-paginated-cats":2918},640,[5,239,442,728,839,976,1172,1362,1558,1687,1872,1988,2152,2273,2387],{"id":6,"title":7,"author":8,"body":11,"category":219,"date":220,"description":221,"extension":222,"featured":223,"image":224,"keywords":225,"meta":228,"navigation":229,"path":230,"readTime":231,"seo":232,"stem":233,"tags":234,"__hash__":238},"blog/blog/seo-technical-audit-guide.md","Technical SEO Audit Guide for Web Developers",{"name":9,"bio":10},"James Ross Jr.","Strategic Systems Architect & Enterprise Software Developer",{"type":12,"value":13,"toc":210},"minimark",[14,19,23,26,29,32,36,48,58,61,64,73,75,79,94,101,145,148,151,154,156,160,163,166,174,177,180,182,186,189,197,200,203,206],[15,16,18],"h2",{"id":17},"technical-seo-is-infrastructure-not-marketing","Technical SEO Is Infrastructure, Not Marketing",[20,21,22],"p",{},"Most SEO discussions focus on content strategy and keyword research. Those matter, but they are irrelevant if search engines cannot properly crawl and index your site. Technical SEO is the infrastructure layer that makes content SEO possible — it ensures that Googlebot can discover your pages, render them correctly, understand their structure, and index them efficiently.",[20,24,25],{},"Developers are better positioned to handle technical SEO than marketers because the work is fundamentally about HTTP responses, HTML structure, server configuration, and rendering architecture. A marketer can identify that a page is not ranking, but only a developer can diagnose whether the issue is a misconfigured canonical tag, a render-blocking JavaScript dependency, or a noindex directive that was accidentally left from staging.",[20,27,28],{},"A technical SEO audit is a systematic review of how search engines experience your site. It covers crawlability (can search engines find your pages?), indexability (are search engines allowed to index them?), renderability (can search engines render JavaScript-dependent content?), and structured data (do search engines understand what your content is?). Each area has specific, testable checkpoints.",[30,31],"hr",{},[15,33,35],{"id":34},"crawlability-can-search-engines-find-your-pages","Crawlability: Can Search Engines Find Your Pages?",[20,37,38,39,43,44,47],{},"The crawl audit starts with ",[40,41,42],"code",{},"robots.txt",". This file, served at your domain root, tells crawlers which paths they may and may not access. A single misplaced ",[40,45,46],{},"Disallow"," directive can block entire sections of your site from indexing. Review it line by line. Common mistakes include blocking CSS and JavaScript files (which prevents Google from rendering pages), blocking URL parameters that generate unique content, and overly broad patterns that match more paths than intended.",[20,49,50,51,54,55,57],{},"Check your XML sitemap. It should list every page you want indexed, with accurate ",[40,52,53],{},"\u003Clastmod>"," dates. Pages that return non-200 status codes should not be in the sitemap. Pages blocked by ",[40,56,42],{}," should not be in the sitemap. Submit sitemaps to Google Search Console and monitor the coverage report for discrepancies between submitted and indexed page counts.",[20,59,60],{},"Crawl your site with a tool like Screaming Frog or Sitebulb. This simulates how a search engine discovers your pages by following links. Look for orphan pages — pages that exist but have no internal links pointing to them. If Googlebot cannot reach a page by following links from your homepage, it may never discover that page. Ensure every important page is reachable within 3 clicks from the homepage through internal linking.",[20,62,63],{},"Check response codes systematically. 404 errors on pages that used to exist indicate missing redirects. 301 redirect chains (A redirects to B which redirects to C) waste crawl budget and dilute link equity. 302 redirects on permanent moves confuse search engines about which URL to index. 500 errors indicate server problems that prevent crawling entirely.",[20,65,66,67,72],{},"Internal linking structure affects crawl priority. Pages with many internal links pointing to them are crawled more frequently because the link structure signals importance. Your most important pages — service pages, key landing pages, product categories — should have the most internal links. Review your ",[68,69,71],"a",{"href":70},"/blog/nuxt-seo-optimization","navigation architecture"," to ensure that high-value pages are prominently linked.",[30,74],{},[15,76,78],{"id":77},"indexability-and-on-page-signals","Indexability and On-Page Signals",[20,80,81,82,85,86,89,90,93],{},"Once pages are crawlable, verify they are indexable. Check for ",[40,83,84],{},"noindex"," meta tags and ",[40,87,88],{},"X-Robots-Tag"," HTTP headers. These are commonly applied during development or staging and accidentally deployed to production. A single ",[40,91,92],{},"\u003Cmeta name=\"robots\" content=\"noindex\">"," tag will remove a page from search results entirely, regardless of how well-optimized everything else is.",[20,95,96,97,100],{},"Canonical tags tell search engines which URL is the authoritative version when the same content is accessible at multiple URLs. Every page should have a self-referencing canonical tag. If multiple URLs serve the same content (with and without trailing slashes, with and without ",[40,98,99],{},"www",", HTTP vs HTTPS), canonical tags should point to the preferred version, and the non-preferred versions should 301 redirect.",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-html shiki shiki-themes github-dark","\u003Clink rel=\"canonical\" href=\"https://example.com/blog/article-title\" />\n","html","",[40,109,110],{"__ignoreMap":107},[111,112,115,119,123,127,130,134,137,139,142],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"s95oV","\u003C",[111,120,122],{"class":121},"s4JwU","link",[111,124,126],{"class":125},"svObZ"," rel",[111,128,129],{"class":117},"=",[111,131,133],{"class":132},"sU2Wk","\"canonical\"",[111,135,136],{"class":125}," href",[111,138,129],{"class":117},[111,140,141],{"class":132},"\"https://example.com/blog/article-title\"",[111,143,144],{"class":117}," />\n",[20,146,147],{},"Duplicate content issues arise from URL parameters, print versions, mobile subdomains, and CMS-generated variations. Identify all URL variations through a crawl and ensure each has proper canonical tags or redirects.",[20,149,150],{},"Title tags and meta descriptions are not ranking factors in isolation, but they directly affect click-through rates from search results. Every page needs a unique, descriptive title under 60 characters and a compelling meta description under 160 characters. Check for missing, duplicate, or truncated tags across the site.",[20,152,153],{},"Structured data (JSON-LD) helps search engines understand your content type and display rich results. Validate structured data with Google's Rich Results Test. Common types include Article, FAQ, HowTo, Product, and LocalBusiness. Ensure required properties are present and values are accurate. Invalid structured data is worse than no structured data because it can prevent rich result eligibility.",[30,155],{},[15,157,159],{"id":158},"rendering-and-javascript-seo","Rendering and JavaScript SEO",[20,161,162],{},"For sites built with JavaScript frameworks — React, Vue, Angular — rendering is a critical SEO concern. Google can render JavaScript, but it does so in a two-phase process: it fetches and parses the HTML immediately, then queues JavaScript rendering for later (sometimes days later). If your content only exists after JavaScript execution, indexing is delayed and unreliable.",[20,164,165],{},"Test how Google sees your pages using the URL Inspection tool in Search Console. The \"View Tested Page\" option shows the rendered HTML that Googlebot sees. Compare this to what users see in a browser. If content is missing from the rendered view, Google is not seeing it.",[20,167,168,169,173],{},"The most reliable solution is server-side rendering (SSR) or static site generation (SSG). Frameworks like ",[68,170,172],{"href":171},"/blog/nuxt-performance-optimization","Nuxt"," render your Vue components to HTML on the server, so Googlebot receives complete content in the initial HTML response without waiting for JavaScript execution. This eliminates the JavaScript rendering dependency entirely.",[20,175,176],{},"If SSR is not feasible, pre-rendering services like Prerender.io can serve static HTML snapshots to search engine crawlers while serving the normal SPA to users. This is a workaround, not a solution — it adds infrastructure complexity and can serve stale content if the pre-rendered snapshots are not updated frequently.",[20,178,179],{},"Check for client-side-only navigation. If your site uses client-side routing (hash-based or pushState), ensure that every URL returns appropriate content when loaded directly, not just when navigated to from within the app. Google crawls individual URLs — it does not navigate through your application the way a user does.",[30,181],{},[15,183,185],{"id":184},"performance-as-an-seo-factor","Performance as an SEO Factor",[20,187,188],{},"Page speed is a confirmed Google ranking factor through the Core Web Vitals program. Pages that fail Core Web Vitals thresholds — LCP over 2.5 seconds, INP over 200ms, CLS over 0.1 — are at a ranking disadvantage compared to pages that meet them.",[20,190,191,192,196],{},"A ",[68,193,195],{"href":194},"/blog/web-app-performance-audit","performance audit"," overlaps significantly with a technical SEO audit. Slow server response times, render-blocking resources, unoptimized images, and excessive JavaScript all affect both user experience and search rankings.",[20,198,199],{},"Mobile performance matters disproportionately because Google uses mobile-first indexing. Test your pages on mobile connections and devices. A page that performs well on desktop fiber but poorly on mobile 4G has a search ranking problem regardless of its desktop speed.",[20,201,202],{},"After the audit, prioritize findings by impact. Fix indexability issues first (noindex tags, broken canonicals, blocked resources) because these prevent indexing entirely. Fix crawlability issues next (broken redirects, orphan pages, sitemap errors) because these limit discovery. Fix rendering issues third (JavaScript dependencies, missing SSR) because these affect content visibility. Address performance last because it affects ranking position rather than indexing itself.",[20,204,205],{},"Document every finding with its location, severity, and recommended fix. A technical SEO audit is not a one-time event — run it quarterly and after every significant site change to catch regressions before they impact traffic.",[207,208,209],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":107,"searchDepth":211,"depth":211,"links":212},3,[213,215,216,217,218],{"id":17,"depth":214,"text":18},2,{"id":34,"depth":214,"text":35},{"id":77,"depth":214,"text":78},{"id":158,"depth":214,"text":159},{"id":184,"depth":214,"text":185},"Engineering","2025-11-15","Technical SEO determines whether search engines can find, crawl, and index your content properly. Here's the developer-focused audit checklist that covers what matters.","md",false,null,[226,227],"technical SEO audit guide","SEO for developers",{},true,"/blog/seo-technical-audit-guide",7,{"title":7,"description":221},"blog/seo-technical-audit-guide",[235,236,237],"SEO","Web Development","Performance","Fy7QxBXnksiP4yzRp3ALHvxgyNvIAEk9OmVXC2iZrHg",{"id":240,"title":241,"author":242,"body":243,"category":427,"date":428,"description":429,"extension":222,"featured":223,"image":224,"keywords":430,"meta":434,"navigation":229,"path":435,"readTime":231,"seo":436,"stem":437,"tags":438,"__hash__":441},"blog/blog/ai-document-processing.md","Intelligent Document Processing with AI",{"name":9,"bio":10},{"type":12,"value":244,"toc":420},[245,249,252,255,258,261,263,267,270,277,283,289,292,298,304,306,310,313,324,330,336,342,345,347,351,354,360,366,377,379,388,390,394],[15,246,248],{"id":247},"the-document-problem","The Document Problem",[20,250,251],{},"Every business runs on documents. Invoices, contracts, purchase orders, insurance claims, medical records, compliance filings, shipping manifests. The critical data in these documents — amounts, dates, parties, terms, line items — needs to get into systems of record where it can be processed, reported on, and acted upon.",[20,253,254],{},"For most businesses, this transfer is manual. A person opens the document, reads the relevant fields, and types them into the appropriate system. This is slow, expensive, error-prone, and scales linearly with volume. Doubling the document volume means doubling the processing staff (or the processing time, or the backlog).",[20,256,257],{},"Traditional automation approaches — template-based extraction that matches fixed fields in fixed positions — work for standardized forms but break when documents vary. Different vendors use different invoice formats. Contracts follow different structures. Even the same form type varies across versions and organizations.",[20,259,260],{},"AI document processing handles this variation by understanding document content semantically rather than positionally. It reads the document the way a human would, identifying fields by their meaning rather than their pixel coordinates.",[30,262],{},[15,264,266],{"id":265},"the-processing-pipeline","The Processing Pipeline",[20,268,269],{},"Intelligent document processing follows a pipeline: capture, classify, extract, validate, and integrate.",[20,271,272,276],{},[273,274,275],"strong",{},"Capture"," converts the physical or digital document into processable form. Paper documents are scanned. PDFs are parsed. Email attachments are extracted. Images are cleaned and oriented. Modern OCR (optical character recognition) handles this step with high accuracy, but document quality varies — faded fax copies, skewed scans, handwritten annotations — and the capture step must handle these gracefully.",[20,278,279,282],{},[273,280,281],{},"Classification"," determines what type of document arrived. Is it an invoice, a purchase order, a contract, or a receipt? Classification routes the document to the appropriate extraction pipeline, since different document types have different fields to extract. AI classifiers handle this well because they can classify based on the document's content and structure rather than relying on metadata that may be absent or incorrect.",[20,284,285,288],{},[273,286,287],{},"Extraction"," pulls specific data fields from the classified document. This is where AI provides the most significant improvement over traditional approaches. An LLM or a specialized document AI model reads the document and extracts the requested fields: vendor name, invoice number, line items with descriptions and amounts, total, due date, payment terms.",[20,290,291],{},"The extraction works across document layouts because the model understands language and document structure semantically. \"Total Due,\" \"Amount Payable,\" \"Grand Total,\" and \"Balance\" all mean the same thing. The model recognizes this regardless of where the field appears on the page or how it is labeled.",[20,293,294,297],{},[273,295,296],{},"Validation"," checks the extracted data against business rules and internal consistency. Do the line items sum to the total? Is the due date in the future? Does the vendor name match a known vendor in the system? Is the invoice number a duplicate? Validation catches extraction errors and flags anomalies for review.",[20,299,300,303],{},[273,301,302],{},"Integration"," delivers the validated data to the target system — the ERP, the accounting software, the contract management platform. This step uses the target system's API to create or update records with the extracted data.",[30,305],{},[15,307,309],{"id":308},"handling-the-hard-cases","Handling the Hard Cases",[20,311,312],{},"The easy documents — clean, well-structured, with clear labels — process accurately on the first pass. The hard cases are where the system's design matters.",[20,314,315,318,319,323],{},[273,316,317],{},"Tables and line items."," Extracting individual values from prose is relatively straightforward for AI. Extracting tabular data — rows and columns of line items with quantities, descriptions, unit prices, and totals — is harder because the model must understand the table's structure to correctly associate values with their columns. Specialized document AI models (like those from ",[68,320,322],{"href":321},"/blog/claude-api-for-developers","Claude's vision capabilities"," or dedicated document processing APIs) are trained specifically on tabular extraction and handle this better than general-purpose LLMs.",[20,325,326,329],{},[273,327,328],{},"Multi-page documents."," A 30-page contract has relevant clauses scattered throughout. Extracting the effective date, the parties, the key terms, and the renewal conditions requires processing the entire document and understanding which sections contain which information. For long documents, a two-stage approach works well: first identify the sections that contain the target information, then extract from those specific sections.",[20,331,332,335],{},[273,333,334],{},"Handwritten content."," Handwritten annotations, signatures, and filled-in form fields remain challenging. Modern OCR handles clear handwriting reasonably well, but messy handwriting, abbreviations, and medical shorthand produce unreliable results. For documents with significant handwritten content, design the pipeline to flag handwritten sections for human review rather than attempting fully automated extraction.",[20,337,338,341],{},[273,339,340],{},"Low-confidence handling."," Not every extraction will be correct. The system must identify when it is uncertain and route those items appropriately. Confidence scores — how sure the model is about each extracted value — provide the signal. Values above a confidence threshold proceed automatically. Values below the threshold go to a human review queue where a person verifies or corrects the extraction.",[20,343,344],{},"The human review interface is a critical component. It should display the original document alongside the extracted data, highlight the specific regions where each value was found, and allow the reviewer to correct values with minimal effort. Corrections feed back into the system's training data, improving accuracy over time.",[30,346],{},[15,348,350],{"id":349},"measuring-roi","Measuring ROI",[20,352,353],{},"Document processing automation has straightforward ROI metrics.",[20,355,356,359],{},[273,357,358],{},"Processing time reduction."," Measure the average time to process a document end-to-end — from receipt to data entry in the target system — before and after automation. Reductions of 70-90% are common for well-suited document types.",[20,361,362,365],{},[273,363,364],{},"Error rate reduction."," Compare the error rate of manual data entry (typically 1-3% per field for experienced operators) with the error rate of AI extraction plus validation. AI extraction with validation typically achieves sub-1% error rates for standard document types, with the remaining errors caught in the review queue.",[20,367,368,371,372,376],{},[273,369,370],{},"Throughput scaling."," Manual processing scales linearly with staff. Automated processing scales with compute. The cost to process 10,000 documents per month versus 100,000 documents per month is marginal in compute costs but substantial in staffing costs. For growing businesses, this scaling advantage compounds. The ",[68,373,375],{"href":374},"/blog/ai-workflow-automation","workflow automation"," extends naturally from document processing into downstream processes triggered by the extracted data.",[30,378],{},[20,380,381,382],{},"If you have documents that need to be processed faster, more accurately, and at scale, ",[68,383,387],{"href":384,"rel":385},"https://calendly.com/jamesrossjr",[386],"nofollow","let's talk about building an intelligent document processing pipeline for your business.",[30,389],{},[15,391,393],{"id":392},"keep-reading","Keep Reading",[395,396,397,403,409,415],"ul",{},[398,399,400],"li",{},[68,401,402],{"href":374},"AI Workflow Automation: Where Machines Beat Manual Processes",[398,404,405],{},[68,406,408],{"href":407},"/blog/natural-language-processing-apps","NLP in Production Applications: Practical Patterns",[398,410,411],{},[68,412,414],{"href":413},"/blog/ai-for-small-business","AI for Small Business: Where It Actually Makes Sense",[398,416,417],{},[68,418,419],{"href":321},"Claude API for Developers",{"title":107,"searchDepth":211,"depth":211,"links":421},[422,423,424,425,426],{"id":247,"depth":214,"text":248},{"id":265,"depth":214,"text":266},{"id":308,"depth":214,"text":309},{"id":349,"depth":214,"text":350},{"id":392,"depth":214,"text":393},"AI","2025-11-13","Documents carry critical business data trapped in unstructured formats. AI document processing extracts, validates, and routes that data automatically.",[431,432,433],"ai document processing","intelligent document processing","automated document extraction",{},"/blog/ai-document-processing",{"title":241,"description":429},"blog/ai-document-processing",[427,439,440],"Document Processing","Automation","6udGXYXqCz1XE4iDPmXSHm5iicdh1yZzcivu0pumPCs",{"id":443,"title":444,"author":445,"body":446,"category":712,"date":713,"description":714,"extension":222,"featured":223,"image":224,"keywords":715,"meta":718,"navigation":229,"path":719,"readTime":231,"seo":720,"stem":721,"tags":722,"__hash__":727},"blog/blog/application-security-testing.md","Application Security Testing: SAST, DAST, and Beyond",{"name":9,"bio":10},{"type":12,"value":447,"toc":706},[448,452,455,458,462,465,468,471,474,482,486,489,497,500,503,506,510,513,519,525,687,691,694,697,700,703],[449,450,444],"h1",{"id":451},"application-security-testing-sast-dast-and-beyond",[20,453,454],{},"Finding security vulnerabilities before attackers do is the entire point of application security testing. But the landscape of testing tools and methodologies is confusing, with overlapping acronyms and vendor marketing that makes everything sound essential and nothing sound sufficient.",[20,456,457],{},"Here is the practical breakdown. There are distinct testing approaches, each catches different classes of vulnerabilities, and the right strategy combines them based on your risk profile and development workflow.",[15,459,461],{"id":460},"sast-finding-bugs-in-your-source-code","SAST: Finding Bugs in Your Source Code",[20,463,464],{},"Static Application Security Testing analyzes your source code without executing it. The tool reads your codebase, builds a model of data flow and control flow, and identifies patterns that match known vulnerability classes — SQL injection, cross-site scripting, path traversal, insecure deserialization, hardcoded credentials.",[20,466,467],{},"SAST tools work early in the development lifecycle. You can run them on every commit, in pull request checks, or as part of your IDE. Catching a SQL injection vulnerability in a pull request costs minutes to fix. Catching it in production costs an incident response, a breach notification, and potentially your customers' data.",[20,469,470],{},"The strength of SAST is coverage. It analyzes every code path, including paths that are difficult to reach through normal application testing. A function that is only called during a rare error condition still gets analyzed.",[20,472,473],{},"The weakness is false positives. SAST tools lack runtime context. They see that user input flows into a database query, but they may not recognize that the input passes through a parameterized query builder that prevents injection. Tuning false positives is an ongoing investment — expect to spend time configuring rules and suppressing known-safe patterns.",[20,475,476,477,481],{},"Popular SAST tools include Semgrep, SonarQube, CodeQL, and Snyk Code. For most teams, Semgrep offers the best balance of accuracy, speed, and ease of custom rule creation. If you are already managing ",[68,478,480],{"href":479},"/blog/dependency-vulnerability-management","dependency vulnerabilities",", adding SAST to the same pipeline is a natural extension.",[15,483,485],{"id":484},"dast-finding-bugs-in-your-running-application","DAST: Finding Bugs in Your Running Application",[20,487,488],{},"Dynamic Application Security Testing takes the opposite approach. Instead of reading your code, it attacks your running application like an external adversary would. DAST tools crawl your application, discover endpoints, and send malicious payloads — SQL injection strings, XSS vectors, authentication bypass attempts — then analyze the responses for signs of vulnerability.",[20,490,491,492,496],{},"DAST catches vulnerabilities that SAST cannot. Server misconfiguration, missing security headers, authentication and session management flaws, and runtime injection vulnerabilities that depend on specific server behavior are all visible to DAST but invisible to static analysis. The ",[68,493,495],{"href":494},"/blog/owasp-top-10-explained","OWASP Top 10"," includes several vulnerability classes that are most reliably detected through dynamic testing.",[20,498,499],{},"The weakness of DAST is coverage. It can only test what it can reach. If your application has endpoints that require complex authentication flows, specific state setup, or unusual input formats, DAST tools may not discover or properly test them. Authenticated scanning — where you provide the tool with valid credentials — improves coverage significantly but adds configuration complexity.",[20,501,502],{},"DAST runs later in the development lifecycle, typically against a staging environment that mirrors production. It is slower than SAST because it makes real HTTP requests and waits for responses. A full DAST scan of a moderately complex application can take hours.",[20,504,505],{},"Tools like OWASP ZAP, Burp Suite, and Nuclei cover different parts of the DAST spectrum. ZAP is open source and works well for automated pipeline scanning. Burp Suite excels in manual and semi-automated penetration testing. Nuclei specializes in known vulnerability detection using community-maintained templates.",[15,507,509],{"id":508},"beyond-sast-and-dast","Beyond SAST and DAST",[20,511,512],{},"Two additional testing approaches fill gaps that SAST and DAST leave open.",[20,514,515,518],{},[273,516,517],{},"Interactive Application Security Testing (IAST)"," instruments your application at runtime with an agent that monitors data flow during normal use or testing. When your test suite runs, the IAST agent observes how data moves through the application and identifies vulnerabilities based on actual execution paths. This combines the accuracy of dynamic testing with the coverage benefits of being embedded inside the application. The trade-off is that IAST requires installing an agent in your runtime environment, which adds a deployment dependency.",[20,520,521,524],{},[273,522,523],{},"Software Composition Analysis (SCA)"," focuses on third-party dependencies rather than your own code. It inventories every library in your dependency tree and checks it against vulnerability databases. Given that most modern applications are more dependency code than application code, SCA catches a significant class of risk that neither SAST nor DAST addresses directly.",[102,526,530],{"className":527,"code":528,"language":529,"meta":107,"style":107},"language-yaml shiki shiki-themes github-dark","# Example CI pipeline combining security testing stages\nsecurity-testing:\n stages:\n - name: sast\n tool: semgrep\n config: .semgrep.yml\n fail-on: error\n - name: sca\n tool: snyk\n fail-on: high\n - name: dast\n tool: zap\n target: https://staging.example.com\n fail-on: medium\n authenticated: true\n","yaml",[40,531,532,538,546,553,568,579,590,600,612,622,632,644,654,665,675],{"__ignoreMap":107},[111,533,534],{"class":113,"line":114},[111,535,537],{"class":536},"sAwPA","# Example CI pipeline combining security testing stages\n",[111,539,540,543],{"class":113,"line":214},[111,541,542],{"class":121},"security-testing",[111,544,545],{"class":117},":\n",[111,547,548,551],{"class":113,"line":211},[111,549,550],{"class":121}," stages",[111,552,545],{"class":117},[111,554,556,559,562,565],{"class":113,"line":555},4,[111,557,558],{"class":117}," - ",[111,560,561],{"class":121},"name",[111,563,564],{"class":117},": ",[111,566,567],{"class":132},"sast\n",[111,569,571,574,576],{"class":113,"line":570},5,[111,572,573],{"class":121}," tool",[111,575,564],{"class":117},[111,577,578],{"class":132},"semgrep\n",[111,580,582,585,587],{"class":113,"line":581},6,[111,583,584],{"class":121}," config",[111,586,564],{"class":117},[111,588,589],{"class":132},".semgrep.yml\n",[111,591,592,595,597],{"class":113,"line":231},[111,593,594],{"class":121}," fail-on",[111,596,564],{"class":117},[111,598,599],{"class":132},"error\n",[111,601,603,605,607,609],{"class":113,"line":602},8,[111,604,558],{"class":117},[111,606,561],{"class":121},[111,608,564],{"class":117},[111,610,611],{"class":132},"sca\n",[111,613,615,617,619],{"class":113,"line":614},9,[111,616,573],{"class":121},[111,618,564],{"class":117},[111,620,621],{"class":132},"snyk\n",[111,623,625,627,629],{"class":113,"line":624},10,[111,626,594],{"class":121},[111,628,564],{"class":117},[111,630,631],{"class":132},"high\n",[111,633,635,637,639,641],{"class":113,"line":634},11,[111,636,558],{"class":117},[111,638,561],{"class":121},[111,640,564],{"class":117},[111,642,643],{"class":132},"dast\n",[111,645,647,649,651],{"class":113,"line":646},12,[111,648,573],{"class":121},[111,650,564],{"class":117},[111,652,653],{"class":132},"zap\n",[111,655,657,660,662],{"class":113,"line":656},13,[111,658,659],{"class":121}," target",[111,661,564],{"class":117},[111,663,664],{"class":132},"https://staging.example.com\n",[111,666,668,670,672],{"class":113,"line":667},14,[111,669,594],{"class":121},[111,671,564],{"class":117},[111,673,674],{"class":132},"medium\n",[111,676,678,681,683],{"class":113,"line":677},15,[111,679,680],{"class":121}," authenticated",[111,682,564],{"class":117},[111,684,686],{"class":685},"sDLfK","true\n",[15,688,690],{"id":689},"building-a-practical-testing-strategy","Building a Practical Testing Strategy",[20,692,693],{},"The mistake most teams make is treating security testing as a single tool decision. \"We use SonarQube\" is not a security testing strategy. It is one input among several.",[20,695,696],{},"A practical strategy layers these approaches based on when they run and what they catch. SAST and SCA run on every pull request because they are fast and catch issues early. DAST runs nightly or on staging deployments because it is slower but catches runtime and configuration issues. IAST runs during integration test suites when you have an instrumented environment.",[20,698,699],{},"Set severity thresholds that match your risk tolerance. Not every finding needs to block a deployment. Critical and high findings from any tool should block. Medium findings should generate tickets. Low findings should be reviewed periodically. This prevents alert fatigue while ensuring that genuinely dangerous vulnerabilities never reach production.",[20,701,702],{},"Review findings regularly, not just when they block a pipeline. A weekly triage session where the development team reviews new findings, closes false positives, and prioritizes fixes keeps your security posture improving over time rather than just maintaining a minimum bar. The teams that treat security testing as a continuous improvement process — rather than a gate to get past — are the ones that actually reduce their vulnerability count over time.",[207,704,705],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":107,"searchDepth":211,"depth":211,"links":707},[708,709,710,711],{"id":460,"depth":214,"text":461},{"id":484,"depth":214,"text":485},{"id":508,"depth":214,"text":509},{"id":689,"depth":214,"text":690},"Security","2025-11-12","SAST finds bugs in your code. DAST finds bugs in your running app. Neither is sufficient alone. Here's how to build a testing strategy that actually catches vulnerabilities.",[716,717],"application security testing","SAST vs DAST",{},"/blog/application-security-testing",{"title":444,"description":714},"blog/application-security-testing",[723,724,725,726],"Security Testing","SAST","DAST","Application Security","pKT6VZ-upMSXHoF6N-PeB0KzDC-FM4Ta-OeHmpndxzs",{"id":729,"title":730,"author":731,"body":732,"category":825,"date":713,"description":826,"extension":222,"featured":223,"image":224,"keywords":827,"meta":830,"navigation":229,"path":831,"readTime":231,"seo":832,"stem":833,"tags":834,"__hash__":838},"blog/blog/developer-portfolio-strategy.md","Building a Developer Portfolio That Generates Leads",{"name":9,"bio":10},{"type":12,"value":733,"toc":819},[734,738,741,744,747,749,753,756,759,762,770,772,776,779,782,790,793,795,799,802,805,808,811],[15,735,737],{"id":736},"your-portfolio-is-a-product-not-a-resume","Your Portfolio Is a Product, Not a Resume",[20,739,740],{},"Most developer portfolios are structured like resumes: a list of skills, a timeline of experience, and links to projects. This format serves one purpose — getting past a recruiter's initial screen. It does almost nothing for attracting clients, establishing authority, or generating inbound leads.",[20,742,743],{},"If you're a freelance developer or run a small consultancy, your portfolio needs to function as a sales tool. It needs to attract the right visitors, demonstrate that you understand their problems, prove you can solve them, and make it easy for them to take the next step. That's a fundamentally different design challenge than impressing a hiring manager.",[20,745,746],{},"I rebuilt my own portfolio with this mindset, and the shift from \"showcase my work\" to \"serve my ideal client\" changed everything about how I structure content, choose which projects to feature, and write about my process.",[30,748],{},[15,750,752],{"id":751},"structuring-for-your-ideal-client","Structuring for Your Ideal Client",[20,754,755],{},"Before writing a single line of code, define who your portfolio is for. \"Anyone who needs a developer\" is not specific enough. Are you targeting startup founders who need an MVP? Enterprise teams who need a systems architect? Small businesses who need a web presence? Each audience has different concerns, different budgets, and different evaluation criteria.",[20,757,758],{},"Once you know your audience, structure every page to address their specific journey. Your homepage should answer three questions within five seconds: what do you do, who do you do it for, and what results have you achieved? Case studies should lead with the business problem, not the technology. Your services page should describe outcomes, not deliverables.",[20,760,761],{},"This is the difference between writing \"Built a full-stack application using Nuxt.js, PostgreSQL, and Stripe\" and writing \"Built a multi-tenant SaaS platform that reduced the client's operational overhead by 60% and now processes $200K in monthly transactions.\" The first describes what you did. The second describes what the client got. Clients care about the second.",[20,763,764,765,769],{},"I've written more about ",[68,766,768],{"href":767},"/blog/building-a-developer-portfolio","the foundational portfolio structure"," that works for both job seekers and client acquisition. The lead generation layer builds on that foundation.",[30,771],{},[15,773,775],{"id":774},"content-as-a-lead-generation-engine","Content as a Lead Generation Engine",[20,777,778],{},"Your portfolio's project pages will attract some traffic, but they're primarily for visitors who already know about you. To generate leads from people who don't know you yet, you need content that targets the questions your ideal clients are asking before they know they need a developer.",[20,780,781],{},"A startup founder googling \"how to build an MVP\" is a potential client. A business owner searching \"custom software vs off-the-shelf\" is a potential client. An executive researching \"how to hire a development team\" is a potential client. If your portfolio has thoughtful, expert content answering these questions, you become the developer they find during their research phase — before they've even decided to hire someone.",[20,783,784,785,789],{},"This is why a ",[68,786,788],{"href":787},"/blog/technical-blog-seo-strategy","technical blog with an SEO strategy"," matters so much for independent developers. Each article is a doorway. Visitors arrive seeking knowledge, find a credible expert, explore your work, and reach out. The conversion funnel builds itself from the content.",[20,791,792],{},"But the content has to be genuinely useful. Thin articles written purely for search ranking will attract visitors and immediately lose their trust. Write about things you actually know, share real experience, and let your expertise speak for itself. The articles that generate the most leads for me are the ones where I share specific lessons from real projects — not generic advice that could come from anyone.",[30,794],{},[15,796,798],{"id":797},"conversion-without-the-hard-sell","Conversion Without the Hard Sell",[20,800,801],{},"Developers hate being sold to, and so do most of the people who hire developers. Your portfolio should convert visitors to leads without aggressive sales tactics, pop-up modals, or countdown timers. Instead, make the next step obvious and low-friction.",[20,803,804],{},"Every case study should end with a clear call to action. Not \"Contact me for a free consultation\" plastered in a flashing banner, but a natural transition: \"If you're facing a similar challenge, I'd enjoy discussing your project.\" The tone matters. You're a professional offering expertise, not a car dealership running a weekend sale.",[20,806,807],{},"Include multiple contact paths for different comfort levels. Some visitors will fill out a contact form. Others prefer to send an email directly. Some want to schedule a call. Offer all three and let the visitor choose their preferred level of commitment.",[20,809,810],{},"Social proof is the most powerful conversion tool you have. Client testimonials, quantified results, recognizable logos — these reduce perceived risk more effectively than any sales copy. If a visitor sees that you've delivered results for someone in their industry, the mental leap from \"this person seems competent\" to \"I should talk to this person\" becomes much shorter.",[20,812,813,814,818],{},"Track what works. Set up basic analytics to understand which pages visitors view before contacting you, which blog posts drive the most traffic, and which case studies get the most engagement. This data tells you what your audience cares about, which informs both your content strategy and the types of projects you ",[68,815,817],{"href":816},"/blog/freelance-developer-vs-agency","choose to pursue",". A portfolio that generates leads is never finished — it's a product you iterate on continuously.",{"title":107,"searchDepth":211,"depth":211,"links":820},[821,822,823,824],{"id":736,"depth":214,"text":737},{"id":751,"depth":214,"text":752},{"id":774,"depth":214,"text":775},{"id":797,"depth":214,"text":798},"Career","How to transform your developer portfolio from a resume supplement into a lead generation engine. Strategy, structure, and content that attracts clients.",[828,829],"developer portfolio leads","developer portfolio strategy",{},"/blog/developer-portfolio-strategy",{"title":730,"description":826},"blog/developer-portfolio-strategy",[835,836,837],"Portfolio Strategy","Lead Generation","Freelance Development","lZb-XECZnOkHK00WoEaPPZqB8GSO6-HHX13toY-1p6I",{"id":840,"title":841,"author":842,"body":843,"category":219,"date":963,"description":964,"extension":222,"featured":223,"image":224,"keywords":965,"meta":968,"navigation":229,"path":969,"readTime":231,"seo":970,"stem":971,"tags":972,"__hash__":975},"blog/blog/real-time-features-mobile-apps.md","Building Real-Time Features in Mobile Applications",{"name":9,"bio":10},{"type":12,"value":844,"toc":957},[845,848,851,855,858,864,867,873,879,882,886,889,892,895,898,901,905,908,911,914,922,925,929,932,935,938,941,949],[20,846,847],{},"Real-time features — live chat, presence indicators, collaborative editing, live location tracking, instant notifications — make apps feel alive. Users expect messages to appear instantly, driver locations to update smoothly, and collaborative changes to reflect without refreshing.",[20,849,850],{},"Building real-time features on mobile is more nuanced than on web. Mobile connections drop, apps get backgrounded, and battery constraints limit how aggressively you can maintain connections. Here is how to build real-time features that work reliably in production.",[15,852,854],{"id":853},"choosing-your-transport","Choosing Your Transport",[20,856,857],{},"The three main options for real-time communication are WebSockets, Server-Sent Events (SSE), and polling. Each fits different use cases.",[20,859,860,863],{},[273,861,862],{},"WebSockets"," provide a persistent, bidirectional connection between client and server. Both sides can send messages at any time without the overhead of HTTP request/response cycles. WebSockets are the right choice for chat, collaborative features, and any scenario where both the client and server initiate communication.",[20,865,866],{},"The mobile-specific challenge with WebSockets is connection management. When the app goes to the background, the OS may suspend the socket connection. When the app returns to the foreground, you need to reconnect and synchronize any missed messages. This reconnection logic is where most real-time implementations get complicated.",[20,868,869,872],{},[273,870,871],{},"Server-Sent Events"," provide a one-directional stream from server to client over HTTP. They are simpler than WebSockets and work well when the server pushes updates and the client only needs to listen — live scores, stock tickers, activity feeds. SSE has built-in reconnection logic, which simplifies the mobile lifecycle handling.",[20,874,875,878],{},[273,876,877],{},"Polling"," sends regular HTTP requests to check for updates. It is the simplest to implement and the least efficient. Short polling (every few seconds) wastes bandwidth and battery. Long polling (holding a connection open until data is available) is more efficient but still has overhead. Use polling only when your update frequency is low (every 30+ seconds) and WebSocket infrastructure is not justified.",[20,880,881],{},"For most real-time features I build, WebSockets are the right choice. The bidirectional communication and low latency justify the additional complexity.",[15,883,885],{"id":884},"connection-lifecycle-on-mobile","Connection Lifecycle on Mobile",[20,887,888],{},"The critical difference between real-time on web and mobile is the app lifecycle. Web apps run in a browser tab that stays active. Mobile apps get backgrounded, suspended, killed, and resumed constantly. Your real-time connection must handle all of these transitions.",[20,890,891],{},"When the app moves to the background, decide whether to maintain or close the WebSocket connection. For chat apps, maintaining the connection briefly (30-60 seconds) lets you receive messages for notification display. For non-essential updates, close the connection immediately to conserve battery.",[20,893,894],{},"On iOS, background execution is strictly limited. You get approximately 30 seconds of background time, after which the OS suspends your process. Use this window to close connections cleanly and persist any pending state. For incoming messages while the app is backgrounded, rely on push notifications through APNs rather than maintaining a WebSocket connection.",[20,896,897],{},"On Android, background restrictions are more complex and vary by manufacturer. Samsung's aggressive battery management, for example, can kill background connections faster than stock Android. Use foreground services for features that genuinely need continuous connections (active delivery tracking, ongoing voice calls), but respect the platform's intent — most features should not run in the background.",[20,899,900],{},"When the app returns to the foreground, reconnect the WebSocket and synchronize state. This means requesting any messages or updates that occurred while disconnected. The server needs to support this — a sync endpoint that returns events after a given timestamp, or a message history API that the client calls on reconnection.",[15,902,904],{"id":903},"building-reliable-message-delivery","Building Reliable Message Delivery",[20,906,907],{},"Real-time does not mean \"fire and forget.\" Users expect messages to be delivered, and they expect to know when delivery fails. Building reliable delivery on top of an unreliable network requires deliberate design.",[20,909,910],{},"Assign a unique ID to every message on the client before sending. When the server receives and processes the message, it sends an acknowledgment with the same ID. If the client does not receive an acknowledgment within a timeout, it retries. The server deduplicates by message ID, so retries are safe.",[20,912,913],{},"Maintain a local message queue on the device. When the user sends a message, it goes into the local queue with a status of \"sending.\" When the server acknowledges receipt, the status changes to \"sent.\" When the recipient reads it, the status changes to \"read.\" The UI reflects these states — users see their message immediately with a subtle \"sending\" indicator that resolves to a checkmark on delivery.",[20,915,916,917,921],{},"For offline scenarios, messages queue locally and send when the connection is re-established. This overlaps with ",[68,918,920],{"href":919},"/blog/offline-first-mobile-apps","offline-first architecture"," — the sync queue for real-time messages follows the same patterns as offline data sync.",[20,923,924],{},"Handle message ordering carefully. Network latency means messages can arrive at the server in a different order than they were sent. Use client-side timestamps for display ordering and server-side timestamps for canonical ordering. When messages from multiple users arrive, sort by server timestamp to ensure all clients see the same order.",[15,926,928],{"id":927},"scaling-considerations","Scaling Considerations",[20,930,931],{},"Real-time connections are stateful, which makes scaling different from stateless HTTP APIs. Each connected client holds a WebSocket connection on a specific server instance, and the server must route messages to clients that may be connected to different instances.",[20,933,934],{},"Use a pub/sub system — Redis Pub/Sub, NATS, or a managed service like Ably or Pusher — to distribute messages across server instances. When a message arrives at one server, it publishes to the channel, and all server instances with subscribed clients receive and deliver it.",[20,936,937],{},"For room-based features (chat rooms, document collaboration), assign each room to a pub/sub channel. Clients subscribe to the channels they need. This keeps the message routing efficient — a message in room A is only delivered to clients in room A, not broadcast to every connected client.",[20,939,940],{},"Monitor your WebSocket connection counts, message throughput, and reconnection rates. A spike in reconnections often indicates network issues or server instability. Track message delivery latency (time from send to delivery) as an SLI — if p95 delivery time exceeds your threshold, investigate.",[20,942,943,944,948],{},"The real-time layer is often the most complex part of a mobile app's ",[68,945,947],{"href":946},"/blog/building-rest-apis-typescript","backend architecture",". Build it as a separate service from your REST API so you can scale it independently. Real-time connections have different resource profiles than HTTP requests, and separating them lets you optimize each independently.",[20,950,951,952,956],{},"Consider managed real-time services for your first implementation. Services like Ably, Pusher, or Firebase Realtime Database handle the connection management, scaling, and reliability concerns so you can focus on your ",[68,953,955],{"href":954},"/blog/mobile-app-development-guide","application features",". Move to a self-hosted solution when the managed service costs exceed the engineering cost of running your own infrastructure.",{"title":107,"searchDepth":211,"depth":211,"links":958},[959,960,961,962],{"id":853,"depth":214,"text":854},{"id":884,"depth":214,"text":885},{"id":903,"depth":214,"text":904},{"id":927,"depth":214,"text":928},"2025-11-10","How to build real-time features in mobile apps — WebSockets, server-sent events, presence indicators, live updates, and managing connection lifecycle on mobile.",[966,967],"real-time mobile features","WebSocket mobile apps",{},"/blog/real-time-features-mobile-apps",{"title":841,"description":964},"blog/real-time-features-mobile-apps",[973,862,974],"Real-Time","Mobile Development","nwNYcgCtwcl2Muze4yNzNk2tBflprJEc_TecaHtzaHI",{"id":977,"title":978,"author":979,"body":980,"category":1152,"date":1153,"description":1154,"extension":222,"featured":223,"image":224,"keywords":1155,"meta":1161,"navigation":229,"path":1162,"readTime":231,"seo":1163,"stem":1164,"tags":1165,"__hash__":1171},"blog/blog/rosetta-stone-decipherment.md","The Rosetta Stone: How We Cracked Egyptian Hieroglyphs",{"name":9,"bio":10},{"type":12,"value":981,"toc":1143},[982,986,989,992,995,999,1002,1005,1008,1011,1015,1018,1021,1024,1028,1031,1043,1058,1061,1068,1072,1075,1089,1097,1101,1109,1112,1115,1118,1120,1124],[15,983,985],{"id":984},"a-thousand-years-of-silence","A Thousand Years of Silence",[20,987,988],{},"By the fifth century AD, the ability to read Egyptian hieroglyphs had been lost. The last known hieroglyphic inscription dates to 394 AD, carved at the Temple of Isis on the island of Philae. After that, the tradition died. The priests who had maintained the knowledge through centuries of Greek and Roman rule were gone, their temples closed, their schools disbanded.",[20,990,991],{},"For the next fourteen centuries, the hieroglyphs remained visible but unreadable. They covered the temples and tombs of Egypt in profusion -- thousands of inscriptions, millions of signs -- and no one alive could read a single word. European scholars assumed the signs were symbolic or mystical, representing ideas rather than sounds. The assumption was wrong, and it blocked progress for generations.",[20,993,994],{},"The breakthrough came, as breakthroughs often do, from an accident of war.",[15,996,998],{"id":997},"the-stone","The Stone",[20,1000,1001],{},"In July 1799, French soldiers under Napoleon's command were reinforcing the fortifications at the town of Rashid (Rosetta) in the Nile Delta when they uncovered a slab of dark granodiorite bearing three blocks of text. The top section was in hieroglyphs. The middle section was in a cursive Egyptian script called Demotic. The bottom section was in Greek.",[20,1003,1004],{},"The significance was immediately recognized. If the three texts said the same thing -- and the Greek text could be read -- then the stone was a bilingual key that might unlock the hieroglyphs.",[20,1006,1007],{},"The Greek section was translated quickly. It was a decree issued in 196 BC by a council of priests at Memphis, honoring the Pharaoh Ptolemy V Epiphanes. The decree listed the king's benefactions to the temples and prescribed that it be set up in temples across Egypt in three scripts: \"sacred letters\" (hieroglyphs), \"native letters\" (Demotic), and \"Greek letters.\"",[20,1009,1010],{},"The race to use the Greek to crack the hieroglyphs began immediately.",[15,1012,1014],{"id":1013},"thomas-young-and-the-first-steps","Thomas Young and the First Steps",[20,1016,1017],{},"Thomas Young, the English polymath who also contributed to the wave theory of light, made the first significant progress. Working in the 1810s, Young focused on the Demotic text and made two critical observations.",[20,1019,1020],{},"First, he noticed that some Demotic signs resembled simplified versions of hieroglyphic signs, suggesting a developmental relationship between the scripts. Second, he identified a group of hieroglyphic signs enclosed in oval rings -- now called cartouches -- and correctly guessed that these spelled the name of Ptolemy. By matching the cartouche signs to the known Greek spelling, he assigned phonetic values to several hieroglyphic signs.",[20,1022,1023],{},"Young proved that hieroglyphs could represent sounds, not just ideas. But he did not go further. He believed that phonetic spelling was used only for foreign names -- that native Egyptian words were written ideographically. This was wrong, and the error prevented him from achieving the full decipherment.",[15,1025,1027],{"id":1026},"champollion-and-the-breakthrough","Champollion and the Breakthrough",[20,1029,1030],{},"Jean-Francois Champollion, a French linguist who had been obsessed with Egypt since childhood, succeeded where Young had stopped. Champollion had one crucial advantage: he knew Coptic, the last stage of the Egyptian language, which survived as the liturgical language of the Egyptian Christian church. Coptic was written in a modified Greek alphabet, so its pronunciation was known. If the hieroglyphs recorded the same language that Coptic preserved, Champollion could use Coptic to check his readings.",[20,1032,1033,1034,1038,1039,1042],{},"In September 1822, Champollion received copies of inscriptions from the temple of Abu Simbel, including cartouches that did not spell foreign names. One cartouche contained four signs. The first was a disc -- which Champollion knew from the Rosetta Stone represented the sun, pronounced ",[1035,1036,1037],"em",{},"ra"," in Coptic. The last two signs were identical -- he had already assigned them the value ",[1035,1040,1041],{},"s"," from the Ptolemy cartouche. The middle sign was unknown.",[20,1044,1045,1046,1049,1050,1053,1054,1057],{},"The cartouche read: Ra-?-s-s. Champollion knew from Coptic that the Egyptian word for \"born\" was ",[1035,1047,1048],{},"mes"," or ",[1035,1051,1052],{},"mis",". If the unknown sign was ",[1035,1055,1056],{},"m",", the name was Ra-m-s-s -- Ramesses.",[20,1059,1060],{},"He had it. The hieroglyphs were not purely symbolic. They recorded the Egyptian language phonetically, using a mixture of phonetic signs, logograms, and determinatives. The system was complex but regular, and Champollion's knowledge of Coptic gave him the pronunciation key that turned silent symbols into spoken words.",[20,1062,1063,1064,1067],{},"Champollion announced his discovery in his famous ",[1035,1065,1066],{},"Lettre a M. Dacier"," on September 27, 1822. He spent the next decade refining and extending the decipherment before his death in 1832 at the age of 41.",[15,1069,1071],{"id":1070},"what-the-decipherment-revealed","What the Decipherment Revealed",[20,1073,1074],{},"The decipherment of hieroglyphs opened three thousand years of Egyptian history to direct reading. Before Champollion, knowledge of ancient Egypt came from Greek and Roman authors -- Herodotus, Diodorus, Manetho. After Champollion, the Egyptians could speak for themselves.",[20,1076,1077,1078,1081,1082,1081,1085,1088],{},"The inscriptions revealed a civilization of extraordinary depth: religious texts, administrative records, literary works, medical treatises, love poetry, legal documents, diplomatic correspondence. The ",[1035,1079,1080],{},"Book of the Dead",", the ",[1035,1083,1084],{},"Instruction of Ptahhotep",[1035,1086,1087],{},"Tale of Sinuhe",", the Amarna letters -- all became readable.",[20,1090,1091,1092,1096],{},"For ",[68,1093,1095],{"href":1094},"/blog/what-is-genetic-genealogy","genealogy"," and historical linguistics, the decipherment provided an anchor point. Egyptian is an Afroasiatic language, related to Semitic languages like Arabic and Hebrew. The hieroglyphic records, stretching back to roughly 3200 BC, provide one of the longest continuous language records in human history, invaluable for understanding language change over time.",[15,1098,1100],{"id":1099},"the-lesson-of-the-rosetta-stone","The Lesson of the Rosetta Stone",[20,1102,1103,1104,1108],{},"The Rosetta Stone's lesson extends beyond Egyptology. It demonstrates that ",[68,1105,1107],{"href":1106},"/blog/linear-a-undeciphered-scripts","undeciphered scripts"," can be cracked when three conditions are met: a bilingual or multilingual text, a sufficient corpus of inscriptions, and knowledge of a related language.",[20,1110,1111],{},"Linear B was deciphered in 1952 because Michael Ventris recognized it as Greek. The Rosetta Stone worked because Champollion knew Coptic. In both cases, the key was not a code-breaking trick but a linguistic connection -- a bridge between the unknown script and a known language.",[20,1113,1114],{},"The scripts that remain undeciphered -- Linear A, the Indus script, Proto-Elamite -- are locked because those bridges have not been found. The languages they record are unknown, and no bilingual key has surfaced.",[20,1116,1117],{},"The Rosetta Stone sits today in the British Museum, Room 4, behind glass. It is the most visited object in the museum. Millions of people look at it every year, most of them unable to read a single sign on its surface. But because of what it made possible, we can read the words of a civilization that fell silent two thousand years ago and hear them speak again.",[30,1119],{},[15,1121,1123],{"id":1122},"related-articles","Related Articles",[395,1125,1126,1131,1137],{},[398,1127,1128],{},[68,1129,1130],{"href":1106},"Undeciphered Scripts: The Languages We Still Can't Read",[398,1132,1133],{},[68,1134,1136],{"href":1135},"/blog/oral-tradition-memory","Oral Tradition: How Cultures Preserved History Without Writing",[398,1138,1139],{},[68,1140,1142],{"href":1141},"/blog/language-families-world","Language Families of the World: How Tongues Diverge",{"title":107,"searchDepth":211,"depth":211,"links":1144},[1145,1146,1147,1148,1149,1150,1151],{"id":984,"depth":214,"text":985},{"id":997,"depth":214,"text":998},{"id":1013,"depth":214,"text":1014},{"id":1026,"depth":214,"text":1027},{"id":1070,"depth":214,"text":1071},{"id":1099,"depth":214,"text":1100},{"id":1122,"depth":214,"text":1123},"Heritage","2025-11-09","For over a thousand years, Egyptian hieroglyphs were unreadable -- beautiful, mysterious, and silent. Then a broken slab of granodiorite turned up in the Nile Delta and changed everything. Here is how the decipherment actually worked.",[1156,1157,1158,1159,1160],"rosetta stone decipherment","egyptian hieroglyphs decoded","jean-francois champollion","ancient egyptian language","how hieroglyphs were deciphered",{},"/blog/rosetta-stone-decipherment",{"title":978,"description":1154},"blog/rosetta-stone-decipherment",[1166,1167,1168,1169,1170],"Rosetta Stone","Egyptian Hieroglyphs","Decipherment","Historical Linguistics","Ancient Egypt","jWep-AK-fcy1AkigYs99QkbY0SNT5hLmnfBBAvhnPvA",{"id":1173,"title":1174,"author":1175,"body":1176,"category":1152,"date":1342,"description":1343,"extension":222,"featured":223,"image":224,"keywords":1344,"meta":1351,"navigation":229,"path":1352,"readTime":602,"seo":1353,"stem":1354,"tags":1355,"__hash__":1361},"blog/blog/ancient-dna-extraction-methods.md","How Scientists Extract DNA from Ancient Bones",{"name":9,"bio":10},{"type":12,"value":1177,"toc":1333},[1178,1182,1185,1188,1196,1200,1207,1210,1213,1216,1220,1223,1226,1246,1249,1253,1256,1259,1266,1270,1273,1279,1282,1290,1294,1307,1310,1312,1314],[15,1179,1181],{"id":1180},"the-problem-dna-was-not-meant-to-last","The Problem: DNA Was Not Meant to Last",[20,1183,1184],{},"DNA is a fragile molecule. In living cells, it is continuously maintained by repair enzymes that fix damage as it occurs. The moment an organism dies, that maintenance stops. Water, oxygen, bacteria, and ultraviolet light begin breaking the long DNA strands into shorter and shorter fragments. Chemical modifications alter the base pairs — cytosine degrades into uracil, creating \"damage patterns\" that are characteristic of ancient DNA but that can also mimic real mutations if not accounted for.",[20,1186,1187],{},"Within a few decades, most DNA in a dead organism has degraded significantly. Within a few centuries, the longest surviving fragments are typically under 200 base pairs — far shorter than the thousands-of-base-pair sequences that modern DNA tests routinely read. Within a few thousand years, the DNA that remains is heavily fragmented, chemically damaged, and overwhelmingly contaminated by bacterial DNA from the soil environment.",[20,1189,1190,1191,1195],{},"And yet scientists have successfully sequenced DNA from remains that are over 400,000 years old. The ",[68,1192,1194],{"href":1193},"/blog/ancient-dna-revolution","ancient DNA revolution"," that has transformed our understanding of human prehistory rests on laboratory methods that can find, extract, and read these vanishingly small fragments of surviving human DNA.",[15,1197,1199],{"id":1198},"step-one-choosing-the-right-bone","Step One: Choosing the Right Bone",[20,1201,1202,1203,1206],{},"Not all bones preserve DNA equally. The single most important methodological advance in ancient DNA research was the discovery that the ",[273,1204,1205],{},"petrous bone"," — the densest bone in the human body, located in the inner ear — preserves DNA at concentrations 10 to 100 times higher than other skeletal elements.",[20,1208,1209],{},"The petrous bone's density is the key. Its tightly packed mineral matrix physically shields DNA molecules from water infiltration and microbial colonization. A petrous bone from a 5,000-year-old skeleton may yield enough human DNA for whole-genome sequencing, while a femur from the same skeleton yields almost nothing usable.",[20,1211,1212],{},"This discovery, published by Ron Pinhasi and colleagues in 2015, was transformative. It meant that remains previously considered too degraded for genetic analysis could suddenly yield results — if the petrous bone was intact. It also meant that museums and archaeological collections had to reconsider which skeletal elements to prioritize for preservation and sampling.",[20,1214,1215],{},"Teeth — particularly the roots of molars — are the second-best source. Like petrous bones, tooth roots are dense and relatively resistant to environmental degradation. When petrous bones are unavailable or too damaged, teeth are the fallback.",[15,1217,1219],{"id":1218},"step-two-the-clean-room","Step Two: The Clean Room",[20,1221,1222],{},"Ancient DNA extraction is performed in dedicated clean room facilities that are physically separated from any laboratory that handles modern DNA. The reason is contamination. A single skin cell from a lab technician contains more intact human DNA than an entire ancient bone sample. If modern DNA contaminates the sample at any point during extraction, it can overwhelm the ancient signal entirely.",[20,1224,1225],{},"Clean room protocols include:",[395,1227,1228,1231,1234,1237,1240,1243],{},[398,1229,1230],{},"Positive air pressure to prevent external particles from entering",[398,1232,1233],{},"UV irradiation of all surfaces and equipment before each session",[398,1235,1236],{},"Full-body suits, double gloves, face shields, and hair covers for all personnel",[398,1238,1239],{},"Bleach treatment of all tools and work surfaces",[398,1241,1242],{},"Dedicated reagents that have never been exposed to modern DNA",[398,1244,1245],{},"Physical separation from post-amplification laboratories (where PCR products are handled)",[20,1247,1248],{},"These protocols are non-negotiable. Some of the most embarrassing episodes in ancient DNA history — including early claims of dinosaur DNA that turned out to be modern contamination — resulted from inadequate clean room discipline. Modern labs treat contamination prevention with the same rigor that semiconductor fabrication facilities treat particle control.",[15,1250,1252],{"id":1251},"step-three-extraction-and-library-preparation","Step Three: Extraction and Library Preparation",[20,1254,1255],{},"The actual extraction process begins with drilling or cutting a small sample from the petrous bone or tooth root — typically 50 to 200 milligrams of bone powder. This powder is digested in a chemical solution that dissolves the mineral matrix and releases the trapped DNA molecules.",[20,1257,1258],{},"The released DNA is then purified — separated from proteins, lipids, and other cellular debris — using silica-based binding columns or magnetic bead protocols. What remains is a solution containing ancient DNA fragments, typically 30 to 150 base pairs long, mixed with a much larger quantity of bacterial and environmental DNA.",[20,1260,1261,1262,1265],{},"The next step is ",[273,1263,1264],{},"library preparation",": converting these short, damaged DNA fragments into a form that can be read by a DNA sequencer. Adaptor sequences are ligated (chemically attached) to both ends of each fragment, creating a \"library\" of fragments that the sequencing machine can recognize and process. During this step, researchers can also treat the DNA with enzymes that remove the damaged bases (particularly uracil) that are characteristic of ancient DNA degradation — reducing the false mutation signal that ancient damage creates.",[15,1267,1269],{"id":1268},"step-four-capture-and-sequencing","Step Four: Capture and Sequencing",[20,1271,1272],{},"A typical ancient DNA extract contains less than 1% human DNA. The rest is bacterial. Sequencing the entire extract would be enormously wasteful — 99% of the sequencing effort would be spent reading bacterial genomes.",[20,1274,1275,1278],{},[273,1276,1277],{},"Targeted capture"," solves this problem. Researchers design synthetic DNA probes — short sequences that are complementary to known regions of the human genome. These probes are mixed with the ancient DNA library, and they bind (hybridize) to any human DNA fragments in the solution. The bound fragments are then physically pulled out of the mixture (using magnetic beads attached to the probes), while the bacterial DNA is washed away.",[20,1280,1281],{},"The captured human DNA fragments are then amplified using PCR (polymerase chain reaction) to create enough copies for sequencing. Modern sequencing platforms — primarily Illumina short-read sequencers — then read millions of these fragments simultaneously, generating raw sequence data that is aligned against the human reference genome.",[20,1283,1284,1285,1289],{},"The result is a genome — sometimes complete, sometimes partial — from a person who died centuries or millennia ago. That genome can be analyzed for ",[68,1286,1288],{"href":1287},"/blog/y-dna-haplogroups-explained","haplogroup assignments",", population ancestry, physical trait predictions (eye color, hair color, skin pigmentation), and relatedness to modern populations and other ancient individuals.",[15,1291,1293],{"id":1292},"what-ancient-dna-has-already-revealed","What Ancient DNA Has Already Revealed",[20,1295,1296,1297,1301,1302,1306],{},"The methods described above have produced results that overturned decades of archaeological assumption. Ancient DNA from Bronze Age Ireland showed that the ",[68,1298,1300],{"href":1299},"/blog/r1b-l21-atlantic-celtic-haplogroup","male lineage of the island was almost entirely replaced"," by incoming Bell Beaker migrants — a finding that no amount of pottery analysis could have revealed. Ancient DNA from Mesolithic European hunter-gatherers showed that they had dark skin and blue eyes — contradicting earlier assumptions about European pigmentation history. Ancient DNA from Neolithic farmers showed that ",[68,1303,1305],{"href":1304},"/blog/neolithic-farming-revolution","the agricultural revolution"," was a migration event, not just a cultural transmission — the farmers moved, bringing their genes and their crops with them.",[20,1308,1309],{},"Every one of these findings started with a fragment of bone, a clean room, and a protocol for reading molecules that were never meant to survive.",[30,1311],{},[15,1313,1123],{"id":1122},[395,1315,1316,1321,1327],{},[398,1317,1318],{},[68,1319,1320],{"href":1193},"The Ancient DNA Revolution: Rewriting Human History",[398,1322,1323],{},[68,1324,1326],{"href":1325},"/blog/archaeogenetics-future","Archaeogenetics: Where Archaeology Meets DNA",[398,1328,1329],{},[68,1330,1332],{"href":1331},"/blog/radiocarbon-dating-explained","Radiocarbon Dating: How We Know How Old Things Are",{"title":107,"searchDepth":211,"depth":211,"links":1334},[1335,1336,1337,1338,1339,1340,1341],{"id":1180,"depth":214,"text":1181},{"id":1198,"depth":214,"text":1199},{"id":1218,"depth":214,"text":1219},{"id":1251,"depth":214,"text":1252},{"id":1268,"depth":214,"text":1269},{"id":1292,"depth":214,"text":1293},{"id":1122,"depth":214,"text":1123},"2025-11-08","Extracting usable DNA from remains that are thousands of years old requires extraordinary precision. Here's how ancient DNA labs do it — from drilling into petrous bones to building sequencing libraries from fragments shorter than a tweet.",[1345,1346,1347,1348,1349,1350],"ancient dna extraction","how ancient dna is extracted","petrous bone dna","adna laboratory methods","paleogenomics techniques","dna from old bones",{},"/blog/ancient-dna-extraction-methods",{"title":1174,"description":1343},"blog/ancient-dna-extraction-methods",[1356,1357,1358,1359,1360],"Ancient DNA","Archaeogenetics","DNA Extraction","Laboratory Methods","Paleogenomics","nDdhvL_-CwDeye0cjRPBGbr75ugPQ0WcKNFmNCvtZu8",{"id":1363,"title":1364,"author":1365,"body":1366,"category":219,"date":1342,"description":1543,"extension":222,"featured":223,"image":224,"keywords":1544,"meta":1548,"navigation":229,"path":1549,"readTime":602,"seo":1550,"stem":1551,"tags":1552,"__hash__":1557},"blog/blog/custom-scheduling-system.md","Custom Scheduling Systems: Calendar, Bookings, and Dispatch",{"name":9,"bio":10},{"type":12,"value":1367,"toc":1535},[1368,1372,1375,1378,1381,1383,1387,1390,1396,1402,1408,1411,1414,1416,1420,1423,1426,1432,1438,1444,1446,1450,1453,1456,1459,1467,1473,1476,1478,1482,1485,1488,1491,1499,1506,1508,1510],[15,1369,1371],{"id":1370},"scheduling-is-a-harder-problem-than-it-looks","Scheduling Is a Harder Problem Than It Looks",[20,1373,1374],{},"Everyone has used a calendar app. Everyone has booked an appointment online. The familiarity breeds a dangerous assumption: scheduling software must be straightforward to build.",[20,1376,1377],{},"It is not. Scheduling systems deal with time — and time is one of the most deceptively complex domains in software engineering. Time zones, daylight saving transitions, recurring events, conflict detection, resource constraints, cancellation policies, buffer times, multi-party coordination. Each of these is individually manageable. Combined, they produce a system with a surprising number of edge cases.",[20,1379,1380],{},"I've built scheduling systems for service businesses, healthcare providers, and field service operations. The lessons are consistent across domains: the data model matters enormously, time zone handling must be correct from day one, and the conflict detection algorithm is the heart of the system.",[30,1382],{},[15,1384,1386],{"id":1385},"the-data-model-that-handles-reality","The Data Model That Handles Reality",[20,1388,1389],{},"A scheduling system has three core entities: resources, time slots, and bookings.",[20,1391,1392,1395],{},[273,1393,1394],{},"Resources"," are the things being scheduled. A technician. A conference room. A piece of equipment. A resource has availability — the times when it can be booked — and constraints — the types of bookings it can accept, the maximum concurrent bookings, the buffer time between bookings.",[20,1397,1398,1401],{},[273,1399,1400],{},"Time slots"," represent available windows. For simple systems, slots are pre-defined: 9:00-9:30, 9:30-10:00. For flexible systems, availability is defined as ranges and the system calculates available slots based on existing bookings and constraints.",[20,1403,1404,1407],{},[273,1405,1406],{},"Bookings"," are commitments of a resource to a time window for a purpose. A booking has a status lifecycle: requested, confirmed, in-progress, completed, cancelled, no-show. Each status transition has business implications — a cancellation within 24 hours might incur a fee, a no-show might trigger a follow-up workflow.",[20,1409,1410],{},"The relationship between these entities determines your system's flexibility. A one-to-one model (one resource per booking) is simple but doesn't handle scenarios where a booking requires multiple resources — a medical appointment needs both a doctor and an exam room. A many-to-many model (a booking can reserve multiple resources, a resource can participate in multiple concurrent bookings) handles more scenarios but makes conflict detection more complex.",[20,1412,1413],{},"For dispatch-oriented systems — field service, delivery, mobile technicians — the model adds a geographic dimension. Resources have locations or service areas. Bookings have service addresses. Route optimization and travel time between appointments become part of the scheduling logic. This is where scheduling systems start to overlap with operations research.",[30,1415],{},[15,1417,1419],{"id":1418},"time-zones-get-this-right-or-get-nothing-right","Time Zones: Get This Right or Get Nothing Right",[20,1421,1422],{},"Every scheduling system that serves users across time zones must store times in UTC and convert to local time for display. This is non-negotiable. Storing times in local time creates ambiguity that will corrupt your data.",[20,1424,1425],{},"But \"store in UTC, display in local\" is only the beginning. The real complexity comes from a few specific scenarios.",[20,1427,1428,1431],{},[273,1429,1430],{},"Recurring events across DST transitions."," A weekly meeting at 2:00 PM Eastern happens at a different UTC offset in January (EST, UTC-5) than in July (EDT, UTC-4). If you store the recurrence as \"every Monday at 19:00 UTC,\" the meeting shifts to 3:00 PM local time when clocks spring forward. The correct approach is to store recurring events in the resource's local time zone and compute the UTC equivalent for each occurrence.",[20,1433,1434,1437],{},[273,1435,1436],{},"Bookings across time zone boundaries."," A customer in Pacific time books a technician in Eastern time. What time zone does the appointment display in? The answer depends on context: the customer sees it in their time zone, the technician sees it in theirs, and the system stores it in UTC. Your API must accept a time zone parameter for display purposes while storing the canonical time in UTC.",[20,1439,1440,1443],{},[273,1441,1442],{},"Business hours in local time."," A business that's open 9-5 Eastern is open 9-5 Eastern year-round, regardless of DST. Business hours should be stored as local time with a time zone identifier, not as UTC offsets. Use the IANA time zone database (America/New_York, not EST or UTC-5) to handle DST transitions correctly.",[30,1445],{},[15,1447,1449],{"id":1448},"conflict-detection-and-availability-calculation","Conflict Detection and Availability Calculation",[20,1451,1452],{},"The core algorithm of any scheduling system answers one question: given a resource and a proposed time window, is the resource available?",[20,1454,1455],{},"For simple cases, this is a database query: find any existing bookings for this resource that overlap with the proposed window. Two time ranges overlap if the start of one is before the end of the other and vice versa. Include buffer times in the overlap calculation — if a resource needs 15 minutes between bookings, extend each existing booking by 15 minutes when checking for conflicts.",[20,1457,1458],{},"For systems with recurring bookings, conflict detection gets more expensive. You need to generate the occurrences of each recurring booking within the query window and check each one for overlap. Materializing recurring occurrences into a separate table (pre-computing the next N occurrences) trades storage for query performance and is usually worth it.",[20,1460,1461,1462,1466],{},"For multi-resource bookings, conflict detection must check all required resources. A booking that needs a doctor and an exam room can only be scheduled when both are available simultaneously. This is a constraint satisfaction problem, and for systems with many resources and constraints, it can benefit from the optimization techniques used in ",[68,1463,1465],{"href":1464},"/blog/enterprise-integration-patterns","enterprise integration patterns",".",[20,1468,1469,1472],{},[273,1470,1471],{},"Availability calculation"," is the inverse of conflict detection: given a resource and a date range, what time slots are available? This involves starting with the resource's availability template, subtracting existing bookings (including buffers), subtracting blocked times (holidays, maintenance windows), and returning the remaining windows.",[20,1474,1475],{},"For customer-facing booking interfaces, the availability calculation runs on every page load and needs to be fast. Caching the availability for the next N days and invalidating on booking changes is a common optimization.",[30,1477],{},[15,1479,1481],{"id":1480},"beyond-the-calendar-dispatch-and-optimization","Beyond the Calendar: Dispatch and Optimization",[20,1483,1484],{},"Dispatch-oriented scheduling adds a layer of optimization on top of basic availability. When a customer requests a service appointment, the system doesn't just find an available technician — it finds the best available technician based on skills, location, travel time, workload balance, and customer priority.",[20,1486,1487],{},"This is where simple database queries give way to scoring algorithms. Each candidate technician gets a score based on weighted factors: proximity to the service location, matching skill set, current daily workload, customer preference for a specific technician, route efficiency relative to their existing schedule. The highest-scoring technician gets the assignment.",[20,1489,1490],{},"Real-time dispatch — reassigning technicians as conditions change throughout the day — adds another dimension. A cancelled appointment frees up a slot that might be better used for a different job. A technician running late cascades delay to their subsequent appointments. These systems need to continuously re-evaluate the schedule and surface recommended changes to dispatchers.",[20,1492,1493,1494,1498],{},"The architecture for dispatch systems benefits from the same ",[68,1495,1497],{"href":1496},"/blog/domain-driven-design-guide","domain-driven design"," principles that apply to any complex business domain. The scheduling bounded context has its own language (slots, availability, conflicts, assignments) and its own rules that shouldn't leak into the rest of the application.",[20,1500,1501,1502],{},"If you're building a scheduling or dispatch system, ",[68,1503,1505],{"href":384,"rel":1504},[386],"I'd be happy to talk through the architecture.",[30,1507],{},[15,1509,393],{"id":392},[395,1511,1512,1518,1523,1529],{},[398,1513,1514],{},[68,1515,1517],{"href":1516},"/blog/field-service-management-software","Field Service Management Software: Architecture and Features",[398,1519,1520],{},[68,1521,1522],{"href":1496},"Domain-Driven Design in Practice",[398,1524,1525],{},[68,1526,1528],{"href":1527},"/blog/custom-erp-development-guide","Custom ERP Development: What It Actually Takes",[398,1530,1531],{},[68,1532,1534],{"href":1533},"/blog/api-design-best-practices","API Design Best Practices for Production Systems",{"title":107,"searchDepth":211,"depth":211,"links":1536},[1537,1538,1539,1540,1541,1542],{"id":1370,"depth":214,"text":1371},{"id":1385,"depth":214,"text":1386},{"id":1418,"depth":214,"text":1419},{"id":1448,"depth":214,"text":1449},{"id":1480,"depth":214,"text":1481},{"id":392,"depth":214,"text":393},"Scheduling looks simple until you build it. Here's how to architect custom scheduling systems that handle time zones, conflicts, recurring events, and real-world complexity.",[1545,1546,1547],"custom scheduling system","booking system architecture","dispatch software design",{},"/blog/custom-scheduling-system",{"title":1364,"description":1543},"blog/custom-scheduling-system",[1553,1554,1555,1556],"Scheduling","Enterprise Software","Systems Design","Calendar","1rZEiwLLr_XBc1biHmQxbNrrmVaecJXHgxKAUmCitPE",{"id":1559,"title":1560,"author":1561,"body":1562,"category":1674,"date":1342,"description":1675,"extension":222,"featured":223,"image":224,"keywords":1676,"meta":1679,"navigation":229,"path":1680,"readTime":231,"seo":1681,"stem":1682,"tags":1683,"__hash__":1686},"blog/blog/headless-cms-development.md","Headless CMS Architecture for Modern Web Apps",{"name":9,"bio":10},{"type":12,"value":1563,"toc":1668},[1564,1568,1571,1574,1577,1580,1582,1586,1589,1592,1595,1598,1605,1607,1611,1614,1617,1620,1623,1631,1633,1637,1640,1646,1652,1658,1665],[15,1565,1567],{"id":1566},"why-headless-cms-exists","Why Headless CMS Exists",[20,1569,1570],{},"Traditional content management systems like WordPress couple the content repository to the presentation layer. Your content lives in a database, and the CMS provides both the admin interface for editors and the templates that render pages for visitors. This works fine for blogs and brochure sites. It falls apart when you need to deliver that same content to a mobile app, a digital kiosk, an email template, and a web application simultaneously.",[20,1572,1573],{},"A headless CMS removes the presentation layer entirely. It provides the content management interface — the admin panel where editors create and organize content — and exposes everything through an API. Your frontend application fetches content via REST or GraphQL and renders it however you want. The CMS manages content. Your code manages presentation. The API is the contract between them.",[20,1575,1576],{},"This separation creates genuine architectural advantages. You choose your frontend framework independently of your CMS. You can rebuild the frontend without migrating content. You can add new delivery channels — an app, a chatbot, a third-party integration — without duplicating content management. Content becomes a service that multiple consumers share.",[20,1578,1579],{},"The tradeoff is complexity. A traditional CMS gives you a working website out of the box. A headless CMS gives you an API and a content admin panel. You still need to build the frontend, handle routing, manage preview and draft states, implement search, and configure caching. For teams that want to launch a basic marketing site quickly, headless CMS can be over-engineering. For teams building multi-channel experiences or complex web applications, the flexibility is worth the investment.",[30,1581],{},[15,1583,1585],{"id":1584},"choosing-between-hosted-and-self-hosted","Choosing Between Hosted and Self-Hosted",[20,1587,1588],{},"The headless CMS market splits into two categories, and the choice between them shapes your entire architecture.",[20,1590,1591],{},"Hosted platforms like Contentful, Sanity, Storyblok, and Hygraph run the infrastructure for you. Content is stored in their cloud, the admin panel is their hosted application, and you consume content through their API. The advantages are zero infrastructure management, global CDN-backed APIs, and polished editing experiences. The disadvantages are vendor lock-in, usage-based pricing that can spike unpredictably, and less control over data residency.",[20,1593,1594],{},"Self-hosted options like Strapi, Directus, and Payload CMS run on your own infrastructure. You own the data, control the hosting, and can customize the CMS internals. The advantages are no vendor lock-in, predictable costs, and full control. The disadvantages are infrastructure responsibility — you manage the database, the CMS application server, backups, updates, and scaling.",[20,1596,1597],{},"My decision framework is straightforward. If the content team is non-technical and the application is content-heavy (marketing site, documentation hub, editorial platform), a hosted CMS with a mature editing experience like Contentful or Sanity is usually the right call. The editing experience matters more than technical flexibility for content-driven projects.",[20,1599,1600,1601,1604],{},"If the project is a custom application where content is one component alongside business logic, user accounts, and complex data relationships, a self-hosted CMS like Payload or Directus integrated into your existing stack gives you better control. You can deploy it alongside your ",[68,1602,1603],{"href":946},"application backend"," and manage everything in one infrastructure layer.",[30,1606],{},[15,1608,1610],{"id":1609},"content-modeling-for-longevity","Content Modeling for Longevity",[20,1612,1613],{},"The most consequential decisions in a headless CMS project happen before you write any frontend code. Content modeling — defining your content types, fields, and relationships — determines how flexible or rigid the system will be over its lifetime.",[20,1615,1616],{},"The cardinal rule: model content by its semantic meaning, not by its visual presentation. Do not create a content type called \"Hero Section\" with fields for \"background image,\" \"headline,\" and \"CTA button text.\" That locks content to a specific design. Instead, create a content type called \"Page\" with modular content blocks: a \"Text Block\" with a rich text field, an \"Image\" with alt text and caption fields, a \"Call to Action\" with text, URL, and style variant fields. The frontend composes these blocks into layouts. When the design changes — and it will — the content survives.",[20,1618,1619],{},"Keep content types focused. A \"Blog Post\" should have a title, slug, body, author reference, category, and publication date. It should not have SEO-specific fields for every possible meta tag. Create a reusable \"SEO Metadata\" component type and attach it to any content type that needs it. This prevents field sprawl and keeps the editing experience clean.",[20,1621,1622],{},"Model relationships explicitly. Authors, categories, and tags should be their own content types referenced by blog posts, not inline text fields. This enables filtering, aggregation, and consistency. When an author's name changes, it updates everywhere automatically.",[20,1624,1625,1626,1630],{},"Plan for localization from the start, even if you only support one language today. Adding ",[68,1627,1629],{"href":1628},"/blog/internationalization-web-apps","internationalization"," to a CMS that was not designed for it means restructuring every content type. Most headless CMS platforms support locale-specific field variants natively — enable it on day one and the migration cost later is zero.",[30,1632],{},[15,1634,1636],{"id":1635},"frontend-integration-patterns","Frontend Integration Patterns",[20,1638,1639],{},"Fetching content from a headless CMS API on every page request is the simplest approach and the worst performing. API calls add latency, and CMS API rate limits can throttle your site under traffic spikes. The integration pattern you choose determines both performance and editorial workflow.",[20,1641,1642,1645],{},[273,1643,1644],{},"Static generation"," (SSG) fetches all content at build time and produces static HTML files. This is the fastest possible delivery — files served directly from a CDN with no runtime API calls. The limitation is that content changes require a rebuild. For sites that publish a few times per day, webhook-triggered rebuilds on content change make this viable. For sites with real-time content needs, it does not work.",[20,1647,1648,1651],{},[273,1649,1650],{},"Incremental static regeneration"," (ISR) combines static performance with dynamic freshness. Pages are statically generated but revalidated on a time interval or on-demand via webhook. Nuxt's hybrid rendering and Next.js ISR both support this model. It is the sweet spot for most content-driven sites — near-static performance with content updates reflected within minutes.",[20,1653,1654,1657],{},[273,1655,1656],{},"Server-side rendering"," (SSR) fetches content on every request. This guarantees fresh content but adds API call latency to every page load. Use SSR when content freshness is critical and caching strategies cannot provide adequate staleness guarantees. Cache CMS responses aggressively on the server side to mitigate latency — a 60-second cache eliminates most redundant API calls while keeping content reasonably fresh.",[20,1659,1660,1661,1664],{},"For preview and draft functionality, most headless CMS platforms provide a preview API that returns unpublished content. Configure your ",[68,1662,1663],{"href":70},"framework's preview mode"," to use draft API endpoints, allowing editors to see unpublished changes in context before publishing. This editorial workflow is what separates a production-quality headless CMS integration from a basic API consumer.",[20,1666,1667],{},"Cache invalidation deserves explicit attention. Set up webhooks from your CMS that trigger cache purges or rebuilds when content changes. Without this, published changes sit invisible behind stale caches until the next TTL expiration, and editors lose trust in the system. A responsive publish-to-live pipeline — content published in the CMS appearing on the site within a minute — is table stakes for editorial adoption.",{"title":107,"searchDepth":211,"depth":211,"links":1669},[1670,1671,1672,1673],{"id":1566,"depth":214,"text":1567},{"id":1584,"depth":214,"text":1585},{"id":1609,"depth":214,"text":1610},{"id":1635,"depth":214,"text":1636},"Architecture","Headless CMS separates content management from presentation. Here's how to architect a headless CMS setup that scales without creating maintenance headaches.",[1677,1678],"headless CMS architecture","headless CMS development",{},"/blog/headless-cms-development",{"title":1560,"description":1675},"blog/headless-cms-development",[1684,1674,1685],"CMS","API","ssf3EX5fi8fl0P9I3CWtQSiR4TR5Ii9CmuumwL8Fo6Y",{"id":1688,"title":1689,"author":1690,"body":1691,"category":1674,"date":1342,"description":1860,"extension":222,"featured":223,"image":224,"keywords":1861,"meta":1864,"navigation":229,"path":1865,"readTime":231,"seo":1866,"stem":1867,"tags":1868,"__hash__":1871},"blog/blog/saas-api-versioning.md","API Versioning Strategies for SaaS Products",{"name":9,"bio":10},{"type":12,"value":1692,"toc":1852},[1693,1697,1700,1703,1706,1708,1712,1715,1729,1738,1747,1756,1759,1761,1765,1768,1774,1780,1783,1786,1788,1792,1795,1798,1801,1808,1810,1814,1817,1824,1827,1830,1832,1834],[15,1694,1696],{"id":1695},"apis-are-contracts-not-implementation-details","APIs Are Contracts, Not Implementation Details",[20,1698,1699],{},"The moment you expose an API to external consumers — whether they're third-party integrations, partner applications, or your own mobile clients — that API becomes a contract. Every endpoint, every response shape, every error code is a promise you've made to developers who have built systems that depend on your behavior being predictable.",[20,1701,1702],{},"Breaking that contract is sometimes necessary. Products evolve. Data models change. Better patterns emerge. The question isn't whether you'll make breaking changes — you will. The question is how you manage those changes so that they don't damage the trust of the developers who depend on you.",[20,1704,1705],{},"API versioning is the mechanism for honoring old contracts while establishing new ones. But which versioning strategy you choose has real consequences for your codebase, your operational complexity, and your developer experience. I've seen teams choose poorly here and spend years living with the consequences.",[30,1707],{},[15,1709,1711],{"id":1710},"the-four-versioning-strategies","The Four Versioning Strategies",[20,1713,1714],{},"There are four commonly used approaches to API versioning, and each has distinct tradeoffs.",[20,1716,1717,1720,1721,1724,1725,1728],{},[273,1718,1719],{},"URL path versioning"," (",[40,1722,1723],{},"/api/v1/users",", ",[40,1726,1727],{},"/api/v2/users",") is the most explicit and the most widely used. The version is visible in every request, making it impossible to accidentally call the wrong version. Documentation is straightforward because each version is a distinct set of endpoints. The downside is that every breaking change requires a new version path, and your routing layer accumulates version-specific logic over time.",[20,1730,1731,1720,1734,1737],{},[273,1732,1733],{},"Header versioning",[40,1735,1736],{},"Accept: application/vnd.myapp.v2+json",") keeps URLs clean and uses content negotiation to select the version. This is more RESTful in a theoretical sense, but it's harder for developers to use casually. You can't test versioned endpoints by pasting a URL into a browser. Most API consumers find it less intuitive than path versioning, and it's easier to misconfigure.",[20,1739,1740,1720,1743,1746],{},[273,1741,1742],{},"Query parameter versioning",[40,1744,1745],{},"/api/users?version=2",") is simple to implement but semantically questionable. Version isn't a query parameter — it's a fundamental property of the request. It also makes caching more complex because cache keys now depend on query parameters. I generally advise against this approach.",[20,1748,1749,1752,1753,1466],{},[273,1750,1751],{},"No explicit versioning with additive-only changes"," is what some teams attempt, committing to never making breaking changes and only adding new fields and endpoints. This works until it doesn't. Eventually, you'll need to rename something, change a data type, or restructure a response. If you haven't built versioning into your architecture from the start, you'll bolt it on under pressure. I covered the broader principles of API contract design in my piece on ",[68,1754,1755],{"href":1533},"API design best practices",[20,1757,1758],{},"For most SaaS products, URL path versioning is the right choice. It's explicit, well-understood, and works with every HTTP client and documentation tool.",[30,1760],{},[15,1762,1764],{"id":1763},"what-constitutes-a-breaking-change","What Constitutes a Breaking Change",[20,1766,1767],{},"Defining what's \"breaking\" is less obvious than it sounds, and getting alignment on this within your team prevents arguments later.",[20,1769,1770,1773],{},[273,1771,1772],{},"Breaking changes"," include removing a field from a response, renaming a field, changing a field's data type, removing an endpoint, changing an endpoint's URL, changing the authentication mechanism, and altering the meaning of an error code.",[20,1775,1776,1779],{},[273,1777,1778],{},"Non-breaking changes"," include adding a new field to a response (existing consumers should ignore unknown fields), adding a new endpoint, adding an optional request parameter, and adding a new error code.",[20,1781,1782],{},"The gray area is changes that are technically non-breaking but behaviorally significant — like changing the default sort order of a list endpoint, adding pagination to an endpoint that previously returned all results, or modifying rate limits. These changes don't break the API contract in a strict sense, but they can break consumer applications that depended on the old behavior.",[20,1784,1785],{},"My rule of thumb: if a consumer's code could behave differently after the change without the consumer modifying their code, treat it as a breaking change and version it accordingly.",[30,1787],{},[15,1789,1791],{"id":1790},"implementation-architecture","Implementation Architecture",[20,1793,1794],{},"The cleanest implementation pattern I've found for URL path versioning uses a router-level version prefix with version-specific controllers that delegate to shared business logic.",[20,1796,1797],{},"Your routing layer handles version extraction and routes to the appropriate controller. Each versioned controller is responsible for request validation (which may differ between versions), response shaping (which almost certainly differs), and mapping between the version-specific API contract and the internal domain model.",[20,1799,1800],{},"The critical architectural decision is keeping business logic version-agnostic. Your domain layer — the code that actually processes data, enforces rules, and interacts with the database — should know nothing about API versions. Versioned controllers translate between the external contract and the internal domain. This means a new API version requires new controllers and request/response types, but no changes to business logic.",[20,1802,1091,1803,1807],{},[68,1804,1806],{"href":1805},"/blog/multi-tenant-architecture","multi-tenant SaaS applications",", versioning adds another dimension to consider. Different tenants may be on different API versions depending on when they integrated. Your system needs to track which version each integration uses and enforce appropriate rate limits and feature access per version.",[30,1809],{},[15,1811,1813],{"id":1812},"deprecation-and-sunset-policy","Deprecation and Sunset Policy",[20,1815,1816],{},"A versioning strategy without a deprecation policy leads to indefinite maintenance of old versions. Every active version is code you maintain, test, and operate. The operational cost accumulates.",[20,1818,1819,1820,1823],{},"Establish a clear policy from day one. Announce deprecation at least 6 months before sunsetting a version. Provide migration guides that explain every change and offer code examples. Monitor usage of deprecated versions so you know who needs to migrate. Enforce sunset dates — at some point, the old version returns ",[40,1821,1822],{},"410 Gone"," and consumers must upgrade.",[20,1825,1826],{},"The deprecation policy should be documented publicly and communicated proactively. A version sunset that surprises your customers is a trust violation, regardless of how clearly it was documented.",[20,1828,1829],{},"Building API versioning correctly from the start is significantly easier than retrofitting it onto a live API with active consumers. The time to make this decision is before your first external integration, not after a breaking change has already caused an outage.",[30,1831],{},[15,1833,393],{"id":392},[395,1835,1836,1841,1846],{},[398,1837,1838],{},[68,1839,1840],{"href":1533},"API Design Best Practices: Building APIs That Last",[398,1842,1843],{},[68,1844,1845],{"href":1805},"Multi-Tenant Architecture: Patterns for Building Software That Serves Many Clients",[398,1847,1848],{},[68,1849,1851],{"href":1850},"/blog/enterprise-api-management","Enterprise API Management and Governance",{"title":107,"searchDepth":211,"depth":211,"links":1853},[1854,1855,1856,1857,1858,1859],{"id":1695,"depth":214,"text":1696},{"id":1710,"depth":214,"text":1711},{"id":1763,"depth":214,"text":1764},{"id":1790,"depth":214,"text":1791},{"id":1812,"depth":214,"text":1813},{"id":392,"depth":214,"text":393},"Breaking API changes are inevitable in a growing SaaS product. The versioning strategy you choose determines whether changes break your customers or your team.",[1862,1863],"SaaS API versioning","API versioning strategies",{},"/blog/saas-api-versioning",{"title":1689,"description":1860},"blog/saas-api-versioning",[1869,1870,1674],"API Design","SaaS","Mrjzd9jyRESkex_20_o_9ZGLnod2XHXetd1X1jzzl2g",{"id":1873,"title":1874,"author":1875,"body":1876,"category":219,"date":1972,"description":1973,"extension":222,"featured":223,"image":224,"keywords":1974,"meta":1978,"navigation":229,"path":1979,"readTime":231,"seo":1980,"stem":1981,"tags":1982,"__hash__":1987},"blog/blog/auto-glass-customer-intake-system.md","Designing the Customer Intake System for an Auto Glass Business",{"name":9,"bio":10},{"type":12,"value":1877,"toc":1966},[1878,1882,1885,1888,1897,1901,1904,1907,1910,1913,1916,1920,1928,1931,1934,1940,1944,1947,1950,1953],[15,1879,1881],{"id":1880},"what-makes-auto-glass-intake-different","What Makes Auto Glass Intake Different",[20,1883,1884],{},"Customer intake for auto glass is not a simple contact form. The business needs specific information before it can do anything useful — and \"specific\" means vehicle year, make, model, and trim, the type and location of damage, whether insurance is involved, the customer's location and preferred service time, and photos of the damage when possible.",[20,1886,1887],{},"Getting all of this in a single form submission is the difference between a lead that can be quoted and scheduled immediately and one that requires two or three follow-up calls to gather missing details. Every follow-up call is friction. Friction kills conversion.",[20,1889,1890,1891,1896],{},"The challenge was designing a form that collected comprehensive information without feeling like a bureaucratic ordeal. You can see the live result at ",[68,1892,1895],{"href":1893,"rel":1894},"https://myautoglassrehab.com",[386],"myautoglassrehab.com",". The target user is someone with a cracked windshield who wants it fixed. They are not in the mood to fill out a twenty-field form. The intake system needed to feel quick and effortless while actually capturing everything Chris needed to dispatch a job.",[15,1898,1900],{"id":1899},"multi-step-form-design","Multi-Step Form Design",[20,1902,1903],{},"The solution was a multi-step form that broke the intake process into logical groups. Each step focused on one category of information and felt light — three to four fields at most. Progress indicators showed the user how far along they were, and each step validated inline before advancing to the next.",[20,1905,1906],{},"Step one captured the basics: name, phone number, and service address. These three fields were enough to start a conversation if the user abandoned the form, so we prioritized them first.",[20,1908,1909],{},"Step two focused on the vehicle: year, make, model, and trim. This is where the form got interesting. We implemented cascading dropdowns that filtered options dynamically — selecting a make narrowed the model list, selecting a model narrowed the trim list. The vehicle data came from a curated database rather than free-text entry, which eliminated the garbage-in problem that plagues auto glass quoting. A customer typing \"Camery\" instead of \"Camry\" creates headaches downstream. Dropdowns prevented that entirely.",[20,1911,1912],{},"Step three addressed the damage: which glass was affected (windshield, rear window, side window, quarter glass), the type of damage (chip, crack, shattered), and approximate size. This step also included an optional photo upload. Photos were not required because mandating them would kill mobile conversion rates, but when customers provided them, they significantly reduced the need for on-site assessment before quoting.",[20,1914,1915],{},"Step four covered insurance and scheduling: whether the customer wanted to file an insurance claim, their insurance provider if applicable, and their preferred service date and time window. Insurance handling is a major differentiator for auto glass businesses, and capturing this information upfront allowed Chris to start the claims process before the technician even arrived.",[15,1917,1919],{"id":1918},"data-contracts-and-erp-integration","Data Contracts and ERP Integration",[20,1921,1922,1923,1927],{},"The form was designed as a frontend for the ",[68,1924,1926],{"href":1925},"/blog/bastionglass-architecture-decisions","BastionGlass ERP",", even though the ERP was still under development when the intake system launched. This was a deliberate architectural choice that saved significant rework later.",[20,1929,1930],{},"Every field in the form mapped to a defined TypeScript interface that both the website and the ERP consumed. The interface looked roughly like this: customer contact information, vehicle specification, damage assessment, insurance details, and scheduling preferences. Each section was a nested type with strict validation rules.",[20,1932,1933],{},"Validation happened at two layers. The frontend used VeeValidate with Zod schemas for instant feedback — the user saw errors as they typed, not after submission. The backend validated against the same Zod schemas before accepting the submission, ensuring that even if someone bypassed the frontend validation, malformed data could not enter the system.",[20,1935,1936,1937,1939],{},"When BastionGlass came online, integrating the intake form was a matter of pointing the submission endpoint to the ERP's ",[68,1938,1685],{"href":946}," instead of the temporary data store. The data shape did not change. The validation did not change. The form itself did not change. The customer experience was identical, but submissions now landed directly in BastionGlass's dispatch queue where they could be assigned to technicians, quoted, and scheduled without manual data entry.",[15,1941,1943],{"id":1942},"conversion-optimization","Conversion Optimization",[20,1945,1946],{},"The multi-step approach had measurable effects on conversion. Compared to the single-page form we initially tested, the multi-step version had a 40% higher completion rate. Users who started the form were more likely to finish it because each individual step felt manageable.",[20,1948,1949],{},"We also implemented progressive data capture — if a user completed step one but abandoned the form, we still had their name and phone number. Chris could follow up with a quick call or text. This recovered leads that would have been completely lost with a traditional all-or-nothing form submission.",[20,1951,1952],{},"Mobile optimization was essential. Over 70% of traffic came from mobile devices, which makes sense for the use case — people discover they have glass damage, pull out their phone, and search for repair services. The form was designed touch-first: large tap targets, no tiny dropdown menus, and keyboard types that matched the field (numeric keyboard for phone numbers, email keyboard for email addresses).",[20,1954,1955,1956,1960,1961,1965],{},"The intake system became the connective tissue between the ",[68,1957,1959],{"href":1958},"/blog/myautoglassrehab-nuxt-build","marketing site"," and the ",[68,1962,1964],{"href":1963},"/blog/building-erp-from-scratch","ERP",". It turned website visitors into structured, actionable data that the business could process without manual intervention. That connection — website visitor to dispatched job with zero manual data entry — is where the real value of the system lives.",{"title":107,"searchDepth":211,"depth":211,"links":1967},[1968,1969,1970,1971],{"id":1880,"depth":214,"text":1881},{"id":1899,"depth":214,"text":1900},{"id":1918,"depth":214,"text":1919},{"id":1942,"depth":214,"text":1943},"2025-11-05","How I designed a customer intake flow that captures vehicle details, damage assessment, and insurance info — then feeds directly into an ERP dispatch queue.",[1975,1976,1977],"customer intake system design","auto glass customer form","service business intake workflow",{},"/blog/auto-glass-customer-intake-system",{"title":1874,"description":1973},"blog/auto-glass-customer-intake-system",[1964,1983,1984,1985,1986],"Form Design","Auto Glass","User Experience","TypeScript","lFtXXWphZ3qfIduAvkWI-NTheBEEA7xYn1vzZ5cvxZk",{"id":1989,"title":1990,"author":1991,"body":1992,"category":1152,"date":1972,"description":2135,"extension":222,"featured":223,"image":224,"keywords":2136,"meta":2142,"navigation":229,"path":2143,"readTime":231,"seo":2144,"stem":2145,"tags":2146,"__hash__":2151},"blog/blog/beaker-people-bronze-age.md","The Beaker People: Trade, Metallurgy, and Genetic Replacement",{"name":9,"bio":10},{"type":12,"value":1993,"toc":2127},[1994,1998,2001,2004,2007,2011,2018,2021,2029,2037,2041,2044,2050,2056,2062,2068,2072,2075,2082,2089,2093,2101,2104,2106,2108],[15,1995,1997],{"id":1996},"the-beaker-question","The Beaker Question",[20,1999,2000],{},"For over a century, archaeologists argued about the Bell Beaker phenomenon. Named for their distinctive bell-shaped drinking vessels -- elegantly decorated pottery found in graves from Hungary to Morocco, from Scandinavia to Sicily -- the Beaker culture appeared across a vast swathe of Europe between approximately 2,800 and 1,800 BC.",[20,2002,2003],{},"The central question was simple: did the Beakers represent a migrating people, or a migrating fashion? Were the bell-shaped pots carried by a specific population expanding across Europe, or were they a prestige good adopted by local communities through trade and cultural contact?",[20,2005,2006],{},"This was not an idle academic debate. The answer determined whether the Bronze Age transition in Western Europe involved actual population replacement or simply cultural change. And in 2018, ancient DNA provided a definitive answer.",[15,2008,2010],{"id":2009},"the-olalde-study","The Olalde Study",[20,2012,2013,2014,2017],{},"In 2018, Inigo Olalde and a large international team published a landmark study in ",[1035,2015,2016],{},"Nature"," examining the genomes of over four hundred ancient individuals associated with the Bell Beaker phenomenon across Europe. The results revealed that the Beaker culture was both things simultaneously -- but not in equal measure everywhere.",[20,2019,2020],{},"In Iberia, where the earliest Beaker pottery appears, the cultural spread was largely a matter of local adoption. The people making and using Beaker pottery in Spain and Portugal were genetically continuous with earlier local populations. The beakers were a local invention, and they spread initially through trade networks.",[20,2022,2023,2024,2028],{},"But when the Beaker phenomenon crossed into Central Europe and then into Britain and Ireland, it became something different. In Britain, the Beaker-associated population was genetically distinct from the preceding Neolithic population. These were not locals who had adopted Beaker fashions -- they were migrants who had arrived carrying ",[68,2025,2027],{"href":2026},"/blog/yamnaya-horizon-steppe-ancestors","Steppe-derived ancestry",", R1b Y-chromosomes, and a material culture package that included the bell beakers, copper daggers, archer's wristguards, and gold ornaments.",[20,2030,2031,2032,2036],{},"The scale of the replacement in Britain was staggering. Within a few centuries of the Beaker arrival, approximately ninety percent of the existing British gene pool had been replaced. The ",[68,2033,2035],{"href":2034},"/blog/megalithic-builders-europe","megalithic builders"," -- the people who had constructed Stonehenge, the Orkney monuments, and the chambered tombs of the British Neolithic -- were genetically overwhelmed.",[15,2038,2040],{"id":2039},"what-the-beaker-people-brought","What the Beaker People Brought",[20,2042,2043],{},"The Beaker migrants carried more than pottery. Their cultural package included a suite of innovations that marked the transition from the Neolithic to the Bronze Age:",[20,2045,2046,2049],{},[273,2047,2048],{},"Copper and bronze metallurgy."," Beaker graves frequently contain copper daggers, gold ornaments, and the tools of metalworking. The Beaker expansion correlates with the spread of metal technology into regions that had previously relied entirely on stone tools. This was not merely a material upgrade -- metallurgy requires specialized knowledge, fuel management, and trade networks for ore, transforming the economic and social structure of the communities that adopted it.",[20,2051,2052,2055],{},[273,2053,2054],{},"The individual burial."," Neolithic Atlantic Europe buried its dead communally -- in passage tombs, chambered cairns, and collective ossuaries. The Beaker people buried their dead individually, often in crouched positions with personal grave goods. This shift from communal to individual burial reflects a fundamental change in how identity was constructed: from community membership to personal status and lineage.",[20,2057,2058,2061],{},[273,2059,2060],{},"The archer complex."," Many Beaker burials include wristguards (bracers), arrowheads, and sometimes bows. The \"archer\" identity appears to have been a central element of Beaker male culture, whether functional or symbolic.",[20,2063,2064,2067],{},[273,2065,2066],{},"Dairy economy."," The Beaker migrants carried the lactase persistence gene at higher frequencies than the Neolithic populations they replaced. The ability to digest milk as adults increased the caloric yield from cattle herds, providing a nutritional advantage in a pastoral economy.",[15,2069,2071],{"id":2070},"the-route-to-ireland","The Route to Ireland",[20,2073,2074],{},"The specific pathway by which R1b-L21-carrying Beaker people reached Ireland is a matter of ongoing research, but the broad outlines are clear. The migration moved from Central Europe through France and along the Atlantic coast, arriving in Britain and Ireland around 2,500-2,400 BC.",[20,2076,2077,2078,2081],{},"In Ireland, the genetic transition mirrors the British pattern. Pre-Beaker Irish burials show Y-chromosome haplogroups I2 and G2a -- the Neolithic farmer profile. Post-Beaker Irish burials are overwhelmingly ",[68,2079,2080],{"href":1299},"R1b-L21",". The replacement was near-total on the male line.",[20,2083,2084,2085,2088],{},"The Irish mythological tradition preserves a memory of this event in the ",[1035,2086,2087],{},"Lebor Gabala Erenn"," -- the Book of Invasions -- which describes the arrival of the Sons of Mil from Spain, who conquered Ireland and established the Gaelic dynasties. The genetic evidence confirms that the modern Irish male lineage was indeed established by a migration from Atlantic Europe during the Bronze Age.",[15,2090,2092],{"id":2091},"the-legacy","The Legacy",[20,2094,2095,2096,2100],{},"The Beaker phenomenon ended as a recognizable archaeological culture around 1,800 BC, but its genetic and cultural legacy is permanent. The populations established by the Beaker migration became the foundation of Bronze Age and Iron Age Atlantic Europe. The ",[68,2097,2099],{"href":2098},"/blog/celtic-languages-family-tree","Celtic languages"," that would later emerge in these regions were spoken by the descendants of Beaker-era migrants. The Y-chromosome haplogroups they carried -- R1b-L21 in Ireland and Scotland, R1b-DF27 in Iberia, R1b-U152 in Italy and Central Europe -- remain the dominant male lineages in those regions today.",[20,2102,2103],{},"The Beaker People were not just potters. They were the founders of the genetic world that Atlantic Europe still inhabits.",[30,2105],{},[15,2107,1123],{"id":1122},[395,2109,2110,2116,2121],{},[398,2111,2112],{},[68,2113,2115],{"href":2114},"/blog/bell-beaker-conquest-ireland-britain","The Bell Beaker Conquest: How Bronze Age Migrants Replaced Ireland's Men",[398,2117,2118],{},[68,2119,2120],{"href":2034},"The Megalithic Builders: Stonehenge, Newgrange, and Beyond",[398,2122,2123],{},[68,2124,2126],{"href":2125},"/blog/r1b-haplogroup-western-europe","R1b: The Most Common Haplogroup in Western Europe",{"title":107,"searchDepth":211,"depth":211,"links":2128},[2129,2130,2131,2132,2133,2134],{"id":1996,"depth":214,"text":1997},{"id":2009,"depth":214,"text":2010},{"id":2039,"depth":214,"text":2040},{"id":2070,"depth":214,"text":2071},{"id":2091,"depth":214,"text":2092},{"id":1122,"depth":214,"text":1123},"The Bell Beaker phenomenon spread distinctive pottery, copper metallurgy, and new genetic ancestry across Europe between 2800 and 1800 BC. But were the Beaker People traders who shared ideas, or migrants who replaced populations? Ancient DNA has given us the answer.",[2137,2138,2139,2140,2141],"beaker people","bell beaker culture","beaker people dna","bronze age migration","bell beaker genetic replacement",{},"/blog/beaker-people-bronze-age",{"title":1990,"description":2135},"blog/beaker-people-bronze-age",[2147,2148,1356,2149,2150],"Bell Beaker","Bronze Age","Genetic Replacement","European Prehistory","mitYof7n-ji7N9h8ok5zRsx490lFyieWQdMmmyaRfLE",{"id":2153,"title":2154,"author":2155,"body":2156,"category":2259,"date":1972,"description":2260,"extension":222,"featured":223,"image":224,"keywords":2261,"meta":2264,"navigation":229,"path":2265,"readTime":231,"seo":2266,"stem":2267,"tags":2268,"__hash__":2272},"blog/blog/customer-feedback-product.md","Using Customer Feedback to Drive Product Development",{"name":9,"bio":10},{"type":12,"value":2157,"toc":2253},[2158,2162,2165,2168,2171,2173,2177,2180,2186,2192,2198,2204,2206,2210,2213,2216,2224,2227,2229,2233,2236,2239,2242,2245],[15,2159,2161],{"id":2160},"the-gap-between-collecting-feedback-and-using-it","The Gap Between Collecting Feedback and Using It",[20,2163,2164],{},"Most software teams collect feedback. Support tickets pile up, feature requests accumulate in spreadsheets, NPS surveys generate scores, user interviews produce notes. The problem is rarely a lack of feedback — it's the lack of a system for transforming raw feedback into product decisions.",[20,2166,2167],{},"Without that system, feedback becomes noise. The loudest customer's request gets built next. The feature request that came in right before sprint planning gets prioritized over the one submitted three months ago. Critical usability issues get lost in a backlog alongside cosmetic suggestions. The team builds features that satisfy individual requests without addressing the underlying patterns that would satisfy many customers at once.",[20,2169,2170],{},"Effective feedback management isn't about collecting more data. It's about building a process that connects what customers say to what the product team builds — with clear logic at every step that anyone on the team can understand and follow.",[30,2172],{},[15,2174,2176],{"id":2175},"building-a-feedback-collection-system","Building a Feedback Collection System",[20,2178,2179],{},"Good feedback collection happens through multiple channels, each capturing different types of insight.",[20,2181,2182,2185],{},[273,2183,2184],{},"In-app feedback"," captures reactions in context. A feedback button on a specific feature, a satisfaction prompt after task completion, or a brief survey triggered by behavior (like a user visiting the help docs multiple times) all capture feedback at the moment when the user's experience is freshest. Keep in-app feedback prompts minimal — one question, not five — to maximize response rates and reduce disruption.",[20,2187,2188,2191],{},[273,2189,2190],{},"Support conversations"," are the richest source of feedback most teams underutilize. Every support ticket represents a moment where a user's expectation didn't match the product's behavior. Categorize support issues systematically: Is this a bug, a usability problem, a missing feature, or a documentation gap? Over time, this categorization reveals patterns that are invisible in any single ticket.",[20,2193,2194,2197],{},[273,2195,2196],{},"Direct conversations"," with customers — interviews, calls, meetings — provide depth that no survey can match. But the insight from these conversations often stays in the head of whoever had them, or in notes that no one else reads. Formalize the sharing: after every customer conversation, post a brief summary to a shared channel. Include direct quotes when they capture the user's experience vividly.",[20,2199,2200,2203],{},[273,2201,2202],{},"Behavioral data"," is the feedback customers give through their actions rather than their words. Which features are used daily? Which are used once and abandoned? Where do users drop off in workflows? Behavioral data won't tell you why something is happening, but it tells you where to focus your qualitative investigation.",[30,2205],{},[15,2207,2209],{"id":2208},"from-raw-feedback-to-actionable-insights","From Raw Feedback to Actionable Insights",[20,2211,2212],{},"Individual feedback items are anecdotes. Patterns across multiple items are insights. The transformation from anecdote to insight requires categorization, aggregation, and interpretation.",[20,2214,2215],{},"Tag every piece of feedback with the feature area it relates to, the type of issue (bug, usability, feature request, performance), and the customer segment it came from. Over time, these tags reveal which areas of the product generate the most friction, which types of issues are most common, and which customer segments have unmet needs.",[20,2217,2218,2219,2223],{},"Resist the temptation to act on individual requests without checking for patterns first. A single customer asking for a dark mode is a data point. Thirty customers asking for dark mode, combined with behavioral data showing evening usage peaks, is a pattern worth acting on. The ",[68,2220,2222],{"href":2221},"/blog/continuous-discovery-product","continuous discovery process"," provides a framework for validating these patterns before committing development resources.",[20,2225,2226],{},"Distinguish between the problem and the proposed solution. Customers describe their pain in terms of solutions: \"I need an export button.\" But the underlying problem might be that they need to share data with a colleague who doesn't have access to the system. The export button is one solution. Sharing permissions might be a better one. Always dig past the requested feature to understand the job the customer is trying to accomplish.",[30,2228],{},[15,2230,2232],{"id":2231},"closing-the-loop","Closing the Loop",[20,2234,2235],{},"The most damaging thing you can do with customer feedback is collect it and then visibly ignore it. Customers who take the time to share feedback and see no acknowledgment or response stop providing feedback — and they tell others about the experience.",[20,2237,2238],{},"Closing the feedback loop means three things. First, acknowledge every piece of feedback, even if you can't act on it immediately. A brief response — \"Thank you, we've logged this and it will be reviewed during our next planning cycle\" — costs almost nothing and preserves the relationship.",[20,2240,2241],{},"Second, communicate when feedback influences a product change. When you ship a feature that was requested by customers, tell them. \"You asked for this, and we built it\" is one of the most powerful retention messages available. It demonstrates that their input matters and incentivizes continued engagement.",[20,2243,2244],{},"Third, explain when you choose not to act on feedback. Not every request will be built, and customers understand that. What damages trust is silence. \"We considered this request and decided to prioritize other improvements because...\" is a response that preserves trust even when the answer is no.",[20,2246,2247,2248,2252],{},"Build feedback review into your regular planning process. Every sprint planning or ",[68,2249,2251],{"href":2250},"/blog/feature-prioritization-frameworks","feature prioritization session"," should include a review of recent feedback patterns. This doesn't mean that feedback dictates the roadmap — strategic vision and technical considerations matter too — but it ensures that the voice of the customer has a seat at the table alongside business objectives and technical concerns. Products built entirely from customer requests lack coherent vision. Products built without customer input lack relevance. The balance between these extremes is where great products live.",{"title":107,"searchDepth":211,"depth":211,"links":2254},[2255,2256,2257,2258],{"id":2160,"depth":214,"text":2161},{"id":2175,"depth":214,"text":2176},{"id":2208,"depth":214,"text":2209},{"id":2231,"depth":214,"text":2232},"Business","How to collect, organize, and act on customer feedback systematically. Turn scattered input into a structured process that improves your product consistently.",[2262,2263],"customer feedback product development","using customer feedback effectively",{},"/blog/customer-feedback-product",{"title":2154,"description":2260},"blog/customer-feedback-product",[2269,2270,2271],"Customer Feedback","Product Development","Product Management","D3yNrUS2aQvZodYYpxN-PAtOeKUf3YdAsNBmN8nFHmw",{"id":2274,"title":2275,"author":2276,"body":2278,"category":1152,"date":1972,"description":2369,"extension":222,"featured":223,"image":224,"keywords":2370,"meta":2376,"navigation":229,"path":2377,"readTime":231,"seo":2378,"stem":2379,"tags":2380,"__hash__":2386},"blog/blog/lindisfarne-viking-raid.md","Lindisfarne 793: The Raid That Changed Everything",{"name":9,"bio":2277},"Author of The Forge of Tongues — 22,000 Years of Migration, Mutation, and Memory",{"type":12,"value":2279,"toc":2363},[2280,2284,2292,2295,2298,2302,2305,2308,2311,2315,2323,2331,2334,2338,2341,2360],[15,2281,2283],{"id":2282},"the-day-the-northmen-came","The Day the Northmen Came",[20,2285,2286,2287,2291],{},"On June 8, 793 AD, ships appeared off the coast of Lindisfarne — Holy Island — a tidal island connected to the Northumbrian mainland by a causeway that disappears beneath the sea at high tide. The monastery there had been founded by Aidan, a monk sent from ",[68,2288,2290],{"href":2289},"/blog/iona-monastery-history","Iona"," in 635, and for over a century and a half it had been one of the most important religious houses in Britain. The Lindisfarne Gospels, one of the masterpieces of Insular art, had been produced there around 715. The island was a center of learning, devotion, and accumulated wealth in the form of sacred vessels, reliquaries, and manuscripts.",[20,2293,2294],{},"The men who came ashore were Norse. They came fast, armed, and without warning. The Anglo-Saxon Chronicle recorded the event with horror: \"In this year terrible portents appeared over Northumbria and sadly affrighted the inhabitants. There were exceptional flashes of lightning, and fiery dragons were seen flying in the air. A great famine followed soon upon these signs, and a little after that in the same year on the 8th of June the harrying of the heathen miserably destroyed God's church in Lindisfarne by rapine and slaughter.\"",[20,2296,2297],{},"The monks were killed or taken as slaves. The treasures were seized. The buildings were damaged but not destroyed — the raiders came for portable wealth, not conquest. They loaded their ships and left with the tide.",[15,2299,2301],{"id":2300},"why-lindisfarne-mattered","Why Lindisfarne Mattered",[20,2303,2304],{},"The raid on Lindisfarne was not, strictly speaking, the first Viking attack on the British Isles. There are references to Norse raids on Portland in Dorset a few years earlier, and the Irish annals record scattered coastal attacks. But Lindisfarne entered the historical record as the event that marked the beginning of the Viking Age because of what it symbolized.",[20,2306,2307],{},"Lindisfarne was not a military target. It was a sacred site, one of the holiest places in Christendom north of Rome. The scholar Alcuin, a Northumbrian serving at the court of Charlemagne, wrote a letter to the bishop of Lindisfarne that captured the shock: \"Never before has such terror appeared in Britain as we have now suffered from a pagan race. Nor was it thought possible that such an inroad from the sea could be made.\"",[20,2309,2310],{},"The significance was psychological as much as material. The monasteries of Britain and Ireland had existed for centuries as places of safety — repositories of knowledge, art, and wealth that were protected by their sacred status. The Norse raiders did not recognize that status. They saw monasteries as what they were in purely material terms: concentrations of portable, valuable objects, located on exposed coastlines, defended by unarmed men. The logic was brutal and, from the raiders' perspective, obvious.",[15,2312,2314],{"id":2313},"the-pattern-that-followed","The Pattern That Followed",[20,2316,2317,2318,2322],{},"After Lindisfarne, the raids accelerated. Iona was hit in 795, 802, and with devastating force in 806. Monasteries along the Irish coast were targeted repeatedly. By the 830s and 840s, the Norse were no longer just raiding — they were establishing permanent bases. Dublin was founded as a Norse longphort around 841. The Hebrides, Orkney, and Shetland came under Norse control. The ",[68,2319,2321],{"href":2320},"/blog/norse-gaels-hybrid-culture","Norse-Gaelic hybrid culture"," that would define the western seaboard for centuries was already taking shape.",[20,2324,2325,2326,2330],{},"In Scotland, the Viking raids had a transformative political effect. The pressure of Norse attacks on both the Pictish and Gaelic kingdoms contributed to their eventual merger under Kenneth MacAlpin around 843, forming the ",[68,2327,2329],{"href":2328},"/blog/kingdom-of-alba-formation","Kingdom of Alba"," — the political entity that would become Scotland. The Norse threat was one of the forces that pushed previously separate peoples toward unification.",[20,2332,2333],{},"In England, the pattern was similar. The Great Heathen Army arrived in 865 and conquered three of the four Anglo-Saxon kingdoms within a decade. Only Wessex survived under Alfred, and the political map of England was permanently redrawn.",[15,2335,2337],{"id":2336},"what-the-raiders-brought","What the Raiders Brought",[20,2339,2340],{},"It would be a mistake to reduce the Viking Age to a story of destruction. The Norse were traders, settlers, and political innovators as well as raiders. They established trade networks that stretched from Scandinavia to Constantinople, from Iceland to the rivers of Russia. They brought new shipbuilding techniques, new forms of governance, and a cultural vitality that, when it mixed with the existing Celtic and Anglo-Saxon traditions, produced something entirely new.",[20,2342,2343,2344,2347,2348,2351,2352,2355,2356,2359],{},"In Scotland and Ireland, the merging of Norse and Gaelic cultures created ",[68,2345,2346],{"href":2320},"a hybrid society"," that was neither purely Scandinavian nor purely Celtic. Norse loanwords entered the Gaelic languages. Place-names across the Hebrides and the Scottish mainland still bear the marks of Norse settlement — ",[1035,2349,2350],{},"vik"," (bay), ",[1035,2353,2354],{},"ness"," (headland), ",[1035,2357,2358],{},"dale"," (valley). The genetic legacy of Norse settlement is visible in modern DNA studies, particularly in Orkney and Shetland where Scandinavian ancestry remains significant.",[20,2361,2362],{},"But none of that was visible on the morning of June 8, 793. On that day, what arrived at Lindisfarne was simply violence — sudden, efficient, and aimed at one of the places where knowledge and beauty had been painstakingly accumulated over generations. The monks who survived carried what they could and fled. The age of the undefended monastery was over. What followed — the political consolidation, the cultural fusion, the new identities that emerged — came later, built on the wreckage of what the raiders left behind.",{"title":107,"searchDepth":211,"depth":211,"links":2364},[2365,2366,2367,2368],{"id":2282,"depth":214,"text":2283},{"id":2300,"depth":214,"text":2301},{"id":2313,"depth":214,"text":2314},{"id":2336,"depth":214,"text":2337},"On June 8, 793 AD, Norse raiders attacked the monastery at Lindisfarne off the Northumbrian coast. It was not the first Viking raid, but it was the one that announced a new era — an age of seaborne violence that would reshape Britain, Ireland, and Scotland for three centuries.",[2371,2372,2373,2374,2375],"lindisfarne viking raid","viking raid 793","viking age beginning","lindisfarne monastery","norse raiders britain",{},"/blog/lindisfarne-viking-raid",{"title":2275,"description":2369},"blog/lindisfarne-viking-raid",[2381,2382,2383,2384,2385],"Lindisfarne","Viking Age","Norse Raiders","Anglo-Saxon England","Northumbria","Fx4nLpqwoB8PnjUiJJ0crS9Wyj1UyispJQdye7sVunA",{"id":2388,"title":2389,"author":2390,"body":2391,"category":2904,"date":2905,"description":2906,"extension":222,"featured":223,"image":224,"keywords":2907,"meta":2910,"navigation":229,"path":2911,"readTime":602,"seo":2912,"stem":2913,"tags":2914,"__hash__":2917},"blog/blog/component-library-development.md","Creating a Component Library: From Scratch to Published Package",{"name":9,"bio":10},{"type":12,"value":2392,"toc":2898},[2393,2396,2399,2403,2406,2433,2590,2593,2611,2619,2623,2626,2629,2692,2699,2824,2827,2846,2850,2853,2856,2859,2867,2871,2874,2877,2884,2887,2895],[20,2394,2395],{},"Building a component library sounds straightforward until you start. You write some components, bundle them, publish to npm — done. But the gap between \"a collection of components\" and \"a library teams actually want to use\" is enormous. It is filled with decisions about API design, build configuration, tree-shaking, type exports, documentation, and versioning that do not have obvious right answers.",[20,2397,2398],{},"I have built internal component libraries for organizations and contributed to public ones. Here is what I wish someone had told me before I started.",[15,2400,2402],{"id":2401},"architecture-and-api-design","Architecture and API Design",[20,2404,2405],{},"The most important decision is not which bundler to use — it is how your components expose their API. Every component has three surfaces: props, slots, and emitted events. The clarity and consistency of these surfaces determine whether your library is a joy or a frustration to use.",[20,2407,2408,2409,2412,2413,2416,2417,2420,2421,2424,2425,2428,2429,2432],{},"Establish conventions before writing a single component. Will boolean props use ",[40,2410,2411],{},"is"," prefixes (",[40,2414,2415],{},"isDisabled"," vs ",[40,2418,2419],{},"disabled",")? Will size props use string literals (",[40,2422,2423],{},"sm | md | lg",") or numeric scales? Will events use past tense (",[40,2426,2427],{},"selected",") or present tense (",[40,2430,2431],{},"select",")?",[102,2434,2438],{"className":2435,"code":2436,"language":2437,"meta":107,"style":107},"language-ts shiki shiki-themes github-dark","// Consistent prop patterns across components\ninterface ButtonProps {\n variant: 'primary' | 'secondary' | 'ghost'\n size: 'sm' | 'md' | 'lg'\n disabled?: boolean\n loading?: boolean\n}\n\nInterface InputProps {\n size: 'sm' | 'md' | 'lg' // Same scale as Button\n disabled?: boolean // Same name as Button\n error?: string\n modelValue: string\n}\n","ts",[40,2439,2440,2445,2457,2480,2500,2511,2520,2525,2530,2535,2556,2568,2578,2586],{"__ignoreMap":107},[111,2441,2442],{"class":113,"line":114},[111,2443,2444],{"class":536},"// Consistent prop patterns across components\n",[111,2446,2447,2451,2454],{"class":113,"line":214},[111,2448,2450],{"class":2449},"snl16","interface",[111,2452,2453],{"class":125}," ButtonProps",[111,2455,2456],{"class":117}," {\n",[111,2458,2459,2463,2466,2469,2472,2475,2477],{"class":113,"line":211},[111,2460,2462],{"class":2461},"s9osk"," variant",[111,2464,2465],{"class":2449},":",[111,2467,2468],{"class":132}," 'primary'",[111,2470,2471],{"class":2449}," |",[111,2473,2474],{"class":132}," 'secondary'",[111,2476,2471],{"class":2449},[111,2478,2479],{"class":132}," 'ghost'\n",[111,2481,2482,2485,2487,2490,2492,2495,2497],{"class":113,"line":555},[111,2483,2484],{"class":2461}," size",[111,2486,2465],{"class":2449},[111,2488,2489],{"class":132}," 'sm'",[111,2491,2471],{"class":2449},[111,2493,2494],{"class":132}," 'md'",[111,2496,2471],{"class":2449},[111,2498,2499],{"class":132}," 'lg'\n",[111,2501,2502,2505,2508],{"class":113,"line":570},[111,2503,2504],{"class":2461}," disabled",[111,2506,2507],{"class":2449},"?:",[111,2509,2510],{"class":685}," boolean\n",[111,2512,2513,2516,2518],{"class":113,"line":581},[111,2514,2515],{"class":2461}," loading",[111,2517,2507],{"class":2449},[111,2519,2510],{"class":685},[111,2521,2522],{"class":113,"line":231},[111,2523,2524],{"class":117},"}\n",[111,2526,2527],{"class":113,"line":602},[111,2528,2529],{"emptyLinePlaceholder":229},"\n",[111,2531,2532],{"class":113,"line":614},[111,2533,2534],{"class":117},"Interface InputProps {\n",[111,2536,2537,2539,2541,2544,2546,2548,2550,2553],{"class":113,"line":624},[111,2538,2484],{"class":125},[111,2540,564],{"class":117},[111,2542,2543],{"class":132},"'sm'",[111,2545,2471],{"class":2449},[111,2547,2494],{"class":132},[111,2549,2471],{"class":2449},[111,2551,2552],{"class":132}," 'lg'",[111,2554,2555],{"class":536}," // Same scale as Button\n",[111,2557,2558,2560,2562,2565],{"class":113,"line":634},[111,2559,2504],{"class":117},[111,2561,2507],{"class":2449},[111,2563,2564],{"class":117}," boolean ",[111,2566,2567],{"class":536},"// Same name as Button\n",[111,2569,2570,2573,2575],{"class":113,"line":646},[111,2571,2572],{"class":117}," error",[111,2574,2507],{"class":2449},[111,2576,2577],{"class":117}," string\n",[111,2579,2580,2583],{"class":113,"line":656},[111,2581,2582],{"class":125}," modelValue",[111,2584,2585],{"class":117},": string\n",[111,2587,2588],{"class":113,"line":667},[111,2589,2524],{"class":117},[20,2591,2592],{},"The patterns you set in your first five components become the patterns every future component follows. Getting them wrong means either living with inconsistency or doing a breaking API change later.",[20,2594,2595,2596,2599,2600,1724,2603,2606,2607,2610],{},"Compound components — where a parent and children work together — need special attention. A ",[40,2597,2598],{},"Tabs"," component with ",[40,2601,2602],{},"TabList",[40,2604,2605],{},"Tab",", and ",[40,2608,2609],{},"TabPanel"," children requires shared state. In Vue, provide/inject handles this cleanly. The parent provides context, and children inject it. Do not rely on DOM structure assumptions or parent component name checks.",[20,2612,2613,2614,2618],{},"The ",[68,2615,2617],{"href":2616},"/blog/vue-3-composition-api-guide","Composition API"," makes it natural to extract the internal logic of each component into composables. This gives advanced users the ability to build custom UIs on top of your logic — a pattern that dramatically extends your library's useful life.",[15,2620,2622],{"id":2621},"build-tooling-and-tree-shaking","Build Tooling and Tree-Shaking",[20,2624,2625],{},"Your library must be tree-shakeable. If someone imports one button component, they should not get every component in the bundle. This requires specific build configuration.",[20,2627,2628],{},"Use named exports from a barrel file for the public API:",[102,2630,2632],{"className":2435,"code":2631,"language":2437,"meta":107,"style":107},"// src/index.ts\nexport { Button } from './components/Button'\nexport { Input } from './components/Input'\nexport { Select } from './components/Select'\nexport type { ButtonProps, InputProps, SelectProps } from './types'\n",[40,2633,2634,2639,2653,2665,2677],{"__ignoreMap":107},[111,2635,2636],{"class":113,"line":114},[111,2637,2638],{"class":536},"// src/index.ts\n",[111,2640,2641,2644,2647,2650],{"class":113,"line":214},[111,2642,2643],{"class":2449},"export",[111,2645,2646],{"class":117}," { Button } ",[111,2648,2649],{"class":2449},"from",[111,2651,2652],{"class":132}," './components/Button'\n",[111,2654,2655,2657,2660,2662],{"class":113,"line":211},[111,2656,2643],{"class":2449},[111,2658,2659],{"class":117}," { Input } ",[111,2661,2649],{"class":2449},[111,2663,2664],{"class":132}," './components/Input'\n",[111,2666,2667,2669,2672,2674],{"class":113,"line":555},[111,2668,2643],{"class":2449},[111,2670,2671],{"class":117}," { Select } ",[111,2673,2649],{"class":2449},[111,2675,2676],{"class":132}," './components/Select'\n",[111,2678,2679,2681,2684,2687,2689],{"class":113,"line":570},[111,2680,2643],{"class":2449},[111,2682,2683],{"class":2449}," type",[111,2685,2686],{"class":117}," { ButtonProps, InputProps, SelectProps } ",[111,2688,2649],{"class":2449},[111,2690,2691],{"class":132}," './types'\n",[20,2693,2694,2695,2698],{},"Your build tool needs to produce ESM output with preserved module structure. Vite's library mode handles this, but you need to set ",[40,2696,2697],{},"build.rollupOptions.output.preserveModules"," to true. Without it, Rollup bundles everything into a single file and tree-shaking at the consumer level becomes impossible.",[102,2700,2702],{"className":2435,"code":2701,"language":2437,"meta":107,"style":107},"// vite.config.ts for library mode\nexport default defineConfig({\n build: {\n lib: {\n entry: resolve(__dirname, 'src/index.ts'),\n formats: ['es'],\n },\n rollupOptions: {\n external: ['vue'],\n output: {\n preserveModules: true,\n preserveModulesRoot: 'src',\n },\n },\n },\n})\n",[40,2703,2704,2709,2722,2727,2732,2749,2760,2765,2770,2780,2785,2796,2806,2810,2814,2818],{"__ignoreMap":107},[111,2705,2706],{"class":113,"line":114},[111,2707,2708],{"class":536},"// vite.config.ts for library mode\n",[111,2710,2711,2713,2716,2719],{"class":113,"line":214},[111,2712,2643],{"class":2449},[111,2714,2715],{"class":2449}," default",[111,2717,2718],{"class":125}," defineConfig",[111,2720,2721],{"class":117},"({\n",[111,2723,2724],{"class":113,"line":211},[111,2725,2726],{"class":117}," build: {\n",[111,2728,2729],{"class":113,"line":555},[111,2730,2731],{"class":117}," lib: {\n",[111,2733,2734,2737,2740,2743,2746],{"class":113,"line":570},[111,2735,2736],{"class":117}," entry: ",[111,2738,2739],{"class":125},"resolve",[111,2741,2742],{"class":117},"(__dirname, ",[111,2744,2745],{"class":132},"'src/index.ts'",[111,2747,2748],{"class":117},"),\n",[111,2750,2751,2754,2757],{"class":113,"line":581},[111,2752,2753],{"class":117}," formats: [",[111,2755,2756],{"class":132},"'es'",[111,2758,2759],{"class":117},"],\n",[111,2761,2762],{"class":113,"line":231},[111,2763,2764],{"class":117}," },\n",[111,2766,2767],{"class":113,"line":602},[111,2768,2769],{"class":117}," rollupOptions: {\n",[111,2771,2772,2775,2778],{"class":113,"line":614},[111,2773,2774],{"class":117}," external: [",[111,2776,2777],{"class":132},"'vue'",[111,2779,2759],{"class":117},[111,2781,2782],{"class":113,"line":624},[111,2783,2784],{"class":117}," output: {\n",[111,2786,2787,2790,2793],{"class":113,"line":634},[111,2788,2789],{"class":117}," preserveModules: ",[111,2791,2792],{"class":685},"true",[111,2794,2795],{"class":117},",\n",[111,2797,2798,2801,2804],{"class":113,"line":646},[111,2799,2800],{"class":117}," preserveModulesRoot: ",[111,2802,2803],{"class":132},"'src'",[111,2805,2795],{"class":117},[111,2807,2808],{"class":113,"line":656},[111,2809,2764],{"class":117},[111,2811,2812],{"class":113,"line":667},[111,2813,2764],{"class":117},[111,2815,2816],{"class":113,"line":677},[111,2817,2764],{"class":117},[111,2819,2821],{"class":113,"line":2820},16,[111,2822,2823],{"class":117},"})\n",[20,2825,2826],{},"Mark framework dependencies as external. Vue should not be bundled with your library — the consuming application provides it. Same for any peer dependency like Tailwind CSS or a CSS-in-JS runtime.",[20,2828,2829,2830,2833,2834,2837,2838,2841,2842,2845],{},"Type declarations need to ship with the package. Use ",[40,2831,2832],{},"vue-tsc"," to generate ",[40,2835,2836],{},".d.ts"," files that match your component signatures. Without types, your library is a black box for TypeScript users, which is most users in 2025. Ensure your ",[40,2839,2840],{},"package.json"," has the ",[40,2843,2844],{},"types"," field pointing to the declaration entry point.",[15,2847,2849],{"id":2848},"documentation-as-product","Documentation as Product",[20,2851,2852],{},"A component library's documentation is its product. Developers evaluate libraries by reading docs, not source code. If they cannot figure out how to use a component in under a minute, they will use a different library.",[20,2854,2855],{},"The minimum for each component: a basic usage example, a prop table, and a visual rendering of every variant combination. Interactive playgrounds are better than static code blocks because developers can experiment without switching to their editor.",[20,2857,2858],{},"Storybook remains the standard tool for this, but alternatives like Histoire (built for Vue) offer tighter integration with Vue's single-file component format. Whichever tool you choose, the documentation must be deployed and publicly accessible. A README on npm is not enough.",[20,2860,2861,2862,2866],{},"Document the patterns your library recommends for common scenarios. How should users compose a form with your Input, Select, and Button components? What is the recommended way to handle form validation? These integration guides are often more valuable than individual component docs. The ",[68,2863,2865],{"href":2864},"/blog/form-validation-patterns","form validation patterns"," article covers approaches that work well when combined with a component library's form primitives.",[15,2868,2870],{"id":2869},"versioning-and-breaking-changes","Versioning and Breaking Changes",[20,2872,2873],{},"Semantic versioning is non-negotiable for a published library. But the hard part is defining what constitutes a breaking change. Obviously, removing a prop is breaking. But what about changing the default value of a prop? Adding a required prop? Changing the HTML structure of a rendered component in a way that could break CSS selectors?",[20,2875,2876],{},"My rule: if a consumer's code could fail to compile, render differently, or behave differently after updating without changing their code, it is a breaking change. This includes visual changes if your library is used for its visual output, which it almost certainly is.",[20,2878,2879,2880,2883],{},"Use a changelog that is written for humans, not generated from commit messages. \"feat: add loading state to Button\" is fine for a commit. For a changelog entry, write \"Button now accepts a ",[40,2881,2882],{},"loading"," prop that shows a spinner and disables interaction. No changes needed for existing usage.\"",[20,2885,2886],{},"Deprecate before removing. When you need to rename a prop or change an API, add the new API alongside the old one with a console warning in development. Give consumers at least one minor version to migrate before the major version removes the deprecated API. This approach respects the time of every team that depends on your library, which is the difference between a library people trust and one they replace at the first opportunity.",[20,2888,2889,2890,2894],{},"Building a component library is a product development exercise disguised as a technical one. The code matters, but the developer experience around that code — types, docs, versioning discipline, migration guides — is what determines whether anyone uses it. For related thinking on how ",[68,2891,2893],{"href":2892},"/blog/tailwind-css-design-system","design system tokens"," feed into component libraries, that article covers the upstream decisions that shape your components.",[207,2896,2897],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":107,"searchDepth":211,"depth":211,"links":2899},[2900,2901,2902,2903],{"id":2401,"depth":214,"text":2402},{"id":2621,"depth":214,"text":2622},{"id":2848,"depth":214,"text":2849},{"id":2869,"depth":214,"text":2870},"Frontend","2025-11-03","Build and publish a component library — architecture decisions, build tooling, documentation, versioning, and the lessons learned shipping real UI packages.",[2908,2909],"component library development","building UI component library",{},"/blog/component-library-development",{"title":2389,"description":2906},"blog/component-library-development",[2915,2916,1986],"Component Libraries","Vue","fEZnDlsRVAu0d2puAq3fRP94NfEtmrLzTZFCuKpV7HU",[2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513,3514,3515,3516,3517,3518,3519,3520,3521,3522,3523,3524,3525,3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557,3558,3559,3560],{"category":2904},{"category":1152},{"category":427},{"category":219},{"category":2259},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":427},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":1674},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":712},{"category":712},{"category":2259},{"category":2259},{"category":1152},{"category":712},{"category":1152},{"category":1674},{"category":712},{"category":219},{"category":2259},{"category":2969},"DevOps",{"category":427},{"category":1152},{"category":219},{"category":1674},{"category":219},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":2969},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":219},{"category":825},{"category":427},{"category":427},{"category":2259},{"category":1674},{"category":2259},{"category":219},{"category":219},{"category":2259},{"category":219},{"category":1674},{"category":219},{"category":2969},{"category":2969},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":427},{"category":1674},{"category":2259},{"category":2969},{"category":2969},{"category":2969},{"category":1152},{"category":219},{"category":219},{"category":1152},{"category":2904},{"category":427},{"category":2969},{"category":2969},{"category":712},{"category":2969},{"category":2259},{"category":427},{"category":1152},{"category":219},{"category":1152},{"category":1674},{"category":1152},{"category":1674},{"category":712},{"category":1152},{"category":1152},{"category":219},{"category":2259},{"category":219},{"category":2904},{"category":219},{"category":219},{"category":219},{"category":219},{"category":2259},{"category":2259},{"category":1152},{"category":2904},{"category":712},{"category":1674},{"category":712},{"category":2904},{"category":219},{"category":219},{"category":2969},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":2969},{"category":219},{"category":219},{"category":1152},{"category":1152},{"category":712},{"category":1674},{"category":1674},{"category":825},{"category":825},{"category":825},{"category":2259},{"category":219},{"category":2969},{"category":1674},{"category":1152},{"category":1152},{"category":2969},{"category":1674},{"category":1674},{"category":2904},{"category":219},{"category":1152},{"category":1152},{"category":219},{"category":1152},{"category":2969},{"category":2969},{"category":1152},{"category":712},{"category":1152},{"category":1674},{"category":712},{"category":1674},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":712},{"category":219},{"category":2969},{"category":2969},{"category":2259},{"category":219},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":1674},{"category":1674},{"category":1674},{"category":219},{"category":1152},{"category":1152},{"category":1152},{"category":2969},{"category":2259},{"category":1152},{"category":1152},{"category":219},{"category":1152},{"category":219},{"category":2904},{"category":1152},{"category":2259},{"category":2259},{"category":219},{"category":219},{"category":427},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":219},{"category":2969},{"category":2969},{"category":2969},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":1152},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":2259},{"category":2259},{"category":1152},{"category":219},{"category":2904},{"category":1674},{"category":825},{"category":1152},{"category":1152},{"category":712},{"category":219},{"category":1152},{"category":1152},{"category":2969},{"category":1152},{"category":2904},{"category":2969},{"category":2969},{"category":712},{"category":219},{"category":219},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":825},{"category":1152},{"category":1674},{"category":219},{"category":219},{"category":1152},{"category":2969},{"category":1152},{"category":1152},{"category":1152},{"category":2904},{"category":1152},{"category":1152},{"category":219},{"category":1152},{"category":219},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":427},{"category":427},{"category":219},{"category":1152},{"category":2969},{"category":2969},{"category":1152},{"category":219},{"category":1152},{"category":1152},{"category":427},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":219},{"category":712},{"category":219},{"category":219},{"category":2904},{"category":219},{"category":2904},{"category":2904},{"category":712},{"category":1674},{"category":219},{"category":1674},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":219},{"category":2259},{"category":219},{"category":219},{"category":1152},{"category":1674},{"category":427},{"category":427},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":2259},{"category":219},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":2904},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":219},{"category":1674},{"category":1152},{"category":2259},{"category":427},{"category":1152},{"category":2259},{"category":712},{"category":1152},{"category":712},{"category":219},{"category":2969},{"category":1152},{"category":1152},{"category":219},{"category":1152},{"category":1674},{"category":1152},{"category":1152},{"category":219},{"category":2259},{"category":219},{"category":219},{"category":219},{"category":219},{"category":2259},{"category":219},{"category":219},{"category":2259},{"category":2969},{"category":219},{"category":427},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":1152},{"category":1152},{"category":1152},{"category":427},{"category":219},{"category":219},{"category":1674},{"category":2904},{"category":219},{"category":1152},{"category":219},{"category":1674},{"category":2259},{"category":2259},{"category":2904},{"category":2904},{"category":1152},{"category":2259},{"category":712},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":219},{"category":219},{"category":1674},{"category":219},{"category":219},{"category":219},{"category":3391},"Programming",{"category":219},{"category":219},{"category":1674},{"category":1674},{"category":219},{"category":219},{"category":2259},{"category":712},{"category":219},{"category":2259},{"category":219},{"category":219},{"category":219},{"category":219},{"category":2969},{"category":1674},{"category":2259},{"category":2259},{"category":219},{"category":219},{"category":2259},{"category":219},{"category":712},{"category":2259},{"category":219},{"category":219},{"category":1674},{"category":1674},{"category":1152},{"category":2259},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":2904},{"category":1152},{"category":2969},{"category":712},{"category":712},{"category":712},{"category":712},{"category":712},{"category":712},{"category":1152},{"category":219},{"category":2969},{"category":1674},{"category":2969},{"category":1674},{"category":219},{"category":2904},{"category":1152},{"category":1674},{"category":2904},{"category":1152},{"category":1152},{"category":1152},{"category":1674},{"category":1674},{"category":1674},{"category":2259},{"category":2259},{"category":2259},{"category":1674},{"category":1674},{"category":2259},{"category":2259},{"category":2259},{"category":1152},{"category":712},{"category":219},{"category":2969},{"category":219},{"category":1152},{"category":2259},{"category":2259},{"category":1152},{"category":1152},{"category":1674},{"category":219},{"category":1674},{"category":1674},{"category":1674},{"category":2904},{"category":219},{"category":1152},{"category":1152},{"category":2259},{"category":2259},{"category":1674},{"category":219},{"category":825},{"category":1674},{"category":825},{"category":2259},{"category":1152},{"category":1674},{"category":1152},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":1152},{"category":427},{"category":427},{"category":2969},{"category":1152},{"category":1152},{"category":1152},{"category":1152},{"category":219},{"category":219},{"category":2904},{"category":219},{"category":712},{"category":1674},{"category":2904},{"category":2904},{"category":219},{"category":219},{"category":2904},{"category":2904},{"category":2904},{"category":712},{"category":219},{"category":219},{"category":2259},{"category":219},{"category":1674},{"category":1152},{"category":1152},{"category":1674},{"category":1152},{"category":1152},{"category":1674},{"category":1152},{"category":219},{"category":1152},{"category":712},{"category":1152},{"category":1152},{"category":1152},{"category":2969},{"category":2969},{"category":712},1772951194636]