<?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: Talha Ramzan</title>
    <description>The latest articles on DEV Community by Talha Ramzan (@talha_ramzan_3878156fea8c).</description>
    <link>https://dev.to/talha_ramzan_3878156fea8c</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%2F4041977%2F00ab3b15-4e2a-4a94-a2db-6784815bd84b.png</url>
      <title>DEV Community: Talha Ramzan</title>
      <link>https://dev.to/talha_ramzan_3878156fea8c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/talha_ramzan_3878156fea8c"/>
    <language>en</language>
    <item>
      <title>Supporting 5 Languages (Including 2 RTL Ones) Almost Broke My Layout System</title>
      <dc:creator>Talha Ramzan</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:08:07 +0000</pubDate>
      <link>https://dev.to/talha_ramzan_3878156fea8c/supporting-5-languages-including-2-rtl-ones-almost-broke-my-layout-system-3che</link>
      <guid>https://dev.to/talha_ramzan_3878156fea8c/supporting-5-languages-including-2-rtl-ones-almost-broke-my-layout-system-3che</guid>
      <description>&lt;p&gt;When I decided to support English, Spanish, Arabic, French, and Urdu on my tools site, I assumed the hard part would be translation. It wasn't. The hard part was that two of those five languages read right-to-left, and "just add &lt;code&gt;dir="rtl"&lt;/code&gt;" turned out to be the beginning of the problem, not the end of it.&lt;/p&gt;

&lt;p&gt;Here's what actually broke, and what fixed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The naive version (day 1)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rtl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ltr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for text direction. It does not work for anything else. The moment I flipped &lt;code&gt;dir&lt;/code&gt; to &lt;code&gt;rtl&lt;/code&gt;, every layout built with directional assumptions, icons pointing the wrong way, padding on the wrong side, flexbox items reversing in ways I didn't want, broke simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: "Left" and "right" in CSS don't mean what you think
&lt;/h3&gt;

&lt;p&gt;Most CSS written without RTL in mind uses physical properties: &lt;code&gt;padding-left&lt;/code&gt;, &lt;code&gt;margin-right&lt;/code&gt;, &lt;code&gt;text-align: left&lt;/code&gt;. These are directionally absolute, they don't care what &lt;code&gt;dir&lt;/code&gt; the page is in. So a "back" arrow icon with &lt;code&gt;margin-right: 8px&lt;/code&gt; (space before the label, in LTR) ends up with the gap on the wrong side once the page flips, because the icon's logical position reversed but the CSS didn't know to follow.&lt;/p&gt;

&lt;p&gt;The fix is switching to logical properties, which are relative to text direction instead of physical screen position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Before: physical, breaks under RTL */&lt;/span&gt;
&lt;span class="nc"&gt;.back-button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* After: logical, follows text direction automatically */&lt;/span&gt;
&lt;span class="nc"&gt;.back-button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;margin-inline-end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;padding-inline-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;margin-inline-end&lt;/code&gt; means "the margin at the end of the reading direction", 8px on the right in LTR, 8px on the left in RTL, automatically. No conditional logic, no locale check in the component. The browser handles it based on &lt;code&gt;dir&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: Numbers and code snippets should not flip
&lt;/h3&gt;

&lt;p&gt;This is the one that actually surprised me. RTL affects the reading direction of &lt;em&gt;text&lt;/em&gt;, but numerals, currency values, and code blocks should stay LTR even inside an RTL page, nobody wants to read &lt;code&gt;123&lt;/code&gt; as &lt;code&gt;321&lt;/code&gt; or a JSON snippet mirrored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ltr"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"inline-block"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;formattedPrice&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapping numeric/code content in an explicit &lt;code&gt;dir="ltr"&lt;/code&gt; override, even inside an RTL parent, keeps it readable. Miss this and a WAPDA bill calculator showing "Rs. 4,500" can visually reorder the digits depending on font and browser, which is a genuinely confusing bug to explain to someone reporting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: Font rendering isn't uniform across scripts
&lt;/h3&gt;

