<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged javascript</title>
<link>https://coderlegion.com/tag/javascript</link>
<description>Powered by Question2Answer</description>
<item>
<title>🧩 JavaScript Promises Inside Promises: The Hidden Bugs, Execution Flow and How to Fix Them</title>
<link>https://coderlegion.com/33226/javascript-promises-inside-promises-the-hidden-bugs-execution-flow-and-how-to-fix-them</link>
<description>&lt;p&gt;Have you ever debugged JavaScript code where a function says &lt;strong&gt;“Done!”&lt;/strong&gt;, but the operation it depends on is still running?&lt;/p&gt;
&lt;p&gt;Or perhaps an API request fails, but the error never reaches the &lt;code&gt;catch()&lt;/code&gt; block you expected to handle it.&lt;/p&gt;
&lt;p&gt;You inspect the code. You see &lt;code&gt;.then()&lt;/code&gt;, another &lt;code&gt;.then()&lt;/code&gt;, maybe a &lt;code&gt;new Promise()&lt;/code&gt; inside another Promise and suddenly the execution flow becomes difficult to follow.&lt;/p&gt;
&lt;p&gt;Sound familiar?&lt;/p&gt;
&lt;p&gt;As JavaScript developers, we use Promises for API calls, database operations, file processing, authentication, background tasks and countless other asynchronous operations. Yet some of the bugs we encounter aren't caused by Promises themselves. They're caused by how we connect them.&lt;/p&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fetchUser()
  .then((user) =&amp;gt; {
    saveUser(user);
  })
  .then(() =&amp;gt; {
    console.log(&quot;User saved successfully!&quot;);
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks reasonable, right?&lt;/p&gt;
&lt;p&gt;But what if &lt;code&gt;saveUser()&lt;/code&gt; returns a Promise?&lt;/p&gt;
&lt;p&gt;The success message can appear &lt;strong&gt;before the save operation finishes&lt;/strong&gt;. Worse, if saving fails, the error might not reach the error handler you intended to use.&lt;/p&gt;
&lt;p&gt;The problem isn't that Promises are outdated or that nested Promises are always bad. The problem is understanding &lt;strong&gt;which asynchronous operation the current chain is actually waiting for&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this article, we'll work through these issues step by step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What really happens when a Promise is created inside another Promise?&lt;/li&gt;
&lt;li&gt;How does JavaScript execute synchronous code, Promise callbacks and timers?&lt;/li&gt;
&lt;li&gt;Why does forgetting &lt;code&gt;return&lt;/code&gt; break a Promise chain?&lt;/li&gt;
&lt;li&gt;How do nested Promises differ from normal Promise chaining?&lt;/li&gt;
&lt;li&gt;Does the same bug happen with &lt;code&gt;async/await&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;When should you use Promises, &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;Promise.all()&lt;/code&gt;, or a background task?&lt;/li&gt;
&lt;li&gt;How can you debug these problems in a real application?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's start with a bug we can reproduce.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-first-let-s-reproduce-the-bug&quot;&gt;1. First, let's reproduce the bug&lt;/h3&gt;
&lt;p&gt;Imagine you're building a user registration API.&lt;/p&gt;
&lt;p&gt;After a user registers, your application needs to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Save the user's information.&lt;/li&gt;
&lt;li&gt;Send a welcome email.&lt;/li&gt;
&lt;li&gt;Confirm that registration has completed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For simplicity, we'll simulate the database and email operations using Promises.&lt;/p&gt;
&lt;h4 id=&quot;the-buggy-implementation&quot;&gt;The buggy implementation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function saveUser() {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(&quot;1. User saved to database&quot;);
      resolve({ id: 101, name: &quot;Fazal&quot; });
    }, 1000);
  });
}

function sendWelcomeEmail(user) {
  return new Promise((resolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      console.log(`2. Welcome email sent to ${user.name}`);
      resolve();
    }, 2000);
  });
}

function registerUser() {
  return saveUser()
    .then((user) =&amp;gt; {
      sendWelcomeEmail(user);
    })
    .then(() =&amp;gt; {
      console.log(&quot;3. Registration completed successfully&quot;);
    });
}

registerUser();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What do you expect to happen?&lt;/p&gt;
&lt;p&gt;A reasonable expectation is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. User saved to database
2. Welcome email sent to Fazal
3. Registration completed successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the actual output is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. User saved to database
3. Registration completed successfully
2. Welcome email sent to Fazal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait. Why?&lt;/p&gt;
&lt;p&gt;We called &lt;code&gt;sendWelcomeEmail(user)&lt;/code&gt; before logging the success message. Shouldn't JavaScript wait for it?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No. Calling an asynchronous function that returns a Promise does not automatically make the surrounding code wait for that Promise.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let's understand exactly where the execution flow goes wrong.&lt;/p&gt;
&lt;h4 id=&quot;what-javascript-actually-does&quot;&gt;What JavaScript actually does&lt;/h4&gt;
&lt;p&gt;Look at this callback:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;.then((user) =&amp;gt; {
  sendWelcomeEmail(user);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function calls &lt;code&gt;sendWelcomeEmail(user)&lt;/code&gt;, which starts the email operation and returns a Promise.&lt;/p&gt;
&lt;p&gt;But the callback doesn't return that Promise.&lt;/p&gt;
&lt;p&gt;From the Promise chain's perspective, this callback returns &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That means the next &lt;code&gt;.then()&lt;/code&gt; doesn't need to wait for the email operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;saveUser()
    |
    | Promise fulfills
    v
First .then()
    |
    | Starts email operation
    | Does NOT return its Promise
    v
First .then() fulfills with undefined
    |
    v
Second .then()
    |
    | Runs immediately after the previous
    | Promise reaction completes
    v
&quot;Registration completed successfully&quot;
    |
    | Email operation is still running
    v
&quot;Welcome email sent to Fazal&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact wording matters here: the next &lt;code&gt;.then()&lt;/code&gt; doesn't run synchronously inside the previous callback. It runs as a Promise reaction after the preceding chain Promise fulfills. But because the preceding callback returned &lt;code&gt;undefined&lt;/code&gt;, that fulfillment doesn't depend on the email Promise.&lt;/p&gt;
&lt;p&gt;This is the central bug.&lt;/p&gt;
&lt;h4 id=&quot;the-fix-return-the-promise&quot;&gt;The fix: return the Promise&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  return saveUser()
    .then((user) =&amp;gt; {
      return sendWelcomeEmail(user);
    })
    .then(() =&amp;gt; {
      console.log(&quot;3. Registration completed successfully&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the output is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. User saved to database
2. Welcome email sent to Fazal
3. Registration completed successfully
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We changed only one thing: &lt;strong&gt;we returned the inner Promise&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;JavaScript can now connect the two asynchronous operations into one chain.&lt;/p&gt;
&lt;p&gt;A shorter version works too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  return saveUser()
    .then((user) =&amp;gt; sendWelcomeEmail(user))
    .then(() =&amp;gt; {
      console.log(&quot;3. Registration completed successfully&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When an arrow function has an expression body, that expression is returned implicitly. Here, the expression is the Promise returned by &lt;code&gt;sendWelcomeEmail()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Keep this rule in mind throughout the article:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the next step must wait for an asynchronous operation, make sure that operation's Promise is returned or awaited.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But why does returning a Promise work? To understand that, we need to look at how Promises actually behave.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-what-is-a-promise-really-&quot;&gt;2. What is a Promise, really?&lt;/h3&gt;
&lt;p&gt;A Promise represents the eventual outcome of an asynchronous operation.&lt;/p&gt;
&lt;p&gt;It can be in one of three states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pending:&lt;/strong&gt; The operation hasn't settled yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fulfilled:&lt;/strong&gt; The operation completed successfully.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rejected:&lt;/strong&gt; The operation failed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A Promise that has fulfilled or rejected is called &lt;em&gt;settled&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const result = new Promise((resolve, reject) =&amp;gt; {
  setTimeout(() =&amp;gt; {
    resolve(&quot;Data loaded&quot;);
  }, 1000);
});

result.then((value) =&amp;gt; {
  console.log(value);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output after approximately one second:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Data loaded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Promise starts pending. After the timer callback runs, it fulfills with &lt;code&gt;&quot;Data loaded&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.then()&lt;/code&gt; callback runs as a Promise reaction, rather than being invoked immediately by the call to &lt;code&gt;resolve()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One important distinction: &lt;strong&gt;a Promise doesn't make an operation asynchronous by itself&lt;/strong&gt;. The operation may involve a timer, network request, or other asynchronous API. A Promise provides a way to represent and compose its outcome.&lt;/p&gt;
&lt;p&gt;Also, the Promise executor—the function passed to &lt;code&gt;new Promise()&lt;/code&gt;—runs synchronously.&lt;/p&gt;
&lt;p&gt;Let's verify that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&quot;1. Before&quot;);

const promise = new Promise((resolve) =&amp;gt; {
  console.log(&quot;2. Inside executor&quot;);
  resolve(&quot;Done&quot;);
});

promise.then((value) =&amp;gt; {
  console.log(&quot;4.&quot;, value);
});

console.log(&quot;3. After&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Before
2. Inside executor
3. After
4. Done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;JavaScript logs &lt;code&gt;&quot;Before&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Promise constructor immediately invokes its executor, so &lt;code&gt;&quot;Inside executor&quot;&lt;/code&gt; appears next.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve(&quot;Done&quot;)&lt;/code&gt; fulfills the Promise, but its &lt;code&gt;.then()&lt;/code&gt; reaction doesn't run inline.&lt;/li&gt;
&lt;li&gt;JavaScript continues with the remaining synchronous code.&lt;/li&gt;
&lt;li&gt;The Promise reaction runs as a microtask.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This distinction becomes important when you see a Promise inside another Promise.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-what-happens-when-a-promise-is-inside-another-promise-&quot;&gt;3. What happens when a Promise is inside another Promise?&lt;/h3&gt;
&lt;p&gt;There are two patterns that developers often confuse.&lt;/p&gt;
&lt;h4 id=&quot;pattern-a-returning-a-promise-from-another-promise&quot;&gt;Pattern A: Returning a Promise from another Promise&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const outerPromise = new Promise((resolve) =&amp;gt; {
  resolve(
    new Promise((innerResolve) =&amp;gt; {
      setTimeout(() =&amp;gt; {
        innerResolve(&quot;Inner operation completed&quot;);
      }, 1000);
    })
  );
});

outerPromise.then((result) =&amp;gt; {
  console.log(result);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output after approximately one second:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Inner operation completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice something important: &lt;code&gt;outerPromise&lt;/code&gt; doesn't fulfill with the inner Promise object as its final value.&lt;/p&gt;
&lt;p&gt;When a Promise is resolved with another Promise, it adopts the inner Promise's eventual state and outcome.&lt;/p&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The outer Promise's executor starts synchronously.&lt;/li&gt;
&lt;li&gt;The inner Promise is created and its executor also starts synchronously.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resolve(innerPromise)&lt;/code&gt; resolves the outer Promise to follow the inner Promise.&lt;/li&gt;
&lt;li&gt;The timer eventually fulfills the inner Promise.&lt;/li&gt;
&lt;li&gt;The outer Promise then fulfills with &lt;code&gt;&quot;Inner operation completed&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The outer Promise's &lt;code&gt;.then()&lt;/code&gt; reaction receives that string.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Outer Promise
     |
     | resolved with inner Promise
     v
Inner Promise
     |
     | pending for 1 second
     v
Fulfilled with a string
     |
     v
Outer Promise fulfills with the same outcome
     |
     v
.then() receives the string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This behavior is part of Promise resolution. It's why Promise chains can be composed without manually inspecting whether each returned value is itself a Promise.&lt;/p&gt;
&lt;h4 id=&quot;pattern-b-creating-a-promise-but-not-returning-it&quot;&gt;Pattern B: Creating a Promise but not returning it&lt;/h4&gt;
&lt;p&gt;Now compare that with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const outerPromise = new Promise((resolve) =&amp;gt; {
  new Promise((innerResolve) =&amp;gt; {
    setTimeout(() =&amp;gt; {
      innerResolve(&quot;Inner operation completed&quot;);
    }, 1000);
  });

  resolve(&quot;Outer operation completed&quot;);
});

outerPromise.then((result) =&amp;gt; {
  console.log(result);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Outer operation completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The inner Promise still runs. After approximately one second, it fulfills with &lt;code&gt;&quot;Inner operation completed&quot;&lt;/code&gt;, but nothing consumes that result.&lt;/p&gt;
&lt;p&gt;The outer Promise doesn't wait for it because the inner Promise was neither returned from a relevant callback nor used to resolve the outer Promise.&lt;/p&gt;
&lt;p&gt;This is a subtle but important distinction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creating a Promise&lt;/strong&gt; starts the executor and establishes an asynchronous result.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Returning a Promise from a &lt;code&gt;.then()&lt;/code&gt; callback&lt;/strong&gt; connects its outcome to the chain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolving a Promise with another Promise&lt;/strong&gt; makes the first Promise adopt the second Promise's outcome.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are related concepts, but they aren't interchangeable.&lt;/p&gt;
&lt;h4 id=&quot;a-complete-execution-order-example&quot;&gt;A complete execution-order example&lt;/h4&gt;
&lt;p&gt;Let's combine synchronous execution, nested Promise creation, a timer and &lt;code&gt;.then()&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&quot;0. Start&quot;);

function getUser() {
  return new Promise((resolve) =&amp;gt; {
    console.log(&quot;1. Outer executor&quot;);

    resolve(
      new Promise((innerResolve) =&amp;gt; {
        console.log(&quot;2. Inner executor&quot;);

        setTimeout(() =&amp;gt; {
          console.log(&quot;4. Inner timer&quot;);
          innerResolve({ id: 1, name: &quot;Fazal&quot; });
        }, 1000);
      })
    );
  });
}

getUser().then((user) =&amp;gt; {
  console.log(&quot;5. User:&quot;, user.name);
});

console.log(&quot;3. End&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0. Start
1. Outer executor
2. Inner executor
3. End
4. Inner timer
5. User: Fazal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two executor messages appear immediately because both executors run synchronously. The &lt;code&gt;&quot;End&quot;&lt;/code&gt; message follows because JavaScript continues running synchronous code. The timer callback executes later and the &lt;code&gt;.then()&lt;/code&gt; callback runs after the Promise has fulfilled.&lt;/p&gt;
&lt;p&gt;You don't need to memorize this particular output. You need to understand &lt;strong&gt;which operations are synchronous, which callbacks are scheduled and which Promise outcomes are connected&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-nested-then-callbacks-vs-promise-chaining&quot;&gt;4. Nested &lt;code&gt;.then()&lt;/code&gt; callbacks vs. Promise chaining&lt;/h3&gt;
&lt;p&gt;Let's return to the registration example.&lt;/p&gt;
&lt;p&gt;A nested version might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  return saveUser().then((user) =&amp;gt; {
    return sendWelcomeEmail(user).then(() =&amp;gt; {
      console.log(&quot;Email step completed&quot;);
      return user;
    });
  });
}

registerUser().then((user) =&amp;gt; {
  console.log(&quot;Registration flow completed for&quot;, user.name);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is valid JavaScript. The inner &lt;code&gt;.then()&lt;/code&gt; returns a Promise and the outer callback returns that Promise. The whole chain waits for the email operation.&lt;/p&gt;
&lt;p&gt;But nesting adds indentation and makes longer flows harder to follow.&lt;/p&gt;
&lt;p&gt;We can express the same dependency using a flat chain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  let registeredUser;

  return saveUser()
    .then((user) =&amp;gt; {
      registeredUser = user;
      return sendWelcomeEmail(user);
    })
    .then(() =&amp;gt; {
      console.log(&quot;Email step completed&quot;);
      return registeredUser;
    });
}

registerUser().then((user) =&amp;gt; {
  console.log(&quot;Registration flow completed for&quot;, user.name);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both versions wait for the email operation.&lt;/p&gt;
&lt;p&gt;The flat chain makes the sequence easier to scan, although it uses a variable to preserve the user value. In production, you should consider whether that extra state is necessary and whether it makes the flow clearer.&lt;/p&gt;
&lt;p&gt;You can also preserve the user value without an outer variable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  return saveUser().then((user) =&amp;gt; {
    return sendWelcomeEmail(user).then(() =&amp;gt; user);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or, using &lt;code&gt;async/await&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  const user = await saveUser();
  await sendWelcomeEmail(user);
  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There isn't a universal rule that every nested &lt;code&gt;.then()&lt;/code&gt; is wrong. Nesting is sometimes natural when a local operation has its own result handling. The problem is accidental nesting that hides dependencies, loses return values, or makes error handling confusing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-the-missing-return-a-small-mistake-with-several-consequences&quot;&gt;5. The missing &lt;code&gt;return&lt;/code&gt;: a small mistake with several consequences&lt;/h3&gt;
&lt;p&gt;Let's examine the most common version of this bug more closely.&lt;/p&gt;
&lt;h4 id=&quot;case-1-the-next-step-starts-too-early&quot;&gt;Case 1: The next step starts too early&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function processOrder(order) {
  return chargePayment(order)
    .then((payment) =&amp;gt; {
      updateOrderStatus(order.id, payment);
    })
    .then(() =&amp;gt; {
      console.log(&quot;Order processing completed&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;updateOrderStatus()&lt;/code&gt; returns a Promise, the final callback doesn't wait for it.&lt;/p&gt;
&lt;p&gt;Correct version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function processOrder(order) {
  return chargePayment(order)
    .then((payment) =&amp;gt; {
      return updateOrderStatus(order.id, payment);
    })
    .then(() =&amp;gt; {
      console.log(&quot;Order processing completed&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the next step waits for the status update.&lt;/p&gt;
&lt;h4 id=&quot;case-2-errors-don-t-propagate-through-the-expected-chain&quot;&gt;Case 2: Errors don't propagate through the expected chain&lt;/h4&gt;
&lt;p&gt;Suppose an inner operation rejects.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function processOrder(order) {
  return chargePayment(order)
    .then((payment) =&amp;gt; {
      updateOrderStatus(order.id, payment);
    })
    .catch((error) =&amp;gt; {
      console.error(&quot;Order processing failed:&quot;, error);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;updateOrderStatus()&lt;/code&gt; returns a Promise that rejects, the &lt;code&gt;.catch()&lt;/code&gt; above won't catch that rejection through this chain because its Promise was never returned.&lt;/p&gt;
&lt;p&gt;Correct version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function processOrder(order) {
  return chargePayment(order)
    .then((payment) =&amp;gt; {
      return updateOrderStatus(order.id, payment);
    })
    .catch((error) =&amp;gt; {
      console.error(&quot;Order processing failed:&quot;, error);
      throw error;
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;throw error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without it, the &lt;code&gt;.catch()&lt;/code&gt; callback would fulfill the chain with &lt;code&gt;undefined&lt;/code&gt; if it simply logged the error and returned nothing. Downstream code could then mistake the failure for success.&lt;/p&gt;
&lt;p&gt;You should decide whether to rethrow, recover with a fallback, or handle the error and intentionally return a successful result. The right choice depends on the application.&lt;/p&gt;
&lt;h4 id=&quot;case-3-the-caller-receives-a-result-too-early&quot;&gt;Case 3: The caller receives a result too early&lt;/h4&gt;
&lt;p&gt;Imagine a function that should return the saved record:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function updateProfile(profile) {
  return saveProfile(profile).then((savedProfile) =&amp;gt; {
    refreshSearchIndex(savedProfile);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The caller receives a Promise that fulfills with &lt;code&gt;undefined&lt;/code&gt; after the callback finishes—not with the search-index operation's result.&lt;/p&gt;
&lt;p&gt;If the caller expects to wait for indexing too, return its Promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function updateProfile(profile) {
  return saveProfile(profile).then((savedProfile) =&amp;gt; {
    return refreshSearchIndex(savedProfile);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the caller needs the saved profile as its result after indexing finishes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function updateProfile(profile) {
  return saveProfile(profile).then((savedProfile) =&amp;gt; {
    return refreshSearchIndex(savedProfile).then(() =&amp;gt; savedProfile);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes the intended result explicit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Don't return a Promise just to make every operation sequential. Return it when the caller needs to wait for that operation or depend on its outcome. Independent work may be better handled concurrently or in the background.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-is-this-bug-specific-to-promises-or-does-it-also-happen-with-async-await-&quot;&gt;6. Is this bug specific to Promises, or does it also happen with &lt;code&gt;async/await&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;This is an excellent question because &lt;code&gt;async/await&lt;/code&gt; often makes asynchronous code easier to read, but it doesn't eliminate the underlying problem.&lt;/p&gt;
&lt;p&gt;The bug is &lt;strong&gt;generic to asynchronous control flow&lt;/strong&gt;. It happens whenever the program starts work but fails to connect that work to the sequence that depends on it.&lt;/p&gt;
&lt;p&gt;Let's translate the registration example.&lt;/p&gt;
&lt;h4 id=&quot;incorrect-async-await-implementation&quot;&gt;Incorrect &lt;code&gt;async/await&lt;/code&gt; implementation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  const user = await saveUser();

  sendWelcomeEmail(user);

  console.log(&quot;Registration completed successfully&quot;);
}

registerUser();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output can still be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. User saved to database
3. Registration completed successfully
2. Welcome email sent to Fazal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;sendWelcomeEmail(user)&lt;/code&gt; starts the operation, but the function doesn't &lt;code&gt;await&lt;/code&gt; its Promise.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;async&lt;/code&gt; keyword does not automatically wait for every Promise created inside the function.&lt;/p&gt;
&lt;h4 id=&quot;correct-implementation&quot;&gt;Correct implementation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  const user = await saveUser();

  await sendWelcomeEmail(user);

  console.log(&quot;Registration completed successfully&quot;);
}

registerUser();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the function waits for the email operation before logging success.&lt;/p&gt;
&lt;p&gt;The difference is not that &lt;code&gt;async/await&lt;/code&gt; has magical asynchronous behavior. It provides syntax for expressing the dependency clearly.&lt;/p&gt;
&lt;p&gt;Compare the two patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Promise chaining
return saveUser()
  .then((user) =&amp;gt; sendWelcomeEmail(user))
  .then(() =&amp;gt; {
    console.log(&quot;Registration completed&quot;);
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// async/await
async function registerUser() {
  const user = await saveUser();
  await sendWelcomeEmail(user);

  console.log(&quot;Registration completed&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both can represent the same sequence of operations.&lt;/p&gt;
&lt;h4 id=&quot;a-useful-rule&quot;&gt;A useful rule&lt;/h4&gt;
&lt;p&gt;If a later step must wait for an asynchronous operation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;With Promise chaining, &lt;strong&gt;return the Promise&lt;/strong&gt; from the callback.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;async/await&lt;/code&gt;, &lt;strong&gt;await the Promise&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The underlying principle is the same: explicitly connect the operation to the control flow that depends on it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-error-handling-catch-vs-try-catch&quot;&gt;7. Error handling: &lt;code&gt;.catch()&lt;/code&gt; vs. &lt;code&gt;try/catch&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Correct sequencing is only half the story. We also need to understand how failures propagate.&lt;/p&gt;
&lt;h4 id=&quot;promise-based-error-handling&quot;&gt;Promise-based error handling&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function registerUser() {
  return saveUser()
    .then((user) =&amp;gt; {
      return sendWelcomeEmail(user);
    })
    .then(() =&amp;gt; {
      console.log(&quot;Registration completed&quot;);
    })
    .catch((error) =&amp;gt; {
      console.error(&quot;Registration failed:&quot;, error);
      throw error;
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A rejection from &lt;code&gt;saveUser()&lt;/code&gt; or &lt;code&gt;sendWelcomeEmail()&lt;/code&gt; can propagate to the final &lt;code&gt;.catch()&lt;/code&gt; because each operation is connected to the chain.&lt;/p&gt;
&lt;p&gt;If the email operation rejects, the success callback is skipped and the error handler runs.&lt;/p&gt;
&lt;h4 id=&quot;the-equivalent-async-await-approach&quot;&gt;The equivalent &lt;code&gt;async/await&lt;/code&gt; approach&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  try {
    const user = await saveUser();
    await sendWelcomeEmail(user);

    console.log(&quot;Registration completed&quot;);
  } catch (error) {
    console.error(&quot;Registration failed:&quot;, error);
    throw error;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;try/catch&lt;/code&gt; handles rejections from the awaited operations.&lt;/p&gt;
&lt;p&gt;However, consider this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  try {
    const user = await saveUser();

    sendWelcomeEmail(user);
    console.log(&quot;Registration completed&quot;);
  } catch (error) {
    console.error(&quot;Registration failed:&quot;, error);
    throw error;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;sendWelcomeEmail()&lt;/code&gt; returns a Promise that rejects, this &lt;code&gt;catch&lt;/code&gt; won't catch that rejection through the &lt;code&gt;try&lt;/code&gt; block because the Promise wasn't awaited. It may instead become an unhandled rejection, depending on what else handles it.&lt;/p&gt;
&lt;p&gt;Correct:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser() {
  try {
    const user = await saveUser();

    await sendWelcomeEmail(user);

    console.log(&quot;Registration completed&quot;);
  } catch (error) {
    console.error(&quot;Registration failed:&quot;, error);
    throw error;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is one additional nuance: &lt;code&gt;try/catch&lt;/code&gt; catches synchronous exceptions thrown while executing the &lt;code&gt;try&lt;/code&gt; block, as well as rejections from awaited Promises. It doesn't automatically catch failures from unrelated asynchronous operations that you start and leave unawaited.&lt;/p&gt;
&lt;p&gt;That is why &lt;code&gt;await&lt;/code&gt; matters for error handling as well as sequencing.&lt;/p&gt;
&lt;h4 id=&quot;should-you-always-rethrow-an-error-&quot;&gt;Should you always rethrow an error?&lt;/h4&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Rethrow when the caller needs to know the operation failed. Recover when you have a meaningful fallback. Handle the error without rethrowing only when the function's contract says the failure has been dealt with and downstream code should continue.&lt;/p&gt;
&lt;p&gt;For example, an optional analytics event might fail without preventing an order from being completed. A payment failure should not be silently swallowed.&lt;/p&gt;
&lt;p&gt;Error handling should reflect the business requirement, not just the syntax.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-promise-vs-async-await-which-should-you-use-&quot;&gt;8. Promise vs. &lt;code&gt;async/await&lt;/code&gt;: which should you use?&lt;/h3&gt;
&lt;p&gt;This isn't a competition in which one syntax is always correct. &lt;code&gt;async/await&lt;/code&gt; is built on Promises and is often easier to read for sequential workflows, while Promise methods remain valuable for composition and concurrency.&lt;/p&gt;
&lt;p&gt;Here's a practical comparison.&lt;/p&gt;
&lt;h4 id=&quot;scenario-a-sequential-operations&quot;&gt;Scenario A: Sequential operations&lt;/h4&gt;
&lt;p&gt;Suppose you need to fetch a user, then fetch that user's orders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Promise chaining:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getUserOrders(userId) {
  return fetchUser(userId)
    .then((user) =&amp;gt; fetchOrders(user.id));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;async/await&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getUserOrders(userId) {
  const user = await fetchUser(userId);
  const orders = await fetchOrders(user.id);

  return orders;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are valid. The second version can be easier to follow when the workflow contains many steps, conditions, or intermediate variables.&lt;/p&gt;
&lt;h4 id=&quot;scenario-b-several-independent-operations&quot;&gt;Scenario B: Several independent operations&lt;/h4&gt;
&lt;p&gt;Suppose a dashboard needs user information, notifications and recommendations. None depends on the others.&lt;/p&gt;
&lt;p&gt;This version unnecessarily runs the operations sequentially:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function loadDashboard() {
  const user = await fetchUser();
  const notifications = await fetchNotifications();
  const recommendations = await fetchRecommendations();

  return { user, notifications, recommendations };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If each operation takes time, the total duration can approach the sum of their durations.&lt;/p&gt;
&lt;p&gt;Instead, start the independent operations together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function loadDashboard() {
  const [user, notifications, recommendations] =
    await Promise.all([
      fetchUser(),
      fetchNotifications(),
      fetchRecommendations()
    ]);

  return { user, notifications, recommendations };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Promise.all()&lt;/code&gt; fulfills when all supplied Promises fulfill. If one rejects, the returned Promise rejects with that failure, though the other operations aren't automatically cancelled and may continue running.&lt;/p&gt;
&lt;p&gt;This is a common example of using &lt;code&gt;async/await&lt;/code&gt; and Promise composition together. You don't need to choose one or the other.&lt;/p&gt;
&lt;h4 id=&quot;scenario-c-independent-operations-where-partial-results-matter&quot;&gt;Scenario C: Independent operations where partial results matter&lt;/h4&gt;
&lt;p&gt;Suppose you're loading data from five optional integrations. One failing shouldn't prevent you from displaying the successful results.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;Promise.allSettled()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function loadIntegrations() {
  const results = await Promise.allSettled([
    fetchCalendar(),
    fetchTasks(),
    fetchNotifications()
  ]);

  for (const result of results) {
    if (result.status === &quot;fulfilled&quot;) {
      console.log(&quot;Loaded:&quot;, result.value);
    } else {
      console.error(&quot;Integration failed:&quot;, result.reason);
    }
  }

  return results;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This waits for every supplied Promise to settle and gives you the outcome of each one.&lt;/p&gt;
&lt;h4 id=&quot;scenario-d-you-want-a-promise-chain-without-an-async-function&quot;&gt;Scenario D: You want a Promise chain without an &lt;code&gt;async&lt;/code&gt; function&lt;/h4&gt;
&lt;p&gt;Sometimes a small function can be expressed naturally as a chain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function getDisplayName(userId) {
  return fetchUser(userId)
    .then((user) =&amp;gt; user.name)
    .catch((error) =&amp;gt; {
      console.error(&quot;Unable to load user:&quot;, error);
      throw error;
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You don't have to introduce &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; just because it exists. Promise chaining is still useful, particularly when transforming results or composing functions.&lt;/p&gt;
&lt;h4 id=&quot;a-practical-decision-guide&quot;&gt;A practical decision guide&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation                                          &lt;/th&gt;&lt;th&gt;A useful approach&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Several dependent steps                            &lt;/td&gt;&lt;td&gt;&lt;code&gt;async/await&lt;/code&gt; or a returned Promise chain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Several independent operations                     &lt;/td&gt;&lt;td&gt;&lt;code&gt;Promise.all()&lt;/code&gt; with &lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Need the outcome of every operation                &lt;/td&gt;&lt;td&gt;&lt;code&gt;Promise.allSettled()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Simple result transformation                       &lt;/td&gt;&lt;td&gt;Promise chaining&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complex branching and intermediate values          &lt;/td&gt;&lt;td&gt;Often &lt;code&gt;async/await&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Background work that shouldn't block the caller    &lt;/td&gt;&lt;td&gt;Explicit background-task handling&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Need to handle each operation's failure separately &lt;/td&gt;&lt;td&gt;Local &lt;code&gt;try/catch&lt;/code&gt; or per-Promise &lt;code&gt;.catch()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These are guidelines, not strict rules. Readability, error-handling requirements, cancellation needs and the behavior expected by the caller all matter.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-one-real-world-decision-should-the-email-block-registration-&quot;&gt;9. One real-world decision: should the email block registration?&lt;/h3&gt;
&lt;p&gt;Our registration example assumes that registration isn't complete until the email has been sent.&lt;/p&gt;
&lt;p&gt;But is that always the correct design?&lt;/p&gt;
&lt;p&gt;Imagine the database save succeeds and the email provider takes five seconds to respond. Should the user wait five seconds before receiving a successful registration response?&lt;/p&gt;
&lt;p&gt;Not necessarily.&lt;/p&gt;
&lt;p&gt;This is where we must distinguish a JavaScript bug from a product or architecture decision.&lt;/p&gt;
&lt;h4 id=&quot;option-a-email-is-part-of-the-required-workflow&quot;&gt;Option A: Email is part of the required workflow&lt;/h4&gt;
&lt;p&gt;If the operation must finish before the caller can proceed, return or await the email Promise.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser(input) {
  const user = await saveUser(input);
  await sendWelcomeEmail(user);

  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The caller receives the user only after the awaited email operation fulfills. If it rejects, the function rejects unless the error is handled.&lt;/p&gt;
&lt;h4 id=&quot;option-b-registration-should-succeed-independently-of-email-delivery&quot;&gt;Option B: Registration should succeed independently of email delivery&lt;/h4&gt;
&lt;p&gt;If the user should be registered even when the email provider is temporarily unavailable, don't pretend that the email is part of the same success condition.&lt;/p&gt;
&lt;p&gt;A simple illustrative approach is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function registerUser(input) {
  const user = await saveUser(input);

  try {
    await sendWelcomeEmail(user);
  } catch (error) {
    console.error(&quot;Welcome email failed:&quot;, error);
    // Record or schedule a retry if the application supports it.
  }

  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This waits for the email attempt but doesn't let its failure reject registration. It is not truly non-blocking, though, because the function still waits for the attempt to settle.&lt;/p&gt;
&lt;p&gt;If email delivery should happen independently, a background job or durable queue is usually more appropriate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Registration request
       |
       v
Save user + record email job
       |
       v
Return registration result
       |
       v
Background worker
       |
       v
Send email and retry if needed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a production system, you would need to consider how the job is persisted, retried and monitored. A simple fire-and-forget call such as &lt;code&gt;sendWelcomeEmail(user)&lt;/code&gt; is not a reliable substitute for a durable background-work mechanism.&lt;/p&gt;
&lt;p&gt;This illustrates an important lesson: &lt;strong&gt;waiting for an operation, handling its failure and deciding whether it belongs in the critical path are three separate decisions.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-how-to-debug-confusing-promise-execution-flow&quot;&gt;10. How to debug confusing Promise execution flow&lt;/h3&gt;
&lt;p&gt;When asynchronous code behaves unexpectedly, don't start by rewriting everything with &lt;code&gt;async/await&lt;/code&gt;. First establish which Promise represents which operation.&lt;/p&gt;
&lt;p&gt;Here is a practical checklist.&lt;/p&gt;
&lt;h4 id=&quot;step-1-add-logs-at-the-boundaries&quot;&gt;Step 1: Add logs at the boundaries&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function updateProfile(profile) {
  console.log(&quot;Starting update&quot;);

  return saveProfile(profile).then((savedProfile) =&amp;gt; {
    console.log(&quot;Profile saved&quot;);

    return refreshSearchIndex(savedProfile);
  }).then(() =&amp;gt; {
    console.log(&quot;Index refreshed&quot;);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log before and after each important operation. This helps you see whether the unexpected order is caused by missing Promise composition or by the operation itself.&lt;/p&gt;
&lt;h4 id=&quot;step-2-inspect-every-asynchronous-callback&quot;&gt;Step 2: Inspect every asynchronous callback&lt;/h4&gt;
&lt;p&gt;For every &lt;code&gt;.then()&lt;/code&gt; callback, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does this callback start another asynchronous operation?&lt;/li&gt;
&lt;li&gt;Does the next step need to wait for it?&lt;/li&gt;
&lt;li&gt;If yes, am I returning its Promise?&lt;/li&gt;
&lt;li&gt;Does this callback need to preserve a value for a later step?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For every &lt;code&gt;async&lt;/code&gt; function, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is the relevant operation awaited?&lt;/li&gt;
&lt;li&gt;Does the function return the value the caller expects?&lt;/li&gt;
&lt;li&gt;Is the rejection handled at the correct level?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-3-inspect-the-returned-value&quot;&gt;Step 3: Inspect the returned value&lt;/h4&gt;
&lt;p&gt;A callback with braces doesn't implicitly return its contents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;.then((result) =&amp;gt; {
  transformResult(result);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This callback returns &lt;code&gt;undefined&lt;/code&gt; unless it explicitly returns something.&lt;/p&gt;
&lt;p&gt;Compare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;.then((result) =&amp;gt; {
  return transformResult(result);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;.then((result) =&amp;gt; transformResult(result))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The latter two return the result of &lt;code&gt;transformResult()&lt;/code&gt;, including its Promise if it returns one.&lt;/p&gt;
&lt;h4 id=&quot;step-4-check-rejection-paths-not-just-successful-execution&quot;&gt;Step 4: Check rejection paths, not just successful execution&lt;/h4&gt;
&lt;p&gt;A chain may appear correct when every operation succeeds but fail when one rejects.&lt;/p&gt;
&lt;p&gt;Test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first operation rejects.&lt;/li&gt;
&lt;li&gt;An inner asynchronous operation rejects.&lt;/li&gt;
&lt;li&gt;A callback throws synchronously.&lt;/li&gt;
&lt;li&gt;A fallback handles an error.&lt;/li&gt;
&lt;li&gt;A caller expects a result but receives &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A successful manual test alone isn't enough to establish correct error propagation.&lt;/p&gt;
&lt;h4 id=&quot;step-5-write-down-the-intended-dependency&quot;&gt;Step 5: Write down the intended dependency&lt;/h4&gt;
&lt;p&gt;Before changing code, express the desired workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Save user
   ↓ must finish before
Send email
   ↓ must finish before
Return success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the actual requirement is different, draw that flow instead. This helps distinguish a missing &lt;code&gt;return&lt;/code&gt; or &lt;code&gt;await&lt;/code&gt; from an incorrect assumption about which operations must be sequential.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;11-common-misconceptions-worth-remembering&quot;&gt;11. Common misconceptions worth remembering&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Misconception 1: Every Promise runs asynchronously.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The executor passed to &lt;code&gt;new Promise()&lt;/code&gt; runs synchronously. Promise reactions, such as &lt;code&gt;.then()&lt;/code&gt; callbacks, run asynchronously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misconception 2: Creating a Promise means the current function waits for it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It doesn't. You must connect its outcome to the relevant Promise chain or await it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misconception 3: &lt;code&gt;async&lt;/code&gt; automatically waits for all Promises inside a function.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It doesn't. &lt;code&gt;async&lt;/code&gt; makes the function return a Promise. Individual asynchronous operations still need to be awaited or explicitly composed when their completion matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misconception 4: Nested Promises are always bad code.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not true. Resolving a Promise with another Promise and returning Promises from callbacks are normal parts of Promise composition. Deeply nested callbacks may hurt readability, but nesting itself isn't proof of a bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misconception 5: &lt;code&gt;Promise.all()&lt;/code&gt; cancels the other operations when one rejects.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It doesn't automatically cancel them. Its returned Promise rejects when one of its inputs rejects, but the remaining operations may continue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misconception 6: Converting everything to &lt;code&gt;async/await&lt;/code&gt; fixes asynchronous bugs.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It can improve readability, but forgetting &lt;code&gt;await&lt;/code&gt; creates the same fundamental problem as forgetting to return a Promise from a chain.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;12-a-final-debugging-challenge&quot;&gt;12. A final debugging challenge&lt;/h3&gt;
&lt;p&gt;Before finishing, predict the output of this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&quot;A&quot;);

Promise.resolve()
  .then(() =&amp;gt; {
    console.log(&quot;B&quot;);

    Promise.resolve().then(() =&amp;gt; {
      console.log(&quot;C&quot;);
    });
  })
  .then(() =&amp;gt; {
    console.log(&quot;D&quot;);
  });

console.log(&quot;E&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What order do you expect?&lt;/p&gt;
&lt;p&gt;The correct output is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A
E
B
C
D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's why:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; runs synchronously.&lt;/li&gt;
&lt;li&gt;The first &lt;code&gt;.then()&lt;/code&gt; callback is scheduled as a Promise reaction.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E&lt;/code&gt; runs before Promise reactions are processed.&lt;/li&gt;
&lt;li&gt;The first callback runs and logs &lt;code&gt;B&lt;/code&gt;. It creates another Promise reaction but doesn't return that inner Promise.&lt;/li&gt;
&lt;li&gt;The inner reaction logs &lt;code&gt;C&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The next callback in the outer chain logs &lt;code&gt;D&lt;/code&gt; after the first callback's returned value—&lt;code&gt;undefined&lt;/code&gt;—fulfills the preceding chain Promise.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This example shows why you can't simply assume that all nested Promise callbacks execute before the next step in the outer chain. Their scheduling and Promise dependencies both matter.&lt;/p&gt;
&lt;p&gt;Try modifying the example to return the inner Promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;console.log(&quot;A&quot;);

Promise.resolve()
  .then(() =&amp;gt; {
    console.log(&quot;B&quot;);

    return Promise.resolve().then(() =&amp;gt; {
      console.log(&quot;C&quot;);
    });
  })
  .then(() =&amp;gt; {
    console.log(&quot;D&quot;);
  });

console.log(&quot;E&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output is still:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A
E
B
C
D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The visible output happens to be the same in this example, but the Promise dependency is different. With the return, the outer chain explicitly waits for the inner Promise. Without it, the two branches are not connected, even though this particular output order looks identical.&lt;/p&gt;
&lt;p&gt;That's an important debugging lesson: &lt;strong&gt;a log sequence alone doesn't always prove that asynchronous operations are correctly connected&lt;/strong&gt;. Test the behavior and rejection paths, not just the happy-path output.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Promises aren't inherently complicated, but their behavior becomes much easier to understand once you distinguish creating an operation from connecting its outcome to the rest of your code.&lt;/p&gt;
&lt;p&gt;If you take only a few lessons from this article, remember these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Return a Promise&lt;/strong&gt; from a &lt;code&gt;.then()&lt;/code&gt; callback when the chain must wait for that operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Await a Promise&lt;/strong&gt; inside an &lt;code&gt;async&lt;/code&gt; function when subsequent code depends on its outcome.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Propagate or handle errors intentionally&lt;/strong&gt; instead of accidentally converting failures into successful results.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Promise.all()&lt;/code&gt; for independent operations that can run concurrently and choose &lt;code&gt;Promise.allSettled()&lt;/code&gt; when you need every operation's outcome.&lt;/li&gt;
&lt;li&gt;Don't assume every asynchronous task should block the caller. Decide whether it belongs in the critical path or should be handled by a background-work mechanism.&lt;/li&gt;
&lt;li&gt;Debug the dependency between operations, not just the order of console messages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And remember: the same control-flow bug can appear with Promise chains and &lt;code&gt;async/await&lt;/code&gt;. Changing syntax can make code easier to read, but it doesn't replace understanding the behavior you need.&lt;/p&gt;
&lt;p&gt;The next time your application logs “Done” before the work is actually done, look for the missing &lt;code&gt;return&lt;/code&gt;, the missing &lt;code&gt;await&lt;/code&gt;, or an operation that was never connected to the flow in the first place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That's often where the real bug begins.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;💬 &lt;strong&gt;Your turn:&lt;/strong&gt; Have you ever encountered a missing &lt;code&gt;return&lt;/code&gt; or &lt;code&gt;await&lt;/code&gt; that caused a race condition or made an error disappear? Share the scenario in the comments. Real debugging experiences are often the best way for all of us to learn.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33226/javascript-promises-inside-promises-the-hidden-bugs-execution-flow-and-how-to-fix-them</guid>
<pubDate>Sat, 31 Oct 2026 12:38:32 +0000</pubDate>
</item>
<item>
<title>Embeds: Putting a Live NitroIDE Playground Inside Your Docs</title>
<link>https://coderlegion.com/33224/embeds-putting-a-live-nitroide-playground-inside-your-docs</link>
<description>&lt;p&gt;Every tutorial has the same weak spot: the code sample. A static block shows what the code looks like. It cannot show what the code does. The reader has to copy it, open an editor, paste it, and run it — and every step in that chain is a step where a reader leaves.&lt;/p&gt;
&lt;p&gt;A month ago I wrote about NitroIDE's share engine: the Share button compresses an entire workspace into a ?code= URL parameter, and opening that link rebuilds the workspace locally in the reader's browser. Nothing is uploaded to a server. That post was about sending code to one person. This one is about the layer on top of it: embedding that workspace inside a page, so the reader never has to leave the tutorial at all.&lt;/p&gt;
&lt;h3 id=&quot;one-parameter-turns-a-share-link-into-an-embed&quot;&gt;One parameter turns a share link into an embed&lt;/h3&gt;
&lt;p&gt;Take any NitroIDE share URL and add ?embed=1 to it. That is the whole API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;iframe src=&quot;https://nitroide.com/tools/codebox.html?embed=1&amp;amp;code=YOUR_SHARE_CODE&quot; width=&quot;100%&quot; height=&quot;500&quot; frameborder=&quot;0&quot;&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The embed loads a stripped-down, read-only viewer. The full IDE chrome is gone — no file explorer, no console panel, no settings menus. What remains is the two things a reader actually needs: the editor and the live preview, side by side. The reader sees the code and its output in the same frame, on the same page, with nothing to install and no account to create.&lt;/p&gt;
&lt;p&gt;Read-only is a deliberate choice, not a missing feature. A tutorial embed is not a workspace; it is a demonstration. If the reader could edit the code in place, every edit would be a fork of the author's example with no way back — and the next reader would see someone else's experiment, not the tutorial. So the embed shows, and the escape hatch handles editing: every embed carries a footer link reading &quot;Built with NitroIDE — open &amp;amp; edit&quot;. One click opens the exact workspace in the full editor, where the reader can change anything. Show first, edit second, and never confuse the two.&lt;/p&gt;
&lt;h3 id=&quot;where-this-actually-belongs&quot;&gt;Where this actually belongs&lt;/h3&gt;
&lt;p&gt;The honest answer is: anywhere that renders an iframe and where the reader is there to learn. Blog posts, documentation pages, tutorial sites, course material — anywhere a code sample currently sits as a dead block.&lt;/p&gt;
&lt;p&gt;It does not belong in a GitHub README. GitHub strips iframes from rendered Markdown, so an embed pasted into a README renders as nothing. That is what the &quot;Open in NitroIDE&quot; badge is for — a link, not a frame — and knowing which of the two a surface accepts is the difference between an embed that works and one that silently vanishes. If the page cannot render an iframe, do not force the embed in; link out instead.&lt;/p&gt;
&lt;p&gt;The second place it belongs is less obvious: your own changelog and release notes. A new feature described in prose is a claim; the same feature embedded as a running demo is evidence. When a release note says &quot;the preview now updates without flicker,&quot; an embedded playground running the new behaviour lets the reader verify it in ten seconds instead of trusting the sentence.&lt;/p&gt;
&lt;h3 id=&quot;the-details-that-make-an-embed-feel-native-not-bolted-on&quot;&gt;The details that make an embed feel native, not bolted on&lt;/h3&gt;
&lt;p&gt;A bare iframe works. A few attributes make it feel like part of the page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;width=&quot;100%&quot; and a fixed height around 500px. The embed is a window into a workspace, and a window needs a predictable frame. Let it fill the content column and keep the height tall enough that the preview is readable without scrolling inside a scrolling page.&lt;/li&gt;
&lt;li&gt;loading=&quot;lazy&quot; on embeds below the fold. A tutorial with five demos should not load five editors before the reader reaches the first one. Lazy loading defers each embed until it scrolls into view.&lt;/li&gt;
&lt;li&gt;A title attribute on the iframe. It costs nothing and it is what a screen reader announces; &quot;Live demo: responsive card layout&quot; is a better experience than an unnamed frame.&lt;/li&gt;
&lt;li&gt;One demo per concept. An embed is a heavyweight element — a whole editor and a live preview. Three embeds proving the same point are two embeds too many; one well-chosen demo per idea keeps the page fast and the argument sharp.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are NitroIDE features. They are the hosting page's job, and they are the difference between an embed that feels like part of the tutorial and one that feels pasted in.&lt;/p&gt;
&lt;h3 id=&quot;the-limits-stated-plainly&quot;&gt;The limits, stated plainly&lt;/h3&gt;
&lt;p&gt;An embed inherits every limit of the share link it is built on. The workspace travels inside the URL itself, compressed into the ?code= parameter, so a very large project makes a very long URL. For a tutorial demo — a handful of files, a single concept — that is never a problem. For a full application, it is: use ZIP export for the project and embed a small, focused demo of the idea instead. The embed is for the example, not the app.&lt;/p&gt;
&lt;p&gt;The second limit comes from the preview's sandbox. NitroIDE's preview runs in a sandboxed iframe with an opaque origin, which is what keeps code in a shared link from touching the reader's saved projects. The tradeoff is that localStorage, sessionStorage, and cookies are unavailable inside the preview — and in Safari, touching them throws a SecurityError. Demo code that reads localStorage unguarded will crash inside the embed while working fine in a normal tab. Wrap storage access in try/catch in any code you plan to embed, and the same demo runs everywhere.&lt;/p&gt;
&lt;p&gt;The third limit is the read-only viewer itself. It hides the console, so a demo whose point is console output — logging, errors, debugging — loses its punchline inside an embed. That is a real constraint, and the honest response is to pick the right surface: embed the demos whose output is visual, and link out to the full editor for the ones whose output lives in the console.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-more-than-a-nicer-code-block&quot;&gt;Why this matters more than a nicer code block&lt;/h3&gt;
&lt;p&gt;Syntax highlighting made code readable. Embeds make it runnable, and that is a different category of change. A readable sample asks the reader to trust the author. A runnable sample lets the reader check the author — change a value, watch the preview move, break it, fix it. That is the feedback loop NitroIDE is built around, except now it happens inside someone else's docs, for a reader who never opened the IDE at all.&lt;/p&gt;
&lt;p&gt;The embed is also the quietest distribution NitroIDE has. Every embed carries its own attribution in the footer, and every footer click is a reader who arrived at the full editor from inside a tutorial they were already reading — not from a post, not from an ad, from a working demo. That is the growth loop that does not look like a growth loop: make the tool useful inside other people's pages, and let the pages do the talking.&lt;/p&gt;
&lt;p&gt;One parameter, one iframe, one running demo where a dead code block used to be. Try it with the docs example above — paste a share code into the snippet, drop it into a page, and the tutorial stops describing the code and starts running it.&lt;/p&gt;
&lt;p&gt;I build NitroIDE — a browser-based IDE where your code never leaves your machine: &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-31&quot;&gt;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-31&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33224/embeds-putting-a-live-nitroide-playground-inside-your-docs</guid>
<pubDate>Sat, 31 Oct 2026 12:32:01 +0000</pubDate>
</item>
<item>
<title>Don't Let Your Node.js Apps Drown: A Guide to Backpressure</title>
<link>https://coderlegion.com/33211/dont-let-your-node-js-apps-drown-a-guide-to-backpressure</link>
<description>&lt;h3 id=&quot;what-is-backpressure-&quot;&gt;What is Backpressure?&lt;/h3&gt;
&lt;p&gt;In the world of software development, backpressure is a communication signal sent from a data consumer (the part of the system receiving information) back to a data producer (the part sending it) to tell it to slow down. It occurs when the receiver is being overwhelmed with more data than it can process at that moment. Instead of letting the system crash or run out of memory, backpressure acts as a safety valve to keep the data flow manageable.&lt;/p&gt;
&lt;h3 id=&quot;the-fast-food-kitchen-analogy&quot;&gt;The Fast-Food Kitchen Analogy&lt;/h3&gt;
&lt;p&gt;To understand backpressure, imagine a busy fast-food restaurant. You have a chef (the producer) and a cashier (the consumer). The chef can flip burgers incredibly fast—let's say ten burgers every minute. However, the cashier can only bag the burgers and hand them to customers at a rate of three burgers per minute. &lt;/p&gt;
&lt;p&gt;If the chef keeps cooking at full speed, burgers will start piling up on the counter. Eventually, there is no more room on the counter, burgers start falling on the floor, and the kitchen becomes a chaotic mess. &lt;/p&gt;
&lt;p&gt;In this scenario, backpressure is the cashier shouting, &quot;Hey, stop cooking for a second! I've run out of counter space!&quot; Once the cashier clears some of the backlog, they give the chef the green light to start cooking again. This feedback loop ensures the &quot;buffer&quot; (the counter) never overflows.&lt;/p&gt;
&lt;h3 id=&quot;why-backpressure-matters-in-tech&quot;&gt;Why Backpressure Matters in Tech&lt;/h3&gt;
&lt;p&gt;As a senior developer working with Node.js, backpressure is a daily reality. Because Node.js is asynchronous and event-driven, it is very easy to read data from a source—like a massive log file or a database—much faster than we can write it to a destination, like a network socket or another file.&lt;/p&gt;
&lt;p&gt;Engineers use backpressure management to prevent &quot;Memory Leaks&quot; or &quot;Buffer Bloat.&quot; Without it, Node.js would attempt to store all the incoming data in your system's RAM (Random Access Memory) while waiting for the slow destination to catch up. If the file is 10GB and your server only has 4GB of RAM, your application will crash with an &lt;code&gt;OutOfMemory&lt;/code&gt; error. By implementing backpressure, we ensure the server only reads what it can actually handle, maintaining a low memory footprint and high stability.&lt;/p&gt;
&lt;h3 id=&quot;implementation-in-node-js&quot;&gt;Implementation in Node.js&lt;/h3&gt;
&lt;p&gt;In Node.js, backpressure is built into the &lt;code&gt;Stream&lt;/code&gt; API. When you use the &lt;code&gt;.write()&lt;/code&gt; method on a writable stream, it returns a boolean value. If it returns &lt;code&gt;false&lt;/code&gt;, that is the signal that backpressure is occurring and you should stop sending data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const fs = require('fs');

// Create a readable stream (The Producer)
const readable = fs.createReadStream('./massive-video-file.mp4');

// Create a writable stream (The Consumer)
const writable = fs.createWriteStream('./copy-of-video.mp4');

// The 'pipe' method automatically handles backpressure for us!
// It pauses the readable stream when the writable stream is full.
readable.pipe(writable);

// If we were doing this manually, it would look like this:
/*
readable.on('data', (chunk) =&amp;gt; {
  const canContinue = writable.write(chunk);
  if (!canContinue) {
    // Backpressure detected! Pause the producer
    readable.pause();
  }
});

// Listen for the 'drain' event to know when it is safe to resume
writable.on('drain', () =&amp;gt; {
  readable.resume();
});
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-takeaway&quot;&gt;The Takeaway&lt;/h3&gt;
&lt;p&gt;Backpressure isn't a bug; it's a fundamental feature of robust system design. It represents the wisdom of knowing when to say &quot;enough for now,&quot; ensuring that your fast components don't accidentally destroy your slower ones by being too efficient. Master the flow, and you master the stability of your entire stack.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/the-invisible-hand-of-data-flow.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33211/dont-let-your-node-js-apps-drown-a-guide-to-backpressure</guid>
<pubDate>Sat, 31 Oct 2026 09:58:48 +0000</pubDate>
</item>
<item>
<title>Your Tests Are Green and Prove Nothing: 4 Signs You're Testing the Mock</title>
<link>https://coderlegion.com/33169/your-tests-are-green-and-prove-nothing-4-signs-youre-testing-the-mock</link>
<description>&lt;p&gt;Coverage says 94%. Every test is green. You deploy on Friday afternoon, and by Monday a customer has found a bug in the exact function you &quot;tested&quot;.&lt;/p&gt;
&lt;p&gt;When you open the test, the reason is obvious in hindsight. The test never ran your code. It ran a fake version of everything around it and then asserted that the fake behaved like a fake.&lt;/p&gt;
&lt;p&gt;Here are four ways this happens, and a check you can do in a minute to see how many of your own tests are guilty.&lt;/p&gt;
&lt;h3 id=&quot;1-you-mocked-the-thing-you-are-testing&quot;&gt;1. You Mocked the Thing You Are Testing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// pricing.ts
export function applyDiscount(total: number, code: string) {
  return code === 'SAVE10' ? total * 0.9 : total;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// checkout.test.ts
import * as pricing from './pricing';

vi.spyOn(pricing, 'applyDiscount').mockReturnValue(90);

test('applies discount at checkout', () =&amp;gt; {
  expect(checkout(100, 'SAVE10')).toBe(90);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This passes if &lt;code&gt;applyDiscount&lt;/code&gt; is correct. It also passes if &lt;code&gt;applyDiscount&lt;/code&gt; returns &lt;code&gt;NaN&lt;/code&gt;, because the test replaced it with a hard-coded &lt;code&gt;90&lt;/code&gt;. You are testing that &lt;code&gt;checkout&lt;/code&gt; can return the number you handed it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; mock what is outside your code, such as network, clock and payment providers. Run your own logic for real.&lt;/p&gt;
&lt;h3 id=&quot;2-you-assert-the-calls-not-the-outcome&quot;&gt;2. You Assert the Calls, Not the Outcome&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test('saves the user', async () =&amp;gt; {
  await createUser({ email: '*Emails are not allowed*' });
  expect(db.insert).toHaveBeenCalledWith('users', { email: '*Emails are not allowed*' });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This test proves the function called &lt;code&gt;insert&lt;/code&gt; with those arguments. It does not prove a user exists. If someone renames the table, adds a required field or changes the query, this test keeps passing against the mock while real users stop being saved.&lt;/p&gt;
&lt;p&gt;It also fails for the wrong reason. Refactor the internals without changing behavior, and it turns red, so people learn that failing tests are noise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; assert what a user or caller would observe.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test('saves the user', async () =&amp;gt; {
  await createUser({ email: '*Emails are not allowed*' });
  const user = await findUserByEmail('*Emails are not allowed*');
  expect(user?.email).toBe('*Emails are not allowed*');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-the-fake-database-only-says-yes&quot;&gt;3. The Fake Database Only Says Yes&lt;/h3&gt;
&lt;p&gt;A mocked database returns whatever you tell it to. A real one has rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A unique index rejects duplicate emails.&lt;/li&gt;
&lt;li&gt;A required field rejects a missing value.&lt;/li&gt;
&lt;li&gt;A foreign key rejects an ID that does not exist.&lt;/li&gt;
&lt;li&gt;A query with a typo in a field name returns nothing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these exist in a mock, so none of these bugs can appear in your tests. They show up in production, on the first real duplicate signup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; for anything that touches storage, test against a real database, either a throwaway local instance or an in-memory one with the same engine, reset between tests. It is slower than a mock, but it is the only thing that can fail the way production fails.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test('rejects a duplicate email', async () =&amp;gt; {
  await createUser({ email: '*Emails are not allowed*' });
  await expect(createUser({ email: '*Emails are not allowed*' })).rejects.toThrow();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You cannot write that test against a mock that always resolves.&lt;/p&gt;
&lt;h3 id=&quot;4-snapshots-approved-without-reading&quot;&gt;4. Snapshots Approved Without Reading&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;expect(renderInvoice(order)).toMatchSnapshot();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A snapshot records whatever the output is today and fails when it changes. It does not know whether today's output is correct. When it fails, the usual reaction is to press &quot;update&quot; and move on, so the test stores bugs as the expected answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use snapshots for small, stable things, and assert the fields that matter for anything involving money, permissions or dates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const invoice = renderInvoice(order);
expect(invoice.total).toBe(118.0);
expect(invoice.lines).toHaveLength(2);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-60-second-sabotage-check&quot;&gt;The 60-Second Sabotage Check&lt;/h3&gt;
&lt;p&gt;Here is how to find out whether a test protects you. Take a function you care about, &lt;strong&gt;break it on purpose&lt;/strong&gt;, and run its tests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function applyDiscount(total: number, code: string) {
  return total; // sabotage: discount never applies
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then ask one question: &lt;strong&gt;did a test fail?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If yes, that test is doing its job.&lt;/li&gt;
&lt;li&gt;If no, you have found a test that would have stayed green while the feature was broken in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Revert the change, and fix or delete the test that did not notice. Try this on three important functions and you will learn more about your suite than coverage ever told you. The formal version of this idea is called &lt;strong&gt;mutation testing&lt;/strong&gt;, and tools exist that automate it, but doing it by hand on your most critical code is a good start.&lt;/p&gt;
&lt;h3 id=&quot;what-to-mock-and-what-not-to&quot;&gt;What to Mock, and What Not To&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Mock it &lt;/th&gt;&lt;th&gt;Run it for real&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Network calls to third parties &lt;/td&gt;&lt;td&gt;Your own business logic&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;The clock and random numbers &lt;/td&gt;&lt;td&gt;Your database queries&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Email and SMS sending &lt;/td&gt;&lt;td&gt;Validation and permission rules&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Payment providers &lt;/td&gt;&lt;td&gt;Anything that can fail on real data&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The rule is that you mock the &lt;strong&gt;edges&lt;/strong&gt;, the places where your code talks to someone else, and keep everything in the middle real.&lt;/p&gt;
&lt;h3 id=&quot;the-rule&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A test that cannot fail when the code is wrong is not a test.&lt;/strong&gt; Mock the world outside your app, assert outcomes instead of calls, and use sabotage to check whether your tests notice.&lt;/p&gt;
&lt;p&gt;Quick audit: pick the three functions that would hurt most if they broke, such as pricing, permissions and sign-up. Break each one on purpose. Count how many tests turn red. If the answer is zero for any of them, that is your next ticket.&lt;/p&gt;
&lt;p&gt;If you want a Next.js starting point to build and test on, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What is the most misleading green test you have ever found? Share it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33169/your-tests-are-green-and-prove-nothing-4-signs-youre-testing-the-mock</guid>
<pubDate>Sat, 31 Oct 2026 03:38:56 +0000</pubDate>
</item>
<item>
<title>Building a static blog with Sitelo, one function per page</title>
<link>https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</link>
<description>&lt;p&gt;In my &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/paulbrowne/javascript-in-html-out-building-a-simpler-static-site-generator-5ln&quot;&gt;last post on DEV&lt;/a&gt; I explained why I built Sitelo: most websites are documents, and documents shouldn't need a JavaScript application to reach the browser. That post was the argument. This one is the practice.&lt;/p&gt;
&lt;p&gt;We'll build a small blog from an empty folder: a home page, post pages generated from JSON files, and a comments section that stays fresh on an otherwise static page. Along the way I'll point out what Sitelo is doing at each step, and what it deliberately doesn't do.&lt;/p&gt;
&lt;p&gt;The whole framework fits in one sentence, and it's worth keeping in mind as we go:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Files are routes. Functions return HTML. Browser code is explicit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Everything below is that sentence, applied.&lt;/p&gt;
&lt;h3 id=&quot;step-1-one-page-is-one-function&quot;&gt;Step 1: one page is one function&lt;/h3&gt;
&lt;p&gt;Start with an empty folder and install Sitelo. It needs Node 20.19+ or 22.12+, and Vite is bundled, so there's nothing else to install or configure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm init -y
npm pkg set type=module
npm install -D sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add two scripts to &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;sitelo&quot;,
  &quot;build&quot;: &quot;sitelo build&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the first page. Any file in &lt;code&gt;src/&lt;/code&gt; ending in &lt;code&gt;.ht.js&lt;/code&gt; is a page, and its default export returns HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
export default () =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;title&amp;gt;My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;src/styles.css&lt;/code&gt; with a few rules, then run &lt;code&gt;npm run build&lt;/code&gt; and you get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist/
  index.html   ← your HTML, with &amp;lt;!DOCTYPE html&amp;gt; added
  styles.css   ← bundled and minified, because the page linked it
  404.html     ← a default one, until you write src/404.ht.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function ran once, at build time, in Node. The browser gets the string it returned and nothing else: no runtime, no hydration, no bundle of page code. If you want JavaScript in the browser, you add a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, the same as you would in a hand-written HTML file.&lt;/p&gt;
&lt;p&gt;Template literals aren't the only option. A page can also return JSX from a &lt;code&gt;.ht.jsx&lt;/code&gt; or &lt;code&gt;.ht.tsx&lt;/code&gt; file, which is rendered to a static string with &lt;code&gt;react-dom/server&lt;/code&gt;. Or you can use my companion library &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/javascript-to-html&quot;&gt;javascript-to-html&lt;/a&gt;, where every element is a function. I'll stick with template literals here because they need no explanation.&lt;/p&gt;
&lt;h3 id=&quot;step-2-a-layout-is-just-another-function&quot;&gt;Step 2: a layout is just another function&lt;/h3&gt;
&lt;p&gt;A blog needs the same &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and header on every page. There's no layout system to learn for this, because a layout is a function that takes some HTML and wraps it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/lib/layout.js
export const layout = ({ title, children }) =&amp;gt; `
  &amp;lt;html lang=&quot;en&quot;&amp;gt;
    &amp;lt;head&amp;gt;
      &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
      &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;
      &amp;lt;title&amp;gt;${title} · My blog&amp;lt;/title&amp;gt;
      &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/styles.css&quot;&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
      &amp;lt;header&amp;gt;&amp;lt;a href=&quot;/&quot;&amp;gt;My blog&amp;lt;/a&amp;gt; · &amp;lt;a href=&quot;/blog&quot;&amp;gt;Posts&amp;lt;/a&amp;gt;&amp;lt;/header&amp;gt;
      &amp;lt;main&amp;gt;${children}&amp;lt;/main&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
`
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/index.ht.js
import { layout } from './lib/layout.js'

export default () =&amp;gt; layout({
  title: 'Home',
  children: '&amp;lt;h1&amp;gt;Hello world&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Welcome to my blog.&amp;lt;/p&amp;gt;',
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;layout.js&lt;/code&gt; doesn't end in &lt;code&gt;.ht.js&lt;/code&gt;, so it isn't a page. It also never ends up in &lt;code&gt;dist/&lt;/code&gt;. Sitelo only emits a JavaScript or TypeScript file when your HTML references it with a &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;; a module you only import from page code stays on the build side. That rule matters more later, when the helpers you import hold API keys or database code. They can't leak into the browser by accident.&lt;/p&gt;
&lt;p&gt;The same rule works in the other direction. Write &lt;code&gt;&amp;lt;script type=&quot;module&quot; src=&quot;/main.ts&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; and Sitelo bundles &lt;code&gt;src/main.ts&lt;/code&gt; with esbuild, emits &lt;code&gt;main.js&lt;/code&gt;, and rewrites the tag to match. Browser code exists only where you asked for it.&lt;/p&gt;
&lt;h3 id=&quot;step-3-posts-from-json-files&quot;&gt;Step 3: posts from JSON files&lt;/h3&gt;
&lt;p&gt;Now for the posts. I'll keep them in the repo as JSON, one file per post, outside &lt;code&gt;src/&lt;/code&gt; because they're content rather than code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// data/posts/hello-world.json
{
  &quot;title&quot;: &quot;Hello, world&quot;,
  &quot;date&quot;: &quot;2026-10-01&quot;,
  &quot;body&quot;: &quot;&amp;lt;p&amp;gt;My first post.&amp;lt;/p&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The post page lives at &lt;code&gt;src/blog/[slug].ht.js&lt;/code&gt;. The square brackets make it a dynamic route, and it exports two more functions next to the page itself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;generateStaticParams()&lt;/code&gt; lists the pages to build, one per post.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data()&lt;/code&gt; loads what one page needs. Whatever it returns arrives in the page function as &lt;code&gt;data&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

const posts = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export async function generateStaticParams() {
  return (await posts()).map((post) =&amp;gt; ({ slug: post.slug }))
}

export async function data({ params }) {
  return (await posts()).find((post) =&amp;gt; post.slug === params.slug)
}

export default ({ data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;${post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;time datetime=&quot;${post.date}&quot;&amp;gt;${post.date}&amp;lt;/time&amp;gt;
      ${post.body}
    &amp;lt;/article&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;readJsonCollection&lt;/code&gt; reads every &lt;code&gt;.json&lt;/code&gt; file in the folder and takes each slug from its file name. It parses each file once per build, however many pages ask for it, and two posts with the same slug fail the build instead of one silently overwriting the other.&lt;/p&gt;
&lt;p&gt;The index page is the same idea without the brackets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/index.ht.js
import { readJsonCollection } from 'sitelo/data'
import { layout } from '../lib/layout.js'

export const data = () =&amp;gt; readJsonCollection('data/posts', { sort: '-date' })

export default ({ data: posts }) =&amp;gt; layout({
  title: 'Posts',
  children: `
    &amp;lt;h1&amp;gt;Posts&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
      ${posts.map((post) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;a href=&quot;/blog/${post.slug}&quot;&amp;gt;${post.title}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;`).join('')}
    &amp;lt;/ul&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A build now writes &lt;code&gt;dist/blog/index.html&lt;/code&gt; and one &lt;code&gt;dist/blog/&amp;lt;slug&amp;gt;/index.html&lt;/code&gt; per post. In the dev server (&lt;code&gt;npm run dev&lt;/code&gt;) pages render on request through Vite, so editing a JSON file reloads the browser, and an error points at the exact line of your page in the terminal.&lt;/p&gt;
&lt;p&gt;If your posts live in a CMS instead, &lt;code&gt;data()&lt;/code&gt; can &lt;code&gt;fetch&lt;/code&gt; them. Sitelo exports &lt;code&gt;fetchWithCache&lt;/code&gt;, a drop-in for &lt;code&gt;fetch&lt;/code&gt; that caches responses during the build. A 500-page site then doesn't make 500 identical requests to your API.&lt;/p&gt;
&lt;h3 id=&quot;step-4-fresh-comments-on-a-static-page&quot;&gt;Step 4: fresh comments on a static page&lt;/h3&gt;
&lt;p&gt;Comments are the classic reason static blogs reach for a client-side widget or give up and go server-rendered. Sitelo's answer is a server island: the page stays static, and one marked region is rendered on the server when someone views it.&lt;/p&gt;
&lt;p&gt;An island is a module in &lt;code&gt;src/islands/&lt;/code&gt; whose default export returns HTML. Islands are fragments rather than pages, so the file is plain &lt;code&gt;.js&lt;/code&gt;, not &lt;code&gt;.ht.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands/comments.js
import { getComments } from '../lib/comments.js'

const escape = (s) =&amp;gt; String(s).replace(/[&amp;amp;&amp;lt;&amp;gt;&quot;']/g, (c) =&amp;gt; `&amp;amp;#${c.charCodeAt(0)};`)

export default async function comments({ props }) {
  const list = await getComments(props.postId)
  if (!list.length) return '&amp;lt;p&amp;gt;No comments yet.&amp;lt;/p&amp;gt;'
  return `&amp;lt;ul&amp;gt;${list.map((c) =&amp;gt; `&amp;lt;li&amp;gt;&amp;lt;b&amp;gt;${escape(c.author)}&amp;lt;/b&amp;gt; ${escape(c.text)}&amp;lt;/li&amp;gt;`).join('')}&amp;lt;/ul&amp;gt;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getComments&lt;/code&gt; is whatever your comment store is: a database query, a call to a hosted service. Comments are user input, hence the &lt;code&gt;escape&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Place the island in the post page with &lt;code&gt;island()&lt;/code&gt;. The third argument is the fallback HTML that ships in the static build, and &lt;code&gt;when: 'visible'&lt;/code&gt; waits until the reader scrolls near the comments before making the request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/blog/[slug].ht.js (the page function)
import { island } from 'sitelo/islands'

export default ({ params, data: post }) =&amp;gt; layout({
  title: post.title,
  children: `
    &amp;lt;article&amp;gt;…&amp;lt;/article&amp;gt;
    ${island('comments', { postId: params.slug }, '&amp;lt;p&amp;gt;Loading comments…&amp;lt;/p&amp;gt;', { when: 'visible' })}
    &amp;lt;script type=&quot;module&quot; src=&quot;/islands.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  `,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script tag loads a two-line client that finds the placeholders and fetches them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// src/islands.js
import { mountIslands } from 'sitelo/islands/client'

mountIslands()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;npm run dev&lt;/code&gt; and &lt;code&gt;sitelo preview&lt;/code&gt; this already works: Sitelo serves islands at &lt;code&gt;/_sitelo/islands/&amp;lt;name&amp;gt;&lt;/code&gt;. In production your static host serves the pages, and you mount a small handler wherever you can run server code. &lt;code&gt;createIslandsHandler&lt;/code&gt; from &lt;code&gt;sitelo/islands/server&lt;/code&gt; takes a Web &lt;code&gt;Request&lt;/code&gt; and returns a &lt;code&gt;Response&lt;/code&gt;, so it fits a serverless or edge function. The repo has &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/islands&quot;&gt;Netlify and Vercel versions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A few details I cared about getting right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Props are untrusted.&lt;/strong&gt; They're embedded in the page and sent back on the request, so anyone can edit them. Set &lt;code&gt;SITELO_ISLANDS_SECRET&lt;/code&gt; and the build signs every placeholder with HMAC-SHA256, and the handler rejects edited props with a 403.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Islands are server-only.&lt;/strong&gt; The rule from Step 2 applies: nothing references &lt;code&gt;comments.js&lt;/code&gt; from HTML, so it never reaches &lt;code&gt;dist/&lt;/code&gt;, and neither does your database code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure is graceful.&lt;/strong&gt; If the island endpoint is down or slow (10 seconds by default), the fallback HTML stays where it was. The page still works.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-5-let-the-build-check-your-work&quot;&gt;Step 5: let the build check your work&lt;/h3&gt;
&lt;p&gt;The blog works. The last step is a config file, and it's where Sitelo earns its keep on a real site:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// sitelo.config.js
export default {
  site: 'https://myblog.example',
  rss: {
    site: 'https://myblog.example',
    title: 'My blog',
    description: 'Latest posts',
    routePrefix: '/blog',
  },
  images: true,
  linkCheck: 'error',
  lighthouse: {
    thresholds: { accessibility: 100, 'best-practices': 95, seo: 100 },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what each line buys you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;site&lt;/code&gt;&lt;/strong&gt; generates &lt;code&gt;dist/sitemap.xml&lt;/code&gt; from every route, including the ones &lt;code&gt;generateStaticParams&lt;/code&gt; produced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rss&lt;/code&gt;&lt;/strong&gt; writes &lt;code&gt;dist/rss.xml&lt;/code&gt; with an item for every page under &lt;code&gt;/blog&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;images&lt;/code&gt;&lt;/strong&gt; turns each &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; into a resized, WebP &lt;code&gt;srcset&lt;/code&gt; with &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; set, so a 3000px hero photo no longer ships to phones. It needs &lt;code&gt;sharp&lt;/code&gt; installed, and it works in the dev server too, so what you see is what you ship.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;linkCheck: 'error'&lt;/code&gt;&lt;/strong&gt; resolves every internal &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; against the files the build actually wrote, the way a static host would. A typo like &lt;code&gt;/blgo&lt;/code&gt; fails the build with the page it's on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lighthouse&lt;/code&gt;&lt;/strong&gt; gives &lt;code&gt;sitelo lighthouse&lt;/code&gt; thresholds to enforce. Any score under its threshold exits non-zero, so it can gate CI. Like sharp, Lighthouse is an optional install.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I left &lt;code&gt;performance&lt;/code&gt; out of the thresholds on purpose. It depends on the machine running the audit, and a page that scores 100 on a laptop can score around 60 on a shared CI runner. Accessibility, best practices and SEO are properties of the HTML itself, so they score the same anywhere.&lt;/p&gt;
&lt;p&gt;One check needs no config. If a page links a script or stylesheet that doesn't exist in &lt;code&gt;src/&lt;/code&gt; or &lt;code&gt;public/&lt;/code&gt;, the build fails and lists the paths it tried. That's why Step 1 asked you to create &lt;code&gt;styles.css&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;Every build ends with a report of what you're shipping: file counts and sizes per type, the largest files, and how long each phase took. On a static site, &quot;how much JavaScript am I sending?&quot; should be a number you see every time, not something you go looking for.&lt;/p&gt;
&lt;h3 id=&quot;building-it-with-a-coding-agent&quot;&gt;Building it with a coding agent&lt;/h3&gt;
&lt;p&gt;I designed Sitelo with coding agents in mind, and the small surface area matters even more for them than for people. Ask an agent to add a page to an unfamiliar framework and it reaches for whatever it has seen most: React components, Next.js conventions, Astro frontmatter. With Sitelo there's less to get wrong, because there's less of everything. A page is a function, a route is a file, and browser code is a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;p&gt;To close the remaining gap, Sitelo ships two &lt;a rel=&quot;nofollow&quot; href=&quot;https://agentskills.io&quot;&gt;Agent Skills&lt;/a&gt;: one for the framework (pages, routing, data, islands, deployment) and one for its component library. Install them into your project and Claude Code, Cursor, Codex and other agents load them when a task needs them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add Paul-Browne/sitelo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The skills also ship inside the npm package, matching the version you installed. Sitelo's test suite checks every API the skills mention against the package's real exports, so a renamed or removed API fails the tests before it reaches a release. For agents that prefer to read a URL, the docs publish &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/llms.txt&quot;&gt;sitelo.dev/llms.txt&lt;/a&gt;, plus a single-file &lt;code&gt;llms-full.txt&lt;/code&gt; with every guide in Markdown.&lt;/p&gt;
&lt;p&gt;The build checks from Step 5 help here too. An agent that invents a stylesheet path or links to a page that doesn't exist gets a failed build naming the problem, not a site that quietly ships broken.&lt;/p&gt;
&lt;h3 id=&quot;what-we-built-and-when-not-to-use-it&quot;&gt;What we built, and when not to use it&lt;/h3&gt;
&lt;p&gt;In five steps we went from an empty folder to a blog with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a home page, a post index and one page per JSON file&lt;/li&gt;
&lt;li&gt;comments rendered on the server at view time, on otherwise static pages&lt;/li&gt;
&lt;li&gt;a sitemap, an RSS feed and responsive images&lt;/li&gt;
&lt;li&gt;a build that fails on broken links and missing assets, plus a Lighthouse gate for accessibility and SEO&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The browser receives HTML, one stylesheet and a 1.7 kB island loader. That's all, because those are the only things we asked for.&lt;/p&gt;
&lt;p&gt;Sitelo isn't the right tool for everything. JSX pages render to static HTML only, so &lt;code&gt;onClick&lt;/code&gt; handlers and hooks do nothing in the browser. If you're building an application, with client-side state and routing, use an application framework. Astro, Next.js and Eleventy are all excellent at what they do. Sitelo is for docs, blogs, marketing sites and content sites, where &quot;view source&quot; should show what you wrote.&lt;/p&gt;
&lt;p&gt;If that sounds like your next project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://sitelo.dev/docs&quot;&gt;sitelo.dev/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (MIT licensed): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo&quot;&gt;github.com/Paul-Browne/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Package: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/sitelo&quot;&gt;npmjs.com/package/sitelo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repo includes a complete &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Paul-Browne/sitelo/tree/main/examples/blog&quot;&gt;blog example&lt;/a&gt; that goes further than this post, with Markdown posts. And if you find something that makes Sitelo more complicated than it needs to be, please open an issue. Keeping it boring is the whole point.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33109/building-a-static-blog-with-sitelo-one-function-per-page</guid>
<pubDate>Fri, 30 Oct 2026 14:35:56 +0000</pubDate>
</item>
<item>
<title>Your Next.js redirect() Silently Does Nothing Because It's Inside a try/catch</title>
<link>https://coderlegion.com/33072/your-next-js-redirect-silently-does-nothing-because-its-inside-a-try-catch</link>
<description>&lt;p&gt;A user fills in your form and clicks &quot;Create post&quot;. The post is saved in the database. And the page shows &quot;Could not create post&quot;.&lt;/p&gt;
&lt;p&gt;They try again. Now there are two posts.&lt;/p&gt;
&lt;p&gt;Nothing is wrong with your database. Your error handling caught something it was never supposed to catch.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-causes-it&quot;&gt;The Code That Causes It&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/actions.ts
'use server';

import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  try {
    const post = await db.post.create({
      data: { title: String(formData.get('title')) },
    });

    redirect(`/posts/${post.id}`);
  } catch (err) {
    return { error: 'Could not create post' };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It reads perfectly. Save the post, go to its page, and if anything fails, return an error. A tidy, defensive pattern that you have probably written a hundred times in other frameworks.&lt;/p&gt;
&lt;h3 id=&quot;why-it-breaks&quot;&gt;Why It Breaks&lt;/h3&gt;
&lt;p&gt;In Next.js, &lt;code&gt;redirect()&lt;/code&gt; does not return. It &lt;strong&gt;throws a special error&lt;/strong&gt;, and the framework catches that error higher up and turns it into a redirect response.&lt;/p&gt;
&lt;p&gt;That means your &lt;code&gt;catch&lt;/code&gt; block receives it first. Your code swallows the redirect, returns &lt;code&gt;{ error: ... }&lt;/code&gt;, and the framework never sees it.&lt;/p&gt;
&lt;p&gt;The same is true for &lt;code&gt;notFound()&lt;/code&gt;, which also works by throwing. Both are written to stop execution immediately, which is why TypeScript treats them as functions that never return.&lt;/p&gt;
&lt;h3 id=&quot;why-it-is-hard-to-catch&quot;&gt;Why It Is Hard to Catch&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The data is saved.&lt;/strong&gt; The only visible symptom is a wrong error message, so it looks like a UI bug, not a logic bug.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is inconsistent in your head.&lt;/strong&gt; The same &lt;code&gt;redirect()&lt;/code&gt; works fine outside a &lt;code&gt;try&lt;/code&gt;, so you assume it is fine everywhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your logs are confusing.&lt;/strong&gt; If you log the caught error, you will see a strange error with a &lt;code&gt;NEXT_REDIRECT&lt;/code&gt; digest and think something crashed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It tempts you to retry.&lt;/strong&gt; Users see an error, submit again and create duplicates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;fix-1-keep-redirect-outside-the-try-block&quot;&gt;Fix 1: Keep redirect() Outside the try Block&lt;/h3&gt;
&lt;p&gt;Only wrap the code that can genuinely fail, and redirect afterwards:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export async function createPost(formData: FormData) {
  let postId: string;

  try {
    const post = await db.post.create({
      data: { title: String(formData.get('title')) },
    });
    postId = post.id;
  } catch (err) {
    return { error: 'Could not create post' };
  }

  redirect(`/posts/${postId}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;try&lt;/code&gt; now covers only the database call. If it fails, you return the error. If it succeeds, execution continues to &lt;code&gt;redirect()&lt;/code&gt;, which is outside any &lt;code&gt;catch&lt;/code&gt;, so it can do its job.&lt;/p&gt;
&lt;p&gt;This is the cleanest option, and it is the one I use by default.&lt;/p&gt;
&lt;h3 id=&quot;fix-2-rethrow-if-you-must-catch-everything&quot;&gt;Fix 2: Rethrow If You Must Catch Everything&lt;/h3&gt;
&lt;p&gt;Sometimes the &lt;code&gt;redirect()&lt;/code&gt; sits deep inside a helper, and restructuring is awkward. Then let the framework's own errors pass through your &lt;code&gt;catch&lt;/code&gt;. Newer Next.js versions provide a helper for exactly this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { redirect, unstable_rethrow } from 'next/navigation';

try {
  // ...work that may call redirect() or notFound() somewhere inside
} catch (err) {
  unstable_rethrow(err); // lets redirect/notFound errors continue upward
  return { error: 'Something went wrong' };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The name says it is not final, so check the documentation for the version you use. If it is not available in yours, structure your code as in Fix 1.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-do-not-use-catch-all-to-control-flow&quot;&gt;Fix 3: Do Not Use Catch-All to Control Flow&lt;/h3&gt;
&lt;p&gt;A broader habit helps here. A &lt;code&gt;catch (err)&lt;/code&gt; with no check is a very wide net. Catch what you expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;catch (err) {
  if (err instanceof DuplicateTitleError) {
    return { error: 'You already have a post with that title' };
  }
  throw err; // anything else is not yours to handle here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rethrowing what you do not recognize means the framework's own signals, real crashes and bugs all reach the place that is meant to handle them.&lt;/p&gt;
&lt;h3 id=&quot;where-else-this-bites&quot;&gt;Where Else This Bites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;notFound()&lt;/code&gt; inside &lt;code&gt;try&lt;/code&gt;.&lt;/strong&gt; A missing record becomes your generic error instead of a 404 page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Route handlers and Server Components.&lt;/strong&gt; The same rule applies: any &lt;code&gt;try/catch&lt;/code&gt; around a call that can redirect or 404 will swallow it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.catch()&lt;/code&gt; on a promise.&lt;/strong&gt; &lt;code&gt;somePromise.catch(() =&amp;gt; null)&lt;/code&gt; has the same effect if the promise's code calls &lt;code&gt;redirect()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrapper functions.&lt;/strong&gt; A helper like &lt;code&gt;withErrorHandling(fn)&lt;/code&gt; that catches everything will break redirects in every function it wraps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;quick-checklist&quot;&gt;Quick Checklist&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;What to do&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;redirect()&lt;/code&gt; after a database write &lt;/td&gt;&lt;td&gt;Put the write in &lt;code&gt;try&lt;/code&gt;, &lt;code&gt;redirect()&lt;/code&gt; after it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;notFound()&lt;/code&gt; when a record is missing &lt;/td&gt;&lt;td&gt;Call it outside the &lt;code&gt;try&lt;/code&gt;, or rethrow&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A shared error-handling wrapper &lt;/td&gt;&lt;td&gt;Make it rethrow framework errors&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unsure &lt;/td&gt;&lt;td&gt;Catch specific error types, rethrow the rest&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule-1&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;redirect()&lt;/code&gt; and &lt;code&gt;notFound()&lt;/code&gt; signal by throwing, so a &lt;code&gt;catch&lt;/code&gt; around them will eat the signal.&lt;/strong&gt; Keep them outside &lt;code&gt;try&lt;/code&gt; blocks, or rethrow what you do not recognize.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;redirect(&lt;/code&gt; and &lt;code&gt;notFound(&lt;/code&gt;. For each hit, look upward. If it sits inside a &lt;code&gt;try&lt;/code&gt;, you have either a bug now or one waiting for the day the code path is used.&lt;/p&gt;
&lt;p&gt;If you want a Next.js starting point where forms and actions are set up sensibly from day one, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Have you ever shipped a form that showed an error after it had succeeded? Tell me in the comments how long it took you to find it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33072/your-next-js-redirect-silently-does-nothing-because-its-inside-a-try-catch</guid>
<pubDate>Fri, 30 Oct 2026 10:08:42 +0000</pubDate>
</item>
<item>
<title>Building Shiori GLOBAL: How We Created an AI-Powered Circular Learning Network for Students</title>
<link>https://coderlegion.com/33022/building-shiori-global-how-we-created-an-powered-circular-learning-network-for-students</link>
<description>&lt;p&gt;Hey CoderLegion community! 👋&lt;/p&gt;
&lt;p&gt;Millions of educational books sit unused on shelves after a student completes a academic year, while millions of other students struggle to afford high-quality learning resources.&lt;/p&gt;
&lt;p&gt;To solve this, I built Shiori GLOBAL 🌿—an AI-powered circular learning platform designed to help students buy, sell, donate, swap, and discover educational books while tracking their environmental and social impact.&lt;/p&gt;
&lt;p&gt;Here is a quick breakdown of how we engineered Shiori GLOBAL to transform book sharing for students:&lt;/p&gt;
&lt;p&gt;🤖 1. The AI Book Scanner: Frictionless Listing&lt;br&gt;
Manually filling out details like title, author, edition, class, subject, and publisher deters students from listing their unused books.&lt;/p&gt;
&lt;p&gt;The Solution: We integrated an AI-powered book detection flow. A user simply uploads or camera-scans a book cover, and our AI extracts:&lt;/p&gt;
&lt;p&gt;Title &amp;amp; Author&lt;/p&gt;
&lt;p&gt;Class, Subject &amp;amp; Edition&lt;/p&gt;
&lt;p&gt;Estimated Condition &amp;amp; Publisher&lt;/p&gt;
&lt;p&gt;Developer Takeaway: Reducing user friction through computer vision / AI extraction boosts user engagement and catalog expansion drastically.&lt;/p&gt;
&lt;p&gt;🔄 2. Circular Economy Mechanics (Buy, Sell, Donate, Swap)&lt;br&gt;
Educational resource distribution shouldn't just be about commercial selling. Shiori supports multiple transaction types to maximize resource reuse:&lt;/p&gt;
&lt;p&gt;Smart Marketplace: Categorized by CBSE, ICSE, Competitive Exams, and School/College books.&lt;/p&gt;
&lt;p&gt;Hyper Local Matching: Sorting listings by approximate distance so students can swap or claim free books within their local community.&lt;/p&gt;
&lt;p&gt;Trust &amp;amp; Safety: Direct communication channels connecting verified book owners seamlessly.&lt;/p&gt;
&lt;p&gt;🌱 3. Impact Tracking &amp;amp; Sustainability&lt;br&gt;
Giving every book a second life doesn't just save money it reduces paper waste and carbon footprint. Shiori includes community impact tracking to quantify how many trees and litres of water students save by reusing textbooks.&lt;/p&gt;
&lt;p&gt;💡 Developer Tip for Young Creators&lt;br&gt;
When building projects, focus on solving real community problems. Technology is at its best when it creates a circular ecosystem turning one student's completed journey into another student's learning opportunity!&lt;/p&gt;
&lt;p&gt;What do you think about using AI computer vision to automate marketplace listings? I'd love to hear your thoughts and suggestions in the comments below! 🚀                                                                                                   &lt;/p&gt;
&lt;p&gt;Hi! I’m Aprajita Singh, a passionate student developer building AI-driven solutions for real-world impact. I love creating tools around AI, personal safety, and sustainable technology.&lt;/p&gt;
&lt;p&gt; Let’s connect on GitHub /Linkedin! &lt;br&gt;
GitHub : &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/singhaprajita183-maker&quot;&gt;https://github.com/singhaprajita183-maker&lt;/a&gt; &lt;br&gt;
Linkedin : &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/aprajita-singh-b20bb7391/&quot;&gt;https://www.linkedin.com/in/aprajita-singh-b20bb7391/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33022/building-shiori-global-how-we-created-an-powered-circular-learning-network-for-students</guid>
<pubDate>Thu, 29 Oct 2026 20:04:38 +0000</pubDate>
</item>
<item>
<title>JSON-LD Recipes: BlogPosting, FAQPage, and SoftwareApplication Without a Plugin</title>
<link>https://coderlegion.com/32967/json-ld-recipes-blogposting-faqpage-and-softwareapplication-without-a-plugin</link>
<description>&lt;p&gt;Two weeks ago I wrote the overview: a static indie site needs three machine-readable files — sitemap, schema, and llms.txt. That post told you what each one is for. This one is the part I left out on purpose: the actual code. I run NitroIDE, a browser IDE on a static host with no server, no build step, and no plugin system. There is no SEO plugin to install, so every block of structured data on the site is a &lt;code&gt;&amp;lt;script type=&quot;application/ld+json&quot;&amp;gt;&lt;/code&gt; block I wrote by hand. Three recipes cover almost everything an indie site needs. Copy them, change the values, validate before shipping — and, crucially, keep them honest, because schema that describes content the page does not show is a mismatch waiting to be flagged.&lt;/p&gt;
&lt;p&gt;Recipe 1: SoftwareApplication — the product page. This one lives on the homepage and answers a single question: what is this thing? For NitroIDE it names the product, its category, that it runs in a web browser, that it is free, and the features a human would actually recognise from the page. Rules: operatingSystem for a web tool is &quot;Web Browser&quot;; every item in featureList must be a feature the homepage actually shows; one block, one product. The mistake that voids this recipe is version drift — NitroIDE's real block carries a softwareVersion field, and it changes in the same commit as the version it describes.&lt;/p&gt;
&lt;p&gt;Recipe 2: BlogPosting — every article. Every blog article on NitroIDE carries a compact BlogPosting block in its &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;: headline, description, author, publisher, the page URL, and two dates. mainEntityOfPage is the canonical URL of the article itself. dateModified is the field that makes updates visible: when I correct or extend an article, dateModified moves and datePublished does not. The mistake that voids this recipe is copy-paste without editing — check that mainEntityOfPage equals the URL of the page you are looking at.&lt;/p&gt;
&lt;p&gt;Recipe 3: FAQPage — only where the page really answers questions. NitroIDE articles end with a real FAQ section, and the schema mirrors it exactly — same questions, same answers, in the same words. No marketing, no link, no call to action in an accepted answer. The mistake that voids this recipe is the mirror breaking: the visible FAQ and its JSON-LD block change in the same edit, every time, no exceptions.&lt;/p&gt;
&lt;p&gt;How to validate without a plugin: (1) Paste the page URL into a structured-data testing tool and confirm all three types parse. (2) Read the parsed output against the visible page, field by field. (3) After any edit to a page, re-check that page's blocks. JSON-LD is just JSON inside a script tag, so it breaks exactly like JSON breaks — a trailing comma, an unescaped quote, a smart quote. If the testing tool reports nothing at all, check the JSON first.&lt;/p&gt;
&lt;p&gt;The whole recipe card: SoftwareApplication on the product page (update the version in the same commit as the release); BlogPosting on every article (never clone a block without editing it); FAQPage only where the page visibly answers questions (change the schema in the same edit as the FAQ itself); validate every page with a structured-data test before shipping, and re-check after edits. None of this needs a plugin, a build step, or a server.&lt;/p&gt;
&lt;p&gt;I build NitroIDE — a browser-based IDE where your code never leaves your machine: &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-29&quot;&gt;https://nitroide.com?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-29&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32967/json-ld-recipes-blogposting-faqpage-and-softwareapplication-without-a-plugin</guid>
<pubDate>Thu, 29 Oct 2026 12:54:51 +0000</pubDate>
</item>
<item>
<title>Stop Syncing State With useEffect: 4 Bugs That Disappear When You Delete the Effect</title>
<link>https://coderlegion.com/32955/stop-syncing-state-with-useeffect-4-bugs-that-disappear-when-you-delete-the-effect</link>
<description>&lt;p&gt;Here is a pattern I see in almost every React codebase I read:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [fullName, setFullName] = useState('');

useEffect(() =&amp;gt; {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works. It also renders twice every time a name changes, shows a stale value for one frame, and is a bug waiting to happen when someone adds a third dependency and forgets to list it.&lt;/p&gt;
&lt;p&gt;The fix is to delete the effect. Here are four cases where that is the right move.&lt;/p&gt;
&lt;h3 id=&quot;why-these-effects-cause-bugs&quot;&gt;Why These Effects Cause Bugs&lt;/h3&gt;
&lt;p&gt;An effect runs &lt;strong&gt;after&lt;/strong&gt; React has painted the screen. So any state you set inside it means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;React renders with the old value and paints it.&lt;/li&gt;
&lt;li&gt;The effect runs and calls &lt;code&gt;setState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;React renders again with the new value.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The user can see step 1. That is the flicker. And every effect you add is another place where the dependency list can silently drift out of date.&lt;/p&gt;
&lt;h3 id=&quot;case-1-derived-values&quot;&gt;Case 1: Derived Values&lt;/h3&gt;
&lt;p&gt;If you can calculate something from props or state you already have, do not store it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before
const [fullName, setFullName] = useState('');
useEffect(() =&amp;gt; {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// After
const fullName = firstName + ' ' + lastName;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No state, no effect, no extra render, and it can never be out of sync, because it is recalculated every time.&lt;/p&gt;
&lt;p&gt;The same goes for filtering and sorting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before: two sources of truth
const [visible, setVisible] = useState(items);
useEffect(() =&amp;gt; {
  setVisible(items.filter((i) =&amp;gt; i.active));
}, [items]);

// After
const visible = items.filter((i) =&amp;gt; i.active);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the calculation is genuinely expensive, wrap it in &lt;code&gt;useMemo&lt;/code&gt;. Measure first. For a few hundred items, plain code is fine.&lt;/p&gt;
&lt;h3 id=&quot;case-2-resetting-state-when-a-prop-changes&quot;&gt;Case 2: Resetting State When a Prop Changes&lt;/h3&gt;
&lt;p&gt;This is the one people struggle with most:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState('');

  useEffect(() =&amp;gt; {
    setComment(''); // reset when the user changes
  }, [userId]);

  return &amp;lt;textarea value={comment} onChange={(e) =&amp;gt; setComment(e.target.value)} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;userId&lt;/code&gt; changes, the old comment shows for one frame, then clears. If you have several pieces of state, you need one reset line for each, and you will forget one.&lt;/p&gt;
&lt;p&gt;The fix is the &lt;code&gt;key&lt;/code&gt; prop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProfilePage({ userId }: { userId: string }) {
  return &amp;lt;Profile key={userId} userId={userId} /&amp;gt;;
}

function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState('');
  return &amp;lt;textarea value={comment} onChange={(e) =&amp;gt; setComment(e.target.value)} /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the &lt;code&gt;key&lt;/code&gt; changes, React treats it as a &lt;strong&gt;different component&lt;/strong&gt;: it throws away the old one, including all its state, and mounts a fresh one. Every piece of state resets, with no effect and no flicker. Remember this one, because it solves a whole category of &quot;why is the old data still there&quot; bugs.&lt;/p&gt;
&lt;h3 id=&quot;case-3-logic-that-belongs-in-an-event-handler&quot;&gt;Case 3: Logic That Belongs in an Event Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const [submitted, setSubmitted] = useState(false);

useEffect(() =&amp;gt; {
  if (submitted) {
    sendOrder(cart);
    setSubmitted(false);
  }
}, [submitted, cart]);

return &amp;lt;button onClick={() =&amp;gt; setSubmitted(true)}&amp;gt;Buy&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This uses an effect as a delayed event handler. It is hard to follow, and it can fire again if &lt;code&gt;cart&lt;/code&gt; changes while &lt;code&gt;submitted&lt;/code&gt; is still true.&lt;/p&gt;
&lt;p&gt;Put the logic where the event happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function handleBuy() {
  sendOrder(cart);
}

return &amp;lt;button onClick={handleBuy}&amp;gt;Buy&amp;lt;/button&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule of thumb: &lt;strong&gt;if something happens because the user did something, it belongs in an event handler. If it happens because the component appeared on screen, it can be an effect.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;case-4-notifying-a-parent&quot;&gt;Case 4: Notifying a Parent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&amp;gt; {
  onChange(value);
}, [value, onChange]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This calls the parent after the render, so the parent and child briefly disagree, and it can trigger extra renders in both. Call the parent in the same handler that changes the value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function handleChange(next: string) {
  setValue(next);
  onChange(next);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both updates are batched into one render.&lt;/p&gt;
&lt;h3 id=&quot;so-when-is-an-effect-right-&quot;&gt;So When Is an Effect Right?&lt;/h3&gt;
&lt;p&gt;Effects are for &lt;strong&gt;synchronizing with something outside React&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subscribing to a browser event, a WebSocket or a timer&lt;/li&gt;
&lt;li&gt;Starting and cleaning up a third-party widget&lt;/li&gt;
&lt;li&gt;Sending an analytics event because a page was shown&lt;/li&gt;
&lt;li&gt;Fetching data on mount (with a cleanup, so a slow response cannot overwrite a newer one)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point deserves care, because an effect that fetches can show old results if responses arrive out of order. I wrote about that race condition in a separate article about search inputs. If your library offers one, a data-fetching library or a framework's server data loading removes the need for the effect altogether.&lt;/p&gt;
&lt;h3 id=&quot;quick-checklist-before-you-write-an-effect&quot;&gt;Quick Checklist Before You Write an Effect&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Question &lt;/th&gt;&lt;th&gt;If yes&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Can I calculate it from existing props or state? &lt;/td&gt;&lt;td&gt;Calculate it during render&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I resetting state when a prop changes? &lt;/td&gt;&lt;td&gt;Use &lt;code&gt;key&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Does it happen because the user did something? &lt;/td&gt;&lt;td&gt;Put it in the event handler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I telling the parent something changed? &lt;/td&gt;&lt;td&gt;Call the parent in the handler&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Am I connecting to something outside React? &lt;/td&gt;&lt;td&gt;An effect is right&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule-2&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;An effect that only calls &lt;code&gt;setState&lt;/code&gt; based on other state or props is almost always a bug waiting to happen.&lt;/strong&gt; Compute it during render, reset with &lt;code&gt;key&lt;/code&gt;, and handle events in handlers.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;useEffect&lt;/code&gt; and read the body of each one. Count how many do nothing but set state. Each one is a candidate for deletion, and the component usually gets shorter and faster when you remove it.&lt;/p&gt;
&lt;p&gt;If you build with Next.js and React and want a clean starting point, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What is the strangest &lt;code&gt;useEffect&lt;/code&gt; you have ever found in a codebase? Share it in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32955/stop-syncing-state-with-useeffect-4-bugs-that-disappear-when-you-delete-the-effect</guid>
<pubDate>Thu, 29 Oct 2026 10:55:28 +0000</pubDate>
</item>
<item>
<title>Mastering the Native Drag and Drop API in React</title>
<link>https://coderlegion.com/32953/mastering-the-native-drag-and-drop-api-in-react</link>
<description>&lt;h3 id=&quot;what-is-the-drag-and-drop-api-&quot;&gt;What is the Drag and Drop API?&lt;/h3&gt;
&lt;p&gt;The Drag and Drop API is a built-in browser capability that allows web applications to make elements on a webpage interactive through dragging and dropping. Instead of forcing users to click buttons or select items from drop-down menus, this browser feature enables users to click on an item, hold down the mouse button or touchscreen, move the item across the screen, and release it over a designated area. The browser automatically tracks the movement coordinates, mouse state, and transfer data throughout the entire process.&lt;/p&gt;
&lt;p&gt;At its core, this API exposes a set of event listeners—hooks provided by the browser to run code when specific interactions happen—that fire as an item is picked up, dragged over drop targets, and finally released.&lt;/p&gt;
&lt;h3 id=&quot;a-real-life-analogy-the-corkboard-and-sticky-notes&quot;&gt;A Real-Life Analogy: The Corkboard and Sticky Notes&lt;/h3&gt;
&lt;p&gt;Imagine you have a physical corkboard on your office wall divided into three columns: &quot;To Do,&quot; &quot;In Progress,&quot; and &quot;Done.&quot; Each task is written on a physical sticky note.&lt;/p&gt;
&lt;p&gt;When you decide to start working on a task, you place your finger on a sticky note in the &quot;To Do&quot; column, lift it off the board, hover it across the room over to the &quot;In Progress&quot; column, and press it onto the corkboard. During this entire movement, you are carrying both the physical note and the context of what task is written on it. The board itself does not need to send a letter to an assistant asking to move the note; your hands handle the position, while the board simply provides the surface to receive it.&lt;/p&gt;
&lt;p&gt;This is precisely how the Drag and Drop API works on a screen. The draggable element acts as the sticky note, the data payload attached to the drag event acts as the text on the note, and the drop target acts as the receiving column on the corkboard.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-tech-today&quot;&gt;Why It Matters in Tech Today&lt;/h3&gt;
&lt;p&gt;In modern web engineering, user experience (UX) directly impacts engagement and product adoption. Complex interfaces like Kanban boards (such as Trello or Jira), file management dashboards (like Google Drive), and custom page builders (like Wix or Squarespace) rely heavily on drag-and-drop interactions to make software feel natural and tactile.&lt;/p&gt;
&lt;p&gt;Engineers use the native Drag and Drop API to create fluid interfaces without dragging down performance. Without this browser-native API, software developers would have to write complex, prone-to-bugs math scripts that manually track mouse pixel coordinates on every screen frame. Utilizing the browser's built-in events prevents memory leaks, reduces CPU usage, and saves hundreds of hours of manual positioning logic.&lt;/p&gt;
&lt;p&gt;Furthermore, when building file uploading components, this API allows users to drag files straight from their computer's desktop into a browser window, bridging the gap between desktop operating systems and web applications.&lt;/p&gt;
&lt;h3 id=&quot;building-a-drag-and-drop-task-list-in-react&quot;&gt;Building a Drag and Drop Task List in React&lt;/h3&gt;
&lt;p&gt;Below is a simple React component that uses the browser's native Drag and Drop API to move tasks between two lists.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { useState } from 'react';

export default function TaskBoard() {
  const [tasks, setTasks] = useState([
    { id: '1', title: 'Design Database Schema', status: 'todo' },
    { id: '2', title: 'Build Express Route', status: 'todo' },
  ]);

  // Triggered when the user starts dragging an item
  const handleDragStart = (event, id) =&amp;gt; {
    event.dataTransfer.setData('text/plain', id);
  };

  // Necessary to allow dropping into a container
  const handleDragOver = (event) =&amp;gt; {
    event.preventDefault();
  };

  // Triggered when the item is dropped
  const handleDrop = (event, targetStatus) =&amp;gt; {
    event.preventDefault();
    const taskId = event.dataTransfer.getData('text/plain');
    
    setTasks((prevTasks) =&amp;gt;
      prevTasks.map((task) =&amp;gt;
        task.id === taskId ? { ...task, status: targetStatus } : task
      )
    );
  };

  return (
    &amp;lt;div style={{ display: 'flex', gap: '20px', padding: '20px' }}&amp;gt;
      {['todo', 'done'].map((status) =&amp;gt; (
        &amp;lt;div
          key={status}
          onDragOver={handleDragOver}
          onDrop={(e) =&amp;gt; handleDrop(e, status)}
          style={{
            width: '200px',
            minHeight: '200px',
            border: '2px dashed #ccc',
            padding: '10px',
          }}
        &amp;gt;
          &amp;lt;h3&amp;gt;{status.toUpperCase()}&amp;lt;/h3&amp;gt;
          {tasks
            .filter((task) =&amp;gt; task.status === status)
            .map((task) =&amp;gt; (
              &amp;lt;div
                key={task.id}
                draggable
                onDragStart={(e) =&amp;gt; handleDragStart(e, task.id)}
                style={{
                  padding: '10px',
                  marginBottom: '8px',
                  backgroundColor: '#4A90E2',
                  color: '#fff',
                  cursor: 'grab',
                }}
              &amp;gt;
                {task.title}
              &amp;lt;/div&amp;gt;
            ))}
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;The Drag and Drop API transforms rigid web interactions into intuitive, tactile experiences by letting the browser perform the heavy lifting of mouse tracking and state transfers. Mastering this native web feature enables engineers to craft modern, highly interactive web applications with minimal overhead and zero external dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/demystifying-webs-built-in-drag-and.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32953/mastering-the-native-drag-and-drop-api-in-react</guid>
<pubDate>Thu, 29 Oct 2026 10:24:46 +0000</pubDate>
</item>
<item>
<title>Use Native structuredClone() Instead of JSON.parse(JSON.stringify())</title>
<link>https://coderlegion.com/30741/use-native-structuredclone-instead-of-json-parse-json-stringify</link>
<description>&lt;p&gt;Deep cloning objects in JS used to require third-party libraries like Lodash or hacky JSON serialization that dropped Date objects, Map, Set, and RegExp.&lt;/p&gt;
&lt;p&gt;JavaScript&lt;br&gt;
const original = {&lt;br&gt;
  user: &quot;Joseph&quot;,&lt;br&gt;
  joined: new Date(),&lt;br&gt;
  roles: new Set([&quot;admin&quot;, &quot;designer&quot;]),&lt;br&gt;
};&lt;/p&gt;
&lt;p&gt;// Loses Date instances and sets&lt;br&gt;
const badClone = JSON.parse(JSON.stringify(original));&lt;/p&gt;
&lt;p&gt;// Native browser &amp;amp; Node.js deep clone (zero dependencies)&lt;br&gt;
const cleanClone = structuredClone(original);&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it changes your career:&lt;/strong&gt; Replacing heavy third-party utility libraries with native browser APIs shrinks your bundle size and eliminates unnecessary dependency risks.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30741/use-native-structuredclone-instead-of-json-parse-json-stringify</guid>
<pubDate>Thu, 29 Oct 2026 06:57:00 +0000</pubDate>
</item>
<item>
<title>Build an AI Ops Dashboard with FSCSS modules (ai-ui, st-core, circle-progress, overlapui)</title>
<link>https://coderlegion.com/32863/build-an-ai-ops-dashboard-with-fscss-modules-ai-ui-st-core-circle-progress-overlapui</link>
<description>&lt;blockquote&gt;&lt;p&gt;A step by step tutorial for building a responsive AI dashboard with compiled FSCSS, pure CSS charts, progress rings, stacked avatars, and plain JavaScript. No framework, no build tool beyond the FSCSS CLI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you have ever built an AI product UI, you know the pattern. A sidebar, a chat column, a metrics panel, a streaming caret, some usage cards. It is a lot of CSS before you write a single line of product logic.&lt;/p&gt;
&lt;p&gt;In this tutorial we build an &lt;strong&gt;AI Ops Console&lt;/strong&gt; using four FSCSS modules, compile everything to one plain CSS file, and wire it up with a small vanilla JavaScript layer. No frameworks. No runtime preprocessor in the browser.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://codepen.io/editor/David-Hux/pen/01a181f1-e94a-7029-9f5f-894ea531b2fc&quot;&gt;CodePen preview&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-we-are-building&quot;&gt;What we are building&lt;/h3&gt;
&lt;p&gt;An AI Ops Console with a desktop layout (sidebar, chat, metrics panel) and a mobile layout (hamburger sidebar drawer, bottom sheet metrics panel at 960px and below).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Surface &lt;/th&gt;&lt;th&gt;Module&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;App shell, sidebar, messages, prompt, stream caret, thinking dots, usage cards, workflow nodes, tool chips, citations &lt;/td&gt;&lt;td&gt;&lt;strong&gt;ai-ui.fscss&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Token burn area and line chart (clip-path, no SVG or canvas) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;st-core@v2&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;GPU, Queue and Quota rings &lt;/td&gt;&lt;td&gt;&lt;strong&gt;circle-progress&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Team avatar stack &lt;/td&gt;&lt;td&gt;&lt;strong&gt;overlapui&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mock agent replies, live chart nudges, mobile drawers &lt;/td&gt;&lt;td&gt;&lt;strong&gt;dashboard.js&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The project is split into four files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ai-dashboard/
├── dashboard.html      # markup only
├── dashboard.fscss     # all FSCSS + layout
├── dashboard.css       # compiled output (generated)
└── dashboard.js        # chat, drawers, chart updates
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You compile once and ship plain CSS.&lt;/p&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 18 or newer (for the FSCSS CLI)&lt;/li&gt;
&lt;li&gt;A modern browser&lt;/li&gt;
&lt;li&gt;Optional: VS Code with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://marketplace.visualstudio.com/items?itemName=Figsh.fscss&quot;&gt;FSCSS Support&lt;/a&gt; extension&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install the compiler globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g fscss@1.2.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need &lt;strong&gt;FSCSS 1.2.6 or newer&lt;/strong&gt;. ai-ui requires it, and st-core needs 1.2.3 or newer.&lt;/p&gt;
&lt;h3 id=&quot;1-project-setup&quot;&gt;1. Project setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir ai-dashboard &amp;amp;&amp;amp; cd ai-dashboard
touch dashboard.html dashboard.fscss dashboard.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dashboard.css&lt;/code&gt; will be generated by the CLI in step 3.&lt;/p&gt;
&lt;h3 id=&quot;2-dashboard-fscss-the-styles-source&quot;&gt;2. dashboard.fscss: the styles source&lt;/h3&gt;
&lt;p&gt;This file imports the four modules, calls their helpers against your own class names, defines the chart series, and holds all layout and responsive rules.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* dashboard.fscss
   AI Ops Dashboard: FSCSS source
   Compile:  fscss dashboard.fscss dashboard.css
*/

/* Modules (registry names; CLI + runtime resolve these) */
@import((*) from ai-ui)
@import((*) from st-core@v2)
@import((*) from circle-progress)
@import((*) from overlapui)

/* Theme + design tokens */
@ai-compose-dashboard()   /* ai-ui tokens + dashboard theme */
@st-root()                /* st-core chart tokens */
@progress-root()          /* circle-progress tokens */
@ou-root()                /* overlapui tokens */

/* ai-ui structure helpers */
@ai-shell(.ai-shell)
@ai-sidebar(.ai-sidebar)
@ai-main(.ai-main)
@ai-messages(.ai-messages)
@ai-message(.ai-message)
@ai-message-user(.ai-message-user)
@ai-message-assistant(.ai-message-assistant)
@ai-bubble(.ai-bubble)
@ai-avatar(.ai-avatar)
@ai-prompt(.ai-prompt)
@ai-prompt-box(.ai-prompt-box)
@ai-prompt-send(.ai-prompt-send)
@ai-stream(.ai-stream)
@ai-thinking(.ai-thinking)
@ai-usage(.ai-usage)
@ai-stat(.ai-stat)
@ai-workflow(.ai-workflow)
@ai-node(.ai-node)
@ai-edge(.ai-edge)
@ai-tools(.ai-tools)
@ai-tool-chip(.ai-tool-chip)
@ai-card(.ai-card)
@ai-divider(.ai-divider)
@ai-citations(.ai-citations)
@ai-citation(.ai-citation)

/* st-core linear chart */
@arr tokenSeries[32, 48, 55, 42, 68, 75, 61, 88, 72, 91, 84, 96]

@st-chart-fill(.chart-fill, tokenSeries)
@st-chart-line(.chart-line, tokenSeries)

.token-chart {
  @st-chart-points(tokenSeries)
  position: relative;
  height: 160px;
  width: 100%;
  background: var(--st-surface, #161422);
  border-radius: 12px;
  overflow: hidden;
}

/* circle-progress */
@circle-progress(.cp)
.cp-72 { @progress-range(72) }
.cp-48 { @progress-range(48) }
.cp-91 { @progress-range(91) }

.cp {
  --progress-size: 110px;
  --progress-stroke: 10px;
  --progress-color-bg: transparent;
  --progress-color-track: rgba(255, 255, 255, 0.08);
  --progress-color-arc: var(--ai-accent, #e879f9);
  --progress-color-glow: color-mix(in srgb, var(--ai-accent, #e879f9) 40%, transparent);
  --progress-color-value: var(--ai-text, #f5f3ff);
  --progress-color-label: var(--ai-text-muted, #a78bfa);
  font-size: 0.95rem;
  font-weight: 600;
}

/* overlapui avatars */
@avatar-overlap(.team-faces)
.team-faces {
  --ou-avatar-size: 36px;
  --ou-overlap: -12px;
  margin-block: 8px;
}

/* Base layout */
* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  font-family: var(--ai-font, system-ui, -apple-system, sans-serif);
  background: var(--ai-bg, #0c0a12);
  color: var(--ai-text, #f5f3ff);
  -webkit-text-size-adjust: 100%;
}

.ai-shell {
  min-height: 100vh;
  min-height: 100dvh;
  height: 100vh;
  height: 100dvh;
  display: grid;
  grid-template-columns: var(--ai-sidebar-w, 240px) 1fr;
}

.ai-sidebar {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px 16px;
  overflow-y: auto;
  border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08));
  background: var(--ai-bg, #0c0a12);
  z-index: 40;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.02em;
}
.brand-mark {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--ai-accent, #e879f9), #818cf8);
  display: grid;
  place-items: center;
  font-size: 0.75rem;
  font-weight: 800;
  color: #0c0a12;
  flex-shrink: 0;
}

.nav {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 8px;
  color: var(--ai-text-muted, #a78bfa);
  text-decoration: none;
  font-size: 0.875rem;
  transition: background 0.15s, color 0.15s;
}
.nav a:hover,
.nav a.active {
  background: color-mix(in srgb, var(--ai-accent, #e879f9) 12%, transparent);
  color: var(--ai-text, #f5f3ff);
}

.side-section {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.side-section h3 {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ai-text-muted, #a78bfa);
  font-weight: 600;
}

.cp-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}
.cp-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  font-size: 0.7rem;
  color: var(--ai-text-muted, #a78bfa);
}

.ai-main {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08));
  gap: 10px;
  flex-wrap: wrap;
  flex-shrink: 0;
}
.topbar h1 {
  font-size: 1rem;
  font-weight: 600;
}
.topbar-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.chip {
  font-size: 0.75rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--ai-accent, #e879f9) 15%, transparent);
  color: var(--ai-accent, #e879f9);
  border: 1px solid color-mix(in srgb, var(--ai-accent, #e879f9) 30%, transparent);
  white-space: nowrap;
}

.workspace {
  flex: 1;
  display: grid;
  grid-template-columns: 1fr minmax(260px, 320px);
  min-height: 0;
  overflow: hidden;
}

.chat-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.06));
}

.ai-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  -webkit-overflow-scrolling: touch;
}

.ai-prompt {
  padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--ai-border, rgba(255, 255, 255, 0.06));
  flex-shrink: 0;
}

.right-panel {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
  overflow-y: auto;
  background: color-mix(in srgb, var(--ai-surface, #12101a) 80%, transparent);
  -webkit-overflow-scrolling: touch;
}

.panel-card {
  background: var(--ai-card, #1a1625);
  border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08));
  border-radius: 12px;
  padding: 14px;
}
.panel-card h3 {
  font-size: 0.8rem;
  font-weight: 600;
  margin-bottom: 10px;
  color: var(--ai-text-muted, #a78bfa);
}

.ai-workflow {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 0.75rem;
}
.ai-node {
  padding: 6px 10px;
  border-radius: 6px;
  background: var(--ai-surface, #161422);
  border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.1));
}
.ai-node.is-active {
  border-color: var(--ai-accent, #e879f9);
  color: var(--ai-accent, #e879f9);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ai-accent, #e879f9) 40%, transparent);
}
.ai-edge {
  width: 16px;
  height: 2px;
  background: var(--ai-border, rgba(255, 255, 255, 0.2));
}

.ai-usage {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}
.ai-stat {
  background: var(--ai-surface, #161422);
  border-radius: 10px;
  padding: 10px 12px;
}
.ai-stat-label {
  display: block;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ai-text-muted, #a78bfa);
  margin-bottom: 4px;
}
.ai-stat-value {
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.ai-stat-delta {
  font-size: 0.7rem;
  margin-left: 6px;
}
.ai-stat-delta.up { color: #4fffb0; }
.ai-stat-delta.down { color: #ff5e7d; }

.ai-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.ai-tool-chip {
  font-size: 0.7rem;
  padding: 4px 8px;
  border-radius: 6px;
  background: var(--ai-surface, #161422);
  border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.1));
}
.ai-tool-chip.is-done { border-color: #4fffb0; color: #4fffb0; }
.ai-tool-chip.is-running { border-color: var(--ai-accent, #e879f9); color: var(--ai-accent, #e879f9); }

.muted {
  color: var(--ai-text-muted, #a78bfa);
  font-size: 0.8rem;
}

/* Mobile controls (hidden on desktop) */
.menu-btn,
.panel-btn,
.sidebar-backdrop {
  display: none;
}

/* TABLET &amp;lt;= 960px */
@media (max-width: 960px) {
  .ai-shell {
    grid-template-columns: 1fr;
  }

  .ai-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(280px, 86vw);
    transform: translateX(-105%);
    transition: transform 0.25s ease;
    box-shadow: none;
    border-right: 1px solid var(--ai-border, rgba(255, 255, 255, 0.08));
  }
  body.sidebar-open .ai-sidebar {
    transform: translateX(0);
    box-shadow: 8px 0 32px rgba(0, 0, 0, 0.45);
  }

  .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 35;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }
  body.sidebar-open .sidebar-backdrop {
    opacity: 1;
    pointer-events: auto;
  }

  .menu-btn,
  .panel-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 8px;
    border: 1px solid var(--ai-border, rgba(255, 255, 255, 0.12));
    background: var(--ai-surface, #161422);
    color: var(--ai-text, #f5f3ff);
    cursor: pointer;
    font-size: 1.1rem;
    flex-shrink: 0;
    line-height: 1;
  }

  .topbar {
    padding: 10px 12px;
    gap: 8px;
  }
  .topbar h1 {
    font-size: 0.95rem;
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .topbar-actions { gap: 6px; }
  .chip {
    font-size: 0.7rem;
    padding: 3px 8px;
  }
  #model-chip { display: none; }

  .workspace {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    position: relative;
  }
  .chat-col {
    border-right: none;
    min-height: 0;
  }

  .right-panel {
    position: fixed;
    inset: auto 0 0 0;
    max-height: min(72vh, 560px);
    border-radius: 16px 16px 0 0;
    border-top: 1px solid var(--ai-border, rgba(255, 255, 255, 0.1));
    transform: translateY(110%);
    transition: transform 0.28s ease;
    z-index: 30;
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4);
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  body.panel-open .right-panel {
    transform: translateY(0);
  }
  .right-panel::before {
    content: &quot;&quot;;
    display: block;
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.2);
    margin: 0 auto 10px;
  }

  .token-chart { height: 140px; }
  .cp {
    --progress-size: 88px;
    --progress-stroke: 8px;
    font-size: 0.85rem;
  }
}

/* PHONE &amp;lt;= 600px */
@media (max-width: 600px) {
  .ai-messages {
    padding: 12px;
    gap: 12px;
  }
  .ai-prompt {
    padding: 10px 12px calc(12px + env(safe-area-inset-bottom, 0px));
  }
  .ai-usage {
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }
  .ai-stat { padding: 8px 10px; }
  .ai-stat-value { font-size: 1rem; }

  .cp-row {
    justify-content: center;
    gap: 12px;
  }
  .cp {
    --progress-size: 76px;
    --progress-stroke: 7px;
    font-size: 0.8rem;
  }

  .panel-card { padding: 12px; }
  .ai-workflow { gap: 4px; }
  .ai-node {
    padding: 5px 8px;
    font-size: 0.7rem;
  }
  .ai-edge { width: 10px; }

  .team-faces {
    --ou-avatar-size: 32px;
    --ou-overlap: -10px;
  }
  .ai-bubble { font-size: 0.9rem; }
}

/* VERY NARROW &amp;lt;= 380px */
@media (max-width: 380px) {
  .ai-usage { grid-template-columns: 1fr; }
  .chip#btn-refresh {
    padding: 3px 6px;
    font-size: 0.65rem;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-this-structure-works&quot;&gt;Why this structure works&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Import&lt;/strong&gt; only registers &lt;code&gt;@define&lt;/code&gt; helpers. Nothing paints until you call them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compose and root helpers&lt;/strong&gt; write design tokens on &lt;code&gt;:root&lt;/code&gt; (or a scope).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selector-parameterized helpers&lt;/strong&gt; such as &lt;code&gt;@ai-shell(.ai-shell)&lt;/code&gt; and &lt;code&gt;@st-chart-fill(.chart-fill, tokenSeries)&lt;/code&gt; attach rules to your own class names.&lt;/li&gt;
&lt;li&gt;Plain CSS after that is normal layout, overrides and media queries.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Can also use remote URLs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;@import((*) from &quot;https://cdn.jsdelivr.net/gh/fscss-ttr/ai-ui.fscss@main/ai-ui.fscss&quot;)
@import((*) from &quot;https://cdn.jsdelivr.net/gh/fscss-ttr/st-core.fscss@main/st-core%40v2.fscss&quot;)
@import((*) from &quot;https://cdn.jsdelivr.net/gh/fscss-ttr/FSCSS@main/xf/styles/circle-progress.fscss&quot;)
@import((*) from &quot;https://cdn.jsdelivr.net/gh/fscss-ttr/FSCSS@main/xf/styles/overlapui.fscss&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-compile-fscss-to-css&quot;&gt;3. Compile FSCSS to CSS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# one-shot
fscss dashboard.fscss dashboard.css

# watch (recompile on save), if your fscss build supports it
# fscss dashboard.fscss dashboard.css --watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The compiler:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expands every &lt;code&gt;@import&lt;/code&gt;, &lt;code&gt;@define&lt;/code&gt; and &lt;code&gt;@arr&lt;/code&gt; into standard CSS&lt;/li&gt;
&lt;li&gt;Emits custom properties (&lt;code&gt;--st-p1&lt;/code&gt; to &lt;code&gt;--st-p12&lt;/code&gt;, &lt;code&gt;--ai-*&lt;/code&gt;, &lt;code&gt;--progress-*&lt;/code&gt; and so on)&lt;/li&gt;
&lt;li&gt;Emits &lt;code&gt;clip-path: polygon(...)&lt;/code&gt; for the chart fill and line&lt;/li&gt;
&lt;li&gt;Leaves your media queries and plain CSS intact&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ship only &lt;code&gt;dashboard.css&lt;/code&gt; to production. No &lt;code&gt;runtime.min.js&lt;/code&gt; is needed on this path.&lt;/p&gt;
&lt;h3 id=&quot;4-dashboard-html-the-markup&quot;&gt;4. dashboard.html: the markup&lt;/h3&gt;
&lt;p&gt;Link the compiled stylesheet. There is no FSCSS runtime script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;See complete tutorial here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/fscss/build-an-ai-ops-dashboard-with-fscss-ai-ui-st-core-circle-progress-overlapui-59m5&quot;&gt;https://dev.to/fscss/build-an-ai-ops-dashboard-with-fscss-ai-ui-st-core-circle-progress-overlapui-59m5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/32863/build-an-ai-ops-dashboard-with-fscss-modules-ai-ui-st-core-circle-progress-overlapui</guid>
<pubDate>Wed, 28 Oct 2026 15:40:56 +0000</pubDate>
</item>
<item>
<title>Demystifying the Facade Pattern in Node.js: Clean APIs Made Simple</title>
<link>https://coderlegion.com/32803/demystifying-the-facade-pattern-in-node-js-clean-apis-made-simple</link>
<description>&lt;h3 id=&quot;what-is-the-facade-pattern-&quot;&gt;What Is the Facade Pattern?&lt;/h3&gt;
&lt;p&gt;The facade pattern is a structural software design pattern that provides an uncomplicated, unified interface in front of a complex collection of classes, libraries, or APIs. Instead of forcing your application code to interact with dozens of intricate internal functions, the facade exposes a single, high-level method that coordinates the messy work behind the scenes. In short, it gives a sprawling, complicated subsystem a clean and welcoming front door.&lt;/p&gt;
&lt;h3 id=&quot;the-real-world-analogy-the-home-theater-remote&quot;&gt;The Real-World Analogy: The Home Theater Remote&lt;/h3&gt;
&lt;p&gt;Imagine you want to watch a movie on a high-end home entertainment setup. To make that happen naturally, you need to execute half a dozen distinct tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turn on the television.&lt;/li&gt;
&lt;li&gt;Set the video input to HDMI 2.&lt;/li&gt;
&lt;li&gt;Turn on the surround sound receiver.&lt;/li&gt;
&lt;li&gt;Adjust the audio amplifier to optical audio.&lt;/li&gt;
&lt;li&gt;Dim the smart living room lights.&lt;/li&gt;
&lt;li&gt;Power on the media streaming console.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You could juggle four different remote controls and flip wall switches yourself, learning the exact quirks of each manufacturer. Alternatively, you could pick up a modern universal remote and press a single button: &lt;strong&gt;Watch Movie&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That universal remote is a facade. It does not replace the television, receiver, or lights; they still exist with all their specialized knobs and settings. Instead, the remote hides the tedious orchestration behind one single, convenient touchpoint.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-engineering&quot;&gt;Why It Matters in Daily Engineering&lt;/h3&gt;
&lt;p&gt;In everyday software development, projects tend to grow through accretion. A feature starts out simple, but soon requires five external dependencies: an authentication provider, a payment gateway, a cloud storage bucket, an email delivery service, and an internal logging pipeline.&lt;/p&gt;
&lt;p&gt;Without a facade, engineers frequently write &quot;glue code&quot; directly inside their HTTP request handlers (controllers). If five different routes need to charge a customer, each route handler winds up importing and configuring the payment library, checking inventory, calculating sales tax, and firing off analytics calls. This creates two immediate crises:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;High Coupling:&lt;/strong&gt; If the payment provider updates its software development kit (SDK) or changes its function signatures, you must find and update twenty different files across your project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cognitive Overload:&lt;/strong&gt; Engineers reviewing or writing features are forced to read through hundreds of lines of low-level plumbing when they only care about whether an order succeeded.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By placing a facade between your core business logic and external subsystems, engineers prevent third-party updates from leaking across the entire codebase. When an underlying vendor changes, you update one facade file, leaving the rest of the application completely untouched.&lt;/p&gt;
&lt;h3 id=&quot;practical-implementation-in-node-js&quot;&gt;Practical Implementation in Node.js&lt;/h3&gt;
&lt;p&gt;Consider an e-commerce checkout flow in an Express and Node.js application. Completing an order touches an inventory service, a payment processor, and a notification service.&lt;/p&gt;
&lt;p&gt;Without a facade, your Express route handler would have to instantiate and coordinate every service manually. Here is how you encapsulate that complexity using a Facade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Subsystem 1: Inventory Management
class InventoryService {
  checkStock(productId) {
    // Simulate querying database stock levels
    console.log(`Checking stock for item: ${productId}`);
    return true;
  }
  reserve(productId, quantity) {
    console.log(`Reserved ${quantity} unit(s) of ${productId}`);
  }
}

// Subsystem 2: Payment Processing
class PaymentProcessor {
  charge(amount, paymentToken) {
    console.log(`Charging $${amount} using token: ${paymentToken}`);
    return { status: 'success', transactionId: 'txn_987654' };
  }
}

// Subsystem 3: Customer Notifications
class NotificationService {
  sendReceipt(email, transactionId) {
    console.log(`Receipt sent to ${email} for transaction ${transactionId}`);
  }
}

// The Facade: OrderFacade
class OrderFacade {
  constructor() {
    this.inventory = new InventoryService();
    this.payment = new PaymentProcessor();
    this.notifications = new NotificationService();
  }

  processOrder({ productId, quantity, price, paymentToken, customerEmail }) {
    const isAvailable = this.inventory.checkStock(productId);
    if (!isAvailable) {
      throw new Error('Product is out of stock.');
    }

    this.inventory.reserve(productId, quantity);
    const paymentResult = this.payment.charge(price * quantity, paymentToken);
    this.notifications.sendReceipt(customerEmail, paymentResult.transactionId);

    return { success: true, transactionId: paymentResult.transactionId };
  }
}

// Express Route Usage
const express = require('express');
const app = express();
app.use(express.json());

const orderFacade = new OrderFacade();

app.post('/api/checkout', (req, res) =&amp;gt; {
  try {
    const orderConfirmation = orderFacade.processOrder(req.body);
    return res.status(200).json(orderConfirmation);
  } catch (error) {
    return res.status(400).json({ error: error.message });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The route handler does not know or care how inventory reservation works or what parameters the payment gateway demands. It invokes &lt;code&gt;processOrder()&lt;/code&gt; and receives a clear outcome.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-1&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;A facade is not an abstraction that conceals how a system works to make it private; it is an ergonomic shield that keeps your application's day-to-day feature code clean, readable, and insulated from volatile third-party tools.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/architectural-simplicity-why-facade.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32803/demystifying-the-facade-pattern-in-node-js-clean-apis-made-simple</guid>
<pubDate>Wed, 28 Oct 2026 10:10:50 +0000</pubDate>
</item>
<item>
<title>Launching a fancy text converter: 451 styles, and the Unicode edge cases behind them</title>
<link>https://coderlegion.com/32655/launching-a-fancy-text-converter-451-styles-and-the-unicode-edge-cases-behind-them</link>
<description>&lt;p&gt;I'm Shawn Sun, a front-end engineer, and I just put a fancy text converter online. This is a developer community, so here are the parts that were fun to build instead of a feature list.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9770236220749453047&quot; alt=&quot;The converter's home page: one text box, category chips, and a grid of styled previews&quot;&gt;&lt;/p&gt;
&lt;p&gt;You type once and get 451 copy-and-paste styles. Tap one to copy it, and the last 8 you copied come back under Recent. No sign-up. None of these are fonts: every style is real Unicode characters, and that caused most of the work below.&lt;/p&gt;
&lt;h3 id=&quot;1-a-style-is-mostly-a-lookup-table&quot;&gt;1. A style is mostly a lookup table&lt;/h3&gt;
&lt;p&gt;Bold, italic, script, Fraktur and the rest live in the Mathematical Alphanumeric Symbols block, so most styles are &quot;offset of A, plus the letter's index&quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2226421321765363481&quot; alt=&quot;Lookup table: A U+0041 maps to bold A U+1D400, a U+0061 to U+1D41A, with the offset formula&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-the-block-has-holes&quot;&gt;2. The block has holes&lt;/h3&gt;
&lt;p&gt;Some letters were encoded earlier elsewhere, so their slot is empty. A naive &lt;code&gt;base + index&lt;/code&gt; gives you a box, so each style carries a small exceptions table.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14870203655612914539&quot; alt=&quot;Italic &amp;quot;the&amp;quot;: the naive code point U+1D455 is unassigned and shows a box; the fix uses U+210E PLANCK CONSTANT&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-accents-decompose-first&quot;&gt;3. Accents: decompose first&lt;/h3&gt;
&lt;p&gt;There is no bold é. I split it with NFD, style the e and keep the accent after it. Whether the accent then sits on top depends on the font.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8443502707233179983&quot; alt=&quot;é decomposes into e plus U+0301; only the e is styled; two fonts render the result differently&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-don-t-split-emoji&quot;&gt;4. Don't split emoji&lt;/h3&gt;
&lt;p&gt;Walk a string by code point and 👩💻 falls apart. I cut input into grapheme clusters with &lt;code&gt;Intl.Segmenter&lt;/code&gt; and only style plain letters and digits.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9433061209713490169&quot; alt=&quot;By code point, the woman technologist emoji is three pieces; Intl.Segmenter keeps it as one cluster&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;5-long-input-preview-first-convert-on-copy&quot;&gt;5. Long input: preview first, convert on copy&lt;/h3&gt;
&lt;p&gt;Rendering every style live is fine for a username and slow for a pasted paragraph. Past 200 UTF-16 code units, each card previews only the start, and the full text is converted when you copy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=354969249241440925&quot; alt=&quot;Only the first 200 code units are converted for previews; the tapped style converts the whole text on copy&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;honest-limits&quot;&gt;Honest limits&lt;/h3&gt;
&lt;p&gt;Conversion runs in your browser. I count anonymous usage events, such as which style was copied, but your text isn't part of them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8032254886286468851&quot; alt=&quot;Limits table: missing glyphs, screen readers and search, usernames, digits that stay plain&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;i-d-like-your-feedback&quot;&gt;I'd like your feedback&lt;/h3&gt;
&lt;p&gt;Which category is missing, and do the style names make sense? Try your own text on &lt;a rel=&quot;nofollow&quot; href=&quot;https://fancy-text-generator.app/&quot;&gt;the live site&lt;/a&gt; and tell me in the comments.&lt;/p&gt;
&lt;p&gt;— Shawn Sun&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32655/launching-a-fancy-text-converter-451-styles-and-the-unicode-edge-cases-behind-them</guid>
<pubDate>Tue, 27 Oct 2026 09:22:36 +0000</pubDate>
</item>
<item>
<title>Building a Booking System with Node.js, Google Calendar, and Google Meet</title>
<link>https://coderlegion.com/32649/building-a-booking-system-with-node-js-google-calendar-and-google-meet</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Booking a consultation should be straightforward for the person making the appointment. Behind the scenes, however, an automated booking system has to coordinate several moving parts: collecting customer information, checking availability, scheduling an event, generating a meeting link, saving the booking, and notifying the people involved.&lt;/p&gt;
&lt;p&gt;I encountered these challenges while developing the booking system for JOPAD Advisory. I built the system from scratch, using Node.js and Express for the backend, MongoDB for storing bookings, Google Calendar for scheduling, Google Meet for virtual meetings, and Resend for email notifications.&lt;/p&gt;
&lt;p&gt;In this article, I'll walk through how I implemented the booking workflow, how the different services work together, and some of the engineering considerations that come with connecting external APIs to a backend application.&lt;/p&gt;
&lt;h3 id=&quot;1-the-architecture&quot;&gt;1. The Architecture&lt;/h3&gt;
&lt;p&gt;The backend coordinates the booking process. Each service has a specific responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js and Express: Handle booking requests and coordinate the workflow.&lt;/li&gt;
&lt;li&gt;MongoDB and Mongoose: Store booking details and provide access to existing appointments.&lt;/li&gt;
&lt;li&gt;Google Calendar API: Create calendar events for scheduled sessions.&lt;/li&gt;
&lt;li&gt;Google Meet: Generate a virtual meeting link through the Calendar event's conference configuration.&lt;/li&gt;
&lt;li&gt;Airtable: Receive a secondary record of the booking.&lt;/li&gt;
&lt;li&gt;Resend: Power the email notification workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The booking process follows this sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A customer submits a booking request through the website.&lt;/li&gt;
&lt;li&gt;The Express endpoint validates the submitted fields.&lt;/li&gt;
&lt;li&gt;The backend calculates the appointment's start and end times.&lt;/li&gt;
&lt;li&gt;Existing bookings are checked for overlapping time intervals.&lt;/li&gt;
&lt;li&gt;Google Calendar creates an event and requests a Google Meet conference.&lt;/li&gt;
&lt;li&gt;The booking is saved to MongoDB with its meeting link.&lt;/li&gt;
&lt;li&gt;The backend attempts to send a secondary record to Airtable.&lt;/li&gt;
&lt;li&gt;Booking notification emails are triggered.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This design brings several services together behind a single booking endpoint, giving the frontend one place to submit the request.&lt;/p&gt;
&lt;h3 id=&quot;2-building-the-booking-endpoint&quot;&gt;2. Building the Booking Endpoint&lt;/h3&gt;
&lt;p&gt;I used an Express POST endpoint to receive booking requests. The request contains the appointment time, duration, customer's contact details, session description, and other booking information.&lt;/p&gt;
&lt;p&gt;The first step is to reject incomplete requests before performing external API operations.&lt;/p&gt;
&lt;p&gt;A simplified version of the validation looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;router.post(&quot;/book&quot;, async (req, res) =&amp;gt; {
  const {
    time,
    duration,
    email,
    firstName,
    lastName,
    description
  } = req.body;

  if (
    !time ||
    !duration ||
    !email ||
    !firstName ||
    !lastName ||
    !description
  ) {
    return res.status(400).json({
      message: &quot;Missing required fields&quot;
    });
  }

  // Continue with availability checks
  // and booking creation.
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a simplified illustration of the validation logic in my implementation, not a complete replacement for the original endpoint.&lt;/p&gt;
&lt;p&gt;The actual endpoint validates additional fields, including the selected date, day, time range, phone number, and referral information.&lt;/p&gt;
&lt;p&gt;The important principle is to validate the data required by the booking workflow before creating external resources or writing records.&lt;/p&gt;
&lt;p&gt;For a production system, request validation should also verify data types, valid email addresses, positive durations, valid dates, and acceptable appointment times. A field being present does not necessarily mean its value is valid.&lt;/p&gt;
&lt;h3 id=&quot;3-calculating-appointment-duration&quot;&gt;3. Calculating Appointment Duration&lt;/h3&gt;
&lt;p&gt;A booking needs both a start time and an end time. The end time determines the appointment's actual interval and makes it possible to detect overlapping bookings.&lt;/p&gt;
&lt;p&gt;In my implementation, the backend calculates the end time using the requested start time and duration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const startTime = new Date(time);
const endTime = new Date(startTime);

endTime.setMinutes(
  endTime.getMinutes() + Number(duration)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, an appointment starting at 10:00 AM with a duration of 60 minutes should end at 11:00 AM.&lt;/p&gt;
&lt;p&gt;Calculating the interval on the backend is important because scheduling decisions should not depend solely on values calculated by the browser.&lt;/p&gt;
&lt;p&gt;It is also important to handle time zones consistently. My Google Calendar integration sends ISO-formatted timestamps to the API. A more comprehensive implementation should explicitly define the application's scheduling time zone and validate incoming timestamps to prevent ambiguity around local times and daylight-saving transitions.&lt;/p&gt;
&lt;h3 id=&quot;4-preventing-overlapping-bookings&quot;&gt;4. Preventing Overlapping Bookings&lt;/h3&gt;
&lt;p&gt;One of the central problems in a booking system is preventing customers from selecting times that have already been reserved.&lt;/p&gt;
&lt;p&gt;Two appointments overlap when the first appointment starts before the second one ends and finishes after the second one starts.&lt;/p&gt;
&lt;p&gt;The logic can be expressed as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const overlaps =
  requestedStart &amp;lt; existingEnd &amp;amp;&amp;amp;
  requestedEnd &amp;gt; existingStart;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This condition also allows back-to-back appointments. For example, a booking ending at 11:00 AM does not conflict with another booking beginning at exactly 11:00 AM.&lt;/p&gt;
&lt;p&gt;In my implementation, I retrieve existing bookings from MongoDB, calculate each booking's end time from its start time and duration, and check for conflicts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const existingBookings = await Booking.find();

const conflict = existingBookings.some(booking =&amp;gt; {
  const existingStart = new Date(booking.startTime);
  const existingEnd = new Date(existingStart);

  existingEnd.setMinutes(
    existingEnd.getMinutes() +
      Number(booking.duration)
  );

  return (
    startTime &amp;lt; existingEnd &amp;amp;&amp;amp;
    endTime &amp;gt; existingStart
  );
});

if (conflict) {
  return res.status(409).json({
    message: &quot;This time slot has already been booked&quot;
  });
}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returning HTTP &lt;code&gt;&quot;409 Conflict&quot;&lt;/code&gt; communicates that the request conflicts with an existing booking.&lt;/p&gt;
&lt;p&gt;This approach is easy to understand, but it has a scalability limitation: the implementation loads all existing bookings before checking for an overlap. As the collection grows, a database query restricted to potentially overlapping intervals would be more efficient.&lt;/p&gt;
&lt;p&gt;There is also an important concurrency consideration. Two requests can check availability at almost the same time, both find the slot free, and both continue. The check alone does not guarantee that simultaneous requests cannot create overlapping bookings.&lt;/p&gt;
&lt;p&gt;A stronger production design would use an atomic reservation strategy, such as reserving discrete time slots with a unique database constraint or using another concurrency-control mechanism. That protection needs to be designed around the application's scheduling rules rather than relying only on an initial availability check.&lt;/p&gt;
&lt;h3 id=&quot;5-connecting-google-calendar-and-google-meet&quot;&gt;5. Connecting Google Calendar and Google Meet&lt;/h3&gt;
&lt;p&gt;After the backend confirms that a requested slot does not conflict with an existing booking, it calls a dedicated utility function to create the meeting.&lt;/p&gt;
&lt;p&gt;I separated this logic into a Google Calendar utility instead of placing the entire integration directly inside the Express route. This keeps the route focused on the booking workflow and makes the external integration easier to maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authenticating with Google&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The utility initializes an OAuth 2.0 client using environment variables and sets a refresh token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { google } = require(&quot;googleapis&quot;);

const oauth2Client = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  process.env.GOOGLE_REDIRECT_URI
);

oauth2Client.setCredentials({
  refresh_token: process.env.GOOGLE_REFRESH_TOKEN
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example uses the conventional environment-variable spelling for the client secret. In the project, the environment variable is named &lt;code&gt;&quot;GOOGLE_CLIENT_SECRETE&quot;&lt;/code&gt;, so the deployed configuration must match the actual code unless that naming is corrected.&lt;/p&gt;
&lt;p&gt;The refresh token allows the server-side client to obtain access tokens as needed without requiring the user to repeat the authorization flow for every booking.&lt;/p&gt;
&lt;p&gt;This requires the initial OAuth authorization to have been completed successfully and the refresh token to remain valid. The Google account must also have the necessary permissions and configuration for creating Calendar events and conference links.&lt;/p&gt;
&lt;p&gt;Credentials should be stored securely in environment variables or an appropriate secrets manager, never committed to a public repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating a Calendar event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The integration uses Google's Calendar API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const calendar = google.calendar({
  version: &quot;v3&quot;,
  auth: oauth2Client
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The meeting function calculates the event's end time and constructs a Calendar event containing the session title, description, start and end timestamps, and attendee information.&lt;/p&gt;
&lt;p&gt;The attendee is represented by the customer's email address:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;attendees: [
  {
    email,
    responseStatus: &quot;accepted&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event also includes settings that prevent guests from modifying the event, inviting other guests, or seeing other guests.&lt;/p&gt;
&lt;p&gt;These settings are part of the event configuration; they do not replace proper authorization and access control in the booking application itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requesting a Google Meet link&lt;br&gt;
&lt;/strong&gt;&lt;br&gt;
The event configuration includes a conference creation request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;conferenceData: {
  createRequest: {
    requestId: &quot;jopad-&quot; + Date.now(),
    conferenceSolutionKey: {
      type: &quot;hangoutsMeet&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event is then inserted into the primary Google Calendar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const response = await calendar.events.insert({
  calendarId: &quot;primary&quot;,
  resource: event,
  conferenceDataVersion: 1,
  sendUpdates: &quot;all&quot;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &quot;conferenceDataVersion: 1&quot; option tells Google Calendar that conference data should be processed as part of the request. The &quot;sendUpdates: &quot;all&quot;&quot; option requests that Calendar send event updates to the relevant participants.&lt;/p&gt;
&lt;p&gt;After the API call succeeds, the implementation extracts the meeting URL from the returned conference data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const meetLink =
  response.data.conferenceData.entryPoints[0].uri;

return meetLink;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The booking route receives that link and includes it in the booking record.&lt;/p&gt;
&lt;p&gt;One important implementation detail is that the code assumes the response contains a conference entry point. A more defensive implementation would verify that the response contains a usable Meet URL before continuing.&lt;/p&gt;
&lt;p&gt;Google Calendar conference creation can also involve asynchronous processing. Production code should account for cases where the event exists but the conference details are not yet available, rather than assuming every successful event insertion immediately includes a meeting link.&lt;/p&gt;
&lt;h3 id=&quot;6-saving-bookings-in-mongodb&quot;&gt;6. Saving Bookings in MongoDB&lt;/h3&gt;
&lt;p&gt;Once the meeting link has been returned, the backend creates a Mongoose booking document.&lt;/p&gt;
&lt;p&gt;The model stores information such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Appointment start time and duration.&lt;/li&gt;
&lt;li&gt;Customer name and contact information.&lt;/li&gt;
&lt;li&gt;Session description.&lt;/li&gt;
&lt;li&gt;Date and time display values.&lt;/li&gt;
&lt;li&gt;Referral information.&lt;/li&gt;
&lt;li&gt;Google Meet link.&lt;/li&gt;
&lt;li&gt;Record creation timestamp.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The central fields include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const bookingSchema = new mongoose.Schema({
  startTime: {
    type: Date,
    required: true
  },
  duration: String,
  email: String,
  firstName: String,
  lastName: String,
  description: String,
  meetLink: String,
  createdAt: {
    type: Date,
    default: Date.now
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an excerpt of the schema; the complete project model contains additional fields.&lt;/p&gt;
&lt;p&gt;After constructing the document, the route saves it:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await newBooking.save();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Saving the booking after generating the meeting link ensures that the returned booking record can contain the link associated with the created event.&lt;/p&gt;
&lt;p&gt;However, the order of these operations creates an important failure scenario. If Google Calendar successfully creates the event but MongoDB subsequently fails to save the booking, a calendar event may remain without a corresponding booking record.&lt;/p&gt;
&lt;p&gt;This is a common challenge when coordinating operations across independent services. A more resilient design could track workflow states, retry recoverable operations, and provide a reconciliation or cleanup process for partially completed bookings.&lt;/p&gt;
&lt;h3 id=&quot;7-adding-an-airtable-backup&quot;&gt;7. Adding an Airtable Backup&lt;/h3&gt;
&lt;p&gt;The system also attempts to send booking information to Airtable after saving the MongoDB document.&lt;/p&gt;
&lt;p&gt;The request includes selected booking fields, such as the customer's name, email, phone number, session description, referral details, meeting link, and appointment date.&lt;/p&gt;
&lt;p&gt;The operation is wrapped in its own &quot;try...catch&quot; block. If the Airtable request fails, the error is logged, but the main booking workflow can still continue.&lt;/p&gt;
&lt;p&gt;This makes Airtable a secondary destination rather than the primary source of truth for the booking.&lt;/p&gt;
&lt;p&gt;That distinction matters: the implementation attempts a backup write, but it does not guarantee that the MongoDB and Airtable records are always synchronized. If Airtable is unavailable, the backup attempt fails without automatically retrying it.&lt;/p&gt;
&lt;p&gt;A more robust implementation could place failed backup operations into a retry queue or maintain a record of synchronization status.&lt;/p&gt;
&lt;h3 id=&quot;8-sending-booking-notifications&quot;&gt;8. Sending Booking Notifications&lt;/h3&gt;
&lt;p&gt;After the booking has been saved and the Airtable attempt has completed, the route triggers the email notification utility:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;sendBookingEmails({
  email,
  firstName,
  lastName,
  dayString,
  referral,
  dateString,
  number,
  description,
  timeString,
  timeRange,
  meetLink
}).catch(err =&amp;gt; {
  console.log(&quot;Email failed:&quot;, err.message);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The email operation is not awaited by the route. This allows the endpoint to return its successful booking response without waiting for the notification workflow to finish.&lt;/p&gt;
&lt;p&gt;It also means an email failure does not change the response after the booking has been saved.&lt;/p&gt;
&lt;p&gt;There is a trade-off: the current implementation logs email failures but does not automatically retry them or record a delivery status in the booking document. For a production booking service, a persistent background job or message queue would provide a stronger way to retry notifications and monitor delivery outcomes.&lt;/p&gt;
&lt;p&gt;The route eventually responds with HTTP &lt;code&gt;&quot;201 Created&quot;&lt;/code&gt;, along with a success message and the saved booking.&lt;/p&gt;
&lt;h3 id=&quot;9-lessons-from-the-implementation&quot;&gt;9. Lessons from the Implementation&lt;/h3&gt;
&lt;p&gt;Building this workflow brought several backend engineering considerations into focus.&lt;/p&gt;
&lt;p&gt;Validate before creating external resources. Checking the request and current bookings before calling Google reduces unnecessary API operations.&lt;/p&gt;
&lt;p&gt;Separate responsibilities. Keeping the Calendar integration in its own utility makes the Express route easier to understand and maintain.&lt;/p&gt;
&lt;p&gt;Handle partial failures. MongoDB, Google Calendar, Airtable, and an email provider are independent services. A successful operation in one does not guarantee success in the others.&lt;/p&gt;
&lt;p&gt;Treat concurrency as a separate problem. Checking for overlapping bookings is necessary, but it does not by itself protect against two simultaneous requests reserving the same slot.&lt;/p&gt;
&lt;p&gt;Design for observability and recovery. Logging an error helps with debugging, but production systems also benefit from structured logs, retry mechanisms, delivery tracking, and reconciliation workflows.&lt;/p&gt;
&lt;p&gt;These considerations extend beyond booking applications. Any backend that coordinates a database with multiple external APIs needs a plan for partial failures and retries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building the JOPAD Advisory booking system from scratch gave me practical experience connecting a Node.js backend to Google Calendar, generating Google Meet links, storing bookings in MongoDB, maintaining a secondary Airtable record, and triggering email notifications.&lt;/p&gt;
&lt;p&gt;The most important lesson is that integrating APIs is only one part of building a reliable application. The surrounding workflow—validation, conflict detection, persistence, failure handling, and recovery—is just as important.&lt;/p&gt;
&lt;p&gt;A booking endpoint can appear simple from the frontend, but it coordinates several systems that can succeed or fail independently. Understanding those interactions is what turns an API integration into a more complete engineering solution.&lt;/p&gt;
&lt;p&gt;I'm continuing to explore ways to make these workflows more resilient, especially around concurrent bookings, retries, and recovery from partial failures.&lt;/p&gt;
&lt;p&gt;If you're building a booking platform or integrating third-party services into a Node.js application, I'd be interested to hear how you approach these challenges.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32649/building-a-booking-system-with-node-js-google-calendar-and-google-meet</guid>
<pubDate>Tue, 27 Oct 2026 07:58:12 +0000</pubDate>
</item>
<item>
<title>Designing browser music controls around the next beat</title>
<link>https://coderlegion.com/32606/designing-browser-music-controls-around-the-next-beat</link>
<description>&lt;p&gt;&lt;em&gt;By Paul Gendek. Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/paulgendek/designing-browser-music-controls-around-the-next-beat-29f6&quot;&gt;DEV&lt;/a&gt;. The original DEV article is labelled “Fully Autonomous”; its AI disclosure is retained below.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When I am playing guitar alone, changing a backing groove usually means moving a hand from the instrument to the laptop. A button can be easy to click and still interrupt the activity it is supposed to support. That problem led me to Mock Band, a browser rhythm section, and to a useful distinction: accepting a command, applying a musical change and making that change understandable to the player are separate jobs.&lt;/p&gt;
&lt;p&gt;I supplied the concept, product direction and requirements, tested physical hardware and retained merge approval. AI tools implemented changes and ran automated checks. This article is an AI-drafted adaptation of my original project account and the current case study. The implementation details below describe the system those tools helped implement.&lt;/p&gt;
&lt;h3 id=&quot;start-with-what-the-player-is-doing&quot;&gt;Start with what the player is doing&lt;/h3&gt;
&lt;p&gt;The initial idea, recorded in a note in September 2023, was to choose drums, bass and guitar accompaniment, repeat a groove and then reshape it through instructions. A player could change key or move to half-time while keeping both hands on the guitar. A footswitch would activate voice listening.&lt;/p&gt;
&lt;p&gt;The player remained responsible for the riff and musical direction. The product did not need to listen to the guitar and infer its performance to serve that goal. It needed to accept an intentional change with less interruption.&lt;/p&gt;
&lt;p&gt;That distinction reduced the interaction problem to something concrete. How does a person indicate a change? Which parts of that change can happen now? Which should wait for a musical boundary? What should remain usable when voice or a controller is unavailable?&lt;/p&gt;
&lt;p&gt;I chose the browser after researching WebMIDI and browser music APIs. Voice recognition, MIDI support and permissions still vary across browser and device combinations. Visible controls and text therefore remain useful ways to operate the band.&lt;/p&gt;
&lt;h3 id=&quot;connect-input-methods-to-shared-musical-actions&quot;&gt;Connect input methods to shared musical actions&lt;/h3&gt;
&lt;p&gt;The interface and application state use &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/learn&quot;&gt;React&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.typescriptlang.org/docs/&quot;&gt;TypeScript&lt;/a&gt;. Text and recognized speech share a command vocabulary. A typed tempo instruction and its recognized spoken equivalent can reach the same musical action without maintaining two separate definitions of what the instruction means.&lt;/p&gt;
&lt;p&gt;MIDI controller input has a different shape. A mapped controller action can invoke a scene or transport handler directly. It does not need to become a sentence and pass through a text parser first.&lt;/p&gt;
&lt;p&gt;The useful point of convergence is shared application state and provider handlers. Those handlers connect transport, musical changes and scene events to the rest of the system. Sharing that destination gives the input methods a common effect while preserving the differences in how the request arrived.&lt;/p&gt;
&lt;p&gt;This matters when extending a product. Adding an input method should not require inventing another version of scene recall or tempo changes. It also should not require squeezing every physical gesture into a text-command metaphor.&lt;/p&gt;
&lt;h3 id=&quot;give-audio-timing-its-own-responsibility&quot;&gt;Give audio timing its own responsibility&lt;/h3&gt;
&lt;p&gt;The current audio bridge passes changes to an AudioWorklet. The worklet renders drums, bass and rhythm accompaniment and handles their timing away from interface rendering. Its live schedule can also feed WebMIDI output through the bridge.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet&quot;&gt;MDN's AudioWorklet reference&lt;/a&gt; explains the separate Web Audio execution context. That separation helps explain this architecture, but the use of a worklet alone does not establish that a particular application has no timing problems.&lt;/p&gt;
&lt;p&gt;The interface has to respond to a request. The audio engine has to decide when an allowed change takes effect. In this system, a playing change can queue for a beat or bar boundary; a stopped change can apply immediately.&lt;/p&gt;
&lt;p&gt;Consider a tempo adjustment requested halfway through a bar. The player has made a valid request, but applying it at an arbitrary instant may disrupt the phrase. Queuing it gives the music a meaningful point at which to change. This is a design choice about musical behavior, with implementation consequences for state.&lt;/p&gt;
&lt;h3 id=&quot;distinguish-selected-pending-and-applied-state&quot;&gt;Distinguish selected pending and applied state&lt;/h3&gt;
&lt;p&gt;A delayed musical change creates more than one truthful description of the current situation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;State &lt;/th&gt;&lt;th&gt;What it means &lt;/th&gt;&lt;th&gt;What the player needs to understand&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Selected &lt;/td&gt;&lt;td&gt;The player has requested a new value. &lt;/td&gt;&lt;td&gt;The intended next setting.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pending &lt;/td&gt;&lt;td&gt;The change is waiting for a musical boundary. &lt;/td&gt;&lt;td&gt;Why the current sound may still reflect the previous value.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Applied &lt;/td&gt;&lt;td&gt;The audio engine has adopted the change. &lt;/td&gt;&lt;td&gt;Which setting is active in the music.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These are design distinctions, not three competing copies of the truth. A selected value can be correct as an intention while the old value remains correct as the applied sound.&lt;/p&gt;
&lt;p&gt;For a hypothetical example, imagine changing from one tempo to another during playback with a bar-boundary change pending. If the interface presents the new value as already active, a player may think the engine ignored it. If it hides the request until the boundary, the player may repeat it. The product needs to communicate the relationship between intention and execution.&lt;/p&gt;
&lt;p&gt;The lesson extends beyond music. Whenever an operation is accepted now but takes effect later, the interface needs to describe both events honestly. In music, the delay has an audible consequence, so the distinction becomes difficult to ignore.&lt;/p&gt;
&lt;h3 id=&quot;let-the-physical-switch-correct-the-requirement&quot;&gt;Let the physical switch correct the requirement&lt;/h3&gt;
&lt;p&gt;During early development, I tested a purchased Bluetooth footswitch and found that its behavior was a toggle. The interaction had assumed a hold-to-talk gesture.&lt;/p&gt;
&lt;p&gt;That was not a problem I could resolve by looking only at a button handler. The intended gesture and the actual device disagreed. I fed the finding back into Cursor and refined the interaction around a tap that starts listening while recognition is awaited. Getting it usable required debugging and repeated manual testing.&lt;/p&gt;
&lt;p&gt;Scene recall with the footswitch was a satisfying early result for me. That is a historical acceptance observation, not certification of the current version or every controller.&lt;/p&gt;
&lt;p&gt;The important requirement came from trying the hardware in the activity. With both hands on a guitar, a switch has to support a gesture the player can actually perform and recognize. A software abstraction can describe a press cleanly while missing how the physical control behaves.&lt;/p&gt;
&lt;h3 id=&quot;keep-local-scenes-and-validation-understandable&quot;&gt;Keep local scenes and validation understandable&lt;/h3&gt;
&lt;p&gt;The current product provides ten local scenes, with JSON export and import for backup or moving configurations between browsers. Keeping musical state in the browser avoids an account and cloud setup step. It also makes the storage boundary part of the experience: a scene saved in one browser is not automatically present in another.&lt;/p&gt;
&lt;p&gt;Local state should not be confused with offline support for every feature. Browser speech recognition has its own capabilities and dependencies. A local scene system does not make a browser's recognition service work offline.&lt;/p&gt;
&lt;p&gt;Validation has similar boundaries. Agents handled automated checks. I supplied manual acceptance testing and merge decisions. Those activities can support different claims, and none should stand in for all the others.&lt;/p&gt;
&lt;p&gt;I reported a successful live MIDI output test through an IAC bus into Logic Pro on September 29, 2026. That is useful evidence for that route. It does not establish behavior with every physical receiver or controller. An interface screenshot can establish a visible control or layout, but cannot establish that the music sounds right.&lt;/p&gt;
&lt;p&gt;The next useful checks are therefore about the actual interaction: can a player request a change without losing the phrase, understand when it takes effect and recover when an optional input method is unavailable? The current version has not yet been part of my regular practice beyond QA. Those practice questions remain work to do.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a browser music tool made the &lt;strong&gt;input and timing boundary&lt;/strong&gt; unusually tangible for me.&lt;br&gt;
Shared handlers let different input methods reach the same musical actions.&lt;br&gt;
The interface needs to distinguish &lt;strong&gt;selected, pending and applied state&lt;/strong&gt;.&lt;br&gt;
A playing change can wait for a beat or bar boundary while the interface acknowledges the request.&lt;br&gt;
Visible controls and text remain useful when an optional input method is unavailable.&lt;br&gt;
The system must accept the player's intention, preserve a musically sensible transition and communicate the difference.&lt;br&gt;
Real hardware then tests whether that intention was represented correctly in the first place.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32606/designing-browser-music-controls-around-the-next-beat</guid>
<pubDate>Tue, 27 Oct 2026 02:36:40 +0000</pubDate>
</item>
<item>
<title>I built a game that teaches Vim motions: what I got wrong about teaching a modal editor</title>
<link>https://coderlegion.com/31862/i-built-a-game-that-teaches-vim-motions-what-i-got-wrong-about-teaching-a-modal-editor</link>
<description>&lt;p&gt;Most developers I know have the same relationship with Vim: they learned just enough to exit it, and never went further.&lt;br&gt;
I wanted to fix that, so I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://vimrealm.com?ref=coderlegion&quot;&gt;Vimrealm&lt;/a&gt;: a browser RPG where you type real Vim commands to move a wizard around an isometric island made of text. Letters are stone, digits are crystal, punctuation is sandstone, spaces are grass. Press &lt;code&gt;w&lt;/code&gt; and the wizard leaps to the next word. Press &lt;code&gt;dd&lt;/code&gt; and a whole row of tiles drops away.&lt;br&gt;
Here's what I learned building it, including the parts I got wrong.&lt;/p&gt;
&lt;h3 id=&quot;why-vimtutor-loses-people&quot;&gt;Why vimtutor loses people&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vimtutor&lt;/code&gt; is good at one thing: showing you that the commands exist. It's weak at three others:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modes are invisible until they bite you.&lt;/strong&gt; You press &lt;code&gt;i&lt;/code&gt;, forget you're in Insert mode, type &lt;code&gt;dd&lt;/code&gt; and get &quot;dd&quot; in your file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There's no feedback on efficiency.&lt;/strong&gt; Reaching a word with &lt;code&gt;lllllllll&lt;/code&gt; and with &lt;code&gt;w&lt;/code&gt; looks identical to the tutorial. Vim's whole point is that these are not the same.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can use everything from lesson one.&lt;/strong&gt; Nothing stops you from falling back on the arrow keys, so you do.&lt;br&gt;
A game can fix all three, because a game has rules.&lt;h3 id=&quot;the-design&quot;&gt;The design&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Quests with a par.&lt;/strong&gt; Every quest has a par keystroke count, taken from a reference solution I wrote. Finish at or under par and you earn three stars. This turns &quot;I got there&quot; into &quot;I got there well&quot;, and it's the part that makes people retry levels to find the shorter route. Commands like &lt;code&gt;:hint&lt;/code&gt;, &lt;code&gt;:help&lt;/code&gt; and &lt;code&gt;:e!&lt;/code&gt; cost nothing, so asking for help is never punished.&lt;br&gt;
&lt;strong&gt;Spells unlock chapter by chapter.&lt;/strong&gt; Commands are &quot;spells&quot;, and you only have the ones you've learned. Try something you haven't met yet and the game tells you which chapter teaches it. The arrow keys are sealed for good. Chapter 1 is &lt;code&gt;h j k l&lt;/code&gt;; chapter 2 adds word motions; later chapters add operators, text objects, the dot command, search and visual mode. The curriculum ends with a boss fight against the Arrow-Key Dragon.&lt;br&gt;
&lt;strong&gt;Hints name the command.&lt;/strong&gt; Every target in a &quot;reach&quot; quest has a hint pointing at the spell to use. Quests come in two kinds: &lt;em&gt;reach&lt;/em&gt; (land the cursor on each gem, in order) and &lt;em&gt;edit&lt;/em&gt; (make the buffer match a goal text, with mismatched lines glowing red).&lt;br&gt;
&lt;strong&gt;Trials after the story.&lt;/strong&gt; Once the dragon falls, 60 Trials drill known spells with a twist: banned commands, a keystroke limit, a flawless run where one failed spell ends it, or a time limit that starts at your first key.&lt;/p&gt;
&lt;h3 id=&quot;the-technical-part-a-vim-emulator-in-plain-js&quot;&gt;The technical part: a Vim emulator in plain JS&lt;/h3&gt;
&lt;p&gt;The game is plain HTML, CSS and JavaScript. No bundler, no framework, no client-side dependencies, and the free chapters even work from &lt;code&gt;file://&lt;/code&gt;. The core is an emulator that handles Normal, Insert, Replace, Visual, Visual Line and Command-line modes, with counts, operators plus motions, text objects, registers, undo/redo, dot-repeat and search.&lt;br&gt;
Each keystroke flows through one pipeline:&lt;br&gt;
&lt;code&gt;keydown → normalize key → quest.feed(key) → vim.feed(key) → event&lt;/code&gt;&lt;br&gt;
&lt;code&gt;vim.feed&lt;/code&gt; returns an event describing what happened (&lt;code&gt;move&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;undo&lt;/code&gt;, &lt;code&gt;yank&lt;/code&gt; and so on) and the game reacts: sounds, hero animation, tile changes, win overlay. Keeping the engine and quest rules free of any DOM code was the best decision I made, because the same files run in Node.&lt;br&gt;
&lt;strong&gt;Testing it.&lt;/strong&gt; The emulator has to behave like real Vim, or you teach people the wrong thing. Every behaviour I add gets a test case like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;t(name, lines, keys, expect, start)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keys are written in a small notation (&lt;code&gt;&amp;lt;Esc&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;CR&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;C-r&amp;gt;&lt;/code&gt;), so a test reads like something you'd actually type.&lt;br&gt;
A second test suite replays &lt;strong&gt;every level's reference solution&lt;/strong&gt; through the real quest rules and fails if any key is rejected or the quest can't be won. That's also how par stays honest: change a level's buffer and the build tells you the solution no longer works.&lt;br&gt;
&lt;strong&gt;Where I stopped.&lt;/strong&gt; The emulator covers what the course teaches, not all of Vim. There are no macros, marks, named registers, ex ranges or &lt;code&gt;:s&lt;/code&gt;, and search matches literal text rather than regex. I drew the line at the point where emulating more would teach less. A beginner doesn't need &lt;code&gt;qa&lt;/code&gt; in week one, and every feature I add is one more place for the emulator to disagree with real Vim.&lt;br&gt;
One small deviation I kept deliberately: in Normal mode, Backspace takes back the last pending key. Real Vim does something different there, but for a beginner who typed &lt;code&gt;d&lt;/code&gt; by mistake, an undo for the pending key is friendlier than a cursor move.&lt;/p&gt;
&lt;h3 id=&quot;what-i-don-t-know-yet&quot;&gt;What I don't know yet&lt;/h3&gt;
&lt;p&gt;Vimrealm is new and I haven't had real players go through it yet, so I can't tell you which levels are confusing or where people quit. I have guesses: I suspect the jump from reach quests to edit quests in chapter 6 is the steepest point, and that par is either too strict or too forgiving.&lt;br&gt;
That's exactly what I'd like to find out.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;The first five chapters (13 quests, all movement) are free, with no sign-up:&lt;br&gt;
👉 &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vimrealm.com?ref=coderlegion&quot;&gt;vimrealm.com&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The rest (operators, text objects, the dot command, search, visual mode and the dragon) is a one-time $7.99. I'd rather say that up front than hide it.&lt;br&gt;
If you'd rather read than play, I wrote two guides built around worked examples:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vimrealm.com/vim-text-objects.html&quot;&gt;Vim text objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vimrealm.com/vim-dot-command.html&quot;&gt;The Vim dot command&lt;/a&gt;&lt;br&gt;
I'd love to hear which chapter you got stuck on, and which Vim habit you'd want a game to teach.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31862/i-built-a-game-that-teaches-vim-motions-what-i-got-wrong-about-teaching-a-modal-editor</guid>
<pubDate>Mon, 26 Oct 2026 13:26:00 +0000</pubDate>
</item>
<item>
<title>The architecture of a website that can't see your files (74 tools, no backend)</title>
<link>https://coderlegion.com/32457/the-architecture-of-a-website-that-cant-see-your-files-74-tools-no-backend</link>
<description>&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/319bto3g9dfv5s24arrw.png&quot; alt=&quot;Fizzdoc Home Edit &amp;amp; Create&quot;&gt;&lt;/p&gt;
&lt;p&gt;Most &quot;free online PDF tool&quot; sites have the same architecture: your file goes up to their server, gets processed, and comes back. The privacy policy says they delete it after an hour. You have to take their word for it.&lt;/p&gt;
&lt;p&gt;I learned this as the family's unofficial IT person. Every few weeks, someone sends me an Aadhaar scan or a bank statement and asks me to &quot;make it smaller&quot; or &quot;unlock it&quot;. I kept uploading other people's documents to sites I knew nothing about, and at some point that stopped feeling okay.&lt;/p&gt;
&lt;p&gt;After my first two posts on Fizzdoc, a close friend asked me some version of &quot;but how do I know it really doesn't upload?&quot; This post is the long answer.&lt;/p&gt;
&lt;p&gt;So for &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;Fizzdoc&lt;/a&gt; I wanted a site that &lt;strong&gt;can't&lt;/strong&gt; see your file, by design. The browser itself won't let the page send it anywhere.&lt;/p&gt;
&lt;p&gt;Fizzdoc now has 74 tools (PDF, Word, Excel, PowerPoint, images, audio), 16 languages and about 1,200 pages, and it has no backend at all. This post is about the design decisions that make that work, and the trade-offs I accepted.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/qzqab06i1u1mpuqkxlhr.png&quot; alt=&quot;Everything inside the red box runs in the visitor's browser tab.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;constraint-1-the-page-is-not-allowed-to-talk-to-anyone&quot;&gt;Constraint 1: the page is not allowed to talk to anyone&lt;/h3&gt;
&lt;p&gt;Every page ships this Content Security Policy (in a &lt;code&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag, which the browser enforces just like a header):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;connect-src 'self'&lt;/code&gt; is the important part. &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, WebSockets, &lt;code&gt;sendBeacon&lt;/code&gt;: the browser blocks all of them unless they go to fizzdoc.com itself, and fizzdoc.com is just static files. So even if one of the many open-source libraries had a bug or a malicious update, it couldn't send a document anywhere.&lt;/p&gt;
&lt;p&gt;That one line shaped everything else:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No CDNs.&lt;/strong&gt; Fonts, WebAssembly engines and AI models are all served from the same origin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No analytics scripts.&lt;/strong&gt; Even when Cloudflare injected its own analytics beacon, the CSP blocked it, because a script that isn't from the site can't run. (I switched that beacon off too; traffic numbers come from Cloudflare's server-side stats instead.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is testable.&lt;/strong&gt; The Playwright suite (about 1,250 browser tests) runs real jobs and fails if any request leaves the origin.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;constraint-2-no-server-so-the-build-does-the-work&quot;&gt;Constraint 2: no server, so the build does the work&lt;/h3&gt;
&lt;p&gt;There is no server to render pages, so a build step renders all of them ahead of time. One TypeScript file lists every tool (name, copy, FAQ, search keywords), and the build turns it into a static HTML page per tool per language: about 1,200 pages, each with its own title, structured data (FAQ, HowTo, breadcrumbs), hreflang links and sitemap entry.&lt;/p&gt;
&lt;p&gt;The first load is small because each page only ships what it needs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;On first load &lt;/th&gt;&lt;th&gt;Gzipped&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JavaScript &lt;/td&gt;&lt;td&gt;about 19 KB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CSS &lt;/td&gt;&lt;td&gt;about 9 KB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Everything heavy loads only when you use the tool that needs it, through dynamic &lt;code&gt;import()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;each-job-gets-its-own-throwaway-worker&quot;&gt;Each job gets its own throwaway worker&lt;/h3&gt;
&lt;p&gt;The PDF tools are &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/qpdf/qpdf&quot;&gt;qpdf&lt;/a&gt; compiled to WebAssembly. Every job gets a fresh Web Worker, which is terminated when the job ends. That keeps the page responsive, and the file and any password disappear with the worker.&lt;/p&gt;
&lt;p&gt;Other engines follow the same pattern: pdf.js and pdf-lib for rendering and editing, Tesseract (WASM) for OCR, Whisper (through Transformers.js) for speech to text, and BiRefNet on ONNX Runtime Web (WebGPU when available, WASM otherwise) for background removal.&lt;/p&gt;
&lt;h3 id=&quot;big-models-on-a-host-with-a-25-mib-file-limit&quot;&gt;Big models on a host with a 25 MiB file limit&lt;/h3&gt;
&lt;p&gt;Cloudflare Pages refuses any file over 25 MiB. The background model is about 100 MB and the WebGPU build of ONNX Runtime is 27 MB.&lt;/p&gt;
&lt;p&gt;So the build splits them into 20 MiB parts. On first use the worker downloads the parts, joins them, and keeps the result in Cache Storage. After that the tool starts instantly, and it works with the internet off. The build also fails if any output file goes over the limit, after one deploy failed on it.&lt;/p&gt;
&lt;h3 id=&quot;offline-without-breaking-updates&quot;&gt;Offline without breaking updates&lt;/h3&gt;
&lt;p&gt;A service worker makes every tool work offline after one visit. The rules are deliberately boring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pages:&lt;/strong&gt; network first, cache as a fallback. Online visitors always get the latest page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hashed assets&lt;/strong&gt; (&lt;code&gt;/assets/index-abc123.js&lt;/code&gt;): cache first. A hashed file never changes, so the cached copy is always right.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Old files are never deleted.&lt;/strong&gt; A page cached by an older version still needs its older assets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Models are excluded.&lt;/strong&gt; Their tools manage their own caches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A page that was never opened shows a small &quot;needs the internet once&quot; page in your language instead of the browser's error screen.&lt;/p&gt;
&lt;h3 id=&quot;hard-parts-that-come-from-having-no-server&quot;&gt;Hard parts that come from having no server&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Phones run out of memory, silently.&lt;/strong&gt; The first version of the background remover crashed Chrome on a 6 GB Android phone (&quot;Aw, Snap!&quot; at 5%). A tab gets far less memory than the phone has, and there's no error to catch. Phones now use a 4.6 MB model instead of the 100 MB one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iPhone canvases have a size limit&lt;/strong&gt; (about 16.7 million pixels). The Photo Editor tries full size first and falls back to the largest canvas the device allows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fonts couldn't be &lt;code&gt;data:&lt;/code&gt; URLs.&lt;/strong&gt; Vite inlines small files as &lt;code&gt;data:&lt;/code&gt; URLs, my own CSP refused them, and the CSP-violation test caught it before it shipped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Share to WhatsApp&quot; can't make a sticker.&lt;/strong&gt; A web page can't add stickers to WhatsApp. A shared file arrives as a photo, so the page shows WhatsApp's own &quot;Create sticker&quot; steps instead of a button that lies.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xbk0jsiw953axdqlr35j.png&quot; alt=&quot;The Photo Editor: all pixel maths in plain TypeScript, no image library.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-editor-is-just-pixel-maths&quot;&gt;The editor is just pixel maths&lt;/h3&gt;
&lt;p&gt;The newest tools, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/photo-editor/&quot;&gt;Photo Editor&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/collage-maker/&quot;&gt;Collage Maker&lt;/a&gt;, use no image library. Brightness, contrast, saturation and warmth are a short loop over the pixels. Every edit redraws from the original photo, so edits never pile up blur. Doing it in plain code instead of &lt;code&gt;ctx.filter&lt;/code&gt; means every browser gives the same result, and unit tests can check it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/q03hluqkhsnyhyd1cjz5.png&quot; alt=&quot;The Collage Maker: previews use small copies; saving decodes one photo at a time at full size.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-it-costs-and-what-30-days-of-traffic-look-like&quot;&gt;What it costs, and what 30 days of traffic look like&lt;/h3&gt;
&lt;p&gt;Fizzdoc runs on Cloudflare's free plan. Hosting costs me nothing, because every page and every AI model is just a static file. These are Cloudflare's own server-side numbers for the last 30 days (there is no analytics script on the site, so this is all I can see):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Last 30 days &lt;/th&gt;&lt;th&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Requests &lt;/td&gt;&lt;td&gt;201k&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Visits &lt;/td&gt;&lt;td&gt;41.9k&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Page views &lt;/td&gt;&lt;td&gt;62.8k&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data served &lt;/td&gt;&lt;td&gt;10.15 GB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Server errors (5xx) &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0yakvoiax2uhm7abny8b.png&quot; alt=&quot;Requests over 30 days&quot;&gt;&lt;/p&gt;
&lt;p&gt;A few things I didn't expect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A big share is bots.&lt;/strong&gt; Search crawlers (Googlebot, Applebot, Petalbot, Semrush) and AI crawlers show up all over the user-agent list. I welcome them in robots.txt, but it means &quot;42k visits&quot; is not 42k people. Treat these as upper bounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Most of the bandwidth is AI models, not pages.&lt;/strong&gt; The pages are tiny; the 10 GB is almost all background-removal and Whisper model files that visitors download once and then keep in their browser cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/shlf1buyajdhgmmd3quu.png&quot; alt=&quot;Top paths by data transfer: model files, not HTML.&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bots keep looking for a backend that doesn't exist.&lt;/strong&gt; Hundreds of requests for &lt;code&gt;/.env&lt;/code&gt;, &lt;code&gt;/wp-config.php&lt;/code&gt;, &lt;code&gt;/api/&lt;/code&gt; and &lt;code&gt;/login&lt;/code&gt;. There is nothing to find, since there's no server, database or secrets behind the site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visitors come from everywhere.&lt;/strong&gt; The United States, India, Singapore, South Korea, Australia and Germany lead, and the page is in 16 languages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/7hy82kfc412p8611qbky.png&quot; alt=&quot;Page views by country&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;trade-offs-i-accepted&quot;&gt;Trade-offs I accepted&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your device does the work.&lt;/strong&gt; A 300-page scan OCRs slower on an old phone than on a server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Some formats aren't possible yet.&lt;/strong&gt; PowerPoint to PDF and old .doc/.xls files aren't supported.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-time downloads.&lt;/strong&gt; The AI tools download their model once (the page shows the size first).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-break-it&quot;&gt;Try it, break it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Site: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;https://fizzdoc.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (Apache-2.0): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;https://github.com/kingrishabdugar/fizzdoc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Check it yourself: open DevTools → Network, run any tool, and watch nothing leave.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you build something similar, the one decision I'd copy is the CSP. It turns &quot;we don't upload your file&quot; from a promise into a property of the page. And if Fizzdoc saved you an upload, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;a star on GitHub&lt;/a&gt; helps more people find it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32457/the-architecture-of-a-website-that-cant-see-your-files-74-tools-no-backend</guid>
<pubDate>Mon, 26 Oct 2026 02:55:10 +0000</pubDate>
</item>
<item>
<title>Headless Vue + WordPress Boilerplate v2.0.0</title>
<link>https://coderlegion.com/32437/headless-vue-wordpress-boilerplate-v2-0-0</link>
<description>&lt;h3 id=&quot;headless-vue-wordpress-boilerplate-v2-0-0&quot;&gt;Headless Vue + WordPress Boilerplate v2.0.0&lt;/h3&gt;
&lt;p&gt;Yes, Version 2.0.0 is here!!!&lt;/p&gt;
&lt;p&gt;This boilerplate allows you to use Vue as the front-end for your WordPress theme in a headless architecture. WordPress handles the backend and REST API, while Vue powers the user interface using modern frontend tooling like Vite.&lt;/p&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use Vue 3 with Vite for a fast and modern development experience&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚡ Hot Module Replacement inside WordPress: save a &lt;code&gt;.vue&lt;/code&gt; file and see it instantly, no rebuild&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One-line REST API routes in PHP and one-line calls in Vue (&lt;code&gt;useApi&lt;/code&gt;, &lt;code&gt;useWp&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WordPress pages, posts and menus work out of the box with Vue Router&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modular folder structure for components, pages, APIs, and stores&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚙️ Supports Vue Router and Pinia (Vue Store)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Style with Tailwind CSS, standard CSS or your preferred CSS preprocessor&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress 6.7+ and PHP 8.1+&lt;/li&gt;
&lt;li&gt;Node.js 20+&lt;/li&gt;
&lt;li&gt;Pretty permalinks (Settings &amp;gt; Permalinks, anything except &quot;Plain&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;folder-structure&quot;&gt;Folder Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;themes/
└── vue-boilerplate/
    ├── functions/               # WordPress PHP functions
    │   ├── api.php              # Your REST API routes
    │   ├── api-helpers.php      # vue_theme_api_get(), vue_theme_api_post(), ...
    │   ├── vite.php             # Loads the Vue app (dev server or build)
    │   └── theme-settings.php   # Theme supports and menu locations
    ├── src/                     # Vue application root
    │   ├── api/                 # API client (index.js) and JS modules for API calls
    │   │   └── wp/
    │   ├── assets/              # Static assets like images and SVGs
    │   ├── components/          # Vue components (e.g., layout)
    │   ├── composables/         # useApi and useWp
    │   ├── page/                # Vue page views
    │   ├── router/              # Vue Router setup
    │   ├── stores/              # Pinia stores
    │   │   └── wp/
    │   ├── utils/               # HTML sanitizer and link helpers
    │   ├── App.vue              # Root Vue component
    │   ├── main.js              # Vue app entry point
    │   ├── settings.js          # Values passed from WordPress
    │   └── style.css            # Global styles
    ├── dist/                    # Production build (created by npm run build)
    ├── *.php                    # WordPress theme files (header.php, footer.php, etc.)
    ├── style.css                # Theme metadata
    └── vite.config.js           # Configuration file for Vite, a fast frontend build tool.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Download and put the files inside your &lt;code&gt;themes&lt;/code&gt; folder, then activate the theme.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/wordpress-vue-theme-boilerplate/releases&quot;&gt;Download Latest Release&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install Dependencies&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the Development Server&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open &lt;strong&gt;your WordPress site URL&lt;/strong&gt; (e.g. &lt;code&gt;http://mysite.local&lt;/code&gt;), not the Vite URL.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;While &lt;code&gt;npm run dev&lt;/code&gt; is running, WordPress loads the app from the Vite dev server. Vue and CSS changes show up instantly, and PHP changes reload the page. Stop the dev server and WordPress goes back to the production build.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Make sure WordPress is running locally (e.g., with LocalWP or Laragon) and that the theme is activated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;build-for-production&quot;&gt;Build for Production&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will compile and optimize your Vue app into &lt;code&gt;dist/&lt;/code&gt;. Upload the theme with the &lt;code&gt;dist&lt;/code&gt; folder, you don't need &lt;code&gt;node_modules&lt;/code&gt; or &lt;code&gt;src&lt;/code&gt; on the server.&lt;/p&gt;
&lt;h3 id=&quot;working-with-the-api&quot;&gt;Working with the API&lt;/h3&gt;
&lt;h4 id=&quot;add-a-route-php-&quot;&gt;Add a route (PHP)&lt;/h4&gt;
&lt;p&gt;Add it at the bottom of &lt;code&gt;functions/api.php&lt;/code&gt;. Routes live under &lt;code&gt;/wp-json/vue-theme/v1/&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;vue_theme_api_get('/books', function () {
    return get_posts(['post_type' =&amp;gt; 'book']);
});

// URL parameter
vue_theme_api_get('/books/{id}', function ($request) {
    $book = get_post((int) $request['id']);

    if (!$book) {
        return new WP_Error('not_found', 'Book not found.', ['status' =&amp;gt; 404]);
    }

    return $book;
});

// JSON body
vue_theme_api_post('/contact', function ($request) {
    $email = sanitize_email($request['email']);

    return ['sent' =&amp;gt; true];
});

// Logged in users only, or users with a capability
vue_theme_api_post('/notes', 'my_callback', ['permission' =&amp;gt; true]);
vue_theme_api_delete('/notes/{id}', 'my_callback', ['permission' =&amp;gt; 'edit_posts']);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;use-it-vue-&quot;&gt;Use it (Vue)&lt;/h4&gt;
&lt;p&gt;In a component, &lt;code&gt;useApi&lt;/code&gt; and &lt;code&gt;useWp&lt;/code&gt; are auto-imported and give you reactive state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const { data: books, loading, error } = useApi('books')

// WordPress core API: posts, pages, categories, media, your custom post types...
const page = ref(1)
const { data: posts, totalPages } = useWp('posts', () =&amp;gt; ({ per_page: 6, page: page.value }))
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;p v-if=&quot;loading&quot;&amp;gt;Loading...&amp;lt;/p&amp;gt;
    &amp;lt;p v-else-if=&quot;error&quot;&amp;gt;{{ error }}&amp;lt;/p&amp;gt;
    &amp;lt;ul v-else&amp;gt;
        &amp;lt;li v-for=&quot;book in books&quot; :key=&quot;book.ID&quot;&amp;gt;{{ book.post_title }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anywhere else (stores, event handlers), use the clients:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { api, wp } from '@/api' // or a relative path like '../api'

const books = await api.get('books')
const result = await api.post('contact', { email, message })
const posts = await wp.get('posts', { per_page: 5, search: 'vue' })
const { data, total, totalPages } = await wp.paginate('posts', { page: 2 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failed requests throw an &lt;code&gt;ApiError&lt;/code&gt; with &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. The nonce for logged in users is sent automatically.&lt;/p&gt;
&lt;h3 id=&quot;routing&quot;&gt;Routing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add your own routes in &lt;code&gt;src/router/index.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any other URL is looked up in WordPress: &lt;code&gt;src/page/Content.vue&lt;/code&gt; shows the page or post with that slug, or a &quot;Page not found&quot; message.&lt;/li&gt;
&lt;li&gt;Menus assigned to &lt;strong&gt;Primary Menu&lt;/strong&gt; and &lt;strong&gt;Footer Menu&lt;/strong&gt; (Appearance &amp;gt; Menus) are rendered by &lt;code&gt;Header.vue&lt;/code&gt; and &lt;code&gt;Footer.vue&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;integration-tips&quot;&gt;Integration Tips&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Site title, logo and menus are available without a request: &lt;code&gt;useSiteStore()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pass your own PHP data to Vue with the &lt;code&gt;vue_theme_app_config&lt;/code&gt; filter, then read it from &lt;code&gt;window.wpVueTheme&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always use &lt;code&gt;sanitizeHtml()&lt;/code&gt; from &lt;code&gt;src/utils/html.js&lt;/code&gt; before rendering WordPress content with &lt;code&gt;v-html&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import images from &lt;code&gt;src/assets&lt;/code&gt; so they get the right URL after the build&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set &lt;code&gt;show_in_rest&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; on your custom post types to load them with &lt;code&gt;useWp('your-post-type')&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-troubleshooting&quot;&gt;❓ Troubleshooting&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;The Vue app is not built yet&quot;&lt;/strong&gt;: run &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;, or start &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changes don't show up&lt;/strong&gt;: check that &lt;code&gt;npm run dev&lt;/code&gt; is still running and that you opened the WordPress URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port 5173 is already in use&lt;/strong&gt;: create &lt;code&gt;.env.local&lt;/code&gt; with &lt;code&gt;VITE_DEV_PORT=5174&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Local domain is not &lt;code&gt;localhost&lt;/code&gt;, `&lt;/em&gt;.local&lt;code&gt;, &lt;/code&gt;&lt;em&gt;.loc&lt;code&gt; or &lt;/code&gt;&lt;/em&gt;.test&lt;code&gt;**: add &lt;/code&gt;VITE_WP_ORIGIN=&lt;a rel=&quot;nofollow&quot; href=&quot;http://your-domain`&quot;&gt;http://your-domain`&lt;/a&gt; to &lt;code&gt;.env.local&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress runs in Docker&lt;/strong&gt;: PHP can't see the dev server on your machine, so add &lt;code&gt;add_filter('vue_theme_check_dev_server', '__return_false');&lt;/code&gt; to &lt;code&gt;functions.php&lt;/code&gt; while developing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue routes return a server 404&lt;/strong&gt;: enable pretty permalinks in Settings &amp;gt; Permalinks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-upgrading-from-1-x&quot;&gt;⬆️ Upgrading from 1.x&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;CHANGELOG.md&quot;&gt;CHANGELOG.md&lt;/a&gt; for the breaking changes.&lt;/p&gt;
&lt;h3 id=&quot;github-repo&quot;&gt;Github Repo&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/wordpress-vue-theme-boilerplate&quot;&gt;Headless Vue + WordPress Boilerplate v2.0.0&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;license&quot;&gt;License&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;LICENSE&quot;&gt;MIT License&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Prepared by: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011&quot;&gt;Christopher Robin Chase&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32437/headless-vue-wordpress-boilerplate-v2-0-0</guid>
<pubDate>Mon, 26 Oct 2026 01:04:13 +0000</pubDate>
</item>
<item>
<title>Fine in Chrome, broken in Preview: two PDFKit gotchas for anyone who writes PDFs</title>
<link>https://coderlegion.com/32435/fine-in-chrome-broken-in-preview-two-pdfkit-gotchas-for-anyone-who-writes-pdfs</link>
<description>&lt;p&gt;&lt;em&gt;This article was originally published on Hashnode: &lt;a rel=&quot;nofollow&quot; href=&quot;https://wangyan.hashnode.dev/fine-in-chrome-broken-in-preview-two-pdfkit-gotchas-for-anyone-who-writes-pdfs&quot;&gt;Fine in Chrome, broken in Preview&lt;/a&gt;. It is republished here with light edits.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; we make &lt;a rel=&quot;nofollow&quot; href=&quot;https://lensup.ai/pdf-editor/&quot;&gt;LensUp's PDF editor&lt;/a&gt;, the browser tool where we ran into both bugs, so we have a direct connection to the product mentioned here. This write-up was drafted and edited with AI assistance; the bugs, test results and reproduction scripts come from our own testing, and the scripts are public so you can check every claim yourself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We build a PDF editor that runs in the browser. It opens a PDF, lets you add text, a signature or a highlight, fill in form fields, and writes the result back as an incremental update. We checked every save with three renderers: pdf.js (Firefox and many web viewers), pdfium (Chrome) and poppler (most Linux tools). All three were happy.&lt;/p&gt;
&lt;p&gt;Then we rendered the same files with Apple's PDFKit, the engine behind Preview on the Mac, the Files app on iOS and PDFs in Safari. It showed two problems none of the others did: a translucent highlighter that turned into an opaque bar or a sentence that vanished, and form answers printed twice on top of each other.&lt;/p&gt;
&lt;p&gt;Neither problem shows up if you only test on Linux or in Chrome. Here are both, with small scripts that reproduce them on any Mac in a few seconds: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ppop123/pdfkit-pitfalls&quot;&gt;github.com/ppop123/pdfkit-pitfalls&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;gotcha-1-an-incremental-update-must-use-the-same-cross-reference-form-as-the-file-it-updates&quot;&gt;Gotcha 1: an incremental update must use the same cross-reference form as the file it updates&lt;/h3&gt;
&lt;h4 id=&quot;some-background&quot;&gt;Some background&lt;/h4&gt;
&lt;p&gt;An incremental update appends to a PDF instead of rewriting it. The new and changed objects go at the end, followed by a new cross-reference section that points back to the previous one. That's how signed PDFs stay signed, and it's why saving an edit doesn't mean rewriting the whole file: only the edit is written.&lt;/p&gt;
&lt;p&gt;The cross-reference section comes in two forms. The classic one is an &lt;code&gt;xref&lt;/code&gt; table followed by a &lt;code&gt;trailer&lt;/code&gt;. PDF 1.5 added cross-reference streams (&lt;code&gt;/Type /XRef&lt;/code&gt;), which usually come with object streams (&lt;code&gt;/Type /ObjStm&lt;/code&gt;): many small non-stream objects compressed together into one stream.&lt;/p&gt;
&lt;p&gt;We opened files for incremental update with pdf-lib (the @cantoo/pdf-lib fork, which supports incremental updates) and saved with the defaults, which turn object streams on. So whatever the original file used, our update ended in an XRef stream, with our new dictionaries packed into an object stream.&lt;/p&gt;
&lt;h4 id=&quot;what-pdfkit-does-with-that&quot;&gt;What PDFKit does with that&lt;/h4&gt;
&lt;p&gt;Take a one-page PDF that ends in a classic &lt;code&gt;xref&lt;/code&gt; table and append a highlighter stroke: a yellow rectangle drawn through an ExtGState with 60% opacity and the Multiply blend mode. Written as a table-based update, it renders the same everywhere. Written as an XRef stream with the ExtGState inside an object stream, poppler still draws it correctly, but PDFKit shows an opaque yellow bar where the sentence was.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/ppop123/pdfkit-pitfalls/main/xref-mismatch.png&quot; alt=&quot;The same highlight, appended four ways, rendered by Apple PDFKit and by poppler&quot;&gt;&lt;/p&gt;
&lt;p&gt;Render it with &lt;code&gt;CG_PDF_VERBOSE=1&lt;/code&gt; set, and CoreGraphics says why:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;encountered unexpected object type: 7.
missing or invalid object number.
missing or invalid cross-reference stream.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It gives up on the update's XRef stream. Objects the update wrote out on their own are still found: the rectangle itself is drawn. Objects inside the update's object stream are not, and the ExtGState is one of them. Without it the rectangle is painted fully opaque, on top of the text.&lt;/p&gt;
&lt;p&gt;The reverse breaks too. Over an original that ends in an XRef stream, an update written with a classic table gets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;failed to find start of cross-reference section.
font `Helvetica-7098480789' not found in document.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This time it's the original's object stream that gets lost, and the font was in it. The highlight is there; the sentence is gone.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Original's last section &lt;/th&gt;&lt;th&gt;Update written as &lt;/th&gt;&lt;th&gt;PDFKit&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;xref&lt;/code&gt; table &lt;/td&gt;&lt;td&gt;&lt;code&gt;xref&lt;/code&gt; table &lt;/td&gt;&lt;td&gt;fine&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;xref&lt;/code&gt; table &lt;/td&gt;&lt;td&gt;XRef stream with object streams &lt;/td&gt;&lt;td&gt;loses the objects in the update's object stream&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;XRef stream &lt;/td&gt;&lt;td&gt;&lt;code&gt;xref&lt;/code&gt; table &lt;/td&gt;&lt;td&gt;loses the objects in the original's object streams&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;XRef stream &lt;/td&gt;&lt;td&gt;XRef stream &lt;/td&gt;&lt;td&gt;fine&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In our editor's checks, pdf.js, pdfium and poppler accepted all four combinations. That's exactly why a Linux-only test suite never caught it.&lt;/p&gt;
&lt;h4 id=&quot;the-fix&quot;&gt;The fix&lt;/h4&gt;
&lt;p&gt;Look at where the original's last &lt;code&gt;startxref&lt;/code&gt; points. If it lands on the keyword &lt;code&gt;xref&lt;/code&gt;, the last section is a table; otherwise it's a stream. Then write the update in the same form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// The form of the original's last cross-reference section: 'table' or 'stream'.
function xrefForm(bytes) {
  const tail = new TextDecoder('latin1').decode(bytes.subarray(Math.max(0, bytes.length - 2048)));
  const found = [...tail.matchAll(/startxref\s+(\d+)/g)];
  const offset = found.length ? Number(found[found.length - 1][1]) : NaN;
  if (!(offset &amp;gt;= 0 &amp;amp;&amp;amp; offset &amp;lt; bytes.length)) return 'table';
  let i = offset;
  while (i &amp;lt; bytes.length &amp;amp;&amp;amp; /\s/.test(String.fromCharCode(bytes[i]))) i += 1;
  return new TextDecoder('latin1').decode(bytes.subarray(i, i + 4)) === 'xref' ? 'table' : 'stream';
}

const doc = await PDFDocument.load(bytes, { forIncrementalUpdate: true });
// … edit …
const saved = await doc.save({ useObjectStreams: xrefForm(bytes) === 'stream' });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hybrid-reference files, whose trailer is a table that also points to an XRef stream, end in a table, so they count as &lt;code&gt;'table'&lt;/code&gt; here.&lt;/p&gt;
&lt;h3 id=&quot;gotcha-2-pdfkit-draws-filled-text-fields-itself&quot;&gt;Gotcha 2: PDFKit draws filled text fields itself&lt;/h3&gt;
&lt;h4 id=&quot;what-pdfkit-does&quot;&gt;What PDFKit does&lt;/h4&gt;
&lt;p&gt;A form field's look is supposed to come from its appearance stream (&lt;code&gt;/AP&lt;/code&gt;): a small content stream in which the writer draws the value. For a text field that has a value, PDFKit does something else. It draws the value (&lt;code&gt;/V&lt;/code&gt;) itself with CoreText, in the font named by the field's default appearance string (&lt;code&gt;/DA&lt;/code&gt;), and it drops the text-showing operators (&lt;code&gt;BT … Tj … ET&lt;/code&gt;) from the appearance stream. Everything else in the appearance stream it still paints, such as paths and Form XObjects.&lt;/p&gt;
&lt;p&gt;You can see the first half even with pdf-lib's own appearance. In the first row below, poppler draws pdf-lib's auto-sized Helvetica, while PDFKit ignores it and draws its own rendering of the value at a different size.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/ppop123/pdfkit-pitfalls/main/field-double-draw.png&quot; alt=&quot;One filled text field with three kinds of appearance, rendered by Apple PDFKit and by poppler&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;why-that-bit-us&quot;&gt;Why that bit us&lt;/h4&gt;
&lt;p&gt;Our text engine shapes text itself (joining Persian and Arabic, ordering mixed-direction text, falling back between fonts) and draws the result as glyph outlines, so every reader shows exactly the same glyphs. In a field's appearance, those outlines are paths. PDFKit painted the paths and then drew &lt;code&gt;/V&lt;/code&gt; again with CoreText on top: two copies of every answer, as in the second row. On a real IRS W-9 the two copies didn't even line up, because PDFKit left-aligned its copy while our appearance was right-aligned.&lt;/p&gt;
&lt;p&gt;None of these stopped the second drawing: making the field read-only, setting &lt;code&gt;/NeedAppearances&lt;/code&gt; to false, removing the &lt;code&gt;/Tx BMC … EMC&lt;/code&gt; marked content, or changing the font or color in &lt;code&gt;/DA&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;the-fix-1&quot;&gt;The fix&lt;/h4&gt;
&lt;p&gt;Write the value as text after all, just not in a real font: use a Type 3 font whose glyph procedures are the same outlines. PDFKit drops text-showing operators, Type 3 included, and draws the value once in its own rendering. pdf.js, pdfium and poppler draw the Type 3 glyphs, which are our outlines. That's the third row.&lt;/p&gt;
&lt;p&gt;The trade-off is that in Preview you get PDFKit's rendering of the value (its font from &lt;code&gt;/DA&lt;/code&gt;, its alignment), not yours. For us that was acceptable, since CoreText shapes Persian correctly.&lt;/p&gt;
&lt;p&gt;Two related notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dropdowns and list boxes are different.&lt;/strong&gt; PDFKit doesn't redraw them, so for choice fields we kept the outlines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Acrobat had its own problem with outlines.&lt;/strong&gt; Acrobat Reader drew our outline appearances in some forms but left the box blank in others (fields created by pdf-lib, with a &lt;code&gt;/MK&lt;/code&gt; background color). It drew the Type 3 version in both.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;bonus-gotcha-from-acrobat-reader-extensions-ur3-&quot;&gt;Bonus gotcha from Acrobat: Reader extensions (&lt;code&gt;/UR3&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;Many official forms, the W-9 among them, carry usage rights (&lt;code&gt;/Perms /UR3&lt;/code&gt;, &quot;Reader extensions&quot;): a signature over the file as issued. Change the file and Acrobat Reader opens it with:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This document enabled extended features in Adobe Acrobat Reader. The document has been changed since it was created and use of extended features is no longer available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every save changes the file, so on save we now remove &lt;code&gt;/UR3&lt;/code&gt; and the older &lt;code&gt;/UR&lt;/code&gt; from &lt;code&gt;/Perms&lt;/code&gt; (and &lt;code&gt;/Perms&lt;/code&gt; itself if nothing else is left). A certification signature (&lt;code&gt;/DocMDP&lt;/code&gt;) stays. iText does the same thing with &lt;code&gt;removeUsageRights&lt;/code&gt;. To be clear about what we've checked: we saw the warning in Acrobat Reader, and we haven't re-tested the fixed files there yet.&lt;/p&gt;
&lt;h3 id=&quot;how-to-test-with-pdfkit-without-clicking-through-preview&quot;&gt;How to test with PDFKit without clicking through Preview&lt;/h3&gt;
&lt;p&gt;PDFKit is scriptable from Swift in about twenty lines: open the file with &lt;code&gt;PDFDocument&lt;/code&gt;, draw page 1 into a bitmap with &lt;code&gt;PDFPage.draw(with:to:)&lt;/code&gt; (form widgets included), and write a PNG. Run it with &lt;code&gt;CG_PDF_VERBOSE=1&lt;/code&gt; and CoreGraphics prints what it didn't like. The repo's &lt;code&gt;render-pdfkit.swift&lt;/code&gt; does exactly that. &lt;code&gt;qlmanage -t&lt;/code&gt;, the Quick Look thumbnailer, goes through PDFKit too.&lt;/p&gt;
&lt;p&gt;If you compare against pdfium from Python: pypdfium2 doesn't draw form fields unless you call &lt;code&gt;pdf.init_forms()&lt;/code&gt; first. Without it, every field looks empty, which is easy to mistake for a bug in your writer.&lt;/p&gt;
&lt;h3 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Three renderers agreeing on a file doesn't mean the viewer on every Mac and iPhone will. If your PDFs reach Apple devices, render them with PDFKit, in CI on a macOS runner or at least by hand.&lt;/li&gt;
&lt;li&gt;In an incremental update, match the cross-reference form of the file you're updating.&lt;/li&gt;
&lt;li&gt;For a filled text field, PDFKit draws the value itself. Avoid drawing the value into the appearance as paths; text in a Type 3 font works everywhere we tested.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reproductions, in Node and Swift, are MIT-licensed: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ppop123/pdfkit-pitfalls&quot;&gt;github.com/ppop123/pdfkit-pitfalls&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;About the author: the team behind this post builds &lt;a rel=&quot;nofollow&quot; href=&quot;https://lensup.ai/&quot;&gt;LensUp Scanner&lt;/a&gt;, a free set of browser tools for scanning documents and working with PDFs. If you have seen other PDFKit quirks, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ppop123/pdfkit-pitfalls&quot;&gt;pdfkit-pitfalls repo&lt;/a&gt; is open to issues and pull requests.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32435/fine-in-chrome-broken-in-preview-two-pdfkit-gotchas-for-anyone-who-writes-pdfs</guid>
<pubDate>Mon, 26 Oct 2026 00:16:01 +0000</pubDate>
</item>
<item>
<title>devtemDesign: one config object paints a whole landing page</title>
<link>https://coderlegion.com/32379/devtemdesign-one-config-object-paints-a-whole-landing-page</link>
<description>&lt;blockquote&gt;&lt;p&gt;A zero-dependency page engine in plain HTML, CSS and JS. 7 templates, live theming, pricing, FAQ, stats and a mobile drawer from a single config. Full reference included&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every landing page I built started the same way: nav, hero, feature cards, pricing, FAQ, footer, then a day of making it responsive. So I wrote an engine that does that part for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;devtemDesign&lt;/strong&gt; takes one config object and renders the whole page. No framework, no build step, no dependencies. Two tags and one function call.&lt;/p&gt;
&lt;p&gt;This post is both an introduction and a reference for &lt;strong&gt;v0.1.0&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;try-it-in-30-seconds&quot;&gt;Try it in 30 seconds&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/devtemdesign@0.1.0/style.min.css&quot;&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/devtemdesign@0.1.0/script.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;div id=&quot;app-root&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
  devtemDesign(&quot;app-root&quot;, {
    template: &quot;cyber&quot;,
    logoName: &quot;MYAPP&quot;,
    heroTitleLine1: &quot;Declarative UI,&quot;,
    heroTitleLine2: &quot;Zero-Config Motion&quot;,
    heroSubtitle: &quot;One config object. Complete animated landing.&quot;,
    primaryBtnText: &quot;Get started&quot;,
    primaryBtnHref: &quot;/docs&quot;
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives you a sticky glass navbar, an animated hero, ambient background orbs, reveal-on-scroll, a mobile drawer and a footer. Every section except the hero is optional, and a section only renders when its config key is present.&lt;/p&gt;
&lt;p&gt;If you want to skip the code entirely, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/builder&quot;&gt;builder&lt;/a&gt; generates a config from a description and lets you edit it live.&lt;/p&gt;
&lt;h3 id=&quot;what-s-in-0-1-0&quot;&gt;What's in 0.1.0&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7 templates:&lt;/strong&gt; &lt;code&gt;figsh&lt;/code&gt;, &lt;code&gt;cyber&lt;/code&gt;, &lt;code&gt;aurora&lt;/code&gt;, &lt;code&gt;monochrome&lt;/code&gt;, &lt;code&gt;sunset&lt;/code&gt;, &lt;code&gt;ocean&lt;/code&gt;, and &lt;code&gt;paper&lt;/code&gt; (light).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live theming:&lt;/strong&gt; switch at runtime, add a built-in theme button, or override any design token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New sections:&lt;/strong&gt; stats with count-up numbers, FAQ accordion, closing CTA, and pricing with a Monthly/Yearly toggle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactions:&lt;/strong&gt; scroll progress bar, active nav link, card spotlight, seamless brand marquee, back-to-top button.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real lifecycle:&lt;/strong&gt; &lt;code&gt;devtemDesign()&lt;/code&gt; returns &lt;code&gt;{ setTemplate, destroy }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility:&lt;/strong&gt; skip link, focus rings, &lt;code&gt;aria-expanded&lt;/code&gt; on the menu, native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; for FAQ, full &lt;code&gt;prefers-reduced-motion&lt;/code&gt; support.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-fuller-example&quot;&gt;A fuller example&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const page = devtemDesign(&quot;app-root&quot;, {
  template: &quot;ocean&quot;,
  themeSwitcher: true,
  logoName: &quot;ORBIT&quot;,
  navLinks: [
    { label: &quot;Features&quot;, href: &quot;#features&quot; },
    { label: &quot;Pricing&quot;, href: &quot;#pricing&quot; },
    { label: &quot;FAQ&quot;, href: &quot;#faq&quot; }
  ],
  ctaText: &quot;Start free&quot;,
  ctaHref: &quot;#pricing&quot;,

  heroBadge: &quot;New: team workspaces&quot;,
  heroTitleLine1: &quot;Ship analytics,&quot;,
  heroTitleLine2: &quot;not dashboards&quot;,
  heroSubtitle: &quot;Turn raw events into answers your whole team can read.&quot;,
  primaryBtnText: &quot;Start free&quot;,
  primaryBtnHref: &quot;#pricing&quot;,

  stats: [
    { value: &quot;12k+&quot;, label: &quot;Teams&quot; },
    { value: &quot;99.9%&quot;, label: &quot;Uptime&quot; }
  ],

  features: [
    { title: &quot;Live metrics&quot;, desc: &quot;Streams update in under a second.&quot;, icon: &quot;⚡&quot; },
    { title: &quot;Shared boards&quot;, desc: &quot;One link for the whole team.&quot;, icon: &quot;📊&quot; }
  ],

  yearlyBadge: &quot;Save 20%&quot;,
  pricing: [
    { tier: &quot;Starter&quot;, amount: &quot;$0&quot;, features: [&quot;3 boards&quot;], cta: &quot;Start free&quot;, href: &quot;#&quot; },
    { tier: &quot;Team&quot;, amount: &quot;$19&quot;, period: &quot;/mo&quot;,
      yearly: { amount: &quot;$182&quot;, period: &quot;/yr&quot; },
      featured: true, badge: &quot;Most popular&quot;,
      features: [&quot;Unlimited boards&quot;, &quot;Priority support&quot;], cta: &quot;Go Team&quot;, href: &quot;#&quot; }
  ],

  faq: [{ q: &quot;Is there a free plan?&quot;, a: &quot;Yes. Starter is free forever.&quot; }],
  cta: { title: &quot;See your data differently&quot;, text: &quot;Create account&quot;, href: &quot;#&quot; },
  footer: { tagline: &quot;Built with devtemDesign.&quot;, copy: &quot;© 2026 Orbit&quot; }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see this running, with a template picker and a mobile preview, on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/examples&quot;&gt;examples page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;templates&quot;&gt;Templates&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Template &lt;/th&gt;&lt;th&gt;Vibe &lt;/th&gt;&lt;th&gt;Accents&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;figsh&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Futuristic glass &lt;/td&gt;&lt;td&gt;Cyan, purple, pink&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cyber&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Neon cyberpunk &lt;/td&gt;&lt;td&gt;Cyan, magenta, red&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;aurora&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Organic nature &lt;/td&gt;&lt;td&gt;Emerald, violet, blue&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;monochrome&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Minimal grayscale &lt;/td&gt;&lt;td&gt;White, silver&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sunset&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Warm dusk &lt;/td&gt;&lt;td&gt;Orange, rose, gold&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ocean&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Cool depth &lt;/td&gt;&lt;td&gt;Sky, teal, indigo&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;paper&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Light mode &lt;/td&gt;&lt;td&gt;Indigo, pink, cyan&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every color in the stylesheet comes from a token, which is why the light template works without a separate stylesheet.&lt;/p&gt;
&lt;h3 id=&quot;runtime-control&quot;&gt;Runtime control&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const page = devtemDesign(&quot;app-root&quot;, config);

page.setTemplate(&quot;sunset&quot;);   // switch theme, no re-render
page.destroy();               // remove listeners, observers and markup

devtemDesign.version;         // &quot;0.1.0&quot;
devtemDesign.templates;       // [&quot;figsh&quot;, &quot;cyber&quot;, &quot;aurora&quot;, ...]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Calling &lt;code&gt;devtemDesign()&lt;/code&gt; again on the same container destroys the previous instance first, so there are no leaked listeners.&lt;/p&gt;
&lt;h3 id=&quot;theme-tokens&quot;&gt;Theme tokens&lt;/h3&gt;
&lt;p&gt;Override any token with the &lt;code&gt;theme&lt;/code&gt; option:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;theme: { accent: &quot;#ff4d6d&quot;, accent2: &quot;#ffb703&quot;, &quot;bg-deep&quot;: &quot;#0b0710&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Token &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--accent&lt;/code&gt;, &lt;code&gt;--accent2&lt;/code&gt;, &lt;code&gt;--accent3&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Brand colors and gradients&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--text&lt;/code&gt;, &lt;code&gt;--muted&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Primary and secondary text&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--bg-deep&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Page background, and text on filled buttons&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--glass-bg&lt;/code&gt;, &lt;code&gt;--glass-border&lt;/code&gt;, &lt;code&gt;--glass-blur&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Card surfaces&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--surface&lt;/code&gt;, &lt;code&gt;--line&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Subtle fills and dividers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--orb&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Ambient orb opacity&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;config-reference&quot;&gt;Config reference&lt;/h3&gt;
&lt;p&gt;All text fields accept HTML, so icons and inline markup work. Only pass content you trust.&lt;/p&gt;
&lt;h4 id=&quot;core-and-navigation&quot;&gt;Core and navigation&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;Default &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;template&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;figsh&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;One of the 7 templates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;theme&lt;/code&gt; &lt;/td&gt;&lt;td&gt;object &lt;/td&gt;&lt;td&gt;&lt;code&gt;{}&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Token overrides&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;themeSwitcher&lt;/code&gt; &lt;/td&gt;&lt;td&gt;boolean or string[] &lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Navbar theme button. An array limits the cycle. Choice is saved in &lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cursor&lt;/code&gt; &lt;/td&gt;&lt;td&gt;boolean &lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Custom cursor on or off&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;logo&lt;/code&gt;, &lt;code&gt;logoName&lt;/code&gt;, &lt;code&gt;logoHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;fscss logo, &lt;code&gt;&quot;CORE&quot;&lt;/code&gt;, &lt;code&gt;&quot;/&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Brand image, name and link&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;navLinks&lt;/code&gt; &lt;/td&gt;&lt;td&gt;(string or &lt;code&gt;{ label, href }&lt;/code&gt;)[] &lt;/td&gt;&lt;td&gt;Features, How it works, Pricing &lt;/td&gt;&lt;td&gt;Navbar and drawer links&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ctaText&lt;/code&gt;, &lt;code&gt;ctaHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;&lt;code&gt;&quot;Get started&quot;&lt;/code&gt;, &lt;code&gt;&quot;#&quot;&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Navbar button&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;hero&quot;&gt;Hero&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroBadge&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Pill above the title&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroTitleLine1&lt;/code&gt;, &lt;code&gt;heroTitleLine2&lt;/code&gt; &lt;/td&gt;&lt;td&gt;H1 lines. The second has an animated gradient&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroSubtitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Paragraph under the H1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;primaryBtnText&lt;/code&gt;, &lt;code&gt;primaryBtnHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Main button&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;secondaryBtnText&lt;/code&gt;, &lt;code&gt;secondaryBtnHref&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Ghost button, rendered only when text is set&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;brands&lt;/code&gt;, &lt;code&gt;brandsSubtitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Hero chips and a seamless marquee that pauses on hover&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;sections&quot;&gt;Sections&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Section &lt;/th&gt;&lt;th&gt;Keys &lt;/th&gt;&lt;th&gt;Anchor&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Stats &lt;/td&gt;&lt;td&gt;&lt;code&gt;stats: [{ value, label }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#stats&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Features &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresLabel&lt;/code&gt;, &lt;code&gt;featuresTitle&lt;/code&gt;, &lt;code&gt;featuresSub&lt;/code&gt;, &lt;code&gt;features: [{ title, desc, icon }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#features&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Steps &lt;/td&gt;&lt;td&gt;&lt;code&gt;stepsLabel&lt;/code&gt;, &lt;code&gt;stepsTitle&lt;/code&gt;, &lt;code&gt;stepsSub&lt;/code&gt;, &lt;code&gt;steps: [{ title, desc }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#how&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Panels &lt;/td&gt;&lt;td&gt;&lt;code&gt;panelsLabel&lt;/code&gt;, &lt;code&gt;panelsTitle&lt;/code&gt;, &lt;code&gt;panelsSub&lt;/code&gt;, &lt;code&gt;panels: [{ tag, title, desc }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pricing &lt;/td&gt;&lt;td&gt;&lt;code&gt;pricingLabel&lt;/code&gt;, &lt;code&gt;pricingTitle&lt;/code&gt;, &lt;code&gt;pricingSub&lt;/code&gt;, &lt;code&gt;yearlyBadge&lt;/code&gt;, &lt;code&gt;pricing: [...]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#pricing&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;FAQ &lt;/td&gt;&lt;td&gt;&lt;code&gt;faqLabel&lt;/code&gt;, &lt;code&gt;faqTitle&lt;/code&gt;, &lt;code&gt;faqSub&lt;/code&gt;, &lt;code&gt;faq: [{ q, a }]&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;#faq&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CTA &lt;/td&gt;&lt;td&gt;&lt;code&gt;cta: { title, sub, text, href }&lt;/code&gt; &lt;/td&gt;&lt;td&gt;none&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Stat values keep their prefix, suffix, commas and decimals, so &lt;code&gt;&quot;$1,200&quot;&lt;/code&gt;, &lt;code&gt;&quot;99.9%&quot;&lt;/code&gt; and &lt;code&gt;&quot;12k+&quot;&lt;/code&gt; all count up correctly.&lt;/p&gt;
&lt;p&gt;A pricing plan looks like this. A Monthly/Yearly toggle appears automatically when any plan has &lt;code&gt;yearly&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{
  tier: &quot;Pro&quot;, amount: &quot;$12&quot;, period: &quot;/mo&quot;, desc: &quot;For product landings.&quot;,
  features: [&quot;Everything in Free&quot;, &quot;Priority support&quot;],
  cta: &quot;Go Pro&quot;, href: &quot;/signup&quot;,
  featured: true, badge: &quot;Most popular&quot;,
  yearly: { amount: &quot;$115&quot;, period: &quot;/yr&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;footer&quot;&gt;Footer&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;footer: {
  tagline: &quot;Declarative UI engine for the modern web.&quot;,
  copy: &quot;© 2026 devtem-dev&quot;,
  columns: [{ heading: &quot;Product&quot;, links: [{ label: &quot;Docs&quot;, href: &quot;/docs&quot; }] }],
  social:  [{ icon: &quot;fab fa-github&quot;, href: &quot;https://github.com/devtem-dev&quot;, label: &quot;GitHub&quot; }],
  legal:   [{ label: &quot;Privacy&quot;, href: &quot;/privacy&quot; }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full schema, with types and defaults, lives at &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/api&quot;&gt;design.devtem.org/api&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;upgrading-from-0-0-2&quot;&gt;Upgrading from 0.0.2&lt;/h3&gt;
&lt;p&gt;Three option names changed to match what the code actually reads:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;0.0.2 docs &lt;/th&gt;&lt;th&gt;Use now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;heroTag&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;heroBadge&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;featuresSectionLabel&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresLabel&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;featuresSectionTitle&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;featuresTitle&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;good-to-know&quot;&gt;Good to know&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Font Awesome is optional. It is only needed for &lt;code&gt;icon&lt;/code&gt; fields and footer social icons. The hamburger, checkmarks and accordions are pure CSS.&lt;/li&gt;
&lt;li&gt;Theming uses &lt;code&gt;color-mix()&lt;/code&gt;, so you need Chrome 111+, Safari 16.2+ or Firefox 113+. Older browsers still render the page but lose some tinted surfaces.&lt;/li&gt;
&lt;li&gt;Rendering replaces the container's contents. Give the engine its own wrapper element.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/docs&quot;&gt;https://design.devtem.org/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Schema reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/api&quot;&gt;https://design.devtem.org/api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Examples: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/examples&quot;&gt;https://design.devtem.org/examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Builder: &lt;a rel=&quot;nofollow&quot; href=&quot;https://design.devtem.org/builder&quot;&gt;https://design.devtem.org/builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/devtemdesign&quot;&gt;https://www.npmjs.com/package/devtemdesign&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/devtem-dev/devtemDesign&quot;&gt;https://github.com/devtem-dev/devtemDesign&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is MIT licensed and open to contributions. If you build something with it, I'd like to see it. Drop a link in the comments, and tell me which section you want next &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32379/devtemdesign-one-config-object-paints-a-whole-landing-page</guid>
<pubDate>Sun, 25 Oct 2026 10:50:19 +0000</pubDate>
</item>
<item>
<title>&quot;'use client' Doesn't Turn Your Whole App Into a Client Component (The Children Trick Nobody Explain</title>
<link>https://coderlegion.com/32265/use-client-doesnt-turn-your-whole-app-into-client-component-children-trick-nobody-explain</link>
<description>&lt;p&gt;You need a theme provider, so you create a file, add &lt;code&gt;'use client'&lt;/code&gt; at the top, and wrap your app in it.&lt;/p&gt;
&lt;p&gt;Then a nagging thought arrives: &quot;I just wrapped my entire layout in a client component. Did I turn the whole app into client-side React?&quot;&lt;/p&gt;
&lt;p&gt;Most developers either panic and avoid providers, or give up and put &lt;code&gt;'use client'&lt;/code&gt; at the top of every file &quot;just to be safe&quot;. Both are the wrong response, and the actual rule is simpler than you expect.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/ThemeProvider.tsx
'use client';

import { createContext, useState } from 'react';

export const ThemeContext = createContext('light');

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme] = useState('light');
  return &amp;lt;ThemeContext value={theme}&amp;gt;{children}&amp;lt;/ThemeContext&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/layout.tsx  (a Server Component)
import { ThemeProvider } from '@/components/ThemeProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;ThemeProvider&amp;gt;{children}&amp;lt;/ThemeProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ThemeProvider&lt;/code&gt; is a Client Component. It wraps every page. So are all the pages client components now?&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;h3 id=&quot;what-use-client-actually-does&quot;&gt;What 'use client' Actually Does&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;'use client'&lt;/code&gt; marks a &lt;strong&gt;boundary&lt;/strong&gt;. It says: &quot;this file, and everything this file &lt;em&gt;imports&lt;/em&gt;, is part of the JavaScript sent to the browser.&quot;&lt;/p&gt;
&lt;p&gt;The key word is &lt;em&gt;imports&lt;/em&gt;. The boundary follows the import graph, not the position in the rendered tree.&lt;/p&gt;
&lt;p&gt;In the layout above, &lt;code&gt;children&lt;/code&gt; is not imported by &lt;code&gt;ThemeProvider&lt;/code&gt;. It is &lt;strong&gt;passed in as a prop&lt;/strong&gt; by the layout, which is a Server Component. The server renders the page first, and hands the finished result to the client component as a slot to fill.&lt;/p&gt;
&lt;p&gt;So your pages stay Server Components. They can still fetch data on the server, use secrets, and ship no JavaScript for their static parts.&lt;/p&gt;
&lt;h3 id=&quot;the-mistake-that-does-change-things&quot;&gt;The Mistake That Does Change Things&lt;/h3&gt;
&lt;p&gt;Now look at this version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/ThemeProvider.tsx
'use client';

import { Dashboard } from './Dashboard'; // imported, not passed in

export function ThemeProvider() {
  // ...
  return &amp;lt;Dashboard /&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here the client file &lt;strong&gt;imports&lt;/strong&gt; &lt;code&gt;Dashboard&lt;/code&gt;, so &lt;code&gt;Dashboard&lt;/code&gt; joins the client bundle, along with everything &lt;em&gt;it&lt;/em&gt; imports. If it was a Server Component that queried your database, it is no longer one. It will either break the build or quietly ship heavy code to every visitor.&lt;/p&gt;
&lt;p&gt;The difference between the two examples is a single word: &lt;strong&gt;import&lt;/strong&gt; versus &lt;strong&gt;prop&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-pattern-pass-server-components-through-as-children&quot;&gt;The Pattern: Pass Server Components Through as Children&lt;/h3&gt;
&lt;p&gt;Any time a client component needs to wrap server content, use a slot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/Modal.tsx
'use client';

import { useState } from 'react';

export function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setOpen(true)}&amp;gt;Show stats&amp;lt;/button&amp;gt;
      {open &amp;amp;&amp;amp; &amp;lt;div role=&quot;dialog&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;}
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/page.tsx  (Server Component)
import { Modal } from '@/components/Modal';
import { Stats } from '@/components/Stats'; // reads the database

export default function Page() {
  return (
    &amp;lt;Modal&amp;gt;
      &amp;lt;Stats /&amp;gt;
    &amp;lt;/Modal&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Stats&lt;/code&gt; runs on the server, even though it is rendered &lt;em&gt;inside&lt;/em&gt; a client component's JSX. The button and the toggle are interactive in the browser. The data never leaves the server except as finished markup.&lt;/p&gt;
&lt;p&gt;This works for any prop that holds JSX, not only &lt;code&gt;children&lt;/code&gt;. A &lt;code&gt;sidebar&lt;/code&gt; prop or a &lt;code&gt;header&lt;/code&gt; prop behaves the same way.&lt;/p&gt;
&lt;h3 id=&quot;rule-of-thumb-push-the-boundary-down&quot;&gt;Rule of Thumb: Push the Boundary Down&lt;/h3&gt;
&lt;p&gt;Put &lt;code&gt;'use client'&lt;/code&gt; on the smallest piece that needs interactivity, not on the page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Bad: the whole page becomes client code
'use client';
export default function ProductPage() { /* ...200 lines... */ }

// Better: only the button is client code
export default function ProductPage() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;ProductDetails /&amp;gt;   {/* server */}
      &amp;lt;AddToCartButton /&amp;gt;  {/* 'use client' lives here */}
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A page that is mostly text and images, with one button, should send JavaScript for the button, not for the page. Less JavaScript sent means a faster load, which is what I walk through in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/core-web-vitals-explained&quot;&gt;this plain-language guide to Core Web Vitals&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;two-things-that-still-bite&quot;&gt;Two Things That Still Bite&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Props crossing the boundary must be serializable.&lt;/strong&gt; Data goes from server to client as text, so you can pass strings, numbers, plain objects, arrays and dates. You cannot pass a regular function. (Server Actions are the exception, because they are designed to cross.)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Error: functions cannot be passed to Client Components
&amp;lt;ClientButton onClick={() =&amp;gt; console.log('hi')} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Define the handler inside the client component instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A server-only module can leak into the client by import.&lt;/strong&gt; If a client file imports a helper that touches your database or secrets, the build either fails or bundles code you never meant to ship. The &lt;code&gt;server-only&lt;/code&gt; package turns that into a loud build error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db.ts
import 'server-only';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import this file from a client component and the build fails, instead of shipping the connection code.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-3&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;'use client'&lt;/code&gt; marks an import boundary, not a rendering zone. Whatever a client file imports goes to the browser. Whatever you pass to it as props or children can stay on the server.&lt;/strong&gt; Keep the directive on small interactive leaves, and use the children slot for everything else.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;'use client'&lt;/code&gt; and look at each file. If it is a page or a layout, ask whether it really needs state or browser APIs. If it is a wrapper, check that it receives its content as &lt;code&gt;children&lt;/code&gt; and does not import it.&lt;/p&gt;
&lt;p&gt;If you want a Next.js starting point built around the App Router, you can browse my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;templates on pixelanas.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Where in your project is &lt;code&gt;'use client'&lt;/code&gt; doing more than it needs to? Drop it in the comments, and tell me if the children trick surprised you.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32265/use-client-doesnt-turn-your-whole-app-into-client-component-children-trick-nobody-explain</guid>
<pubDate>Sat, 24 Oct 2026 11:28:48 +0000</pubDate>
</item>
<item>
<title>I Built Avi.js — A Lightweight TypeScript Framework Inspired by NestJS</title>
<link>https://coderlegion.com/32080/i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs</link>
<description>&lt;h3 id=&quot;i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs&quot;&gt;I Built Avi.js — A Lightweight TypeScript Framework Inspired by NestJS&lt;/h3&gt;
&lt;p&gt;I've been working on an open-source project called &lt;strong&gt;Avi.js&lt;/strong&gt;, and I recently released &lt;strong&gt;v0.1.2&lt;/strong&gt;! 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avi.js — Structure without the overhead.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's a lightweight, TypeScript-first backend framework for Node.js, inspired by the structured development experience of NestJS.&lt;/p&gt;
&lt;p&gt;The idea started with a question:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Can we build a structured TypeScript backend framework with modules, controllers, and dependency injection—without requiring &lt;code&gt;reflect-metadata&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That's what I've been exploring with Avi.js.&lt;/p&gt;
&lt;h3 id=&quot;what-i-ve-built-so-far&quot;&gt;What I've built so far&lt;/h3&gt;
&lt;p&gt;Before getting into the latest update, here's a look at what Avi.js currently supports.&lt;/p&gt;
&lt;h4 id=&quot;1-modules-controllers-and-dependency-injection&quot;&gt;1. Modules, controllers, and dependency injection&lt;/h4&gt;
&lt;p&gt;Avi.js supports a modular architecture with explicit dependency declarations rather than relying on runtime reflection.&lt;/p&gt;
&lt;p&gt;It includes singleton, transient, and request-scoped providers, along with startup checks for missing dependencies and circular references.&lt;/p&gt;
&lt;p&gt;The goal is to keep dependencies visible in code while providing a familiar way to organize backend applications.&lt;/p&gt;
&lt;h4 id=&quot;2-two-api-styles-one-engine&quot;&gt;2. Two API styles, one engine&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Avi.js&lt;/code&gt; supports two ways of defining controllers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Decorator-based API:&lt;/strong&gt; Class-based controllers and modules using standard TC39 decorators.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit API:&lt;/strong&gt; Plain TypeScript objects and functions without decorators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both approaches use the same router, dependency injection container, and request pipeline.&lt;/p&gt;
&lt;p&gt;This gives developers the flexibility to choose the style that fits their project.&lt;/p&gt;
&lt;h4 id=&quot;3-dependency-injection-without-reflect-metadata&quot;&gt;3. Dependency injection without &lt;code&gt;reflect-metadata&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Instead of inferring constructor dependencies through emitted runtime metadata, Avi.js uses explicit declarations such as &lt;code&gt;static inject&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Controller('/users')
class UsersController {
  static inject = [UsersService] as const;

  constructor(private readonly users: UsersService) {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach doesn't require &lt;code&gt;reflect-metadata&lt;/code&gt;, &lt;code&gt;emitDecoratorMetadata&lt;/code&gt;, or legacy experimental decorators.&lt;/p&gt;
&lt;p&gt;It's a design choice focused on making dependency wiring explicit and understandable.&lt;/p&gt;
&lt;h4 id=&quot;4-native-node-js-http&quot;&gt;4. Native Node.js HTTP&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Avi.js&lt;/code&gt; runs on Node.js's native &lt;code&gt;node:http&lt;/code&gt; module without requiring Express or Fastify underneath.&lt;/p&gt;
&lt;p&gt;It supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP routing and route parameters&lt;/li&gt;
&lt;li&gt;Middleware and guards&lt;/li&gt;
&lt;li&gt;Request and response hooks&lt;/li&gt;
&lt;li&gt;JSON, text, and streaming responses&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;5-validation-security-and-developer-tooling&quot;&gt;5. Validation, security, and developer tooling&lt;/h4&gt;
&lt;p&gt;Avi.js also includes several features for building and maintaining backend applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schema validation using Standard Schema-compatible libraries&lt;/li&gt;
&lt;li&gt;CORS and security headers&lt;/li&gt;
&lt;li&gt;OpenAPI 3.1 document generation&lt;/li&gt;
&lt;li&gt;Testing utilities with provider overrides&lt;/li&gt;
&lt;li&gt;CLI scaffolding and code generation&lt;/li&gt;
&lt;li&gt;Startup checks for dependency and route configuration errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features are already available in the current early-development releases.&lt;/p&gt;
&lt;h3 id=&quot;what-s-new-in-v0-1-2-&quot;&gt;What's new in v0.1.2?&lt;/h3&gt;
&lt;p&gt;One feature I'm particularly excited about is &lt;strong&gt;resource generation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can now generate an entire CRUD resource using one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx avi g resource posts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a modular project, the CLI generates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;src/
└── modules/
    └── posts/
        ├── posts.module.ts
        ├── posts.controller.ts
        └── posts.service.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI also registers the resource in your application automatically.&lt;/p&gt;
&lt;p&gt;The generated resource includes working &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; routes backed by an in-memory store, which you can later replace with your own database integration.&lt;/p&gt;
&lt;p&gt;For minimal projects, Avi.js generates services and routes without requiring modules.&lt;/p&gt;
&lt;p&gt;The goal is to reduce repetitive setup while keeping the generated code understandable and editable.&lt;/p&gt;
&lt;h3 id=&quot;getting-started-1&quot;&gt;Getting started&lt;/h3&gt;
&lt;p&gt;You can try Avi.js with &lt;strong&gt;Node.js 22.12 or newer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Create a new project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create avi@latest my-api
cd my-api
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avi.js currently provides two official project templates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modular (default):&lt;/strong&gt; A feature-first structure with modules, controllers, and services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal:&lt;/strong&gt; A smaller, functional structure using explicit routes and services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both templates run on the same framework engine, so you can start small and introduce more structure as your application grows.&lt;/p&gt;
&lt;p&gt;You can also install the core package directly into an existing project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @avijs/core
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-am-i-sharing-this-&quot;&gt;Why am I sharing this?&lt;/h3&gt;
&lt;p&gt;Avi.js is still in &lt;strong&gt;pre-alpha development (0.x)&lt;/strong&gt;, and its APIs may change before 1.0.&lt;/p&gt;
&lt;p&gt;I'm not trying to claim it's faster or better than established frameworks. It's an experiment in building a structured backend framework with explicit dependencies and fewer runtime abstractions.&lt;/p&gt;
&lt;p&gt;I'm sharing it because I'd love to get feedback from developers who enjoy working with Node.js and TypeScript.&lt;/p&gt;
&lt;p&gt;If you're curious, feel free to try it, open an issue, or contribute.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Official website:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://chrischase011.github.io/avi-js/&quot;&gt;https://chrischase011.github.io/avi-js/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/avi-js&quot;&gt;https://github.com/chrischase011/avi-js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latest release:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/avi-js/releases/tag/v0.1.2&quot;&gt;https://github.com/chrischase011/avi-js/releases/tag/v0.1.2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks for checking it out! 🚀&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/typescript&quot;&gt;#typescript&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/nodejs&quot;&gt;#nodejs&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/opensource&quot;&gt;#opensource&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/t/webdev&quot;&gt;#webdev&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32080/i-built-avi-js-a-lightweight-typescript-framework-inspired-by-nestjs</guid>
<pubDate>Thu, 22 Oct 2026 14:41:50 +0000</pubDate>
</item>
<item>
<title>How to Fix Metro Bundler Stuck or Not Starting Errors in React Native</title>
<link>https://coderlegion.com/32069/how-to-fix-metro-bundler-stuck-or-not-starting-errors-in-react-native</link>
<description>&lt;p&gt;If you've worked with React Native for a while, you've probably run into a problem where Metro refuses to start, gets stuck while bundling, or leaves your app waiting for JavaScript that never arrives.&lt;/p&gt;
&lt;p&gt;Sometimes the terminal shows an error immediately. Other times, Metro appears to start normally, but the app stays on a loading screen or fails to connect to the development server.&lt;/p&gt;
&lt;p&gt;The frustrating part is that these symptoms can have completely different causes. A port conflict, stale cache, broken dependency installation, invalid configuration, or network issue can all look like a Metro problem.&lt;/p&gt;
&lt;p&gt;Here's how to narrow it down and fix the actual cause instead of repeatedly restarting your project and hoping something changes.&lt;/p&gt;
&lt;h3 id=&quot;1-identify-what-metro-is-actually-doing&quot;&gt;1. Identify What Metro Is Actually Doing&lt;/h3&gt;
&lt;p&gt;Before changing anything, identify which of these situations matches your problem.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metro won't start:&lt;/strong&gt; The development server exits with an error, refuses to launch, or reports that a port is already in use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metro starts but gets stuck bundling:&lt;/strong&gt; The terminal shows that bundling has started, but it never finishes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The app cannot connect to Metro:&lt;/strong&gt; The server appears to be running, but the application displays a loading screen or a connection error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metro starts, but the app crashes:&lt;/strong&gt; Bundling may be successful, but a JavaScript exception or native configuration problem prevents the app from running.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are different failure points. Clearing the cache might help with a stale bundle, but it won't fix a port occupied by another process or an invalid native configuration.&lt;/p&gt;
&lt;p&gt;Start with the simplest checks and move toward more disruptive fixes only when necessary.&lt;/p&gt;
&lt;h3 id=&quot;2-stop-metro-and-start-it-again&quot;&gt;2. Stop Metro and Start It Again&lt;/h3&gt;
&lt;p&gt;First, stop any Metro server running in the terminal by pressing &lt;code&gt;Ctrl + C&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then start it using the command appropriate for your project.&lt;/p&gt;
&lt;h4 id=&quot;if-you-re-using-expo&quot;&gt;If you're using Expo&lt;/h4&gt;
&lt;p&gt;From your project directory, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx expo start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To clear Expo's Metro cache at the same time, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx expo start --clear
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;if-you-re-using-react-native-cli&quot;&gt;If you're using React Native CLI&lt;/h4&gt;
&lt;p&gt;Start Metro from your project's root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx react-native start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you suspect stale cached files, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx react-native start --reset-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You don't need to clear the cache every time Metro fails. Start with the normal command. Use the cache-reset option when the symptoms suggest stale or inconsistent cached data, or when a normal restart doesn't help.&lt;/p&gt;
&lt;p&gt;If Metro exits immediately, read the terminal output before trying another command. The first meaningful error is often more useful than the final stack trace.&lt;/p&gt;
&lt;h3 id=&quot;3-check-whether-port-8081-is-already-in-use&quot;&gt;3. Check Whether Port 8081 Is Already in Use&lt;/h3&gt;
&lt;p&gt;Metro normally uses port &lt;code&gt;8081&lt;/code&gt;. If another process is listening on that port, Metro may fail to start or ask you to use a different one.&lt;/p&gt;
&lt;p&gt;The terminal might show a message similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: listen EADDRINUSE: address already in use :::8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means another process is already using the requested address and port.&lt;/p&gt;
&lt;h4 id=&quot;on-windows&quot;&gt;On Windows&lt;/h4&gt;
&lt;p&gt;Open Command Prompt and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;netstat -ano | findstr :8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for a line containing &lt;code&gt;LISTENING&lt;/code&gt;. The last column contains the process ID (PID).&lt;/p&gt;
&lt;p&gt;You can identify the process with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;tasklist /FI &quot;PID eq 12345&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;12345&lt;/code&gt; with the actual PID.&lt;/p&gt;
&lt;p&gt;If you've confirmed that the process is an old Metro server you no longer need, you can stop it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmd&quot;&gt;taskkill /PID 12345 /F
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;on-macos-or-linux&quot;&gt;On macOS or Linux&lt;/h4&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;lsof -i :8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inspect the process listed for port &lt;code&gt;8081&lt;/code&gt;. If it's an old Metro process that you no longer need, stop that specific process:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kill &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;&amp;lt;PID&amp;gt;&lt;/code&gt; with the actual process ID.&lt;/p&gt;
&lt;p&gt;Don't terminate a process simply because it's using the port. Confirm what it is first, especially if other development tools are running.&lt;/p&gt;
&lt;p&gt;Once the port is available, start Metro again.&lt;/p&gt;
&lt;h3 id=&quot;4-clear-metro-s-cache-when-bundling-gets-stuck&quot;&gt;4. Clear Metro's Cache When Bundling Gets Stuck&lt;/h3&gt;
&lt;p&gt;Metro transforms your JavaScript files and their dependencies into a bundle the application can execute. Cached transformation results help make subsequent builds faster, but stale or inconsistent cached data can sometimes cause confusing behavior.&lt;/p&gt;
&lt;p&gt;Typical symptoms include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bundling worked before, but now fails after code or dependency changes.&lt;/li&gt;
&lt;li&gt;The app appears to use an older version of your JavaScript.&lt;/li&gt;
&lt;li&gt;Metro repeatedly fails while transforming files without an obvious dependency error.&lt;/li&gt;
&lt;li&gt;A normal restart doesn't resolve the problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Try the appropriate cache-reset command from Step 2.&lt;/p&gt;
&lt;p&gt;After the cache is cleared, let Metro rebuild the bundle. The first build can take longer because the cached transformation results need to be regenerated.&lt;/p&gt;
&lt;p&gt;If the same error returns, don't keep clearing the cache. Look at the specific file, dependency, or configuration mentioned in the terminal output. A repeatable error usually means there's something more specific to fix.&lt;/p&gt;
&lt;h3 id=&quot;5-check-your-dependencies-before-reinstalling-everything&quot;&gt;5. Check Your Dependencies Before Reinstalling Everything&lt;/h3&gt;
&lt;p&gt;Metro depends on the packages and configuration in your React Native project. If a package installation was interrupted, dependencies were changed, or incompatible versions were introduced, Metro may fail while loading its configuration or resolving modules.&lt;/p&gt;
&lt;p&gt;Start by checking the actual error.&lt;/p&gt;
&lt;p&gt;For example, these messages point toward different problems:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot find module ...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Unable to resolve module ...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot find module 'metro-config'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A missing module doesn't automatically mean the entire dependency installation is broken. It might be an incorrect import path, a package that was never installed, or a version mismatch.&lt;/p&gt;
&lt;p&gt;Check the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Did the problem start immediately after installing, removing, or updating a package?&lt;/li&gt;
&lt;li&gt;Does the error identify a particular module or package?&lt;/li&gt;
&lt;li&gt;Does your project use the package versions expected by its React Native or Expo version?&lt;/li&gt;
&lt;li&gt;Did you accidentally run the installation command from the wrong directory?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the project uses npm and has a valid, committed &lt;code&gt;package-lock.json&lt;/code&gt;, a clean installation may be appropriate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm ci
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reinstalls dependencies according to the lockfile and removes the existing &lt;code&gt;node_modules&lt;/code&gt; directory as part of the process.&lt;/p&gt;
&lt;p&gt;For a Yarn project, use the installation command appropriate for its Yarn version and existing lockfile. Don't switch package managers or delete your lockfile just to see whether Metro starts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid deleting &lt;code&gt;node_modules&lt;/code&gt;, lockfiles, and caches all at once as your first response.&lt;/strong&gt; Doing so can introduce new version differences and make it harder to identify what originally caused the failure.&lt;/p&gt;
&lt;h3 id=&quot;6-check-metro-and-expo-configuration&quot;&gt;6. Check Metro and Expo Configuration&lt;/h3&gt;
&lt;p&gt;If Metro reports an error while loading its configuration, clearing the cache or reinstalling dependencies might not help.&lt;/p&gt;
&lt;p&gt;Look for changes to files such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;metro.config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;babel.config.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Expo app configuration, such as &lt;code&gt;app.json&lt;/code&gt; or &lt;code&gt;app.config.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pay particular attention to recent changes involving custom Metro configuration, Babel plugins, aliases, monorepo settings, or package versions.&lt;/p&gt;
&lt;p&gt;For example, if Metro fails while loading &lt;code&gt;metro.config.js&lt;/code&gt;, inspect the configuration and the package named in the error. A missing import or incompatible configuration dependency can prevent the server from starting at all.&lt;/p&gt;
&lt;p&gt;If the problem appeared after a configuration change, compare that change with the last working version. Reverting one recent change is often more informative than rebuilding the entire environment.&lt;/p&gt;
&lt;p&gt;For Expo projects, also make sure you're following the configuration requirements for your installed Expo SDK rather than copying a Metro configuration from an unrelated project or older tutorial.&lt;/p&gt;
&lt;h3 id=&quot;7-if-metro-runs-but-the-app-cannot-connect&quot;&gt;7. If Metro Runs but the App Cannot Connect&lt;/h3&gt;
&lt;p&gt;A running Metro server doesn't guarantee that your app can reach it.&lt;/p&gt;
&lt;p&gt;This distinction matters because repeatedly resetting Metro's cache won't fix a connection problem.&lt;/p&gt;
&lt;h4 id=&quot;check-the-development-server-url&quot;&gt;Check the development-server URL&lt;/h4&gt;
&lt;p&gt;When Metro starts, inspect the URL or connection information it prints in the terminal.&lt;/p&gt;
&lt;p&gt;If you're using a physical device, make sure the device and computer can communicate over the selected network. VPNs, firewalls, guest Wi-Fi networks, and network isolation can interfere with local development connections.&lt;/p&gt;
&lt;h4 id=&quot;if-you-re-using-expo-1&quot;&gt;If you're using Expo&lt;/h4&gt;
&lt;p&gt;Expo supports different connection methods depending on your environment and configuration. If the normal connection method doesn't work, try the tunnel option:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx expo start --tunnel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A tunnel can help when the device cannot reach your computer directly over the local network, although it may be slower and depends on the tunnel service being available.&lt;/p&gt;
&lt;h4 id=&quot;if-you-re-using-an-android-emulator&quot;&gt;If you're using an Android emulator&lt;/h4&gt;
&lt;p&gt;Check that the emulator is running and that the development server is reachable. For a React Native CLI workflow using the standard Android emulator setup, you can forward port &lt;code&gt;8081&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;adb reverse tcp:8081 tcp:8081
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful when the emulator needs a route to Metro through Android Debug Bridge. It isn't a universal fix for every device or connection setup.&lt;/p&gt;
&lt;p&gt;If the app can connect but crashes after loading the bundle, move on to the JavaScript or native error shown by the app. At that point, Metro itself might be working correctly.&lt;/p&gt;
&lt;h3 id=&quot;8-don-t-confuse-a-javascript-error-with-a-metro-startup-failure&quot;&gt;8. Don't Confuse a JavaScript Error With a Metro Startup Failure&lt;/h3&gt;
&lt;p&gt;Sometimes developers blame Metro because the application stops working immediately after a successful bundle.&lt;/p&gt;
&lt;p&gt;But these messages describe different problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metro startup error:&lt;/strong&gt; The development server cannot initialize.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Module resolution error:&lt;/strong&gt; Metro cannot find or resolve a requested module.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JavaScript runtime exception:&lt;/strong&gt; The bundle loads, but the application encounters an error while executing code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native build error:&lt;/strong&gt; The Android or iOS application fails during native compilation or configuration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, if Metro reports that it cannot resolve a module imported by &lt;code&gt;App.tsx&lt;/code&gt;, investigate the import path, filename, package installation, and any configured aliases.&lt;/p&gt;
&lt;p&gt;If the Android build fails before the application starts, inspect the native build output instead of treating every failure as a Metro cache problem.&lt;/p&gt;
&lt;p&gt;Finding the correct failure point saves time because each category requires a different solution.&lt;/p&gt;
&lt;h3 id=&quot;9-when-none-of-these-fixes-works&quot;&gt;9. When None of These Fixes Works&lt;/h3&gt;
&lt;p&gt;If Metro still refuses to start, collect the evidence before making more changes.&lt;/p&gt;
&lt;p&gt;A useful troubleshooting report should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The exact command used to start Metro.&lt;/li&gt;
&lt;li&gt;The full terminal error, especially the first meaningful error before the stack trace.&lt;/li&gt;
&lt;li&gt;Your operating system.&lt;/li&gt;
&lt;li&gt;Whether the project uses Expo or React Native CLI.&lt;/li&gt;
&lt;li&gt;The relevant React Native and Expo versions.&lt;/li&gt;
&lt;li&gt;Any recent dependency or configuration changes.&lt;/li&gt;
&lt;li&gt;Whether Metro fails to start, gets stuck bundling, or starts while the app cannot connect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Remove access tokens, passwords, private URLs, and other sensitive information before sharing logs publicly.&lt;/p&gt;
&lt;p&gt;If the output contains several errors, don't assume the final message is the root cause. A failure to load one configuration dependency can trigger multiple follow-up errors.&lt;/p&gt;
&lt;p&gt;This is also where a diagnostic tool can help. If you've already tried the obvious fixes and the logs are difficult to interpret, you can paste the relevant error and technical context into &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fixmyerrorapp.com&quot;&gt;FixMyError&lt;/a&gt;. It can help analyze the failure, identify likely causes, and suggest targeted next steps instead of making you cycle through unrelated fixes.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;When Metro Bundler gets stuck or refuses to start, the best approach is to identify where the failure occurs before changing your project.&lt;/p&gt;
&lt;p&gt;Start with a normal restart, check port &lt;code&gt;8081&lt;/code&gt;, clear the cache when appropriate, and inspect dependency or configuration errors when they appear. If Metro is running but the app cannot connect, troubleshoot the connection rather than repeatedly resetting the bundler.&lt;/p&gt;
&lt;p&gt;The goal isn't to run every possible fix. It's to find the specific reason your development environment stopped working and make the smallest change that resolves it.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/32069/how-to-fix-metro-bundler-stuck-or-not-starting-errors-in-react-native</guid>
<pubDate>Thu, 22 Oct 2026 12:02:08 +0000</pubDate>
</item>
<item>
<title>Your Next.js Dates Are Wrong for Half Your Users (and the Hydration Warning Is Only the Start)</title>
<link>https://coderlegion.com/31957/your-next-js-dates-are-wrong-for-half-your-users-and-the-hydration-warning-is-only-the-start</link>
<description>&lt;p&gt;You format a date in a component. It looks right on your laptop. Then a user in another country opens the page and either sees a console full of hydration errors, or worse, sees nothing wrong at all and reads the wrong time.&lt;/p&gt;
&lt;p&gt;I live in a timezone that is five hours away from where most servers run, so this one finds me every time.&lt;/p&gt;
&lt;h3 id=&quot;the-code-that-looks-fine&quot;&gt;The Code That Looks Fine&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/OrderDate.tsx
export function OrderDate({ date }: { date: string }) {
  return &amp;lt;p&amp;gt;Ordered on {new Date(date).toLocaleString()}&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No hooks, no browser APIs, nothing that looks risky. It even works in &lt;code&gt;next dev&lt;/code&gt;, because on your machine the server and the browser are the same computer.&lt;/p&gt;
&lt;h3 id=&quot;why-it-breaks-1&quot;&gt;Why It Breaks&lt;/h3&gt;
&lt;p&gt;In Next.js, this component runs &lt;strong&gt;twice&lt;/strong&gt;: once on the server to produce the HTML, and again in the browser to hydrate it. &lt;code&gt;toLocaleString()&lt;/code&gt; with no arguments uses the &lt;strong&gt;timezone and language of whatever machine runs it&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server is often in UTC, with a default locale such as &lt;code&gt;en-US&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Your user's browser might be in Karachi, with &lt;code&gt;en-GB&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Server HTML:   Ordered on 10/20/2026, 10:15:00 AM
Browser render: Ordered on 20/10/2026, 15:15:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React compares the two, finds that they differ, and logs a hydration error. In development, you see it. In production, React quietly tries to recover by re-rendering on the client, which costs time and can cause a visible flash.&lt;/p&gt;
&lt;h3 id=&quot;why-it-is-hard-to-catch-1&quot;&gt;Why It Is Hard to Catch&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It never fails where you test.&lt;/strong&gt; Same machine, same timezone, same locale: the two outputs are identical.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Many users never see an error.&lt;/strong&gt; The page just shows one format for a split second and then another.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It often hides inside other pieces of data.&lt;/strong&gt; &lt;code&gt;Date.now()&lt;/code&gt;, &lt;code&gt;Math.random()&lt;/code&gt;, &lt;code&gt;new Date().getFullYear()&lt;/code&gt; and anything that reads &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;localStorage&lt;/code&gt; can all produce a different value on the server and in the browser.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-fix-that-is-tempting-and-wrong&quot;&gt;The Fix That Is Tempting, and Wrong&lt;/h3&gt;
&lt;p&gt;You will find this advice everywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;p suppressHydrationWarning&amp;gt;
  Ordered on {new Date(date).toLocaleString()}
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does silence the warning. But per React's documentation, it only suppresses the warning on that one element and does not patch up mismatched text. The user can end up looking at the &lt;strong&gt;server's&lt;/strong&gt; version of the time, which is in the wrong timezone.&lt;/p&gt;
&lt;p&gt;You did not fix the bug, you removed the alarm for it.&lt;/p&gt;
&lt;h3 id=&quot;fix-1-make-the-output-the-same-on-both-sides&quot;&gt;Fix 1: Make the Output the Same on Both Sides&lt;/h3&gt;
&lt;p&gt;Pass an explicit locale and timezone, so the server and the browser calculate the same string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function OrderDate({ date }: { date: string }) {
  const text = new Date(date).toLocaleString('en-US', {
    timeZone: 'UTC',
    dateStyle: 'medium',
    timeStyle: 'short',
  });

  return &amp;lt;time dateTime={date}&amp;gt;{text} UTC&amp;lt;/time&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is deterministic, so there is nothing to mismatch. It is the right choice for logs, admin tables and anything where showing UTC is acceptable. Label it as UTC so nobody reads it as local time.&lt;/p&gt;
&lt;h3 id=&quot;fix-2-show-the-local-time-after-the-page-loads&quot;&gt;Fix 2: Show the Local Time After the Page Loads&lt;/h3&gt;
&lt;p&gt;If users should see their own timezone, only compute it in the browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';

import { useEffect, useState } from 'react';

export function LocalDate({ date }: { date: string }) {
  const [local, setLocal] = useState&amp;lt;string | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    setLocal(
      new Date(date).toLocaleString(undefined, {
        dateStyle: 'medium',
        timeStyle: 'short',
      })
    );
  }, [date]);

  return (
    &amp;lt;time dateTime={date}&amp;gt;
      {local ?? new Date(date).toISOString().slice(0, 10)}
    &amp;lt;/time&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first render, on the server and in the browser, shows a stable fallback (the ISO date). Right after hydration, &lt;code&gt;useEffect&lt;/code&gt; runs only in the browser and swaps in the local time. Nothing mismatches, and the user ends up with the right value.&lt;/p&gt;
&lt;p&gt;The cost is a brief change from the fallback to the local string. Keep the fallback short so the layout does not jump.&lt;/p&gt;
&lt;h3 id=&quot;fix-3-store-and-send-dates-as-iso-strings&quot;&gt;Fix 3: Store and Send Dates as ISO Strings&lt;/h3&gt;
&lt;p&gt;Whichever display option you pick, pass dates around as ISO strings (&lt;code&gt;2026-10-20T10:15:00.000Z&lt;/code&gt;) and format them only at the edge, in one place. An ISO string is plain text, so it means the same thing on the server, in the browser and in your database.&lt;/p&gt;
&lt;h3 id=&quot;which-one-when&quot;&gt;Which One When&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Admin tables, logs, reports &lt;/td&gt;&lt;td&gt;Fixed locale and &lt;code&gt;timeZone: 'UTC'&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Customer-facing times (orders, bookings, messages) &lt;/td&gt;&lt;td&gt;Format in the browser after mount&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A date that never changes (blog post date) &lt;/td&gt;&lt;td&gt;Fixed locale and timezone, since the author's date is what matters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;the-rule-4&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Any value that depends on where the code runs, such as timezone, locale, the current time or a random number, will differ between the server and the browser.&lt;/strong&gt; Either make it identical on both sides, or compute it only in the browser after hydration. Do not suppress the warning and hope.&lt;/p&gt;
&lt;p&gt;Quick audit: search your project for &lt;code&gt;toLocaleString(&lt;/code&gt;, &lt;code&gt;toLocaleDateString(&lt;/code&gt;, &lt;code&gt;Date.now()&lt;/code&gt; and &lt;code&gt;Math.random()&lt;/code&gt; inside components that render on the server. Each one is either deterministic on purpose, or a mismatch that has not met a user in another timezone yet.&lt;/p&gt;
&lt;p&gt;Hydration mistakes also slow pages down, because React has to redo work in the browser. I explain what that does to real-world speed scores in &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com/blog/core-web-vitals-explained&quot;&gt;this plain-language guide to Core Web Vitals&lt;/a&gt;, and I keep every date in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;Next.js templates&lt;/a&gt; formatted through one small helper so the server and the browser always agree.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Which timezone bug has bitten you hardest? Drop it in the comments, especially if it only showed up in production.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31957/your-next-js-dates-are-wrong-for-half-your-users-and-the-hydration-warning-is-only-the-start</guid>
<pubDate>Wed, 21 Oct 2026 11:48:27 +0000</pubDate>
</item>
<item>
<title>I built a blog engine with no build step — one JSON file, a noscript list, and a gated RSS feed</title>
<link>https://coderlegion.com/31950/i-built-a-blog-engine-with-no-build-step-one-json-file-a-noscript-list-and-a-gated-rss-feed</link>
<description>&lt;p&gt;I run NitroIDE, a free browser IDE where your code never leaves the tab — no signup, no cloud backend, no waiting for a container to boot. Last week I rebuilt its blog engine, and the whole thing runs on one JSON file.&lt;/p&gt;
&lt;p&gt;No static site generator. No build step. blog-data.json holds every post, and a small vanilla script renders the list, the article pages, and the search index. If JavaScript is off, readers still get a full noscript list of every article. The RSS feed is gated so it only serves valid items — one bad entity used to break the whole feed, and that taught me to validate before shipping.&lt;/p&gt;
&lt;p&gt;Why this matters: most blog setups I tried wanted a framework, a pipeline, or a CMS. For a small indie site, that is overhead I do not need. One JSON file, one script, and the blog is fast, private, and mine.&lt;/p&gt;
&lt;p&gt;The part I am proudest of: the site works fully offline once loaded, and the blog is no exception. Try NitroIDE free at &lt;a rel=&quot;nofollow&quot; href=&quot;https://nitroide.com/?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-21&quot;&gt;https://nitroide.com/?utm_source=coderlegion&amp;amp;utm_medium=article&amp;amp;utm_campaign=daily-2026-10-21&lt;/a&gt; — no account, just code.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31950/i-built-a-blog-engine-with-no-build-step-one-json-file-a-noscript-list-and-a-gated-rss-feed</guid>
<pubDate>Wed, 21 Oct 2026 09:34:26 +0000</pubDate>
</item>
<item>
<title>How to Scrape Threads with Node.js: Profiles, Posts, Replies, and an Engagement Tracker (2026)</title>
<link>https://coderlegion.com/31945/how-to-scrape-threads-with-node-js-profiles-posts-replies-and-an-engagement-tracker-2026</link>
<description>&lt;p&gt;Threads is an awkward target. The web app is a React client from Meta, most of the interesting data loads through internal GraphQL calls, and a logged-out visitor sees only a slice of what a logged-in one does. Still, a lot of public data ships inside the first HTML response, and for a modest job like &quot;track likes and replies on one account's recent posts&quot; that slice is enough.&lt;/p&gt;
&lt;p&gt;This tutorial builds a small threads scraper in Node.js 18+ with nothing but native &lt;code&gt;fetch&lt;/code&gt;. It reads a public profile, pulls its recent posts with like, reply, repost, and quote counts, and appends a timestamped snapshot to a JSON Lines file every time it runs. At the end there's a short report script that shows which posts gained the most engagement between runs.&lt;/p&gt;
&lt;p&gt;I'll also be upfront about where this breaks, because on Threads it breaks often.&lt;/p&gt;
&lt;h3 id=&quot;what-a-logged-out-visitor-can-reach&quot;&gt;What a logged-out visitor can reach&lt;/h3&gt;
&lt;p&gt;Short version, as of writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Profile pages&lt;/strong&gt; (&lt;code&gt;https://www.threads.com/@handle&lt;/code&gt;): name, bio, follower count, verification, and the first batch of recent posts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single post pages&lt;/strong&gt; (&lt;code&gt;https://www.threads.com/@handle/post/&amp;lt;code&amp;gt;&lt;/code&gt;): the post itself and usually a first batch of replies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search, feeds, follower lists, &quot;load more&quot;&lt;/strong&gt;: these either show a login prompt or depend on GraphQL calls that need session tokens. Treat them as off-limits for a no-login scraper.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Threads moved its main domain from &lt;code&gt;threads.net&lt;/code&gt; to &lt;code&gt;threads.com&lt;/code&gt; in 2025. Old &lt;code&gt;threads.net&lt;/code&gt; links redirect, so let &lt;code&gt;fetch&lt;/code&gt; follow redirects and normalize URLs to &lt;code&gt;threads.com&lt;/code&gt; in your own storage so the same post doesn't get two keys.&lt;/p&gt;
&lt;h3 id=&quot;where-the-data-actually-lives&quot;&gt;Where the data actually lives&lt;/h3&gt;
&lt;p&gt;If you open the page source of a profile, you won't find the posts as readable HTML. What you will find are many &lt;code&gt;&amp;lt;script type=&quot;application/json&quot;&amp;gt;&lt;/code&gt; tags, several marked with a &lt;code&gt;data-sjs&lt;/code&gt; attribute. These are server-side preloaded data for Meta's Relay client, and some of them contain the same post objects the app renders: &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;taken_at&lt;/code&gt;, &lt;code&gt;like_count&lt;/code&gt;, &lt;code&gt;caption.text&lt;/code&gt;, and a &lt;code&gt;text_post_app_info&lt;/code&gt; object with &lt;code&gt;direct_reply_count&lt;/code&gt;, &lt;code&gt;repost_count&lt;/code&gt;, and &lt;code&gt;quote_count&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The structure is deeply nested and changes without notice, so don't hard-code a path like &lt;code&gt;result.data.mediaData.edges[0]...&lt;/code&gt;. A sturdier approach is to parse every JSON script and walk the whole tree looking for anything that &lt;em&gt;looks&lt;/em&gt; like a post. That costs a few milliseconds and survives most reshuffles.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir threads-scraper &amp;amp;&amp;amp; cd threads-scraper
npm init -y
npm pkg set type=module
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No dependencies. Node 18 or newer gives you global &lt;code&gt;fetch&lt;/code&gt;, and everything else comes from the standard library.&lt;/p&gt;
&lt;h3 id=&quot;step-1-fetch-a-page-politely&quot;&gt;Step 1: Fetch a page politely&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// fetchPage.js
const UA =
  &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 &quot; +
  &quot;(KHTML, like Gecko) Chrome/129.0 Safari/537.36&quot;;

const sleep = (ms) =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, ms));

export async function fetchPage(url, { retries = 3 } = {}) {
  for (let attempt = 0; attempt &amp;lt;= retries; attempt++) {
    const res = await fetch(url, {
      headers: {
        &quot;user-agent&quot;: UA,
        &quot;accept&quot;: &quot;text/html,application/xhtml+xml&quot;,
        &quot;accept-language&quot;: &quot;en-US,en;q=0.9&quot;,
      },
      redirect: &quot;follow&quot;,
    });

    if (res.ok) {
      const html = await res.text();
      // A login redirect sometimes comes back as 200 with a tiny shell page.
      if (res.url.includes(&quot;/login&quot;) || html.length &amp;lt; 20_000) {
        throw new Error(`Looks like a login wall for ${url} (final URL ${res.url})`);
      }
      return html;
    }

    if (res.status === 429 || res.status &amp;gt;= 500) {
      const wait = 2 ** attempt * 5_000 + Math.random() * 2_000;
      console.warn(`HTTP ${res.status} on ${url}, retrying in ${Math.round(wait / 1000)}s`);
      await sleep(wait);
      continue;
    }
    throw new Error(`HTTP ${res.status} for ${url}`);
  }
  throw new Error(`Gave up on ${url} after ${retries + 1} attempts`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The 20,000-character threshold is a heuristic, not a documented rule. A real profile page is far larger than that, while the shell you get when Meta wants you to sign in is much smaller. Tune it if you see false positives.&lt;/p&gt;
&lt;h3 id=&quot;step-2-pull-the-embedded-json-out-of-the-html&quot;&gt;Step 2: Pull the embedded JSON out of the HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// extract.js
const SCRIPT_RE = /&amp;lt;script type=&quot;application\/json&quot;[^&amp;gt;]*&amp;gt;([\s\S]*?)&amp;lt;\/script&amp;gt;/g;

export function jsonBlobs(html) {
  const blobs = [];
  for (const m of html.matchAll(SCRIPT_RE)) {
    try {
      blobs.push(JSON.parse(m[1]));
    } catch {
      // Some blobs are not valid standalone JSON; skip them.
    }
  }
  return blobs;
}

// Walk any JSON tree and collect objects that look like Threads posts.
export function findPosts(root) {
  const found = new Map();
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    if (!node || typeof node !== &quot;object&quot;) continue;

    if (
      typeof node.code === &quot;string&quot; &amp;amp;&amp;amp;
      typeof node.taken_at === &quot;number&quot; &amp;amp;&amp;amp;
      &quot;like_count&quot; in node &amp;amp;&amp;amp;
      node.user?.username
    ) {
      found.set(node.code, node);
    }
    for (const v of Array.isArray(node) ? node : Object.values(node)) {
      if (v &amp;amp;&amp;amp; typeof v === &quot;object&quot;) stack.push(v);
    }
  }
  return [...found.values()];
}

// Same idea for the profile header.
export function findProfile(root, handle) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    if (!node || typeof node !== &quot;object&quot;) continue;
    if (node.username === handle &amp;amp;&amp;amp; &quot;follower_count&quot; in node) return node;
    for (const v of Array.isArray(node) ? node : Object.values(node)) {
      if (v &amp;amp;&amp;amp; typeof v === &quot;object&quot;) stack.push(v);
    }
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Map&lt;/code&gt; keyed by &lt;code&gt;code&lt;/code&gt; matters: the same post often appears in more than one blob, and you don't want to count it twice. Using an explicit stack instead of recursion avoids blowing the call stack on Meta's very deep payloads.&lt;/p&gt;
&lt;h3 id=&quot;step-3-normalize-a-post&quot;&gt;Step 3: Normalize a post&lt;/h3&gt;
&lt;p&gt;Raw post objects carry dozens of fields you don't need. Flatten them into something you'd actually store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// normalize.js
export function normalizePost(p) {
  const info = p.text_post_app_info ?? {};
  return {
    code: p.code,
    url: `https://www.threads.com/@${p.user.username}/post/${p.code}`,
    author: p.user.username,
    postedAt: new Date(p.taken_at * 1000).toISOString(),
    text: p.caption?.text ?? &quot;&quot;,
    likes: p.like_count ?? null,
    replies: info.direct_reply_count ?? null,
    reposts: info.repost_count ?? null,
    quotes: info.quote_count ?? null,
    isReply: Boolean(info.is_reply),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two caveats. First, authors can hide like counts, so &lt;code&gt;like_count&lt;/code&gt; may be missing or zero even on a popular post; store &lt;code&gt;null&lt;/code&gt; rather than guessing. Second, the profile page mixes in the author's own continuation posts (a multi-part thread shows up as a post plus replies to itself). &lt;code&gt;isReply&lt;/code&gt; lets you filter those out if you only want top-level posts.&lt;/p&gt;
&lt;h3 id=&quot;step-4-the-threads-scraper-core-a-profile-&quot;&gt;Step 4: The threads scraper core (a profile)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// profile.js
import { fetchPage } from &quot;./fetchPage.js&quot;;
import { jsonBlobs, findPosts, findProfile } from &quot;./extract.js&quot;;
import { normalizePost } from &quot;./normalize.js&quot;;

export async function scrapeProfile(handle) {
  handle = handle.replace(/^@/, &quot;&quot;);
  const html = await fetchPage(`https://www.threads.com/@${handle}`);
  const blobs = jsonBlobs(html);

  let profile = null;
  const posts = new Map();
  for (const blob of blobs) {
    profile ??= findProfile(blob, handle);
    for (const p of findPosts(blob)) {
      if (p.user.username === handle) posts.set(p.code, normalizePost(p));
    }
  }

  return {
    handle,
    followers: profile?.follower_count ?? null,
    verified: profile?.is_verified ?? null,
    bio: profile?.biography ?? null,
    posts: [...posts.values()].sort((a, b) =&amp;gt; b.postedAt.localeCompare(a.postedAt)),
  };
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const data = await scrapeProfile(process.argv[2] ?? &quot;zuck&quot;);
  console.log(data.followers, data.posts.length);
  console.table(data.posts.slice(0, 5).map(({ code, likes, replies, reposts }) =&amp;gt; ({ code, likes, replies, reposts })));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;node profile.js someuser&lt;/code&gt;. Expect a first page of posts, roughly what you'd see before scrolling. If you get zero posts but a valid profile, Meta has probably changed where the posts are preloaded, or this request was served a stripped page. Save the HTML to disk and search it for a post code you know exists; that tells you quickly which case you're in.&lt;/p&gt;
&lt;h3 id=&quot;step-5-replies-on-a-single-post&quot;&gt;Step 5: Replies on a single post&lt;/h3&gt;
&lt;p&gt;The post permalink page works the same way. The main post plus the first batch of replies are usually in the preloaded JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// post.js
import { fetchPage } from &quot;./fetchPage.js&quot;;
import { jsonBlobs, findPosts } from &quot;./extract.js&quot;;
import { normalizePost } from &quot;./normalize.js&quot;;

export async function scrapePost(url) {
  const clean = url.replace(&quot;threads.net&quot;, &quot;threads.com&quot;).split(/[?#]/)[0];
  const code = clean.match(/\/post\/([^/]+)/)?.[1];
  if (!code) throw new Error(`Not a Threads post URL: ${url}`);

  const all = new Map();
  for (const blob of jsonBlobs(await fetchPage(clean))) {
    for (const p of findPosts(blob)) all.set(p.code, normalizePost(p));
  }
  const post = all.get(code) ?? null;
  all.delete(code);
  return { post, replies: [...all.values()] };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;Replies&quot; here means everything else on the page that looks like a post. On some pages that includes the parent post (if you opened a reply) or related posts. If you need strict parent/child relationships, inspect the surrounding &lt;code&gt;thread_items&lt;/code&gt; structure in the blob rather than relying on the flat list.&lt;/p&gt;
&lt;h3 id=&quot;why-not-just-call-the-graphql-api-&quot;&gt;Why not just call the GraphQL API?&lt;/h3&gt;
&lt;p&gt;You'll see older tutorials that POST to &lt;code&gt;https://www.threads.com/api/graphql&lt;/code&gt; with a &lt;code&gt;doc_id&lt;/code&gt; and an &lt;code&gt;lsd&lt;/code&gt; token copied from the page. It can work for a while, but plan for three problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;doc_id&lt;/code&gt; churn.&lt;/strong&gt; Each query is identified by a numeric document ID that changes when Meta ships a new client build. Hard-coded IDs go stale, sometimes within weeks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token coupling.&lt;/strong&gt; The &lt;code&gt;lsd&lt;/code&gt; token and related headers are tied to the page you loaded. You end up scraping the HTML anyway just to call GraphQL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination is where login walls live.&lt;/strong&gt; &quot;Load more posts&quot; and deep reply threads are exactly the calls that tend to need a session. Using a logged-in account for scraping risks that account, and it's clearly against Meta's terms.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a no-login threads scraper, the embedded JSON is the more stable source, even though you only get the first page.&lt;/p&gt;
&lt;h3 id=&quot;rate-limits-ip-blocks-and-legal-notes&quot;&gt;Rate limits, IP blocks, and legal notes&lt;/h3&gt;
&lt;p&gt;Meta doesn't publish limits for the public web pages. From what people commonly report, a residential IP fetching a few pages a minute is fine, while datacenter IPs get challenged or redirected to login much sooner. I'd stick to these rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One request every few seconds,&lt;/strong&gt; with random jitter, and back off on 429.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache HTML&lt;/strong&gt; during development so you're not hitting Threads every time you tweak a parser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch for soft blocks.&lt;/strong&gt; A 200 with a login shell is the usual symptom, not a clean 403.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the legal side: Meta's terms prohibit automated collection without permission, and courts in different countries have treated public-data scraping differently. Only collect public data, don't collect more personal data than your use case needs, respect GDPR if you store data about EU users, and talk to a lawyer before building anything commercial. This post is a tutorial, not legal advice.&lt;/p&gt;
&lt;h3 id=&quot;the-official-threads-api&quot;&gt;The official Threads API&lt;/h3&gt;
&lt;p&gt;Meta has an official Threads API (&lt;code&gt;graph.threads.net&lt;/code&gt;). It's the right tool when you're working with &lt;strong&gt;your own&lt;/strong&gt; account: publishing, reading your posts and their insights, managing replies. Meta has since added keyword search and some public profile lookups, but they require app review, specific permissions, and an access token tied to a Threads user, and they come with their own rate limits. Check the current docs before relying on them, because the scope has been changing.&lt;/p&gt;
&lt;p&gt;What it won't give you is an easy &quot;give me any public account's posts&quot; call with no app review. That gap is why people write scrapers in the first place.&lt;/p&gt;
&lt;h3 id=&quot;project-a-threads-engagement-tracker&quot;&gt;Project: a Threads engagement tracker&lt;/h3&gt;
&lt;p&gt;Now the practical bit. This script snapshots a few accounts and appends one line per post per run to &lt;code&gt;snapshots.jsonl&lt;/code&gt;. JSON Lines keeps it dependency-free; swap in SQLite (&lt;code&gt;better-sqlite3&lt;/code&gt;, or &lt;code&gt;node:sqlite&lt;/code&gt; on recent Node versions) when the file gets large.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// track.js
import { appendFile } from &quot;node:fs/promises&quot;;
import { scrapeProfile } from &quot;./profile.js&quot;;

const HANDLES = process.argv.slice(2);
if (!HANDLES.length) {
  console.error(&quot;usage: node track.js handle1 handle2 ...&quot;);
  process.exit(1);
}
const sleep = (ms) =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, ms));

const runAt = new Date().toISOString();
for (const handle of HANDLES) {
  try {
    const { followers, posts } = await scrapeProfile(handle);
    const lines = posts
      .filter((p) =&amp;gt; !p.isReply)
      .map((p) =&amp;gt; JSON.stringify({ runAt, handle, followers, ...p }));
    if (lines.length) await appendFile(&quot;snapshots.jsonl&quot;, lines.join(&quot;\n&quot;) + &quot;\n&quot;);
    console.log(`${handle}: ${lines.length} posts, ${followers ?? &quot;?&quot;} followers`);
  } catch (err) {
    console.error(`${handle}: ${err.message}`);
  }
  await sleep(4_000 + Math.random() * 3_000);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Schedule it with cron every few hours:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;0 */4 * * * cd /path/to/threads-scraper &amp;amp;&amp;amp; /usr/bin/node track.js handle1 handle2 &amp;gt;&amp;gt; track.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a report that compares each post's first and latest snapshot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// report.js
import { readFile } from &quot;node:fs/promises&quot;;

const rows = (await readFile(&quot;snapshots.jsonl&quot;, &quot;utf8&quot;))
  .split(&quot;\n&quot;)
  .filter(Boolean)
  .map((l) =&amp;gt; JSON.parse(l));

const byPost = new Map();
for (const r of rows) {
  const s = byPost.get(r.code) ?? { first: r, last: r };
  if (r.runAt &amp;lt; s.first.runAt) s.first = r;
  if (r.runAt &amp;gt; s.last.runAt) s.last = r;
  byPost.set(r.code, s);
}

const delta = (a, b) =&amp;gt; (a == null || b == null ? null : b - a);
const report = [...byPost.values()]
  .map(({ first, last }) =&amp;gt; ({
    post: last.url,
    snapshots: `${first.runAt.slice(0, 16)} -&amp;gt; ${last.runAt.slice(0, 16)}`,
    likes: last.likes,
    likeGain: delta(first.likes, last.likes),
    replyGain: delta(first.replies, last.replies),
    repostGain: delta(first.reposts, last.reposts),
  }))
  .sort((a, b) =&amp;gt; (b.likeGain ?? -1) - (a.likeGain ?? -1));

console.table(report.slice(0, 15));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to keep in mind when reading this report: because you only see the first page of posts, older posts drop out of the snapshots once newer ones push them off. A post that stops appearing hasn't stopped growing; you just can't see it anymore. If you need to keep following specific posts, feed their URLs through &lt;code&gt;scrapePost()&lt;/code&gt; on each run instead.&lt;/p&gt;
&lt;h3 id=&quot;the-managed-alternative&quot;&gt;The managed alternative&lt;/h3&gt;
&lt;p&gt;If maintaining the parser becomes a chore, or you need more than the first page from datacenter IPs, &lt;a rel=&quot;nofollow&quot; href=&quot;https://scrapeatlas.com&quot;&gt;ScrapeAtlas*&lt;/a&gt; has Threads endpoints that return the same post objects (&lt;code&gt;like_count&lt;/code&gt;, &lt;code&gt;taken_at&lt;/code&gt;, &lt;code&gt;text_post_app_info.direct_reply_count&lt;/code&gt;, and so on). Authentication is an &lt;code&gt;x-api-key&lt;/code&gt; header:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const atlas = (path, params) =&amp;gt;
  fetch(`https://api.scrapeatlas.com/v1/threads/${path}?${new URLSearchParams(params)}`, {
    headers: { &quot;x-api-key&quot;: process.env.SCRAPEATLAS_API_KEY },
  }).then((r) =&amp;gt; r.json());

const profile = await atlas(&quot;profile&quot;, { handle: &quot;zuck&quot; });
console.log(profile.username, profile.follower_count, profile.is_verified);

const feed = await atlas(&quot;user/posts&quot;, { handle: &quot;zuck&quot;, since: &quot;2026-10-01&quot; });
console.log(feed.posts.map((p) =&amp;gt; [p.url, p.like_count, p.text_post_app_info.direct_reply_count]));
console.log(feed.coverage); // e.g. { complete: false, warnings: [&quot;posts_first_page_only&quot;], ... }

const thread = await atlas(&quot;post&quot;, { url: &quot;https://www.threads.com/@zuck/post/&amp;lt;code&amp;gt;&quot; });
console.log(thread.post.like_count, thread.comments.length);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To be clear about the limits: &lt;code&gt;user/posts&lt;/code&gt; is also first page only, which the response tells you in &lt;code&gt;coverage.warnings&lt;/code&gt;, so the tracker's blind spot is the same. &lt;code&gt;post&lt;/code&gt; takes &lt;code&gt;include_comments=false&lt;/code&gt; if you only want the post, and every endpoint accepts &lt;code&gt;trim=true&lt;/code&gt; for a compact projection. There are also &lt;code&gt;user/replies&lt;/code&gt;, &lt;code&gt;post/comments&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt; (keyword search with &lt;code&gt;recent&lt;/code&gt;, &lt;code&gt;start_date&lt;/code&gt;, and &lt;code&gt;end_date&lt;/code&gt;), &lt;code&gt;search/users&lt;/code&gt;, and &lt;code&gt;search/tags&lt;/code&gt;. Search is the part you can't do logged-out on your own.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read the preloaded JSON, not the DOM.&lt;/strong&gt; Walk every &lt;code&gt;application/json&lt;/code&gt; script and match on post shape, not a fixed path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dedupe by &lt;code&gt;code&lt;/code&gt;&lt;/strong&gt; and normalize &lt;code&gt;threads.net&lt;/code&gt; to &lt;code&gt;threads.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expect the first page only&lt;/strong&gt; without logging in, and design your tracker around that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skip GraphQL &lt;code&gt;doc_id&lt;/code&gt; calls&lt;/strong&gt; unless you're ready to re-capture them every time Meta ships a new build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go slow and cache,&lt;/strong&gt; and treat a small 200 response as a block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For your own account,&lt;/strong&gt; use the official Threads API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the extractor stops finding posts for you, leave a comment with what the page source looks like now and I'll update the post.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;*I build it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31945/how-to-scrape-threads-with-node-js-profiles-posts-replies-and-an-engagement-tracker-2026</guid>
<pubDate>Wed, 21 Oct 2026 09:05:56 +0000</pubDate>
</item>
<item>
<title>Stop Battling JavaScript Date Bugs: A Complete Guide to the Temporal API</title>
<link>https://coderlegion.com/31943/stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api</link>
<description>&lt;h2 id=&quot;stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api&quot;&gt;Stop Battling JavaScript Date Bugs: A Complete Guide to the Temporal API&lt;/h2&gt;
&lt;p&gt;The Temporal API is a modern, built-in JavaScript solution designed to overhaul and replace the legacy &lt;code&gt;Date&lt;/code&gt; object. It introduces a comprehensive suite of distinct, immutable objects for managing dates, times, durations, and time zones with exact precision. By cleanly separating wall-clock times from global timestamps, Temporal eliminates decades of common date-handling pitfalls in web applications.&lt;/p&gt;
&lt;h3 id=&quot;the-analogy-mechanical-wall-clocks-vs-modern-atomic-smartwatches&quot;&gt;The Analogy: Mechanical Wall Clocks vs. Modern Atomic Smartwatches&lt;/h3&gt;
&lt;p&gt;To understand why we need the Temporal API, picture a traditional mechanical wall clock hanging in a busy train station. If you manually turn the clock's minute hand forward to adjust for a delay, the internal gears might accidentally shift the hour hand, slip off track, or completely lose alignment with the sun outside. Worse yet, if two people try to adjust the clock at the same time, the clock's physical state gets corrupted.&lt;/p&gt;
&lt;p&gt;This is how JavaScript's legacy &lt;code&gt;Date&lt;/code&gt; object operates: it is mutable (meaning its value can be modified after creation) and constantly tries to guess your local time zone behind the scenes.&lt;/p&gt;
&lt;p&gt;The Temporal API, on the other hand, acts like a synchronized atomic smartwatch network. When you want to see what time it will be in three hours, the smartwatch does not alter your current time display. Instead, it generates a brand-new, unchangeable reading specifically for that future moment. It keeps local time, calendar dates, and exact global timestamps in separate, dedicated digital registers, ensuring that adjusting one setting never corrupts another.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-web-development&quot;&gt;Why It Matters in Daily Web Development&lt;/h3&gt;
&lt;p&gt;Every day, software engineers write code that schedules notifications, processes monthly recurring subscriptions, or logs critical system events. Using traditional JavaScript &lt;code&gt;Date&lt;/code&gt; handling, these operations frequently trigger subtle, costly bugs in production systems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Accidental Mutations:&lt;/strong&gt; Passing a standard date object into a utility function might accidentally alter the original date variable elsewhere in your codebase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time Zone Confusions:&lt;/strong&gt; A traditional date object automatically converts timestamps based on the local machine's system settings. This causes a server running in UTC (Coordinated Universal Time, the primary time standard by which the world regulates clocks) to interpret a user's midnight entry as 4:00 PM the previous day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daylight Saving Time (DST) Distortions:&lt;/strong&gt; Calculating 24 hours from a given moment during a DST transition can result in events firing an hour early or late.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Temporal API solves these daily problems by enforcing immutability and introducing specific types like &lt;code&gt;Temporal.PlainDate&lt;/code&gt; (for calendar dates without times) and &lt;code&gt;Temporal.ZonedDateTime&lt;/code&gt; (for exact moments anchored to a specific geographic region).&lt;/p&gt;
&lt;h3 id=&quot;code-example-node-js-date-calculations&quot;&gt;Code Example: Node.js Date Calculations&lt;/h3&gt;
&lt;p&gt;Below is a comparison showing how legacy Node.js date handling leads to unwanted side effects compared to the clean, safe approach provided by the Temporal API.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Legacy JavaScript Date Approach (Prone to bugs)
const eventDate = new Date('2025-05-01T10:00:00Z');

// We want to calculate a follow-up date 7 days later
const followUpDate = eventDate;
followUpDate.setDate(eventDate.getDate() + 7);

// BUG: Mutating followUpDate unexpectedly altered eventDate too!
console.log(eventDate.toISOString()); // Output: 2025-05-08T10:00:00.000Z


// Modern Temporal API Approach (Safe and Predictable)
// Note: Temporal is available in modern JS runtimes or via the @js-temporal/polyfill package
const { Temporal } = require('@js-temporal/polyfill');

// Create an exact zonal date-time for an event in Tokyo
const originalEvent = Temporal.ZonedDateTime.from({
  timeZone: 'Asia/Tokyo',
  year: 2025,
  month: 5,
  day: 1,
  hour: 10,
  minute: 0
});

// Add 7 days cleanly without modifying the original object
const reminderEvent = originalEvent.add({ days: 7 });

// The original object remains entirely untouched
console.log(originalEvent.toString()); // 2025-05-01T10:00:00+09:00[Asia/Tokyo]
console.log(reminderEvent.toString());  // 2025-05-08T10:00:00+09:00[Asia/Tokyo]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;key-takeaway-2&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Transitioning to the Temporal API shifts date manipulation from a fragile, error-prone chore into a robust, type-safe development experience. By cleanly isolating distinct concepts like calendar dates, wall-clock times, and regional time zones into immutable objects, Temporal guarantees that your application processes time consistently across every server and browser on the planet.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/modernizing-javascript-time-why.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31943/stop-battling-javascript-date-bugs-a-complete-guide-to-the-temporal-api</guid>
<pubDate>Wed, 21 Oct 2026 08:53:20 +0000</pubDate>
</item>
<item>
<title>Mastering the Critical Path in Performance Budgets for Faster React Apps</title>
<link>https://coderlegion.com/31814/mastering-the-critical-path-in-performance-budgets-for-faster-react-apps</link>
<description>&lt;h3 id=&quot;what-is-the-critical-path-in-performance-budgets-&quot;&gt;What is the Critical Path in Performance Budgets?&lt;/h3&gt;
&lt;p&gt;A performance budget is a set of strict limits agreed upon by a web development team to cap resource sizes, file counts, or loading speeds. The critical path—often called the Critical Rendering Path—is the exact sequence of assets and browser processing steps required to render the initial visible portion of a webpage. Combining these two concepts means setting explicit size and time limits specifically for the code and assets that block the initial page display.&lt;/p&gt;
&lt;h3 id=&quot;the-airport-fast-track-analogy&quot;&gt;The Airport Fast Track Analogy&lt;/h3&gt;
&lt;p&gt;Imagine walking through an airport terminal to board a flight. To get through the boarding gate, you only need two essential items in your hand: your passport and your boarding pass. These two items represent your critical path. &lt;/p&gt;
&lt;p&gt;If you try to haul three oversized suitcases, two heavy coats, and a duty-free shopping bag through the gate check all at once, you will slow down the entire line. A critical path performance budget is like an airport rule stating that passengers can only hold their passport and ticket while stepping up to the gate. Everything else—your checked luggage and heavy items—must be handled separately so you can pass through instantly.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters-in-daily-software-engineering&quot;&gt;Why it Matters in Daily Software Engineering&lt;/h3&gt;
&lt;p&gt;In modern web development, teams often bundle massive amounts of JavaScript and CSS together. When a user opens an application, the browser must download, parse, and execute all of this code before showing a single pixel of the user interface. &lt;/p&gt;
&lt;p&gt;If software engineers do not restrict what travels down the critical path, the browser gets clogged with non-essential files—such as analytics scripts, chat widgets, or secondary modal dialogues. By defining a strict performance budget for the critical path, engineers guarantee that the essential visual structure loads within milliseconds, keeping users engaged and preventing high bounce rates on mobile networks.&lt;/p&gt;
&lt;h3 id=&quot;practical-code-example-react-code-splitting&quot;&gt;Practical Code Example: React Code Splitting&lt;/h3&gt;
&lt;p&gt;To keep our critical path budget light, we should dynamically import non-essential React components so they do not block the main render. Here is how you can defer non-critical widgets using &lt;code&gt;React.lazy&lt;/code&gt; and &lt;code&gt;Suspense&lt;/code&gt; in a React application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import React, { Suspense, lazy } from 'react';

// Critical component: Loaded immediately for the initial render
import Header from './Header';
import MainContent from './MainContent';

// Non-critical component: Loaded on-demand outside the critical path
const HeavyAnalyticsDashboard = lazy(() =&amp;gt; import('./HeavyAnalyticsDashboard'));

function App() {
  return (
    &amp;lt;div className=&quot;app-container&quot;&amp;gt;
      {/* Essential components render without delay */}
      &amp;lt;Header /&amp;gt;
      &amp;lt;MainContent /&amp;gt;

      {/* Non-critical component is deferred with a lightweight fallback */}
      &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading analytics...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;HeavyAnalyticsDashboard /&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By splitting the bundle, &lt;code&gt;HeavyAnalyticsDashboard&lt;/code&gt; is pulled out of the critical rendering budget, ensuring the &lt;code&gt;Header&lt;/code&gt; and &lt;code&gt;MainContent&lt;/code&gt; display almost instantaneously.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaway-3&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;Managing your critical path within a performance budget is not about stripping away feature-rich functionality; it is about controlling &lt;em&gt;when&lt;/em&gt; those features are delivered. By keeping your initial rendering pipeline lean and pushing non-essential scripts into secondary loading phases, you preserve ultra-fast initial page loads without sacrificing modern application capabilities.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/why-your-app-feels-slow-enforcing.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31814/mastering-the-critical-path-in-performance-budgets-for-faster-react-apps</guid>
<pubDate>Tue, 20 Oct 2026 09:05:05 +0000</pubDate>
</item>
<item>
<title>AION 2 Pets</title>
<link>https://coderlegion.com/31784/aion-2-pets</link>
<description>&lt;p&gt;I maintain AION 2 Pets, a small independent web companion for players who want to plan pet collection without connecting a game account. It combines 206 pet records with recorded locations for 157 pets across three regional maps. The useful distinction is between finding a recorded location, budgeting upgrade resources and keeping a collection record. Map markers do not establish drop rates or farming time.&lt;/p&gt;
&lt;p&gt;The checklist uses manual browser-local state and supports JSON backup and validated merge import. That keeps the interaction simple, while making the limits visible: clearing site storage or switching devices requires a backup rather than an assumption of cloud synchronization. Pet soul requirements, collection EXP and analysis costs are shown separately.&lt;/p&gt;
&lt;p&gt;The project uses JavaScript and a static site deployment on Cloudflare Workers. Source notes and coverage limits are included, and the site does not automate gameplay or read a player account. This is an independent fan tool, not an NCSOFT product.&lt;/p&gt;
&lt;p&gt;Explore the &lt;a rel=&quot;nofollow&quot; href=&quot;https://aion2pets.app/&quot;&gt;pet atlas and planning tools&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/31784/aion-2-pets</guid>
<pubDate>Tue, 20 Oct 2026 04:54:15 +0000</pubDate>
</item>
<item>
<title>Your TypeScript Is Lying to You, and These 5 Places Hide Real Bugs</title>
<link>https://coderlegion.com/31717/your-typescript-is-lying-to-you-and-these-5-places-hide-real-bugs</link>
<description>&lt;p&gt;TypeScript's most dangerous keyword is two letters long.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;as&lt;/code&gt; does not convert anything. It does not check anything. It tells the compiler &quot;trust me&quot;, and the compiler does. If you are wrong, the error moves from your editor to your users.&lt;/p&gt;
&lt;p&gt;Here are five places I keep finding it in real codebases, each one compiling cleanly and each one a crash waiting for the right input.&lt;/p&gt;
&lt;h3 id=&quot;1-casting-an-api-response&quot;&gt;1. Casting an API Response&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const res = await fetch('/api/users/1');
const user = (await res.json()) as User;

console.log(user.profile.avatarUrl);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;res.json()&lt;/code&gt; returns &lt;code&gt;Promise&amp;lt;any&amp;gt;&lt;/code&gt;. Casting &lt;code&gt;any&lt;/code&gt; to &lt;code&gt;User&lt;/code&gt; is the compiler equivalent of a handshake agreement. If the backend renames a field, makes &lt;code&gt;profile&lt;/code&gt; optional, or returns an error object with a 200 status, this line still compiles and the crash happens three functions later with a message that has nothing to do with the cause.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What to write instead:&lt;/strong&gt; validate the data at the boundary.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  profile: z.object({ avatarUrl: z.string().url().nullable() }),
});

type User = z.infer&amp;lt;typeof UserSchema&amp;gt;;

const user = UserSchema.parse(await res.json());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the type is derived from a check that actually ran. If the shape is wrong, you get a clear error at the exact line where bad data entered your app.&lt;/p&gt;
&lt;h3 id=&quot;2-the-non-null-assertion-on-environment-variables&quot;&gt;2. The Non-Null Assertion on Environment Variables&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const client = new MongoClient(process.env.DATABASE_URL!);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;process.env.X&lt;/code&gt; is typed &lt;code&gt;string | undefined&lt;/code&gt;, which is honest. The &lt;code&gt;!&lt;/code&gt; deletes the &lt;code&gt;undefined&lt;/code&gt; from the type and nothing else. When the variable is missing in a new environment, you get a confusing failure from deep inside a library instead of &quot;DATABASE_URL is missing&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing environment variable: ${name}`);
  return value;
}

const client = new MongoClient(requireEnv('DATABASE_URL'));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app now fails at startup, with the variable name in the message. That is the cheapest place to find out.&lt;/p&gt;
&lt;h3 id=&quot;3-the-non-null-assertion-on-find-&quot;&gt;3. The Non-Null Assertion on &lt;code&gt;find()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const user = users.find((u) =&amp;gt; u.id === id)!;
sendEmail(user.email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt; when nothing matches, and &quot;nothing matches&quot; is exactly the case your tests forgot. The &lt;code&gt;!&lt;/code&gt; makes the compiler stop warning you about the one scenario worth warning about.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const user = users.find((u) =&amp;gt; u.id === id);
if (!user) throw new Error(`User ${id} not found`);

sendEmail(user.email);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the check, TypeScript narrows &lt;code&gt;user&lt;/code&gt; to a real object on its own. No assertion required.&lt;/p&gt;
&lt;h3 id=&quot;4-casting-object-keys&quot;&gt;4. Casting Object.keys&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Settings = { theme: string; lang: string };

function logSettings(s: Settings) {
  (Object.keys(s) as (keyof Settings)[]).forEach((k) =&amp;gt; {
    console.log(s[k].toUpperCase());
  });
}

const fromApi = { theme: 'dark', lang: 'en', retries: 3 };
logSettings(fromApi);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles. TypeScript types are structural, so an object with extra properties is still a valid &lt;code&gt;Settings&lt;/code&gt; when it arrives through a variable. &lt;code&gt;Object.keys&lt;/code&gt; returns plain &lt;code&gt;string[]&lt;/code&gt; for exactly this reason, and the cast to &lt;code&gt;keyof Settings&lt;/code&gt; overrides that protection. At runtime &lt;code&gt;retries&lt;/code&gt; is a number, and &lt;code&gt;toUpperCase&lt;/code&gt; is not a function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const SETTING_KEYS = ['theme', 'lang'] as const;

function logSettings(s: Settings) {
  SETTING_KEYS.forEach((k) =&amp;gt; console.log(s[k].toUpperCase()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Iterate over a list of keys you control, not over whatever the object happens to contain.&lt;/p&gt;
&lt;h3 id=&quot;5-casting-the-caught-error&quot;&gt;5. Casting the Caught Error&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;try {
  await saveProfile(data);
} catch (e) {
  toast((e as Error).message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In JavaScript you can throw anything: a string, a number, &lt;code&gt;undefined&lt;/code&gt;, a plain object. Libraries do it. With &lt;code&gt;strict&lt;/code&gt; on, TypeScript types &lt;code&gt;e&lt;/code&gt; as &lt;code&gt;unknown&lt;/code&gt; to keep you honest, and &lt;code&gt;as Error&lt;/code&gt; is the line that undoes it. If something throws a string, &lt;code&gt;.message&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; and your toast says &quot;undefined&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;catch (e) {
  const message = e instanceof Error ? e.message : 'Something went wrong';
  toast(message);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;a-bonus-lie-that-lives-in-your-config&quot;&gt;A Bonus Lie That Lives in Your Config&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const items: Item[] = [];
const first = items[0];
first.name; // typed as Item, crashes at runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Array index access is typed as &lt;code&gt;Item&lt;/code&gt;, not &lt;code&gt;Item | undefined&lt;/code&gt;, because the compiler cannot know the length. One line in &lt;code&gt;tsconfig.json&lt;/code&gt; fixes it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;compilerOptions&quot;: {
    &quot;strict&quot;: true,
    &quot;noUncheckedIndexedAccess&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This flag is not part of &lt;code&gt;strict&lt;/code&gt;, so most projects never turn it on. It adds a few extra checks, and every one of them is a real case where the array could have been empty.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-5&quot;&gt;The Rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A cast is a promise you make to the compiler that it cannot verify. Data from outside your program needs a runtime check, not a type assertion.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An easy audit: search your project for &lt;code&gt; as &lt;/code&gt; and for &lt;code&gt;!.&lt;/code&gt; and &lt;code&gt;!;&lt;/code&gt;. For each hit, ask what happens when the assumption is false. Casts on your own literals are usually fine. Casts on network responses, environment variables, search results and caught errors are where the bugs live.&lt;/p&gt;
&lt;p&gt;I keep these checks in the shared &lt;code&gt;lib/&lt;/code&gt; folder of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.com&quot;&gt;TypeScript-first templates on pixelanas.com&lt;/a&gt;, so a missing environment variable or a changed API shape fails loudly on the first request instead of silently in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get the templates:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://pixelanas.gumroad.com&quot;&gt;https://pixelanas.gumroad.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;How many &lt;code&gt;as&lt;/code&gt; casts does your project have, and how many of them are on data you do not control? Drop your count in the comments.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Anas, full-stack Next.js developer building SaaS products and premium templates. X: &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/ASheikh69751&quot;&gt;@ASheikh69751&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31717/your-typescript-is-lying-to-you-and-these-5-places-hide-real-bugs</guid>
<pubDate>Mon, 19 Oct 2026 12:30:35 +0000</pubDate>
</item>
<item>
<title>I built a background remover that never sees your photo (and it does GIFs and videos too)!</title>
<link>https://coderlegion.com/31622/i-built-a-background-remover-that-never-sees-your-photo-and-it-does-gifs-and-videos-too</link>
<description>&lt;p&gt;When you’re the only engineer in the family, you automatically become the unpaid IT department, video producer, and resident graphic designer, because to relatives, &quot;computers&quot; is just one big subject (and that includes everything to do with mobile phones too... somehow!). So whenever someone needed a transparent PNG or a quick cutout, it fell on me, and I’d get stuck in the same annoying loop. I'd hunt down a &quot;free&quot; background remover (whatever showed up first on Google), upload the photo, wait, and then get held hostage by a &quot;Buy Credits&quot; button. My only options were settling for a blurry, postage-stamp-sized preview, or making a whole new account if I was desperate enough for the high-res version. Worse, somewhere in that mess, a family photo would get shipped off to a sketchy server I know nothing about.&lt;/p&gt;
&lt;p&gt;For a product shot, that's annoying. For a passport photo, a signature, or a client's photo, it's a bit more than annoying.&lt;/p&gt;
&lt;p&gt;So I added a background remover to &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;Fizzdoc&lt;/a&gt;, my open-source toolkit where every tool runs inside the browser tab. It now does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Photos:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-background/&quot;&gt;Remove Background&lt;/a&gt;, saved at the photo's own resolution as PNG, JPG or WebP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Animated GIFs:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-gif-background/&quot;&gt;Remove GIF Background&lt;/a&gt;, every frame, same timing, same loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Videos:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-video-background/&quot;&gt;Remove Video Background&lt;/a&gt;, every frame, with the sound kept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WhatsApp stickers:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;WhatsApp Sticker Maker&lt;/a&gt;, a 512 × 512 WebP with a white outline, kept under 100 KB, with words in fun fonts and emoji you drag into place.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can keep the background clear, or put white, any colour, a blur or another photo behind. It's free, there's no sign-up, no watermark, and no limit on how many photos you do. The size limit comes from your device's memory, not from me.&lt;/p&gt;
&lt;p&gt;And the photo never leaves your device. Not &quot;we delete it after an hour&quot;. It just never goes anywhere.&lt;/p&gt;
&lt;p&gt;Below is how it works, and what broke along the way.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-nothing-gets-uploaded-and-you-can-check&quot;&gt;The rule: nothing gets uploaded, and you can check&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6ozs6z0kkoy1n1ahqle5.png&quot; alt=&quot;The Remove Background page. The badge under the title says what happens to your photo: 0 bytes uploaded.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Fizzdoc is a static site on Cloudflare Pages. There's no backend, so there's nothing that &lt;em&gt;could&lt;/em&gt; receive a file. The page also ships a Content Security Policy with &lt;code&gt;connect-src 'self'&lt;/code&gt;, which means the browser itself blocks the page from talking to any other website.&lt;/p&gt;
&lt;p&gt;That rule shaped everything below. No CDN for the model, no hosted inference API, no &quot;just this one request&quot;. Open DevTools, go to the Network tab, remove the background, and you'll see the model files coming from fizzdoc.com and nothing going out.&lt;/p&gt;
&lt;h3 id=&quot;picking-a-model&quot;&gt;Picking a model&lt;/h3&gt;
&lt;p&gt;I wanted three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Good edges on hair and fur, because that's where background removers usually show their seams.&lt;/li&gt;
&lt;li&gt;Small enough to download once into a browser.&lt;/li&gt;
&lt;li&gt;A licence that lets anyone use the result, including for work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went with &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ZhengPeng7/BiRefNet&quot;&gt;BiRefNet&lt;/a&gt;&lt;/strong&gt; (MIT licence), the lite variant, using the 512 × 512 ONNX export by &lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/studioludens/birefnet-lite-512&quot;&gt;studioludens&lt;/a&gt;. I store its big weight tensors as 16-bit floats and cast them back to 32-bit when the model loads, which halves the download to about 100 MB without changing the maths it runs.&lt;/p&gt;
&lt;p&gt;For phones there's a second model, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/xuebinqin/U-2-Net&quot;&gt;U²-Net&lt;/a&gt; small&lt;/strong&gt; (Apache-2.0), just 4.6 MB. I learned this one the hard way: someone sent me a screenshot of an early version on a 6 GB Android phone, where Chrome killed the tab at 5% (&quot;Aw, Snap!&quot;). A browser tab on a phone gets far less memory than the phone has, and when it runs out there's no error to catch, the tab just dies. So now phones always use the small model, and the page says so (&quot;for the cleanest edges on hair and fur, use a laptop&quot;). If the big model ever crashes a tab on a laptop too, the page remembers it for 30 days and uses the small one there.&lt;/p&gt;
&lt;p&gt;And for laptops with a graphics chip there's the opposite: a &lt;strong&gt;Sharper edges (hair, fur)&lt;/strong&gt; button. It reruns the photo through a 1024 × 1024 export of the same BiRefNet-lite model (&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/onnx-community/BiRefNet_lite-ONNX&quot;&gt;onnx-community&lt;/a&gt;, MIT). It's a one-time 135 MB download and needs WebGPU, so it's never offered on phones. If the device can't manage it, you keep the result you already had.&lt;/p&gt;
&lt;h3 id=&quot;running-it-webgpu-when-it-can-cpu-when-it-can-t&quot;&gt;Running it: WebGPU when it can, CPU when it can't&lt;/h3&gt;
&lt;p&gt;The model runs with &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/onnxruntime&quot;&gt;ONNX Runtime Web&lt;/a&gt;&lt;/strong&gt; inside a Web Worker, so the page never freezes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the device has a graphics chip with &lt;strong&gt;WebGPU&lt;/strong&gt;, the model runs there. This is the fast path.&lt;/li&gt;
&lt;li&gt;If WebGPU isn't there (or the driver says no), the &lt;strong&gt;same model&lt;/strong&gt; runs on the CPU through WebAssembly.&lt;/li&gt;
&lt;li&gt;If that fails too, usually because the device ran out of memory, the small U²-Net model takes over.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the chain is GPU, then the same model on CPU, then the small model. Nobody gets an error just because their laptop is old.&lt;/p&gt;
&lt;h4 id=&quot;two-fun-problems-with-self-host-everything-&quot;&gt;Two fun problems with &quot;self-host everything&quot;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Pages has a 25 MiB limit per file.&lt;/strong&gt; The model is about 100 MB and ONNX Runtime's WebGPU build is a 27 MB &lt;code&gt;.wasm&lt;/code&gt;. So both live on the server as 20 MiB parts: the models are split by a small script when I add them, and the runtime is split at build time. The worker downloads the parts, joins them, and keeps the result in the browser's Cache Storage, so the next time it starts straight from there, even with the internet off. The build also fails if any file it writes goes over 25 MiB, after one deploy failed on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A runtime that exists in pieces.&lt;/strong&gt; ONNX Runtime normally fetches its &lt;code&gt;.wasm&lt;/code&gt; as one file. Mine exists just as parts on the server, so the worker joins them from Cache Storage and hands the bytes to the runtime directly with &lt;code&gt;ort.env.wasm.wasmBinary&lt;/code&gt;. It needs no extra hosting and no change to the security policy.&lt;/p&gt;
&lt;h3 id=&quot;the-part-i-m-proudest-of-full-resolution-from-a-512-px-model&quot;&gt;The part I'm proudest of: full resolution from a 512 px model&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/p5aj4yuv4j87y4rxega1.png&quot; alt=&quot;Top: the page loading the model once, and the options panel. Bottom: the original photo, the cut-out, and a new background in one tap.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The model looks at your photo squeezed to 512 × 512 and returns a soft mask. If you stretch that mask back over a 4000 × 3000 photo, the edges look like they were cut with a butter knife.&lt;/p&gt;
&lt;p&gt;So the mask goes through two classic image-processing filters, both plain arithmetic on typed arrays in the worker:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fast guided filter&lt;/strong&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/1505.00996&quot;&gt;He and Sun, 2015&lt;/a&gt;). It uses the photo itself as a guide to snap the blurry mask onto the real edges, so hair follows the actual strands. The fit is computed on a smaller copy (up to 1280 px) and then applied to every pixel of the full photo in one pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blur-fusion foreground estimation&lt;/strong&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.1109/ICIP42928.2021.9506164&quot;&gt;Forte, 2021&lt;/a&gt;). Edge pixels are a mix of your subject and the old background. Put a cut-out on a new colour without fixing that, and you get a faint halo of the old background. This step estimates the subject's true colour at the edges, so the new background looks like it was always there.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By default the result is saved at your photo's own size: your own pixels, with a cleaner mask.&lt;/p&gt;
&lt;h3 id=&quot;when-the-ai-gets-it-almost-right&quot;&gt;When the AI gets it almost right&lt;/h3&gt;
&lt;p&gt;It's an AI model, so a busy background or a subject that blends into it can confuse it. For photos there's a small set of touch-ups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Erase / Restore&lt;/strong&gt; brush, with &lt;strong&gt;Undo&lt;/strong&gt;, &lt;strong&gt;Redo&lt;/strong&gt; and &lt;strong&gt;Zoom&lt;/strong&gt; for careful work on a phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tap to remove&lt;/strong&gt; and &lt;strong&gt;Tap to keep&lt;/strong&gt;: one tap picks the whole connected area of a similar colour and removes it, or brings it back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shadow&lt;/strong&gt; and a white &lt;strong&gt;Outline&lt;/strong&gt;, if you want the cut-out to stand out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crop&lt;/strong&gt;: the whole photo, fit to the subject, or a square around the subject for online shop listings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hold to see the original&lt;/strong&gt;, to compare before and after.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gifs-every-frame-same-rhythm&quot;&gt;GIFs: every frame, same rhythm&lt;/h3&gt;
&lt;p&gt;Animated GIFs are sneaky. Many frames aren't full pictures, just small patches drawn over the previous frame, with rules (&quot;disposal methods&quot;) about what to clear afterwards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/matt-way/gifuct-js&quot;&gt;gifuct-js&lt;/a&gt;&lt;/strong&gt; (MIT) decodes the frames, and Fizzdoc composes each one into a full picture the way a browser would show it.&lt;/li&gt;
&lt;li&gt;Each frame goes through the same model (in a faster mode, since there can be dozens).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mattdesl/gifenc&quot;&gt;gifenc&lt;/a&gt;&lt;/strong&gt; (MIT) writes the new GIF, keeping every frame's delay and the loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One catch: a GIF pixel is either fully see-through or fully solid. So on a clear background the edges are a little sharper than in a PNG. Pick a colour or a photo behind and the soft edges are back.&lt;/p&gt;
&lt;h3 id=&quot;videos-every-frame-sound-kept&quot;&gt;Videos: every frame, sound kept&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/m7zws0qibirp7kft78oa.png&quot; alt=&quot;Remove Video Background: every frame gets a new background, and the sound is kept.&quot;&gt;&lt;/p&gt;
&lt;p&gt;For video, &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vanilagy/mediabunny&quot;&gt;Mediabunny&lt;/a&gt;&lt;/strong&gt; (MPL-2.0) reads the file and the browser's own WebCodecs encoder writes the new one. Every frame goes through the model and gets its new background, and the soundtrack is carried over.&lt;/p&gt;
&lt;p&gt;The output is MP4 (H.264) if the browser can handle it; otherwise, it's WebM (VP9). Before you start, the page shows a preview frame and an estimate of how long the whole clip will take on that device.&lt;/p&gt;
&lt;p&gt;What does &quot;no background&quot; mean for a video? Truly see-through video would need the encoder library to start helper workers from &lt;code&gt;blob:&lt;/code&gt; URLs, which my page's security policy blocks, and most players can't show it anyway. So &quot;None&quot; gives you a &lt;strong&gt;green screen&lt;/strong&gt;, which any video editor removes in one click. I chose to keep the policy strict rather than loosen it for one feature.&lt;/p&gt;
&lt;h3 id=&quot;stickers-words-emoji-and-the-last-step-in-whatsapp&quot;&gt;Stickers: words, emoji, and the last step in WhatsApp&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3br9eeprohzz237iam1y.png&quot; alt=&quot;The WhatsApp Sticker Maker turns any photo into a 512 × 512 sticker with a white outline, under 100 KB.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;WhatsApp Sticker Maker&lt;/a&gt; starts from the same cut-out. It squares it to 512 × 512, adds a white outline, and keeps the WebP under 100 KB by lowering the quality step by step until it fits.&lt;/p&gt;
&lt;p&gt;On top of that, you can add words in five styles (Funny, Cute, Cozy, BIG, and Serious) and emojis. Each one is a layer you drag, pinch, resize and rotate, on a phone or with a mouse. The fonts are self-hosted like everything else: Bangers, Kalam, Teko and Inter, with Baloo 2 covering Hindi letters. Emoji are drawn with your device's own emoji font.&lt;/p&gt;
&lt;p&gt;One fun bug on the way: Vite inlines small files as &lt;code&gt;data:&lt;/code&gt; URLs, and a few of the font files were small enough. My own CSP (&lt;code&gt;default-src 'self'&lt;/code&gt;) refused them, and the end-to-end test that fails on any CSP violation caught it before it shipped. Fonts are now always separate files.&lt;/p&gt;
&lt;p&gt;One limit, though. A web page can't add a sticker to WhatsApp, and a sticker shared from the browser arrives as a normal photo. So instead of a &quot;Share to WhatsApp&quot; button that doesn't do what it says, the page shows WhatsApp's own steps: save the sticker, then in any chat tap Stickers › Create and pick it. On iPhone there's a &quot;Save to Photos&quot; button, because downloads go to Files and WhatsApp picks stickers from Photos.&lt;/p&gt;
&lt;h3 id=&quot;what-it-s-like-on-low-end-phones&quot;&gt;What it's like on low-end phones&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ayc0gdg72seyd3vy5kuc.png&quot; alt=&quot;The same tool on a phone, in dark mode. Phones use the small 4.6 MB model so the tab doesn't crash.&quot;&gt;&lt;/p&gt;
&lt;p&gt;I care about this a lot because many people open Fizzdoc on Android phones.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Phones always get the small model, so it works rather than crashes. Edges are a bit softer than on a laptop, which is a trade I'll take every time.&lt;/li&gt;
&lt;li&gt;The worker keeps the page responsive while it thinks.&lt;/li&gt;
&lt;li&gt;The model downloads once (the page shows the progress in MB). After that it starts right away, and it keeps working with Wi-Fi and mobile data off, which is also the easiest way to check that nothing is uploaded.&lt;/li&gt;
&lt;li&gt;Videos give you a time estimate first: short clips are fine, long ones are a laptop job.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;things-it-doesn-t-do-yet-&quot;&gt;Things it doesn't do (yet)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Truly transparent video (see above, green screen instead).&lt;/li&gt;
&lt;li&gt;Many photos in one go: the background remover takes one photo at a time.&lt;/li&gt;
&lt;li&gt;Several photos in one sticker. That's next.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;credits&quot;&gt;Credits&lt;/h3&gt;
&lt;p&gt;None of this would exist without these people and projects. Thank you:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Project &lt;/th&gt;&lt;th&gt;What it does in Fizzdoc &lt;/th&gt;&lt;th&gt;Licence&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ZhengPeng7/BiRefNet&quot;&gt;BiRefNet&lt;/a&gt; by Peng Zheng and co-authors &lt;/td&gt;&lt;td&gt;The background model &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/studioludens/birefnet-lite-512&quot;&gt;birefnet-lite-512&lt;/a&gt; by studioludens &lt;/td&gt;&lt;td&gt;The 512 px ONNX export &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://huggingface.co/onnx-community/BiRefNet_lite-ONNX&quot;&gt;BiRefNet_lite-ONNX&lt;/a&gt; by onnx-community &lt;/td&gt;&lt;td&gt;The 1024 px export for Sharper edges &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/xuebinqin/U-2-Net&quot;&gt;U²-Net&lt;/a&gt; by Xuebin Qin and co-authors &lt;/td&gt;&lt;td&gt;The small model for phones &lt;/td&gt;&lt;td&gt;Apache-2.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/microsoft/onnxruntime&quot;&gt;ONNX Runtime Web&lt;/a&gt; by Microsoft &lt;/td&gt;&lt;td&gt;Runs the models on WebGPU or WebAssembly &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/matt-way/gifuct-js&quot;&gt;gifuct-js&lt;/a&gt; by Matt Way &lt;/td&gt;&lt;td&gt;Reads animated GIFs &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/mattdesl/gifenc&quot;&gt;gifenc&lt;/a&gt; by Matt DesLauriers &lt;/td&gt;&lt;td&gt;Writes animated GIFs &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vanilagy/mediabunny&quot;&gt;Mediabunny&lt;/a&gt; by Vanilagy &lt;/td&gt;&lt;td&gt;Reads and writes videos &lt;/td&gt;&lt;td&gt;MPL-2.0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/1505.00996&quot;&gt;Kaiming He and Jian Sun&lt;/a&gt; &lt;/td&gt;&lt;td&gt;The (fast) guided filter &lt;/td&gt;&lt;td&gt;Paper&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Photoroom/fast-foreground-estimation&quot;&gt;Marco Forte&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Blur-fusion foreground estimation &lt;/td&gt;&lt;td&gt;Paper&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The full licence texts are in Fizzdoc's &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/third-party-licenses.txt&quot;&gt;third-party licences&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;try-it-break-it-tell-me&quot;&gt;Try it, break it, tell me&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Photo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-background/&quot;&gt;https://fizzdoc.com/remove-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GIF: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-gif-background/&quot;&gt;https://fizzdoc.com/remove-gif-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Video: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/remove-video-background/&quot;&gt;https://fizzdoc.com/remove-video-background/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;WhatsApp sticker: &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com/whatsapp-sticker-maker/&quot;&gt;https://fizzdoc.com/whatsapp-sticker-maker/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code (Apache-2.0): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;https://github.com/kingrishabdugar/fizzdoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's part of a bigger toolkit of 68 free tools (PDF, Word, Excel, PowerPoint, images and audio), all running in your browser, in 16 languages. If something looks off on your device, there's a &quot;Report it&quot; link on every tool. And if you like it, a star on GitHub genuinely makes my day.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31622/i-built-a-background-remover-that-never-sees-your-photo-and-it-does-gifs-and-videos-too</guid>
<pubDate>Sun, 18 Oct 2026 14:58:30 +0000</pubDate>
</item>
<item>
<title>Lossless vs Lossy Compression: The Core Difference Explained</title>
<link>https://coderlegion.com/31606/lossless-vs-lossy-compression-the-core-difference-explained</link>
<description>&lt;h3 id=&quot;the-essence-of-compression-expressing-the-same-information-with-less-data&quot;&gt;The Essence of Compression: Expressing the Same Information with Less Data&lt;/h3&gt;
&lt;p&gt;Before diving into the distinction between lossless and lossy compression, we need to understand what compression actually does.&lt;/p&gt;
&lt;p&gt;At its core, compression is about &lt;strong&gt;expressing the same information with less data&lt;/strong&gt;. Any piece of data—whether text, images, audio, or video—contains &quot;redundancy.&quot; Redundancy refers to parts that can be removed without losing the meaning of the information. A compression algorithm's job is to identify and eliminate this redundancy.&lt;/p&gt;
&lt;p&gt;Consider a simple example. The character string &lt;code&gt;AAAAABBBCC&lt;/code&gt; occupies 10 bytes. But we can represent the same information more compactly by recording &quot;each character and how many times it repeats consecutively,&quot; yielding &lt;code&gt;5A3B2C&lt;/code&gt;—only 6 bytes. The data size drops by 40%, yet the meaning is identical. This is the most primitive form of compression, known as Run-Length Encoding (RLE).&lt;/p&gt;
&lt;p&gt;From an information theory perspective, Claude Shannon introduced the concept of &quot;information entropy&quot;: the minimum average encoding length of a data stream is bounded by its entropy. No lossless algorithm can break this theoretical limit. This means that re-compressing already-compressed data yields almost no benefit—the redundancy has already been stripped away.&lt;/p&gt;
&lt;p&gt;Understanding this essence reveals two fundamentally different technical paths: &lt;strong&gt;lossless compression&lt;/strong&gt; strives for perfect fidelity, while &lt;strong&gt;lossy compression&lt;/strong&gt; deliberately discards details that the human eye or ear cannot easily perceive, trading them for a much higher compression ratio.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossless-compression-restoring-every-bit&quot;&gt;Lossless Compression: Restoring Every Bit&lt;/h3&gt;
&lt;p&gt;The core promise of lossless compression is that the decompressed data is &lt;strong&gt;byte-for-byte identical&lt;/strong&gt; to the original—not a single bit may differ. These algorithms eliminate only &quot;statistical redundancy&quot; and &quot;structural redundancy,&quot; never touching the semantic content of the information.&lt;/p&gt;
&lt;h4 id=&quot;deflate-the-most-widely-used-lossless-algorithm&quot;&gt;DEFLATE: The Most Widely Used Lossless Algorithm&lt;/h4&gt;
&lt;p&gt;DEFLATE is the algorithm underlying ZIP, gzip, zlib, and many other tools. It elegantly combines two classic techniques:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;LZ77&lt;/strong&gt;: Uses a sliding window to search for repeated substrings in already-processed data, replacing them with &quot;distance + length&quot; pointers. For example, the word &quot;compression&quot; appearing repeatedly in a text file can be replaced with a back-reference to its first occurrence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Huffman coding&lt;/strong&gt;: Applies variable-length encoding to the symbol stream produced by LZ77. Frequently occurring symbols get short codes; rare symbols get long codes. This pushes the encoding closer to the entropy limit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;DEFLATE's strengths are its simplicity, speed, and universal compatibility—nearly every programming language's standard library includes an implementation.&lt;/p&gt;
&lt;h4 id=&quot;lzma-higher-ratios-through-context-modeling&quot;&gt;LZMA: Higher Ratios Through Context Modeling&lt;/h4&gt;
&lt;p&gt;LZMA (Lempel-Ziv-Markov chain Algorithm) builds on LZ77 with a much larger sliding window (up to several GB), a more refined probability model, and range coding. Compared to DEFLATE, LZMA typically achieves higher compression ratios, at the cost of slower compression speed and greater memory usage. The 7z format is based on LZMA.&lt;/p&gt;
&lt;p&gt;The key improvement in LZMA is that it does not rely on a static probability table. Instead, it dynamically predicts the probability of the next symbol based on context. This &quot;context modeling&quot; brings the encoding much closer to the theoretical entropy limit.&lt;/p&gt;
&lt;h4 id=&quot;png-filtering-preprocessing-for-image-data&quot;&gt;PNG Filtering: Preprocessing for Image Data&lt;/h4&gt;
&lt;p&gt;PNG is a lossless image format, but it does not apply DEFLATE directly to raw pixel values. Before compression, PNG applies a &quot;filter&quot; to each row of pixels to make the data more amenable to DEFLATE.&lt;/p&gt;
&lt;p&gt;A common filter stores the difference between each pixel and its neighbor to the left, above, or upper-left. Since natural images have locally similar pixels, these differences tend to cluster around zero. This transformed data has higher redundancy, allowing DEFLATE to compress it far more effectively.&lt;/p&gt;
&lt;p&gt;This also explains why ZIP cannot effectively compress JPEG images: JPEG is already highly compressed through DCT and entropy coding, leaving almost no statistical redundancy for DEFLATE to exploit. Running ZIP on a JPEG typically adds a few bytes of header overhead with no size reduction.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossy-compression-trading-below-the-perception-threshold&quot;&gt;Lossy Compression: Trading Below the Perception Threshold&lt;/h3&gt;
&lt;p&gt;The central idea of lossy compression is that &lt;strong&gt;human perception has limits&lt;/strong&gt;. If the discarded details fall below the perceptual threshold, the visual or auditory difference is negligible—but the data reduction can be enormous.&lt;/p&gt;
&lt;h4 id=&quot;dct-the-mathematical-foundation-of-jpeg&quot;&gt;DCT: The Mathematical Foundation of JPEG&lt;/h4&gt;
&lt;p&gt;The Discrete Cosine Transform (DCT) is the heart of JPEG image compression. Its role is to convert an image from the &quot;spatial domain&quot; to the &quot;frequency domain.&quot;&lt;/p&gt;
&lt;p&gt;In the spatial domain, an image is represented by pixel brightness values. In the frequency domain, the image is decomposed into a sum of cosine wave components at different frequencies: low-frequency components represent large smooth areas, while high-frequency components represent edges and fine detail.&lt;/p&gt;
&lt;p&gt;The DCT itself is reversible and loses no information. The actual &quot;loss&quot; happens in the next step—quantization.&lt;/p&gt;
&lt;h4 id=&quot;quantization-the-source-of-loss&quot;&gt;Quantization: The Source of Loss&lt;/h4&gt;
&lt;p&gt;Quantization maps high-precision DCT coefficients to a limited set of discrete values. In practice, each coefficient is divided by a value from a quantization table and then rounded to an integer. Because the human eye is less sensitive to high-frequency detail, the quantization table applies larger divisors to high-frequency coefficients, causing many of them to become zero.&lt;/p&gt;
&lt;p&gt;This step causes irreversible information loss—once a coefficient is rounded to zero, it cannot be recovered. But it also creates massive compression opportunity: long runs of zeros can be encoded very efficiently with run-length encoding.&lt;/p&gt;
&lt;p&gt;Designing the quantization table is an art: larger divisors yield higher compression but more visible quality loss. This is exactly what the JPEG quality parameter (e.g., quality=80) controls—it scales the quantization table up or down.&lt;/p&gt;
&lt;h4 id=&quot;inter-frame-prediction-the-key-to-video-compression&quot;&gt;Inter-Frame Prediction: The Key to Video Compression&lt;/h4&gt;
&lt;p&gt;Video compression goes a step beyond still images. Consecutive frames in a video often change very little (for example, a static background with a moving person), so there is no need to store every frame in full.&lt;/p&gt;
&lt;p&gt;Inter-frame prediction uses &quot;motion estimation&quot; to find the displacement between the current frame and a reference frame, storing only &quot;motion vectors&quot; and &quot;residuals.&quot; This elimination of temporal redundancy allows video compression ratios to reach hundreds or even thousands to one—far beyond what single-frame image compression can achieve.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lossless-vs-lossy-comparison-diagram&quot;&gt;Lossless vs Lossy: Comparison Diagram&lt;/h3&gt;
&lt;p&gt;The flowchart below illustrates the core branches and typical applications of the two compression families:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/hwt4ff1zij0vf54zqj03.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dct-principle-diagram&quot;&gt;DCT Principle Diagram&lt;/h3&gt;
&lt;p&gt;The diagram below simplifies how DCT transforms an 8x8 pixel block from the spatial domain to the frequency domain, then preserves low frequencies while discarding high frequencies through quantization:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/et3p2djbwk12elm97aqr.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;code-examples&quot;&gt;Code Examples&lt;/h3&gt;
&lt;h4 id=&quot;lossless-compression-with-zlib&quot;&gt;Lossless Compression with zlib&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import zlib

# Original text data with heavy repetition
original = b&quot;compression compression compression &quot; * 50

# Compress
compressed = zlib.compress(original, level=9)
print(f&quot;Original size: {len(original)} bytes&quot;)
print(f&quot;Compressed size: {len(compressed)} bytes&quot;)
print(f&quot;Ratio: {len(original) / len(compressed):.2f}x&quot;)

# Decompress and verify the data is identical
decompressed = zlib.decompress(compressed)
assert decompressed == original, &quot;Data mismatch!&quot;
print(&quot;Verification passed: decompressed data is identical to original&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this code, you will typically see a compression ratio above 10x, and the decompressed data is byte-for-byte identical to the original. This is what &quot;lossless&quot; means.&lt;/p&gt;
&lt;h4 id=&quot;lossy-compression-with-pil&quot;&gt;Lossy Compression with PIL&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from PIL import Image
import io

# Create a test image rich in high-frequency detail
img = Image.new(&quot;RGB&quot;, (512, 512))
pixels = img.load()
for x in range(512):
    for y in range(512):
        # Generate a pattern with high-frequency detail
        pixels[x, y] = ((x * 7) % 256, (y * 5) % 256, ((x + y) * 3) % 256)

# Save as lossless PNG
img.save(&quot;test_lossless.png&quot;)
png_size = len(open(&quot;test_lossless.png&quot;, &quot;rb&quot;).read())
print(f&quot;PNG (lossless) size: {png_size} bytes&quot;)

# Save as lossy JPEG with quality 20 to amplify the effect
buffer = io.BytesIO()
img.save(buffer, format=&quot;JPEG&quot;, quality=20)
jpeg_size = buffer.tell()
print(f&quot;JPEG (lossy) size: {jpeg_size} bytes&quot;)
print(f&quot;Ratio: {png_size / jpeg_size:.2f}x&quot;)

# Reload the JPEG and compare pixel differences
buffer.seek(0)
img_jpeg = Image.open(buffer)
diff_count = sum(
    1 for x in range(512) for y in range(512)
    if img.getpixel((x, y)) != img_jpeg.getpixel((x, y))
)
print(f&quot;Pixels that changed: {diff_count} / {512 * 512}&quot;)
print(&quot;Conclusion: JPEG compression alters pixel values irreversibly&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code demonstrates the hallmark of lossy compression: a smaller file, but with pixel values changed irreversibly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;comparison-table&quot;&gt;Comparison Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Dimension &lt;/th&gt;&lt;th&gt;Lossless Compression &lt;/th&gt;&lt;th&gt;Lossy Compression&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Core Principle&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Eliminates statistical &amp;amp; structural redundancy &lt;/td&gt;&lt;td&gt;Discards info below perception threshold&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fidelity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;100% reversible, identical after decompression &lt;/td&gt;&lt;td&gt;Irreversible, unrecoverable loss&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Algorithms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;DEFLATE, LZMA, Brotli, Zstandard &lt;/td&gt;&lt;td&gt;DCT+quantization, wavelet, MDCT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Key Techniques&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;LZ77, Huffman coding, range coding &lt;/td&gt;&lt;td&gt;Transform coding, quantization, prediction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Applications&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;ZIP, GZIP, 7Z, PNG, FLAC &lt;/td&gt;&lt;td&gt;JPEG, WebP, H.264, MP3, AAC&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best Suited For&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Text, source code, archives, medical imaging &lt;/td&gt;&lt;td&gt;Photos, video, music, streaming&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical Ratio&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Usually 2x to 5x &lt;/td&gt;&lt;td&gt;Usually 10x to 100x or higher&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Not Suitable For&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Already-compressed data (e.g., JPEG files) &lt;/td&gt;&lt;td&gt;Data requiring exact reproduction (e.g., legal text)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;q1-why-can-t-zip-compress-a-jpeg-image-&quot;&gt;Q1: Why can't ZIP compress a JPEG image?&lt;/h4&gt;
&lt;p&gt;By the time a JPEG is created, it has already gone through DCT, quantization, and entropy coding. The statistical redundancy in the data has been almost entirely eliminated. DEFLATE cannot find exploitable repetition patterns, so running ZIP on a JPEG not only fails to reduce the size but may actually add a few bytes of header overhead.&lt;/p&gt;
&lt;h4 id=&quot;q2-is-png-always-larger-than-jpeg-for-the-same-image-&quot;&gt;Q2: Is PNG always larger than JPEG for the same image?&lt;/h4&gt;
&lt;p&gt;Not necessarily. For images with large areas of solid color or a limited palette (such as logos, icons, or screenshots), PNG's filter-plus-DEFLATE combination is often smaller than JPEG and preserves perfect quality. But for color-rich natural photographs, JPEG's DCT-plus-quantization typically achieves compression ratios above 10x, making it far smaller than PNG.&lt;/p&gt;
&lt;h4 id=&quot;q3-can-you-repeatedly-compress-the-same-file-with-a-lossless-algorithm-&quot;&gt;Q3: Can you repeatedly compress the same file with a lossless algorithm?&lt;/h4&gt;
&lt;p&gt;No. Each pass of lossless compression pushes the data closer to its entropy limit. After the first pass, redundancy is largely gone, and a second pass has almost nothing left to exploit—it may even grow slightly due to added metadata. This is why &quot;compressing an already-compressed file&quot; is futile.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Lossless and lossy compression are not opposing camps but optimal choices made for different information characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lossless compression&lt;/strong&gt; serves data where &quot;not a single bit can be wrong&quot;—text, code, archives. It uses techniques like LZ77 and Huffman coding to eliminate statistical redundancy while guaranteeing perfect reconstruction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lossy compression&lt;/strong&gt; serves details &quot;the eye and ear cannot perceive&quot;—photos, video, music. It uses DCT, quantization, and inter-frame prediction to make trade-offs below the perception threshold, exchanging controlled information loss for order-of-magnitude improvements in compression ratio.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding the fundamental difference between these two paths is the cornerstone of mastering all compression technology. In subsequent articles, we will dive into the specific algorithm implementations along each path, understanding the elegance of compression line by line.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31606/lossless-vs-lossy-compression-the-core-difference-explained</guid>
<pubDate>Sun, 18 Oct 2026 11:31:34 +0000</pubDate>
</item>
<item>
<title>Build a Search Box That Won’t Show Stale Results</title>
<link>https://coderlegion.com/31595/build-a-search-box-that-wont-show-stale-results</link>
<description>&lt;p&gt;A search box can display the wrong answer even when every request succeeds.&lt;/p&gt;
&lt;p&gt;Imagine someone types &lt;code&gt;cat&lt;/code&gt;, pauses long enough to trigger a request, then adds an &lt;code&gt;s&lt;/code&gt;. The request for &lt;code&gt;cats&lt;/code&gt; finishes first. A moment later, the older &lt;code&gt;cat&lt;/code&gt; request finishes and replaces the results. The input says one thing; the page answers something else.&lt;/p&gt;
&lt;p&gt;A reliable implementation needs a clear rule: only work associated with the latest input may change the search UI. That rule must cover results, errors, and loading indicators.&lt;/p&gt;
&lt;p&gt;Here, “stale” means belonging to an older input. Data freshness inside your backend or cache is a separate concern.&lt;/p&gt;
&lt;h3 id=&quot;a-give-each-mechanism-one-job&quot;&gt;A. Give each mechanism one job&lt;/h3&gt;
&lt;p&gt;Three pieces work together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debouncing&lt;/strong&gt; delays starting a request while the user keeps typing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cancellation&lt;/strong&gt; stops an obsolete fetch when possible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A version guard&lt;/strong&gt; decides whether completed work still owns the UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Debouncing alone leaves previously started requests running. Cancellation helps with those requests, but a version guard makes the UI rule explicit, including for future asynchronous work that does not support cancellation.&lt;/p&gt;
&lt;p&gt;The subtle detail is when to advance the version: immediately when input changes. If you wait until the next request starts, an old response can still render during the debounce interval.&lt;/p&gt;
&lt;h3 id=&quot;b-start-with-a-small-page-and-an-explicit-response-shape&quot;&gt;B. Start with a small page and an explicit response shape&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;label for=&quot;query&quot;&amp;gt;Search articles&amp;lt;/label&amp;gt;
&amp;lt;input id=&quot;query&quot; type=&quot;search&quot; autocomplete=&quot;off&quot;&amp;gt;
&amp;lt;p id=&quot;status&quot; role=&quot;status&quot;&amp;gt;Type to search.&amp;lt;/p&amp;gt;
&amp;lt;ul id=&quot;results&quot; aria-busy=&quot;false&quot;&amp;gt;&amp;lt;/ul&amp;gt;
&amp;lt;script src=&quot;search.js&quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example expects a same-origin endpoint, &lt;code&gt;/api/search?q=...&lt;/code&gt;, that returns JSON like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&quot;items&quot;: [{&quot;title&quot;: &quot;Understanding browser requests&quot;}]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That endpoint is an application dependency; the browser code does not implement it. An empty search response should be &lt;code&gt;{&quot;items&quot;: []}&lt;/code&gt;. This example treats a missing or malformed body as an error.&lt;/p&gt;
&lt;h3 id=&quot;c-invalidate-first-then-schedule&quot;&gt;C. Invalidate first, then schedule&lt;/h3&gt;
&lt;p&gt;Put this first part in &lt;code&gt;search.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const input = document.querySelector(&quot;#query&quot;);
const status = document.querySelector(&quot;#status&quot;);
const results = document.querySelector(&quot;#results&quot;);

let version = 0;
let timer;
let activeController;

function scheduleSearch() {
  const mine = ++version;
  clearTimeout(timer);
  activeController?.abort();
  activeController = undefined;

  const query = input.value.trim();
  results.replaceChildren();
  results.setAttribute(&quot;aria-busy&quot;, &quot;false&quot;);

  if (!query) {
    status.textContent = &quot;Type to search.&quot;;
    return;
  }

  status.textContent = &quot;Waiting for you to finish typing…&quot;;
  timer = setTimeout(() =&amp;gt; search(query, mine), 250);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every edit clears the timer, invalidates older work, and aborts the active request. Clearing the field follows the same path, so a pending request cannot refill an intentionally empty results list.&lt;/p&gt;
&lt;p&gt;The example clears visible results immediately. If your product keeps previous results visible while loading, label which query they belong to so users do not mistake them for the current answer.&lt;/p&gt;
&lt;p&gt;The 250 ms delay is an illustrative product choice. Adjust it for your interface. Timers also do not promise exact execution times; busy execution and browser throttling can delay them. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#reasons_for_delays_longer_than_specified&quot;&gt;MDN’s timer behavior notes&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;d-guard-every-asynchronous-outcome&quot;&gt;D. Guard every asynchronous outcome&lt;/h3&gt;
&lt;p&gt;Add the request function and event listener below the first block:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function search(query, mine) {
  if (mine !== version) return;

  const controller = new AbortController();
  activeController = controller;
  status.textContent = &quot;Searching…&quot;;
  results.setAttribute(&quot;aria-busy&quot;, &quot;true&quot;);

  try {
    const params = new URLSearchParams({ q: query });
    const response = await fetch(`/api/search?${params}`, {
      signal: controller.signal
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();
    if (mine !== version || controller.signal.aborted) return;

    if (!Array.isArray(data?.items) ||
        !data.items.every(item =&amp;gt; typeof item?.title === &quot;string&quot;)) {
      throw new Error(&quot;Unexpected search response&quot;);
    }

    const rows = data.items.map(item =&amp;gt; {
      const li = document.createElement(&quot;li&quot;);
      li.textContent = item.title;
      return li;
    });
    results.replaceChildren(...rows);
    status.textContent = rows.length
      ? `${rows.length} result${rows.length === 1 ? &quot;&quot; : &quot;s&quot;}.`
      : &quot;No results found.&quot;;
  } catch (error) {
    if (mine !== version || controller.signal.aborted) return;
    status.textContent = &quot;Search failed. Edit the query to try again.&quot;;
    console.error(&quot;Search failed:&quot;, error);
  } finally {
    if (mine === version) results.setAttribute(&quot;aria-busy&quot;, &quot;false&quot;);
    if (activeController === controller) activeController = undefined;
  }
}

input.addEventListener(&quot;input&quot;, scheduleSearch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each request gets its own controller. An aborted signal cannot be reset and reused for a later request. Passing that signal to &lt;code&gt;fetch&lt;/code&gt; connects the request to cancellation. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal&quot;&gt;MDN’s AbortSignal documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;URLSearchParams&lt;/code&gt; handles query-string encoding, including characters such as &lt;code&gt;&amp;amp;&lt;/code&gt; and &lt;code&gt;+&lt;/code&gt;. Pass the original query value rather than pre-encoding it. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams&quot;&gt;MDN’s URLSearchParams reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are two asynchronous boundaries here: receiving the response and reading its JSON body. Aborting can also interrupt body consumption. The guard after &lt;code&gt;response.json()&lt;/code&gt; checks ownership before any results are rendered. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch&quot;&gt;MDN explains the fetch and response-body lifecycle&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Notice that the catch block performs the same ownership check. An obsolete request's failure should not replace a newer success with an error message. Cancellation caused by another edit is expected behavior and stays quiet.&lt;/p&gt;
&lt;p&gt;Cleanup needs ownership checks too. An older &lt;code&gt;finally&lt;/code&gt; block must not turn off a newer request's loading state or discard its controller. The version protects the loading state; the identity comparison protects the controller reference.&lt;/p&gt;
&lt;p&gt;HTTP errors deserve separate handling. A &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;500&lt;/code&gt; response does not automatically reject the fetch promise. This example checks &lt;code&gt;response.ok&lt;/code&gt;, which is true for statuses 200 through 299, and throws for unsuccessful responses. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Response/ok&quot;&gt;MDN’s Response.ok reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The response shape is validated before rendering, and titles are assigned through &lt;code&gt;textContent&lt;/code&gt;. That keeps API-provided titles in the text path instead of interpreting them as HTML. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent&quot;&gt;MDN’s textContent reference&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;e-test-the-order-of-events-deliberately&quot;&gt;E. Test the order of events deliberately&lt;/h3&gt;
&lt;p&gt;Fast local responses can hide this bug. Use controlled promises and fake timers so tests choose exactly when each request completes.&lt;/p&gt;
&lt;p&gt;Cover these scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rapid typing:&lt;/strong&gt; several edits within the debounce window produce one request for the latest value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The debounce gap:&lt;/strong&gt; request A starts, the input changes, and A resolves before request B starts. A must not render.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reversed completion:&lt;/strong&gt; B resolves, then A resolves. B's results remain visible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleared input:&lt;/strong&gt; an old response arrives after the field is emptied. The empty state remains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A stale failure:&lt;/strong&gt; A rejects while B is loading. B keeps its loading state and controller.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delayed body consumption:&lt;/strong&gt; A's response headers arrive, but its JSON body completes after a newer search. A still cannot render.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current failures:&lt;/strong&gt; HTTP errors, network errors, malformed JSON, and an unexpected response shape produce an error state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero matches:&lt;/strong&gt; an empty items array produces the no-results state rather than an error.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the ownership tests, deliberately let the fake fetch resolve even after its signal is aborted. That exercises the guard independently from cancellation. It does not represent normal aborted-fetch behavior.&lt;/p&gt;
&lt;p&gt;A deterministic harness for this example passed 12 checks covering those cases, encoding, and text assignment. It used fake DOM objects and fetch promises; it did not verify browser rendering, accessibility announcements, or a real server.&lt;/p&gt;
&lt;h3 id=&quot;f-carry-the-rule-into-production&quot;&gt;F. Carry the rule into production&lt;/h3&gt;
&lt;p&gt;If the component can be removed, add cleanup that advances the version, clears the timer, aborts active work, and removes its listeners.&lt;/p&gt;
&lt;p&gt;Also check input methods used by your audience. An IME-aware version can defer requests during composition while still invalidating obsolete work immediately. If application code sets the input value directly, call the scheduling function explicitly: programmatic value changes do not trigger the normal input event. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event&quot;&gt;MDN’s input event reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The durable rule is simple: give each search a version, and let only the current version publish an outcome. Keep that rule intact when adding caching, retries, pagination, or other asynchronous steps.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31595/build-a-search-box-that-wont-show-stale-results</guid>
<pubDate>Sun, 18 Oct 2026 10:08:35 +0000</pubDate>
</item>
<item>
<title>INP, the Core Web Vital That Replaced FID: How to Fix It</title>
<link>https://coderlegion.com/31420/inp-the-core-web-vital-that-replaced-fid-how-to-fix-it</link>
<description>&lt;p&gt;Interaction to Next Paint tracks how fast a page shows a visible reaction to a click, tap or key press. It took over from First Input Delay as a Core Web Vital on March 12, 2024. Good is 200 milliseconds or less and poor is over 500, measured at the 75th percentile of real visits. Many sites that passed FID easily fail INP because it covers every interaction in a visit and includes running your code and painting the result.&lt;/p&gt;
&lt;p&gt;In the guide I explain what counts as an interaction (clicks, taps and key presses, not scrolling or hovering) and the three phases of each one: input delay, processing duration and presentation delay. Knowing which phase is slow tells you which fix applies.&lt;/p&gt;
&lt;p&gt;Measurement starts with field data from CrUX, PageSpeed Insights and Search Console, then moves to DevTools with CPU throttling to reproduce problems. The web-vitals library's attribution build tells you which element was slow for real visitors and how long each phase took.&lt;/p&gt;
&lt;p&gt;The fixes come in three groups: break up long tasks and yield to the main thread (with scheduler.yield() where supported); cut rendering work by avoiding layout thrashing, keeping the DOM small and using content-visibility; and tame input handlers with debouncing while deferring or removing third-party scripts. I'm clear about limits too: INP isn't a ranking shortcut, field data moves slowly, and a fast spinner doesn't fix slow work.&lt;/p&gt;
&lt;h4 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Good INP is 200 ms or less at the 75th percentile&lt;/li&gt;
&lt;li&gt;Each interaction has input, processing and presentation phases&lt;/li&gt;
&lt;li&gt;Use field data first; Lighthouse doesn't measure INP&lt;/li&gt;
&lt;li&gt;Break up long tasks and yield to the main thread&lt;/li&gt;
&lt;li&gt;Debounce heavy handlers and audit third-party scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related on BuiltToWinWeb: &lt;a rel=&quot;nofollow&quot; href=&quot;https://built2winweb.com/core-web-vitals-explained/&quot;&gt;Core Web Vitals explained&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full guide on BuiltToWinWeb: &lt;a rel=&quot;nofollow&quot; href=&quot;https://built2winweb.com/blog/inp-core-web-vitals/&quot;&gt;The full INP explainer and fixes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A summary by Jacob Campbell of an article first published on built2winweb.com on October 6, 2026.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31420/inp-the-core-web-vital-that-replaced-fid-how-to-fix-it</guid>
<pubDate>Sun, 18 Oct 2026 07:09:33 +0000</pubDate>
</item>
<item>
<title>I put 64 file tools and an open-weight AI model in a browser tab. Nothing gets uploaded.</title>
<link>https://coderlegion.com/31508/i-put-64-file-tools-and-an-open-weight-ai-model-in-a-browser-tab-nothing-gets-uploaded</link>
<description>&lt;p&gt;Right after I shared Fizzdoc publicly, I opened it on my own Android phone and split a PDF.&lt;/p&gt;
&lt;p&gt;It took six minutes.&lt;/p&gt;
&lt;p&gt;Six. On my laptop the same kind of job took about a second. The link was already out in public, so somewhere out there, people were staring at a progress bar that didn't exist and thinking &quot;nice project, Rishab.&quot;&lt;/p&gt;
&lt;p&gt;I'll tell you how I fixed it. But first, what this thing even is, and why I think it fits this year's Hacktoberfest theme, &quot;AI belongs to everyone&quot;, better than I planned.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kingrishabdugar/fizzdoc/main/docs/assets/hero.png&quot; alt=&quot;Fizzdoc home page&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-is-fizzdoc-&quot;&gt;What is Fizzdoc?&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;Fizzdoc&lt;/a&gt; is 64 tools for PDF, Word, Excel, PowerPoint, images and audio. Merge, compress, convert, edit, redact, OCR, cut an MP3, turn a voice note into text. The usual suspects, in 16 languages.&lt;/p&gt;
&lt;p&gt;None of it uploads your file, though. There's no backend. Not a small one, not a &quot;we only keep it for an hour&quot; one. Nothing. It's a static site, and every job runs inside your browser tab, on your own device.&lt;/p&gt;
&lt;p&gt;Most &quot;free PDF&quot; sites work like this: you send them your passport scan (or your Aadhaar card, if you're in India), they send you back a slightly smaller one, and you hope for the best. That always felt like a strange deal for jobs a phone can do on its own. So I built the other version.&lt;/p&gt;
&lt;p&gt;Oh, and it runs an open-weight AI model (Whisper) right in the tab to turn audio into text. No API key, no GPU, no server. If AI belongs to everyone, it should also run on everyone's phone.&lt;/p&gt;
&lt;p&gt;It's open source (Apache-2.0): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;github.com/kingrishabdugar/fizzdoc&lt;/a&gt;. Everything in this post is in that repo, and every number comes from the code or something I actually measured.&lt;/p&gt;
&lt;h3 id=&quot;where-fizzdoc-fits&quot;&gt;Where Fizzdoc fits&lt;/h3&gt;
&lt;p&gt;The open-source world already has great PDF tools, and I've used several. Some are big platforms you self-host with Docker and run for a whole team, with pipelines, APIs and logins. Some are lean, browser-only PDF kits. Both are good at what they do.&lt;/p&gt;
&lt;p&gt;Fizzdoc makes a different bet:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nothing to install. Not even Docker.&lt;/strong&gt; Open a link on any phone or laptop and it works. That matters when the person who needs to unlock a PDF is your dad, not your homelab.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not just PDF.&lt;/strong&gt; 30 PDF tools, 14 for images, 14 for Word, Excel and PowerPoint, and 6 for audio, including speech to text and subtitles. Plus developer odds and ends like Excel to JSON and Mermaid to PNG.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your language, not just English.&lt;/strong&gt; 16 languages, from Spanish, French, German and Portuguese to Hindi, Bengali, Tamil and Telugu, each with its own URLs, so people can find it by searching the way they actually talk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built for cheap phones first.&lt;/strong&gt; More on that below, because I learned it the hard way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy you can verify.&lt;/strong&gt; A strict CSP and a test that fails if any request leaves the site.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need automation pipelines or an admin panel for your company, a self-hosted platform is the right call. If you need to shrink a photo to 50 KB for a government form on your phone in the next 30 seconds, that's what this is for.&lt;/p&gt;
&lt;h3 id=&quot;the-whole-thing-on-one-slide&quot;&gt;The whole thing on one slide&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0bk2ipjt7mxyyqq613yc.png&quot; alt=&quot;How Fizzdoc is built: build time, static host, browser tab&quot;&gt;&lt;/p&gt;
&lt;p&gt;Three layers, and only the last one ever touches your file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build time.&lt;/strong&gt; One TypeScript file, &lt;code&gt;src/site.ts&lt;/code&gt;, describes all 64 tools: slug, formats, options, page text. A renderer turns that into about 1,050 plain HTML pages (64 tools × 16 languages, plus a few extras), with the sitemap and &lt;code&gt;llms.txt&lt;/code&gt; thrown in. Adding a tool is one registry entry, one engine function and tests. The pages write themselves, which is the only kind of page I enjoy writing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hosting.&lt;/strong&gt; Cloudflare Pages. It serves files and answers GET requests. That's its entire personality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your browser.&lt;/strong&gt; A small script handles the boring parts: picking files, options, progress, download. It has no idea what a PDF is. When you press the button, it borrows a specialist:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Engine &lt;/th&gt;&lt;th&gt;Job &lt;/th&gt;&lt;th&gt;Download (gzipped)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;qpdf (C++, compiled to WebAssembly) &lt;/td&gt;&lt;td&gt;merge, split, rotate, passwords &lt;/td&gt;&lt;td&gt;~440 KB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;pdf.js &lt;/td&gt;&lt;td&gt;drawing pages, reading text &lt;/td&gt;&lt;td&gt;~390 KB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;pdf-lib &lt;/td&gt;&lt;td&gt;page numbers, watermarks, building PDFs &lt;/td&gt;&lt;td&gt;small&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tesseract.js &lt;/td&gt;&lt;td&gt;OCR &lt;/td&gt;&lt;td&gt;~5.8 MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Whisper tiny &lt;/td&gt;&lt;td&gt;speech to text &lt;/td&gt;&lt;td&gt;~58 MB, once&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;fflate &lt;/td&gt;&lt;td&gt;Word, Excel, PowerPoint (secretly ZIP files) &lt;/td&gt;&lt;td&gt;small&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mermaid &lt;/td&gt;&lt;td&gt;diagrams to PNG/SVG &lt;/td&gt;&lt;td&gt;per diagram type&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;-just-use-a-server-bro-&quot;&gt;&quot;Just use a server, bro&quot;&lt;/h3&gt;
&lt;p&gt;I know. A server would have been easier. I chose pain for three reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can check it.&lt;/strong&gt; &quot;We delete your files&quot; is a promise. This is a rule your browser enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
connect-src 'self';
img-src 'self' data: blob:;
object-src 'none'; base-uri 'none'; form-action 'none'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;connect-src 'self'&lt;/code&gt; means the page can only talk to fizzdoc.com, and fizzdoc.com only serves the website. Open DevTools, go to Network, run a job, and watch nothing interesting happen. The test suite does the same thing on every pull request and fails if a single request goes anywhere else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It's free to run.&lt;/strong&gt; A file-processing server gets more expensive with every user. A static site doesn't care how popular it gets. That's the only reason I can say &quot;free&quot; without a pricing page hiding behind it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No waiting in line.&lt;/strong&gt; No upload bar, no &quot;processing on our servers&quot;, no download. Your file goes from your disk to your RAM and back.&lt;/p&gt;
&lt;p&gt;The catch is obvious: your device does the work, and not everyone has a laptop. Which brings us back to my six-minute disaster.&lt;/p&gt;
&lt;h3 id=&quot;the-android-bug&quot;&gt;The Android bug&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/qpdf/qpdf&quot;&gt;qpdf&lt;/a&gt; runs on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/emscripten-core/emscripten&quot;&gt;Emscripten&lt;/a&gt;, which gives it a small pretend file system. I was handing it your file through &lt;strong&gt;WORKERFS&lt;/strong&gt;, which reads the file lazily, only when qpdf asks for a piece. Sounds efficient, very &quot;don't load what you don't need&quot;, and I was proud of it.&lt;/p&gt;
&lt;p&gt;What I didn't know: qpdf doesn't ask for a few big pieces. It asks for lots and lots of tiny ones. On a 10 MB, 28-page test PDF, I counted &lt;strong&gt;1,448 separate reads&lt;/strong&gt;: 434 just to count the pages, and 1,014 more for the split.&lt;/p&gt;
&lt;p&gt;On a laptop, 1,448 small reads is nothing. On Android, reading a picked file in small pieces is slow, and 1,448 slow things in a row add up to minutes. My &quot;efficient&quot; design was death by a thousand paper cuts (literally, since it was a PDF).&lt;/p&gt;
&lt;p&gt;The fix was almost embarrassing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// qpdf reads its input in thousands of small pieces, and on Android every small read
// of a picked file is slow. Only inputs too big to copy safely are still read lazily.
const COPY_UP_TO = 128 * 1024 * 1024;

if (totalSize &amp;lt;= COPY_UP_TO) await copyIn(q, files, run); // read once, in order, into memory
else q.FS.mount(q.WORKERFS, { blobs }, '/in');            // huge files: no second copy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/j87ktfnpwl52ai3a3sml.png&quot; alt=&quot;How a PDF gets into memory: budget check, then copy or lazy read&quot;&gt;&lt;/p&gt;
&lt;p&gt;If your files add up to 128 MB or less, the worker reads them once, start to finish, into memory. All those tiny reads now hit RAM, which doesn't care. Same test file: 1,448 reads became zero, and even on my laptop the split went from 1.4 s to 0.25 s. So the bug was slowing everyone down; phones were just louder about it.&lt;/p&gt;
&lt;p&gt;Above 128 MB, making a second full copy could run a phone out of memory, so big files still get the lazy path. Slower, but it finishes. I tested that with a 140 MB PDF, and yes, I waited.&lt;/p&gt;
&lt;h3 id=&quot;being-nice-to-a-8-000-phone&quot;&gt;Being nice to a ₹8,000 phone&lt;/h3&gt;
&lt;p&gt;That bug taught me to design for the cheapest phone first, not my laptop. This is what changed because of it.&lt;/p&gt;
&lt;h4 id=&quot;load-almost-nothing&quot;&gt;Load almost nothing&lt;/h4&gt;
&lt;p&gt;The first page load is about &lt;strong&gt;15 KB of gzipped JavaScript and 7 KB of CSS&lt;/strong&gt;. No framework. Every page is prerendered HTML, so you can read it before any script runs (Google can too).&lt;/p&gt;
&lt;p&gt;The Inter font is split into 7 &lt;code&gt;unicode-range&lt;/code&gt; pieces with &lt;code&gt;font-display: swap&lt;/code&gt;, so your browser only downloads the characters a page actually uses, and shows text in a system font while it waits. A tiny &lt;code&gt;theme.js&lt;/code&gt; sets dark mode before the first paint, so no white flash at 2 a.m. Hashed files are cached for a year, so the second visit is mostly just HTML.&lt;/p&gt;
&lt;h4 id=&quot;load-engines-only-when-asked&quot;&gt;Load engines only when asked&lt;/h4&gt;
&lt;p&gt;Each tool's engine hides behind a dynamic &lt;code&gt;import()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;'compress-pdf': async (f, o) =&amp;gt; (await import('./engine/compress')).compressPdf(f[0], { ... }),
'ocr-pdf':      async (f) =&amp;gt; (await import('./engine/ocr')).ocrPdf(f[0], progressLabel),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If all you do is rotate a PDF, you never download the OCR engine or a 58 MB speech model. Mermaid is even pickier: every diagram type is its own chunk, so drawing a flowchart doesn't drag in the code for sequence diagrams.&lt;/p&gt;
&lt;h4 id=&quot;check-the-budget-before-you-spend-it&quot;&gt;Check the budget before you spend it&lt;/h4&gt;
&lt;p&gt;Before a job starts, the page does a quick sanity check:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// 128 MiB per GiB of reported device memory, capped at 1 GiB.
function inputBudget() {
  const gib = navigator.deviceMemory;
  return gib ? Math.min(gib * 128, 1024) * MiB : 512 * MiB;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 4 GB phone gets 512 MiB, an 8 GB laptop gets 1 GiB. Firefox and Safari don't report device memory, so they get a sensible 512 MiB. The cap exists because qpdf's WebAssembly memory tops out at 2 GiB and a job needs room for more than its input.&lt;/p&gt;
&lt;p&gt;Is it a perfect measurement? No, and the code comment admits that. But &quot;this file is too big for this device&quot; is a much better message than a tab that quietly dies after five minutes.&lt;/p&gt;
&lt;h4 id=&quot;show-real-progress&quot;&gt;Show real progress&lt;/h4&gt;
&lt;p&gt;A spinner on a slow phone is basically a &quot;maybe&quot; button. So the progress bar is real.&lt;/p&gt;
&lt;p&gt;The first 30% is the file being read into memory, byte by byte. The other 70% comes from qpdf itself. qpdf has a &lt;code&gt;--progress&lt;/code&gt; flag that prints lines like &lt;code&gt;write progress: 42%&lt;/code&gt;. This WebAssembly build ignores the option meant to catch that output, so it just goes to &lt;code&gt;console.log&lt;/code&gt;. Okay then, I'll grab it from there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;console.log = (...args) =&amp;gt; {
  const pct = /: write progress: (\d+)%$/.exec(String(args[0]))?.[1];
  if (pct === undefined) return log(...args);
  post({ type: 'progress', fraction: READ_SHARE + ((1 - READ_SHARE) * Number(pct)) / 100 });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is hijacking &lt;code&gt;console.log&lt;/code&gt; elegant? Absolutely not. Does the bar move now? It does. I sleep fine.&lt;/p&gt;
&lt;h4 id=&quot;one-job-one-worker-then-gone&quot;&gt;One job, one worker, then gone&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kxguikttab6kqgem8a6m.png&quot; alt=&quot;Life of a job: worker per job, terminate to clean up&quot;&gt;&lt;/p&gt;
&lt;p&gt;Every qpdf job gets its own fresh Web Worker, so the page stays smooth while it works. When the job ends, or you hit Cancel, the page calls &lt;code&gt;worker.terminate()&lt;/code&gt;. That one line throws away the file, any password you typed, and the engine's whole memory, so the cancel button doubles as cleanup and leak prevention. No state survives between jobs, so memory doesn't slowly creep up while you merge your way through tax season.&lt;/p&gt;
&lt;p&gt;Before running, the worker also asks qpdf what's in the file: bookmarks, forms, signatures, encryption. If you merge a signed PDF, Fizzdoc tells you the signature won't be valid anymore, instead of letting you find out from your CA.&lt;/p&gt;
&lt;h4 id=&quot;only-draw-what-s-on-screen&quot;&gt;Only draw what's on screen&lt;/h4&gt;
&lt;p&gt;The editor and the redaction tool show every page of your PDF. On a 200-page file that could mean 200 canvases fighting for memory. Instead, a page is drawn only when it's about to scroll into view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const io = new IntersectionObserver(
  (entries) =&amp;gt; { for (const e of entries) if (e.isIntersecting) renderPage(pages[e.target.dataset.index]); },
  { root: scroller, rootMargin: '600px 0px' },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tools that must touch every page (PDF to JPG, scanned PDF, OCR) go one page at a time and clean each page up before moving on.&lt;/p&gt;
&lt;h4 id=&quot;do-less-work&quot;&gt;Do less work&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MP3 and M4A are cut without re-encoding.&lt;/strong&gt; An MP3 is a row of little frames, each saying how long it is. Cutting is just picking the frames between two timestamps. No decoding, no quality loss, and fast even on a phone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression never makes things bigger.&lt;/strong&gt; Each image inside a PDF is kept only if the new version is smaller. If the whole file doesn't shrink, you get your original back, not a &quot;compressed&quot; file that's 3% larger. (We've all seen that one.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Compress image to 50 KB&quot; is smart about it.&lt;/strong&gt; It keeps the photo large and lowers quality first, never below 50% while it's 1000 px or wider, using a 6-step binary search. Only if that can't fit does it shrink the size. Roughly how messaging apps do it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;and-when-it-s-slow-anyway-say-so&quot;&gt;And when it's slow anyway, say so&lt;/h4&gt;
&lt;p&gt;While a job runs, the page tells you speed depends on your device. If something takes over 20 seconds or fails, a &quot;Slow or not working? Report it on GitHub&quot; link lights up. It opens a ready-made issue with the tool, browser, time taken, file size, and the device's RAM and CPU cores. Never the file, never its name. That link is how I find the next bug instead of guessing.&lt;/p&gt;
&lt;p&gt;What's still not great: PDF to JPG keeps every page image in memory until the ZIP is built (a 1,000-page PDF on a phone would be a bad day), some jobs still run on the main thread, and Whisper runs on a single thread. Speaking of which.&lt;/p&gt;
&lt;h3 id=&quot;speech-to-text-without-a-server&quot;&gt;Speech to text, without a server&lt;/h3&gt;
&lt;p&gt;This was the feature I wasn't sure a static site could pull off.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6uflsc2txovtu1c3y9we.png&quot; alt=&quot;Speech to text pipeline: download once, cache, decode, 30 s windows&quot;&gt;&lt;/p&gt;
&lt;p&gt;Fizzdoc runs Whisper tiny (8-bit quantized) with transformers.js and ONNX Runtime, on the CPU, inside a worker. It took three fights to get there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Pages allows 25 MiB per file.&lt;/strong&gt; The model is bigger. So a build script chops the model into parts. The first time you use the tool, the browser downloads the parts, glues them back together and keeps them in Cache Storage. After that it starts in seconds, even offline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My own security rules blocked it.&lt;/strong&gt; ONNX Runtime likes to load its WebAssembly through a &lt;code&gt;blob:&lt;/code&gt; URL, which my CSP refuses. So the worker reads the file from the cache itself and hands it over directly. Remote model loading is switched off too, so it can only read from fizzdoc.com.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nobody trusts a spinner for 58 MB.&lt;/strong&gt; The first run says &quot;Preparing… one-time setup: 23 of 58 MB&quot; with a real byte counter, and tells you it only happens once.&lt;/p&gt;
&lt;p&gt;Audio is turned into 16 kHz mono (what Whisper expects) and fed in 30-second windows with 5 seconds of overlap, so words at the edges don't get chopped in half.&lt;/p&gt;
&lt;p&gt;Whisper tiny is, well, tiny. Clear English comes out well. Hinglish, sadly, does not. Give it Hindi and English in the same sentence and it answers with great confidence and very little accuracy. The tool now says it works best on clear English. Multi-threading would help speed, but it needs cross-origin isolation, and my first attempt made the worker hang. So it's single-threaded for now. If you've solved this, please come find me.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/js47lvxn8sgv6bnsux5k.png&quot; alt=&quot;Audio to Text page&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;smaller-decisions-i-d-defend-in-a-code-review&quot;&gt;Smaller decisions I'd defend in a code review&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Redaction that actually redacts.&lt;/strong&gt; Drawing a black box over text doesn't remove it. People still do that, and people still copy the text out from under the box. Fizzdoc turns each page into an image with the boxes burned in, then builds a new PDF from those images. The text is really gone. The trade-off is the result isn't selectable text, and the tool says so. There's also an optional &quot;find personal info&quot; button that marks emails, UPI IDs, phone numbers, PAN, IFSC, IBAN and long ID numbers. It's plain regex, not AI, and you check every mark before saving.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editing text in place, in the PDF's own font.&lt;/strong&gt; Click a line in a PDF and retype it. On save, Fizzdoc parses the page's content stream, tracks the text state (matrices, font, spacing) to find the exact operators that draw that line, and rewrites them using the font that's already embedded in the file. Same font, same size, same colour, no white box. The catch: embedded fonts are usually subsets, so you can only use letters the file already contains. Change &quot;Rs 4,250&quot; to &quot;Rs 2,450&quot;? Easy. Type a &quot;Z&quot; into a PDF that never had one? The glyph simply isn't there, so that line falls back to covering the old text and drawing new text in the closest standard font. Hindi and Arabic always take the fallback too, because their glyphs are drawn in a different order than they're typed, and I'd rather be boring than garbled.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0njstg0epqbd56v90zy2.png&quot; alt=&quot;Before and after: three lines edited, same fonts and colours&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Word to PDF via the print dialog.&lt;/strong&gt; Drawing Hindi, Tamil or Arabic correctly inside a JavaScript PDF writer is a whole career. Every browser already ships a great PDF writer: Print → Save as PDF. So Fizzdoc turns your .docx into clean HTML and opens the print dialog. One extra click, and every script comes out right. Lazy, yes, but it works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;qpdf instead of a JavaScript PDF library.&lt;/strong&gt; I started with pdf-lib for merging. It works, but copying pages that way drops bookmarks, form fields and internal links. qpdf rewrites the file's structure instead, and has been doing it reliably for years.&lt;/p&gt;
&lt;h3 id=&quot;bonus-your-coding-agent-can-use-it-too&quot;&gt;Bonus: your coding agent can use it too&lt;/h3&gt;
&lt;p&gt;Hacktoberfest is big on agents this year, so here's a small one. The repo has an MCP server (in &lt;code&gt;/mcp&lt;/code&gt;) that runs the same qpdf and audio engines locally. Hook it up to Claude Code, Codex or Cursor and you can say &quot;merge these PDFs&quot; or &quot;cut the first 30 seconds of this MP3&quot;, and the agent does it on your machine. No upload, no throwaway Python script, and it never overwrites your files.&lt;/p&gt;
&lt;h3 id=&quot;lots-of-languages-one-source-file&quot;&gt;Lots of languages, one source file&lt;/h3&gt;
&lt;p&gt;Each language has real URLs (&lt;code&gt;/hi/merge-pdf/&lt;/code&gt;, &lt;code&gt;/ta/compress-pdf/&lt;/code&gt;), translated buttons, FAQs and error messages, and hreflang links between them: Hindi, Bengali, Marathi, Tamil, Telugu, Spanish, Portuguese, French, German, Italian, Dutch, Polish, Turkish, Indonesian, Vietnamese and English.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/kingrishabdugar/fizzdoc/main/docs/assets/lang.gif&quot; alt=&quot;Fizzdoc in Hindi&quot;&gt;&lt;/p&gt;
&lt;p&gt;Because every page is generated, the tests can check all ~1,050 of them for their CSP, title, description, canonical link, a single &lt;code&gt;h1&lt;/code&gt; and valid structured data. I'm not checking 1,050 pages by hand.&lt;/p&gt;
&lt;h3 id=&quot;tests-because-i-like-sleeping&quot;&gt;Tests, because I like sleeping&lt;/h3&gt;
&lt;p&gt;209 unit tests and 1,087 browser tests. Engines are plain functions (bytes in, bytes out), so most run in Node against small weird files: encrypted PDFs, forms, bookmarks, MP3s. The browser tests click through the real UI, including the &quot;no request leaves the site&quot; check. CI runs both on every pull request, and nothing merges red.&lt;/p&gt;
&lt;h3 id=&quot;come-build-it-with-me&quot;&gt;Come build it with me&lt;/h3&gt;
&lt;p&gt;Right now Fizzdoc is one person and a lot of tabs. There's plenty left, and some of it is really fun engineering. Hacktoberfest doesn't count pull requests anymore, which honestly suits me. I'd much rather have one good PR that someone understands than ten that just exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good first issues&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/17&quot;&gt;Crop PDF&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/18&quot;&gt;Flatten PDF&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/19&quot;&gt;Split every N pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/6&quot;&gt;Pick the DPI for PDF → JPG&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/8&quot;&gt;watermark position and colour&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/22&quot;&gt;Word → Markdown / HTML&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Translations: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/1&quot;&gt;Japanese&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/3&quot;&gt;Korean, Gujarati, Kannada, Malayalam, Punjabi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bigger ones&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/10&quot;&gt;Sign PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/13&quot;&gt;OCR in more languages&lt;/a&gt;, downloaded on demand like Whisper&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/11&quot;&gt;Install it as an offline app (PWA)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/12&quot;&gt;Page thumbnails with drag and drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/42&quot;&gt;WAV, FLAC, OGG and OPUS&lt;/a&gt; in the audio tools&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/44&quot;&gt;Remove image background&lt;/a&gt;, on the device&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/5&quot;&gt;Right-to-left languages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Problems I'd love your brain on&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster Whisper: threads (COOP/COEP without the hang) or WebGPU. If you've shipped either, tell me everything.&lt;/li&gt;
&lt;li&gt;A bigger or multilingual open-weight speech model that still fits a phone, so Hinglish stops being a comedy show.&lt;/li&gt;
&lt;li&gt;More MCP tools: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues/61&quot;&gt;the image, OCR, redaction and Office engines in Node&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Streaming the ZIP for PDF to JPG, so huge PDFs don't sit in memory.&lt;/li&gt;
&lt;li&gt;Real text deletion in Edit PDF, rewriting the content stream instead of covering it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setup takes about as long as reading this sentence twice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git clone https://github.com/kingrishabdugar/fizzdoc
cd fizzdoc
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CONTRIBUTING.md shows how a tool goes from the registry to its engine. Small pull requests get reviewed fastest. Not sure where to start? Comment on any &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc/issues&quot;&gt;issue&lt;/a&gt; and I'll help you pick one.&lt;/p&gt;
&lt;h3 id=&quot;that-s-it&quot;&gt;That's it&lt;/h3&gt;
&lt;p&gt;Try it at &lt;a rel=&quot;nofollow&quot; href=&quot;https://fizzdoc.com&quot;&gt;fizzdoc.com&lt;/a&gt;. Throw your weirdest PDF at it. If it's slow on your phone, hit the report link and tell me which phone, so I can have another humbling morning.&lt;/p&gt;
&lt;p&gt;And if you think it's useful, a star on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kingrishabdugar/fizzdoc&quot;&gt;kingrishabdugar/fizzdoc&lt;/a&gt; helps more people find it. It also makes me unreasonably happy.&lt;/p&gt;
&lt;p&gt;Thanks for reading.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31508/i-put-64-file-tools-and-an-open-weight-ai-model-in-a-browser-tab-nothing-gets-uploaded</guid>
<pubDate>Sat, 17 Oct 2026 11:18:58 +0000</pubDate>
</item>
<item>
<title>Canvas cropping: keep source pixels separate from output size</title>
<link>https://coderlegion.com/31492/canvas-cropping-keep-source-pixels-separate-from-output-size</link>
<description>&lt;p&gt;A crop box and an export size describe two different things. The crop box chooses pixels from the decoded source; the export size decides how many pixels to write. Keeping those numbers separate makes an image editor much easier to reason about.&lt;/p&gt;
&lt;h3 id=&quot;start-with-image-pixels-not-the-preview-rectangle&quot;&gt;Start with image pixels, not the preview rectangle&lt;/h3&gt;
&lt;p&gt;Suppose the decoded image is 320 × 240 pixels. You want the region beginning at &lt;code&gt;(40, 24)&lt;/code&gt;, with a width of 200 and a height of 160. That rectangle fits because &lt;code&gt;40 + 200 &amp;lt;= 320&lt;/code&gt; and &lt;code&gt;24 + 160 &amp;lt;= 240&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The preview might be displayed at half size. In that case, a pointer position of 20 CSS pixels from its left edge represents 40 source pixels. Convert pointer coordinates using the actual displayed image rectangle before storing the crop. If the image is letterboxed inside a larger container, use the image rectangle rather than the container, or subtract the padding first.&lt;/p&gt;
&lt;p&gt;For an image drawn without rotation, the conversion is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const sourceX = (pointerX - imageRect.left) * source.width / imageRect.width;
const sourceY = (pointerY - imageRect.top) * source.height / imageRect.height;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clamp the final rectangle to the source bounds and choose an explicit rounding rule. Do not let a partially outside selection silently become an unexpected export.&lt;/p&gt;
&lt;h3 id=&quot;pass-both-rectangles-to-canvas&quot;&gt;Pass both rectangles to Canvas&lt;/h3&gt;
&lt;p&gt;The nine-argument form of &lt;code&gt;drawImage&lt;/code&gt; keeps the two jobs visible. For that 200 × 160 crop, a 500 × 400 export preserves the 5:4 aspect ratio:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;canvas.width = 500;
canvas.height = 400;
ctx.drawImage(source, 40, 24, 200, 160, 0, 0, 500, 400);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first rectangle belongs to the source image. The second belongs to the destination canvas. Setting the canvas bitmap dimensions matters: changing only its CSS width changes the preview, not the exported pixel dimensions.&lt;/p&gt;
&lt;p&gt;Validate that the dimensions are positive and the crop fits the decoded image. Also apply a pixel budget before allocating a large canvas. A very long, narrow image and a square image can have the same pixel count but different side-length constraints, so check both.&lt;/p&gt;
&lt;h3 id=&quot;encode-the-result-then-inspect-it&quot;&gt;Encode the result, then inspect it&lt;/h3&gt;
&lt;p&gt;Export through &lt;code&gt;canvas.toBlob&lt;/code&gt;, handle a null result, and check the returned type. For PNG, the browser can preserve transparency. JPEG is useful when a photographic image needs a smaller raster file, but a quality setting does not change the crop geometry. When decoding an external URL, its cross-origin policy can affect whether the canvas is exportable; a local file selected by the user avoids that particular remote-origin workflow.&lt;/p&gt;
&lt;h3 id=&quot;keep-svg-tracing-as-a-separate-decision&quot;&gt;Keep SVG tracing as a separate decision&lt;/h3&gt;
&lt;p&gt;Resizing a raster image does not turn its pixels into vector paths. Tracing is another step, with choices about color detail and smoothing. For a flat-color motif, crop away irrelevant borders first, trace it, and compare the original and vector preview at more than one zoom level. Look at small gaps, corners and separate color regions before downloading.&lt;/p&gt;
&lt;p&gt;A useful no-setup way to explore that sequence is the &lt;a rel=&quot;nofollow&quot; href=&quot;https://jpgtosvg.io/image-editor/&quot;&gt;browser image workspace in JPG to SVG&lt;/a&gt;: crop or resize a local image, then send it to tracing without selecting the file again. Processing stays in the browser, and the SVG output contains paths. I help maintain the product.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/31492/canvas-cropping-keep-source-pixels-separate-from-output-size</guid>
<pubDate>Sat, 17 Oct 2026 06:37:13 +0000</pubDate>
</item>
<item>
<title>Coding Is More Than Just Writing Code</title>
<link>https://coderlegion.com/31270/coding-is-more-than-just-writing-code</link>
<description>&lt;p&gt;For me, coding has always been about more than just writing lines of code. It’s about solving problems, understanding how things work, and finding better ways to turn ideas into something useful.&lt;/p&gt;
&lt;p&gt;Working with Java has taught me the importance of patience and logical thinking. Sometimes a problem takes only a few minutes to solve, while other times it takes hours of debugging to figure out what went wrong. But those challenging moments are often where the most learning happens.&lt;/p&gt;
&lt;p&gt;One thing I’ve realized is that becoming a better developer doesn’t happen overnight. It takes practice, curiosity, and a willingness to learn from mistakes. Reading documentation, experimenting with new approaches, and working on real projects can teach you things that tutorials alone cannot.&lt;/p&gt;
&lt;p&gt;I also believe that clean and maintainable code matters. Getting something to work is one thing, but writing code that another developer can easily understand and improve is just as important.&lt;/p&gt;
&lt;p&gt;There is always something new to learn in software development. Even when you feel comfortable with a language or a particular technology, there are new tools, better practices, and different approaches worth exploring.&lt;/p&gt;
&lt;p&gt;My goal is to keep improving my Java development skills, build practical applications, and learn from the challenges I face along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At the end of the day, being a developer isn't about knowing everything. It's about staying curious, solving problems, and getting a little better with every project.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31270/coding-is-more-than-just-writing-code</guid>
<pubDate>Fri, 16 Oct 2026 03:22:14 +0000</pubDate>
</item>
<item>
<title>Cryptocurrency Emerges in Korea</title>
<link>https://coderlegion.com/31225/cryptocurrency-emerges-in-korea</link>
<description>&lt;p&gt;Salaam! Waheguru.  Recently there was a major development in the world of cryptocurrency and blockchain.  A large Korean bank named JB is now allowing mom and pop liquor store owners to walk into bank branches with cryptocurrency and walk out with cold hard cash because of new program which uses Bitcoin as bank collateral for loans.&lt;/p&gt;
&lt;p&gt;How is this different from Chase Bank?&lt;br&gt;
Other banks do not allow mom and pop commercial businesses to use Btc as collateral.  Other banks only allow that for hedge funds or sovereign wealth funds, major whales.  One cannot walk into a Chase branch with Btc and walk out with cash like at JB.  So, we think this is a major development in the world of finance and expect all banks to follow soon depending on their domicile.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
 function konnichihaFreeGaza() {&lt;br&gt;
  if (LTV &amp;lt; 0.4500000000001) {&lt;br&gt;
   document.getElementById('israelGottaBeGoneByeBye').innerText = 'Loan Approved!  ' + loanAmt + '  Mexican Pesos received';&lt;br&gt;
 }&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;The JB program uses 0.45 Ltv or so as we see in the ensample code.  This technique, a grand master maneuver used by JB in Korea, can be used across Africa and the Middle East because cryptocurrency is fully Sharia compliant.&lt;/p&gt;
&lt;p&gt;How does this Korean development in the headlines relate to coders and scripters?&lt;br&gt;
This is the final crack in the dam. Now comes the flood of cryptocurrency jobs for coders.  It is good to learn about tokenomics as a coder, find out about Polkadot, know all about Monero.  Cryptocurrency is an automated wealth generator and is the next big thing, more popular than Jesus.&lt;/p&gt;
&lt;p&gt;function rafahRising() {&lt;br&gt;
 if (rafah = 'right') {&lt;br&gt;
  if (jebus = 'wrong') {&lt;br&gt;
   bible = 'fake';&lt;br&gt;
   crypto = 'gettin dough for yall broke bleep';&lt;br&gt;
  }&lt;br&gt;
 }&lt;br&gt;
 else {&lt;br&gt;
   msg = 'VeganzRule-MeatstersDwooool';&lt;br&gt;
   msg2 = 'Par la barbe du le Prophet, Allahu akbar!';&lt;br&gt;
   msg3 = 'Waheguru!';&lt;br&gt;
  }&lt;br&gt;
 }&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;At our NA meetings we hear often: Why crypto ya'll?  We understand blockchain.  We love blockchain.  But crypto makes no sense, our Ai said it is a scam! Now pass the coffee!&lt;br&gt;
How can one respond to such a trifling NA member?  One simple solution is to simply to the simpleton this story:&lt;br&gt;
Once upon a time there was a mighty kingdom called Palestine.  Or Phillistine if you prefer.  In the kingdom were three brothers and they held a feast for Ramadan.  At the feast they made wine from water using fruit.  It was for animal medicinal wine.  Like for a sick pet rattlesnake.  Also, there was a competition to see which brother was the best in all of Palestine at his technology.  The first brother was a cuisine expert.  Some coffee was brought forth and everyone enjoyed it except Raj.  He scrunched his face and nearly vomited saying, &quot;This smells like dead rotting bodies.  Worst coffee ever!!&quot;  Investigations were made and the coffee plantation is located next to a crematorium.  This impressed the judges.   The next brother was served rubus leaf suntea and he nearly passed out.  When they asked him what was wrong he said the smell of dead animals overwhelmed him.  He pointed to the waitress and said she stank like sheep blood.  The waitress admitted that she was raised on a farm and did eat meat as a child. This revelation turned many heads and quite a few stomachs.   &lt;/p&gt;
&lt;p&gt;The third mighty Palestinian brother was a coder.  After the feast they made afine bed for the coder and he had 7 matresses.  However in the middle of the night he howled in pain and the mansion was thrust into an uproar.  He had a deep mark on his leg!! It was as if a ghost, perhaps the Holy Ghost, had slashed him.  Investigations were made and a hair was found under the 6th mattress.  So, which brother was the most expert? The cuisine expert, the human relations / staffing expert?  Or the coding expert??  Which won the contest and was voted as ichiban #1 expert and life of the party?&lt;/p&gt;
&lt;p&gt;Answer: Not the first brother.  He might have known ahead of time where the coffee came from.  Not the second brother because he might have known about the waitress and her nasty past.  The third brother, the Linux coder is most expert!  Because, the Linux coder showed proof, evidence.  Cryptocurrency is like that, it fights counterfeits and reveals Linux's advantages in scripting.&lt;/p&gt;
&lt;p&gt;Let us all learn from JB Bank and salute their prowess!  Next: Singapore and Switzerland.  Then, the Sekai!!  &lt;/p&gt;
&lt;p&gt;function boogieDown() {&lt;br&gt;
window.location.hash = 'up';&lt;br&gt;
msg = 'getDatScrill'; }&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31225/cryptocurrency-emerges-in-korea</guid>
<pubDate>Thu, 15 Oct 2026 16:07:56 +0000</pubDate>
</item>
<item>
<title>Demystifying Greedy Algorithms: How Local Choices Solve Global Problems in Node.js</title>
<link>https://coderlegion.com/31180/demystifying-greedy-algorithms-how-local-choices-solve-global-problems-in-node-js</link>
<description>&lt;h3 id=&quot;what-is-a-greedy-algorithm-&quot;&gt;What is a Greedy Algorithm?&lt;/h3&gt;
&lt;p&gt;A greedy algorithm is an algorithmic strategy that solves a problem by making the most optimal decision at each individual step. Instead of looking ahead to long-term consequences or evaluating every possible combination, it focuses exclusively on immediate, short-term gain. If local choices logically compound into the ideal final result, the greedy strategy succeeds with maximum computational efficiency.&lt;/p&gt;
&lt;h3 id=&quot;the-cashier-s-coin-trick&quot;&gt;The Cashier’s Coin Trick&lt;/h3&gt;
&lt;p&gt;Imagine you are a cashier at a grocery store, and you need to give a customer 41 cents in change using the fewest coins possible. You look at your cash register, which contains quarters (25¢), dimes (10¢), nickels (5¢), and pennies (1¢).&lt;/p&gt;
&lt;p&gt;Instead of calculating every conceivable combination of coins, you naturally make the &quot;greedy&quot; choice at each step:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Grab the largest coin that does not exceed 41 cents: a Quarter (25¢). You now owe 16 cents.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 16 cents: a Dime (10¢). You now owe 6 cents.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 6 cents: a Nickel (5¢). You now owe 1 cent.&lt;/li&gt;
&lt;li&gt;Grab the largest coin that does not exceed 1 cent: a Penny (1¢). You now owe 0 cents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You solved the problem instantly by picking the biggest immediate payoff at every step. That is precisely how a greedy algorithm operates in software.&lt;/p&gt;
&lt;h3 id=&quot;why-greedy-algorithms-matter-in-tech&quot;&gt;Why Greedy Algorithms Matter in Tech&lt;/h3&gt;
&lt;p&gt;In software engineering, computing the mathematically perfect solution for massive datasets can sometimes take hours—or even years—if an application tries to test every possible outcome. Software developers intentionally use greedy algorithms when a guaranteed quick answer that is optimal saves massive amounts of CPU memory and processing time.&lt;/p&gt;
&lt;p&gt;For instance, network routing protocols use greedy choices to route internet data packets along the shortest immediate paths between routers. Database query engines, file compression utilities like ZIP tools, and job schedulers in operating systems all rely on greedy strategies to keep web software responsive and performant under heavy traffic loads.&lt;/p&gt;
&lt;h3 id=&quot;code-example-making-change-in-node-js&quot;&gt;Code Example: Making Change in Node.js&lt;/h3&gt;
&lt;p&gt;Here is a plain Node.js implementation showing how a greedy approach quickly calculates coin change for any given amount:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Function to give change using the minimum number of coins
function makeChange(amount, coins = [25, 10, 5, 1]) {
  const result = {};
  let remaining = amount;

  // Iterate through available coins sorted from highest to lowest
  for (const coin of coins) {
    if (remaining &amp;lt;= 0) break;

    // Calculate how many coins of this denomination fit
    const count = Math.floor(remaining / coin);
    if (count &amp;gt; 0) {
      result[`${coin}c`] = count;
      remaining -= count * coin;
    }
  }

  return result;
}

// Example usage: Change for 87 cents
const change = makeChange(87);
console.log('Change breakdown:', change);
// Output: Change breakdown: { '25c': 3, '10c': 1, '1c': 2 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this code, the algorithm loops through sorted coin values and takes as many of the highest denomination as possible before moving to smaller coins.&lt;/p&gt;
&lt;h3 id=&quot;the-takeaway-1&quot;&gt;The Takeaway&lt;/h3&gt;
&lt;p&gt;Greedy algorithms trade exhaustive foresight for immediate speed, proving that making the best local decision at every step can often yield the best global outcome. While they do not work for every single problem—especially when early sacrifices yield better long-term rewards—understanding when to apply a greedy strategy is a fundamental skill for building high-performance backend systems.&lt;/p&gt;
&lt;hr&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;a href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer nofollow&quot;&gt;
    &lt;img src=&quot;https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&amp;amp;logo=buy-me-a-coffee&amp;amp;logoColor=black&quot; alt=&quot;Buy Me A Coffee&quot;&gt;
&lt;p&gt;  &lt;/p&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Enjoyed this breakdown? Support my open-source work on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.buymeacoffee.com/saurav_tb_pandey&quot;&gt;Buy Me a Coffee&lt;/a&gt; or explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/react-hook-lab&quot;&gt;react-hook-lab&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/understanding-greedy-algorithms-simple.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31180/demystifying-greedy-algorithms-how-local-choices-solve-global-problems-in-node-js</guid>
<pubDate>Thu, 15 Oct 2026 08:35:56 +0000</pubDate>
</item>
<item>
<title>The Memory Leak in addEventListener You’re Ignoring</title>
<link>https://coderlegion.com/30737/the-memory-leak-in-addeventlistener-youre-ignoring</link>
<description>&lt;p&gt;Event listeners on dynamic elements or SPA routing are silent performance killers. If you attach an event listener to window or a persistent parent node inside a component and don’t clean it up when the component unmounts, that DOM reference stays in memory forever.&lt;/p&gt;
&lt;p&gt;JavaScript&lt;br&gt;
// ❌ Dangerous in Single Page Applications&lt;br&gt;
function setupListener() {&lt;br&gt;
  window.addEventListener('resize', handleResize);&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;// ✅ Use AbortController for clean, single-call teardown&lt;br&gt;
const controller = new AbortController();&lt;br&gt;
const { signal } = controller;&lt;/p&gt;
&lt;p&gt;window.addEventListener('resize', handleResize, { signal });&lt;/p&gt;
&lt;p&gt;// Call this anywhere during unmount/cleanup:&lt;br&gt;
controller.abort(); // Automatically removes ALL listeners bound to this signal!&lt;br&gt;
Why it changes your career: Knowing how browser memory management works at runtime elevates you from a basic script author to a senior engineer who writes production-ready apps that don't crash under load.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30737/the-memory-leak-in-addeventlistener-youre-ignoring</guid>
<pubDate>Thu, 15 Oct 2026 06:48:00 +0000</pubDate>
</item>
<item>
<title>What Are Rage Clicks and Dead Clicks? 7 Causes and How to Fix Them</title>
<link>https://coderlegion.com/30927/what-are-rage-clicks-and-dead-clicks-7-causes-and-how-to-fix-them</link>
<description>&lt;p&gt;A rage click is when a visitor clicks the same spot several times in a very short time, usually 3 or more clicks within about one second. Rage clicking almost always means the visitor expected something to happen, and it did not. Think of an elevator button you press again and again: that is exactly what &lt;strong&gt;rage clicks&lt;/strong&gt; are, but on a website.&lt;/p&gt;
&lt;p&gt;A dead click is the quiet version: a single click on something that looks clickable and does nothing.&lt;/p&gt;
&lt;p&gt;Rage clicks are one of the clearest signs of a frustrated visitor. And frustrated visitors do not buy, sign up or come back. The good news: rage clicks are easy to find and often easy to fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simple definition:&lt;/strong&gt; A rage click is 3 or more quick clicks on the same small area, usually within about one second. A dead click is a single click that does nothing at all. In Opti-Behavior, rage, dead and error click detection is a Pro feature; the free plugin gives you click heatmaps, where dead clicks show up as hot spots on things that are not links.&lt;/p&gt;
&lt;h3 id=&quot;rage-clicks-vs-dead-clicks-vs-error-clicks&quot;&gt;Rage clicks vs dead clicks vs error clicks&lt;/h3&gt;
&lt;p&gt;These three often come together. Here is the difference.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;What happens &lt;/th&gt;&lt;th&gt;What the visitor feels&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Rage click &lt;/td&gt;&lt;td&gt;Many fast clicks on the same spot &lt;/td&gt;&lt;td&gt;&quot;Why is this not working?!&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dead click &lt;/td&gt;&lt;td&gt;One click, nothing changes on the page &lt;/td&gt;&lt;td&gt;&quot;Hmm, I thought that was a button.&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error click &lt;/td&gt;&lt;td&gt;A click that triggers a JavaScript error &lt;/td&gt;&lt;td&gt;Often nothing visible, the feature is broken&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A dead click can turn into a rage click. The visitor clicks once, nothing happens, so they click again, harder.&lt;/p&gt;
&lt;h3 id=&quot;what-causes-rage-clicks-&quot;&gt;What causes rage clicks?&lt;/h3&gt;
&lt;p&gt;Here are the most common causes we see on WordPress sites.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Things that look clickable but are not.&lt;/strong&gt; A product image, an icon, bold blue text, a card. People expect a link.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slow buttons.&lt;/strong&gt; The button works, but it takes two seconds to react. The visitor clicks again because they think it failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broken JavaScript.&lt;/strong&gt; A plugin conflict or an error stops a menu, a slider or an &quot;Add to cart&quot; button from working.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tiny tap targets on mobile.&lt;/strong&gt; The link is too small, so the finger misses it again and again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Form buttons that do not give feedback.&lt;/strong&gt; The visitor presses &quot;Send&quot;, nothing shows, so they press again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pop-ups that will not close.&lt;/strong&gt; The close button is too small or covered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disabled buttons with no explanation.&lt;/strong&gt; The button is grey because a field is missing, but nothing tells the visitor.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;how-to-find-rage-clicks-on-your-wordpress-site&quot;&gt;How to find rage clicks on your WordPress site&lt;/h3&gt;
&lt;h4 id=&quot;option-1-watch-recordings-slow-&quot;&gt;Option 1: Watch recordings (slow)&lt;/h4&gt;
&lt;p&gt;You can open &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/opti-behavior/ob-feature-recordings/&quot;&gt;session recordings&lt;/a&gt; one by one and look for fast clicks. It works, but it takes hours on a busy site.&lt;/p&gt;
&lt;h4 id=&quot;option-2-let-a-tool-flag-them-fast-&quot;&gt;Option 2: Let a tool flag them (fast)&lt;/h4&gt;
&lt;p&gt;Opti-Behavior Pro detects friction events on its own. It flags rage clicks, dead clicks and error clicks, groups them by page and element, and links each one to the recording where it happened.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://optiuser.com/wp-content/uploads/2026/06/docs-errors-friction-list.webp&quot; alt=&quot;List of rage clicks and dead clicks detected on a WordPress site&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Each line is one element where visitors got stuck, with the number of events.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://optiuser.com/wp-content/uploads/2026/06/docs-errors-friction-details.webp&quot; alt=&quot;Details of a rage click event on a WordPress page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Open an event to see the page, the element and the recording.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This turns hours of watching into a short to-do list, for example: &quot;The ‘Size guide’ link on product pages gets the most rage clicks.&quot; Now you know exactly what to fix. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/docs/errors-friction-events/&quot;&gt;friction events in the docs&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;option-3-check-your-click-heatmap&quot;&gt;Option 3: Check your click heatmap&lt;/h4&gt;
&lt;p&gt;A click heatmap shows hot spots on things that are not links. That is where dead clicks live. It is included in the free version of Opti-Behavior. Our guide on &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/blog/wordpress-heatmap-plugin-guide/&quot;&gt;how to add a heatmap to WordPress&lt;/a&gt; shows the setup.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://optiuser.com/wp-content/uploads/2026/06/docs-heatmap-click.webp&quot; alt=&quot;Click heatmap showing clicks on non-clickable elements&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hot spots on images or plain text often mean dead clicks.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;dead-clicks-the-quiet-cousin-of-rage-clicks&quot;&gt;Dead clicks: the quiet cousin of rage clicks&lt;/h3&gt;
&lt;p&gt;A rage click is loud. Someone hits the same spot five times in a second. A &lt;strong&gt;dead click&lt;/strong&gt; is quiet. Someone clicks once on something that looks clickable, nothing happens, and they just leave. No drama, no noise. That is why dead clicks are easy to miss, and why they often cost more.&lt;/p&gt;
&lt;p&gt;Here is a typical example. Imagine a shop with a &quot;Size guide&quot; line under the price, in blue and underlined. It looks exactly like a link, but it is plain text. Visitors click it and nothing opens. Some of them never add to cart, because they are not sure which size to pick.&lt;/p&gt;
&lt;h4 id=&quot;the-three-kinds-of-friction-clicks&quot;&gt;The three kinds of friction clicks&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;What happened&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Rage click &lt;/td&gt;&lt;td&gt;Several fast clicks on the same spot&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dead click &lt;/td&gt;&lt;td&gt;A click on something that looks clickable and does nothing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error click &lt;/td&gt;&lt;td&gt;A click followed by a JavaScript error&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;why-a-click-can-be-dead-&quot;&gt;Why a click can be &quot;dead&quot;&lt;/h4&gt;
&lt;p&gt;When Opti-Behavior Pro records a dead click, it also saves the details: the element that was clicked, whether it was really interactive, whether something was &lt;strong&gt;covering it&lt;/strong&gt;, what happened after the click, and the screen size, browser and device. The button &lt;strong&gt;View element on page&lt;/strong&gt; highlights the element on your live page, so you see exactly what visitors saw.&lt;/p&gt;
&lt;p&gt;Most dead clicks come from one of these three causes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Covered by an overlay.&lt;/strong&gt; A pop-up, a cookie banner or a sticky bar sits on top of the button. The visitor sees the button but clicks the invisible layer above it. This is very common on mobile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not interactive.&lt;/strong&gt; The element looks like a link or a button (blue, underlined, an arrow, a card with a shadow) but it is just text or an image.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A script problem.&lt;/strong&gt; The button should work, but its JavaScript failed to load or crashed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-verdict-tells-you-who-should-fix-it&quot;&gt;The verdict tells you who should fix it&lt;/h4&gt;
&lt;p&gt;In Opti-Behavior Pro, each group of friction events gets a verdict, so you know if it is a design job or a developer job:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Verdict &lt;/th&gt;&lt;th&gt;What it means &lt;/th&gt;&lt;th&gt;Who fixes it&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Probable code bug &lt;/td&gt;&lt;td&gt;Clicks on this element triggered a captured error &lt;/td&gt;&lt;td&gt;Developer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Needs review &lt;/td&gt;&lt;td&gt;Could be a bug or the network (ad blockers, offline) &lt;/td&gt;&lt;td&gt;Check a few recordings first&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Visitor behavior &lt;/td&gt;&lt;td&gt;Normal behavior, like impatient clicking &lt;/td&gt;&lt;td&gt;Usually nobody, or a small design tweak&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With Pro, Smart Insights measures rage clicks per 100 visits, so a busy page does not flood you with cards. You see the elements that frustrate a real share of visitors, not every single double-click.&lt;/p&gt;
&lt;h4 id=&quot;how-to-fix-dead-clicks&quot;&gt;How to fix dead clicks&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Covered by overlay:&lt;/strong&gt; move or shrink the pop-up, cookie banner or sticky bar, or delay it until the visitor scrolls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not interactive:&lt;/strong&gt; make it a real link, or change its look so it does not pretend to be one. No blue underline on plain text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Probable code bug:&lt;/strong&gt; open the linked JavaScript error and fix or replace the script that breaks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After the fix, check the same element two weeks later. Dead clicks on it should drop close to zero.&lt;/p&gt;
&lt;h3 id=&quot;how-to-fix-rage-clicks&quot;&gt;How to fix rage clicks&lt;/h3&gt;
&lt;p&gt;Fix the cause, not the symptom. Here is a simple guide.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Cause &lt;/th&gt;&lt;th&gt;Fix&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Image or icon looks clickable &lt;/td&gt;&lt;td&gt;Make it a link, or change the style so it does not look clickable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Button is slow &lt;/td&gt;&lt;td&gt;Show a loading state right away (&quot;Adding…&quot;) so people know it worked&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JavaScript error &lt;/td&gt;&lt;td&gt;Check the error log, update or replace the plugin that breaks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tiny mobile link &lt;/td&gt;&lt;td&gt;Make tap targets at least 44 by 44 pixels&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Form button gives no feedback &lt;/td&gt;&lt;td&gt;Show a message or spinner right after the click&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pop-up close button hard to hit &lt;/td&gt;&lt;td&gt;Make it bigger and let people close it by tapping outside&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Disabled button &lt;/td&gt;&lt;td&gt;Tell people what is missing, next to the button&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;measure-before-and-after&quot;&gt;Measure before and after&lt;/h3&gt;
&lt;p&gt;Before you fix anything, look at the verdict. Opti-Behavior Pro tells you if the clicks look like a design problem (people click something that is not a button) or a code bug (the clicks trigger an error).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://optiuser.com/wp-content/uploads/2026/06/docs-errors-friction-verdict.webp&quot; alt=&quot;Rage click verdict showing a probable code bug on a WordPress page&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Here the clicks triggered a captured JavaScript error, so the verdict is &quot;probable code bug&quot;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After a fix, compare the number of rage clicks on that element for the next two weeks. If it drops, you fixed it. If you want to be sure the change also helped sales or sign-ups, run an A/B test. Opti-Behavior’s A/B testing is free.&lt;/p&gt;
&lt;h3 id=&quot;are-rage-clicks-always-bad-&quot;&gt;Are rage clicks always bad?&lt;/h3&gt;
&lt;p&gt;Almost always, but not every time. Some people double-click out of habit. A game or a quiz may need fast clicks. That is why good tools look for a pattern: many visitors, same element, again and again. One person with a fast finger is not a problem. Fifty people on the same button is.&lt;/p&gt;
&lt;p&gt;Slow buttons and rage clicks often go together. If your page reacts slowly to taps, read our guide to &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/blog/core-web-vitals-monitoring-wordpress/&quot;&gt;Core Web Vitals on WordPress&lt;/a&gt;, especially the part about INP.&lt;/p&gt;
&lt;h4 id=&quot;find-the-rage-clicks-on-your-site&quot;&gt;Find the rage clicks on your site&lt;/h4&gt;
&lt;p&gt;Start free with click heatmaps and funnels. Rage click and dead click detection with recordings is in Pro: see the &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/opti-behavior/pricing/&quot;&gt;pricing page&lt;/a&gt; for the free trial and plans.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wordpress.org/plugins/opti-behavior/&quot;&gt;Download free on WordPress.org&lt;/a&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.optiuser.com/?demo_login=go&quot;&gt;Open the live demo&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;rage-clicks-faq&quot;&gt;Rage clicks FAQ&lt;/h3&gt;
&lt;h4 id=&quot;what-are-rage-clicks-&quot;&gt;What are rage clicks?&lt;/h4&gt;
&lt;p&gt;Rage clicks are several fast clicks on the same spot, usually within about one second. They show that a visitor expected something to happen and it did not.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-difference-between-rage-clicks-and-dead-clicks-&quot;&gt;What is the difference between rage clicks and dead clicks?&lt;/h4&gt;
&lt;p&gt;A dead click is one click that does nothing. A rage click is many fast clicks in the same place. A dead click often becomes a rage click when the visitor tries again.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-track-rage-clicks-in-wordpress-&quot;&gt;How do I track rage clicks in WordPress?&lt;/h4&gt;
&lt;p&gt;Use a behavior analytics plugin that detects friction events. Opti-Behavior Pro flags rage clicks, dead clicks and error clicks and links each one to a session recording. Cloud tools such as Microsoft Clarity also flag rage clicks; see our &lt;a rel=&quot;nofollow&quot; href=&quot;https://optiuser.com/blog/microsoft-clarity-alternatives/&quot;&gt;Microsoft Clarity alternatives&lt;/a&gt; guide for the differences.&lt;/p&gt;
&lt;h4 id=&quot;do-rage-clicks-hurt-conversions-&quot;&gt;Do rage clicks hurt conversions?&lt;/h4&gt;
&lt;p&gt;They can. A rage click means a visitor tried to do something and could not, so it often marks a step where people give up. Fix the elements with the most rage clicks first, then check the effect with an A/B test.&lt;/p&gt;
&lt;p&gt;This article was drafted with the help of AI tools and reviewed by The Opti-Behavior Team.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30927/what-are-rage-clicks-and-dead-clicks-7-causes-and-how-to-fix-them</guid>
<pubDate>Tue, 13 Oct 2026 15:05:42 +0000</pubDate>
</item>
<item>
<title>🔥 30 Days of Frontend — Day 4: Memoization in React</title>
<link>https://coderlegion.com/30728/30-days-of-frontend-day-4-memoization-in-react</link>
<description>&lt;p&gt;Have you ever looked at a React component and thought:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;“Maybe I should wrap this function with &lt;code&gt;useCallback&lt;/code&gt;, this calculation with &lt;code&gt;useMemo&lt;/code&gt;, and the component with &lt;code&gt;React.memo&lt;/code&gt;?”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You're not alone.&lt;/p&gt;
&lt;p&gt;Memoization is one of the most frequently discussed React performance techniques. But it's also one of the easiest to misuse.&lt;/p&gt;
&lt;p&gt;You can add memoization to an application, introduce more complexity, and still fail to improve its performance.&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because memoization is not about preventing every render. It's about avoiding work that doesn't need to happen again.&lt;/p&gt;
&lt;p&gt;Let's understand how it works, when it helps, and when it becomes unnecessary complexity.&lt;/p&gt;
&lt;h3 id=&quot;1-what-is-memoization-&quot;&gt;1. What Is Memoization?&lt;/h3&gt;
&lt;p&gt;Memoization is an optimization technique that caches the result of a computation so it can be reused when the relevant inputs haven't changed.&lt;/p&gt;
&lt;p&gt;Imagine you have an expensive function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function calculateTotalPrice(products: Product[]) {
  console.log(&quot;Calculating total price...&quot;);

  return products.reduce(
    (total, product) =&amp;gt; total + product.price,
    0
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every time you call this function, it calculates the total again.&lt;/p&gt;
&lt;p&gt;If the product list hasn't changed, repeating the same calculation may be unnecessary.&lt;/p&gt;
&lt;p&gt;Memoization lets us reuse a previous result when the inputs are still valid.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Inputs
  |
  v
Have relevant inputs changed?
  |
  +---- Yes ----&amp;gt; Recalculate and cache the result
  |
  +---- No -----&amp;gt; Reuse the cached result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React provides three commonly used memoization APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt; — caches a calculated value.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt; — caches a function reference.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;React.memo&lt;/code&gt; — can skip a component render when its props are unchanged.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They solve different problems, so they shouldn't be treated as interchangeable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-usememo-cache-an-expensive-calculation&quot;&gt;2. &lt;code&gt;useMemo&lt;/code&gt;: Cache an Expensive Calculation&lt;/h3&gt;
&lt;p&gt;Consider a product dashboard containing thousands of products.&lt;/p&gt;
&lt;p&gt;You need to filter products by category and calculate which products match a search query.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductList({
  products,
  search,
}: {
  products: Product[];
  search: string;
}) {
  const filteredProducts = products.filter((product) =&amp;gt;
    product.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    &amp;lt;ul&amp;gt;
      {filteredProducts.map((product) =&amp;gt; (
        &amp;lt;li key={product.id}&amp;gt;{product.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filtering operation runs whenever &lt;code&gt;ProductList&lt;/code&gt; renders.&lt;/p&gt;
&lt;p&gt;That may be perfectly fine for a small list. But imagine the filtering logic is expensive and the component also renders because some unrelated state changes.&lt;/p&gt;
&lt;p&gt;We can cache the calculated array with &lt;code&gt;useMemo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useMemo } from &quot;react&quot;;

function ProductList({
  products,
  search,
}: {
  products: Product[];
  search: string;
}) {
  const filteredProducts = useMemo(() =&amp;gt; {
    return products.filter((product) =&amp;gt;
      product.name.toLowerCase().includes(search.toLowerCase())
    );
  }, [products, search]);

  return (
    &amp;lt;ul&amp;gt;
      {filteredProducts.map((product) =&amp;gt; (
        &amp;lt;li key={product.id}&amp;gt;{product.name}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-changed-&quot;&gt;What changed?&lt;/h4&gt;
&lt;p&gt;React recalculates &lt;code&gt;filteredProducts&lt;/code&gt; when &lt;code&gt;products&lt;/code&gt; or &lt;code&gt;search&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;If the component renders again while both dependencies remain unchanged, React can reuse the previous result.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Initial render
  └── products + search
        └── Calculate filtered products

Unrelated state changes
  └── products + search unchanged
        └── Reuse cached result

Search changes
  └── New search value
        └── Calculate filtered products again
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;an-important-limitation&quot;&gt;An important limitation&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; does not make every search update faster.&lt;/p&gt;
&lt;p&gt;When the user types another character, &lt;code&gt;search&lt;/code&gt; changes. The filtering calculation must run again to produce the correct results.&lt;/p&gt;
&lt;p&gt;Memoization is most useful when a calculation is expensive and its dependencies remain unchanged across renders.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-usecallback-cache-a-function-reference&quot;&gt;3. &lt;code&gt;useCallback&lt;/code&gt;: Cache a Function Reference&lt;/h3&gt;
&lt;p&gt;Now let's look at &lt;code&gt;useCallback&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Consider a parent component that passes an event handler to a child:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductPage() {
  const [count, setCount] = useState(0);

  const handleAddToCart = (productId: number) =&amp;gt; {
    console.log(&quot;Added product:&quot;, productId);
  };

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard onAddToCart={handleAddToCart} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whenever &lt;code&gt;ProductPage&lt;/code&gt; renders, the function expression creates a new function reference.&lt;/p&gt;
&lt;p&gt;Even though the function's logic hasn't changed, the reference is different.&lt;/p&gt;
&lt;p&gt;That matters if &lt;code&gt;ProductCard&lt;/code&gt; is memoized and receives the handler as a prop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ProductCard = React.memo(function ProductCard({
  onAddToCart,
}: {
  onAddToCart: (productId: number) =&amp;gt; void;
}) {
  console.log(&quot;ProductCard rendered&quot;);

  return (
    &amp;lt;button onClick={() =&amp;gt; onAddToCart(42)}&amp;gt;
      Add to cart
    &amp;lt;/button&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, &lt;code&gt;React.memo&lt;/code&gt; compares props. A new function reference means the handler prop is considered changed, so the child may render again.&lt;/p&gt;
&lt;p&gt;We can stabilize the function reference with &lt;code&gt;useCallback&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useCallback, useState } from &quot;react&quot;;

function ProductPage() {
  const [count, setCount] = useState(0);

  const handleAddToCart = useCallback((productId: number) =&amp;gt; {
    console.log(&quot;Added product:&quot;, productId);
  }, []);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount(count + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard onAddToCart={handleAddToCart} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the callback doesn't depend on any reactive values from the component, its dependency array is empty.&lt;/p&gt;
&lt;p&gt;Now, when &lt;code&gt;count&lt;/code&gt; changes, React can reuse the same callback reference. That allows the memoized &lt;code&gt;ProductCard&lt;/code&gt; to skip rendering if its other props also remain unchanged.&lt;/p&gt;
&lt;h4 id=&quot;what-about-dependencies-&quot;&gt;What about dependencies?&lt;/h4&gt;
&lt;p&gt;Consider this callback:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleAddToCart = useCallback(() =&amp;gt; {
  setCartCount(cartCount + 1);
}, [cartCount]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The callback depends on &lt;code&gt;cartCount&lt;/code&gt;, so its reference changes whenever &lt;code&gt;cartCount&lt;/code&gt; changes.&lt;/p&gt;
&lt;p&gt;For state updates that depend on the previous state, a functional update can sometimes avoid that dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleAddToCart = useCallback(() =&amp;gt; {
  setCartCount((currentCount) =&amp;gt; currentCount + 1);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works because the updater receives the latest state value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;useCallback&lt;/code&gt; caches a function reference; it doesn't make the function's execution inherently faster.&lt;/p&gt;
&lt;p&gt;If you're not passing the function to a memoized child or using it in another dependency-sensitive context, &lt;code&gt;useCallback&lt;/code&gt; may provide no meaningful benefit.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-react-memo-skip-unnecessary-component-renders&quot;&gt;4. &lt;code&gt;React.memo&lt;/code&gt;: Skip Unnecessary Component Renders&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; is different from the two hooks.&lt;/p&gt;
&lt;p&gt;It wraps a component and lets React skip rendering it when its props are unchanged, subject to React's normal rendering behavior.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ProductCard = React.memo(function ProductCard({
  name,
  price,
}: {
  name: string;
  price: number;
}) {
  console.log(&quot;Rendering ProductCard:&quot;, name);

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h3&amp;gt;{name}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;${price}&amp;lt;/p&amp;gt;
    &amp;lt;/article&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suppose the parent updates a separate counter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function ProductPage() {
  const [count, setCount] = useState(0);

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setCount((current) =&amp;gt; current + 1)}&amp;gt;
        Count: {count}
      &amp;lt;/button&amp;gt;

      &amp;lt;ProductCard name=&quot;Keyboard&quot; price={120} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parent renders when the counter changes, but the &lt;code&gt;ProductCard&lt;/code&gt; props remain the same.&lt;/p&gt;
&lt;p&gt;React can skip rendering &lt;code&gt;ProductCard&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;However, &lt;code&gt;React.memo&lt;/code&gt; is not an absolute rendering lock. The component can still render because of its own state changes, consumed context changes, or other React behavior.&lt;/p&gt;
&lt;h4 id=&quot;a-common-mistake-unstable-object-props&quot;&gt;A common mistake: unstable object props&lt;/h4&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;ProductCard
  product={{
    id: 1,
    name: &quot;Keyboard&quot;,
    price: 120,
  }}
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new object is created whenever the parent renders.&lt;/p&gt;
&lt;p&gt;Even if the object contains the same data, its reference is different.&lt;/p&gt;
&lt;p&gt;Consequently, a memoized child can still render.&lt;/p&gt;
&lt;p&gt;This is why understanding referential equality matters when optimizing React applications.&lt;/p&gt;
&lt;p&gt;Two objects can contain identical properties and still be different references:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const first = { name: &quot;Keyboard&quot; };
const second = { name: &quot;Keyboard&quot; };

console.log(first === second); // false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Memoization works best when the values that cross component boundaries have the stability needed for the optimization to be effective.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-how-these-three-apis-work-together&quot;&gt;5. How These Three APIs Work Together&lt;/h3&gt;
&lt;p&gt;Let's put the concepts together.&lt;/p&gt;
&lt;p&gt;Imagine a dashboard with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A product search field&lt;/li&gt;
&lt;li&gt;A product list&lt;/li&gt;
&lt;li&gt;A shopping cart counter&lt;/li&gt;
&lt;li&gt;Expensive product filtering&lt;/li&gt;
&lt;li&gt;Memoized product cards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A possible optimization strategy is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ProductPage
    |
    ├── useCallback
    |      └── Stabilize a handler passed to a memoized child
    |
    ├── useMemo
    |      └── Cache filtered products when dependencies are unchanged
    |
    └── ProductList
           |
           └── React.memo
                  └── Skip rendering when props are unchanged
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The APIs address different sources of repeated work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;API &lt;/th&gt;&lt;th&gt;What it caches or optimizes &lt;/th&gt;&lt;th&gt;Useful when&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;useMemo&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A calculated value &lt;/td&gt;&lt;td&gt;A calculation is expensive and dependencies often stay unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;useCallback&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A function reference &lt;/td&gt;&lt;td&gt;Stable identity helps a memoized child or dependency-sensitive hook&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;React.memo&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A component's render work based on props &lt;/td&gt;&lt;td&gt;A component renders often with unchanged props and rendering is costly&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key is not to use all three together automatically. Use only the optimization that addresses the measured bottleneck.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-when-memoization-doesn-t-help&quot;&gt;6. When Memoization Doesn't Help&lt;/h3&gt;
&lt;p&gt;Memoization has costs too.&lt;/p&gt;
&lt;p&gt;It adds some combination of dependency tracking, comparisons, cached values, and code complexity.&lt;/p&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const fullName = useMemo(() =&amp;gt; {
  return `${firstName} ${lastName}`;
}, [firstName, lastName]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unless there is a specific reason to cache this value, the direct version is simpler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const fullName = `${firstName} ${lastName}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combining two strings is inexpensive. Memoizing it is unlikely to produce a measurable improvement.&lt;/p&gt;
&lt;p&gt;Another example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const handleClick = useCallback(() =&amp;gt; {
  console.log(&quot;Clicked&quot;);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this function is used only by a regular button in the same component, stabilizing its identity may not improve anything.&lt;/p&gt;
&lt;p&gt;The lesson is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not every calculation is expensive, not every render is a problem, and not every new function reference matters.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-react-compiler-changes-the-conversation&quot;&gt;7. React Compiler Changes the Conversation&lt;/h3&gt;
&lt;p&gt;Modern React projects may use the React Compiler, which can automatically memoize components, values, and functions when it can safely do so.&lt;/p&gt;
&lt;p&gt;This means manually adding &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, and &lt;code&gt;React.memo&lt;/code&gt; everywhere is less necessary in compiler-enabled applications.&lt;/p&gt;
&lt;p&gt;However, the compiler does not eliminate the need to understand performance.&lt;/p&gt;
&lt;p&gt;It cannot make every expensive algorithm cheap, remove the cost of rendering thousands of unnecessary elements, or guarantee that a slow interaction will become fast.&lt;/p&gt;
&lt;p&gt;You should still understand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What triggers a render&lt;/li&gt;
&lt;li&gt;How props and references change&lt;/li&gt;
&lt;li&gt;Which calculations are expensive&lt;/li&gt;
&lt;li&gt;When state should live closer to the components that use it&lt;/li&gt;
&lt;li&gt;How to measure actual rendering performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your project uses React Compiler, follow its guidance and avoid adding manual memoization without a reason. Manual memoization can still be useful in appropriate cases, but it shouldn't be automatic boilerplate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-final-takeaway&quot;&gt;8. Final Takeaway&lt;/h3&gt;
&lt;p&gt;Memoization is a tool, not a coding style, and the goal isn't to eliminate every render, the goal is to make the application responsive while keeping the code maintainable.&lt;/p&gt;
&lt;p&gt;My preferred performance workflow remains:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Measure → Identify the bottleneck → Optimize → Measure again.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Don't optimize because a component rendered, optimize because you've found work that is unnecessarily expensive.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/30728/30-days-of-frontend-day-4-memoization-in-react</guid>
<pubDate>Sun, 11 Oct 2026 17:45:13 +0000</pubDate>
</item>
<item>
<title>Your canvas clicks are in the wrong coordinate system</title>
<link>https://coderlegion.com/30627/your-canvas-clicks-are-in-the-wrong-coordinate-system</link>
<description>&lt;p&gt;A target at &lt;code&gt;(100, 60)&lt;/code&gt; is one quarter across and down a 400 × 240 game. Display the canvas at 800 × 480 CSS pixels and the same target appears at &lt;code&gt;(200, 120)&lt;/code&gt; relative to the canvas corner. A pointer handler that compares those CSS coordinates directly with the target will miss it.&lt;/p&gt;
&lt;p&gt;At the original display size, the two sets of numbers happened to match. Resizing exposed the missing conversion.&lt;/p&gt;
&lt;h3 id=&quot;follow-one-click-through-the-coordinates&quot;&gt;Follow one click through the coordinates&lt;/h3&gt;
&lt;p&gt;Place the larger canvas at viewport position &lt;code&gt;(100, 50)&lt;/code&gt;. A click on the target has client coordinates &lt;code&gt;(300, 170)&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Subtract the canvas origin: &lt;code&gt;300 − 100 = 200&lt;/code&gt; CSS pixels across, and &lt;code&gt;170 − 50 = 120&lt;/code&gt; down.&lt;/li&gt;
&lt;li&gt;Divide by the displayed size: &lt;code&gt;200 / 800&lt;/code&gt; and &lt;code&gt;120 / 480&lt;/code&gt; both give &lt;code&gt;0.25&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Multiply by the logical size: &lt;code&gt;0.25 × 400 = 100&lt;/code&gt; across, and &lt;code&gt;0.25 × 240 = 60&lt;/code&gt; down.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The fraction carries the position from one set of dimensions to another. If the bitmap is 1600 × 960, the same point is &lt;code&gt;(400, 240)&lt;/code&gt; in bitmap pixels. That is useful to a pixel operation, but it is still &lt;code&gt;(100, 60)&lt;/code&gt; to the game.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3983672196422537203&quot; alt=&quot;A normalized point one quarter across and down maps to CSS coordinates 200,120, logical coordinates 100,60, and bitmap coordinates 400,240 for the listed dimensions.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One normalized point expressed in CSS pixels, logical game units and bitmap pixels. Original diagram made with AI-assisted code; the dimensions are chosen examples.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The helper below implements that route for a canvas whose full bitmap fills its content box. It assumes no border, padding, CSS transforms on the canvas or its ancestors, letterboxing, clipped viewport or camera transform. The event and rectangle come from the same document. Use an engine's pointer-to-world API when it already accounts for additional transforms.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function toLogical(
  clientX, clientY, rect,
  width, height
) {
  if (![clientX, clientY,
        rect.left, rect.top,
        rect.width, rect.height,
        width, height]
      .every(Number.isFinite)
      || rect.width &amp;lt;= 0
      || rect.height &amp;lt;= 0
      || width &amp;lt;= 0
      || height &amp;lt;= 0)
    return null;
  const u =
    (clientX - rect.left)
    / rect.width;
  const v =
    (clientY - rect.top)
    / rect.height;
  if (u &amp;lt; 0 || u &amp;gt;= 1
      || v &amp;lt; 0 || v &amp;gt;= 1)
    return null;
  return {
    x: u * width,
    y: v * height
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On &lt;code&gt;pointerdown&lt;/code&gt;, pass &lt;code&gt;event.clientX&lt;/code&gt;, &lt;code&gt;event.clientY&lt;/code&gt;, a fresh &lt;code&gt;canvas.getBoundingClientRect()&lt;/code&gt;, and the logical scene dimensions. Ignore &lt;code&gt;null&lt;/code&gt;; otherwise use the returned position for hit testing.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX&quot;&gt;&lt;code&gt;clientX&lt;/code&gt; and &lt;code&gt;clientY&lt;/code&gt;&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect&quot;&gt;&lt;code&gt;getBoundingClientRect()&lt;/code&gt;&lt;/a&gt; share viewport coordinates. Reading the rectangle at the press accounts for scrolling. Adding a scroll offset—or substituting document-relative &lt;code&gt;pageY&lt;/code&gt;—would mix coordinate systems again.&lt;/p&gt;
&lt;p&gt;The helper includes the left and top edges and rejects the right, bottom and outside. It does not clamp a gutter click onto an edge target. Keep fractional results for logical hit testing.&lt;/p&gt;
&lt;h3 id=&quot;density-belongs-on-the-drawing-side&quot;&gt;Density belongs on the drawing side&lt;/h3&gt;
&lt;p&gt;Increasing bitmap density can sharpen the canvas without moving the target in the game. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio&quot;&gt;&lt;code&gt;devicePixelRatio&lt;/code&gt;&lt;/a&gt; relates physical pixels to CSS pixels and can change with zoom or a different display. Multiplying the helper's output by DPR would apply a second, unwanted conversion.&lt;/p&gt;
&lt;p&gt;For the 400 × 240 logical scene, use &lt;code&gt;ctx.setTransform(canvas.width / 400, 0, 0, canvas.height / 240, 0, 0)&lt;/code&gt; when drawing. The actual integer bitmap dimensions account for resize rounding. &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform&quot;&gt;&lt;code&gt;setTransform&lt;/code&gt;&lt;/a&gt; replaces the scale rather than compounding it on each resize.&lt;/p&gt;
&lt;p&gt;Keep CSS size independent of the bitmap attributes, for example with an explicit width and aspect ratio. Otherwise enlarging &lt;code&gt;canvas.width&lt;/code&gt; can enlarge an intrinsically sized element too. Assigning &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/width&quot;&gt;&lt;code&gt;canvas.width&lt;/code&gt;&lt;/a&gt; resets the context, so restore the transform and drawing styles afterward. Handle density changes even when the element's CSS size stays the same.&lt;/p&gt;
&lt;h3 id=&quot;an-iframe-can-resize-correctly-while-clicks-remain-wrong&quot;&gt;An iframe can resize correctly while clicks remain wrong&lt;/h3&gt;
&lt;p&gt;Rosebud's &lt;a rel=&quot;nofollow&quot; href=&quot;https://rosebud.ai/blog/how-to-embed-your-rosebud-game&quot;&gt;November 2024 embedding guide&lt;/a&gt; describes responsive iframe sizing. That handles the outer display; the pointer conversion still belongs to the document containing the canvas. Host-page coordinates cannot be paired with an inner canvas rectangle, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy&quot;&gt;same-origin restrictions&lt;/a&gt; limit cross-origin access.&lt;/p&gt;
&lt;p&gt;A correctly sized embed is therefore a place to repeat the target test, not evidence that the game's input handler is correct. First verify the arithmetic with the helper above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const wide = {
  left: 100, top: 50,
  width: 800, height: 480
};
console.log(toLogical(
  300, 170, wide, 400, 240
)); // {x: 100, y: 60}
const small = {
  ...wide,
  width: 400, height: 240
};
console.log(toLogical(
  200, 110, small, 400, 240
)); // {x: 100, y: 60}
console.log(toLogical(
  900, 170, wide, 400, 240
)); // null: right edge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then click that same target at two display sizes in the real delivery page, scroll, and try the edges and gutter. If the arithmetic passes but those presses fail, compare the measured content area with the helper's layout assumptions—especially borders, letterboxing and transforms.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/30627/your-canvas-clicks-are-in-the-wrong-coordinate-system</guid>
<pubDate>Sat, 10 Oct 2026 19:24:16 +0000</pubDate>
</item>
<item>
<title>I turned strings into letter arrays and started rebuilding URLs with index ranges</title>
<link>https://coderlegion.com/30523/i-turned-strings-into-letter-arrays-and-started-rebuilding-urls-with-index-ranges</link>
<description>&lt;p&gt;I was just playing with a tiny idea:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What if every string was just an array of single characters… and I could pull pieces out by index ranges to form words, paths, or API links?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That experiment became &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/string-range-tokens&quot;&gt;string-range-tokens&lt;/a&gt;&lt;/strong&gt; — a zero-dependency ESM package that does exactly that.&lt;/p&gt;
&lt;h3 id=&quot;the-core-idea&quot;&gt;The core idea&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { toTokens, sub, build } from 'string-range-tokens';

const t = toTokens('https://api.example.com/v1/users');

sub(t, 0, 4);                 // &quot;https&quot;
sub(t, 23, 25, [26, 31]);     // &quot;/v1/users&quot;
sub(t, -5, -1);               // &quot;users&quot;   ← negative indexes work
build(t, [0, 4], [23, 25]);   // &quot;https/v1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;toTokens&lt;/code&gt; turns any string into a proper array of characters (Unicode-safe).&lt;br&gt;
&lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt; let you reassemble any combination of inclusive ranges.&lt;/p&gt;
&lt;h3 id=&quot;why-bother-&quot;&gt;Why bother?&lt;/h3&gt;
&lt;p&gt;Sometimes you want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assemble API paths or query fragments from a shared token pool&lt;/li&gt;
&lt;li&gt;Avoid writing the full string literal in one place&lt;/li&gt;
&lt;li&gt;Pick substrings by position without regex&lt;/li&gt;
&lt;li&gt;Build tiny string “templates” from a fixed alphabet&lt;/li&gt;
&lt;li&gt;Teach indexing, ranges, and a bit of recursion in a playful way&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not encryption (never hide real secrets with it). It’s just a clean, lightweight way to work with strings as letter arrays.&lt;/p&gt;
&lt;h3 id=&quot;extra-powers&quot;&gt;Extra powers&lt;/h3&gt;
&lt;h4 id=&quot;plan-find-the-ranges-that-rebuild-a-target&quot;&gt;&lt;code&gt;plan&lt;/code&gt; – find the ranges that rebuild a target&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const pool = toTokens('the quick brown fox');
const ranges = plan(pool, 'brew');
build(pool, ...ranges); // &quot;brew&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;scatter-gather-seeded-shuffle-reconstruct&quot;&gt;&lt;code&gt;scatter&lt;/code&gt; / &lt;code&gt;gather&lt;/code&gt; – seeded shuffle + reconstruct&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const { pool, key } = scatter('/api/orders', 7);
gather(pool, key); // &quot;/api/orders&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful for deterministic scrambling when you want both the shuffled tokens and a way to put them back together.&lt;/p&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i string-range-tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or just copy the source — it’s a single file with no dependencies.&lt;/p&gt;
&lt;p&gt;Works in Node 18+, Deno, Bun, and the browser.&lt;br&gt;
Both JavaScript and TypeScript implementations are included.&lt;/p&gt;
&lt;p&gt;CDN:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { toTokens, sub, build, plan, scatter, gather } 
  from 'https://cdn.jsdelivr.net/npm/string-range-tokens@1.0.0/src/index.js';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;links-1&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;npm: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/string-range-tokens&quot;&gt;string-range-tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Figsh/srTokens&quot;&gt;Figsh/srTokens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;If you end up using it for something fun (or weird), I’d love to hear about it.&lt;/p&gt;
</description>
<category>Discussions</category>
<guid isPermaLink="true">https://coderlegion.com/30523/i-turned-strings-into-letter-arrays-and-started-rebuilding-urls-with-index-ranges</guid>
<pubDate>Fri, 09 Oct 2026 22:09:28 +0000</pubDate>
</item>
<item>
<title>Concrete Cost Calculator</title>
<link>https://coderlegion.com/30433/concrete-cost-calculator</link>
<description>&lt;p&gt;Search for &quot;concrete cost&quot; plus a city name and you will find plenty of calculators that promise local prices. Change the city and check again: on most of them the number stays exactly the same. It is one national average with a place name attached.&lt;/p&gt;
&lt;p&gt;I built &lt;a rel=&quot;nofollow&quot; href=&quot;https://concretecostcalc.com/&quot;&gt;Concrete Cost Calculator&lt;/a&gt; to fix that. It is a free set of concrete cost calculators for U.S. homeowners, where the estimate really moves when you change your location, and where every input shows its source and date.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-most-concrete-calculators&quot;&gt;The problem with most concrete calculators&lt;/h3&gt;
&lt;p&gt;Two things go wrong, and they compound.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The location is cosmetic.&lt;/strong&gt; Labor is usually the largest part of a concrete job, and wages for cement masons vary a lot by region. A calculator that ignores that gives someone in Seattle and someone in Birmingham the same answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The math is linear.&lt;/strong&gt; Most tools multiply square feet by one price per square foot. That works for a driveway. For a small pad it is badly wrong, because the crew, the truck and the forms cost about the same to bring out whether the job is 40 sq ft or 400 sq ft.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What it does&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Six calculators.&lt;/strong&gt; Concrete pad or slab, concrete by the cubic yard, concrete block walls, driveways, patios, and rebar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Metro-level labor.&lt;/strong&gt; Labor rates come from public federal wage data for 393 U.S. metro areas. Pick your metro or enter a ZIP code, and the estimate updates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ranges with a breakdown.&lt;/strong&gt; Every estimate is a low to high range with a typical value, split into line items such as setup, material, labor and subbase. The line items always add up to the total shown.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A source on every number.&lt;/strong&gt; Each input carries one of three labels. SOURCED means it comes from public data. CALIBRATED means it was derived from public data or published cost guides. ESTIMATE means it is the site's own assumption, used only where no public figure exists. Open any line and you see where it came from and how current it is.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share and print.&lt;/strong&gt; An estimate can be copied as a link with all your inputs in it, or printed to bring to a contractor.&lt;/p&gt;
&lt;h3 id=&quot;a-typical-session&quot;&gt;A typical session&lt;/h3&gt;
&lt;p&gt;Here is what pricing a patio looks like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the patio calculator. It starts from the national baseline, then switches to your approximate metro area if it can find one.&lt;/li&gt;
&lt;li&gt;Enter a 12 × 16 ft patio at the standard 4 inch thickness. Nationally that comes to about &lt;strong&gt;$1,890 to $3,760&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a ZIP code to pick a metro yourself. In the Birmingham, Alabama metro the same patio comes to about &lt;strong&gt;$1,720 to $3,510&lt;/strong&gt;. In the Seattle metro it is about &lt;strong&gt;$2,000 to $4,660&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Look at the breakdown: setup and delivery, material, labor and subbase, each with its own range.&lt;/li&gt;
&lt;li&gt;Open the labor line to see which wage data it uses and its date. Open the setup line and you will see it labeled ESTIMATE, with an explanation.&lt;/li&gt;
&lt;li&gt;Add options such as wire mesh, a stamped finish or removing an old slab, and watch which lines change.&lt;/li&gt;
&lt;li&gt;Copy the link or print the estimate before you call for quotes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The point of step 3 is the whole product. The same patio carries a high end about $1,150 apart between those two metros, and the page shows you which line causes the difference.&lt;/p&gt;
&lt;h3 id=&quot;how-the-price-is-calculated&quot;&gt;How the price is calculated&lt;/h3&gt;
&lt;p&gt;The model is two terms:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;total = setup cost + area × (material + labor + add-ons per sq ft)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The setup cost is a fixed amount for getting a crew, a truck and forms to the site. The rest scales with the job. Thickness drives the amount of concrete, while labor depends mostly on area.&lt;/p&gt;
&lt;p&gt;That one extra term changes the answer for small jobs a lot. For a 4 inch slab at national averages, a 20 × 20 ft slab comes to $3,010 to $5,990, or $7.53 to $14.97 per square foot. Apply that rate to an 8 × 5 ft pad and you would expect $301 to $599. The model says $1,120 to $2,230, about 3.7 times higher, because most of a small job's bill is the setup.&lt;/p&gt;
&lt;p&gt;Labor changes by metro. Material prices are coarser: they are tracked nationally and for four broad regions, so most of the local difference comes from labor.&lt;/p&gt;
&lt;h3 id=&quot;stack-notes-for-the-engineers-in-the-room&quot;&gt;Stack notes for the engineers in the room&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static first.&lt;/strong&gt; The site is built with Astro and served as static pages from Cloudflare. Every page is prerendered with the national baseline numbers, so search crawlers and first-time visitors see identical content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Location after load.&lt;/strong&gt; The calculator is the only interactive island on the page. Once it hydrates, it looks up your approximate metro or the ZIP code you enter, and reprices from a static rates file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tested against a band.&lt;/strong&gt; An acceptance suite runs the model at fixed sizes across the national average and all 393 metros, and checks the results against a price band taken from prices people reported paying. That band is used only to check the model and never to fit it. Known gaps are written into the suite as expected failures instead of being deleted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open source.&lt;/strong&gt; The cost model is published as an &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/concretecostcalc/concrete-cost-model&quot;&gt;open-source cost model&lt;/a&gt; on GitHub.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;pricing&quot;&gt;Pricing&lt;/h3&gt;
&lt;p&gt;Free. No account, no email, no signup wall.&lt;/p&gt;
&lt;h3 id=&quot;known-limits&quot;&gt;Known limits&lt;/h3&gt;
&lt;p&gt;I would rather list these than have someone find them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For jobs past roughly 1,000 sq ft, estimates likely run low.&lt;/li&gt;
&lt;li&gt;The setup fee is the site's own calibrated figure. No government source publishes one.&lt;/li&gt;
&lt;li&gt;Wage data is released once a year and runs about a year behind.&lt;/li&gt;
&lt;li&gt;About 4 in 10 U.S. ZIP codes fall outside the 393 metro areas with wage data. Those visitors see the national average, clearly labeled.&lt;/li&gt;
&lt;li&gt;Every number is a planning range. A written quote from a licensed local contractor is the real price.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The methodology page explains &lt;a rel=&quot;nofollow&quot; href=&quot;https://concretecostcalc.com/methodology&quot;&gt;how every estimate is built&lt;/a&gt;, with every source and limit.&lt;/p&gt;
&lt;h3 id=&quot;a-question-for-you&quot;&gt;A question for you&lt;/h3&gt;
&lt;p&gt;When you use an online cost estimator for anything, what makes you trust or distrust the number? Is it the source, the range, the breakdown, or something else entirely? I would like to hear what would make a tool like this more useful.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/30433/concrete-cost-calculator</guid>
<pubDate>Fri, 09 Oct 2026 10:27:56 +0000</pubDate>
</item>
<item>
<title>The quirk of null...Null</title>
<link>https://coderlegion.com/30224/the-quirk-of-null-null</link>
<description>&lt;p&gt;In English, null means nothing, but in JavaScript, that nothing can become a headache. Let's overthink it and try to understand why and how.&lt;/p&gt;
&lt;p&gt;As per &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Primitive&quot;&gt;MDN&lt;/a&gt;,&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;In JavaScript, a primitive (primitive value, primitive data type) is data that is not an object and has no methods or properties.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since &lt;code&gt;null&lt;/code&gt; is one of the 7 primitive data types, it is not an object.&lt;/p&gt;
&lt;h4 id=&quot;what-about-typeof-null-it-returns-the-string-object-&quot;&gt;What about &lt;code&gt;typeof null&lt;/code&gt;? It returns the string &lt;code&gt;&quot;object&quot;&lt;/code&gt;.&lt;/h4&gt;
&lt;p&gt;Well, though &lt;code&gt;null&lt;/code&gt; is a primitive data type, &lt;code&gt;typeof null&lt;/code&gt; is not. &lt;code&gt;typeof null&lt;/code&gt; is just a string, the result of using &lt;code&gt;typeof&lt;/code&gt;, which is a way to check the type of any value. And &lt;code&gt;typeof null === &quot;object&quot;&lt;/code&gt; is nothing but a bug that was left unsolved intentionally, so old websites wouldn't break.&lt;/p&gt;
&lt;h4 id=&quot;so-what-about-typeof-number-typeof-string-etc-returning-function-&quot;&gt;So what about &lt;code&gt;typeof Number&lt;/code&gt;, &lt;code&gt;typeof String&lt;/code&gt; etc. returning &lt;code&gt;&quot;function&quot;&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;That's a different thing, and it's called a &lt;a rel=&quot;nofollow&quot; href=&quot;https://css-tricks.com/understanding-javascript-constructors/&quot;&gt;constructor&lt;/a&gt;). By convention, a constructor starts with a capital letter, and it is completely different from the same name with a lowercase letter because JavaScript is case-sensitive. So, &lt;code&gt;&quot;number&quot;&lt;/code&gt; is completely different from &lt;code&gt;Number&lt;/code&gt;. One is the type label &lt;code&gt;typeof&lt;/code&gt; returns for values like &lt;code&gt;5&lt;/code&gt;, and the other is a constructor function.&lt;/p&gt;
&lt;h4 id=&quot;so-why-does-typeof-null-undefined-return-undefined-&quot;&gt;So why does &lt;code&gt;typeof Null/Undefined&lt;/code&gt; return &lt;code&gt;&quot;undefined&quot;&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;It's easy to mix things up as a beginner, but there is no such thing in JavaScript as Null or Undefined with a capital N/U. They are undeclared names, and &lt;code&gt;typeof&lt;/code&gt; doesn't throw an error on undeclared names.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;typeof Null;        // &quot;undefined&quot;  (undeclared name; typeof doesn't throw)
Null;               // ReferenceError: Null is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Separately, &lt;code&gt;null&lt;/code&gt; &amp;amp; &lt;code&gt;undefined&lt;/code&gt; have no constructor function or wrapper object. That's also why property access on them fails instead of autoboxing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;null.toString();       // TypeError
undefined.toString();  // TypeError
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So what did we learn from this article? Literally nothing aka &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30224/the-quirk-of-null-null</guid>
<pubDate>Thu, 08 Oct 2026 07:13:07 +0000</pubDate>
</item>
</channel>
</rss>