<?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: Serhii Kalyna</title>
    <description>The latest articles on DEV Community by Serhii Kalyna (@serhii_kalyna_730b636889c).</description>
    <link>https://dev.to/serhii_kalyna_730b636889c</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%2F3820869%2Ff684a191-0ecb-4858-9f64-dd15a4ad9e07.png</url>
      <title>DEV Community: Serhii Kalyna</title>
      <link>https://dev.to/serhii_kalyna_730b636889c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serhii_kalyna_730b636889c"/>
    <language>en</language>
    <item>
      <title>Adding PDF support to a Rust image converter — what I learned about libvips and PDF rendering</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Wed, 22 Apr 2026 15:40:54 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/adding-pdf-support-to-a-rust-image-converter-what-i-learned-about-libvips-and-pdf-rendering-3g9k</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/adding-pdf-support-to-a-rust-image-converter-what-i-learned-about-libvips-and-pdf-rendering-3g9k</guid>
      <description>&lt;p&gt;Been building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; — a free image converter in Rust + Axum + libvips. This week I added PDF to JPG/PNG support. Thought I'd write up what actually happened because it was more interesting than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach
&lt;/h2&gt;

&lt;p&gt;My existing image pipeline is straightforward — &lt;code&gt;VipsImage::new_from_file(path)&lt;/code&gt;, some processing, &lt;code&gt;image_write_to_file(out_path)&lt;/code&gt;. Images just work. I assumed PDFs would be similar.&lt;/p&gt;

&lt;p&gt;First attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;VipsImage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}[dpi={}]"&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;dpi&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&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 actually works. libvips accepts PDF paths with a &lt;code&gt;dpi&lt;/code&gt; parameter and returns a VipsImage. For a single-page PDF it's almost trivially simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-page gets interesting
&lt;/h2&gt;

&lt;p&gt;The problem is multi-page PDFs. You need to render each page separately and either return them as individual files or bundle them into a ZIP. My &lt;code&gt;load_pdf&lt;/code&gt; function probes the file first to get the page count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;VipsImage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&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;let&lt;/span&gt; &lt;span class="n"&gt;quantity_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="nf"&gt;.get_n_pages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then for each page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;VipsImage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"{}[dpi={},page={}]"&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;dpi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_page&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page parameter is zero-indexed. Simple enough. The output goes into individual files which then get zipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ZipWriter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&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_file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;name_pages&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="nf"&gt;.start_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&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;let&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./tmp/{page_file}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remove_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./tmp/{page_file}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// cleanup registry too&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;zip&lt;/span&gt;&lt;span class="nf"&gt;.finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rendering engine question
&lt;/h2&gt;

&lt;p&gt;libvips doesn't have its own PDF renderer — it delegates to either poppler or pdfium depending on how it was compiled. On most Linux distros you get poppler via the &lt;code&gt;libvips-dev&lt;/code&gt; package. On a clean Ubuntu 24 EC2 instance this just worked, but I hit a subtle issue: some PDFs rendered correctly in Acrobat but had slightly different text positioning in my output. Turns out poppler's Splash rasterizer and pdfium's Skia backend produce noticeably different results on complex layouts.&lt;/p&gt;

&lt;p&gt;If you're doing this in production and care about rendering quality, check what backend your libvips was compiled against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vips &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# also check what PDF loader is available:&lt;/span&gt;
vips &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;pdfiumload&lt;/code&gt; is available alongside &lt;code&gt;pdfload&lt;/code&gt;, you can use pdfium explicitly. The quality difference on text-heavy PDFs is visible — Skia's analytical coverage rasterizer produces cleaner edges than Splash.&lt;/p&gt;

&lt;h2&gt;
  
  
  DPI clamping
&lt;/h2&gt;

&lt;p&gt;I added a DPI clamp on the server side — users can request any DPI but I limit it to 72–300:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dpi"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="nf"&gt;.clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&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;The reason: a 600 DPI A4 page produces a ~139 MB raw RGBA buffer. libvips streams this in tiles so it doesn't OOM, but it's still slow and the output file is huge. 300 DPI is the sweet spot for print-quality output — 2480×3508 pixels for A4, ~900 KB as JPG.&lt;br&gt;
The cleanup bug I fixed this week&lt;br&gt;
Completely unrelated to PDF but worth mentioning — noticed in the logs that cleanup_files was reporting it deleted hundreds of files when ./tmp was actually empty.&lt;br&gt;
The bug: I was counting files to delete as to_delete.len() before actually attempting deletion. Page files from multi-page PDFs get removed immediately after zip creation, but their registry entries stuck around. So the cleaner found stale registry entries with no corresponding files on disk and counted them as successfully deleted.&lt;br&gt;
Fix — count only files actually removed from disk, handle NotFound separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remove_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;old_files_deleted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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;e&lt;/span&gt;&lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stale registry entry, skip count&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;eprint!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to delete {file_name}: {e}"&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;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;PDF frontend is now live — DPI selection (72/150/300) and multi-page ZIP download are both shipped. You can try it at &lt;a href="//convertifyapp.net/pdf-to-png"&gt;convertifyapp.net/pdf-to-png&lt;/a&gt; and &lt;a href="//convertifyapp.net/pdf-to-jpg"&gt;convertifyapp.net/pdf-to-jpg&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Still considering whether to expose pdfium explicitly as a build option for better rendering quality, or just document the poppler default behavior. For now poppler works fine for the majority of PDFs people actually upload.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>imageprocessing</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Building in public — week 5</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sat, 18 Apr 2026 13:37:36 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-5-5g84</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-5-5g84</guid>
      <description>&lt;p&gt;Another week, mixed bag.&lt;/p&gt;