&lt;p&gt;Arabic and Urdu use different script systems (Urdu is written in a Nastaliq-influenced Arabic script, but the typographic conventions differ enough that a font tuned for Arabic can render Urdu text with incorrect letter joining). Using one font stack for both was a mistake I didn't catch until a native Urdu speaker pointed out that words looked "technically correct but wrong," which is a hard bug to self-diagnose if you don't read the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"ar"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Noto Naskh Arabic'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"ur"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Noto Nastaliq Urdu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scoping font-family by the actual &lt;code&gt;lang&lt;/code&gt; attribute, not just a shared "RTL font" bucket, fixed rendering quality for both without either language compromising for the other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 4: hreflang tags are easy to add and easy to add wrong
&lt;/h3&gt;

&lt;p&gt;Each tool page exists in 5 locale variants, and search engines need to know they're the same content in different languages, not 5 separate, duplicate pages. That's what &lt;code&gt;hreflang&lt;/code&gt; is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;LOCALES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toolUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting the mapping right matters more than it sounds like it should. Every locale variant needs to list &lt;em&gt;every other&lt;/em&gt; locale variant as an alternate, including itself, a page that lists alternates for the other 4 languages but forgets to include a self-referencing hreflang tag is a common mistake that quietly confuses crawlers about which page is canonical.&lt;/p&gt;

&lt;p&gt;The gotcha I hit: this metadata structure existed correctly from early on, but the actual visible title and description text didn't get translated, every locale was serving the same English title. Bing's crawler flagged this as "too many pages with identical titles," which is a fair complaint: the hreflang tags said "these are translations of each other," but the content proved otherwise for anyone actually reading the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 5: Locale detection vs. locale preference are different problems
&lt;/h3&gt;

&lt;p&gt;Auto-detecting a visitor's language from browser settings (&lt;code&gt;navigator.language&lt;/code&gt; or the &lt;code&gt;Accept-Language&lt;/code&gt; header) feels like the right default, but it fights with an explicit user choice. If someone's browser is set to Arabic but they click "English" in the site's language switcher, that choice needs to persist, otherwise every page navigation silently reverts them to the auto-detected language, which reads as broken rather than helpful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferred-locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;explicit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;explicit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;detectFromAcceptLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit preference always wins over detection. Detection is only a first-visit default, never an override.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell someone adding RTL support for the first time
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logical CSS properties aren't optional once RTL is in scope.&lt;/strong&gt; Physical &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt; properties will break silently and inconsistently, not obviously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numerals and code need explicit direction overrides&lt;/strong&gt;, even inside RTL content, this is the bug most likely to ship unnoticed because it looks fine to anyone not reading the specific numbers carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume one font serves multiple scripts well&lt;/strong&gt;, even ones that look superficially related. Get someone who actually reads the language to review rendering, because typographic correctness isn't something you can proofread visually if you don't read the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hreflang tags are a promise your actual content has to keep.&lt;/strong&gt; Metadata saying "these pages are translations of each other" doesn't make it true, the visible title/description text has to actually be translated for the promise to hold up to a crawler or a user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools are live in all 5 languages if you want to see the RTL layout in action: &lt;a href="https://dukotools.com/ar" rel="noopener noreferrer"&gt;dukotools.com/ar&lt;/a&gt; or &lt;a href="https://dukotools.com/ur" rel="noopener noreferrer"&gt;dukotools.com/ur&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to go deeper on the logical-properties migration in the comments if anyone's dealing with a large existing LTR-only codebase and dreading the RTL retrofit.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What Nobody Tells You About Building "Simple" PDF Tools</title>
      <dc:creator>Talha Ramzan</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:25:02 +0000</pubDate>
      <link>https://dev.to/talha_ramzan_3878156fea8c/what-nobody-tells-you-about-building-simple-pdf-tools-2p17</link>
      <guid>https://dev.to/talha_ramzan_3878156fea8c/what-nobody-tells-you-about-building-simple-pdf-tools-2p17</guid>
      <description>&lt;p&gt;PDF merge, split, and compress sound like the most boring possible features to build. Take some files, do an operation, return a file. I believed that too, until real user files started hitting the backend and every one of these tools broke in a different, specific way.&lt;/p&gt;

