{"version":"https://jsonfeed.org/version/1.1","title":"Dawit","home_page_url":"https://blog.dawit.dev/","feed_url":"https://blog.dawit.dev/feed.json","items":[{"id":"https://blog.dawit.dev/blog/shipping-search-without-a-server/","url":"https://blog.dawit.dev/blog/shipping-search-without-a-server/","title":"Shipping search without a server","summary":"Part three of building this blog — full-text search from a static index with Pagefind, costing nothing at rest and loading only on demand.","content_text":"Parts [one](/blog/designing-a-content-model-that-cannot-break/) and [two](/blog/seo-without-a-marketing-team/) built the content model and the discovery layer. The remaining reader-facing gap was search — the feature that traditionally ends the \"no server\" streak. It doesn't have to. The claim of this part: for a content site, a pre-built static search index beats a search backend on every axis that matters here, and it costs exactly nothing at rest.\n\n## Search is an index, not a service\n\nThe mental unlock is that search has two phases, and only one of them needs to happen when the reader types. *Indexing* — tokenizing text, building the lookup structures — can happen at build time, because a static site's content only changes at build time. Running an always-on service to re-answer a question whose inputs change weekly is the same mistake as [rendering the same article on every request](/blog/why-static-sites-win/).\n\nPagefind implements this cleanly. After the site builds, it crawls the finished HTML and writes a fragmented index alongside the static files. The fragments are the clever part: the browser fetches only the pieces relevant to the query — a few tens of kilobytes for a typical search — rather than downloading the whole index. No server, no per-query cost, no third-party service holding the feature hostage.\n\n## Paying for search only when it's used\n\nThe performance rule for a feature like this: readers who don't use it must not pay for it. Most article visits never touch search, so the [page-weight budget](/blog/performance-budgets-are-design/) should not carry it by default.\n\nImplementation is a modal that lazy-loads everything on first open. Until then, search costs a button. After first use, the library is cached and subsequent queries are instant. The search UI itself is the page's one substantial piece of JavaScript, mounted idle so it never competes with rendering the article — and with a keyboard shortcut, because returning readers are the ones who search.\n\nTwo details earn their keep. Only the article body gets indexed — navigation and footer text are excluded, so searching \"archive\" doesn't return every page on the site. And the no-JavaScript path still works: the search page renders the full article list, so content remains reachable when the enhancement doesn't load.\n\n## The empty state is interface copy\n\nA search box with no results is a fork: \"No results\" is a dead end, while \"try a topic name or a phrase from an article\" is a direction. On a small site this matters more, not less — queries miss more often against a few dozen articles than against a million, so the recovery path *is* the feature. Same principle as a 404 page: never leave a reader nowhere.\n\n## The series so far\n\nThree parts in, the pattern behind every decision is visible: **move work to build time, keep the reader's path free of moving parts.** Validation happens at build. Metadata is generated at build. Search is indexed at build. What ships is inert files — fast because there is nothing left to be slow, reliable because there is nothing left to fail.\n\nThe series continues as the site grows. Next candidates: the newsletter pipeline, and what a year of Core Web Vitals field data says about these choices. If you want the next part when it lands, [subscribe](/subscribe/) — or watch the feed.","date_published":"2026-08-07T00:00:00.000Z","date_modified":"2026-08-07T00:00:00.000Z","tags":["search","pagefind","performance"]},{"id":"https://blog.dawit.dev/blog/seo-without-a-marketing-team/","url":"https://blog.dawit.dev/blog/seo-without-a-marketing-team/","title":"SEO without a marketing team","summary":"Part two of building this blog — the complete technical SEO layer for a personal site, and the inputs that actually move rankings.","content_text":"Part one built [a content model that cannot break](/blog/designing-a-content-model-that-cannot-break/). This part builds the layer that makes the content findable. The thesis: for a personal site, technical SEO is a finite, completable checklist — not a dark art — and once it is built into the templates, ongoing \"SEO work\" reduces to writing things people want to read.\n\n## What search engines actually need\n\nStrip away the industry noise and a crawler needs a small set of things from a blog:\n\n**Complete HTML without JavaScript.** The article, its headings, and its links must exist in the initial response. A static build satisfies this by construction.\n\n**One canonical URL per page.** Every page declares its own authoritative URL, so `?utm_source=` variants and stray query strings collapse into one entry in the index instead of competing as duplicates.\n\n**Honest metadata.** A unique title under ~60 characters and a description under ~155 — which our schema from part one enforces as types, so this bullet is already impossible to get wrong.\n\n**Structured data.** A `BlogPosting` JSON-LD block per article — headline, dates, author, word count — plus `Person` on the about page and `BreadcrumbList` on articles. This is how a crawler stops parsing your HTML heuristically and starts reading facts.\n\n**A map and a feed.** A sitemap index with accurate modification dates, a robots.txt pointing at it, and full-content RSS and JSON feeds. Feeds are not nostalgia; they are how your most loyal readers and half the syndication ecosystem consume you.\n\nEvery item on that list is template work. Build it once, and article number two hundred gets it identically to article number one.\n\n## The parts people over-build\n\nTwo temptations deserve naming. First, **fabricated schema** — inventing an `Organization` publisher for a one-person site because it looks more serious. Search engines' guidelines penalize markup that misrepresents reality; a personal blog's author and publisher are the same person, and saying so is both accurate and fine.\n\nSecond, **index bloat as ambition** — letting every tag page, every paginated shell, every thin variant into the sitemap on the theory that more indexed pages means more traffic. It means the opposite: crawl budget spent on pages with nothing on them, competing with your real articles. Thin pages get `noindex`; the sitemap lists things worth ranking.\n\n## What actually moves rankings\n\nThe honest hierarchy, having watched this work: **content that answers a real query** dwarfs everything else. Then internal linking — every article linking to related ones, in context, so both crawlers and readers can walk the graph. Then [page speed](/blog/measuring-core-web-vitals/), a genuine ranking signal, which our architecture gets nearly free. The technical layer's job is mostly to *not lose* rankings the writing earned — a losable game you make unlosable by template.\n\nThat framing is liberating for a solo writer. There is no ongoing SEO grind. There is a one-time engineering task, a schema that keeps metadata honest, and then the actual work: writing, weekly, about a small set of subjects until depth accumulates into authority.\n\n## Verifying it\n\nBefore calling the layer done: run key pages through a structured-data validator, submit the sitemap in Search Console, fetch a page with JavaScript disabled and confirm the article is fully present, and load the RSS feed in a real reader. Fifteen minutes, once — then the layer is invisible until [part three](/blog/shipping-search-without-a-server/), where we make the site searchable without adding a server.","date_published":"2026-07-31T00:00:00.000Z","date_modified":"2026-07-31T00:00:00.000Z","tags":["seo","structured-data","architecture"]},{"id":"https://blog.dawit.dev/blog/designing-a-content-model-that-cannot-break/","url":"https://blog.dawit.dev/blog/designing-a-content-model-that-cannot-break/","title":"Designing a content model that cannot break","summary":"Part one of building this blog — a Zod-validated content schema that makes broken articles impossible to publish, not merely discouraged.","content_text":"import { Callout } from '@/components/mdx';\n\nThis series documents building the site you are reading, one decision at a time. The first decision — before layout, before a single color — was the content model, and the principle behind it: **every editorial rule that can be enforced by the compiler should be**. Discipline is a finite resource. Schemas are not.\n\n## Rules as types, not reminders\n\nA blog accumulates editorial rules: descriptions must fit a search snippet, every image needs alt text, series parts need an order. Enforced by memory, each rule holds until a tired evening. Enforced by schema, each rule holds forever, because violating it fails the build with a filename and a field.\n\nAstro's content collections make this nearly free — articles are Markdown files, and each one's frontmatter is validated against a Zod schema at build time. The interesting part is what the bounds encode:\n\n```ts\nconst blog = defineCollection({\n  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),\n  schema: ({ image }) =>\n    z.object({\n      title: z.string().min(10).max(70),        // fits a search result\n      description: z.string().min(50).max(155), // fits a meta description\n      pubDate: z.coerce.date(),\n      topic: reference('topics'),               // must actually exist\n      tags: z.array(z.string()).max(6),\n      heroImage: image().optional(),\n      heroAlt: z.string().min(5).optional(),\n      draft: z.boolean().default(true),         // safe by default\n    })\n    .refine((d) => !d.heroImage || !!d.heroAlt, {\n      message: 'heroAlt is required when heroImage is set',\n    }),\n});\n```\n\nThat `description` bound is not a style preference. It is an SEO requirement — the length a search engine will actually display — promoted into a type. It is now *structurally impossible* for me to publish an article with a missing or truncated meta description. The refinement below it does the same for accessibility: a hero image without alt text is not a warning, it is a build failure.\n\n## Three choices worth stealing\n\n**`draft: true` by default.** A new file is a draft until explicitly told otherwise. The failure mode this prevents — half-written thoughts going live because a default leaned the wrong way — is rare, but its cost is public.\n\n**References, not strings.** `topic` is a reference to a real topics collection, so a typo like `enginering` fails the build instead of silently creating an orphan page. Referential integrity is not just for databases.\n\n**One query helper, used everywhere.** Every page pulls articles through a single function that filters drafts and future-dated posts. Filtering inline at each call site is how a draft eventually leaks into a feed — centralizing it is the [boring, obvious choice](/blog/choosing-boring-technology/), which is exactly why it works.\n\n<Callout type=\"note\" title=\"Slugs are forever\">\nThe filename is the URL, and the URL is the one thing on a site you cannot cheaply change after search engines index it. Choose slugs as if they are permanent, because they are — a rename means a 301 redirect, maintained forever.\n</Callout>\n\n## What this buys at write time\n\nThe payoff arrives every publishing day. Writing happens in any editor, against a schema that catches mistakes at build rather than after deploy. There is no admin panel to maintain and [no server behind it](/blog/why-static-sites-win/) — the whole CMS is a folder of files and a contract.\n\nWith the model fixed, the next problem is making the site discoverable: titles, canonicals, structured data, feeds — the invisible layer that decides whether anyone finds the writing. That is [part two](/blog/seo-without-a-marketing-team/).","date_published":"2026-07-24T00:00:00.000Z","date_modified":"2026-07-24T00:00:00.000Z","tags":["architecture","astro","zod","content-modeling"]},{"id":"https://blog.dawit.dev/blog/choosing-boring-technology/","url":"https://blog.dawit.dev/blog/choosing-boring-technology/","title":"Choosing boring technology on purpose","summary":"Novelty in your stack is a loan against future maintenance. Why proven tools win for solo builders, and where the innovation budget should go.","content_text":"Every technology choice is a loan. Exciting technology borrows heavily: you get capability now and repay in debugging sessions, migration weekends, and abandoned-project graveyards later. My position is that a solo builder should spend novelty in exactly one place — the thing that makes the project distinctive — and choose aggressively boring technology everywhere else.\n\n\"Boring\" here is a term of praise. It means the failure modes are documented, the answers are on the first page of search results, and the tool will still exist, in recognizable form, in five years.\n\n## The real cost is total cost of ownership\n\nComparing technologies at adoption time measures the wrong week. The exciting option usually *is* better in week one — that is why it is exciting. The comparison that matters covers the years after: every hour spent fighting a leaky abstraction, chasing a breaking change across a major version, or migrating off something abandoned, is an hour not spent on the product.\n\nThis asymmetry is brutal for one-person projects specifically. A team amortizes the cost of a finicky tool across specialists; a solo builder absorbs all of it, on nights and weekends, in the exact hours that were supposed to produce the work itself. The [quiet-year test](/blog/why-static-sites-win/) applies to stacks as much as servers: what does this choice cost during a year when I touch nothing? Boring technology's answer is \"nothing.\" Novel technology's answer is \"you'll find out.\"\n\n## An innovation budget, spent deliberately\n\nThe useful framing is a budget: you get a small number of deliberate bets on new technology per project — one is a good number — and everything else defaults to proven.\n\nSpend the bet where it compounds. If the project's distinctiveness is its reading experience, spend novelty there and keep the deployment pipeline primitive. If the product is the pipeline, invert it. What is never worth the bet is infrastructure adjacent to the actual work: the newest database for a site with no queries, the experimental bundler for a site that could be plain files. Every dependency, however small, is a future migration — the same accounting that [performance budgets](/blog/performance-budgets-are-design/) apply to kilobytes applies to `package.json` lines.\n\n## Boring is not stagnant\n\nThe common objection is that this reasoning would have kept us all writing CGI scripts. It misunderstands the claim. Technologies graduate into boring: they survive their hype cycle, accumulate a decade of answered questions, and become safe defaults — that is the *goal* of the pipeline, not a refusal to use it. Choosing boring technology means preferring graduates, not refusing progress.\n\nIt also does not mean refusing to learn. Learn the new thing on a toy; bet the project on it after it has survived contact with other people's production systems for a few years. The trick is not confusing the two activities, because the toy is allowed to die and the project is not.\n\n## The test I actually use\n\nBefore adopting anything, one question: **when this breaks at the worst possible time, what happens?** For boring technology the answer is \"search the error, find the answer, apply it, done in an hour.\" For exciting technology it is \"read the source, file an issue, wait.\" Both are survivable. Only one is survivable *repeatedly*, by one person, while also trying to ship.\n\nExcitement belongs in the work. The stack's job is to be forgettable — and the highest compliment a tool can earn is that you stopped thinking about it years ago.","date_published":"2026-07-17T00:00:00.000Z","date_modified":"2026-07-17T00:00:00.000Z","tags":["architecture","tooling","process"]},{"id":"https://blog.dawit.dev/blog/the-discipline-of-removing-things/","url":"https://blog.dawit.dev/blog/the-discipline-of-removing-things/","title":"The discipline of removing things","summary":"Good interfaces are built by subtraction. A practical method for deleting UI, and why every element must pay rent in reader attention.","content_text":"Design maturity is not learning to add better things. It is learning to remove things, systematically, on a schedule, against the resistance of your own past decisions. That is the thesis, and it is less obvious than it sounds, because addition is what everyone rewards — additions demo well, appear in changelogs, and feel like progress. Removal is invisible. It is also where the quality is.\n\n## Attention is a budget you don't control\n\nA reader arrives with a fixed and fairly small amount of attention, and every element on the page draws from it. Not just the loud things — every border, every secondary widget, every \"related\" module and social button consumes a little processing before the reader's eye settles on what they came for. The sidebar does not have to be looked at to cost something; it has to be *not looked at*, and that filtering is work.\n\nThis is why cluttered sites feel tiring before a single word is read, and why the same article feels smarter on a quiet page. The words did not change. The tax on reading them did — the same tax that makes [typography the interface](/blog/typography-is-the-interface/) rather than a finish applied to one.\n\n## A method, not a mood\n\n\"Keep it minimal\" is a mood, and moods lose arguments. What works is a procedure with a burden of proof:\n\n**Every element answers one question: what breaks if this is gone?** Not \"is this nice\" — most things are somewhat nice — but what specifically fails. If the answer is vague (\"engagement, probably?\"), remove it and watch. In my experience the metric that was supposed to collapse almost never moves, which tells you what the element was actually doing: nothing, expensively.\n\n**Removal gets a recurring slot.** Additions happen continuously under deadline pressure; subtraction only happens if scheduled. Once a quarter, walk every page with the question inverted — not \"what should we add\" but \"what has stopped paying rent.\" Interfaces accrete the way [dependency lists do](/blog/choosing-boring-technology/), and the countermeasure is the same: a periodic audit with the default set to *delete*.\n\n**One thing per screen gets to be loud.** Emphasis is zero-sum — when everything is bold, colored, or boxed, nothing is. Pick the single element each view exists for and let everything else recede. If you cannot name the one thing, the screen has a strategy problem that no styling will fix.\n\n## What survives subtraction\n\nRun a blog through this method honestly and the survivors are few: the article, navigation enough to orient, a way to search, one clear next step at the end, an author page. Dates and reading time survive — they answer real questions readers actually have. The tag cloud does not. The three competing calls to action at the bottom collapse into one. The share buttons shrink into plain links.\n\nNotice that nothing essential was lost, and something was gained: the elements that remain each carry more weight, because they no longer compete. Restraint is not the absence of design. It is design concentrated.\n\n## The hard part is the second year\n\nAnyone can launch a clean page. The discipline is keeping it clean when every month offers a reasonable-sounding addition — a banner for the new series, a badge, a widget someone genuinely liked. Each is defensible alone. Their sum is the cluttered site you started by refusing to build.\n\nSo the real practice is a habit of saying *not yet* by default, and a calendar reminder to delete. The best interface for reading is mostly an absence, carefully maintained — and maintenance, not taste, is what it costs.","date_published":"2026-07-10T00:00:00.000Z","date_modified":"2026-07-10T00:00:00.000Z","tags":["minimalism","process","ux"]},{"id":"https://blog.dawit.dev/blog/typography-is-the-interface/","url":"https://blog.dawit.dev/blog/typography-is-the-interface/","title":"Typography is the interface","summary":"On a text site, type is not part of the design — it is the design. The four decisions that matter and the defaults that quietly sabotage reading.","content_text":"On a site whose product is writing, typography is not a layer of polish over the interface. It *is* the interface. A reader's opinion of your ideas is formed while their eyes move through your paragraphs, and everything about that movement — its speed, its comfort, its rhythm — is typography. My claim: get four decisions right and the design is mostly done; get them wrong and no amount of decoration recovers it.\n\n## Measure: the width of a line\n\nThe single highest-leverage decision is line length. Body text should run 60–75 characters per line. Shorter, and the eye jumps back before finding a rhythm; longer, and the return sweep to the next line loses its place, producing the re-read-the-same-line stutter every reader knows and nobody names.\n\nImplementation is one line — `max-width: 68ch` on the text column — plus one rule of discipline: the measure is enforced on the *column*, never the page. On a wide monitor the margins grow; the text never does. Most \"modern\" redesigns that feel subtly exhausting to read are full-width text on a 27-inch display, a 200-character measure nobody would ever typeset on paper.\n\n## Size and leading: air for long-form\n\nBrowser-default 16 px is a UI convention, not a reading recommendation. Long-form body text reads comfortably at 17–20 px, and the difference over three thousand words is the difference between finishing and drifting off. Pair it with line-height around 1.7 — noticeably more than the 1.5 that suits buttons and labels — because paragraphs need vertical air that interface chrome does not.\n\nHeadings invert the rule: large type needs *less* leading, around 1.1–1.3, or the lines of a wrapped heading float apart into separate thoughts. Using one line-height token everywhere guarantees it is wrong somewhere.\n\n## Rhythm: space that means something\n\nVertical spacing is doing semantic work, whether you intend it or not. The space above a heading should be two to three times the space below it — the heading must visibly belong to the section it introduces, not dangle equidistant between two. Reversing this ratio is possibly the most common typographic error on the web, and the fix costs nothing but attention.\n\nThe same principle scales down. Derive every gap from one spacing scale and paragraphs, lists, and figures fall into a rhythm the reader feels as calm without ever noticing why. Arbitrary pixel values, each reasonable alone, accumulate into low-grade visual noise — the kind of clutter [the discipline of removing things](/blog/the-discipline-of-removing-things/) exists to catch.\n\n## The details that cost two lines of CSS\n\nModern CSS quietly solved several old miseries, and adopting the fixes is nearly free. `text-wrap: balance` on headings ends the single orphaned word on the second line. `text-wrap: pretty` on body text prevents paragraph-ending orphans. An underline offset keeps link underlines from slicing through descenders. Real typographic characters — curly quotes, em dashes — come free from a Markdown plugin and signal care at a level readers register without articulating.\n\nNone of these are visible as features. All of them are visible as quality.\n\n## Performance is a typographic property\n\nFonts sit on the critical rendering path, so the type system and [the performance budget](/blog/performance-budgets-are-design/) are the same conversation. Self-host the files, subset them to the characters you use, preload the one the title renders in, and let text show in a fallback while fonts load — invisible text during load is a typographic failure as real as bad kerning. A latin subset lands well under half the weight of the full file, which on a phone is felt directly in [Largest Contentful Paint](/blog/measuring-core-web-vitals/).\n\nBudget honestly and you will discover something clarifying: you can afford about two font files. Spent well — one distinctive display face, one hardworking body face — that is not a limitation. It is the whole identity, and on a text site it does more than a logo ever will.","date_published":"2026-07-03T00:00:00.000Z","date_modified":"2026-07-03T00:00:00.000Z","tags":["typography","readability","css"]},{"id":"https://blog.dawit.dev/blog/performance-budgets-are-design/","url":"https://blog.dawit.dev/blog/performance-budgets-are-design/","title":"Performance budgets are a design decision","summary":"A page-weight budget set before the first mockup changes what gets designed at all. Treating speed as engineering cleanup is why sites are slow.","content_text":"Here is the position: a performance budget belongs at the start of the design process, stated in kilobytes and milliseconds, agreed before anyone opens a design tool. Set there, it shapes what gets designed. Set anywhere later, it becomes a cleanup exercise that always loses to the calendar.\n\n## Why \"optimize it later\" structurally fails\n\nBy the time a slow site reaches an engineer with instructions to make it fast, the expensive decisions are already embedded: the design language needs four font weights, the hero concept is a video, the analytics suite was promised to marketing. Optimization at that stage is negotiating each decision backwards against people who have already shipped their part. You win small concessions and lose the war.\n\nA budget agreed up front inverts this. \"The article page ships under 200 KB\" is a sentence a designer can work inside, the same way they work inside a brand palette. The four-weight typography concept never gets designed, because it never fit. Nobody has to be the villain later.\n\n## A budget with teeth\n\nNumbers vary by product; the structure does not. Mine, for this site:\n\n| Resource | Budget |\n|---|---|\n| HTML, gzipped | ~15 KB |\n| CSS, gzipped | ~12 KB |\n| JavaScript on an article page | < 15 KB |\n| Hero image | ~80 KB |\n| Fonts, total | ~40 KB |\n| **Whole page** | **< 200 KB** |\n\nTwo properties make a budget real rather than aspirational. First, it is **per-resource**, so an overrun points at a culprit instead of a vague total. Second, it is **enforced by a machine**: continuous integration runs Lighthouse with the thresholds set as errors, and a pull request that busts the budget does not merge. The build failing is the feature. A budget that relies on someone remembering to check is, as I argued in [measuring Core Web Vitals without fooling yourself](/blog/measuring-core-web-vitals/), a number that drifts until it is fiction.\n\n## Budgets generate better design, not less of it\n\nThe predictable objection is that budgets constrain creativity. The observable reality is the opposite — constraint is where the craft shows up:\n\n- **Typography gets sharper.** Two font files force a real decision about what the type system is doing, and [a considered type system](/blog/typography-is-the-interface/) beats a sprawling one on identity as well as bytes.\n- **Imagery gets intentional.** When a hero image costs a third of the page budget, it has to earn its place. Decoration dies; meaning survives.\n- **Features justify themselves.** Every embedded widget arrives with its cost printed on the box. Most of them, examined this way, were never worth it — the same conclusion [the discipline of removing things](/blog/the-discipline-of-removing-things/) reaches from the aesthetic side.\n\nNone of this produces austere pages. It produces pages where everything present was chosen.\n\n## The reader is the reason\n\nIt is worth keeping the human stakes in view. A 3 MB page on an unlimited fibre connection is an abstraction problem. The same page on metered mobile data is a small tax, levied on the reader, for the privilege of reading you — and on a mid-range phone it is also seconds of parse and layout time before the first word renders. Readers do not file complaints about this. They leave.\n\nA budget is the design process taking those readers seriously from the first sketch. Set one number for the whole page, split it across resources, wire it into CI so it cannot erode quietly, and let it push back on ideas while they are still cheap to change. Speed is not a phase of the project. It is a property of the design, and properties are decided at the start.","date_published":"2026-06-26T00:00:00.000Z","date_modified":"2026-06-26T00:00:00.000Z","tags":["performance","budgets","process"]},{"id":"https://blog.dawit.dev/blog/measuring-core-web-vitals/","url":"https://blog.dawit.dev/blog/measuring-core-web-vitals/","title":"Measuring Core Web Vitals without fooling yourself","summary":"Lab scores lie to you politely. How to measure LCP, INP, and CLS in ways that reflect what readers on real phones actually experience.","content_text":"The most common performance mistake is not slow code. It is measuring on your own machine, seeing a good number, and believing it. My thesis: a Lighthouse score collected on a developer laptop is a compliment, not a measurement, and treating it as ground truth is how fast-looking sites end up slow for the people who matter.\n\n## The three numbers, in reader terms\n\nCore Web Vitals compress the reading experience into three metrics, and each maps to a moment a reader actually feels:\n\n- **LCP — Largest Contentful Paint.** How long until the main thing is visible. For an article, that is the title or the hero image. Target: under 2.5 seconds; under 1.8 if you are serious.\n- **INP — Interaction to Next Paint.** When the reader taps something, how long before the page visibly responds. Above ~200 ms, the interface feels like it is ignoring them.\n- **CLS — Cumulative Layout Shift.** How much the page jumps around while loading. The paragraph you were reading leaping downward because an image finally arrived is CLS, and readers experience it as hostility.\n\n## Lab data versus field data\n\nLab tools — Lighthouse, WebPageTest — run your page in a controlled environment. Field data is what real visitors' browsers report back through the Chrome UX Report. The two disagree constantly, and when they do, **the field is right**, because the field is the actual readers.\n\nThe gap has a shape: your lab machine has a fast CPU, a warm cache, and an unmetered connection. Your median reader — as I argued in [why static sites win](/blog/why-static-sites-win/) — is on a mid-range Android phone on a cellular connection. A page that scores 99 on an M-series laptop can take six seconds to paint on a $200 phone. Same page. Both numbers true. Only one of them matters.\n\n## Measuring honestly\n\nThree practices close the gap between what you measure and what readers get:\n\n### Throttle like you mean it\n\nRun Lighthouse with mobile CPU throttling *on* and treat the throttled number as the real one. Better, test on an actual cheap Android device over a cellular connection at least once per redesign. The device emulator in desktop Chrome underestimates both CPU and network pain — it simulates a slow network on a fast computer, which is exactly the combination readers do not have.\n\n### Watch the field, not the snapshot\n\nSearch Console's Core Web Vitals report and the CrUX dashboard show the 75th-percentile experience of your actual traffic over 28 days. This is the number Google uses as a ranking signal, and it is the number that reflects reality. Check it monthly. A regression there is a real regression even if every lab run looks clean.\n\n### Gate in CI so regressions can't land quietly\n\nPerformance decays by accumulation — one more script, one unsized image at a time. The countermeasure is a hard threshold in continuous integration that fails the build when a budget is exceeded, which is the subject of [performance budgets are a design decision](/blog/performance-budgets-are-design/). A budget that only produces warnings is a budget that will be exceeded.\n\n## What usually moves the numbers\n\nHaving profiled more pages than I can count, the fixes cluster with boring regularity. For LCP: the hero image is too large, the wrong format, or not prioritized — fix the format, size it responsively, mark it high priority, and preload the font the title renders in. For CLS: something has no reserved space — give every image and embed explicit dimensions. For INP: too much JavaScript runs on the main thread — ship less of it.\n\nThat last one deserves its own sentence. The most reliable INP optimization on the modern web is not clever scheduling; it is not sending the JavaScript at all.\n\n## The habit that matters\n\nMeasure where your readers are, not where you are. Once a month, open the field data, and once a quarter, hold a cheap phone in your hand and load your own site on it. Everything else in performance work follows from refusing to be flattered by your own hardware.","date_published":"2026-06-19T00:00:00.000Z","date_modified":"2026-06-19T00:00:00.000Z","tags":["performance","web-vitals","measurement"]},{"id":"https://blog.dawit.dev/blog/why-static-sites-win/","url":"https://blog.dawit.dev/blog/why-static-sites-win/","title":"Why static sites win for long-form writing","summary":"For a single-author publication, static generation beats every server-rendered alternative on speed, cost, security, and survival. Here's the case.","content_text":"import { Callout } from '@/components/mdx';\n\nMy thesis is simple: if you are one person publishing written articles, a statically generated site is not one reasonable option among several — it is the correct default, and every alternative should have to argue its way past it.\n\nI say this having built the other kinds. I have shipped server-rendered apps with databases, caching layers, and background jobs, and I will again — for products that need them. A blog does not need them. A blog is a set of documents that change when the author changes them, which for most of us is at most weekly. Rendering the same document on every request, forever, is work a computer does because nobody told it to stop.\n\n## The reader's case: speed is the whole experience\n\nA static page is HTML sitting on a CDN edge node near the reader. There is no database round-trip, no template rendering, no cold start. Time to first byte is measured in tens of milliseconds, and the [Core Web Vitals](/blog/measuring-core-web-vitals/) targets that most teams sweat over become almost hard to miss.\n\nThis matters more than it looks like it should. Most article traffic arrives from search, on phones, often on connections much worse than the one you built the site on. The reader has never heard of you and owes you nothing. Every hundred milliseconds you spend is spent from an account with a low balance and no overdraft.\n\n## The author's case: nothing to babysit\n\nThe strongest argument for static sites has nothing to do with readers. It is what *isn't* there:\n\n- **No server to patch.** There is no process running at 3 a.m. that can crash, leak memory, or be found by a vulnerability scanner.\n- **No database to back up.** The content is files in a Git repository. The backup story is `git push`, and the version history is better than anything a CMS offers.\n- **No bill that scales with attention.** When an article gets ten thousand readers in an hour, a CDN shrugs. A $5 VPS does not.\n\n<Callout type=\"tip\" title=\"The quiet-year test\">\nAsk of any architecture: what does it cost during a year when I publish nothing? A static site's answer is roughly the price of a domain. Every other answer is a reason blogs die.\n</Callout>\n\nA publication owned by one person has to survive that person's busy months. The systems that survive are the ones that require nothing when you are not looking at them. This is the same instinct behind [choosing boring technology](/blog/choosing-boring-technology/): total cost of ownership is dominated by the years after launch, not the weeks before it.\n\n## The machine's case: crawlers get finished HTML\n\nSearch engines, social preview bots, and feed readers all consume your site as text. A static build hands them complete HTML with every heading, link, and structured-data block already in place — no JavaScript execution required, no rendering queue, no ambiguity. Client-rendered sites make Google do extra work and make lesser crawlers fail outright.\n\nFor a publication whose growth strategy is organic search, this is not a nice-to-have. It is the strategy.\n\n## What you give up, honestly\n\nStatic generation is not free of trade-offs, and pretending otherwise is how people get burned:\n\n**Dynamic features need a plan.** Search, comments, and forms all need to be solved without a server. In practice each has a good static-era answer — a pre-built search index, a comments widget backed by an external store, a form that posts to a provider — but each is a decision, not a default.\n\n**Builds are a step.** Publishing means running a build. With a Git-connected host this is invisible — push, wait ninety seconds, live — but it is still a pipeline that can fail, and you want it to fail loudly before deploy rather than silently after.\n\n**Truly per-user content is out of scope.** If your site needs accounts, personalization, or payments, you have left blog territory and should architect accordingly. The mistake is not needing a server; the mistake is carrying one for years before you need it.\n\n## The decision, stated plainly\n\nEvery dynamic-looking feature a blog needs can be resolved at build time or pushed to a small, isolated island of JavaScript. What remains is a site that is faster than what a team of engineers maintains at most companies, costs about as much as a coffee per year, and cannot be hacked in any interesting way because there is nothing running.\n\nOne person, writing, on a system that never pages you. That is the entire pitch, and for this class of site I have never seen it lose on the merits.","date_published":"2026-06-12T00:00:00.000Z","date_modified":"2026-06-12T00:00:00.000Z","tags":["architecture","static-sites","astro"]}]}