&lt;p&gt;The main blocker is still HN karma. Sitting at 4, need 10+ for Show HN. Been leaving technical comments every day — libvips internals, AVIF encoder tradeoffs, Rust FFI stuff — but it just doesn't move. Not sure if it's timing, thread selection, or just how HN works for new accounts. Either way, Show HN moves to week 6.&lt;/p&gt;

&lt;p&gt;What did happen: added Convertify to Wellfound and SourceForge this week. Small wins but both are DR80+ so it adds up over time. Also published a piece on TIFF in 2026 — researched the format properly and honestly it's kind of fascinating how dead it is on the web but still everywhere in print and professional workflows.&lt;/p&gt;

&lt;p&gt;Also started PDF conversion. Using &lt;code&gt;libvips pdfload&lt;/code&gt; instead of calling poppler directly — whole backend is already libvips so adding a separate dependency felt wrong. The tricky part is the probe load to get page count before processing, still working out the multi-page output strategy.&lt;/p&gt;

&lt;p&gt;GSC is moving in the right direction — 52 pages indexed now, was 33 two weeks ago. Impressions are there, clicks still basically zero because everything sits around position 40. Need to crack top 20 on at least a few queries for that to change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 5 vs week 4
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Week 4&lt;/th&gt;
&lt;th&gt;Week 5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Indexed pages&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit karma&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;75+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SO rep&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impressions&lt;/td&gt;
&lt;td&gt;updating Sunday&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>backend</category>
      <category>buildinpublic</category>
      <category>devjournal</category>
      <category>startup</category>
    </item>
    <item>
      <title>TIFF in 2026: what I learned researching the format nobody uses on the web</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Tue, 14 Apr 2026 15:18:18 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/tiff-in-2026-what-i-learned-researching-the-format-nobody-uses-on-the-web-50ln</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/tiff-in-2026-what-i-learned-researching-the-format-nobody-uses-on-the-web-50ln</guid>
      <description>&lt;p&gt;I'm building a free image converter. One day I looked at my landing page for &lt;code&gt;/tiff-to-webp&lt;/code&gt; and realized I had 4 sections of generic content — "TIFF is a lossless format, WebP is smaller, click convert." The kind of content that exists on 500 other sites.&lt;/p&gt;

&lt;p&gt;So I spent a few hours actually researching TIFF. Here's what surprised me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The file size math nobody explains
&lt;/h2&gt;

&lt;p&gt;A 24-megapixel photo at 16-bit per channel is &lt;strong&gt;144 MB&lt;/strong&gt;. Not because TIFF is inefficient — because it stores every pixel at full depth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Width × Height × Channels × Bytes per channel
6000 × 4000 × 3 × 2 = 144 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 45MP Canon R5 TIFF hits ~260 MB. That's not a bug. That's the point. TIFF was designed for editing, not delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nine compression types — most people know two
&lt;/h2&gt;

&lt;p&gt;Everyone says "TIFF is lossless." That's incomplete. TIFF supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uncompressed&lt;/strong&gt; — raw pixels, maximum compatibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZW&lt;/strong&gt; — 1.5–2× compression, most common&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZIP/Deflate&lt;/strong&gt; — slightly better than LZW for 16-bit images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG-in-TIFF&lt;/strong&gt; — lossy, 10–20× compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CCITT Group 4&lt;/strong&gt; — 15–20× compression, black-and-white only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is interesting. CCITT Group 4 is a lossless codec designed for bilevel (1-bit) images — fax machines used it for 30 years. A scanned business document compressed with Group 4 goes from 5 MB to ~250 KB. That's why multi-page TIFF became the standard for document archiving, legal scanning, and insurance workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero browser support is by design
&lt;/h2&gt;

&lt;p&gt;Chrome, Firefox, Edge — none of them render TIFF inline. Not a bug, not an oversight. TIFF was never meant for web delivery.&lt;/p&gt;

&lt;p&gt;The Library of Congress lists TIFF as their &lt;strong&gt;preferred format&lt;/strong&gt; for permanent digital preservation. They hold over 3.5 petabytes of TIFF files. The specification hasn't changed since &lt;strong&gt;June 3, 1992&lt;/strong&gt; — TIFF Revision 6.0. Thirty-plus years of stability is a feature when you're archiving cultural heritage.&lt;/p&gt;




&lt;h2&gt;
  
  
  The industries that actually use TIFF
&lt;/h2&gt;

&lt;p&gt;When I dug deeper, TIFF turns out to be everywhere in specialized fields:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Professional photography&lt;/strong&gt; — Lightroom exports 16-bit TIFF for Photoshop roundtrips. 65,536 tonal values per channel vs JPEG's 256.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medical imaging&lt;/strong&gt; — whole-slide pathology scanners produce BigTIFF files (64-bit offsets, no 4 GB limit). A tissue sample at 0.25 µm/pixel = ~80,000 × 60,000 pixels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GIS / satellite imagery&lt;/strong&gt; — GeoTIFF embeds coordinate systems and projections directly in the file. NASA Earthdata recommends Cloud Optimized GeoTIFF for all their data distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document scanning&lt;/strong&gt; — multi-page TIFF with Group 4 compression is the standard for batch document processing. Every enterprise scanner defaults to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Archival&lt;/strong&gt; — FADGI (Federal Agencies Digital Guidelines Initiative) requires TIFF for all government digitization projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  The conversion numbers
&lt;/h2&gt;