&lt;p&gt;Here's what actually went wrong, and what fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PDF that wasn't actually a PDF&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first crash report was a "corrupted file" error on a PDF that opened fine in every desktop viewer. Turns out plenty of real-world PDFs are technically malformed, a missing &lt;code&gt;xref&lt;/code&gt; table, a truncated stream, an object reference pointing at nothing  but viewers like Chrome and Acrobat are extremely forgiving about it. Most Python PDF libraries are not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;PdfReadError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# strict=False alone doesn't save you from everything —
&lt;/span&gt;    &lt;span class="c1"&gt;# some files need the xref table rebuilt from scratch
&lt;/span&gt;    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_override_encryption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strict=False&lt;/code&gt; fixed maybe 70% of the "corrupted" reports. The rest needed a repair pass first — scanning the raw byte stream for object markers and reconstructing a valid cross-reference table before the normal parser ever touches it. Painful to write, but it turned "please fix your PDF" into "it just works," which matters a lot when the whole pitch of the tool is "no signup, just upload and go."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merging PDFs is not free, memory-wise&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The naive merge implementation loads every input PDF fully into memory, concatenates pages, writes the output. Fine for two 200KB files. Not fine when someone merges fifteen scanned documents at 40MB each, because now you're holding the equivalent of 600MB of parsed PDF objects in memory at once on a backend container that doesn't have unlimited RAM.&lt;/p&gt;

&lt;p&gt;The fix was switching to incremental writes  process one input file at a time, write its pages to the output stream, then explicitly drop the reference before moving to the next file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfWriter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input_paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;  &lt;span class="c1"&gt;# don't let the parsed object tree accumulate
&lt;/span&gt;    &lt;span class="n"&gt;gc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;gc.collect()&lt;/code&gt; call looks paranoid, and normally I'd agree it's unnecessary, but with large parsed PDF object trees holding circular references (pages referencing parent objects referencing the page tree back), Python's reference counting alone doesn't reliably free them fast enough between iterations under real memory pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decompression bombs are a real concern for PDF compression&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A PDF compression tool that accepts arbitrary uploads and re-encodes images inside them has to worry about the same class of attack as a zip bomb: a small, legitimately-sized PDF that contains an image with an absurd decompressed resolution. A 500KB PDF containing a "photo" that decompresses to 40,000 × 40,000 pixels will happily consume gigabytes of memory the moment you try to re-encode it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MAX_PIXELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50_000_000&lt;/span&gt;  &lt;span class="c1"&gt;# ~50 megapixels, generous for real photos
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_PIXELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Image dimensions exceed safe processing limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This check has to happen &lt;strong&gt;before&lt;/strong&gt; decompression, based on header metadata alone, checking after you've already decoded the full image into memory defeats the entire point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF-to-Word is where font and layout fidelity actually breaks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Converting a text-heavy PDF to editable Word is mostly solved. Converting a PDF with multi-column layouts, embedded custom fonts, and tables is a different problem entirely. The PDF format doesn't really have a concept of "this text belongs to this table cell"  it just has text positioned at specific x/y coordinates on a page. Reconstructing table structure means inferring it from whether text blocks are roughly aligned in a grid, which is a heuristic, not a guarantee.&lt;/p&gt;

