<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mohamed Idris</title>
    <description>The latest articles on Forem by Mohamed Idris (@edriso).</description>
    <link>https://forem.com/edriso</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F542036%2F3d38f955-495f-4ee8-9445-db0d27f2fd7b.png</url>
      <title>Forem: Mohamed Idris</title>
      <link>https://forem.com/edriso</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/edriso"/>
    <language>en</language>
    <item>
      <title>Stop Fighting Your AI About shadcn Components: Install the Skill</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sun, 03 May 2026 11:21:29 +0000</pubDate>
      <link>https://forem.com/edriso/stop-fighting-your-ai-about-shadcn-components-install-the-skill-1moc</link>
      <guid>https://forem.com/edriso/stop-fighting-your-ai-about-shadcn-components-install-the-skill-1moc</guid>
      <description>&lt;p&gt;If you use Cursor, Claude Code, GitHub Copilot, or any AI coding assistant with shadcn/ui, you probably hit this wall:&lt;/p&gt;

&lt;p&gt;You ask the AI to add a Combobox using Base UI. It generates code with &lt;code&gt;asChild&lt;/code&gt;. But Base UI does not have &lt;code&gt;asChild&lt;/code&gt;, it uses &lt;code&gt;render&lt;/code&gt;. You correct it. Next prompt, same mistake. You correct again. The cycle never ends.&lt;/p&gt;

&lt;p&gt;Or you ask for "a login form with shadcn". The AI invents components that don't exist in your registry, or it imports from &lt;code&gt;@radix-ui/react-dialog&lt;/code&gt; when your project uses Base UI.&lt;/p&gt;

&lt;p&gt;The reason is simple: &lt;strong&gt;the AI does not know your project&lt;/strong&gt;. It has no idea which framework you use, which components you already installed, which engine (Radix or Base UI) you picked, or how shadcn's APIs evolved this year. It guesses based on training data that might be a year old.&lt;/p&gt;

&lt;p&gt;There are two tools that fix this. Let me walk you through both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool 1: The shadcn skill
&lt;/h2&gt;

&lt;p&gt;A skill is a small bundle of instructions and helpers that gets injected into your AI assistant's context whenever you work in a project. The shadcn skill specifically tells your AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which framework you use (Next.js, Vite, Remix, Astro, etc.)&lt;/li&gt;
&lt;li&gt;which components are already installed in your project&lt;/li&gt;
&lt;li&gt;which engine you chose (Radix or Base UI)&lt;/li&gt;
&lt;li&gt;the correct, current API patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Behind the scenes, the skill activates when it sees a &lt;code&gt;components.json&lt;/code&gt; file in your project, runs &lt;code&gt;shadcn info --json&lt;/code&gt;, and feeds the result to your assistant. So instead of guessing, the AI reads your actual setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to install the skill
&lt;/h3&gt;

&lt;p&gt;In your project root, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx skills add shadcn/ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you use npm, yarn, or bun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm dlx skills add shadcn/ui
yarn dlx skills add shadcn/ui
bun dlx skills add shadcn/ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. Restart your AI editor, and now when you ask it for a component, it knows your context.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes after you install it
&lt;/h3&gt;

&lt;p&gt;Before the skill, you would write:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add a button using shadcn"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And get something close, but maybe wrong import, maybe outdated API, maybe a component you didn't install yet.&lt;/p&gt;

&lt;p&gt;After the skill, you can write things like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add a login form with email and password and a submit button"&lt;/p&gt;

&lt;p&gt;"Build a dashboard page with a sidebar, three stat cards, and a data table"&lt;/p&gt;

&lt;p&gt;"Create a settings page where the user can update their profile"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the AI generates code that uses &lt;strong&gt;exactly&lt;/strong&gt; the components in your project, with the correct API for the engine you picked.&lt;/p&gt;

&lt;p&gt;This is what fixes the asChild vs render problem mentioned earlier. The skill tells the AI "this project uses Base UI, use &lt;code&gt;render&lt;/code&gt;", and the AI listens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool 2: The shadcn MCP server
&lt;/h2&gt;

&lt;p&gt;The skill is great for context, but it does not add new components for you. That is what the MCP server does.&lt;/p&gt;

&lt;p&gt;MCP stands for &lt;strong&gt;Model Context Protocol&lt;/strong&gt;. It is a standard that lets AI assistants talk to external tools and services. The shadcn MCP server gives your AI a direct connection to component registries (the official shadcn registry, plus any custom ones you add).&lt;/p&gt;

&lt;p&gt;With it installed, the AI can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search across thousands of blocks and components&lt;/li&gt;
&lt;li&gt;preview a component before installing&lt;/li&gt;
&lt;li&gt;actually run the install command for you&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Install for Claude Code
&lt;/h3&gt;

&lt;p&gt;In your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx shadcn@latest mcp init &lt;span class="nt"&gt;--client&lt;/span&gt; claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After it finishes, restart Claude Code and run &lt;code&gt;/mcp&lt;/code&gt; to confirm the shadcn server shows up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install for Cursor
&lt;/h3&gt;

&lt;p&gt;Add the shadcn server to &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project. Check the &lt;a href="https://ui.shadcn.com/docs/mcp" rel="noopener noreferrer"&gt;shadcn MCP docs&lt;/a&gt; for the exact JSON since the format updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install for VS Code with GitHub Copilot
&lt;/h3&gt;

&lt;p&gt;Add the shadcn server to &lt;code&gt;.vscode/mcp.json&lt;/code&gt; in your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes after you install it
&lt;/h3&gt;

&lt;p&gt;You can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Show me all available components in the shadcn registry"&lt;/p&gt;

&lt;p&gt;"Add the button, dialog, and card components to my project"&lt;/p&gt;

&lt;p&gt;"Find me a pricing page block"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it just works. No more "what is the install command for X again", no more searching the docs in another tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skill or MCP, which one do I need?
&lt;/h2&gt;

