<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Journal Blog</title>
    <link>https://boramriganka.github.io/get_news_app/blog</link>
    <description>Essays on engineering, design, and the craft of building for the web.</description>
    <language>en-us</language>
    <lastBuildDate>Sun, 14 Jun 2026 18:07:29 GMT</lastBuildDate>
    <atom:link href="https://boramriganka.github.io/get_news_app/rss.xml" rel="self" type="application/rss+xml"/>
    <managingEditor>blog@journal.dev (Mriganka Bora)</managingEditor>
    
  <item>
    <title>Turning a News App Into a Personal Blog</title>
    <link>https://boramriganka.github.io/get_news_app/blog/turning-a-news-app-into-a-personal-blog</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/turning-a-news-app-into-a-personal-blog</guid>
    <pubDate>Wed, 10 Jun 2026 12:00:00 GMT</pubDate>
    <description>The homepage used to be a feed of other people&apos;s headlines. Now it is my writing, and the reader lives one level down. Flipping that hierarchy took routing surgery, not a rewrite.</description>
    <content:encoded><![CDATA[For years this domain served a news aggregator. It worked, but it was a tool, not a place. Nothing on it was mine: every headline, every image, every word belonged to someone else, fetched fresh and forgotten. The blogs I have followed for a decade are the opposite. They accumulate a voice. I wanted that, without throwing away a reader I still use every morning. Flip the hierarchy, keep the appliance The conversion was an information architecture change before it was a code change. The homepage now introduces a person and a body of writing. The news reader did not get deleted, and it did not get buried: it moved into its own section at /news, with everything it had before, sources, categories, search, bookmarks, the queue, behind a section sub-navigation of its own. The global header carries the blog: Writing, About, News. Routing surgery The mechanical work was nesting every news route under a /news prefix using a layout route: a NewsLayout component renders the section navigation and an Outlet, and the old top-level routes became its children. Article pages moved from /article/:id to /news/article/:id, search from /search to /news/search, and so on. The part people skip: every legacy path still works. Old bookmarks and shared links hit redirect routes that forward them into the new structure, preserving params and query strings. A route that 404s after a restructure is a promise broken retroactively. The redirects cost maybe forty lines and they mean no link I ever shared has died. Content as data, not markdown Posts are structured data: an array of typed blocks, paragraphs, headings, quotes, lists, rather than markdown strings. That choice came from the reader side of the app, where article rendering was already component-driven. Typed blocks mean the post page can render a pull quote as a designed object, build a table of contents from the heading blocks, and estimate reading time from the text blocks, all without a parser dependency. The blog and the reader share one design system: the same theme tokens, the same typography stack, the same dark mode. The Redux store is untouched, because the blog pages do not need it and the news section already had it. The footer and branding flipped from a generic masthead to my own name, which felt strange for exactly one day "A tool is something you use and leave. A place is somewhere a voice accumulates. The trick was making one contain the other." If you have a side project with traffic and history, you do not have to choose between preserving it and outgrowing it. Nest it. The appliance keeps running in its room, and the front door finally says your name.]]></content:encoded>
    <category>Case Study</category>
    <category>News App</category>
    <category>Meta</category>
  </item>
  <item>
    <title>What Award Sites Taught Me About a Reading Page</title>
    <link>https://boramriganka.github.io/get_news_app/blog/what-award-sites-taught-me-about-a-reading-page</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/what-award-sites-taught-me-about-a-reading-page</guid>
    <pubDate>Tue, 02 Jun 2026 12:00:00 GMT</pubDate>
    <description>I studied Awwwards winners with editorial DNA before redesigning this blog. Most of what makes them feel expensive costs nothing: measure, hierarchy, and restraint.</description>
    <content:encoded><![CDATA[Before redesigning this site I spent time pulling apart Awwwards winners that share its content shape: personal sites and portfolios built around writing. Units.gr, the Lando Norris site by OFF+BRAND, the typographic portfolios that circulate every year. The surprise was how little of their quality comes from technology. Most of it is typesetting discipline that any site can afford. The label and the headline The single most reusable pattern: a small all-caps label in a monospace or technical face, sitting above an enormous display headline. Units.gr does it with a warm orange label over near-black. The label does the categorizing, which frees the headline to be purely expressive, set at clamp sizes that reach 6 or 7 viewport-widths. Two text elements, total. The hierarchy is so strong it reads from across a room, and it costs zero kilobytes. Measure is the whole game Every winner with long-form text sets it between roughly 60 and 75 characters per line. Not because a rule says so, but because reading rhythm collapses outside that range: short lines exhaust the eye with returns, long lines lose it mid-track. On this site the post body is capped in ch units and the reading face switched to a serif cut for text sizes, not display. The display serif that makes a headline gorgeous makes a paragraph exhausting. Two optical sizes of one family, or two families, but never one face doing both jobs. Metadata as telemetry The Lando Norris site styles its statistics like F1 telemetry: small, monospaced, precise. Applied to a blog, that grammar fits metadata perfectly: dates, read times, tags, all set in a small mono face with wide tracking. It separates the apparatus of the page from the writing itself, the way a well-set book keeps folios and running heads visually distinct from the text block. What I deliberately did not take WebGL scenes and shader backgrounds, because they tax exactly the budget a reading page needs: time to first word. Custom cursors, which demo well and annoy daily, especially on a page whose core gesture is selecting text. Scroll hijacking of any kind, because a reader's scroll position is theirs, not mine I shipped a custom cursor here briefly. It followed the pointer with a trailing ring, scaled on hover, respected reduced-motion, and was technically clean. It was also wrong, in the way only a borrowed pattern can be wrong: it belonged to portfolio sites where the cursor is part of a performance, not to a page that asks you to read two thousand words. Removing it taught me more than building it did. "Award sites are not a style to copy. They are a budget allocation to study: every one of them spends extravagantly on exactly one thing." For a reading site, that one thing has to be the text: its face, its measure, its hierarchy, its rhythm. Everything else, the marquees, the reveals, the grain, is seasoning. Get the typesetting right and the page feels expensive before a single animation fires.]]></content:encoded>
    <category>Case Study</category>
    <category>Design</category>
    <category>Typography</category>
  </item>
  <item>
    <title>Typography for Engineers: Five Rules, Applied Stubbornly</title>
    <link>https://boramriganka.github.io/get_news_app/blog/typography-for-engineers-five-rules</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/typography-for-engineers-five-rules</guid>
    <pubDate>Tue, 26 May 2026 12:00:00 GMT</pubDate>
    <description>You do not need taste to make text readable. You need a handful of mechanical rules and the discipline to never break them. These are mine.</description>
    <content:encoded><![CDATA[Most engineering side projects are unreadable, and it is rarely the color scheme. It is the text: lines too long, type too small, everything one weight, hierarchy invisible. The good news is that readable typography is not a talent. It is five mechanical rules, and the rules can be applied by anyone willing to be stubborn about them. Rule one: cap the measure Body text should run 60 to 75 characters per line. Shorter and the eye exhausts itself on line returns; longer and it loses the track on the way back. You do not need to count: set max-width in ch units, somewhere around 65ch, on every block of prose, and the rule enforces itself at every viewport. This single line of CSS does more for readability than any font choice you will ever make. Rule two: body text is bigger than you think Sixteen pixels is a floor, not a target. Long-form reading wants 18 to 20 pixels, 1.1 to 1.25rem, with line-height around 1.7. Tight line-height is the most common self-inflicted wound I see in developer projects, because code editors train us on 1.4 and prose suffocates there. Headings go the other way: large heading sizes want line-height down near 1.05, or the lines of a wrapped headline drift apart like strangers. Rule three: two faces, two jobs One display face for headings, one text face for body, and nothing else. The pairing can be safe, a contrast serif over a quiet sans, or this site's arrangement, a display serif for headlines and a serif with a text optical size for reading. What kills a page is four fonts doing one job, or one font asked to do both: display faces are drawn for impact at large sizes and their letterforms clog at paragraph size, while text faces look anemic blown up to 6rem. Rules four and five: visible hierarchy, whitespace as the separator Headings should be 2.5 to 4 times the body size, so structure is legible from across the room; timid 1.3x headings make every page read as one grey slab. Separate sections with space, not lines; borders are punctuation for when space is exhausted, and a page striped with hairline dividers is a page that did not trust its margins. Spacing should be asymmetric: more space above a heading than below it, so each heading visibly belongs to the text that follows "Good typography is invisible. Bad typography is the reason nobody finishes your README." None of this requires a design degree. It requires treating these five rules the way you treat a linter: not as suggestions to feel about, but as checks that pass or fail. The taste can come later. The measure cap ships today.]]></content:encoded>
    <category>Design</category>
    <category>Typography</category>
  </item>
  <item>
    <title>Building a News Reader I Actually Use</title>
    <link>https://boramriganka.github.io/get_news_app/blog/building-a-news-reader-i-actually-use</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/building-a-news-reader-i-actually-use</guid>
    <pubDate>Thu, 14 May 2026 12:00:00 GMT</pubDate>
    <description>Curated sources, political bias labels, a capped read-later queue, and skeleton loading. The news section of this site started as a standalone app. This is its engineering story.</description>
    <content:encoded><![CDATA[The News section of this site began life as a standalone project: a React and Redux app that pulled headlines from NewsAPI and let me read them without the ad-ridden chrome of the original publishers. Like most side projects it was an excuse to practice the stack. Unlike most, I still use it every morning, which forced it to grow features a toy never needs. The shape of the thing The reader has a top stories feed, category pages for technology, business, and entertainment, full-text search, bookmarks, and a read-later queue. State lives in Redux: the older slices are hand-written reducers with thunks, and the newer ones, like the queue, use Redux Toolkit. The two styles coexist in one store, which is less a design choice than an honest fossil record of when each feature was built. API calls go through a serverless proxy at /api/news rather than hitting NewsAPI directly from the browser. That keeps the API key off the client, and it gave me one place to normalize responses when the upstream shape wobbled. Bias labels, the feature I argue about most Every source in the feed carries a small label: Left, Centre-Left, Centre, Centre-Right, or Right. The mapping lives in a plain data file, sourceBias.js, keyed by source id. The labels are approximations and the UI says so in an info popover, plainly: they are not editorial judgments, they are context. I built it because I noticed my own feed skewing and I wanted the skew to be visible. When a week of reading lights up one side of the spectrum, the labels make it obvious, and I correct course on purpose instead of pretending I am neutral. Friends who saw the feature split instantly into two camps: this is essential, and this is dangerous. Both camps kept using it. The queue with a ceiling The read-later queue is deliberately hostile to hoarding. Saving feels like reading, which is exactly why read-it-later apps decay into guilt repositories. So the queue is small and the interface keeps the count in your face, as a badge in the navigation. Adding an item when the queue is full means deciding what to drop. The constraint is the feature. Loading states are part of the design Skeleton cards mirror the exact layout of real article cards, hero, featured, and compact variants, so the page does not jump when content lands. Images load through an ImageWithFallback component, because news APIs return broken image URLs constantly and a grey box is better than a broken glyph. Reading time is estimated from description and content length, a rough number that still beats no number "A side project you use every day stops being practice and starts being a product, whether you meant it to or not." The reader now lives inside this blog as its own section, which is a separate story about information architecture. But the lesson from the build stands on its own: pick a side project you will use daily, because daily use is the only code review that never lets anything slide.]]></content:encoded>
    <category>Case Study</category>
    <category>News App</category>
    <category>React</category>
  </item>
  <item>
    <title>Custom Color Palette: A Local-First Color Tool</title>
    <link>https://boramriganka.github.io/get_news_app/blog/custom-color-palette-a-local-first-color-tool</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/custom-color-palette-a-local-first-color-tool</guid>
    <pubDate>Sat, 18 Apr 2026 12:00:00 GMT</pubDate>
    <description>I built a browser-based color tool with three core flows and zero sign-up. Every palette lives in localStorage. This is the product thinking behind it.</description>
    <content:encoded><![CDATA[Custom Color Palette is a browser-based color tool for designers and developers. It does three things. The generator produces a five-color palette every time you press the spacebar, or tap the floating action button on mobile. You lock the colors you love and regenerate the rest, drag swatches to reorder them, expand any swatch into a shade ramp, and fine-tune with a picker. The palette list is the home screen: your saved palettes next to twenty-plus curated seed palettes, each one openable back into the generator. The third flow is an explore page with category-browsable presets, plus a two-color contrast checker that calculates WCAG AA and AAA compliance. The decision I defend hardest is that there is no account system. Everything is local-first. Palettes live in localStorage, full stop. No sign-up wall, no email capture, no sync service to maintain. Why local-first Color tools are tools of the moment. You reach for one mid-task, with a design file open in the next tab, and you want an answer in seconds. Every screen between you and the swatches is a tax. The moment I sketched a login page I knew it was wrong: I would never sign in to a color picker myself, so why would I ask anyone else to. Local-first also collapsed the backend to nothing. There is no database, no auth provider, no API to version. The entire product deploys as static files on Vercel. That constraint forced clarity elsewhere: if there is no server, the data model has to be simple enough to serialize into localStorage and back without a migration system. One data model decision carried the app The architecture decision that mattered most was separating user palettes from seed palettes. User-created palettes live under a userPalettes key in localStorage. The curated presets live in a seedColors.js module shipped with the bundle, and they are immutable. That single separation is the source of truth for every can-delete and can-edit decision in the UI. A seed palette can be opened, remixed, and saved as a new user palette, but the original never mutates. Before that split, edit and delete logic was a pile of special cases. After it, the rule fits in one sentence: if it came from seedColors.js you can copy it, and if it came from localStorage you own it. The stack, honestly React 17, with class and functional components mixed, because the older parts of the app predate my hooks habits. Material-UI v4 with JSS styling through withStyles and makeStyles. chroma-js for every piece of color math: conversions, scales, harmony, distance. React Router v5 and react-sortable-hoc for drag-and-drop reordering. Plain CSS in index.css for all animation, which was a deliberate decision I wrote about separately None of that is fashionable. All of it is boring and known, which meant the interesting problems stayed where they belong: in the color math and the interactions, not in the plumbing. "A tool you reach for mid-task earns its keep by the second screen. There should not be a second screen." What I would keep If I rebuilt it today I would swap Material-UI for hand-rolled components and React 17 for whatever is current. I would not touch the local-first decision, the seed and user palette split, or the spacebar. Some decisions are stack decisions and some are product decisions. The product decisions are the ones that survived.]]></content:encoded>
    <category>Case Study</category>
    <category>Color Palette</category>
  </item>
  <item>
    <title>A Reading System That Actually Sticks</title>
    <link>https://boramriganka.github.io/get_news_app/blog/a-reading-system-that-actually-sticks</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/a-reading-system-that-actually-sticks</guid>
    <pubDate>Thu, 02 Apr 2026 12:00:00 GMT</pubDate>
    <description>I have abandoned every read-it-later app I ever installed. The system that finally worked has three rules, a hard ceiling, and zero new software.</description>
    <content:encoded><![CDATA[Every read-it-later app I have used became a guilt repository: hundreds of saved articles, none read, the unread count growing like interest on a debt. The problem was never the software. The problem was that saving feels like reading. The tap that files an article away delivers a small completion, just enough satisfaction to substitute for the thing it was supposed to schedule. The three rules Rule one: the queue never holds more than ten items. To add an eleventh, something gets deleted. Not archived, deleted, because archives are where queues go to stop being queues. Rule two: the queue is processed at a fixed time, in the morning, with tea, before anything else opens. Reading that floats free in the day gets displaced by everything with a deadline, which is everything. Rule three: anything that survives two purges gets read immediately or deleted on the spot, because an article I have declined to read twice is an article I am never going to read, and the queue should stop lying to me about it. The ceiling is the whole mechanism The cap is what changed the psychology. With infinite space, saving is free, so the decision about whether something is worth reading gets deferred forever, one tap at a time. With ten slots, saving has a price: is this worth one of ten places, possibly at the cost of something already there? That is exactly the judgment the infinite queue let me skip, moved from someday to now, where it takes three seconds and is usually no. "A queue with a hundred items is not a queue. It is a graveyard with notifications." Build the rules into the tool The news reader on this site implements the queue this way: small, capped, with the count displayed as a badge I cannot avoid seeing. I built the constraint into the interface because I know exactly how long my discipline lasts against a frictionless save button, roughly four days. Willpower is a terrible enforcement mechanism and a fine design input. Encode the rule where the temptation lives. A year in, the system holds. Not because I became a more virtuous reader, but because the queue finally tells the truth: ten things I will actually read, processed daily, nothing rotting underneath. The articles I delete unread were never going to be read in the archive either. The only difference is that now nobody is pretending.]]></content:encoded>
    <category>Craft</category>
    <category>Reading</category>
  </item>
  <item>
    <title>Every Line of Color Math Is chroma-js</title>
    <link>https://boramriganka.github.io/get_news_app/blog/every-line-of-color-math-is-chroma-js</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/every-line-of-color-math-is-chroma-js</guid>
    <pubDate>Sun, 29 Mar 2026 12:00:00 GMT</pubDate>
    <description>Shade ramps, WCAG ratios, harmony generation, perceptual distance: my color tool delegates all of it to one library. Here is what chroma-js does and where its edges are.</description>
    <content:encoded><![CDATA[When I started Custom Color Palette I assumed I would end up writing color conversion code. RGB to HSL is a Stack Overflow classic, and every tutorial wants you to hand-roll it. I wrote none of it. Every conversion, every shade ramp, every contrast ratio and harmony calculation in the app goes through chroma-js. It is the only dependency I never once considered replacing. What the app asks of it The generator needs harmonious-ish randomness. Pure random RGB looks like a paint spill, so the generate function works in HSL space through chroma.hsl(), constraining saturation and lightness into ranges that read as designed rather than random, while letting hue roam. The result is not a color theory engine. It is a cheap trick that produces palettes people actually lock and keep, which was the only metric I cared about. Shade ramps are chroma.scale(). Give it a swatch color, anchor the ends toward near-white and near-black, ask for ten steps, and you get a usable tint-and-shade ramp for free. The naive version, interpolating in RGB, produces muddy midtones. Interpolating in LAB through the mode option fixes that, and switching modes is one string argument. That is the kind of edge a library earns its place with. Contrast as a first-class feature The contrast checker is chroma.contrast(a, b) and a handful of thresholds: 4.5 for AA normal text, 3.0 for AA large text, 7.0 for AAA. The hard part was never the math. The hard part was deciding to surface it everywhere instead of hiding it in a dedicated screen. Each swatch in the generator shows a small monospace caption like LUM 0.17 with its contrast ratio against white or black and the badge it earns, AA or AAA. Designers read hex codes and developers read contrast ratios, and both get what they need inline without leaving the flow. Luminance comes from chroma(color).luminance(), one call. Distance, the unsung function chroma.distance() computes perceptual distance between two colors in LAB space. I use it to stop the generator from producing two near-identical swatches in one palette: regenerate any color that lands too close to a locked one. It is four lines of code and it removed the single most common complaint from early testers, which was that palettes felt samey. Where the edges are chroma-js is around 14kb gzipped, noticeable in a tool this small, and I accepted it because the alternative was maintaining color math myself. Its harmony helpers are primitive, so anything resembling taste still lives in my constraint code. OKLCH support arrived late in its life, and if I were starting today I would evaluate culori for a more modern color-space story "The best dependency is the one that turns a research project into a function call." There is a genre of engineering writing about why you should not take dependencies. This post is the other genre. Color science is deep, well-studied, and entirely orthogonal to my product. Delegating it was not laziness. It was the decision that let a one-person project ship a contrast checker that actually agrees with the WCAG spec.]]></content:encoded>
    <category>Case Study</category>
    <category>Color Palette</category>
    <category>Engineering</category>
  </item>
  <item>
    <title>The Case for Boring State Management</title>
    <link>https://boramriganka.github.io/get_news_app/blog/the-case-for-boring-state-management</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/the-case-for-boring-state-management</guid>
    <pubDate>Thu, 12 Mar 2026 12:00:00 GMT</pubDate>
    <description>This site still runs Redux, and twice a year I decide not to migrate it. A defense of spending your novelty budget where users can feel it.</description>
    <content:encoded><![CDATA[Twice a year I open this codebase, look at the Redux store, and consider migrating to whatever the current consensus is. Twice a year I close the tab. The store works. Its bugs are known and old. The migration would consume a month of evenings and buy nothing a reader of this site could ever feel. Innovation tokens are real Every project gets a small budget of novelty, a handful of places where you can afford the unknown unknowns of a new tool. Spend them where they compound. For this site, that is the writing and the reading experience: the typography, the queue, the bias labels. Those are visible. The plumbing is not. Boring technology in the plumbing is not a compromise; it is the strategy that keeps the budget available for the parts that face outward. The asymmetry people miss: the cost of a new tool is not the learning curve, it is the operational surprise. Old Redux has no surprises left for me. I know how it fails, how it debugs, how it behaves at the edges. A newer, objectively better library starts that clock over, and the clock runs in production. "Nobody has ever bookmarked a blog because of its state management library." Boring is not the same as frozen The position has limits, and pretending otherwise turns pragmatism into superstition. New slices in this codebase use Redux Toolkit, because Toolkit removed real, recurring costs, the boilerplate, the typo-able string constants, without changing the mental model. That is the test for letting new technology in: does it delete a cost I pay weekly, or does it promise a future I might want someday? Toolkit passed. Each year's fashionable replacement keeps failing, not because it is bad, but because my store is too boring to benefit. And when a token does get spent, spend it loudly, on something a user touches. The read queue, the contrast tooling, the type system of this redesign: that is where this project's novelty went. The corollary to boring plumbing is exciting fixtures. A project that is boring everywhere is not disciplined, it is dead. So the store stays. Not forever, just until the day the boring choice starts costing me weekly. That day keeps not coming, and I have stopped being surprised.]]></content:encoded>
    <category>Craft</category>
    <category>Engineering</category>
  </item>
  <item>
    <title>The JSS Keyframes Bug That Changed How I Ship Animation</title>
    <link>https://boramriganka.github.io/get_news_app/blog/the-jss-keyframes-bug-that-changed-how-i-ship-animation</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/the-jss-keyframes-bug-that-changed-how-i-ship-animation</guid>
    <pubDate>Sat, 07 Mar 2026 12:00:00 GMT</pubDate>
    <description>A scoped keyframe name that never resolved left my entire app invisible at opacity zero. The fix was philosophical: all motion moved to plain CSS.</description>
    <content:encoded><![CDATA[Early in the Custom Color Palette build, I opened the app and got a blank page. Not an error page, not a crash. The React tree mounted, the DOM was fully populated, and every element sat there at opacity zero, invisible and waiting for an animation that would never come. The mechanics of an invisible app The app styles components with JSS through Material-UI v4. JSS scopes keyframe names: you define keyframes in a style object and reference them with a dollar-sign syntax like fadeIn referenced as $fadeIn, and JSS rewrites both to a hashed name at runtime so animations cannot collide across components. Useful in theory. My entrance animation set animation-fill-mode to both. Fill-mode both means the element holds the first keyframe before the animation starts and the last keyframe after it ends. The from frame was opacity zero. And the keyframe reference, after a refactor moved the keyframes into a different style sheet, no longer resolved. The browser saw an animation-name pointing at nothing, so the animation never ran. But fill-mode logic combined with the unresolved name left the element pinned at its declared starting state. "animation-fill-mode: both plus an unresolved keyframe name equals content that never appears. No error, no warning, nothing in the console." That is the worst class of bug: silent, styling-layer, and invisible to every tool. No console error, because nothing threw. No failing test, because the DOM was correct. You find it by staring at computed styles in DevTools until you notice the animation-name does not match any registered keyframes. The decision I could have fixed the reference and moved on. Instead I made a rule: no more animations in JSS, ever, in this codebase. Every animation from that day on lives in index.css as a global class with a plain, unhashed keyframe name. The copy pill that confirms a hex copy, the diagonal generate wipe, the brightness flash on a locked swatch, the marquee on the home page: all plain CSS, all toggled by adding and removing class names through element.classList or a React key. Global CSS is supposed to be the thing we grew out of. But for animation it has properties the scoped system gave up. Names are greppable: search the codebase for generate-wipe and you find the keyframes, the class, and every usage. The cascade is auditable in one file. And nothing can silently fail to resolve, because there is no resolution step. The trade I accepted The cost is a shared global namespace, so my animation classes carry intent-revealing names and the file is organized by feature with comments. For a single-developer project that discipline costs nothing. On a forty-person team I might choose differently, or I might not: the failure mode of a name collision is a wrong animation you can see, while the failure mode of scoped keyframes was an invisible app. I think about that asymmetry a lot now. When choosing between two systems, do not just compare the happy paths. Compare what each one looks like when it breaks, and choose the one whose failures announce themselves.]]></content:encoded>
    <category>Case Study</category>
    <category>Color Palette</category>
    <category>CSS</category>
  </item>
  <item>
    <title>Designing a Bento Grid That Demos Itself</title>
    <link>https://boramriganka.github.io/get_news_app/blog/designing-a-bento-grid-that-demos-itself</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/designing-a-bento-grid-that-demos-itself</guid>
    <pubDate>Sat, 21 Feb 2026 12:00:00 GMT</pubDate>
    <description>The home page of my color tool has no screenshots and no feature list. Every cell in the grid is the product, live and running. This is how each interaction earned its place.</description>
    <content:encoded><![CDATA[The home page of Custom Color Palette went through the usual draft: hero text, feature list, screenshots. It was fine and it was dead. A screenshot of a color tool is a contradiction, because the entire point of the product is that it moves. So I threw the page out and rebuilt it as a bento grid where every cell is a live demo of the real product. The product is the animation I borrowed the principle from Igloo Inc, an Awwwards Site of the Year winner whose micro-interactions are surgical: every action has a tactile response, and the site demonstrates its craft rather than describing it. Applied to my home page, that meant the generator cell does not show a picture of the generator. It reshuffles a real palette every 2.6 seconds, using the same chroma.hsl() harmonious-ish algorithm as the actual tool. The contrast cell computes the live WCAG ratio of whatever that palette currently shows. Nothing is canned. The product sells itself by running. The spacebar, taught without a tutorial The generator’s core interaction is pressing space to generate. On the home page, pressing space reshuffles the entire palette wall with a smooth crossfade. A small keycap-styled hint reads PRESS [SPACE] TO SHUFFLE. That one element does three jobs: it previews the generator’s character before a single click, it teaches the product’s most important shortcut, and it replaces what would otherwise be an onboarding modal. Nobody has ever wanted an onboarding modal. Hovers that respond to content On the palette wall, every card borders in its own dominant color on hover. A warm orange palette gets an orange border, a deep teal palette gets teal. Technically it is one CSS custom property, card-accent, set inline per card and consumed by the hover rule. Cheap to build, but it changes the feel of the page completely: the UI is responding to the content instead of imposing a single brand color on everything. Of all the details on the page, this is the one other developers ask about. The generate wipe Each generate fires a diagonal light sweep across the swatches. The obvious implementation is clip-path, which I tried first, and which clips the children along with the effect. The shipped version is a translateX gradient overlay, pure CSS, mounted and unmounted through a React key on every generate so the animation restarts cleanly. It gives the spacebar a mechanical, satisfying feel, like a shutter firing. Without it, generating feels like a page refresh. With it, generating feels like an action. Copy pill: clicking a swatch copies the hex and a small pill confirms it inline, a pattern lifted from Coolors because it is the most-loved interaction in any color tool. Telemetry labels: monospace captions like LUM 0.69 with the contrast ratio and badge, inspired by the telemetry styling on the Lando Norris site by OFF+BRAND. Preview drawer: a see-it-applied panel showing the palette on real buttons and cards, taken from Happy Hues, because non-technical users need context, not hex codes "A feature list claims. A live cell proves." The editorial frame around all of this comes from units.gr: near-black background, one warm orange accent, oversized display type with a small all-caps label above the heading. The full-width marquee strip is borrowed from the Festival Les Francouvertes site, including the calc(50% minus 50vw) trick to break a full-bleed strip out of a constrained container. I keep a list of what I borrowed and from where. Influence is not theft if you can cite it.]]></content:encoded>
    <category>Case Study</category>
    <category>Color Palette</category>
    <category>Design</category>
  </item>
  <item>
    <title>Guwahati Mornings</title>
    <link>https://boramriganka.github.io/get_news_app/blog/guwahati-mornings</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/guwahati-mornings</guid>
    <pubDate>Sat, 14 Feb 2026 12:00:00 GMT</pubDate>
    <description>Notes from home: the Brahmaputra at six in the morning, ten-rupee tea, and why my best debugging happens three kilometers from the nearest screen.</description>
    <content:encoded><![CDATA[There is a stretch of the Brahmaputra near Uzan Bazar where the river is so wide the far bank is a rumor. When I am home in Guwahati I walk there early, before the heat, and buy tea from a stall that has not changed its bench or its price in fifteen years. Ten rupees. The man does not ask what I want anymore. I do not bring my phone. This is not discipline, and I distrust essays that pretend it is. The river simply makes the phone feel small. Whatever urgency the screen was holding dissolves against something that has been moving water for longer than the city has had a name. The debugging that happens off-screen More than once, a bug that survived a week of focused effort has dissolved in forty minutes of watching the water. Not because I was thinking hard about it, but because I had finally stopped. The mind keeps working when you close the editor; it just works differently, sideways, making the associations that directed attention is too narrow to allow. The walk does not feel like work, which is precisely why it functions as the part of the work the desk cannot do. "The best debugging tool I own does not fit in my pocket. It is three kilometers long and made of water." Distance as a feature Most of what I write here starts on those walks, dictated into memory in fragments, serialized later at a desk. The screen is where ideas get written down. It is mostly not where they happen. I notice this and keep forgetting it, the way everyone does, because the industry quietly equates presence at the screen with progress on the problem, and the equation is wrong in both directions. If your output feels thin, the fix is rarely more hours at the desk. It is usually a river. Yours might be a different river, or a kitchen, or a long bus route. The shape does not matter. What matters is that it is somewhere your problems can follow you, but your tools cannot.]]></content:encoded>
    <category>Craft</category>
    <category>Personal</category>
  </item>
  <item>
    <title>Same Tool, Two Devices: Designing the Split</title>
    <link>https://boramriganka.github.io/get_news_app/blog/same-tool-two-devices-designing-the-split</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/same-tool-two-devices-designing-the-split</guid>
    <pubDate>Mon, 02 Feb 2026 12:00:00 GMT</pubDate>
    <description>Desktop gets hover toolbars, keyboard shortcuts, and telemetry labels. Mobile gets a FAB, fat touch targets, and a bottom sheet. Neither is a degraded version of the other.</description>
    <content:encoded><![CDATA[Responsive design is usually framed as one layout that bends. For Custom Color Palette I ended up somewhere stronger: two deliberate designs that share a codebase. The breakpoint at 900 pixels is not where the layout reflows. It is where the interaction model changes. The keyboard is a platform On desktop, the primary generate action is the spacebar, and the hero advertises it with a keycap hint reading PRESS [SPACE] TO SHUFFLE. On mobile there is no keyboard, so the hint disappears entirely and a floating action button takes over as the generate trigger. Even the helper snackbar adapts its copy: desktop says press spacebar to generate, mobile says tap the generate button and points at the FAB. Telling a phone user to press space would be worse than saying nothing. Hover does not exist on glass The swatch toolbar, with copy, lock, ramp, and picker actions, appears on hover on desktop. Hover is free real estate: the actions are invisible until your cursor declares interest, so the swatches stay clean. On mobile that toolbar would be unreachable, so it is always visible, pinned to the top of each swatch with a blur backdrop so it stays legible over any color. The copy interaction makes the same move: on desktop you click anywhere on the swatch, on mobile there is a dedicated copy button with a fat 44-pixel target, because a whole-swatch tap target conflicts with scroll gestures. What mobile loses on purpose Telemetry labels, the LUM and contrast captions, are hidden below 900 pixels because the columns are too narrow to set monospace text without wrapping into noise. The hero palette wall, a three-column grid of thumbnails on desktop, is gone on mobile to reclaim vertical space for the primary action. Toolbar buttons drop their text labels and become icon-only I want to defend the first cut, because hiding information feels like a failure. The telemetry labels exist for a desk context: a developer checking contrast ratios while building a UI in the next window. The phone context is browsing and collecting. Cramming AA badges into a 160-pixel column serves the desk user’s needs in the phone user’s context, and serves neither. One drawer, two physics The palette preview, which shows your colors applied to sample buttons and cards, slides in from the right as a 400-pixel drawer on desktop. On mobile the same component is a bottom sheet covering 85 percent of the viewport height. Side drawers on phones are awkward: thumbs travel vertically, and edge swipes collide with browser navigation gestures. Bottom sheets are native grammar on a phone the way side panels are native grammar on a desktop. Same React component, same data, different physics. "The breakpoint is not where the layout breaks. It is where the user’s posture changes." The lesson I took into every project since: do not ask which elements shrink on mobile. Ask what the person is doing differently when they hold the product in one hand, and design for that posture. Sometimes the answer is the same UI, smaller. Usually it is not.]]></content:encoded>
    <category>Case Study</category>
    <category>Color Palette</category>
    <category>Design</category>
  </item>
  <item>
    <title>Migrating Vue 2 to 3 on a Platform With 130 Million Users</title>
    <link>https://boramriganka.github.io/get_news_app/blog/migrating-vue-2-to-3-on-a-platform-with-130-million-users</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/migrating-vue-2-to-3-on-a-platform-with-130-million-users</guid>
    <pubDate>Tue, 20 Jan 2026 12:00:00 GMT</pubDate>
    <description>You cannot stop the world to swap a framework under an e-commerce platform. Notes on migration order, the compat build, and the bugs that only appear at scale.</description>
    <content:encoded><![CDATA[A framework migration on a small app is a weekend. On an e-commerce platform serving over 130 million users, it is a campaign measured in quarters, where the constraint is not the new API but the requirement that revenue pages never flinch while the ground moves under them. Order of operations beats cleverness The migration that works is boring: upgrade to the last Vue 2.7 release first, because it backports the Composition API and lets you modernize components while still on the old runtime. Then run the official compat build, which boots Vue 3 with Vue 2 behavior shims and screams warnings at every deprecated pattern. Each warning category becomes a ticket. You burn them down by directory, not by feature, because directory ownership maps to team ownership and a migration without clear ownership stalls in review. The seductive alternative, a long-lived migration branch where the rewrite happens in parallel, fails every time at this scale. The main branch moves daily. By the time the branch is ready, it is a second application that has to be reconciled with three months of drift. Migrate in place, behind flags, shipping every week. What actually broke Every usage of $children, which Vue 3 removed, and which had quietly become load-bearing in older layout components. Event buses built on $on and $off, gone in Vue 3, replaced with small explicit emitter modules. Filters in templates, removed entirely, each one rewritten as a computed or a plain function. Third-party component libraries that lagged the upgrade, which forced either forks or replacements, and the audit of those dependencies should have happened months earlier than it did None of these are hard individually. At scale the problem is census: finding every instance, knowing which are dead code, and proving the fix in pages you cannot manually test because there are thousands of them. We leaned on codemods for the mechanical rewrites and on production error monitoring, sliced by route, to catch what the codemods missed. The bugs only scale reveals The reactivity system change, from Object.defineProperty to Proxies, is invisible in a demo and very visible against ten years of accumulated code. Patterns that mutated arrays by index, or added properties to objects and relied on Vue.set, behaved subtly differently. The worst bugs were not crashes. They were stale views: a price that did not update, a badge that did not clear. Crashes page someone. Staleness just quietly costs money. "On a platform this size, the framework is the easy part. The migration is really a census of every shortcut anyone took in eight years." If I compress the campaign into advice: take the 2.7 stepping stone, treat compat warnings as the backlog, migrate in place by ownership boundary, and instrument staleness, not just errors. The new framework is nice. The codebase knowing itself again is the actual prize.]]></content:encoded>
    <category>Engineering</category>
    <category>Vue</category>
  </item>
  <item>
    <title>What I Read to Stay Current, Without Drowning</title>
    <link>https://boramriganka.github.io/get_news_app/blog/what-i-read-to-stay-current-without-drowning</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/what-i-read-to-stay-current-without-drowning</guid>
    <pubDate>Fri, 09 Jan 2026 12:00:00 GMT</pubDate>
    <description>My actual information diet: a fixed set of sources, read at a fixed time, with each source&apos;s political lean labeled so I can watch my own bubble forming.</description>
    <content:encoded><![CDATA[Staying informed is a solved problem. Staying informed without a low hum of anxiety is not, and most advice solves the wrong half. Mine is a system with three components: a small fixed set of sources, a fixed reading time, and labels that keep the shape of my bubble visible. Fixed sources, fixed time The source list is short and changes maybe twice a year, deliberately, never by drift. An open-ended feed is a slot machine; a fixed roster is a newspaper, finite by design. It gets read in the morning alongside the read queue, and then it is done, in the satisfying way a newspaper used to be done. News that arrives all day is not information, it is weather, and I stopped checking the weather hourly once I noticed it never changed what I did. Label the lean The component people ask about: the news reader on this site tags every source with its rough political position, Left through Right, in a small label beside the source name. Not as a verdict, as a mirror. When a week of reading lights up one end of the spectrum, the labels make the skew visible while it is still a tendency rather than a worldview, and I can correct deliberately instead of pretending neutrality I do not have. "You cannot escape your bubble. You can keep its walls labeled." The labels live in a plain data file mapping source to position, approximate by necessity and annotated as such in the interface. Friends who try the feature split between essential and unsettling, which I have come to think is the mark of a feature doing something true. What fell away What this system removed surprised me more than what it kept: the recap podcasts, the aggregator scrolls, the commentary on the commentary. None survived contact with the question the fixed roster forces, which is: did this source ever change what I understood, or did it only confirm that I was keeping up? Keeping up is a feeling, not a state. The diet that works optimizes for the other thing.]]></content:encoded>
    <category>Craft</category>
    <category>Reading</category>
    <category>News App</category>
  </item>
  <item>
    <title>WCAG Contrast in Practice, Not Just in the Checker</title>
    <link>https://boramriganka.github.io/get_news_app/blog/wcag-contrast-in-practice-not-just-the-checker</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/wcag-contrast-in-practice-not-just-the-checker</guid>
    <pubDate>Thu, 08 Jan 2026 12:00:00 GMT</pubDate>
    <description>I built a contrast checker into my color tool, then learned that passing ratios is the start of the job. Where the 4.5 comes from and where teams actually fail.</description>
    <content:encoded><![CDATA[Building the contrast checker in my color tool forced me to actually read the WCAG contrast math instead of cargo-culting the thresholds. The short version: contrast ratio compares the relative luminance of two colors on a scale from 1 to 21, where luminance weights the channels by how the eye perceives them, green heaviest, blue lightest. AA compliance asks 4.5 to 1 for normal text and 3 to 1 for large text. AAA asks 7 and 4.5. Why the threshold moves with size The size carve-out is not bureaucratic mercy. Thin strokes at small sizes are genuinely harder to resolve than thick strokes at large sizes, so an 24-pixel bold heading legitimately needs less contrast than 14-pixel body text. The practical consequence runs the other direction too: light font weights at small sizes can be hard to read even when the checker passes, because the ratio measures color, not stroke width. The checker is a floor. Squinting at your own UI on a cheap monitor in daylight is the test. Where real interfaces actually fail Placeholder text, almost universally set in a grey that fails against the input background. Disabled-looking secondary text that is not actually disabled, the opacity: 0.5 habit, which silently halves contrast on whatever passes below it. Text over images and gradients, where the ratio is undefined because the background is, solved with scrims, not hope. Focus indicators and form borders, which are non-text contrast and need 3 to 1 against adjacent colors, the criterion everyone forgets exists. Dark mode, where the palette inverted in five minutes and nobody re-checked a single pair That last one deserves its own sentence: every theme is a separate compliance surface. My tool computes ratios live per swatch, in a monospace caption next to the color, because the only checks that happen are the ones that sit inside the workflow. A checker on a separate site, behind a context switch, is a checker that runs twice and then never again. Beyond the pass badge Two honest limitations. First, WCAG 2 contrast math has known perceptual blind spots, certain pairs pass numerically and read poorly, which is why the draft APCA model exists; treat borderline AA passes with suspicion rather than triumph. Second, maximum contrast everywhere is not the goal: pure black on pure white produces a glare that some readers find harder, which is why this site runs near-black ink on warm paper. Accessible design is contrast sufficiency plus judgment, not ratio maximalism. "The ratio is a floor you must clear, not a target you optimize. Above the floor, design judgment resumes." If you ship UI, wire luminance and ratio into wherever you already look at colors, your Storybook, your palette tool, your tokens pipeline. The math is one function call. The compliance failures are never in the math. They are in the checks that never ran.]]></content:encoded>
    <category>Design</category>
    <category>Accessibility</category>
  </item>
  <item>
    <title>Adopting the Composition API Without Rewriting Everything</title>
    <link>https://boramriganka.github.io/get_news_app/blog/adopting-the-composition-api-without-rewriting-everything</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/adopting-the-composition-api-without-rewriting-everything</guid>
    <pubDate>Wed, 12 Nov 2025 12:00:00 GMT</pubDate>
    <description>The Composition API is not a style preference. It is an answer to a specific pain: logic that cannot be extracted. Adopt it where that pain lives and nowhere else.</description>
    <content:encoded><![CDATA[When the Composition API landed, our codebase had hundreds of Options API components that worked fine. The wrong response is a rewrite. The right response is a rule for when each style earns its place, because the Composition API solves one specific problem, and components without that problem gain nothing from the churn. The problem it actually solves In the Options API, a single concern, say, pagination, smears across data, computed, methods, and watchers. Two concerns in one component interleave in all four buckets. Extracting one of them into something reusable meant mixins, and mixins are where clarity goes to die: invisible property injection, name collisions, and no way to trace where anything came from. A composable replaces that with a function. usePagination() owns its state, its computeds, its methods, in one closure, returning an explicit object. The component that consumes it names what it takes. Greppable, typeable, testable in isolation. That is the entire pitch, and it is enough. The adoption rule we settled on New components with shared or extractable logic: Composition API with script setup. Old components being touched for a feature anyway: migrate if they contain a mixin, otherwise leave them. Old components that work and are not being touched: do not touch them, ever, the diff is risk with no payoff. Any logic used by two or more components: extract to a composable regardless of what style its consumers use, since Options components can call composables through setup() That last point surprises people. You do not need to convert a component to consume a composable. The setup() option works inside an Options component, which means the library of shared logic can grow years ahead of full migration. Where teams hurt themselves The failure mode is composables that are just relocated mixins: grab-bag use-everything functions with twelve return values. A composable should be named after one capability, take its dependencies as arguments, and return the smallest surface that serves it. The second failure mode is reactivity leaks, destructuring a reactive object and wondering why updates stopped. The team needs the refs-versus-reactive conversation exactly once, written down, with a lint rule if you can manage it. "The Composition API is a tool for extracting logic. If a component has no logic worth extracting, it has no reason to change." Two years into this policy, the codebase is maybe forty percent Composition API by file count, near one hundred percent for components written this year, and zero rewrites were ever scheduled as projects. The styles coexist fine. Dogma was the only thing that ever threatened the migration, and we declined it.]]></content:encoded>
    <category>Engineering</category>
    <category>Vue</category>
  </item>
  <item>
    <title>Color Theory for Developers: HSL Is the API</title>
    <link>https://boramriganka.github.io/get_news_app/blog/color-theory-for-developers-hsl-is-the-api</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/color-theory-for-developers-hsl-is-the-api</guid>
    <pubDate>Thu, 16 Oct 2025 12:00:00 GMT</pubDate>
    <description>Hex codes are storage, not thought. Working in hue, saturation, and lightness is what let my palette generator produce colors that look designed rather than random.</description>
    <content:encoded><![CDATA[Developers mostly meet color as hex strings, six opaque characters that resist every intuition. The unlock, the one that made my palette generator possible, is that hex is a storage format, not a thinking format. The thinking format is hue, saturation, lightness: a color is a position on a wheel, an intensity, and a brightness, three knobs your hands can actually hold. Random colors versus designed colors Generate five colors with random RGB and you get a paint spill: some neon, some mud, no relationship. The fix is to randomize in HSL with constraints. Let hue roam the full wheel, but hold saturation and lightness inside bands that read as intentional, roughly 30 to 80 percent saturation, 35 to 70 lightness, and suddenly the output looks chosen. Harmony, it turns out, is mostly agreement in saturation and lightness while hue provides the variety. That one insight is the entire difference between my generator's output and noise. The classic schemes are wheel geometry Complementary: two hues 180 degrees apart, maximum tension, best as a base plus an accent rather than equal partners. Analogous: neighbors within about 30 degrees, low tension, calm, needs lightness variation to avoid mush. Triadic: three hues 120 degrees apart, lively and hard to balance, usually one dominant and two supporting. Monochromatic: one hue, varied saturation and lightness, impossible to make ugly, easy to make boring Every one of these is a one-liner once color is HSL: rotate hue by a constant, keep the other knobs. The schemes are not laws; they are defaults with a high hit rate, which is exactly what a generator needs. Where HSL lies to you HSL's lightness is mathematical, not perceptual. Yellow at 50 percent lightness blazes; blue at 50 percent broods. Two colors with equal L can have wildly different apparent brightness and wildly different contrast ratios against white. This is why shade ramps interpolated in HSL or RGB go muddy in the middle, and why color libraries offer LAB and OKLCH spaces, built to make equal steps look equal. My rule of thumb: generate and reason in HSL, because it is intuitive; interpolate and compare in a perceptual space, because it is honest. "Hex is how colors are stored. HSL is how they are thought. Perceptual spaces are how they are compared. Most color bugs are a confusion about which layer you are in." You do not need the full colorimetry rabbit hole to ship good color. You need the three knobs, the wheel geometry, and one healthy suspicion about lightness. That toolkit fits in an afternoon and pays out on every project that has a UI, which is all of them.]]></content:encoded>
    <category>Design</category>
    <category>Color</category>
  </item>
  <item>
    <title>Webpack 5 Optimization Notes From a Long-Lived Codebase</title>
    <link>https://boramriganka.github.io/get_news_app/blog/webpack-5-optimization-notes-from-a-long-lived-codebase</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/webpack-5-optimization-notes-from-a-long-lived-codebase</guid>
    <pubDate>Wed, 03 Sep 2025 12:00:00 GMT</pubDate>
    <description>Persistent caching, deterministic ids, real code splitting, and the art of reading a bundle analyzer without lying to yourself.</description>
    <content:encoded><![CDATA[Webpack is unfashionable, which is convenient for those of us still operating large codebases on it, because the discourse moved on while the tool quietly got good. Upgrading a long-lived application to Webpack 5 and then actually tuning it cut our cold builds by more than half and our CI rebuilds by far more. Notes follow. The filesystem cache is the headline Webpack 5 ships persistent caching: set cache.type to filesystem and rebuilds reuse work across process restarts, which transforms CI. The catch is cache invalidation. The cache must key on everything that affects output, so declare your config and its dependencies in cache.buildDependencies, and version the cache name when you change loaders. The team that does not do this gets a haunted build, where stale artifacts appear and everyone learns to distrust the cache and turns it off, losing the headline feature to one missing config line. Determinism is a performance feature Module and chunk ids default to deterministic in production mode, and that matters more than it sounds: deterministic ids mean a one-line change does not rename every chunk, which means long-term browser caching actually works. Pair it with contenthash in filenames and a stable runtime chunk. The fastest bytes are the ones the returning user already has. Splitting where the seams are Route-level dynamic imports first, because they map to real user intent and pay for themselves immediately. A vendor split that separates the stable heavy libraries from the volatile application code, so the framework chunk survives deploys untouched. Component-level splitting only for genuinely heavy leaves, charts, editors, players, because a waterfall of forty tiny chunks trades one problem for another Then open the bundle analyzer and resist the urge to feel productive. The analyzer rewards honesty: that 300kb chart library is not large because Webpack failed, it is large because someone imported all of it for one sparkline. Most bundle weight is a product decision wearing a build-tool costume. The config can split weight and cache weight. Only a code change can remove it. Two sharp edges Webpack 5 stopped polyfilling Node core modules, so a decade of transitive dependencies that casually required path or buffer surfaced at once during the upgrade; budget a day for resolve.fallback archaeology. And tree shaking still depends on sideEffects flags being truthful in package.json. One library lying about side effects can anchor a surprising amount of dead code in your output. Audit the big ones by hand. "Cache correctness is a feature you configure once and benefit from forever, or misconfigure once and distrust forever." None of this is glamorous. All of it compounds. A build that is fast, deterministic, and trusted changes how a team ships: smaller diffs, more frequent deploys, less fear. That is the actual deliverable of build engineering, and Webpack 5, tuned, delivers it fine.]]></content:encoded>
    <category>Engineering</category>
    <category>Build Tools</category>
  </item>
  <item>
    <title>Design Tokens Before a Design System</title>
    <link>https://boramriganka.github.io/get_news_app/blog/design-tokens-before-a-design-system</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/design-tokens-before-a-design-system</guid>
    <pubDate>Tue, 29 Jul 2025 12:00:00 GMT</pubDate>
    <description>You probably do not need a design system. You almost certainly need tokens: a small named vocabulary for color, space, and type that ends the hardcoding.</description>
    <content:encoded><![CDATA[Design systems are an organization. Tokens are a vocabulary. Teams reach for the organization when the vocabulary is what is missing, then drown in governance before a single screen improves. The sequence that works runs the other way: name your values first, and let the system grow out of the names only if it must. What a token actually is A token is a named design decision: accent is #a8331f, space-4 is 1rem, font-read is the reading serif stack. The name is the point. Hardcoded values are decisions nobody can find, scattered across a hundred files, each one a tiny fork of intent. A token collects all those forks into one place with one name, which makes the decision visible, changeable, and arguable. The day you swap an accent color by editing one line, the abstraction has paid for itself in full. Two layers, and stop there The structure that holds up: a primitive layer naming raw values, red-600, grey-100, and a semantic layer naming roles that reference primitives, accent, surface, border, text-secondary. Components consume only the semantic layer. This is what makes theming a data change instead of a rewrite: this site's dark mode is the semantic layer re-pointed at different primitives, ink and paper trading places, while every component stays untouched. Teams that skip the semantic layer end up with components hardcoding red-600 and a theme that requires a migration. Teams that add a third and fourth layer end up debating taxonomy in meetings while the product hardcodes hex in protest. The discipline that makes it real A small fixed spacing scale, four or five steps, because spacing chosen from a scale produces rhythm and spacing chosen by eye produces 14 slightly different margins. A lint rule or review convention against raw hex and raw pixel values in component code, since a vocabulary that is optional is decorative. Tokens live where code lives, CSS custom properties or a theme object, not in a wiki, because documentation that is not executable drifts within a month "A design system is a product with governance. Tokens are just agreeing on names. Start with the names." This site has no design system. It has a theme object with a dozen semantic tokens, a type stack of four named faces, and a habit of refusing raw values in components. That is enough vocabulary for one person to keep a multi-section product visually coherent, and crucially, enough for the redesign I just shipped to be mostly a token edit. When the names are right, even the rewrites get cheaper.]]></content:encoded>
    <category>Design</category>
    <category>Engineering</category>
  </item>
  <item>
    <title>Bringing Vite Into a Legacy Build Without a Rewrite</title>
    <link>https://boramriganka.github.io/get_news_app/blog/bringing-vite-into-a-legacy-build-without-a-rewrite</link>
    <guid isPermaLink="true">https://boramriganka.github.io/get_news_app/blog/bringing-vite-into-a-legacy-build-without-a-rewrite</guid>
    <pubDate>Wed, 25 Jun 2025 12:00:00 GMT</pubDate>
    <description>We wanted Vite&apos;s instant dev server without betting the production pipeline on a migration. Running both, honestly, was the right call for a year.</description>
    <content:encoded><![CDATA[Our dev server took most of a minute to boot and several seconds to reflect a save. Vite boots in milliseconds because it does not bundle in development at all: it serves native ES modules and transforms files on demand with esbuild. The temptation is to migrate everything. The discipline is to remember that the production build, with years of Webpack-specific configuration, loaders, and deploy tooling, was not the thing that hurt. Development was. The split-stack arrangement So we ran both: Vite for the dev server, Webpack for production builds. It sounds like heresy and it works, with one ironclad rule: the application code must stay tool-agnostic. That means standard ESM imports everywhere, no webpack-specific magic comments doing load-bearing work in app code, no require calls in module scope, and environment variables accessed through a thin wrapper so import.meta.env and process.env never appear raw in components. The wrapper point sounds trivial and is the whole game. Every place the two tools disagree, env vars, glob imports, asset URLs, worker instantiation, gets an adapter module with one implementation per tool, selected at build time. App code imports the adapter. The list of adapters is the honest, complete inventory of your coupling to the bundler, and ours fit on one screen. What surfaced in the first week CommonJS dependencies that Webpack silently interop-ed but Vite's stricter ESM pipeline rejected, fixed with optimizeDeps entries and two dependency upgrades. Circular imports that Webpack's bundling happened to tolerate and native ESM evaluation order did not, which were real latent bugs worth fixing anyway. SCSS imports relying on Webpack resolve aliases, mirrored into Vite's resolve.alias in ten minutes Notice the shape: almost everything Vite rejected was something Webpack was forgiving about, not something Vite was wrong about. The stricter tool became a lint pass for module hygiene. Dev-prod divergence, the obvious risk of a split stack, bit us exactly once in a year, in an asset URL edge case, which the adapter layer then absorbed. The endgame A year later, moving production to Vite was an afternoon, because the split-stack year had already forced every module to be honest. That is the reframe I offer anyone staring at a legacy build: introducing Vite as dev-only is not a compromise on the way to a migration. It is the migration, running in production-shadow mode, paying out dev speed the whole time. "The fastest migration is the one where the old and new systems run side by side long enough that the cutover becomes boring."]]></content:encoded>
    <category>Engineering</category>
    <category>Build Tools</category>
  </item>
  </channel>
</rss>