&lt;p&gt;The honest fix here wasn't a clever algorithm — it was setting expectations correctly. Simple, single-column PDFs convert close to perfectly. Complex multi-column academic papers or financial statements with dense tables convert "usably editable, expect to fix formatting," and the tool now says exactly that instead of silently producing a mangled table and letting the user discover it themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup matters more than it sounds like it should&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backend hosting on a platform like Railway means the filesystem is ephemeral, but "ephemeral" doesn't mean "instantly cleaned between requests"  temp files from a crashed or abandoned conversion can sit around until the container itself restarts. Under real traffic, that adds up to disk pressure from files nobody's coming back for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_input_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_input_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_input_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block matters more than it looks like it should  an exception partway through processing shouldn't leave an orphaned file, and it's an easy thing to skip when you're focused on the happy path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell someone building similar tools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-world files are messier than test files.&lt;/strong&gt; Malformed PDFs are common enough that a repair/recovery path isn't optional if you're accepting arbitrary uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any tool that decodes user-supplied compressed data needs a size check before decompression&lt;/strong&gt;, not after  the same principle as zip bombs applies directly to embedded images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory accumulation across a loop is easy to miss&lt;/strong&gt; when each individual file seems small it's the cumulative footprint across an entire batch operation that actually causes problems in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Being honest about fidelity limits&lt;/strong&gt; (this converts cleanly, this one needs manual cleanup) builds more trust than silently producing a degraded result and letting the user find out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools are live and free if you want to see them in action: PDF merger, splitter, and compressor are all part of &lt;a href="https://dukotools.com/tools/pdf-merger" rel="noopener noreferrer"&gt;DukoTools&lt;/a&gt;  no signup required.&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any part of this in the comments, especially the PDF repair/recovery logic if anyone's dealt with similarly malformed real-world files.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Video Downloader That Doesn't Break Every Time YouTube Sneezes</title>
      <dc:creator>Talha Ramzan</dc:creator>
      <pubDate>Thu, 30 Jul 2026 11:21:07 +0000</pubDate>
      <link>https://dev.to/talha_ramzan_3878156fea8c/building-a-video-downloader-that-doesnt-break-every-time-youtube-sneezes-lpa</link>
      <guid>https://dev.to/talha_ramzan_3878156fea8c/building-a-video-downloader-that-doesnt-break-every-time-youtube-sneezes-lpa</guid>
      <description>&lt;p&gt;A few weeks back I wrote about the lessons I learned building 120+ free tools solo. A few people asked which tool was the hardest to actually keep working  hands down, it's the video downloader. Not because the initial version was hard to build. Because YouTube (and every other platform) actively fights you, and the "happy path" breaks constantly in production.&lt;/p&gt;

&lt;p&gt;Here's what building and maintaining it actually looked like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The naive version (day 1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Like everyone, I started with yt-dlp. It's genuinely excellent handles 1000+ sites out of the box, actively maintained, and for the first few weeks it just worked.&lt;/p&gt;

&lt;p&gt;python:&lt;br&gt;
import yt_dlp&lt;/p&gt;

&lt;p&gt;def get_video_info(url):&lt;br&gt;
    with yt_dlp.YoutubeDL({'quiet': True}) as ydl:&lt;br&gt;
        return ydl.extract_info(url, download=False)&lt;/p&gt;

&lt;p&gt;Clean, simple, done. Or so I thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: YouTube blocks you back&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Within a couple weeks of real traffic, YouTube started throwing bot-detection errors intermittently  not always, just often enough to be genuinely annoying to debug. Some requests worked fine, others got flagged. Classic cat-and-mouse: yt-dlp ships a fix, YouTube tweaks detection, repeat.&lt;/p&gt;

&lt;p&gt;Two things helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PO tokens (proof-of-origin tokens) — a bypass mechanism yt-dlp added support for&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cookie injection — passing a logged-in session's cookies through YOUTUBE_COOKIES_TEXT/YOUTUBE_COOKIES_FILE env vars, in Netscape cookie format&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But neither is bulletproof, and yt-dlp itself needs constant updates to keep pace. I ended up version-checking and self-updating yt-dlp on every cold start (pip install --upgrade git+...) rather than pinning a version, since a week-old release could already be obsolete against YouTube's latest detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 2: When the primary engine fails, fail gracefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with cookies and PO tokens, yt-dlp occasionally gets fully blocked for a stretch. Instead of just erroring out to the user, I added a fallback engine: pytubefix, specifically for YouTube.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't just "try A, then try B", it was building a proper circuit breaker:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
class YouTubeCircuitBreaker:&lt;br&gt;
    def &lt;strong&gt;init&lt;/strong&gt;(self):&lt;br&gt;
        self.consecutive_failures = 0&lt;br&gt;
        self.backoff_until = None&lt;br&gt;
        self.backoff_minutes = 1&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def record_failure(self):
    self.consecutive_failures += 1
    if self.consecutive_failures &amp;gt;= 5:
        self.backoff_until = now() + timedelta(minutes=self.backoff_minutes)
        self.backoff_minutes = min(self.backoff_minutes * 2, 60)