&lt;p&gt;Once I understood what TIFF actually is, the conversion ratios made sense:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Reduction&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;~96%&lt;/td&gt;
&lt;td&gt;36 MB → 1.6 MB at quality 80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPG&lt;/td&gt;
&lt;td&gt;~91%&lt;/td&gt;
&lt;td&gt;82 MB → 7.6 MB at quality 85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;~40–60%&lt;/td&gt;
&lt;td&gt;Lossless, same quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC&lt;/td&gt;
&lt;td&gt;~95–98%&lt;/td&gt;
&lt;td&gt;96 MB → 2 MB, Apple only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PNG being "only" 40–60% smaller makes sense now — both are lossless, PNG just has better compression. HEIC being 98% smaller makes sense too — HEVC is an extremely efficient codec.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this did for my content
&lt;/h2&gt;

&lt;p&gt;Before research, my &lt;code&gt;/tiff-to-webp&lt;/code&gt; page said: "TIFF is big, WebP is small, convert here."&lt;/p&gt;

&lt;p&gt;After research, I could write about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why photographers export 16-bit TIFF from Lightroom before Photoshop retouching&lt;/li&gt;
&lt;li&gt;Why NASA uses GeoTIFF for satellite imagery distribution&lt;/li&gt;
&lt;li&gt;Why CCITT Group 4 still powers document scanning workflows&lt;/li&gt;
&lt;li&gt;The difference between LZW and ZIP compression for 16-bit images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same converter. Same tool. Completely different content that's actually useful to someone searching the topic.&lt;/p&gt;

&lt;p&gt;That's the SEO approach I'm taking with &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; — research every format deeply before writing a word. It takes longer. But generic content is invisible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Week 5 of building in public. Previous posts in the series on my profile.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>imageprocessing</category>
      <category>seo</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Building in public week 4: first organic clicks and what drove them</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sat, 11 Apr 2026 19:42:18 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-4-first-organic-clicks-and-what-drove-them-2dfg</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-4-first-organic-clicks-and-what-drove-them-2dfg</guid>
      <description>&lt;p&gt;Week 4 of building my side project in public. Still early stage, but this week something shifted — first real organic clicks from Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I worked on this week
&lt;/h2&gt;

&lt;p&gt;Most of the week went into SEO fundamentals I'd been putting off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added Organization and WebSite schema to the whole site&lt;/li&gt;
&lt;li&gt;Created Privacy Policy and Contact pages (E-E-A-T signals)&lt;/li&gt;
&lt;li&gt;Fixed internal link ordering — replaced alphabetical with semantic scoring based on topical relevance&lt;/li&gt;
&lt;li&gt;Expanded thin content pages with proper sections&lt;/li&gt;
&lt;li&gt;Fixed HTTP→HTTPS redirect via Cloudflare&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is glamorous. But it's the kind of work that compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually drove the clicks
&lt;/h2&gt;

&lt;p&gt;Honest answer: I don't know yet. Too early to attribute. GSC data lags by a few days and the sample size is too small to draw conclusions.&lt;/p&gt;

&lt;p&gt;What I can say is that impressions are trending up week over week, and the pages getting impressions are the ones where I added real content — not the thin placeholder pages.&lt;/p&gt;

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

&lt;p&gt;More content on thin pages, waiting for Google to reindex the schema and E-E-A-T changes from this week, and continuing to build community presence slowly.&lt;/p&gt;

&lt;p&gt;Building in public means sharing the slow weeks too. This was one of them — but the trend is pointing in the right direction.&lt;/p&gt;




&lt;p&gt;Previous updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/from-0-to-30-indexed-pages-in-3-weeks-what-actually-moved-the-needle-of2"&gt;From 0 to 30 indexed pages in 3 weeks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/why-i-added-internal-linking-to-my-image-converter-and-what-changed-in-google-19jn"&gt;Why I added internal linking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/how-i-fixed-google-ignoring-160-pages-migrating-from-vite-spa-to-nextjs-ssg-1mfh"&gt;How I fixed Google ignoring 160 pages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Best Free Online Image Converters in 2026 — WebP, AVIF, HEIC and more</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Tue, 07 Apr 2026 15:39:18 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/best-free-online-image-converters-in-2026-webp-avif-heic-and-more-29f2</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/best-free-online-image-converters-in-2026-webp-avif-heic-and-more-29f2</guid>
      <description>&lt;p&gt;If you work with web images, you're converting between formats constantly. Here are 6 free tools worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Convertify
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; supports WebP, HEIC, AVIF, PNG, JPG, TIFF and 20+ other formats. Built with Rust + libvips. Batch conversion up to 10 files, no signup, no watermarks, files deleted within 6 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fast, no account required, 20+ formats, quality control slider, batch conversion&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; 20MB per file limit, no editing features&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Squoosh
&lt;/h2&gt;

&lt;p&gt;Google's open-source tool. Runs entirely in the browser via WebAssembly. Excellent for fine-tuning compression — side-by-side preview and full control over encoding parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; In-browser, open source, excellent compression control, supports AVIF&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Single file only, no batch conversion&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cloudinary
&lt;/h2&gt;

&lt;p&gt;The industry standard for production image pipelines. Free tier gives you 25GB storage and on-the-fly format conversion via URL parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; API access, CDN delivery, automatic format negotiation&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Requires account, overkill for simple conversions&lt;/p&gt;

&lt;h2&gt;
  
  
  4. ILoveIMG
&lt;/h2&gt;

