<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Junaid Asghar</title>
    <description>The latest articles on DEV Community by Muhammad Junaid Asghar (@muhammad_junaidasghar_58).</description>
    <link>https://dev.to/muhammad_junaidasghar_58</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4091891%2F4f2ebb24-5cd2-4035-b09c-207a1b690e61.png</url>
      <title>DEV Community: Muhammad Junaid Asghar</title>
      <link>https://dev.to/muhammad_junaidasghar_58</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammad_junaidasghar_58"/>
    <language>en</language>
    <item>
      <title>I Built 11 Free Writing Tools That Run 100% in Your Browser — No Backend, No Tracking (Next.js 16)</title>
      <dc:creator>Muhammad Junaid Asghar</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:08:55 +0000</pubDate>
      <link>https://dev.to/muhammad_junaidasghar_58/i-built-11-free-writing-tools-that-run-100-in-your-browser-no-backend-no-tracking-nextjs-16-5dod</link>
      <guid>https://dev.to/muhammad_junaidasghar_58/i-built-11-free-writing-tools-that-run-100-in-your-browser-no-backend-no-tracking-nextjs-16-5dod</guid>
      <description>&lt;p&gt;When I set out to build PickBlend, I gave myself one &lt;br&gt;
hard constraint that shaped every decision after it: &lt;br&gt;
your text must never leave your browser.&lt;/p&gt;

&lt;p&gt;Not "we promise not to look." Not "we delete it after &lt;br&gt;
30 days." Literally never sent anywhere. No server &lt;br&gt;
receives it, because there is no server processing &lt;br&gt;
text at all.&lt;/p&gt;

&lt;p&gt;This post is a deep dive into how I built &lt;br&gt;
&lt;a href="https://pickblend.com" rel="noopener noreferrer"&gt;PickBlend&lt;/a&gt; — 11 free, &lt;br&gt;
privacy-first writing tools — using Next.js 16, &lt;br&gt;
TypeScript and Tailwind CSS, with zero backend, zero &lt;br&gt;
database and zero API cost. If you are building &lt;br&gt;
client-side tools, static sites, or just care about &lt;br&gt;
privacy-respecting architecture, there is something &lt;br&gt;
here for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Constraint: Everything Runs Client-Side
&lt;/h2&gt;

&lt;p&gt;Most "free" online tools follow a familiar pattern. &lt;br&gt;
You paste your text, it gets POSTed to a server, the &lt;br&gt;
server processes it, and the result comes back. Somewhere &lt;br&gt;
in that round trip your data is logged, analyzed, or &lt;br&gt;
used to train something.&lt;/p&gt;

&lt;p&gt;I wanted the opposite. Every tool on PickBlend is a &lt;br&gt;
React client component that runs entirely in the &lt;br&gt;
browser. The word count, the readability score, the &lt;br&gt;
keyword density — all of it computes on your device &lt;br&gt;
using plain JavaScript. There is no fetch() call when &lt;br&gt;
you use a tool. You can open DevTools, go to the &lt;br&gt;
Network tab, type into any tool, and watch it stay &lt;br&gt;
completely empty.&lt;/p&gt;

&lt;p&gt;This is not just a privacy talking point. It has real &lt;br&gt;
architectural consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No backend to maintain or pay for&lt;/li&gt;
&lt;li&gt;No database to secure&lt;/li&gt;
&lt;li&gt;No cold starts or server latency&lt;/li&gt;
&lt;li&gt;Sub-100ms interactions because there is no network hop&lt;/li&gt;
&lt;li&gt;Tools keep working offline once the page loads&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Here is what powers the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Framework: Next.js 16 (App Router)&lt;/li&gt;
&lt;li&gt;Language: TypeScript&lt;/li&gt;
&lt;li&gt;Styling: Tailwind CSS with a custom design-token system&lt;/li&gt;
&lt;li&gt;Hosting: Vercel (static generation + global CDN)&lt;/li&gt;
&lt;li&gt;Persistence: localStorage (no database)&lt;/li&gt;
&lt;li&gt;Contact form: Resend (the only server touch, and only 
when you deliberately submit a message)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every tool page is statically generated. When Googlebot &lt;br&gt;
or a user hits a page, they get complete HTML on the &lt;br&gt;
first request — no client-side rendering required for &lt;br&gt;
content. This matters enormously for SEO, which I will &lt;br&gt;
come back to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Tools: The Interesting Problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Word Counter — the deceptively hard one
&lt;/h3&gt;