def should_bypass_primary(self):
    return self.backoff_until and now() &amp;lt; self.backoff_until
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After 5 consecutive yt-dlp failures on YouTube specifically, the circuit "trips" and routes straight to pytubefix for a backoff window — doubling up to a 60-minute ceiling. This meant users stopped seeing hard failures during YouTube's flakier periods, and yt-dlp got breathing room instead of hammering an API that was already rejecting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3: People will absolutely try to use this to hit internal infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any tool that accepts a URL and fetches it server-side is a potential SSRF (Server-Side Request Forgery) vector. Someone will eventually try passing &lt;a href="http://169.254.169.254/latest/meta-data/" rel="noopener noreferrer"&gt;http://169.254.169.254/latest/meta-data/&lt;/a&gt; (cloud metadata endpoints) or &lt;a href="http://localhost:6379" rel="noopener noreferrer"&gt;http://localhost:6379&lt;/a&gt; (internal Redis) just to see what happens.&lt;/p&gt;

&lt;p&gt;Before any URL reaches yt-dlp, it goes through a private-host check:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
def _is_private_host(hostname):&lt;br&gt;
    ip = socket.gethostbyname(hostname)&lt;br&gt;
    ip_obj = ipaddress.ip_address(ip)&lt;br&gt;
    return (&lt;br&gt;
        ip_obj.is_private or&lt;br&gt;
        ip_obj.is_loopback or&lt;br&gt;
        ip_obj.is_link_local or&lt;br&gt;
        hostname.endswith(('.internal', '.local'))&lt;br&gt;
    )&lt;/p&gt;

&lt;p&gt;Blocking private/loopback/link-local ranges and internal TLDs before the extractor ever sees the URL. Cheap to implement, and it closes off an entire class of abuse that's easy to forget about until someone finds it for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 4: Format strings are basically user input into a shell-adjacent tool&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;yt-dlp's format selector syntax is powerful  you can do things like bestvideo+bestaudio/best. But if you're building that string from user-supplied quality/format choices, you're one step away from format-selector injection. A whitelist regex on allowed format-selector patterns closed that gap before it became a real problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 5: Rate limiting, because bandwidth isn't free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;200MB max file size, 10 downloads per IP per hour, and a 5 minute in-memory cache (max 500 entries) for repeated metadata lookups  mostly to avoid re-extracting info for the same viral video getting hit 40 times in an hour by different users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell someone building something similar&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the "it just works" phase.&lt;/strong&gt; It works until the platform you depend on changes something, and it will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fallback engine is worth the complexity&lt;/strong&gt; if your primary dependency has any adversarial relationship with the source (bot detection, rate limits, ToS enforcement).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breakers aren't just for microservices&lt;/strong&gt;  even a single external dependency benefits from "stop hammering this after N failures" logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any tool that fetches user-supplied URLs server-side needs SSRF protection from day one&lt;/strong&gt;, not as an afterthought after a security report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool is live and free if you want to see it in action: dukotools.com/tools/video-downloader — supports YouTube, TikTok, Instagram, and 13 other platforms.&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any part of this in the comments especially curious if anyone's solved the yt-dlp bot-detection cat-and-mouse better than cookie injection + PO tokens.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
      <category>development</category>
    </item>
    <item>
      <title>I Built 120+ Free Online Tools. Here Are 10 Mistakes I'd Never Make Again.</title>
      <dc:creator>Talha Ramzan</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:28:04 +0000</pubDate>
      <link>https://dev.to/talha_ramzan_3878156fea8c/i-built-120-free-online-tools-here-are-10-mistakes-id-never-make-again-k2p</link>
      <guid>https://dev.to/talha_ramzan_3878156fea8c/i-built-120-free-online-tools-here-are-10-mistakes-id-never-make-again-k2p</guid>
      <description>&lt;p&gt;Four months ago I decided to build a simple utility website.&lt;/p&gt;