&lt;p&gt;Simple interface, handles batch WebP conversion well. Supports resize, compress, and crop alongside conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Batch conversion, extra editing tools, no signup needed for basic use&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Ads on the page, slower than dedicated converters&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Convertio
&lt;/h2&gt;

&lt;p&gt;Handles 300+ formats including WebP. Good for occasional use when you need a format the other tools don't support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Huge format support, cloud storage integration&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Free tier limited to 100MB, requires account for batch&lt;/p&gt;

&lt;h2&gt;
  
  
  6. EZGIF
&lt;/h2&gt;

&lt;p&gt;Old-school UI but reliable. Good for WebP to GIF and animated WebP specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Free, handles animated WebP, no account&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Outdated interface, file size limits, ads&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;For most web developers: Squoosh for single files where you want compression control, Convertify for batch conversion across multiple formats without friction.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>images</category>
      <category>tooling</category>
      <category>webperf</category>
    </item>
    <item>
      <title>How I built a semantic scoring algorithm for internal links across &gt;200 pages</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Tue, 07 Apr 2026 11:01:50 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/how-i-built-a-semantic-scoring-algorithm-for-internal-links-across-200-pages-50n4</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/how-i-built-a-semantic-scoring-algorithm-for-internal-links-across-200-pages-50n4</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/serhii_kalyna_730b636889c/why-i-added-internal-linking-to-my-image-converter-and-what-changed-in-google-19jn"&gt;why I added internal linking to my image converter&lt;/a&gt;. This is the technical follow-up: how I replaced alphabetical ordering with a semantic scoring algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with alphabetical ordering
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;RelatedConversions&lt;/code&gt; component was showing links in alphabetical order. For &lt;code&gt;/heic-to-jpg&lt;/code&gt;, it would show &lt;code&gt;avif-to-heic&lt;/code&gt;, &lt;code&gt;avif-to-jpg&lt;/code&gt;, &lt;code&gt;bmp-to-heic&lt;/code&gt; — technically related, but not semantically prioritized.&lt;/p&gt;

&lt;p&gt;What I wanted: show conversions that share the most format overlap first, then factor in keyword similarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring model: 70/30
&lt;/h2&gt;