&lt;p&gt;Honestly, install both. They solve different problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Knows your project setup&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generates correct API code&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (via registry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can install new components for you&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Searches registries&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The skill is "give my AI context about my project". The MCP server is "give my AI hands to actually do registry stuff". Together they make your AI feel like it actually works on your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;Here is the difference in practice. Junior dev has a Base UI shadcn project, and asks the AI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without skill or MCP:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@radix-ui/react-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt; &lt;span class="na"&gt;asChild&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrong import (project uses Base UI, not Radix), and &lt;code&gt;asChild&lt;/code&gt; does not exist in Base UI. The dev now spends 10 minutes fixing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With skill installed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/ui/dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Open
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct import path, correct API. Zero fighting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick troubleshooting
&lt;/h2&gt;

&lt;p&gt;A few things that bit me when I tried this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The skill needs &lt;code&gt;components.json&lt;/code&gt; in your project&lt;/strong&gt;. If you never ran &lt;code&gt;shadcn init&lt;/code&gt;, the skill has nothing to read. Run &lt;code&gt;pnpm dlx shadcn@latest init&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart your AI editor after install&lt;/strong&gt;. Hot reload does not pick up new skills or MCP servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;/mcp&lt;/code&gt; in Claude Code&lt;/strong&gt; to confirm the server is connected. If it is not, the install probably failed silently. Run it again and watch the output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The first time AI auto installs the right shadcn component, with the right API, on the right engine, it feels like cheating. It is not cheating, it is just giving the AI the same context a human team member would have.&lt;/p&gt;

&lt;p&gt;If you are on shadcn/ui in 2026 and your AI is still generating wrong code, you are working with one hand tied behind your back. Spend 5 minutes installing the skill. Future you will thank you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>shadcn</category>
      <category>react</category>
      <category>productivity</category>
    </item>
    <item>
      <title>shadcn vs Radix vs Base UI: Which One Should a Junior Pick in 2026?</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sun, 03 May 2026 11:20:16 +0000</pubDate>
      <link>https://forem.com/edriso/shadcn-vs-radix-vs-base-ui-which-one-should-a-junior-pick-in-2026-1jml</link>
      <guid>https://forem.com/edriso/shadcn-vs-radix-vs-base-ui-which-one-should-a-junior-pick-in-2026-1jml</guid>
      <description>&lt;p&gt;If you spent any time on React Twitter or LinkedIn lately, you saw three names everywhere: &lt;strong&gt;shadcn/ui&lt;/strong&gt;, &lt;strong&gt;Radix&lt;/strong&gt;, and &lt;strong&gt;Base UI&lt;/strong&gt;. People talk about them like they compete with each other, but they don't really. Let me explain what each one actually is, and when you should reach for which.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what is a "headless" UI library?
&lt;/h2&gt;

&lt;p&gt;Before we compare anything, you need this idea.&lt;/p&gt;

&lt;p&gt;A normal UI library like Bootstrap or Material UI gives you components that already look a certain way. You import a &lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt; and it comes with colors, padding, hover effects, the full package. You can override the styles, but you are fighting the library.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;headless&lt;/strong&gt; library does the opposite. It gives you the &lt;strong&gt;behavior&lt;/strong&gt; of a component (open and close, keyboard navigation, focus management, accessibility) without any styling at all. You bring your own CSS. You decide how it looks.&lt;/p&gt;

&lt;p&gt;Think of it like this: a headless library is the engine of a car. shadcn, Bootstrap, MUI are full cars. You can swap the engine, but the car already has paint.&lt;/p&gt;

&lt;p&gt;This is important because the boring part of building a UI is not the colors. It is making a dropdown that closes when you press Esc, returns focus to the right place, traps focus in a modal, supports arrow keys in a menu, and works with screen readers. That stuff is HARD. Headless libraries solve it once so you don't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Radix UI
&lt;/h2&gt;

&lt;p&gt;Radix is a headless library by the team behind WorkOS (they bought Radix). It gives you primitives like &lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;DropdownMenu&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Tabs&amp;gt;&lt;/code&gt;, and so on, fully accessible, no styles.&lt;/p&gt;

&lt;p&gt;It became the default choice in the React ecosystem over the past few years. Vercel, Linear, Supabase, and a huge chunk of modern apps use it under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why people love it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;battle tested, used in millions of production apps&lt;/li&gt;
&lt;li&gt;great accessibility out of the box&lt;/li&gt;
&lt;li&gt;clean API with the &lt;code&gt;asChild&lt;/code&gt; pattern (more on this below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest downside:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Updates have slowed down a lot since the WorkOS acquisition. Many devs feel it has reached its peak and won't get major new features. It is stable, but stable can mean "frozen".&lt;/p&gt;

&lt;h2&gt;
  
  
  Base UI
&lt;/h2&gt;

&lt;p&gt;Base UI is the new kid, started in 2024 by people from Radix, MUI, and Floating UI (so basically the dream team of headless React). It is also a headless primitives library, very similar API to Radix, but with some upgrades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why people are excited:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;actively developed (7+ engineers shipping new releases regularly)&lt;/li&gt;
&lt;li&gt;ships components Radix doesn't have, like &lt;code&gt;Combobox&lt;/code&gt; and &lt;code&gt;Autocomplete&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;better focus on performance and tiny modern a11y wins (for example, accordions automatically open when the user does Ctrl+F to search hidden text)&lt;/li&gt;
&lt;li&gt;API is close enough to Radix that migrating is mostly find and replace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest downside:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Newer means smaller community, fewer Stack Overflow answers, and your AI assistant might not know it as well (we will fix that in the next post).&lt;/p&gt;

&lt;h2&gt;
  
  
  The asChild vs render difference
&lt;/h2&gt;

&lt;p&gt;This is one of the few API differences worth knowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Radix uses &lt;code&gt;asChild&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@radix-ui/react-dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt; &lt;span class="na"&gt;asChild&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-custom-button"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;asChild&lt;/code&gt; tells Radix: "don't render your own element, merge your behavior into my child instead".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Base UI uses &lt;code&gt;render&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Dialog&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@base-ui-components/react/dialog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-custom-button"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Open
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea, different shape. Base UI's &lt;code&gt;render&lt;/code&gt; prop is a bit more flexible because you can also pass a function for full control.&lt;/p&gt;

&lt;h2&gt;
  
  
  shadcn/ui
&lt;/h2&gt;

&lt;p&gt;Now the third name. shadcn/ui is &lt;strong&gt;not&lt;/strong&gt; a headless library. It is also not really a library at all in the npm sense.&lt;/p&gt;

&lt;p&gt;It is a collection of pre-styled, copy paste components built on top of Radix or Base UI, using Tailwind CSS. When you want a &lt;code&gt;Button&lt;/code&gt; or &lt;code&gt;Dialog&lt;/code&gt;, you run a CLI command and it copies the component code directly into your project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx shadcn@latest add button
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now own that component. You can edit it, delete props, add variants. There is no &lt;code&gt;node_modules/shadcn-ui&lt;/code&gt; to fight with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this changed everything:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you get gorgeous defaults that you can fully customize because the code is yours&lt;/li&gt;
&lt;li&gt;a11y is handled by the underlying Radix or Base UI primitive&lt;/li&gt;
&lt;li&gt;you don't carry the weight of components you never use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Big news from late 2025:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;shadcn/ui now supports &lt;strong&gt;both&lt;/strong&gt; Radix and Base UI as the underlying engine. When you start a project, you pick one. Every component was rebuilt for Base UI while keeping the same API.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what should you actually pick?
&lt;/h2&gt;

&lt;p&gt;Here is my honest junior friendly advice:&lt;/p&gt;

&lt;h3&gt;
  
  
  If you are starting a new project today:
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;shadcn/ui with Base UI&lt;/strong&gt;. You get pretty defaults you control, accessibility for free, and you are betting on the library that is actively growing instead of the one that is frozen.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you are joining an existing team:
&lt;/h3&gt;

&lt;p&gt;Use whatever they already use. If it is Radix, don't migrate just because Base UI is trendy. Radix in production today is fine. It works.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you need a component Radix doesn't have:
&lt;/h3&gt;

&lt;p&gt;Like Combobox or Autocomplete, just go with Base UI. You will save yourself a week of building it from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you want a fully styled out of the box library:
&lt;/h3&gt;

&lt;p&gt;Then you don't want any of these three. Look at MUI, Mantine, or Chakra. shadcn, Radix, and Base UI all assume you are styling things yourself with Tailwind or CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple decision tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Need full control over styles?
├── Yes
│   └── Want a copy paste starter with nice defaults?
│       ├── Yes → shadcn/ui (pick Base UI as the engine)
│       └── No → Base UI directly (or Radix if your team uses it)
└── No → MUI, Mantine, or Chakra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The whole "Radix vs Base UI" debate online is loud, but in practice the difference is small for a junior. Both give you accessible, headless primitives. Both work great. Base UI is the safer long term bet because the team is shipping, but you cannot go wrong with either.&lt;/p&gt;

&lt;p&gt;The real win is just stopping yourself from building a custom modal from scratch in 2026. That is the bug.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>ui</category>
    </item>
    <item>
      <title>Web Accessibility (A11y) for Juniors: What It Is and Why It Matters</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sun, 03 May 2026 11:18:36 +0000</pubDate>
      <link>https://forem.com/edriso/web-accessibility-a11y-for-juniors-what-it-is-and-why-it-matters-2eoe</link>
      <guid>https://forem.com/edriso/web-accessibility-a11y-for-juniors-what-it-is-and-why-it-matters-2eoe</guid>
      <description>&lt;p&gt;You probably saw the word &lt;strong&gt;a11y&lt;/strong&gt; on Twitter, in a job post, or in a code review comment, and wondered what it actually means. So let me explain it the way I wish someone explained it to me when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is A11y?
&lt;/h2&gt;

&lt;p&gt;A11y is a short way of writing &lt;strong&gt;accessibility&lt;/strong&gt;. The number 11 is just the count of letters between the &lt;code&gt;a&lt;/code&gt; and the &lt;code&gt;y&lt;/code&gt; in the word &lt;code&gt;accessibility&lt;/code&gt;. That is it. No hidden meaning.&lt;/p&gt;

&lt;p&gt;Accessibility means building websites and apps that everyone can actually use, including people who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can't see well or can't see at all and use a screen reader&lt;/li&gt;
&lt;li&gt;can't use a mouse and only navigate with the keyboard&lt;/li&gt;
&lt;li&gt;have low color vision and can't tell red from green&lt;/li&gt;
&lt;li&gt;have motor issues and can't click tiny buttons&lt;/li&gt;
&lt;li&gt;are on a slow phone, in bright sunlight, or in a noisy room&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a11y is not a "feature for blind users". It is just good engineering that respects how different humans interact with your UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should you care as a junior?
&lt;/h2&gt;

&lt;p&gt;Three honest reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It is the law in many places.&lt;/strong&gt; In the EU, the European Accessibility Act started applying in 2025. In the US, the ADA Title II deadline hits in April 2026 for public sector sites. Big clients will not hire teams that ship inaccessible products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Better a11y means better UX for everyone.&lt;/strong&gt; Captions help deaf users, but they also help anyone watching a video on the bus without headphones. Keyboard support helps blind users, but it also helps power users who hate touching the mouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It makes you stand out.&lt;/strong&gt; Most juniors do not know a11y. If you do, you are already different from 80% of the candidates applying to the same job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 principles (POUR)
&lt;/h2&gt;

&lt;p&gt;WCAG (the official accessibility guidelines, currently version 2.2) groups everything under 4 ideas. The acronym is POUR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P&lt;/strong&gt;erceivable: users can sense the content (with eyes, ears, or fingers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;O&lt;/strong&gt;perable: users can interact with it (mouse, keyboard, touch, voice)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;U&lt;/strong&gt;nderstandable: users can read it and predict what happens next&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R&lt;/strong&gt;obust: it keeps working in different browsers and assistive tech&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't memorize this. Just remember: if a real human can't perceive, operate, understand, or rely on your UI, you have an a11y problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real examples you can fix today
&lt;/h2&gt;

&lt;p&gt;Let me show you the most common mistakes I see in junior code, and the fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Using &lt;code&gt;div&lt;/code&gt; for buttons
&lt;/h3&gt;

&lt;p&gt;This looks fine in the browser, but a screen reader will not announce it as a button, and you can't reach it with the Tab key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"submit()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"submit()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; gives you keyboard focus, Enter and Space activation, and the right ARIA role for free. You get all that by typing 4 letters less. Use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Images with no alt text
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"profile.jpg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"profile.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Mohamed smiling at his graduation"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- also good if the image is just decoration --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"sparkle.svg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty &lt;code&gt;alt=""&lt;/code&gt; is not lazy, it is correct for decorative images. It tells screen readers to skip them. Missing alt completely is the bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Form inputs with no label
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- bad --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- good --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Placeholders disappear when the user types and they are usually low contrast. A real &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; stays, gets read by screen readers, and lets the user click the label to focus the input.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Color as the only signal
&lt;/h3&gt;

&lt;p&gt;Look at this error message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"color: red"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email is invalid&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user with color blindness might not see the red at all. Add an icon or text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"color: red"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✗&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt; Email is invalid
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Low contrast text
&lt;/h3&gt;

&lt;p&gt;Light grey text on white looks "clean and minimal" but it is unreadable for many users. WCAG asks for a contrast ratio of at least &lt;strong&gt;4.5:1&lt;/strong&gt; for normal text. Use a tool like &lt;a href="https://webaim.org/resources/contrastchecker/" rel="noopener noreferrer"&gt;WebAIM Contrast Checker&lt;/a&gt; or your browser DevTools to check.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Custom components without keyboard support
&lt;/h3&gt;

&lt;p&gt;If you build a custom dropdown, modal, or tabs component, ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I open and close it with only the keyboard?&lt;/li&gt;
&lt;li&gt;Does Tab move focus in a logical order?&lt;/li&gt;
&lt;li&gt;Does Esc close the modal?&lt;/li&gt;
&lt;li&gt;Does focus return to the trigger button when the modal closes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to any of these is no, your component has a bug. This is exactly why headless libraries like Radix and Base UI exist. They handle this for you (more on that in another post).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools that will save you
&lt;/h2&gt;

&lt;p&gt;You don't have to memorize WCAG. Use tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;axe DevTools&lt;/strong&gt; browser extension: scans your page and lists violations with fixes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse&lt;/strong&gt; (built into Chrome): gives you an a11y score&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VoiceOver&lt;/strong&gt; (Mac, Cmd+F5) or &lt;strong&gt;NVDA&lt;/strong&gt; (Windows, free): try using your own site with eyes closed for 60 seconds. Trust me, you will feel every bug&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab key&lt;/strong&gt;: just press Tab through your page. If focus disappears or jumps around weirdly, you have an issue&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple checklist before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every image has &lt;code&gt;alt&lt;/code&gt; (or &lt;code&gt;alt=""&lt;/code&gt; if decorative)&lt;/li&gt;
&lt;li&gt;[ ] Every form input has a real &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] You can use the whole page with only the keyboard&lt;/li&gt;
&lt;li&gt;[ ] Focus is always visible (don't kill the focus ring with &lt;code&gt;outline: none&lt;/code&gt; and nothing else)&lt;/li&gt;
&lt;li&gt;[ ] Color contrast is 4.5:1 or higher for text&lt;/li&gt;
&lt;li&gt;[ ] Headings go in order (&lt;code&gt;h1&lt;/code&gt;, then &lt;code&gt;h2&lt;/code&gt;, then &lt;code&gt;h3&lt;/code&gt;, no skipping)&lt;/li&gt;
&lt;li&gt;[ ] Buttons are &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, links are &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;. Don't mix them up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A11y is not a separate phase you do at the end. It is part of writing good HTML. Most of the fixes above cost you nothing extra and take 10 seconds. Start with semantic HTML, test with your keyboard, and you are already ahead of most devs.&lt;/p&gt;

&lt;p&gt;In the next post I will show how libraries like Radix, Base UI, and shadcn handle the harder a11y patterns for you, so you don't have to reinvent a focus trap every time you build a modal.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>frontend</category>
      <category>beginners</category>
    </item>
    <item>
      <title>A No-Build Markdown Site for Study Notes (or any docs)</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Fri, 01 May 2026 16:59:15 +0000</pubDate>
      <link>https://forem.com/edriso/a-no-build-markdown-site-for-study-notes-or-any-docs-83e</link>
      <guid>https://forem.com/edriso/a-no-build-markdown-site-for-study-notes-or-any-docs-83e</guid>
      <description>&lt;p&gt;I needed a way to study a stack of notes I had written for a job interview. Eight markdown files, plus some code examples, plus a couple of reference pages. I wanted to read them in a browser with a sidebar, dark mode, and clickable navigation between files. But I did not want to install a static site generator, run a dev server, or maintain a build pipeline for what is basically a personal study folder.&lt;/p&gt;

&lt;p&gt;So I built a single-page viewer in one HTML file that reads the markdown directly. The whole site is one &lt;code&gt;index.html&lt;/code&gt; plus the markdown files. You open the HTML in a browser, it fetches the markdown, renders it with &lt;a href="https://marked.js.org" rel="noopener noreferrer"&gt;marked&lt;/a&gt;, and you have a real navigable site.&lt;/p&gt;

&lt;p&gt;This post walks you through the whole pattern. By the end you will be able to drop the same setup into any folder of notes you want to read like a website.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;A folder that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-notes/
├── README.md
├── index.html
├── docs/
│   ├── 01-introduction.md
│   ├── 02-setup.md
│   └── 03-deeper-topics.md
└── code-examples/
    └── hello.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you open &lt;code&gt;index.html&lt;/code&gt; in a browser you get a sidebar on the left listing every doc, and the rest of the page renders the selected markdown file with code highlighting, tables, and proper typography. Click a link in the sidebar, the URL changes to &lt;code&gt;index.html#docs/02-setup.md&lt;/code&gt;, and the content swaps. No page reload, no build step, no node_modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this approach
&lt;/h2&gt;

&lt;p&gt;Most "docs as code" setups are heavier than they need to be for a small personal project. They assume you want a public site, search, versioning, or a custom theme. For a study folder or a small team handbook, you do not need any of that. You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Markdown files you can edit in any editor.&lt;/li&gt;
&lt;li&gt;A way to read them in a browser without typing &lt;code&gt;cat file.md&lt;/code&gt; in a terminal.&lt;/li&gt;
&lt;li&gt;Navigation between files that does not require remembering which folder you are in.&lt;/li&gt;
&lt;li&gt;Something that works the same on Windows, macOS, and Linux.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A single HTML file that renders markdown gives you all four. It is also harder to break than a tooling chain, because there is almost nothing to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder structure
&lt;/h2&gt;

&lt;p&gt;Before the code, let us talk about the organising principles. The exact names do not matter; what matters is the pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  One README at the top, as an index
&lt;/h3&gt;

&lt;p&gt;The top-level &lt;code&gt;README.md&lt;/code&gt; is not the first chapter. It is a table of contents. Its job is to tell a new reader where to start and what each file contains.&lt;/p&gt;

&lt;p&gt;A good index README has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A one-paragraph description of what this folder is.&lt;/li&gt;
&lt;li&gt;A small table linking each file with a short hook ("what you will learn") and an estimated reading time.&lt;/li&gt;
&lt;li&gt;Maybe a "TL;DR" if you only have ten minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a snippet of what mine looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Read in this order&lt;/span&gt;

| # | Document | What you'll learn | Time |
|---|----------|-------------------|------|
| 1 | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Introduction&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;docs/01-introduction.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | What this is for | 5 min |
| 2 | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;docs/02-setup.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | How to install everything | 10 min |
| 3 | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Deeper topics&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;docs/03-deeper-topics.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | The interesting bits | 25 min |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reader who lands on the folder for the first time can immediately tell where to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbered file names for ordering
&lt;/h3&gt;

&lt;p&gt;Notice the &lt;code&gt;01-&lt;/code&gt;, &lt;code&gt;02-&lt;/code&gt;, &lt;code&gt;03-&lt;/code&gt; prefixes. They serve two purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The OS sorts the files alphabetically, so the reading order is also the file-listing order.&lt;/li&gt;
&lt;li&gt;The number is a stable handle. You can refer to "doc 04" in conversation and find it instantly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you later want to insert a new file between 02 and 03, name it &lt;code&gt;02b-...&lt;/code&gt; or just renumber. Renumbering is annoying but rare in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Group by purpose, not by file type
&lt;/h3&gt;

&lt;p&gt;I see a lot of folders where everything goes in &lt;code&gt;/docs/&lt;/code&gt; because it is markdown. That is fine when you have five files. When you have twenty it stops being fine.&lt;/p&gt;

&lt;p&gt;Group by what the content does for the reader, not by file extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-notes/
├── docs/             &amp;lt;- the actual narrative documents
├── code-examples/    &amp;lt;- runnable snippets the docs reference
├── reference/        &amp;lt;- looked-up not read-through (glossary, cheat sheet)
└── assets/           &amp;lt;- images and diagrams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reader reaches for &lt;code&gt;code-examples/&lt;/code&gt; because they want code, not because it is markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  One file per concept, short and focused
&lt;/h3&gt;

&lt;p&gt;If a doc is over 600 lines, split it. Each markdown file should answer one question. The interview prep had eight files, each between 100 and 400 lines. None of them tried to be a textbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing principles I used in the docs
&lt;/h2&gt;

&lt;p&gt;A quick aside before the HTML viewer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lead with a sentence that previews the doc
&lt;/h3&gt;

&lt;p&gt;Every doc opens with a single sentence in italics or quotes that tells you what to expect. That way a reader skimming the index can decide whether to commit five minutes to reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tables for reference, prose for narrative
&lt;/h3&gt;

&lt;p&gt;Tables are great for "look up what this does." They are terrible for "explain this concept." Mixing both keeps a doc readable. The tech-stack page in my prep folder is mostly prose with one or two tables. The glossary is one big table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show code with the why
&lt;/h3&gt;

&lt;p&gt;Every code block should be preceded by a sentence that says what it is and why. Code without context is just decoration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoid em dashes and other typography tricks
&lt;/h3&gt;

&lt;p&gt;I deliberately stick to commas, parentheses, and full stops. Em dashes look fancy but they often hide a sentence that should have been split in two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-file HTML viewer
&lt;/h2&gt;

&lt;p&gt;Now the fun part. Here is the whole HTML file in pieces.&lt;/p&gt;

&lt;h3&gt;
  
  
  The skeleton
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My Notes&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;/* styles go here */&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My Notes&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#README.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#docs/01-introduction.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Introduction&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#docs/02-setup.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Setup&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#docs/03-deeper-topics.md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Deeper topics&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"loader"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/marked@13.0.0/marked.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
      &lt;span class="cm"&gt;/* router goes here */&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it for the structure. Two main blocks (a sidebar and a content pane), one CDN script for markdown rendering, one script for the routing logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layout with CSS Grid
&lt;/h3&gt;

&lt;p&gt;The sidebar plus content layout is one CSS Grid declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.6&lt;/span&gt; &lt;span class="n"&gt;-apple-system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlinkMacSystemFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Segoe UI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;280px&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;280px 1fr&lt;/code&gt; means the sidebar is a fixed 280 pixels wide, and the rest of the row stretches to fill the remaining space. Three lines, no flexbox, no media queries needed at this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sticky sidebar
&lt;/h3&gt;

&lt;p&gt;You want the sidebar to stay visible when the content scrolls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;nav&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;position: sticky&lt;/code&gt; plus &lt;code&gt;top: 0&lt;/code&gt; plus &lt;code&gt;height: 100vh&lt;/code&gt; makes the sidebar pin itself to the top and become its own scroll container. Three properties, no JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dark mode for free
&lt;/h3&gt;

&lt;p&gt;You can detect the user's system preference with &lt;code&gt;prefers-color-scheme&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0e1116&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e6edf3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#58a6ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#30363d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1c2128&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1f2328&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0969da&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d1d9e0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f6f8fa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define your colours as CSS custom properties (variables) once. Override them inside a &lt;code&gt;prefers-color-scheme: light&lt;/code&gt; block. Use the variables everywhere else. The OS theme switch flips the whole page automatically.&lt;/p&gt;

&lt;p&gt;If you want a manual toggle, you can store a class on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; (&lt;code&gt;light&lt;/code&gt; or &lt;code&gt;dark&lt;/code&gt;) and override the variables based on the class instead of the media query. But for a personal study folder, following the OS preference is enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile responsive in three lines
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;800px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;nav&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;static&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On narrow screens, collapse the grid to a single column, unstick the sidebar, and shrink padding. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The router (the JavaScript part)
&lt;/h2&gt;

&lt;p&gt;The interesting trick is using the URL hash as the route. When you click &lt;code&gt;&amp;lt;a href="#docs/01-introduction.md"&amp;gt;&lt;/code&gt;, the browser changes the hash but does not reload the page. We listen for that change and load the right markdown file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: load and render
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;README.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;div class="loader"&amp;gt;Loading...&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;h2&amp;gt;Could not load &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h2&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetch(path)&lt;/code&gt; reads the markdown file from disk (or rather, from wherever the HTML is being served). &lt;code&gt;marked.parse(markdown)&lt;/code&gt; turns the markdown text into HTML. We dump that HTML into the content area.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: react to URL changes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onHashChange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// strip the leading "#"&lt;/span&gt;
  &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hashchange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onHashChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;onHashChange&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// also run once on initial page load&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user clicks a sidebar link, &lt;code&gt;location.hash&lt;/code&gt; changes, the &lt;code&gt;hashchange&lt;/code&gt; event fires, and we reload the content. When they reload the page, the same code runs once and shows the right doc based on the URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: rewrite relative links inside the markdown
&lt;/h3&gt;

&lt;p&gt;This is the bit that trips most people up. If your doc says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;See &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;02-setup.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That link, after rendering, points to &lt;code&gt;02-setup.md&lt;/code&gt;, which the browser tries to fetch as a real page. It will fail (or load the raw markdown), not navigate inside our viewer.&lt;/p&gt;

&lt;p&gt;We need to rewrite those links to use the hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Resolve relative to the current doc's directory&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;README.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;currentDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;currentDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;currentDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This walks every link the renderer just produced. If it points to a markdown file with a relative path, we resolve it relative to the current doc's directory and prepend a &lt;code&gt;#&lt;/code&gt;. Now &lt;code&gt;[setup](02-setup.md)&lt;/code&gt; from inside &lt;code&gt;docs/01-introduction.md&lt;/code&gt; becomes &lt;code&gt;#docs/02-setup.md&lt;/code&gt;, which our router knows how to handle.&lt;/p&gt;

&lt;p&gt;You only need this if you write relative links between your docs. If every link in the markdown is absolute (&lt;code&gt;/docs/...&lt;/code&gt;), you can skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one caveat: file:// and CORS
&lt;/h2&gt;

&lt;p&gt;If you double-click &lt;code&gt;index.html&lt;/code&gt; in your file manager, the browser opens it from &lt;code&gt;file://&lt;/code&gt;. Some browsers (Chrome, in particular) block &lt;code&gt;fetch()&lt;/code&gt; from &lt;code&gt;file://&lt;/code&gt; for security reasons. Your viewer will load with the sidebar, then show "Could not load" when it tries to fetch the markdown.&lt;/p&gt;

&lt;p&gt;There are two easy fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 1: serve the folder with any tiny HTTP server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pick whichever you have&lt;/span&gt;
npx http-server &lt;span class="nb"&gt;.&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; (or &lt;code&gt;:8000&lt;/code&gt;) instead of the file path. Done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 2: use Firefox
&lt;/h3&gt;

&lt;p&gt;Firefox allows &lt;code&gt;fetch()&lt;/code&gt; from &lt;code&gt;file://&lt;/code&gt; by default. If you do not want a server, just use Firefox.&lt;/p&gt;

&lt;p&gt;I documented this clearly in the loader's error message so a reader is never confused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;lt;h2&amp;gt;Could not load &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h2&amp;gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;If you opened this file directly via &amp;lt;code&amp;gt;file://&amp;lt;/code&amp;gt;, `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`your browser may be blocking &amp;lt;code&amp;gt;fetch&amp;lt;/code&amp;gt;. Run a tiny `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`server in this folder instead:&amp;lt;/p&amp;gt;`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;lt;pre&amp;gt;npx http-server .&amp;lt;/pre&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A graceful error message is the best documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling tips for nice rendering
&lt;/h2&gt;

&lt;p&gt;Marked outputs plain HTML (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;). You can style them like any other page. A few things make the result look more like GitHub or a real docs site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;pre&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--code-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--code-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.92em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;blockquote&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-collapse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underlined H1 and H2, padded code blocks with rounded corners, blockquotes with a left accent stripe, properly bordered tables. Maybe twenty lines of CSS gets you a result that looks intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you outgrow this
&lt;/h2&gt;

&lt;p&gt;This pattern is great until you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search across all docs.&lt;/li&gt;
&lt;li&gt;Build-time syntax highlighting (the marked CDN does not include it, although you can add &lt;code&gt;highlight.js&lt;/code&gt; the same way).&lt;/li&gt;
&lt;li&gt;Versioning (multiple snapshots of the same docs).&lt;/li&gt;
&lt;li&gt;A public site with a domain and analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, take a look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.mkdocs.org" rel="noopener noreferrer"&gt;MkDocs&lt;/a&gt; with the Material theme. Best balance of "barely any config" and "production-ready output."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docusaurus.io" rel="noopener noreferrer"&gt;Docusaurus&lt;/a&gt; if you want React components inside your markdown.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://astro.build" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; if you want a faster site with more flexibility.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pages.github.com" rel="noopener noreferrer"&gt;GitHub Pages&lt;/a&gt; plus Jekyll if you just want to publish a folder of markdown to the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everything else, the no-build approach holds up surprisingly well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;A folder of markdown plus a single HTML viewer gets you most of what a docs site does, with no install step and no maintenance burden. The pattern is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Numbered markdown files in a &lt;code&gt;docs/&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;README.md&lt;/code&gt; at the top that is an index, not a chapter.&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;index.html&lt;/code&gt; with CSS Grid layout, a sticky sidebar, and CSS variables for dark mode.&lt;/li&gt;
&lt;li&gt;A small router that listens to &lt;code&gt;hashchange&lt;/code&gt;, fetches the markdown, parses it with marked, and rewrites relative links.&lt;/li&gt;
&lt;li&gt;A clear error message when &lt;code&gt;file://&lt;/code&gt; blocks &lt;code&gt;fetch&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to study something, document something, or share notes with a small team, this is the smallest amount of effort that gets you a real navigable site. Try it on your next folder of notes.&lt;/p&gt;

&lt;p&gt;Happy writing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>markdown</category>
    </item>
    <item>
      <title>How to Reset Your MySQL Root Password on Ubuntu (When Nothing Works)</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Fri, 01 May 2026 13:25:13 +0000</pubDate>
      <link>https://forem.com/edriso/how-to-reset-your-mysql-root-password-on-ubuntu-when-nothing-works-64</link>
      <guid>https://forem.com/edriso/how-to-reset-your-mysql-root-password-on-ubuntu-when-nothing-works-64</guid>
      <description>&lt;p&gt;I was working on a Laravel task and got this error in my logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: YES)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;.env&lt;/code&gt; file had &lt;code&gt;DB_PASSWORD=password&lt;/code&gt;, but MySQL was rejecting it. I tried the usual tricks and ran into a wall. Here is what was happening, what fixed it, and a few small things that confused me along the way. If you are a junior dev and any of this sounds familiar, this post is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check if your Laravel .env is the problem first
&lt;/h2&gt;

&lt;p&gt;Before anything wild, open your &lt;code&gt;.env&lt;/code&gt; file and check these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;mysql&lt;/span&gt;
&lt;span class="py"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
&lt;span class="py"&gt;DB_PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3306&lt;/span&gt;
&lt;span class="py"&gt;DB_DATABASE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;my_app&lt;/span&gt;
&lt;span class="py"&gt;DB_USERNAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your password is blank or wrong, just fix it and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan config:clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. If the password really is what you think it is and MySQL still rejects it, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Try the classic Ubuntu trick
&lt;/h2&gt;

&lt;p&gt;On a lot of Ubuntu installs, the MySQL root user does not use a password at all. It uses something called socket authentication, which means you log in by being a Linux superuser. So this usually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that opens a MySQL prompt, great. You can set a real password from inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'root'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;mysql_native_password&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in my case I got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means root already had a password set, just not one I knew. Time for the real recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Reset the password using an init file
&lt;/h2&gt;

&lt;p&gt;The safest way to reset a forgotten root password is to start MySQL with a special init file. The init file runs SQL the moment MySQL boots, so you can change the password without ever logging in.&lt;/p&gt;

&lt;p&gt;Here is a small bash script that does the whole thing. Save it as &lt;code&gt;mysql-reset.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;NEW_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;
&lt;span class="nv"&gt;INIT_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; /tmp/mysql-init-XXXXXX.sql&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;SQL&lt;/span&gt;&lt;span class="sh"&gt;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_PASS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;';
FLUSH PRIVILEGES;
&lt;/span&gt;&lt;span class="no"&gt;SQL

&lt;/span&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;mysql:mysql &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Stopping mysql..."&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop mysql

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting mysql with the init file..."&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysqld &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql &lt;span class="nt"&gt;--init-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--daemonize&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Stopping the temporary mysqld..."&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pkill &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"mysqld --user=mysql --init-file=&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
sleep &lt;/span&gt;2

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting mysql normally..."&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start mysql

&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INIT_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Testing the new password..."&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_PASS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT 'OK' AS status;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash mysql-reset.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will ask for your sudo password. After it finishes, root has the password you set. Mine is now &lt;code&gt;password&lt;/code&gt;, which matches my Laravel &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create your database if it does not exist yet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-ppassword&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"CREATE DATABASE IF NOT EXISTS my_app;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see a warning that goes like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysql: [Warning] Using a password on the command line interface can be insecure.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is just a warning, not an error. Your command still ran. We will fix that warning in the last step.&lt;/p&gt;

&lt;p&gt;Now run your migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your tables get created, you are back in business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two confusing errors that tripped me up
&lt;/h2&gt;

&lt;p&gt;After the reset I tried a couple of things that looked broken but were actually fine. If you see these, do not panic.&lt;/p&gt;

&lt;h3&gt;
  
  
  "sudo mysql" stops working
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ALTER USER 'root'@'localhost' ..."&lt;/span&gt;
ERROR 1045 &lt;span class="o"&gt;(&lt;/span&gt;28000&lt;span class="o"&gt;)&lt;/span&gt;: Access denied &lt;span class="k"&gt;for &lt;/span&gt;user &lt;span class="s1"&gt;'root'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;using password: NO&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct now. We changed root from socket login to password login. So &lt;code&gt;sudo mysql&lt;/code&gt; cannot just walk in anymore. You have to give the password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Just typing "mysql" fails
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mysql
ERROR 1045 (28000): Access denied for user 'your_username'@'localhost' (using password: NO)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run &lt;code&gt;mysql&lt;/code&gt; with no flags, it tries to log in using your Linux username with no password. There is no MySQL user with your Linux name, so it fails. This is normal. You need &lt;code&gt;-u root -p&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Stop typing your password every time
&lt;/h2&gt;

&lt;p&gt;This was the nicest little win. You can put your client credentials in a config file and the &lt;code&gt;mysql&lt;/code&gt; command will read it automatically.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/.my.cnf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[client]&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;
&lt;span class="py"&gt;password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lock down the permissions so other users on the machine cannot read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.my.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this just works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No prompt, no flags, no warning. Quick test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT CURRENT_USER();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;root@localhost&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer setup for real projects
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;root&lt;/code&gt; for your app is fine on a learning machine, but on anything you care about, make a dedicated user with access to one database only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;my_app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'my_app_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'a_real_password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;my_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'my_app_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your &lt;code&gt;.env&lt;/code&gt; at that user. If the app password ever leaks, your other databases are still safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check the &lt;code&gt;.env&lt;/code&gt; first. The simplest fix is usually the right one.&lt;/li&gt;
&lt;li&gt;Try &lt;code&gt;sudo mysql&lt;/code&gt;. If it works, you are using socket auth and you can set a password right there.&lt;/li&gt;
&lt;li&gt;If root already has a password you do not know, reset it with an init file. Do not edit &lt;code&gt;mysql.user&lt;/code&gt; by hand and do not run with &lt;code&gt;--skip-grant-tables&lt;/code&gt; if you can avoid it.&lt;/li&gt;
&lt;li&gt;After the reset, remember that &lt;code&gt;sudo mysql&lt;/code&gt; will not work anymore. Use &lt;code&gt;mysql -u root -p&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Drop a &lt;code&gt;~/.my.cnf&lt;/code&gt; with mode 600 so you can just type &lt;code&gt;mysql&lt;/code&gt; and get in.&lt;/li&gt;
&lt;li&gt;For real projects, do not use root. Make an app user with access to one database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hope this saves someone an hour. Forgetting a database password feels scary, but the recovery is short once you know the steps.&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>ubuntu</category>
      <category>laravel</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I made a swing boat that reacts to doom scrolling</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sat, 25 Apr 2026 15:53:48 +0000</pubDate>
      <link>https://forem.com/edriso/i-made-a-swing-boat-that-reacts-to-doom-scrolling-1cmh</link>
      <guid>https://forem.com/edriso/i-made-a-swing-boat-that-reacts-to-doom-scrolling-1cmh</guid>
      <description>&lt;p&gt;When I was a kid, I tried the swing boat a few times and hated it. It made me dizzy. The kind of dizzy where you are not sure if it is fun anymore. You start slow, then someone keeps pushing, and before you know it you are too high and your stomach does not agree.&lt;/p&gt;

&lt;p&gt;I got that same feeling recently, but from scrolling.&lt;/p&gt;

&lt;p&gt;Doom scrolling is that thing you do when you open your phone for no reason and close it twenty minutes later having seen nothing worth remembering. Your finger goes up and down, up and down, the same motion as a swing. Your brain sits there getting rocked back and forth until it is numb. That is the brain rot part. Not dramatic, just slow and quiet. Your attention gets shorter. Your mood gets worse. You feel tired but you also cannot stop.&lt;/p&gt;

&lt;p&gt;I kept thinking about how the finger movement looks exactly like a swing. Up and down, rhythm, momentum. The more you do it, the harder it is to stop. Same physics, different damage.&lt;/p&gt;

&lt;p&gt;So I made a small thing to show that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It is a red swing boat hanging in the dark. When you scroll, it swings. Keep scrolling and it goes higher. Stop, and it slowly dies down the way a real swing does. Short messages appear below the boat as the energy builds up. They start gentle. They do not stay that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The whole thing is a single HTML file with no dependencies. The boat is an inline SVG so CSS and JavaScript can interact with it directly. The swing uses real pendulum physics, gravity and damping included, so it coasts and rocks naturally after you stop. Scroll speed feeds into the angular velocity, which means the harder you scroll the higher the swing goes.&lt;/p&gt;

&lt;p&gt;The scroll itself is captured with a &lt;code&gt;wheel&lt;/code&gt; event and &lt;code&gt;preventDefault&lt;/code&gt;, so the page never actually scrolls. There is nothing to read. Just the boat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Demo: &lt;a href="https://github.com/edriso/doomswing" rel="noopener noreferrer"&gt;github.com/edriso/doomswing&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
      <category>html</category>
    </item>
    <item>
      <title>My bot stopped sending questions the day Egypt turned on Daylight Saving Time</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Sat, 25 Apr 2026 08:16:24 +0000</pubDate>
      <link>https://forem.com/edriso/my-bot-stopped-sending-questions-the-day-egypt-turned-on-daylight-saving-time-59ce</link>
      <guid>https://forem.com/edriso/my-bot-stopped-sending-questions-the-day-egypt-turned-on-daylight-saving-time-59ce</guid>
      <description>&lt;p&gt;I built a Telegram bot that sends math questions to kids every day at 2:30 PM Egypt time. It had been live for two weeks, running every single day without a problem, questions going out on time, kids answering, streaks building up. Then recently I ran &lt;code&gt;/admin_health&lt;/code&gt; in the bot and saw &lt;strong&gt;0&lt;/strong&gt; scheduled questions. That's when I knew something was wrong and went digging through the logs.&lt;/p&gt;

&lt;p&gt;The last successful run was April 23. That night, Egypt turned on Daylight Saving Time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdro3h8q7eu7eqkg1nx98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdro3h8q7eu7eqkg1nx98.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me walk you through how the whole thing worked, what broke, and how I fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the bot schedules questions
&lt;/h2&gt;

&lt;p&gt;The bot has two main daily jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;01:30 AM Cairo&lt;/strong&gt;: prepare today's questions for every user (pick 3 questions based on each kid's weak topics, save them to the database)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2:30 PM Cairo&lt;/strong&gt;: send the first question to everyone&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use a library called &lt;code&gt;node-cron&lt;/code&gt; for scheduling. It supports timezones, so I just tell it "run this at 00:30 Cairo time" and it figures out the UTC equivalent automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30 0 * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;prepareScheduledQuestions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Africa/Cairo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How "Cairo time" works in the code
&lt;/h3&gt;

&lt;p&gt;The bot server runs on Railway (cloud), so it lives in UTC. Every time I need to know "what day is it in Cairo?", I use this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;todayCairoAsUtcMidnight&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get today's date as a string in Cairo timezone, e.g. "2026-04-23"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dateStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-CA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Africa/Cairo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// Return that date as UTC midnight so we can compare with database values&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dateStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T00:00:00.000Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "today in Cairo" becomes a fixed timestamp I can store in the database and compare against later. A question prepared at 01:30 AM Cairo gets tagged with &lt;code&gt;2026-04-23T00:00:00.000Z&lt;/code&gt;. When the 2:30 PM cron runs, it looks up questions tagged with that same date. If they match, the question gets sent.&lt;/p&gt;

&lt;p&gt;This works great as long as Cairo is always UTC+2.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Daylight Saving Time?
&lt;/h2&gt;

&lt;p&gt;DST is when a country moves its clocks forward by 1 hour in summer to make better use of daylight. Egypt reinstated DST in 2023 after not using it for about 10 years.&lt;/p&gt;

&lt;p&gt;In Egypt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Last Friday of April at midnight&lt;/strong&gt; → clocks jump from 00:00 to 01:00 (spring forward)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last Thursday of October at midnight&lt;/strong&gt; → clocks go back from 01:00 to 00:00 (fall back)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key phrase is &lt;strong&gt;"clocks jump from 00:00 to 01:00"&lt;/strong&gt;. That means 00:30 literally does not exist on that night.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;My prepare-questions cron was set to run at &lt;strong&gt;00:30 Cairo time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On April 24, 2026 (the DST transition day), at midnight Cairo, clocks skipped forward one hour. The time went from &lt;code&gt;00:00&lt;/code&gt; directly to &lt;code&gt;01:00&lt;/code&gt;. There was no &lt;code&gt;00:30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-cron&lt;/code&gt; saw that 00:30 didn't exist that night and &lt;strong&gt;silently skipped the job&lt;/strong&gt;. No error, no warning, nothing in the logs. But it gets worse. After that skip, node-cron's internal scheduler got into a broken state. It was not just the prepare job that stopped. Every single cron job stopped firing. The 2:30 PM send job never ran on April 24. Or April 25. The bot process was alive on Railway, no crashes, no restarts, just complete silence for two days.&lt;/p&gt;

&lt;p&gt;Here are the actual logs. Everything stops on April 23:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[2026-04-22T22:30:00.006Z] [INFO] [CRON] Preparing daily questions...
[2026-04-22T22:30:31.408Z] [INFO] Prepared adaptive questions for 9 users
[2026-04-23T12:30:00.025Z] [INFO] [CRON] Sending first question...
[2026-04-23T12:30:25.042Z] [INFO] First question sent {"sent":8,"failed":1,"total":9}
[2026-04-23T17:30:00.016Z] [INFO] [CRON] Sending reminders...
[2026-04-23T17:30:05.120Z] [INFO] Reminders sent {"sent":8,"total":9}

(silence after this)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;22:30 UTC&lt;/code&gt; = &lt;code&gt;00:30 Cairo (UTC+2)&lt;/code&gt;, and that was the last time questions were prepared. After that, the clocks changed and 00:30 disappeared.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why there were no errors
&lt;/h2&gt;

&lt;p&gt;This is the sneaky part. &lt;code&gt;node-cron&lt;/code&gt; doesn't throw an error when a scheduled time is skipped due to DST. And when the scheduler breaks internally, it also does not throw an error. The bot process just keeps running, healthy from Railway's point of view, with zero indication that all the scheduled work stopped.&lt;/p&gt;

&lt;p&gt;No crash to investigate. No alert to wake you up. Just kids not getting their questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 1: Move the cron to a safe time
&lt;/h2&gt;

&lt;p&gt;The simplest fix: change &lt;code&gt;00:30&lt;/code&gt; to &lt;code&gt;01:30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Egypt's clocks spring from &lt;code&gt;00:00&lt;/code&gt; to &lt;code&gt;01:00&lt;/code&gt;, so &lt;code&gt;01:30&lt;/code&gt; always exists, even on DST transition day. Problem solved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before (broken on DST spring-forward day):&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30 0 * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Africa/Cairo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After (safe):&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30 1 * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Africa/Cairo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing: on the fall-back day in October, clocks go from &lt;code&gt;01:00&lt;/code&gt; back to &lt;code&gt;00:00&lt;/code&gt;, so &lt;code&gt;01:30&lt;/code&gt; actually happens twice that night. That means the job runs twice. But that is totally fine, because at the start of &lt;code&gt;prepareScheduledQuestions()&lt;/code&gt; there is a check: if a user already has questions prepared for today, skip them. So the second run just finds everyone already done and exits. No duplicates, no problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 2: Add a fallback inside the send job
&lt;/h2&gt;

&lt;p&gt;The cron time fix handles DST. But what if the prepare job fails for any other reason, like a server hiccup, a deploy at the wrong time, whatever?&lt;/p&gt;

&lt;p&gt;I added one line at the top of the send-questions job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendFirstQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If questions weren't prepared for any reason, prepare them now before sending&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;prepareScheduledQuestions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// ... rest of the send logic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prepareScheduledQuestions()&lt;/code&gt; already skips users who have questions today, so calling it here costs nothing on a normal day. But on a broken day, it saves everyone from getting nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;If your cron jobs use a timezone that observes DST, avoid scheduling at times that get skipped during the spring-forward transition. In Egypt that means avoid &lt;code&gt;00:01&lt;/code&gt; through &lt;code&gt;00:59&lt;/code&gt;. Use &lt;code&gt;01:30&lt;/code&gt; or later to be safe.&lt;/p&gt;

&lt;p&gt;More generally: &lt;strong&gt;cron + timezones is a place where things can go silently wrong&lt;/strong&gt;. The job doesn't crash, there's no alert, users just don't hear from your app. Always add a fallback or at least an explicit log when your main jobs find zero work to do. That's usually a sign something upstream failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prepared&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No questions prepared, double check the prepare-questions job ran today&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one log line would have caught this immediately.&lt;/p&gt;




&lt;p&gt;The bot is now fixed and sending questions again. And next April, when Egypt springs forward, it'll handle it quietly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A question:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Someone may ask: what if you really wanted the job to always run at exactly 00:30 Cairo, no matter what happens to the clock?&lt;/p&gt;

&lt;p&gt;Short answer: you can't. Not on spring-forward night.&lt;/p&gt;

&lt;p&gt;00:30 Cairo on that night does not exist. The clock jumps from 00:00 directly to 01:00. There is no UTC value that maps to 00:30 Cairo on that specific night. It was erased. No library, no trick, no workaround can schedule something at a time that literally never happens.&lt;/p&gt;

&lt;p&gt;The closest you can get is one of two things.&lt;/p&gt;

&lt;p&gt;First option is what we already did: move to 01:30. It always exists, even on the transition night. On 364 days a year it runs at 01:30 Cairo. On that one spring-forward night it also runs at 01:30 Cairo, which is just one hour later than you wanted. Not a big deal.&lt;/p&gt;

&lt;p&gt;Second option is to schedule at two UTC times, one for winter and one for summer. Winter Cairo is UTC+2, so 00:30 Cairo = 22:30 UTC. Summer Cairo is UTC+3, so 00:30 Cairo = 21:30 UTC. You run both. Since the prepare function skips users who already have questions today, only one of the two actually does any work on a normal night. On the transition night, 22:30 UTC lands at 01:30 Cairo so you again get one hour late, but questions are still prepared. This gets you 00:30 on all regular nights in both seasons.&lt;/p&gt;

&lt;p&gt;But honestly the real safety net is the fallback we added inside the send job. Even if the prepare cron runs late, or gets skipped, or breaks entirely, questions get prepared right before they are sent at 2:30 PM. That is what actually protects users, not the exact minute the prepare job fires.&lt;/p&gt;

</description>
      <category>node</category>
      <category>cron</category>
      <category>timezone</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Stop arguing about formatting in code reviews. Use Husky and lint-staged instead.</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Fri, 24 Apr 2026 19:52:09 +0000</pubDate>
      <link>https://forem.com/edriso/stop-arguing-about-formatting-in-code-reviews-use-husky-and-lint-staged-instead-1lp</link>
      <guid>https://forem.com/edriso/stop-arguing-about-formatting-in-code-reviews-use-husky-and-lint-staged-instead-1lp</guid>
      <description>&lt;p&gt;If your team has ever left a comment like "missing semicolon" or "wrong quote style" on a pull request, this post is for you.&lt;/p&gt;

&lt;p&gt;Formatting is not a code review topic. It should be handled automatically before the code even reaches a PR. Here is how to do that cleanly using Husky and lint-staged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we actually solving?
&lt;/h2&gt;

&lt;p&gt;The problem is simple: developers format code differently, editors have different defaults, and without enforcement, you end up with inconsistent style that creates noisy diffs and unnecessary review comments.&lt;/p&gt;

&lt;p&gt;The goal is to auto-format and auto-lint staged files right before every commit, so bad formatting never reaches the repo.&lt;/p&gt;

&lt;p&gt;Two tools make this easy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prettier&lt;/strong&gt; handles formatting (spacing, quotes, semicolons, line length)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESLint&lt;/strong&gt; handles code quality (unused variables, missing dependencies, bad patterns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Husky&lt;/strong&gt; lets you run scripts automatically on git events like pre-commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lint-staged&lt;/strong&gt; runs those scripts only on staged files, not your entire codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running tools on the full project on every commit would be slow. lint-staged keeps it fast by scoping to only what you changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; husky lint-staged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do not already have Prettier and ESLint set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; prettier eslint eslint-config-prettier eslint-plugin-prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eslint-config-prettier&lt;/code&gt; disables ESLint rules that conflict with Prettier. &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; makes ESLint report Prettier violations as lint errors. Without these two, ESLint and Prettier will fight each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Initialize Husky
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx husky init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.husky/&lt;/code&gt; folder and adds a &lt;code&gt;prepare&lt;/code&gt; script to your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"husky"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;prepare&lt;/code&gt; script runs automatically on &lt;code&gt;npm install&lt;/code&gt;. That means anyone who clones your repo and installs dependencies gets the pre-commit hook set up without doing anything manually. This is the key to making it work across a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set up the pre-commit hook
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;.husky/pre-commit&lt;/code&gt; and replace whatever is in it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx lint-staged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all. Every time you run &lt;code&gt;git commit&lt;/code&gt;, Husky will run lint-staged before the commit goes through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Configure lint-staged
&lt;/h2&gt;

&lt;p&gt;Add this to your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"src/**/*.{js,jsx,ts,tsx}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"src/**/*.{css,json,md}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This says: on staged JS and JSX files inside &lt;code&gt;src/&lt;/code&gt;, run ESLint with auto-fix. On staged CSS, JSON, and Markdown files, run Prettier.&lt;/p&gt;

&lt;p&gt;Why split them? Because &lt;code&gt;eslint-plugin-prettier&lt;/code&gt; already runs Prettier internally when you run &lt;code&gt;eslint --fix&lt;/code&gt; on JS files, so you do not need to run both. For CSS and other file types, Prettier handles it directly since ESLint does not cover them.&lt;/p&gt;

&lt;p&gt;If you are working on a plain HTML or CSS project without ESLint, simplify to just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"**/*.{html,css,js,json,md}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Add a .prettierignore file
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.prettierignore&lt;/code&gt; in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dist/
node_modules/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents Prettier from formatting compiled or generated output. Without this, it might reformat your minified CSS or bundled JS, which is not what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works in practice
&lt;/h2&gt;

&lt;p&gt;You make changes to a few files, stage them with &lt;code&gt;git add&lt;/code&gt;, and run &lt;code&gt;git commit&lt;/code&gt;. Husky fires the pre-commit hook. lint-staged checks which staged files match your patterns and runs the tools only on those. If ESLint finds an error it cannot auto-fix, the commit is blocked and you see the error. If everything passes, the commit goes through with clean, formatted code.&lt;/p&gt;

&lt;p&gt;The first time you set this up, you will probably want to run a full format pass on the whole project so everything starts from a clean state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prettier &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="s2"&gt;"src/**/*.{js,jsx,css,json}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit that as a single formatting commit, then the pre-commit hook keeps things clean going forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  VS Code integration (the finishing touch)
&lt;/h2&gt;

&lt;p&gt;The pre-commit hook is your safety net. But you can also make formatting happen as you type by adding a &lt;code&gt;.vscode/settings.json&lt;/code&gt; to your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor.formatOnSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor.defaultFormatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esbenp.prettier-vscode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"editor.codeActionsOnSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source.fixAll.eslint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"always"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eslint.validate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"javascript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"javascriptreact"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files.autoSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, every time you hit save, VS Code runs Prettier and ESLint automatically. By the time you stage your files, they are already formatted. The Husky hook just confirms nothing slipped through.&lt;/p&gt;

&lt;p&gt;Commit this file to the repo so the whole team gets the same editor behavior without any manual setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No more formatting comments in code reviews&lt;/li&gt;
&lt;li&gt;Clean diffs that show only real changes&lt;/li&gt;
&lt;li&gt;Commits that are always consistent, regardless of who made them&lt;/li&gt;
&lt;li&gt;New team members get the full setup just by running &lt;code&gt;npm install&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It takes about ten minutes to set up and saves hours of back-and-forth over style issues. Worth it.&lt;/p&gt;

</description>
      <category>git</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Resume Update!</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Thu, 23 Apr 2026 16:01:33 +0000</pubDate>
      <link>https://forem.com/edriso/resume-update-1f0f</link>
      <guid>https://forem.com/edriso/resume-update-1f0f</guid>
      <description>&lt;p&gt;A while back I shared that I built my resume as a website so I can update it in one place without re-uploading files or changing links everywhere.&lt;/p&gt;

&lt;p&gt;Now that I'm looking for a new job, I ran into a new problem; I wanted to tailor my resume depending on the role I'm applying to. Sending the same resume to a Magento recruiter and a frontend recruiter doesn't really make sense.&lt;/p&gt;

&lt;p&gt;So I fixed that by adding a query param to the resume URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;edriso.github.io?r=fullstack
edriso.github.io?r=frontend
edriso.github.io?r=php
edriso.github.io?r=magento

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one shows a different version; different title, different bio, different skills order 😃.&lt;/p&gt;

&lt;p&gt;And it saves to localStorage, so if the recruiter refreshes the page or comes back later without the param, they still see the right version. &lt;/p&gt;

&lt;p&gt;Same thing on my portfolio site. The resume link in the footer automatically includes the right role param based on which version of the portfolio the recruiter is viewing.&lt;/p&gt;

&lt;p&gt;One link per recruiter. No confusion.&lt;/p&gt;

&lt;p&gt;Alhamdulillah 🙌&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Undo a Git Commit Without Losing History</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Thu, 23 Apr 2026 13:43:41 +0000</pubDate>
      <link>https://forem.com/edriso/how-to-undo-a-git-commit-without-losing-history-3ifp</link>
      <guid>https://forem.com/edriso/how-to-undo-a-git-commit-without-losing-history-3ifp</guid>
      <description>&lt;p&gt;We've all been there — you made a commit, pushed it, and then realized: &lt;em&gt;that was a mistake&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The good news? Git has a safe way to undo it without rewriting history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You might think of using &lt;code&gt;git reset&lt;/code&gt; to go back in time. But if you've already pushed the commit to a shared branch, resetting and force-pushing can cause problems for everyone else on the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Better Way: &lt;code&gt;git revert&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; creates a &lt;strong&gt;new commit&lt;/strong&gt; that undoes the changes from a previous commit. Your history stays intact — you just add an undo step on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Do It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Find the commit you want to undo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a8636a4 Tailor portfolio for e-commerce role
d56677a Fix hover effect
5315182 Add new project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the hash of the commit you want to revert (e.g. &lt;code&gt;a8636a4&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Revert it&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert a8636a4 &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;a8636a4&lt;/code&gt; — the commit hash you want to undo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-edit&lt;/code&gt; — skips the editor and uses the default revert message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git will create a new commit like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bf880c5 Revert "Tailor portfolio for e-commerce role"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your mistake is undone and your history is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use &lt;code&gt;git revert&lt;/code&gt; vs &lt;code&gt;git reset&lt;/code&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;git revert&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;git reset&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Creates a new commit&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safe for shared branches&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rewrites history&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; If the commit is already pushed, use &lt;code&gt;git revert&lt;/code&gt;. If it's only local, &lt;code&gt;git reset&lt;/code&gt; is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See your commits&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;

&lt;span class="c"&gt;# Revert the one you don't want&lt;/span&gt;
git revert &amp;lt;commit-hash&amp;gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, safe, and no drama. That's the Git way.&lt;/p&gt;

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The 3-Second Rule: How to Write a Freelance Proposal That Gets Read (and Gets Replies)</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Wed, 22 Apr 2026 17:27:58 +0000</pubDate>
      <link>https://forem.com/edriso/the-3-second-rule-how-to-write-a-freelance-proposal-that-gets-read-and-gets-replies-3l9g</link>
      <guid>https://forem.com/edriso/the-3-second-rule-how-to-write-a-freelance-proposal-that-gets-read-and-gets-replies-3l9g</guid>
      <description>&lt;p&gt;A friend told you to use "a 3-second role point" in your proposal. Good advice. But what does that actually mean?&lt;/p&gt;

&lt;p&gt;This post breaks it down in plain English — with real examples, clear comparisons, and things you can use today on Upwork, Freelancer, Toptal, or any similar platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "3 Seconds" at All?
&lt;/h2&gt;

&lt;p&gt;When a client posts a job on Upwork, they might get 20, 50, or even 100 proposals. They are not reading every word. They are &lt;strong&gt;scanning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Studies and freelance coaches consistently say the same thing: &lt;strong&gt;a client decides in the first 3 to 8 seconds whether your proposal is worth reading — or worth skipping.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those first few seconds happen at the very first line of your proposal. That is where you either win their attention or lose it forever.&lt;/p&gt;

&lt;p&gt;This is the "3-second rule": your opening must be so relevant, so specific, and so interesting that the client stops scrolling and reads the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Mistake Freelancers Make
&lt;/h2&gt;

&lt;p&gt;Before we get to the good stuff, let us look at what most people write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A typical (losing) proposal opening:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hello, my name is Ahmed and I am a full-stack developer with 5 years of experience. I have worked with React, Node.js, MongoDB, and more. I am very passionate about coding and I believe I am the perfect candidate for this job..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sound familiar? This is what 80% of proposals look like.&lt;/p&gt;

&lt;p&gt;The client does not care about your name in the first sentence. They do not care about your years of experience yet. They care about &lt;strong&gt;their problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You spent the first 3 seconds talking about yourself. They moved on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3-Second Rule Point: Make It About Them First
&lt;/h2&gt;

&lt;p&gt;Your opening line — your "3-second point" — should do one thing: &lt;strong&gt;show the client that you actually read their job post and you understand their specific problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not a general problem. Their specific, stated problem.&lt;/p&gt;

&lt;p&gt;Here is the formula for that opening line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Specific observation about their project] + [How you can help with that specific thing]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. One or two sentences. Sharp and relevant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Good vs Bad: Side by Side
&lt;/h2&gt;

&lt;p&gt;Let us look at a real job post and compare proposals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job Post:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need a developer to fix my WooCommerce checkout page. Customers are abandoning at the payment step — I think it is a PayPal integration bug. My site is slow too. Budget: $200."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Bad proposal opening:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hi, I am a WordPress and WooCommerce expert with 6 years of experience. I can help you with your website. I am very hardworking and deliver on time. Please check my profile."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What is wrong here?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is generic. This could be sent to any WooCommerce job.&lt;/li&gt;
&lt;li&gt;It does not mention the PayPal bug.&lt;/li&gt;
&lt;li&gt;It does not mention the checkout abandonment problem.&lt;/li&gt;
&lt;li&gt;It does not mention the slow site.&lt;/li&gt;
&lt;li&gt;The client feels like you did not read their post at all.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Good proposal opening:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Checkout abandonment at the PayPal step is almost always a redirect loop or a mismatched IPN URL — I have fixed this exact issue three times this month. I can also audit your page speed in the same session since slow checkout pages make abandonment worse."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What is different here?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It names the exact problem: checkout abandonment.&lt;/li&gt;
&lt;li&gt;It names the exact tool: PayPal.&lt;/li&gt;
&lt;li&gt;It shows specific knowledge: "redirect loop or mismatched IPN URL" — this is credible.&lt;/li&gt;
&lt;li&gt;It connects the two problems they mentioned (PayPal bug + slow site) in one sentence.&lt;/li&gt;
&lt;li&gt;The client thinks: "This person actually read what I wrote."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is 3 seconds well spent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Proposal Structure (After the Hook)
&lt;/h2&gt;

&lt;p&gt;The 3-second opening is just the beginning. Here is the full structure that converts attention into replies:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Hook (Seconds 1-3)
&lt;/h3&gt;

&lt;p&gt;Your one or two sentence opener. Specific, relevant, and about them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Bridge (Your Relevant Experience)
&lt;/h3&gt;

&lt;p&gt;Now — and only now — you talk about yourself. But keep it tied to their problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have been building WooCommerce sites for 4 years, and payment gateway bugs are one of the most common things I fix. Here is a similar case: a client's Stripe checkout was silently failing and costing them $3,000/month. I found it in under an hour."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice: one data point, one number, tied directly to their situation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Plan (What You Will Do)
&lt;/h3&gt;

&lt;p&gt;Tell them how you will solve it. Not in vague words — in concrete steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Here is my plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce the checkout error and check the PayPal IPN logs (1-2 hours)&lt;/li&gt;
&lt;li&gt;Fix the integration and test on staging (1-2 hours)&lt;/li&gt;
&lt;li&gt;Run a quick speed audit using GTmetrix and apply the top 3 fixes (1 hour)&lt;/li&gt;
&lt;li&gt;Full test before handover"&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;This builds trust. It shows you have a process, not just "I will figure it out."&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Proof (Why Trust You)
&lt;/h3&gt;

&lt;p&gt;A portfolio link, a relevant result, or a short testimonial quote. Keep it to one thing — do not list 10 projects.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Here is a WooCommerce project where I fixed a similar Stripe issue: [link]"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5. The Call to Action (CTA)
&lt;/h3&gt;

&lt;p&gt;End with a clear, low-pressure next step. Do not just say "let me know."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Happy to jump on a 15-minute call to look at the issue together — no cost, just to confirm I can help before you commit. Or if you prefer, send me a test account and I can start today."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Full Example: Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here is a complete proposal for the WooCommerce job above (under 200 words):&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Checkout abandonment at the PayPal step is almost always a redirect loop or a mismatched IPN URL — I have fixed this exact issue three times this month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I have been building WooCommerce stores for 4 years. Last month I helped a client in a similar situation: their PayPal checkout was silently failing for mobile users only, and they had no idea. I found it in the PayPal sandbox logs in about an hour.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My plan for your project:&lt;/em&gt;&lt;br&gt;
&lt;em&gt;1. Reproduce the error and check your PayPal IPN and webhook settings&lt;/em&gt;&lt;br&gt;
&lt;em&gt;2. Fix the bug and test on staging&lt;/em&gt;&lt;br&gt;
&lt;em&gt;3. Run a GTmetrix speed audit and apply the top fixes&lt;/em&gt;&lt;br&gt;
&lt;em&gt;4. Deliver with a short Loom video explaining what I found and fixed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Portfolio: [link to similar WooCommerce project]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am available to start today. Want to do a quick 10-minute call so I can see the error myself before we start? Or just send me a staging login and I will take a look.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Clean. Specific. Confident. Under 200 words.&lt;/p&gt;




&lt;h2&gt;
  
  
  More Hook Examples You Can Adapt
&lt;/h2&gt;

&lt;p&gt;Here are 5 quick openers for different situations. Notice each one names a specific detail from a hypothetical job post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a React performance job:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A 4-second load time on the dashboard is almost always a missing memo or a useEffect firing too often — both are straightforward to diagnose with React DevTools."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For a landing page redesign:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If your current page is converting at under 2%, the problem is usually the headline or the CTA placement — the design is secondary. I would start there before touching anything visual."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For a Node.js API bug:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A race condition on concurrent requests is one of the trickier Node bugs because it does not always reproduce in dev. I would start with your async/await error handling and check if you have any unhandled promise rejections in production logs."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For a WordPress migration:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Moving 1,200 posts from Wix to WordPress without losing SEO rankings is totally doable if you set up 301 redirects correctly from day one — I have done three migrations like this in the last two months."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For a Python scraping job:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If the site blocks you after 50 requests, it is almost certainly rate-limiting by IP. Rotating proxies plus random delays between requests fixes this 90% of the time."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Things That Kill Good Proposals
&lt;/h2&gt;

&lt;p&gt;Even with a great hook, these common mistakes will lose you the job:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too long.&lt;/strong&gt; If your proposal is over 300 words, most clients will not finish reading it. Say less, say it better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy-paste feel.&lt;/strong&gt; Clients can tell when you sent the same proposal 50 times. Even one tiny detail from their post makes it feel personal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No proof.&lt;/strong&gt; Saying "I am an expert" means nothing. One relevant link, one number, one past result — that means something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak ending.&lt;/strong&gt; "Looking forward to your response" is not a CTA. Give them something to do: a question to answer, a call to book, a next step to take.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting with "I."&lt;/strong&gt; The very first word of your proposal should never be "I." It signals immediately that you are about to talk about yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Checklist Before You Send
&lt;/h2&gt;

&lt;p&gt;Before hitting send on any proposal, run through this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Does my first sentence name something specific from their job post?&lt;/li&gt;
&lt;li&gt;[ ] Did I avoid starting with my name or years of experience?&lt;/li&gt;
&lt;li&gt;[ ] Did I explain what I will do, not just that I can do it?&lt;/li&gt;
&lt;li&gt;[ ] Did I include one concrete result or proof point?&lt;/li&gt;
&lt;li&gt;[ ] Did I end with a clear, specific next step?&lt;/li&gt;
&lt;li&gt;[ ] Is it under 250 words?&lt;/li&gt;
&lt;li&gt;[ ] Does it sound like a human wrote it, not a template?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all boxes are checked, send it.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The "3-second rule point" your friend mentioned is simply this: &lt;strong&gt;your first sentence must prove you read the job post and understand the client's actual problem — before you say a single word about yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Structure your proposal like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hook&lt;/strong&gt; — one specific, relevant sentence about their problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bridge&lt;/strong&gt; — your experience, tied to their situation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; — concrete steps, not vague promises&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof&lt;/strong&gt; — one link or result&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTA&lt;/strong&gt; — a simple, clear next step&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most freelancers write about themselves. You write about the client. That is the difference.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What is the hardest part of writing proposals for you? Drop a comment — I read every one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.upwork.com/resources/how-to-create-a-proposal-that-wins-jobs" rel="noopener noreferrer"&gt;How To Create a Proposal That Wins Jobs - Upwork&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gigradar.io/blog/25-upwork-proposal-openers-that-get-replies" rel="noopener noreferrer"&gt;25 Upwork Proposal Openers That Get Replies Fast - GigRadar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://freelancewitherica.substack.com/p/get-your-upwork-proposals-opened" rel="noopener noreferrer"&gt;The Secret Formula to Getting Your Proposals ACTUALLY SEEN - Freelance with Erica&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.udemy.com/course/upwork-proposal-writing-hacks-3-steps-rule14-case-studies/" rel="noopener noreferrer"&gt;Upwork Proposal Writing Hacks (3-Step Rule) - Udemy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getmany.com/blog/7-upwork-proposal-sample-ideas-to-boost-your-success-in-2026" rel="noopener noreferrer"&gt;7 Upwork Proposal Sample Ideas to Boost Your Success - Getmany&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tymora.io/blog/how-to-write-an-upwork-proposal-2025" rel="noopener noreferrer"&gt;How to Write a Winning Upwork Proposal - Tymora&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>If You Were a Server: How to Detect Issues and Keep Things Running Smoothly</title>
      <dc:creator>Mohamed Idris</dc:creator>
      <pubDate>Wed, 22 Apr 2026 17:26:15 +0000</pubDate>
      <link>https://forem.com/edriso/if-you-were-a-server-how-to-detect-issues-and-keep-things-running-smoothly-48dm</link>
      <guid>https://forem.com/edriso/if-you-were-a-server-how-to-detect-issues-and-keep-things-running-smoothly-48dm</guid>
      <description>&lt;p&gt;Here is a question that often pops up in senior web developer and backend interviews:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"If you were a server, how would you detect that you're having issues, and what would you do next to make things run smoothly?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, it sounds a little weird. &lt;em&gt;Be a server?&lt;/em&gt; But that is actually the whole point. The interviewer wants to know if you can &lt;strong&gt;think like infrastructure&lt;/strong&gt; — can you see problems before they become disasters, and do you know how to respond?&lt;/p&gt;

&lt;p&gt;This post breaks it all down in simple English. Whether you are a junior developer hearing these concepts for the first time, or someone preparing for a senior interview, you will walk away with a solid mental model.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: What Is the Question Really Asking?
&lt;/h2&gt;

&lt;p&gt;The question is covering two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detection&lt;/strong&gt; — How does a server (or you, the developer/SRE watching it) know something is wrong?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response&lt;/strong&gt; — What actions do you take to fix it or at least keep things stable?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Think of it like being a doctor for your own body. You check your temperature, blood pressure, and pulse. If something is off, you take medicine, rest, or call a specialist. Servers work the same way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: How Does a Server Detect It Has Issues?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Four Core Vitals
&lt;/h3&gt;

&lt;p&gt;Just like a doctor checks your basic vitals, a server has its own set of core health signals. These are the first things to look at.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. CPU Usage
&lt;/h4&gt;

&lt;p&gt;CPU (Central Processing Unit) is the brain of your server. It handles every calculation, request, and operation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Healthy:&lt;/strong&gt; Under 60-70% usage during normal load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning:&lt;/strong&gt; Consistently above 80%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical:&lt;/strong&gt; Sustained 90%+ means your server is struggling to breathe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When CPU is maxed out, your server starts slowing down responses, queuing requests, or dropping them entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to check it:&lt;/strong&gt; &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, &lt;code&gt;vmstat&lt;/code&gt;, or any cloud monitoring dashboard like AWS CloudWatch or Datadog.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Memory (RAM) Usage
&lt;/h4&gt;

&lt;p&gt;Memory is your server's short-term workspace. Every running process uses RAM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Healthy:&lt;/strong&gt; Enough free RAM to handle spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning:&lt;/strong&gt; When RAM fills up, your OS starts using &lt;strong&gt;swap&lt;/strong&gt; — which is disk space acting as fake RAM. Disk is &lt;strong&gt;much&lt;/strong&gt; slower than RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical:&lt;/strong&gt; If swap fills up too, processes start crashing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common mistake is ignoring memory leaks — situations where an application keeps grabbing more memory and never releasing it. Over time, this silently kills your server.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Disk Usage and I/O
&lt;/h4&gt;

&lt;p&gt;Disk usage is how much of your storage is full. Disk I/O is how fast data is being read and written.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best practice:&lt;/strong&gt; Keep at least 20-30% disk space free at all times.&lt;/li&gt;
&lt;li&gt;A full disk can crash your database, stop logging, and break deployments.&lt;/li&gt;
&lt;li&gt;High disk I/O (lots of reads/writes happening at once) can make everything slow, even if CPU and RAM look fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4. Network Bandwidth
&lt;/h4&gt;

&lt;p&gt;Your server talks to the outside world through the network. If the pipe gets clogged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests take longer to arrive and respond.&lt;/li&gt;
&lt;li&gt;Large file uploads or downloads can saturate the connection.&lt;/li&gt;
&lt;li&gt;You may see packet loss — data literally gets dropped mid-transfer.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Beyond the Core Vitals: Application-Level Signals
&lt;/h3&gt;

&lt;p&gt;The four core vitals tell you about the &lt;strong&gt;hardware&lt;/strong&gt;. But you also need to watch what your &lt;strong&gt;application&lt;/strong&gt; is doing.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Response Time / Latency
&lt;/h4&gt;

&lt;p&gt;How long does your server take to respond to a request?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A healthy API might respond in 100ms.&lt;/li&gt;
&lt;li&gt;If latency jumps to 2-3 seconds, something is wrong — maybe a slow database query, a blocked thread, or an overloaded service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Users notice latency before they notice anything else. A slow page feels broken.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Error Rate
&lt;/h4&gt;

&lt;p&gt;What percentage of your requests are returning errors?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5xx errors (500, 502, 503, 504) mean &lt;strong&gt;your server is the problem&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If your error rate goes from 0.1% to 5%, something just broke.&lt;/li&gt;
&lt;li&gt;Spikes in 4xx errors (404, 403) can also signal broken deployments or misconfigured routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  7. Throughput / Request Rate
&lt;/h4&gt;

&lt;p&gt;How many requests per second is your server handling?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A sudden &lt;strong&gt;drop&lt;/strong&gt; in traffic can be as alarming as a spike — it might mean your server is down and clients are not even reaching it.&lt;/li&gt;
&lt;li&gt;A sudden &lt;strong&gt;spike&lt;/strong&gt; might mean a traffic surge, a bot attack, or a viral moment — all of which need different responses.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Health Checks: The Server Checking Itself
&lt;/h3&gt;

&lt;p&gt;Modern servers do not wait for humans to notice something is wrong. They &lt;strong&gt;self-report&lt;/strong&gt; through health check endpoints.&lt;/p&gt;

&lt;p&gt;There are two main types used in systems like Kubernetes:&lt;/p&gt;

&lt;h4&gt;
  
  
  Liveness Probe
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;"Am I still alive?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a simple check: is the process running at all? If a liveness probe fails, the orchestrator (like Kubernetes) will &lt;strong&gt;restart&lt;/strong&gt; the container.&lt;/p&gt;

&lt;p&gt;Example endpoint: &lt;code&gt;GET /health/live&lt;/code&gt; → returns &lt;code&gt;200 OK&lt;/code&gt; if running.&lt;/p&gt;

&lt;h4&gt;
  
  
  Readiness Probe
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;"Am I ready to receive traffic?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is more nuanced: is the server running &lt;strong&gt;and&lt;/strong&gt; fully ready to handle requests? Maybe it is warming up a cache, waiting for a database connection, or doing startup migrations.&lt;/p&gt;

&lt;p&gt;If a readiness probe fails, the load balancer &lt;strong&gt;stops sending traffic&lt;/strong&gt; to that instance — without killing it. Once it recovers, traffic resumes.&lt;/p&gt;

&lt;p&gt;Example endpoint: &lt;code&gt;GET /health/ready&lt;/code&gt; → checks DB connection, cache, etc.&lt;/p&gt;




&lt;h3&gt;
  
  
  Logging: The Server's Diary
&lt;/h3&gt;

&lt;p&gt;Logs are your best friend when something goes wrong. A server should write meaningful logs that tell a story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured logging&lt;/strong&gt; means logging in a consistent format (usually JSON) so machines can read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-22T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Database connection failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without good logs, debugging is like trying to solve a crime with no evidence. With good logs, you can trace exactly what happened, when, and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log levels&lt;/strong&gt; to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DEBUG&lt;/code&gt; — detailed developer info (not for production usually)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INFO&lt;/code&gt; — normal operations ("User logged in")&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WARN&lt;/code&gt; — something is off but not broken yet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ERROR&lt;/code&gt; — something broke, needs attention&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FATAL&lt;/code&gt; / &lt;code&gt;CRITICAL&lt;/code&gt; — the server cannot continue&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alerting: Getting Notified Before It's Too Late
&lt;/h3&gt;

&lt;p&gt;Monitoring without alerting is useless. You cannot stare at dashboards 24/7.&lt;/p&gt;

&lt;p&gt;Set up &lt;strong&gt;alerts&lt;/strong&gt; with smart thresholds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Warning Threshold&lt;/th&gt;
&lt;th&gt;Critical Threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU Usage&lt;/td&gt;
&lt;td&gt;&amp;gt; 80% for 5 min&lt;/td&gt;
&lt;td&gt;&amp;gt; 90% for 2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory Usage&lt;/td&gt;
&lt;td&gt;&amp;gt; 85%&lt;/td&gt;
&lt;td&gt;&amp;gt; 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk Space&lt;/td&gt;
&lt;td&gt;&amp;lt; 25% free&lt;/td&gt;
&lt;td&gt;&amp;lt; 10% free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error Rate&lt;/td&gt;
&lt;td&gt;&amp;gt; 1%&lt;/td&gt;
&lt;td&gt;&amp;gt; 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response Time&lt;/td&gt;
&lt;td&gt;&amp;gt; 500ms avg&lt;/td&gt;
&lt;td&gt;&amp;gt; 2s avg&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Do not alert on every small blip.&lt;/strong&gt; Use time windows (e.g., "CPU &amp;gt; 80% for 5 consecutive minutes") to avoid alert fatigue — a flood of noisy alerts that people start ignoring.&lt;/p&gt;

&lt;p&gt;Tools: PagerDuty, OpsGenie, Slack alerts, AWS CloudWatch Alarms, Grafana Alerts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: What Do You Do Next to Keep Things Running Smoothly?
&lt;/h2&gt;

&lt;p&gt;You detected the problem. Now what? Here are the key strategies — from automatic to manual.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Auto-Scaling: Get More Help
&lt;/h3&gt;

&lt;p&gt;If your server is overloaded, the simplest answer is: &lt;strong&gt;add more servers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Auto-scaling is when your infrastructure automatically spins up new server instances when load is high, and shuts them down when load drops.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal traffic:   [Server 1]
Traffic spike:    [Server 1] [Server 2] [Server 3]
Traffic drops:    [Server 1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works with a &lt;strong&gt;load balancer&lt;/strong&gt; sitting in front — it distributes incoming requests across all available instances so no single server gets crushed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal scaling&lt;/strong&gt; = more instances (this is what auto-scaling does).&lt;br&gt;
&lt;strong&gt;Vertical scaling&lt;/strong&gt; = bigger instance (more CPU/RAM on the same machine). Harder to do automatically.&lt;/p&gt;

&lt;p&gt;Cloud providers (AWS, GCP, Azure) all have auto-scaling built in.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Circuit Breaker: Stop the Bleeding
&lt;/h3&gt;

&lt;p&gt;Imagine your server calls another service (a payment API, a database, a third-party service). That service is slow or down. Your server keeps waiting... and waiting... and all your threads are now stuck waiting for something that will never respond. Your whole server grinds to a halt because of &lt;strong&gt;someone else's problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Circuit Breaker pattern&lt;/strong&gt; prevents this.&lt;/p&gt;

&lt;p&gt;It works in three states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLOSED (Normal)
  → Requests pass through
  → Failures are counted

OPEN (Problem detected)
  → Requests immediately fail fast
  → No waiting, no timeout
  → Returns an error or fallback instantly

HALF-OPEN (Testing recovery)
  → A few test requests get through
  → If they succeed → back to CLOSED
  → If they fail → back to OPEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is called a circuit breaker because it works exactly like the electrical circuit breaker in your house — when something goes wrong, it cuts the connection to stop further damage.&lt;/p&gt;

&lt;p&gt;Libraries: &lt;code&gt;opossum&lt;/code&gt; (Node.js), &lt;code&gt;resilience4j&lt;/code&gt; (Java), &lt;code&gt;polly&lt;/code&gt; (.NET).&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Graceful Degradation: Do Less, Not Nothing
&lt;/h3&gt;

&lt;p&gt;When part of your system is broken, the goal is to &lt;strong&gt;keep the core experience working&lt;/strong&gt;, even if some features are temporarily unavailable.&lt;/p&gt;

&lt;p&gt;Real-world examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YouTube&lt;/strong&gt; goes down? Show the homepage with a "video unavailable" message instead of crashing entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation engine&lt;/strong&gt; fails? Show generic popular content instead of personalized picks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment service&lt;/strong&gt; is slow? Disable the "buy now" button and show a "try again shortly" message instead of hanging the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is better than an error page that says "500 Internal Server Error" with nothing else.&lt;/p&gt;

&lt;p&gt;The key idea: &lt;strong&gt;degrade gracefully, fail loudly only when you have no other choice.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Caching: Avoid Repeating Work
&lt;/h3&gt;

&lt;p&gt;Many server problems come from doing the same expensive work over and over. Caching stores the result of expensive operations so you can reuse them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without cache:
User → Server → Database (100ms) → User

With cache:
User → Server → Cache (1ms) → User  (if cached)
User → Server → Database (100ms) → Cache → User  (if not cached)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Types of caching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-memory cache:&lt;/strong&gt; Redis, Memcached — extremely fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP caching:&lt;/strong&gt; &lt;code&gt;Cache-Control&lt;/code&gt; headers tell browsers and CDNs to store responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database query caching:&lt;/strong&gt; Cache frequently run queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your server is struggling, a well-configured cache can reduce database load by 80% or more.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Rate Limiting: Control Incoming Traffic
&lt;/h3&gt;

&lt;p&gt;If traffic spikes and you cannot scale fast enough, you need to protect yourself by &lt;strong&gt;limiting how many requests any single client can make&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Example: Allow 100 requests per minute per IP. After that, return &lt;code&gt;429 Too Many Requests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This protects against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accidental infinite loops in client code&lt;/li&gt;
&lt;li&gt;DDoS (Distributed Denial of Service) attacks&lt;/li&gt;
&lt;li&gt;Scrapers hammering your API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not just about bad actors — rate limiting is also good for your own internal services to prevent one slow consumer from starving everyone else.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Retry with Exponential Backoff: Be Patient, Not Aggressive
&lt;/h3&gt;

&lt;p&gt;Sometimes a service is temporarily down and recovers in seconds. Clients should retry — but &lt;strong&gt;smart retries&lt;/strong&gt;, not aggressive ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad retry:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fail → Retry immediately → Fail → Retry immediately → Fail...
(This makes the overloaded server worse)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good retry with exponential backoff:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fail → Wait 1s → Retry
Fail → Wait 2s → Retry
Fail → Wait 4s → Retry
Fail → Wait 8s → Retry (give up after X attempts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each retry waits twice as long as the last. Add a bit of random delay (called &lt;strong&gt;jitter&lt;/strong&gt;) so thousands of clients do not all retry at the exact same moment, which would cause another spike.&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Rollbacks: Undo What Broke It
&lt;/h3&gt;

&lt;p&gt;Sometimes the issue is a bad deployment. The fastest fix is often to &lt;strong&gt;roll back&lt;/strong&gt; to the previous working version.&lt;/p&gt;

&lt;p&gt;Blue-green deployment and canary releases are strategies to reduce this risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blue-Green:&lt;/strong&gt; You have two identical environments (blue = live, green = new). Deploy to green, test it, then switch traffic. If something breaks, switch back to blue instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary Release:&lt;/strong&gt; Roll out the new version to 5% of users first. If metrics look good, increase to 20%, then 50%, then 100%. If something breaks, only 5% of users were affected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns let you catch problems early without taking down everything.&lt;/p&gt;




&lt;h3&gt;
  
  
  8. Runbooks: The Human Response Plan
&lt;/h3&gt;

&lt;p&gt;All the automation in the world cannot cover every scenario. You need a &lt;strong&gt;runbook&lt;/strong&gt; — a documented set of steps that a human follows when a specific alert fires.&lt;/p&gt;

&lt;p&gt;A good runbook answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does this alert mean?&lt;/li&gt;
&lt;li&gt;How urgent is it?&lt;/li&gt;
&lt;li&gt;What are the first three things to check?&lt;/li&gt;
&lt;li&gt;How do I escalate if I cannot fix it?&lt;/li&gt;
&lt;li&gt;What commands should I run?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example runbook entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Alert: High Memory Usage (&amp;gt; 90% for 10 minutes)
&lt;span class="p"&gt;
1.&lt;/span&gt; SSH into the affected server
&lt;span class="p"&gt;2.&lt;/span&gt; Run: &lt;span class="sb"&gt;`ps aux --sort=-%mem | head -20`&lt;/span&gt; to find the top memory consumers
&lt;span class="p"&gt;3.&lt;/span&gt; Check for memory leaks in the app logs: &lt;span class="sb"&gt;`grep "OutOfMemory" /var/log/app.log`&lt;/span&gt;
&lt;span class="p"&gt;4.&lt;/span&gt; If a rogue process is found: restart the service
&lt;span class="p"&gt;5.&lt;/span&gt; If memory does not recover: trigger a scale-out event
&lt;span class="p"&gt;6.&lt;/span&gt; Escalate to on-call engineer if unresolved after 15 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A runbook turns a stressful incident into a checklist. It is boring when things are calm and invaluable at 3am during an outage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together: The Full Answer Framework
&lt;/h2&gt;

&lt;p&gt;If an interviewer asks you this question, here is the structure of a great answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection (Observability):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I would monitor core metrics: CPU, memory, disk, network&lt;/li&gt;
&lt;li&gt;I would track application metrics: latency, error rate, throughput&lt;/li&gt;
&lt;li&gt;I would have health check endpoints (liveness and readiness)&lt;/li&gt;
&lt;li&gt;I would use structured logging for traceability&lt;/li&gt;
&lt;li&gt;I would set up smart alerts with meaningful thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Response (Resilience):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-scaling to handle load spikes&lt;/li&gt;
&lt;li&gt;Circuit breakers to prevent cascading failures from downstream services&lt;/li&gt;
&lt;li&gt;Graceful degradation to keep core features working&lt;/li&gt;
&lt;li&gt;Caching to reduce repeated expensive work&lt;/li&gt;
&lt;li&gt;Rate limiting to protect against traffic floods&lt;/li&gt;
&lt;li&gt;Retry with exponential backoff for transient failures&lt;/li&gt;
&lt;li&gt;Rollback strategies (blue-green, canary) for bad deployments&lt;/li&gt;
&lt;li&gt;Runbooks so on-call engineers know exactly what to do&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Tools Worth Knowing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Popular Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metrics &amp;amp; Dashboards&lt;/td&gt;
&lt;td&gt;Grafana, Datadog, Prometheus, AWS CloudWatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;ELK Stack (Elasticsearch, Logstash, Kibana), Loki, Splunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerting&lt;/td&gt;
&lt;td&gt;PagerDuty, OpsGenie, Grafana Alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APM (App Performance)&lt;/td&gt;
&lt;td&gt;Datadog APM, New Relic, Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime Monitoring&lt;/td&gt;
&lt;td&gt;UptimeRobot, Pingdom, Checkly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circuit Breakers&lt;/td&gt;
&lt;td&gt;Resilience4j, Opossum (Node.js), Polly (.NET)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;Redis, Memcached, Varnish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balancing / Scaling&lt;/td&gt;
&lt;td&gt;AWS ALB + Auto Scaling, Kubernetes HPA, NGINX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The interview question is asking you to think like a system that &lt;strong&gt;observes itself&lt;/strong&gt; and &lt;strong&gt;heals itself&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detect&lt;/strong&gt; with metrics (CPU, memory, disk, network), application signals (latency, error rate), health checks, and structured logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respond&lt;/strong&gt; with auto-scaling, circuit breakers, graceful degradation, caching, rate limiting, smart retries, rollbacks, and runbooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best systems do not just survive failures — they are designed to &lt;strong&gt;expect them&lt;/strong&gt; and handle them without waking anyone up at 3am.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Drop a comment with the trickiest server question you have faced in an interview — I would love to hear it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.manageengine.com/network-monitoring/blog/server-monitoring-checklist.html" rel="noopener noreferrer"&gt;The only server health monitoring checklist you need in 2025 - ManageEngine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://uptimerobot.com/knowledge-hub/monitoring/ultimate-guide-to-server-monitoring-metrics-tools-and-best-practices/" rel="noopener noreferrer"&gt;Ultimate Guide to Server Monitoring - UptimeRobot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sematext.com/blog/server-monitoring-best-practices/" rel="noopener noreferrer"&gt;Server Health and Performance Monitoring Best Practices - Sematext&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/system-design/what-is-circuit-breaker-pattern-in-microservices/" rel="noopener noreferrer"&gt;Circuit Breaker Pattern in Microservices - GeeksforGeeks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zuplo.com/learning-center/api-gateway-resilience-fault-tolerance" rel="noopener noreferrer"&gt;API Gateway Resilience and Fault Tolerance - Zuplo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sreschool.com/blog/graceful-degradation/" rel="noopener noreferrer"&gt;What is Graceful Degradation? - SRE School&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/system-design/microservices-resilience-patterns/" rel="noopener noreferrer"&gt;Microservices Resilience Patterns - GeeksforGeeks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>beginners</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