&lt;p&gt;The original plan was maybe 15–20 tools.&lt;/p&gt;

&lt;p&gt;Instead, I somehow ended up shipping 120+ tools covering PDFs, images, developers, students, finance, AI utilities, and a lot of niche calculators.&lt;/p&gt;

&lt;p&gt;Looking back, I made almost every mistake a solo developer can make.&lt;/p&gt;

&lt;p&gt;Hopefully this saves someone else a few months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Stop waiting for the "perfect launch"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent four months building before showing the project to anyone.&lt;/p&gt;

&lt;p&gt;Huge mistake.&lt;/p&gt;

&lt;p&gt;Nobody cares how many features you have.&lt;/p&gt;

&lt;p&gt;People care whether you solve their problem.&lt;/p&gt;

&lt;p&gt;If I started again, I'd launch after 10–20 tools and let users decide what to build next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Boring tools get used more than clever ones&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I thought people would love the complicated AI tools.&lt;/p&gt;

&lt;p&gt;Instead, the most used ones were things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image Compressor&lt;/li&gt;
&lt;li&gt;PDF Merger&lt;/li&gt;
&lt;li&gt;JSON Formatter&lt;/li&gt;
&lt;li&gt;QR Code Generator&lt;/li&gt;
&lt;li&gt;Password Generator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple wins.&lt;/p&gt;

&lt;p&gt;People search for solutions, not engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Niche tools are an SEO superpower&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone builds a BMI calculator.&lt;/p&gt;

&lt;p&gt;Almost nobody builds things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pakistani Electricity Bill Calculator&lt;/li&gt;
&lt;li&gt;Zakat Calculator&lt;/li&gt;
&lt;li&gt;CNIC Validator&lt;/li&gt;
&lt;li&gt;Cricket Run Rate Calculator&lt;/li&gt;
&lt;li&gt;Construction Bid Comparison Tool
The search volume is lower.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The competition is almost zero.&lt;/p&gt;

&lt;p&gt;Those pages have become some of my favourites because they solve real problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Browser APIs are incredibly underrated&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many tools don't need a backend.&lt;/p&gt;

&lt;p&gt;Compression.&lt;/p&gt;

&lt;p&gt;Image editing.&lt;/p&gt;

&lt;p&gt;QR generation.&lt;/p&gt;

&lt;p&gt;JSON formatting.&lt;/p&gt;

&lt;p&gt;Regex testing.&lt;/p&gt;

&lt;p&gt;UUID generation.&lt;/p&gt;

&lt;p&gt;Password generation.&lt;/p&gt;

&lt;p&gt;CSV editing.&lt;/p&gt;

&lt;p&gt;All of these can happen directly inside the browser.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster&lt;/li&gt;
&lt;li&gt;More private&lt;/li&gt;
&lt;li&gt;Cheaper to host&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Docker can save you from cold starts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One problem almost drove me insane.&lt;/p&gt;

&lt;p&gt;My AI background remover worked perfectly locally.&lt;/p&gt;

&lt;p&gt;Production?&lt;/p&gt;

&lt;p&gt;Every first request timed out.&lt;/p&gt;

&lt;p&gt;The reason:&lt;/p&gt;

&lt;p&gt;The U2Net model (used by rembg) downloaded on first request.&lt;/p&gt;

&lt;p&gt;Around 170 MB.&lt;/p&gt;

&lt;p&gt;The fix?&lt;/p&gt;

&lt;p&gt;Download the model during the Docker build instead.&lt;/p&gt;

&lt;p&gt;One tiny change.&lt;/p&gt;

&lt;p&gt;Massive improvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Users hate unnecessary accounts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I removed logins completely.&lt;/p&gt;

&lt;p&gt;No email.&lt;/p&gt;

&lt;p&gt;No registration.&lt;/p&gt;

&lt;p&gt;No "Create an account to continue."&lt;/p&gt;

&lt;p&gt;People just use the tool and leave.&lt;/p&gt;

&lt;p&gt;That's exactly what most utility websites should encourage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Performance matters more than animations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent hours making animations look perfect.&lt;/p&gt;