&lt;p&gt;A simple weighted formula:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;format overlap&lt;/strong&gt; — how many formats the two conversions share (weight: 0.7)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;keyword similarity&lt;/strong&gt; — do the slugs share meaningful terms (weight: 0.3)
&lt;/li&gt;
&lt;/ul&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;scoreRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candidate&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fromA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toA&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-to-&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fromB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toB&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-to-&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;formatsA&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;fromA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toA&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;formatsB&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;fromB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toB&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;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;formatsA&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;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;formatsB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&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;formatScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formatsA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatsB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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;wordsA&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wordsB&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharedWords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;wordsA&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;w&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;wordsB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&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;keywordScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sharedWords&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wordsA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wordsB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&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="nx"&gt;formatScore&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keywordScore&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;h2&gt;
  
  
  How it works in practice
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;/heic-to-jpg&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jpg-to-heic&lt;/code&gt; → score 1.0 (both formats shared)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;heic-to-png&lt;/code&gt; → score 0.7 (one shared format: heic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webp-to-jpg&lt;/code&gt; → score 0.7 (one shared format: jpg)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;avif-to-png&lt;/code&gt; → score 0.0 (no shared formats)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The component
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RelatedConversions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-to-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SLUGS&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;slug&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scoreRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Related&lt;/span&gt; &lt;span class="nx"&gt;Conversions&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h2&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&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;href&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;slug&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="o"&gt;&amp;gt;&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-to-&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; to &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/a&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/section&lt;/span&gt;&lt;span class="err"&gt;&amp;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;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Planning to weight by GSC impression data — pages that already rank should get higher weight in related links. That closes the feedback loop: organic traffic signals inform which related conversions to surface.&lt;/p&gt;

&lt;p&gt;Building in public at &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>seo</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Is HEIC Patent Risk Real for Small Web Services?</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:51:35 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/is-heic-patent-risk-real-for-small-web-services-3g6g</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/is-heic-patent-risk-real-for-small-web-services-3g6g</guid>
      <description>&lt;p&gt;Apple uses HEIC as the default iPhone photo format — which means almost every image your users upload from iOS is potentially HEIC. But HEIC is built on HEVC, which is encumbered by a messy patent pool (MPEG LA + HEVC Advance).&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;If you're running a small, free image converter that processes HEIC files server-side, are you actually exposed to legal risk?&lt;/p&gt;

&lt;p&gt;Here's what I found after researching this for &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patent holders typically target &lt;strong&gt;device manufacturers and encoders&lt;/strong&gt;, not web services&lt;/li&gt;
&lt;li&gt;There's no known case of a small web service being sued for HEIC processing&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;libheif&lt;/strong&gt; (open source) doesn't grant a patent license, but practically, enforcement at this level hasn't happened&lt;/li&gt;
&lt;li&gt;The real risk threshold seems to be commercial scale — think millions of users or hardware bundling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My conclusion
&lt;/h2&gt;

&lt;p&gt;Theoretical risk exists, but practical risk for indie/small services is low.&lt;/p&gt;

&lt;p&gt;Has anyone dealt with this, consulted a lawyer, or found solid resources on this topic? Would love to hear from others who've thought it through.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>discuss</category>
      <category>img</category>
    </item>
    <item>
      <title>From 0 to 30 indexed pages in 3 weeks — what actually moved the needle</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sat, 04 Apr 2026 10:45:14 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/from-0-to-30-indexed-pages-in-3-weeks-what-actually-moved-the-needle-of2</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/from-0-to-30-indexed-pages-in-3-weeks-what-actually-moved-the-needle-of2</guid>
      <description>&lt;h1&gt;
  
  
  From 0 to 30 indexed pages in 3 weeks — what actually moved the needle
&lt;/h1&gt;

&lt;p&gt;I'm building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; — a free image converter that supports 20+ formats with no signup, no limits, and no file tracking. Solo project, built with Rust + libvips on the backend and Next.js on the frontend.&lt;/p&gt;

&lt;p&gt;Three weeks ago Google had indexed exactly 0 pages. Here's what I changed and what actually worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point: a Vite SPA that Google couldn't read
&lt;/h2&gt;

&lt;p&gt;The original version was a classic React SPA bundled with Vite. Fast to build, great DX — and completely invisible to Google. Googlebot would hit the page, get an empty HTML shell, and move on. No content to index.&lt;/p&gt;

&lt;p&gt;The fix was straightforward in theory: migrate to Next.js App Router with SSG. In practice it took a few days, but the result was 186 fully rendered static pages generated at build time — one for every format combination the tool supports.&lt;/p&gt;

&lt;p&gt;That alone got me from 0 to around 16 indexed pages in the first two weeks. But then growth stalled.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually moved the needle in week 3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Schema markup on every page
&lt;/h3&gt;

&lt;p&gt;I added FAQPage + HowTo JSON-LD schema to every converter page. Not just one or two — all 186.&lt;/p&gt;

&lt;p&gt;The FAQPage schema answers the questions people actually ask: "What is HEIC?", "Will my photos lose quality?", "How long does conversion take?". The HowTo schema describes the conversion steps in structured format Google can parse.&lt;/p&gt;

&lt;p&gt;Result: impressions jumped, and GSC started showing the pages in rich result eligibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Content expansion from ~500 to 5000–7000 characters
&lt;/h3&gt;

&lt;p&gt;This was the most time-consuming part. Each landing page went from a thin paragraph to a full article: format comparison tables, browser support matrices, compression benchmarks, use case explanations, and a 6–8 question FAQ.&lt;/p&gt;

&lt;p&gt;Thin content is invisible. Google doesn't rank pages that don't say anything useful. The pages that got indexed first were the ones with the most content.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Internal linking cluster (RelatedConversions component)
&lt;/h3&gt;

&lt;p&gt;I built a &lt;code&gt;RelatedConversions&lt;/code&gt; component that appears on every converter page and links to 8–10 related conversions. For example, &lt;code&gt;/heic-to-jpg&lt;/code&gt; links to &lt;code&gt;/heic-to-png&lt;/code&gt;, &lt;code&gt;/jpg-to-webp&lt;/code&gt;, &lt;code&gt;/png-to-jpg&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;This does two things: it passes PageRank between pages in the same cluster, and it gives Googlebot a clear crawl path through the entire site. Before this, pages were islands. After this, they're a connected graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. About page with E-E-A-T signals
&lt;/h3&gt;

&lt;p&gt;Google's helpful content guidelines care about who is writing the content. I added an About page with Person schema (name, job title, links to dev.to articles) and WebSite schema with a sitelinks search box.&lt;/p&gt;

&lt;p&gt;It's not a magic ranking factor, but it signals to Google that there's a real person behind the site — not a content farm.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results after 3 weeks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Week 0&lt;/th&gt;
&lt;th&gt;Week 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Indexed pages&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSC impressions (7d)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique search queries&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageSpeed (mobile)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100/100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;30 indexed out of 186 total means Google is still crawling. The indexing curve is moving in the right direction — pages that got schema + expanded content are getting indexed first.&lt;/p&gt;

&lt;p&gt;Positions are averaging around 55, which means page 5–6. Clicks come when you hit the top 10. That's the next phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  What didn't work (or not yet)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backlinks&lt;/strong&gt; — I have ~47 linking domains in Ahrefs, but most are toxic spam (pharma sites, fake testimonial networks). Real quality backlinks: about 3–5. This is the biggest gap right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reddit&lt;/strong&gt; — posted in r/webdev and r/juststart, both got auto-removed. New accounts trigger spam filters regardless of content quality. Building karma through comments first before trying posts again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impressions without clicks&lt;/strong&gt; — 160 impressions at position 55 means the pages are being seen but aren't competitive yet. More content depth and backlinks should push positions into the top 20, where CTR starts to matter.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Push &lt;code&gt;/heic-to-jpg&lt;/code&gt; and &lt;code&gt;/avif-to-jpg&lt;/code&gt; to 1000+ words with full schema (these are the highest-volume queries)&lt;/li&gt;
&lt;li&gt;Get HN karma to 10+ and do a Show HN post&lt;/li&gt;
&lt;li&gt;Outreach to "best free image converters 2026" listicle authors for genuine backlinks&lt;/li&gt;
&lt;li&gt;Track whether internal linking cluster improves crawl coverage in GSC&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Rust + Axum + libvips (image processing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 16 App Router, SSG&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL (landing page content)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; VPS + Caddy + PM2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site: &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt; — no signup, no limits, 20+ formats.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>AVIF in 2026: why it's the best format for web images</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Thu, 02 Apr 2026 16:30:31 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/avif-in-2026-why-its-the-best-format-for-web-images-epj</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/avif-in-2026-why-its-the-best-format-for-web-images-epj</guid>
      <description>&lt;p&gt;AVIF has been available since 2020, but it's only in 2026 that it became the obvious default choice for web images. Browser support crossed 94.9%, Google officially recommends it in PageSpeed Insights, and the compression advantages over JPG are too large to ignore. Here's what you need to know.&lt;/p&gt;

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

&lt;p&gt;AVIF (AV1 Image File Format) is an image format developed by the Alliance for Open Media — a consortium that includes Google, Apple, Mozilla, Microsoft, and Netflix. It uses the AV1 video codec to compress images, which is why it achieves compression rates that traditional image codecs like JPG can't match.&lt;/p&gt;

&lt;p&gt;AV1 was designed for video — it's optimized for the kind of complex, natural content that appears in photos and real-world scenes. When you use it for a single image frame instead of a video stream, you get dramatically better compression than JPG's DCT-based algorithm, which dates back to 1992.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compression numbers
&lt;/h2&gt;

&lt;p&gt;At equivalent visual quality (measured by SSIM/DSSIM), AVIF consistently beats JPG by 40–60%:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;File size vs JPG&lt;/th&gt;
&lt;th&gt;Browser support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPG&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;All browsers &amp;amp; apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;~30% smaller&lt;/td&gt;
&lt;td&gt;95.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;~50% smaller&lt;/td&gt;
&lt;td&gt;94.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG XL&lt;/td&gt;
&lt;td&gt;~45% smaller&lt;/td&gt;
&lt;td&gt;14.7% (Safari only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A real example from my own testing with libvips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JPEG (q80): 540 KB&lt;/li&gt;
&lt;li&gt;WebP: 350 KB (−35%)&lt;/li&gt;
&lt;li&gt;AVIF: 210 KB (−61%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not a cherry-picked example — Netflix's internal benchmarks across a diverse image dataset showed AVIF achieving ~50% better compression than JPG on average.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AVIF wins over WebP in 2026
&lt;/h2&gt;

&lt;p&gt;WebP was Google's previous recommendation and it's still a solid format. But AVIF has caught up in browser support (94.9% vs 95.67%) while delivering meaningfully better compression.&lt;/p&gt;

&lt;p&gt;The gap matters at scale. If you have 1,000 product images averaging 400 KB as JPG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WebP: ~270 KB each → 270 MB total&lt;/li&gt;
&lt;li&gt;AVIF: ~200 KB each → 200 MB total&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 70 MB less bandwidth per 1,000 images served. For high-traffic sites, this compounds into significant CDN cost savings and faster page loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Web Vitals impact
&lt;/h2&gt;

&lt;p&gt;Largest Contentful Paint (LCP) measures how quickly the main image on a page loads. It's a Google ranking signal and one of the Core Web Vitals metrics.&lt;/p&gt;

&lt;p&gt;Large images are the most common cause of poor LCP scores. Switching from JPG to AVIF typically reduces image payload by 40–60%, directly improving LCP. Google's PageSpeed Insights will flag JPG images as a performance opportunity and explicitly recommend AVIF as the preferred replacement.&lt;/p&gt;

&lt;p&gt;For e-commerce sites where product images are the LCP element, converting to AVIF is the highest-impact single change for page performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser support in 2026
&lt;/h2&gt;

&lt;p&gt;AVIF achieved Baseline status in 2024:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome 85+ (2020)&lt;/li&gt;
&lt;li&gt;Firefox 93+ (2021)&lt;/li&gt;
&lt;li&gt;Safari 16+ (2022)&lt;/li&gt;
&lt;li&gt;Edge 121+ (2024)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ~5% without support is primarily legacy mobile browsers and Internet Explorer. For most sites, you can serve AVIF as the primary format with a JPG fallback using the HTML &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"image.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"image.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"image.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browsers download only the first format they support.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encoding speed problem
&lt;/h2&gt;

&lt;p&gt;AVIF's biggest practical limitation is slow encoding. While JPG encodes in ~2ms and WebP in ~168ms, AVIF takes 1–2 seconds per image with the libaom reference encoder.&lt;/p&gt;

&lt;p&gt;This makes real-time AVIF encoding impractical for user-uploaded content without a job queue. For static assets (product photos, blog images, landing page assets), encode offline during your build process and serve the pre-encoded AVIF files.&lt;/p&gt;

&lt;p&gt;Alternative encoders help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rav1e&lt;/strong&gt; — faster than libaom, slightly larger files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVT-AV1&lt;/strong&gt; — good balance of speed and compression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my Rust + libvips backend for &lt;a href="https://convertifyapp.net/avif-to-jpg" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt;, I use libvips's built-in AVIF support which wraps libaom. For batch conversion of user files, the encoding time is acceptable. For real-time conversion of large files, it's a constraint worth planning around.&lt;/p&gt;

&lt;h2&gt;
  
  
  AVIF transparency and animation
&lt;/h2&gt;

&lt;p&gt;Unlike JPG, AVIF supports full alpha channel transparency. This makes it a potential replacement for PNG in web contexts — smaller files with the same transparency support.&lt;/p&gt;

&lt;p&gt;AVIF also supports animation, making it an alternative to GIF. Animated AVIF files are significantly smaller than GIF while supporting a full color range (GIF is limited to 256 colors).&lt;/p&gt;

&lt;h2&gt;
  
  
  What about JPEG XL?
&lt;/h2&gt;

&lt;p&gt;JPEG XL (JXL) is technically superior to AVIF in some dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better compression at high quality settings (~20% smaller than AVIF)&lt;/li&gt;
&lt;li&gt;Progressive decoding (shows usable image with 1% of data loaded)&lt;/li&gt;
&lt;li&gt;Lossless JPEG transcoding (20% smaller, byte-perfect reconstruction)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem is browser support: 14.7% as of early 2026, limited to Safari 17+.&lt;/p&gt;

&lt;p&gt;The big news: Chrome Canary 145 shipped a Rust-based JXL decoder in late 2025, reversing Google's 2022 decision to remove JXL support. Realistic stable Chrome support: H2 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recommendation:&lt;/strong&gt; Use AVIF now. Watch JXL for H2 2026 when Chrome stable support arrives. Don't wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For static sites:&lt;/strong&gt; Convert during build. Store both AVIF and JPG. Use &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; for fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For CDNs:&lt;/strong&gt; Cloudflare, Cloudinary, and Imgix serve AVIF automatically via Accept header negotiation. Upload once, let the CDN handle format selection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Next.js:&lt;/strong&gt; The &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component serves AVIF automatically for supported browsers since Next.js 13.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For WordPress:&lt;/strong&gt; ShortPixel, Imagify, and WebP Express all support AVIF conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For manual conversion:&lt;/strong&gt; Tools like &lt;a href="https://convertifyapp.net/avif-to-jpg" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; handle batch AVIF conversion without requiring local tooling.&lt;/p&gt;

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

&lt;p&gt;AVIF is the right default for web images in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 94.9% browser support — effectively universal with JPG fallback&lt;/li&gt;
&lt;li&gt;✅ ~50% smaller than JPG at the same quality&lt;/li&gt;
&lt;li&gt;✅ Recommended by Google PageSpeed Insights&lt;/li&gt;
&lt;li&gt;✅ Transparency and animation support&lt;/li&gt;
&lt;li&gt;⚠️ Slow encoding — plan around it for real-time use cases&lt;/li&gt;
&lt;li&gt;⚠️ Limited app support outside browsers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is straightforward. The performance gains are significant. There's no good reason to keep shipping JPG for web images in 2026.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I added internal linking to my image converter (and what changed in Google)</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Tue, 31 Mar 2026 17:11:48 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/why-i-added-internal-linking-to-my-image-converter-and-what-changed-in-google-19jn</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/why-i-added-internal-linking-to-my-image-converter-and-what-changed-in-google-19jn</guid>
      <description>&lt;p&gt;I've been building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; — a free image converter powered by Rust + libvips. Two weeks in, Google had indexed 18 of my 186 static pages. The numbers were moving, but slowly.&lt;br&gt;
Then I read a recommendation that changed my approach: internal linking is 80% of cluster SEO.&lt;/p&gt;
&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;My converter pages existed in isolation. /heic-to-jpg had no idea /heic-to-png existed. Google couldn't see a cluster — just a collection of unrelated tools.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I built
&lt;/h3&gt;

&lt;p&gt;A RelatedConversions component that generates contextual links on every page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RELATED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&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="na"&gt;heic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jpg&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="s2"&gt;png&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="s2"&gt;webp&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="s2"&gt;avif&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;avif&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jpg&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="s2"&gt;png&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="s2"&gt;webp&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="s2"&gt;heic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RelatedConversions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// generates links like "HEIC to PNG", "WEBP to JPG" etc.&lt;/span&gt;
  &lt;span class="c1"&gt;// deduplicates, shows related formats from both source and target&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the homepage — "Popular Conversions". On converter pages — "Related Conversions". Every page now links to 6-8 related pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  The immediate result
&lt;/h3&gt;

&lt;p&gt;Deployed on day 15. The same day I submitted the key pages for re-indexing in Search Console.&lt;br&gt;
Within 24 hours Google crawled the updated pages. The internal links give Googlebot a clear path through the entire site — it no longer needs to discover pages through the sitemap alone.&lt;/p&gt;

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

&lt;p&gt;Still too early to see ranking changes — SEO takes weeks. But the site now looks like a cluster to Google, not a collection of isolated tools. That's the foundation for ranking multiple pages simultaneously for the same search query.&lt;/p&gt;

&lt;h4&gt;
  
  
  Stats so far (week 3, day 2):
&lt;/h4&gt;

&lt;p&gt;18 pages indexed (up from 2 three weeks ago)&lt;/p&gt;

&lt;p&gt;Building in public. Follow along if you're curious how Rust + Next.js SSG performs for SEO.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building Convertify in public. Previous posts:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/how-i-fixed-google-ignoring-160-pages-migrating-from-vite-spa-to-nextjs-ssg-1mfh"&gt;How I fixed Google ignoring 160 pages: migrating from Vite SPA to Next.js SSG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/avif-converter-why-i-built-one-and-what-i-learned-la"&gt;AVIF converter: why I built one and what I learned&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/serhii_kalyna_730b636889c/jpeg-xl-in-chrome-145-what-it-means-for-web-developers-1n1m"&gt;JPEG XL in Chrome 145: what it means for web developers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>webdev</category>
      <category>rust</category>
    </item>
    <item>
      <title>AVIF converter: why I built one and what I learned</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Mon, 23 Mar 2026 16:04:29 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/avif-converter-why-i-built-one-and-what-i-learned-la</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/avif-converter-why-i-built-one-and-what-i-learned-la</guid>
      <description>&lt;p&gt;Large images slow down websites, frustrate users, and hurt SEO. Modern formats like AVIF promise smaller sizes with better quality, but the ecosystem is still young. I decided to explore AVIF and build my own converter to see what works in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AVIF
&lt;/h2&gt;

&lt;p&gt;AVIF images can be 50–70% smaller than JPEG or PNG at similar or better visual quality. This means faster loading times, lower bandwidth, and improved PageSpeed scores. Yet, support for AVIF in editing tools and converters is still limited, which makes it hard for developers and content creators.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built my own converter
&lt;/h2&gt;

&lt;p&gt;Existing tools are either slow, require file uploads (privacy concerns), or are paid. I wanted a converter that is fast, privacy-first, and free. That led me to create &lt;strong&gt;Convertify&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; for performance-critical processing
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;libvips&lt;/strong&gt; as a high-speed image processing backend
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js SSG&lt;/strong&gt; for static landing pages and SEO
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AVIF encoding is slower than JPEG/PNG
&lt;/li&gt;
&lt;li&gt;Browser compatibility is still patchy
&lt;/li&gt;
&lt;li&gt;UX: making drag &amp;amp; drop intuitive without blocking the main thread
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Conversion speed improved significantly with Rust + libvips
&lt;/li&gt;
&lt;li&gt;PageSpeed scores for test pages increased
&lt;/li&gt;
&lt;li&gt;Early users appreciated privacy-first, instant conversion
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AVIF adoption will grow as tools improve. Building your own converter is a great way to learn about modern image formats and get early traction. Check out a quick AVIF-to-JPG demo &lt;a href="https://convertifyapp.net/avif-to-jpg" rel="noopener noreferrer"&gt;Convertifyapp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I fixed Google ignoring 160 pages: migrating from Vite SPA to Next.js SSG</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sat, 21 Mar 2026 17:59:51 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/how-i-fixed-google-ignoring-160-pages-migrating-from-vite-spa-to-nextjs-ssg-1mfh</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/how-i-fixed-google-ignoring-160-pages-migrating-from-vite-spa-to-nextjs-ssg-1mfh</guid>
      <description>&lt;h1&gt;
  
  
  How I fixed Google ignoring 160 pages: migrating from Vite SPA to Next.js SSG
&lt;/h1&gt;

&lt;p&gt;I've been building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; — a free image converter &lt;br&gt;
that supports HEIC, WebP, AVIF, PNG, JPG and 20+ formats.&lt;/p&gt;

&lt;p&gt;The problem: after weeks of work, Google had indexed exactly &lt;strong&gt;2 pages&lt;/strong&gt; out of 160+.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Google ignored my pages
&lt;/h2&gt;

&lt;p&gt;My app was a Vite React SPA. Every route looked like this in HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Convertify&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/assets/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google crawls without executing JavaScript. So every single page — &lt;br&gt;
/heic-to-jpg, /webp-to-png, /avif-to-jpg — returned identical empty HTML. &lt;br&gt;
Google saw them all as duplicates and indexed only the homepage.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: Next.js App Router + generateStaticParams
&lt;/h2&gt;

&lt;p&gt;I migrated to Next.js and used &lt;code&gt;generateStaticParams()&lt;/code&gt; to pre-render &lt;br&gt;
all format pairs at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&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;formats&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;heic&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;jpg&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;jpeg&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;png&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;webp&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;avif&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;gif&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;bmp&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;tiff&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;params&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="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;formats&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;to&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&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="na"&gt;slug&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="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-to-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&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="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="nx"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;186 static HTML pages&lt;/strong&gt; generated at build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server components for SEO content
&lt;/h2&gt;

&lt;p&gt;The key insight: not everything needs to be a client component.&lt;/p&gt;

&lt;p&gt;I split the page into two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client (interactive):&lt;/strong&gt; DropZone, Settings, ConvertButton, FilesViewer&lt;br&gt;
&lt;strong&gt;Server (static HTML):&lt;/strong&gt; FAQ, HowToConvert, SupportedFormats, Header, Footer&lt;/p&gt;

&lt;p&gt;For server components I replaced MUI Accordion with native HTML &lt;code&gt;&amp;lt;details&amp;gt;/&amp;lt;summary&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No 'use client', no JavaScript, pure HTML&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FAQ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;to&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dl&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;faqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;faq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;summary&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;faq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;question&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;summary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dd&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;faq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&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;dd&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Google gets full FAQ content without running any JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic metadata per page
&lt;/h2&gt;

&lt;p&gt;Instead of react-helmet-async, Next.js handles metadata server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&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="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-to-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Convert &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; Online Free — Convertify`&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="s2"&gt;`Free online &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; converter.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://convertifyapp.net/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&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;Every page now has unique title, description and canonical tag in the HTML &lt;br&gt;
— no JavaScript required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Before: 2 indexed pages&lt;/li&gt;
&lt;li&gt;After: 186 static pages pre-rendered&lt;/li&gt;
&lt;li&gt;PageSpeed Desktop: 100/100 Performance, 100/100 SEO&lt;/li&gt;
&lt;li&gt;PageSpeed Mobile: 83/100 Performance, 100/100 SEO&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mobile performance hit comes from MUI (emotion) on the client side. &lt;br&gt;
Working on reducing that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SPAs are SEO poison&lt;/strong&gt; for content pages. Use SSG or SSR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split server/client components&lt;/strong&gt; — only interactive parts need JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native HTML elements&lt;/strong&gt; (&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;) work great for accordions 
and are fully server-renderable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;generateStaticParams&lt;/strong&gt; is incredibly powerful for programmatic SEO pages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building a tool with many landing pages — don't make the same &lt;br&gt;
mistake I did. Start with Next.js from day one.&lt;/p&gt;




&lt;p&gt;Check out &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; if you need to convert &lt;br&gt;
HEIC, WebP, AVIF or any other image format — free, no signup, no limits.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
