<?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: Bright Agbomado</title>
    <description>The latest articles on DEV Community by Bright Agbomado (@relahconvert).</description>
    <link>https://dev.to/relahconvert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3811672%2F1aab6fab-aa6b-4bc0-bf95-b36f16e96e93.png</url>
      <title>DEV Community: Bright Agbomado</title>
      <link>https://dev.to/relahconvert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/relahconvert"/>
    <language>en</language>
    <item>
      <title>TIL a PowerPoint file is just a zip — so I converted .pptx to Word entirely in the browser</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Mon, 25 May 2026 00:12:26 +0000</pubDate>
      <link>https://dev.to/relahconvert/til-a-powerpoint-file-is-just-a-zip-so-i-converted-pptx-to-word-entirely-in-the-browser-18a8</link>
      <guid>https://dev.to/relahconvert/til-a-powerpoint-file-is-just-a-zip-so-i-converted-pptx-to-word-entirely-in-the-browser-18a8</guid>
      <description>&lt;p&gt;Most file converters upload your files to a server. I wanted to see how far I could get without one. Turns out: all the way.&lt;br&gt;
The trick is that Office files aren't really binary blobs — a .pptx is a zip archive full of XML. Rename it to .zip, unzip it, and you'll find each slide sitting there as its own XML file. Once you know that, "convert PowerPoint to Word in the browser" stops sounding impossible.&lt;br&gt;
Three libraries chained together, all running client-side:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;JSZip&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jszip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Packer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Paragraph&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Crack open the .pptx (it's a zip)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;JSZip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Walk each slide's XML for text&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&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;slideFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/ppt&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;slides&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;slide&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;xml$/&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;paragraphs&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;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;slideFiles&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;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a:t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;a:t&amp;gt; holds the text runs&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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;paragraphs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Paragraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Write it out as a .docx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Packer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paragraphs&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;JSZip unzips, the browser's own DOMParser reads the slide XML ( tags are where the text lives), and docx writes the result to a .docx blob you can download. No server. No upload. No API key. The file never leaves your machine.&lt;/p&gt;

&lt;p&gt;The nice side effect: because there's no upload cost, I let this one batch up to 25 files at once.&lt;/p&gt;

&lt;p&gt;The honest tradeoff — pulling clean text and tables out of slide XML is the easy 80%; perfectly preserving every layout quirk is the hard 20%, and a pure-browser approach trades some of that fidelity for privacy and speed. For getting your slide content into an editable Word doc fast, it holds up well.&lt;/p&gt;

&lt;p&gt;Live here if you want to try it: &lt;a href="//relahconvert.com/powerpoint-to-word"&gt;PowerPoint to word&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I just launched RelahConvert on TinyLaunch</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Mon, 18 May 2026 13:59:28 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-just-launched-relahconvert-on-tinylaunch-482b</link>
      <guid>https://dev.to/relahconvert/i-just-launched-relahconvert-on-tinylaunch-482b</guid>
      <description>&lt;p&gt;After 3 months of solo building from Calgary, RelahConvert is live on TinyLaunch today.&lt;/p&gt;

&lt;p&gt;RelahConvert is a free file converter and editor. The goal was simple: build a tool you can just open and use without thinking about it.&lt;br&gt;
What's inside right now:&lt;/p&gt;

&lt;p&gt;Image tools — convert, compress, resize, crop, background removal, passport photos&lt;br&gt;
PDF tools — merge, split, rotate, compress, reorder, extract pages, add page numbers, watermark, crop, protect, unlock, extract images&lt;br&gt;
Office tools — Word, Excel, and PowerPoint to PDF, plus PDF back to all three (just shipped last week)&lt;/p&gt;

&lt;p&gt;Everything supports bulk processing. Available in 25 languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I built it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is I wanted to learn by building something real, not by following tutorials. File conversion felt like a useful problem space — most existing tools either lock features behind paywalls, require accounts for basic use, or feel slow.&lt;br&gt;
I'm not a trained developer. I started with no team, no funding, and no roadmap beyond "build something people might actually use."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it stands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three months in, ~1,400 pages indexed, growing slowly. Real visits are still small. The product is functional and shipping, but the marketing and distribution side is the part I'm learning in public.&lt;br&gt;
Today is a milestone — first proper launch on a directory. I have no expectation of cracking the top 3. The point was to do the thing, not to win.&lt;br&gt;
If you want to check it out&lt;br&gt;
The launch is live here: &lt;a href="https://www.tinylaunch.com/launch/13598" rel="noopener noreferrer"&gt;https://www.tinylaunch.com/launch/13598&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome. I'm especially curious what people think about the no-account-required default — that was a deliberate choice and I'd love to hear if it works for you or feels weird.&lt;br&gt;
Happy to support other indie launches too — drop yours in the comments.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>hreflang tags were duplicating and I didn't notice for weeks</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Sat, 16 May 2026 14:24:57 +0000</pubDate>
      <link>https://dev.to/relahconvert/hreflang-tags-were-duplicating-and-i-didnt-notice-for-weeks-h8</link>
      <guid>https://dev.to/relahconvert/hreflang-tags-were-duplicating-and-i-didnt-notice-for-weeks-h8</guid>
      <description>&lt;p&gt;Quick story about a bug I shipped without knowing for weeks.&lt;/p&gt;

&lt;p&gt;Ahrefs flagged a warning: "More than one page for same language in hreflang — 161 pages affected." I went to view source on a tool page and counted the hreflang tags.&lt;/p&gt;

&lt;p&gt;52.&lt;/p&gt;

&lt;p&gt;Should have been 26.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was happening
&lt;/h2&gt;

&lt;p&gt;My build process correctly generated 26 hreflang tags per page — 25 languages plus &lt;code&gt;x-default&lt;/code&gt;. Clean.&lt;/p&gt;

&lt;p&gt;But then at runtime, JavaScript was doing this:&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;injectHreflang&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;langs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alternate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hreflang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;
    &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&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="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;appendChild&lt;/code&gt;. Not &lt;code&gt;replace&lt;/code&gt;. Not "check if exists first." Just append, every time.&lt;/p&gt;

&lt;p&gt;So every page shipped with 26 build-time tags in the static HTML, and then JS added 26 more on page load. Net result: 52 tags, every language listed twice pointing to identical URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it mattered
&lt;/h2&gt;

&lt;p&gt;Google's rule on duplicate hreflang: when a language appears twice with different URLs, it ignores both. When it appears twice with the same URL, Google still flags it as a signal-quality issue and downgrades trust in your hreflang setup overall.&lt;/p&gt;

&lt;p&gt;I'd been wondering for weeks why non-English variants weren't ranking. Turns out I was telling Google "the French version is here AND also here" on 1,400+ pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I deleted the runtime &lt;code&gt;injectHreflang()&lt;/code&gt; function entirely. Forty-one tool files were calling it — I removed the calls and the import everywhere.&lt;/p&gt;

&lt;p&gt;The build-time tags were already correct. The runtime injection was redundant code from an earlier phase of the project that had outlived its purpose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://relahconvert.com/merge-pdf | &lt;span class="nb"&gt;grep &lt;/span&gt;hreflang | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
52

&lt;span class="c"&gt;# After&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://relahconvert.com/merge-pdf | &lt;span class="nb"&gt;grep &lt;/span&gt;hreflang | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;When something works at build time AND at runtime, it doesn't mean it's working twice as well. It usually means you have two systems fighting each other, and you'll only notice when an audit tool flags it.&lt;/p&gt;

&lt;p&gt;You can see the fix in action on the &lt;a href="https://relahconvert.com/merge-pdf" rel="noopener noreferrer"&gt;RelahConvert PDF merger&lt;/a&gt; — view-source and count the hreflang tags yourself.&lt;/p&gt;

&lt;p&gt;If you're building a multilingual site, check this on your own pages. Takes 10 seconds and the bug is more common than you'd think.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>seo</category>
    </item>
    <item>
      <title>How I Added Pre-Rendering to a Vite Multi-Page App Without SSR</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Mon, 11 May 2026 00:20:14 +0000</pubDate>
      <link>https://dev.to/relahconvert/how-i-added-pre-rendering-to-a-vite-multi-page-app-without-ssr-3mo4</link>
      <guid>https://dev.to/relahconvert/how-i-added-pre-rendering-to-a-vite-multi-page-app-without-ssr-3mo4</guid>
      <description>&lt;p&gt;I run &lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt;, an online file conversion site with 50+ tools across 25 languages. Last week I noticed a problem in my Ahrefs audit: every single one of my 1,449 pages was flagged as "H1 missing" and "Low word count."&lt;br&gt;
The pages weren't empty. They had H1s, descriptions, FAQs — all rendering correctly when you visited them. But here's what crawlers were seeing:&lt;br&gt;
&lt;/p&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;


&lt;p&gt;Yep. Empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The site is a Vite MPA (multi-page app), not a SPA. Each tool has its own .html file. Tool-specific JavaScript injects the page content at runtime via document.querySelector('#app').innerHTML = template.&lt;br&gt;
This works great for users. The page loads, JS runs, content appears. Fast and clean.&lt;br&gt;
But it breaks for crawlers that don't execute JavaScript (Ahrefs, basic bots, social media scrapers like Facebook and X). They see an empty body. Even Google, which does run JS, has a delayed second-pass rendering that costs crawl budget and makes new pages slow to index.&lt;br&gt;
The Usual Solutions Didn't Fit&lt;br&gt;
The standard fixes for this problem:&lt;/p&gt;

&lt;p&gt;Server-Side Rendering (SSR) — requires a runtime backend. I'm on Cloudflare Pages (static hosting). Not happening.&lt;br&gt;
Static Site Generation (SSG) — would mean restructuring around a framework like Astro or Vike. Massive refactor for a site already shipping.&lt;br&gt;
Pre-rendering with Puppeteer — spinning up a headless browser for every route during build. ~3-5 minute build time hit for 1,449 pages, plus heavy dependencies.&lt;/p&gt;

&lt;p&gt;I wanted something lighter: just inject the SEO-relevant content (H1, description, FAQs) into the static HTML at build time, without re-implementing my entire UI in Node.&lt;br&gt;
The Approach: Hybrid Pre-Render via Vite Plugin&lt;br&gt;
I extended the existing build plugin to read my i18n dictionaries and write the SEO content directly into each HTML file. The interactive tool UI (drag-drop, canvas operations, conversion logic) stays JS-rendered — only the content that matters for crawlers gets baked in.&lt;br&gt;
Rough shape of the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preRenderPlugin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pre-render-seo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;build&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;closeBundle&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;tools&lt;/span&gt; &lt;span class="o"&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;compress&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;resize&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;merge-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;langs&lt;/span&gt; &lt;span class="o"&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;en&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;fr&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;es&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;de&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;ar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;

      &lt;span class="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;tool&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&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;lang&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;langs&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;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadI18n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&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;seo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`dist/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;slugFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/index.html`&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;injected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;injectSEOContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nav_short&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heroDesc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;seo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;})&lt;/span&gt;

          &lt;span class="nf"&gt;writeHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;injected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The content gets injected into the &lt;/p&gt; placeholder. When JS runs at runtime, it replaces the contents with the interactive UI — but the pre-rendered version is what crawlers see.

&lt;p&gt;&lt;strong&gt;Three Gotchas&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Per-language metadata. My tool pages had hardcoded English &lt;/p&gt;
 and , even on French URLs. Pre-rendering exposed this — crawlers were seeing French H1s with English titles. I had to extend the same plugin to resolve title and meta description per language from i18n at build time.
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FOUC on per-language URLs. Initially I derived per-language URLs from the homepage template. JS at runtime detected the route and wiped the body to inject the tool. Result: brief flash of French content, then disappearance, then return. Fixed by switching per-language URLs to use the tool's own template as the base.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cloudflare bot protection blocking social scrapers. Unrelated discovery — after fixing the OG tags, Facebook's Sharing Debugger returned 403. Cloudflare's Browser Integrity Check was challenging the Facebook scraper. The fix was a Cloudflare dashboard config, not code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build time went from 22.7s to ~23s. Negligible. Every tool page now ships with proper H1, description, FAQs, and internal links in the static HTML across all 25 languages. Social previews work. Ahrefs and Google can read content on first crawl.&lt;/p&gt;

&lt;p&gt;You can see the live result on the &lt;a href="https://relahconvert.com/compress" rel="noopener noreferrer"&gt;image compressor&lt;/a&gt; — view-source on the page shows the pre-rendered content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway&lt;/strong&gt;: if you're on Vite without a framework and need crawler-friendly HTML without going full SSG, a build-time plugin that injects SEO content into your existing structure is a surprisingly clean middle ground.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>seo</category>
      <category>vite</category>
    </item>
    <item>
      <title>I Asked 500 Developers What AI They Use to Code. The Answers Were Surprising</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Fri, 10 Apr 2026 22:17:56 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-asked-500-developers-what-ai-they-use-to-code-the-answers-were-surprising-4hi4</link>
      <guid>https://dev.to/relahconvert/i-asked-500-developers-what-ai-they-use-to-code-the-answers-were-surprising-4hi4</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt; — a 37-tool image toolkit — entirely with Claude Code. No traditional coding background. Just prompts and persistence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It got me thinking. Everyone claims to use AI for coding now. But which one actually wins in the real world?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I started paying attention. Forums, Twitter, Reddit, dev.to comments. Here's what I actually found:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest breakdown:&lt;/strong&gt;&lt;br&gt;
GitHub Copilot — the corporate safe choice. Used by developers who already know how to code and want autocomplete on steroids. Nobody brags about it. Nobody hates it. It just exists.&lt;/p&gt;

&lt;p&gt;ChatGPT — the first tool everyone tried. Still popular for quick questions and explaining code. But people rarely use it to actually build full projects anymore.&lt;/p&gt;

&lt;p&gt;Cursor — the new darling. Every developer on Twitter seems to be using Cursor right now. It's eating Copilot's lunch among serious developers.&lt;/p&gt;

&lt;p&gt;Claude Code — the one nobody talks about publicly but keeps appearing in solo founder stories. Especially for people building full products from scratch, not just autocompleting lines.&lt;/p&gt;

&lt;p&gt;Gemini — Google's answer. Technically impressive. Nobody seems emotionally attached to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern I noticed:&lt;/strong&gt;&lt;br&gt;
Developers who already code → use Copilot or Cursor to go faster&lt;br&gt;
Non-developers building products → use Claude Code or ChatGPT to actually ship&lt;br&gt;
Two completely different use cases. Two completely different tools winning.&lt;/p&gt;

&lt;p&gt;The controversial part:&lt;br&gt;
The "vibe coder" debate is pointless.&lt;br&gt;
People who gatekeep coding — "you're not a real developer if you use AI" — are the same people who said "you're not a real developer if you use Stack Overflow" in 2010.&lt;br&gt;
The output is what matters. I shipped 37 tools in 6 weeks. I don't know what a for loop looks like from memory. My users don't care.&lt;/p&gt;

&lt;p&gt;The real question nobody asks:&lt;br&gt;
Not "which AI do you use?"&lt;br&gt;
But "did you actually ship anything?"&lt;br&gt;
Most people collecting AI coding tools haven't shipped a single thing. The tool isn't the flex. The product is.&lt;/p&gt;

&lt;p&gt;What are you actually using to build — and have you shipped something with it? 👇&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>9 Things I Did Wrong Building My Image Tool (And What Actually Fixed Them)</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Fri, 03 Apr 2026 22:12:32 +0000</pubDate>
      <link>https://dev.to/relahconvert/9-things-i-did-wrong-building-my-image-tool-and-what-actually-fixed-them-326l</link>
      <guid>https://dev.to/relahconvert/9-things-i-did-wrong-building-my-image-tool-and-what-actually-fixed-them-326l</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://relahconvert.com/passport-photo" rel="noopener noreferrer"&gt;Relahconvert&lt;/a&gt; — a free browser-based image toolkit — for about a month now. 37 tools, 25 languages, zero backend for most of it.&lt;br&gt;
Sounds clean. It wasn't 😄&lt;br&gt;
Here are 9 real mistakes I made and what actually solved them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;"I'll just use a library for that" → Canvas API&lt;br&gt;
My first instinct for image processing was to reach for a library. Turns out the browser's Canvas API handles resize, crop, flip, grayscale, and more natively. No dependencies. No bundle size. Just pixels.&lt;br&gt;
&lt;code&gt;js&lt;br&gt;
const ctx = canvas.getContext('2d');&lt;br&gt;
ctx.drawImage(img, 0, 0, newWidth, newHeight);&lt;br&gt;
canvas.toBlob(blob =&amp;gt; downloadFile(blob), 'image/jpeg', 0.9);&lt;/code&gt;&lt;br&gt;
I now use this for 90% of my tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Dark mode needs JavaScript" → prefers-color-scheme&lt;br&gt;
I built a whole JS toggle system before realizing the browser already knows what the user wants.&lt;br&gt;
css@media (prefers-color-scheme: dark) {&lt;br&gt;
:root {&lt;br&gt;
--bg: #1a1a1a;&lt;br&gt;
--text: #f0f0f0;&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
Still kept the manual toggle — but the default is now instant and correct.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"I need a backend for file downloads" → URL.createObjectURL&lt;br&gt;
I thought serving processed images required a server. Nope.&lt;br&gt;
jsconst url = URL.createObjectURL(blob);&lt;br&gt;
const a = document.createElement('a');&lt;br&gt;
a.href = url;&lt;br&gt;
a.download = 'converted.png';&lt;br&gt;
a.click();&lt;br&gt;
URL.revokeObjectURL(url);&lt;br&gt;
Everything stays in the browser. No uploads. No server costs. Users love the privacy angle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Batch processing will kill performance" → Web Workers&lt;br&gt;
Processing 25 images at once was freezing the UI. Web Workers run in a separate thread — the page stays responsive while the heavy lifting happens in the background.&lt;br&gt;
jsconst worker = new Worker('process.js');&lt;br&gt;
worker.postMessage({ imageData, format: 'webp' });&lt;br&gt;
worker.onmessage = e =&amp;gt; renderResult(e.data);&lt;br&gt;
Game changer for batch tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"I need an API for compression" → canvas.toBlob quality param&lt;br&gt;
I almost paid for a compression API. Then I noticed toBlob has a quality parameter.&lt;br&gt;
jscanvas.toBlob(blob =&amp;gt; save(blob), 'image/jpeg', 0.7);&lt;br&gt;
0.7 gives you roughly 60-70% size reduction with barely visible quality loss. Free. Built-in. Already there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"RTL Arabic will break my layout" → CSS logical properties&lt;br&gt;
Supporting 25 languages including Arabic felt terrifying. Logical properties made it manageable.&lt;br&gt;
css.tool-container {&lt;br&gt;
margin-inline-start: 1rem;&lt;br&gt;
padding-inline: 1.5rem;&lt;br&gt;
}&lt;br&gt;
One declaration. Works LTR and RTL automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"I'll handle the API key in the frontend" → Cloudflare Workers&lt;br&gt;
I exposed an API key in my frontend. Bots found it within days 😬&lt;br&gt;
The fix: a Cloudflare Worker as a proxy. Your key lives server-side, requests go through the worker, users never see it.&lt;br&gt;
jsexport default {&lt;br&gt;
async fetch(request) {&lt;br&gt;
const response = await fetch('&lt;a href="https://api.remove.bg/v1.0/removebg" rel="noopener noreferrer"&gt;https://api.remove.bg/v1.0/removebg&lt;/a&gt;', {&lt;br&gt;
  method: 'POST',&lt;br&gt;
  headers: { 'X-Api-Key': API_KEY },&lt;br&gt;
  body: await request.formData()&lt;br&gt;
});&lt;br&gt;
return response;&lt;br&gt;
}&lt;br&gt;
}&lt;br&gt;
Lesson learned the hard way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Multi-language SEO is just hreflang tags" → it's not&lt;br&gt;
I had hreflang conflicts with canonical tags for weeks and couldn't figure out why my impressions were dropping. Canonicals, sitemaps, and hreflang all need to agree with each other.&lt;br&gt;
html&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
One wrong canonical and Google ignores your entire language setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"More tools = more traffic" → wrong&lt;br&gt;
I built 37 tools thinking volume would win. But 37 unindexed tools is worth less than 5 indexed and ranking ones.&lt;br&gt;
Now I ask before building: is this keyword searchable? Is the difficulty realistic for my domain authority? Does it solve a real problem?&lt;br&gt;
Tools are only as valuable as the traffic they attract.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I'm still figuring out&lt;/p&gt;

&lt;p&gt;Supabase auth across 25 languages&lt;br&gt;
Getting Google to index all 950 pages faster&lt;br&gt;
Whether PDF to PNG is worth the complexity&lt;/p&gt;

&lt;p&gt;Building in public is humbling. But the browser is genuinely more powerful than most tutorials let on — and that's what makes solo projects like this possible.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I launched a free image tool 30 days ago. Here's what Google did next</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Tue, 31 Mar 2026 23:12:49 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-launched-a-free-image-tool-30-days-ago-heres-what-google-did-next-2cab</link>
      <guid>https://dev.to/relahconvert/i-launched-a-free-image-tool-30-days-ago-heres-what-google-did-next-2cab</guid>
      <description>&lt;p&gt;30 days ago I launched &lt;a href="https://relahconvert.com/compress" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt; — a free browser-based image toolkit with 37 tools across 25 languages.&lt;/p&gt;

&lt;p&gt;I thought the hard part was building it. I was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Week Felt Amazing
&lt;/h2&gt;

&lt;p&gt;Impressions climbed daily. Pages were getting indexed fast. &lt;br&gt;
GSC was showing 100-150 impressions per day. I thought I was &lt;br&gt;
onto something.&lt;/p&gt;

&lt;p&gt;Then Google pulled the rug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drop Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;Around day 20, impressions crashed to near zero. Some days &lt;br&gt;
literally 0 or 1 impression.&lt;/p&gt;

&lt;p&gt;Meanwhile GSC kept indexing more pages. 207 indexed. Then &lt;br&gt;
952 Pages being discovered.&lt;/p&gt;

&lt;p&gt;But impressions? Flatline.&lt;/p&gt;

&lt;p&gt;I spent days trying to figure out what I did wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;Running a Semrush audit revealed three real issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;48 hreflang conflicts&lt;/strong&gt; — my language pages had canonical &lt;br&gt;
tags pointing to the English version instead of themselves. &lt;br&gt;
Google was getting contradictory signals on 48 pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;24 incorrect sitemap URLs&lt;/strong&gt; — a trailing slash &lt;br&gt;
inconsistency between my sitemap and canonical tags. &lt;br&gt;
The sitemap said /compress/ but the canonical said /compress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Google was seeing 3 different signals for the &lt;br&gt;
same URL and didn't know which one to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Each language page canonical now points to itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/fr/compress/ canonical → relahconvert.com/fr/compress/&lt;/li&gt;
&lt;li&gt;/ar/compress/ canonical → relahconvert.com/ar/compress/&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the sitemap now consistently matches the canonical &lt;br&gt;
format across all 1,075 URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened After
&lt;/h2&gt;

&lt;p&gt;External backlinks jumped from 2 to 10 in GSC.&lt;br&gt;
Internal links jumped from 531 to 1,770.&lt;br&gt;
FAQ rich results grew from 38 to 57.&lt;/p&gt;

&lt;p&gt;Impressions? Still recovering. I learnt Google needs 2-4 weeks &lt;br&gt;
to recrawl and reassess after canonical changes.&lt;/p&gt;

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

&lt;p&gt;Google doesn't punish you for fixing things. But it &lt;br&gt;
does take time to process changes — especially on a &lt;br&gt;
domain that's only 30 days old.&lt;/p&gt;

&lt;p&gt;The hardest part isn't the technical fixes. It's &lt;br&gt;
watching your numbers flatline while knowing the &lt;br&gt;
fix is in and you just have to wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Am Now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;207 pages indexed&lt;/li&gt;
&lt;li&gt;57 valid FAQ rich results&lt;/li&gt;
&lt;li&gt;10 external backlinks&lt;/li&gt;
&lt;li&gt;1,770 internal links&lt;/li&gt;
&lt;li&gt;Position 76 average (working on it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Day 30. Still building. Still waiting.&lt;/p&gt;

&lt;p&gt;If you're in the same spot — new site, confusing GSC &lt;br&gt;
data, impression drops that make no sense — you're &lt;br&gt;
probably not doing anything wrong. Google just takes &lt;br&gt;
time with new domains.&lt;/p&gt;

&lt;p&gt;Building &lt;a href="https://relahconvert.com/compress" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt; in public&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>beginners</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Got 149 Pages Indexed in 27 Days With a New Domain</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Sat, 28 Mar 2026 23:23:10 +0000</pubDate>
      <link>https://dev.to/relahconvert/how-i-got-149-pages-indexed-in-27-days-with-a-new-domain-17pp</link>
      <guid>https://dev.to/relahconvert/how-i-got-149-pages-indexed-in-27-days-with-a-new-domain-17pp</guid>
      <description>&lt;p&gt;When I launched &lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt; — a free browser-based image toolkit — I had one big SEO concern: would Google even bother indexing a brand new domain with hundreds of pages?&lt;/p&gt;

&lt;p&gt;27 days later, 149 pages are indexed with 816 still pending. &lt;br&gt;
Here's exactly what I did and what I learned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Site Structure
&lt;/h2&gt;

&lt;p&gt;RelahConvert has 37 image tools across 25 languages. That's &lt;br&gt;
theoretically 925 pages total. Getting Google to find and &lt;br&gt;
index all of them quickly required being intentional about &lt;br&gt;
a few things.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Submit a Sitemap Immediately
&lt;/h2&gt;

&lt;p&gt;The first thing I did after launching was submit a sitemap &lt;br&gt;
to Google Search Console. This tells Google exactly which &lt;br&gt;
pages exist and where to find them.&lt;/p&gt;

&lt;p&gt;In Vite, generating a sitemap is straightforward with &lt;br&gt;
vite-plugin-sitemap:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sitemap&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-plugin-sitemap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://relahconvert.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submit it in GSC under Sitemaps and Google will start &lt;br&gt;
crawling within days.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Strong Internal Linking
&lt;/h2&gt;

&lt;p&gt;Every tool page on RelahConvert links to related tools &lt;br&gt;
through the navbar, footer, and a "What's Next" section &lt;br&gt;
after each conversion. This means Googlebot can crawl &lt;br&gt;
from any page to every other page easily.&lt;/p&gt;

&lt;p&gt;GSC confirmed 531 internal links across the site. That's &lt;br&gt;
Googlebot's roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Request Indexing for Priority Pages
&lt;/h2&gt;

&lt;p&gt;For your most important pages don't just wait — request &lt;br&gt;
indexing manually in GSC using the URL Inspection tool.&lt;/p&gt;

&lt;p&gt;Enter the URL → click "Request Indexing" → Google &lt;br&gt;
prioritizes crawling that page.&lt;/p&gt;

&lt;p&gt;I did this for my core tool pages and they showed up in &lt;br&gt;
results within days.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. FAQ Structured Data
&lt;/h2&gt;

&lt;p&gt;Every tool page has FAQ schema markup. Within 27 days &lt;br&gt;
GSC showed 48 valid FAQ rich results with zero errors.&lt;/p&gt;

&lt;p&gt;This doesn't directly cause indexing but it signals to &lt;br&gt;
Google that your pages are well structured and trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Be Patient With Multilingual Pages
&lt;/h2&gt;

&lt;p&gt;With 25 languages, most of my non-English pages are still &lt;br&gt;
in the "Discovered — not yet indexed" queue (964 pages). &lt;br&gt;
Google indexes the primary language first then works &lt;br&gt;
through translations.&lt;/p&gt;

&lt;p&gt;Make sure your hreflang tags are correct so Google &lt;br&gt;
understands the relationship between language versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually Indexed After 27 Days
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;149 pages indexed&lt;/li&gt;
&lt;li&gt;964 discovered but pending&lt;/li&gt;
&lt;li&gt;1,140 impressions in 28 days&lt;/li&gt;
&lt;li&gt;Position 76 average (still early)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The indexing is happening. The rankings take longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Reality
&lt;/h2&gt;

&lt;p&gt;Getting pages indexed quickly is the easy part. Getting &lt;br&gt;
them to rank is the slow part that requires backlinks, &lt;br&gt;
time, and domain authority that a 27 day old site simply &lt;br&gt;
doesn't have yet.&lt;/p&gt;

&lt;p&gt;But you can't rank what isn't indexed. Step one is done.&lt;/p&gt;

&lt;p&gt;37 free image tools at &lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>seo</category>
    </item>
    <item>
      <title>How I Added Dark Mode to a 37-Tool Vite SPA in One Prompt</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Tue, 24 Mar 2026 23:41:18 +0000</pubDate>
      <link>https://dev.to/relahconvert/how-i-added-dark-mode-to-a-37-tool-vite-spa-in-one-prompt-3o88</link>
      <guid>https://dev.to/relahconvert/how-i-added-dark-mode-to-a-37-tool-vite-spa-in-one-prompt-3o88</guid>
      <description>&lt;p&gt;I've been building RelahConvert — a browser-only image converter &lt;br&gt;
with 37 tools — and yesterday I decided to add dark mode.&lt;/p&gt;

&lt;p&gt;I was expecting it to take hours. It took minutes.&lt;/p&gt;

&lt;p&gt;Here's exactly how it works and what I learned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem With Hardcoded Colors
&lt;/h2&gt;

&lt;p&gt;Most projects start with colors scattered everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;#ffffff&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;color&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#1&lt;/span&gt;&lt;span class="nt"&gt;a1a1a&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;border&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;solid&lt;/span&gt; &lt;span class="nf"&gt;#e0e0e0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you have 37 tool pages, finding and replacing every &lt;br&gt;
hardcoded color manually would take forever. There had to be &lt;br&gt;
a better way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: CSS Variables + data-theme
&lt;/h2&gt;

&lt;p&gt;The entire dark mode system comes down to two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Define all colors as CSS variables&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a1a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f5f5f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e0e0e0&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;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"dark"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#18181b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f4f4f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#27272a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3f3f46&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;strong&gt;2. Toggle the attribute on the root element&lt;/strong&gt;&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;toggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;light&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;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remembering the Preference
&lt;/h2&gt;

&lt;p&gt;You also want to respect the user's OS preference on first &lt;br&gt;
visit, then let them override manually:&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="c1"&gt;// On page load&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefersDark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&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;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefersDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Toggle Button
&lt;/h2&gt;

&lt;p&gt;A simple sun/moon icon in the navbar:&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;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;☀️&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌙&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place it top right — that's where users expect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips for Dark Mode That Doesn't Look Weird
&lt;/h2&gt;

&lt;p&gt;A few things I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never use pure black (#000000)&lt;/strong&gt; — use #18181b or similar. 
Pure black feels harsh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never use pure white text&lt;/strong&gt; — use #f4f4f5. Pure white on 
dark backgrounds causes eye strain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give cards depth&lt;/strong&gt; — make cards slightly lighter than the 
background so elements don't flatten out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch out for images&lt;/strong&gt; — if users upload images with white 
backgrounds, they'll float awkwardly on dark pages. Keep 
preview areas neutral.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Result&lt;/p&gt;

&lt;p&gt;38 tool pages, all consistently dark, zero hardcoded color &lt;br&gt;
conflicts. The site went from feeling like a utility tool to &lt;br&gt;
feeling like a proper app.&lt;/p&gt;

&lt;p&gt;If you're building a Vite SPA and haven't added dark mode yet &lt;br&gt;
— it's simpler than you think. One CSS variable system and a &lt;br&gt;
10-line toggle is all it takes.&lt;/p&gt;

&lt;p&gt;Built with Vite. All processing is browser-only — files never &lt;br&gt;
leave the device.&lt;/p&gt;

&lt;p&gt;Check it out at &lt;a href="https://relahconvert.com/" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
      <category>vite</category>
    </item>
    <item>
      <title>I launched a free image tool site 3 weeks ago. Here's what actually happened.</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Mon, 23 Mar 2026 11:55:59 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-launched-a-free-image-tool-site-3-weeks-ago-heres-what-actually-happened-3onk</link>
      <guid>https://dev.to/relahconvert/i-launched-a-free-image-tool-site-3-weeks-ago-heres-what-actually-happened-3onk</guid>
      <description>&lt;p&gt;3 weeks ago I launched relahconvert.com — a free browser-based image converter with 37 tools and 25 languages.&lt;br&gt;
Here's the honest numbers so far:&lt;/p&gt;

&lt;p&gt;1,090 Google impressions&lt;br&gt;
11 real clicks&lt;br&gt;
Position 76 average&lt;br&gt;
2 external backlinks (both from dev.to)&lt;br&gt;
109 pages indexed out of 843&lt;/p&gt;

&lt;p&gt;Not viral. Not impressive. But moving in the right direction.&lt;br&gt;
What I thought would happen:&lt;br&gt;
Launch, get traffic, make money.&lt;br&gt;
What actually happened:&lt;br&gt;
Built 37 tools. Realized no one can find you without backlinks. Realized backlinks require effort. Realized SEO takes months not days.&lt;br&gt;
What I learned:&lt;br&gt;
Building is the easy part. Distribution is the real work.&lt;br&gt;
Everyone talks about what they built. Nobody talks about the 3 weeks after launch when Google doesn't care you exist yet.&lt;br&gt;
I'm documenting the whole journey — the tools, the SEO, the slow grind of getting traffic from zero.&lt;br&gt;
If you're building something right now and wondering why nothing is happening yet — you're not alone.&lt;br&gt;
Try the tools here: &lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;https://relahconvert.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I built a free passport photo maker for 160+ countries — no server uploads</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Sat, 21 Mar 2026 18:59:06 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-built-a-free-passport-photo-maker-for-160-countries-no-server-uploads-1bbf</link>
      <guid>https://dev.to/relahconvert/i-built-a-free-passport-photo-maker-for-160-countries-no-server-uploads-1bbf</guid>
      <description>&lt;p&gt;I just added a passport photo maker to RelahConvert — here's what makes it different.&lt;br&gt;
Most passport photo tools upload your photo to their server, process it, then send it back. That means your face is sitting on some random server somewhere.&lt;br&gt;
Mine works entirely in the browser. The photo never leaves your device.&lt;br&gt;
What it does:&lt;/p&gt;

&lt;p&gt;Supports 160+ countries with correct dimensions&lt;br&gt;
Auto-detects and crops to head and shoulders&lt;br&gt;
Removes background using AI (runs locally in browser)&lt;br&gt;
Downloads single photo or printable 4×6 sheet&lt;br&gt;
Available in 25 languages.&lt;/p&gt;

&lt;p&gt;The hard part was getting the crop right for every photo type — full body, portrait, headshot. Still not perfect but works well for standard portrait photos.&lt;br&gt;
Built with vanilla JS + Canvas API + &lt;a class="mentioned-user" href="https://dev.to/imgly"&gt;@imgly&lt;/a&gt;/background-removal for the AI part.&lt;br&gt;
Try it here: Try it here: &lt;a href="https://relahconvert.com/passport-photo" rel="noopener noreferrer"&gt;https://relahconvert.com/passport-photo&lt;/a&gt;&lt;br&gt;
Would love feedback — especially if the crop doesn't work for your photo type.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a free browser-based image converter — no uploads, files never leave your device</title>
      <dc:creator>Bright Agbomado</dc:creator>
      <pubDate>Sat, 07 Mar 2026 14:15:44 +0000</pubDate>
      <link>https://dev.to/relahconvert/i-built-a-free-browser-based-image-converter-no-uploads-files-never-leave-your-device-21nf</link>
      <guid>https://dev.to/relahconvert/i-built-a-free-browser-based-image-converter-no-uploads-files-never-leave-your-device-21nf</guid>
      <description>&lt;p&gt;I've been frustrated with image tools that upload your files to their &lt;br&gt;
servers. You never really know what happens to your photos after that.&lt;/p&gt;

&lt;p&gt;So I built RelahConvert — a completely free image tool suite that runs &lt;br&gt;
100% in your browser. No uploads, no servers, no accounts required.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Compress images — reduce file size while keeping quality&lt;/li&gt;
&lt;li&gt;Resize images — by pixels or percentage&lt;/li&gt;
&lt;li&gt;Convert between JPG, PNG and WebP formats&lt;/li&gt;
&lt;li&gt;Convert images to PDF&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All 10 tools are free and your files never leave your device. Everything &lt;br&gt;
runs using the browser's built-in Canvas API.&lt;/p&gt;

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

&lt;p&gt;Instead of sending files to a server, I use the HTML5 Canvas element to &lt;br&gt;
process images directly in the browser. The user picks a file, Canvas &lt;br&gt;
reads the pixel data, applies the transformation, and outputs the result &lt;br&gt;
— all locally.&lt;/p&gt;

&lt;p&gt;For the PDF conversion I used a lightweight JS library to bundle images &lt;br&gt;
into a proper PDF structure without any server involvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm adding 30 more tools over the next few weeks including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GIF, BMP and TIFF conversions&lt;/li&gt;
&lt;li&gt;Crop, rotate and flip&lt;/li&gt;
&lt;li&gt;Grayscale, brightness and contrast adjustments&lt;/li&gt;
&lt;li&gt;Watermark tool&lt;/li&gt;
&lt;li&gt;EXIF metadata remover&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://relahconvert.com" rel="noopener noreferrer"&gt;RelahConvert&lt;/a&gt; — completely free, no account needed.&lt;/p&gt;

&lt;p&gt;Would love any feedback from the dev community on the approach or &lt;br&gt;
any tools you'd like to see added.&lt;/p&gt;

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