&lt;p&gt;Users never mentioned them.&lt;/p&gt;

&lt;p&gt;They did notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow loading&lt;/li&gt;
&lt;li&gt;laggy interactions&lt;/li&gt;
&lt;li&gt;mobile issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speed is a feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Mobile users deserve first-class treatment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;More than half the internet is mobile.&lt;/p&gt;

&lt;p&gt;If your utility only works nicely on desktop...&lt;/p&gt;

&lt;p&gt;You've already lost a huge percentage of users.&lt;/p&gt;

&lt;p&gt;Every tool I build now starts with mobile in mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Build things you personally need&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of my favourite features is an Online Clipboard.&lt;/p&gt;

&lt;p&gt;Open it on your desktop.&lt;/p&gt;

&lt;p&gt;Scan a QR code.&lt;/p&gt;

&lt;p&gt;Start typing on your phone.&lt;/p&gt;

&lt;p&gt;The text appears instantly.&lt;/p&gt;

&lt;p&gt;No login.&lt;/p&gt;

&lt;p&gt;No app.&lt;/p&gt;

&lt;p&gt;No emailing yourself.&lt;/p&gt;

&lt;p&gt;It wasn't built because someone requested it.&lt;/p&gt;

&lt;p&gt;It solved a problem I had every week.&lt;/p&gt;

&lt;p&gt;Those are often the best products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Marketing is part of building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers often think marketing starts after shipping.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;If nobody knows your project exists...&lt;/p&gt;

&lt;p&gt;It doesn't matter how clean your architecture is.&lt;/p&gt;

&lt;p&gt;Building is comfortable.&lt;/p&gt;

&lt;p&gt;Marketing is uncomfortable.&lt;/p&gt;

&lt;p&gt;Unfortunately, both are required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the project is today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project has grown into DukoTools, a collection of 120+ free online tools.&lt;/p&gt;

&lt;p&gt;Everything is free.&lt;/p&gt;

&lt;p&gt;No signup.&lt;/p&gt;

&lt;p&gt;Many tools work entirely in the browser, and the site can even be installed as a Progressive Web App for offline use.&lt;/p&gt;

&lt;p&gt;I'm now focusing on improving the existing tools based on real user feedback instead of endlessly adding new ones.&lt;/p&gt;

&lt;p&gt;If you have a favourite online tool that doesn't exist or one that could be significantly better I'd genuinely love to hear about it.&lt;/p&gt;

&lt;p&gt;Building is fun.&lt;/p&gt;

&lt;p&gt;Building things people actually use is even better.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built 120+ free online tools as a solo dev — here is what I actually learned</title>
      <dc:creator>Talha Ramzan</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:02:47 +0000</pubDate>
      <link>https://dev.to/talha_ramzan_3878156fea8c/i-built-120-free-online-tools-as-a-solo-dev-here-is-what-i-actually-learned-41pa</link>
      <guid>https://dev.to/talha_ramzan_3878156fea8c/i-built-120-free-online-tools-as-a-solo-dev-here-is-what-i-actually-learned-41pa</guid>
      <description>&lt;p&gt;I am going to be honest with you. This project started out of pure frustration.&lt;/p&gt;

&lt;p&gt;Every time I needed to do something simple — compress an image, convert a PDF, format some JSON — I would land on a site that hit me with a cookie banner, then a popup asking me to sign up, then an ad covering half the tool, and then after all that the tool would not even work properly on my phone.&lt;/p&gt;

&lt;p&gt;I got tired of it. So I built my own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I built&lt;/strong&gt;&lt;br&gt;
dukotools.com is a free utility website with 120+ tools. No ads right now. No signup ever. Works offline as a PWA. You can install it on your phone like an app and use most tools without internet.&lt;/p&gt;

&lt;p&gt;It covers the usual stuff — PDF conversion, image compression, background removal, JSON formatting, regex testing, SQL formatting. But it also has tools I genuinely could not find anywhere else free:&lt;/p&gt;