&lt;p&gt;Counting words sounds trivial until you handle real &lt;br&gt;
input. Splitting on whitespace is the naive approach &lt;br&gt;
and it breaks immediately on hyphenated words, &lt;br&gt;
contractions, and stray punctuation.&lt;/p&gt;

&lt;p&gt;The keyword-density feature was the fun part. It needs &lt;br&gt;
to strip punctuation, normalize case, filter stop &lt;br&gt;
words, and compute frequency percentages — all under a &lt;br&gt;
millisecond on every keystroke so the UI never stutters.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getKeywordDensity(text) {
  const words = text
    .toLowerCase()
    .replace(/[^a-z\s]/g, "")
    .split(/\s+/)
    .filter(w =&amp;gt; w.length &amp;gt; 2 &amp;amp;&amp;amp; !STOP_WORDS.has(w));

  const freq = new Map();
  for (const w of words) {
    freq.set(w, (freq.get(w) ?? 0) + 1);
  }
  return freq;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Using a Map instead of a plain object avoids prototype &lt;br&gt;
pollution edge cases and is measurably faster for the &lt;br&gt;
frequent get/set pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Readability Score — six formulas, one hard dependency
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://pickblend.com/tools/readability-score" rel="noopener noreferrer"&gt;readability checker&lt;/a&gt; &lt;br&gt;
runs six formulas at once: Flesch Reading Ease, &lt;br&gt;
Flesch-Kincaid Grade Level, Gunning Fog, SMOG, ARI and &lt;br&gt;
Coleman-Liau. Five of the six depend on one genuinely &lt;br&gt;
hard problem: counting syllables in English, a language &lt;br&gt;
famous for ignoring its own rules.&lt;/p&gt;

&lt;p&gt;There is no perfect client-side syllable counter, but a &lt;br&gt;
vowel-group approach with corrections for silent-e and &lt;br&gt;
common suffixes gets you close enough for readability &lt;br&gt;
scoring:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function countSyllables(word) {
  word = word.toLowerCase().replace(/[^a-z]/g, "");
  if (word.length &amp;lt;= 3) return 1;

  word = word.replace(/e$/, "");
  const groups = word.match(/[aeiouy]+/g);
  let count = groups ? groups.length : 1;

  if (word.endsWith("le") &amp;amp;&amp;amp; word.length &amp;gt; 2) count++;
  if (word.endsWith("ed") &amp;amp;&amp;amp; !word.match(/[td]ed$/)) count--;

  return Math.max(1, count);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Text Case Converter — normalize first, then transform
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://pickblend.com/tools/case-converter" rel="noopener noreferrer"&gt;case converter&lt;/a&gt; &lt;br&gt;
handles 10 formats: UPPERCASE, lowercase, Title Case, &lt;br&gt;
Sentence case, camelCase, PascalCase, snake_case, &lt;br&gt;
kebab-case, SCREAMING_SNAKE_CASE and alternating case.&lt;/p&gt;

&lt;p&gt;The trick that makes it robust is normalizing any input &lt;br&gt;
— whatever case it arrives in — down to an array of &lt;br&gt;
lowercase words first, then building the target format &lt;br&gt;
from that clean base:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function normalizeWords(text) {
  return text
    .replace(/([a-z])([A-Z])/g, "$1 $2")
    .replace(/[_\-]+/g, " ")
    .toLowerCase()
    .split(/\s+/)
    .filter(Boolean);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This means it correctly converts snake_case to camelCase, &lt;br&gt;
kebab-case to PascalCase, or any other combination, &lt;br&gt;
without special-casing each pair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text to Speech — the browser already has an engine
&lt;/h3&gt;

&lt;p&gt;This is my favorite because it costs nothing and most &lt;br&gt;
people do not know it exists. The &lt;br&gt;
&lt;a href="https://pickblend.com/tools/text-to-speech" rel="noopener noreferrer"&gt;text to speech tool&lt;/a&gt; &lt;br&gt;
uses the native Web Speech API — window.speechSynthesis &lt;br&gt;
— which ships in every modern browser.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = selectedVoice;
utterance.rate = rate;
utterance.pitch = pitch;
window.speechSynthesis.speak(utterance);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;No API key. No per-character billing. No text sent to a &lt;br&gt;
cloud voice service. The voices come from the user's own &lt;br&gt;
operating system.&lt;/p&gt;

&lt;p&gt;The one gotcha: voices load asynchronously and &lt;br&gt;
getVoices() often returns an empty array on first call. &lt;br&gt;
You have to listen for the onvoiceschanged event:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speechSynthesis.onvoiceschanged = () =&amp;gt; {
  setVoices(speechSynthesis.getVoices());
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Also remember to call speechSynthesis.cancel() on &lt;br&gt;
component unmount, or speech keeps playing after the &lt;br&gt;
user navigates away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The SEO Architecture
&lt;/h2&gt;

&lt;p&gt;A privacy tool that nobody can find is a tree falling in &lt;br&gt;
an empty forest. Because every page is statically &lt;br&gt;
generated, Google receives full HTML with all content &lt;br&gt;
present — a big advantage over client-rendered &lt;br&gt;
competitors whose content only appears after JavaScript &lt;br&gt;
executes.&lt;/p&gt;

&lt;p&gt;On top of that, every page ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique title tags and meta descriptions&lt;/li&gt;
&lt;li&gt;JSON-LD structured data: WebApplication, BreadcrumbList, 
FAQPage and HowTo schemas&lt;/li&gt;
&lt;li&gt;800+ words of genuinely useful content per tool&lt;/li&gt;
&lt;li&gt;Internal links forming a tight topic cluster&lt;/li&gt;
&lt;li&gt;Open Graph images for social sharing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The FAQPage schema is the highest-leverage piece. It &lt;br&gt;
makes pages eligible for FAQ rich results and &lt;br&gt;
People-Also-Ask boxes, which expand your footprint in &lt;br&gt;
search results even before you crack page one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Static generation is underrated. Eleven tools, five blog &lt;br&gt;
posts, legal pages — all static HTML served from a CDN. &lt;br&gt;
No server, no scaling worries, no bill that grows with &lt;br&gt;
traffic.&lt;/p&gt;

&lt;p&gt;localStorage is enough for most tools. Developers reach &lt;br&gt;
for a database by reflex. For tools that process text &lt;br&gt;
locally, localStorage handles persistence with zero &lt;br&gt;
infrastructure. Every tool auto-saves your work so you &lt;br&gt;
can close the tab and come back to it.&lt;/p&gt;

&lt;p&gt;Design tokens pay off on day one. Setting up CSS &lt;br&gt;
variables for color, spacing and typography before &lt;br&gt;
building the first tool meant all 11 tools look &lt;br&gt;
consistent with no extra effort per tool.&lt;/p&gt;

&lt;p&gt;The browser is more capable than we give it credit for. &lt;br&gt;
Speech synthesis, localStorage, the full text-processing &lt;br&gt;
power of JavaScript — a huge amount of "needs a backend" &lt;br&gt;
functionality actually does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Toolkit
&lt;/h2&gt;

&lt;p&gt;All 11 tools, all free, all client-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/word-counter" rel="noopener noreferrer"&gt;Word Counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/reading-time-calculator" rel="noopener noreferrer"&gt;Reading Time Calculator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/character-counter" rel="noopener noreferrer"&gt;Character Counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/case-converter" rel="noopener noreferrer"&gt;Text Case Converter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/lorem-ipsum-generator" rel="noopener noreferrer"&gt;Lorem Ipsum Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/sentence-counter" rel="noopener noreferrer"&gt;Sentence Counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/text-repeater" rel="noopener noreferrer"&gt;Text Repeater&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/readability-score" rel="noopener noreferrer"&gt;Readability Score Checker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/word-frequency" rel="noopener noreferrer"&gt;Word Frequency Counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/paragraph-counter" rel="noopener noreferrer"&gt;Paragraph Counter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pickblend.com/tools/text-to-speech" rel="noopener noreferrer"&gt;Text to Speech&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try them all at &lt;a href="https://pickblend.com" rel="noopener noreferrer"&gt;pickblend.com&lt;/a&gt;. &lt;br&gt;
No sign-up, no tracking, and your text genuinely never &lt;br&gt;
leaves your browser.&lt;/p&gt;




&lt;p&gt;If you are building client-side tools and want to &lt;br&gt;
compare notes on syllable counting, case normalization, &lt;br&gt;
or wrangling the Web Speech API, drop a comment. Always &lt;br&gt;
happy to talk shop.&lt;/p&gt;

&lt;p&gt;What "needs a backend" feature have you managed to move &lt;br&gt;
fully client-side? I would love to hear it.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foltx0gbg0du2ig9uyz5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foltx0gbg0du2ig9uyz5l.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