&lt;p&gt;A Zakat calculator for Muslim users that handles gold and silver Nisab thresholds. A Pakistani electricity bill calculator that uses the current NEPRA tariff slabs. A CNIC validator that detects province and gender from the ID number. A cricket run rate calculator that supports custom balls per over because street cricket in Pakistan does not always use 6-ball overs. A construction bid comparison tool for US contractors that scores bids on price, completeness, and qualifications.&lt;/p&gt;

&lt;p&gt;Some of these are niche. But they solve real problems for real people and nothing good existed for them before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tech stack&lt;/strong&gt;&lt;br&gt;
Frontend is Next.js 14 App Router with TypeScript, Tailwind CSS, Zustand for global state, and next-intl for 5 language support covering English, Urdu, Arabic, Spanish, and French. Deployed on Vercel.&lt;/p&gt;

&lt;p&gt;Backend is Python FastAPI with PyMuPDF for PDF processing, pdf2docx for Word conversion, rembg for AI background removal, yt-dlp for video downloading, pytesseract for OCR, and LibreOffice headless for converting Excel and PowerPoint files to PDF. Deployed on Render.&lt;/p&gt;

&lt;p&gt;Realtime features use Supabase. The rest is just browser APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most interesting thing I built&lt;/strong&gt;&lt;br&gt;
The online clipboard tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; you are on your phone and you need to get a chunk of text to your laptop without emailing yourself or using WhatsApp. Most solutions require an account or an app install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My solution:&lt;/strong&gt; open dukotools.com/tools/online-clipboard on both devices. A QR code generates instantly on the desktop pointing to a URL with a 6 character room code. Scan it on your phone. Both devices connect to the same Supabase Realtime WebSocket channel. Type on one device and the other updates in under a second. No account. No app. Session expires in 60 minutes and nothing is stored after that.&lt;/p&gt;

&lt;p&gt;The interesting engineering parts were managing WebSocket reconnection with exponential backoff when the connection drops, handling the edge case where both devices type simultaneously without one overwriting the other, and generating the QR code entirely client side with zero server calls using qrcode.react so the feature works even if the backend is down.&lt;/p&gt;

&lt;p&gt;It is a small feature but users genuinely love it. It solves a daily annoyance in a way that feels like magic the first time you use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was harder than I expected&lt;/strong&gt;&lt;br&gt;
Getting the background remover to work reliably in production.&lt;/p&gt;

&lt;p&gt;rembg is an incredible open source library that removes image backgrounds using a U2Net machine learning model. The problem is it downloads a 170MB model file on first run. On Railway and Render free tier the first request after a cold start would time out waiting for the model to download.&lt;/p&gt;

&lt;p&gt;The fix was embarrassingly simple once I figured it out. I added a line to the Dockerfile that runs a dummy background removal operation during the Docker build phase. This forces the model to download and cache during build time rather than at runtime. First request now responds in under 3 seconds instead of timing out.&lt;/p&gt;

&lt;p&gt;One line of Python in the Dockerfile saved me days of debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I would do differently&lt;/strong&gt;&lt;br&gt;
Start marketing on day one.&lt;/p&gt;

&lt;p&gt;I built for four months before telling a single stranger about the site. Every tool I added in month three could have waited. The first 20 tools were more than enough to start collecting feedback and finding out what people actually wanted.&lt;/p&gt;

&lt;p&gt;Building feels productive. It is comfortable, measurable, and completely in your control. Marketing feels uncertain and uncomfortable. So you keep building instead.&lt;/p&gt;

&lt;p&gt;But 120 tools with zero users is worth less than 20 tools with 500 daily visitors. I know this now. I am telling you so you do not make the same mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stands today&lt;/strong&gt;&lt;br&gt;
The site is live at dukotools.com. It is completely free. I am only starting to do marketing now after 4 months of building in silence.&lt;/p&gt;

&lt;p&gt;If you have 2 minutes I would genuinely appreciate you trying any tool and telling me what is broken, what feels slow, or what tool you use daily that I have not built yet. That feedback is more valuable to me right now than anything else.&lt;/p&gt;

&lt;p&gt;And if you have ever built something and kept it hidden for too long , you are not alone. Ship earlier than you are comfortable with. Every time.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I am documenting this whole journey publicly if you want to follow along.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
