<?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>AVIF encoding speed — the numbers nobody talks about</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Thu, 14 May 2026 16:16:27 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/avif-encoding-speed-the-numbers-nobody-talks-about-a2h</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/avif-encoding-speed-the-numbers-nobody-talks-about-a2h</guid>
      <description>&lt;p&gt;Everyone talks about how small AVIF files are.&lt;/p&gt;

&lt;p&gt;Almost nobody talks about what it costs to generate them in production.&lt;/p&gt;

&lt;p&gt;I run a free image converter built on Rust + libvips. After processing thousands of conversions, here are the numbers that changed how I think about format choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compression story you already know
&lt;/h2&gt;

&lt;p&gt;AVIF beats WebP on file size. For photos and gradients:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AVIF saves ~50% vs JPEG (median across 600 photos)&lt;/li&gt;
&lt;li&gt;WebP saves ~31% vs JPEG&lt;/li&gt;
&lt;li&gt;At the same DSSIM score, AVIF produces files ~50% smaller than WebP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real and impressive. But that's only half the equation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The encoding cost nobody talks about
&lt;/h2&gt;

&lt;p&gt;Here's what actually happens when you encode with libaom (the default AVIF encoder):&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;Encoding time (1080p)&lt;/th&gt;
&lt;th&gt;Peak CPU&lt;/th&gt;
&lt;th&gt;Peak RAM&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;~90ms&lt;/td&gt;
&lt;td&gt;~20%&lt;/td&gt;
&lt;td&gt;~200MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF (libaom, default settings)&lt;/td&gt;
&lt;td&gt;1–4 seconds&lt;/td&gt;
&lt;td&gt;~400% (4 cores)&lt;/td&gt;
&lt;td&gt;~2.5GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF (maximum quality)&lt;/td&gt;
&lt;td&gt;up to 48 seconds&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In my tests, AVIF encoding with libaom was &lt;strong&gt;up to 47× slower than WebP&lt;/strong&gt; at comparable quality settings. Peak memory usage during a single 4000px AVIF conversion reached ~2.5GB in Sharp/libvips using libaom. WebP used ~200MB for the same image.&lt;/p&gt;

&lt;p&gt;The memory usage surprised me most. For a service handling concurrent batch uploads, that's not a benchmark number - it's a capacity planning problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AVIF optimizes bandwidth. WebP optimizes compute.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;effort&lt;/code&gt; parameter an oversimplification
&lt;/h2&gt;

&lt;p&gt;If you use Sharp, you've probably assumed: higher &lt;code&gt;effort&lt;/code&gt; = smaller files.&lt;/p&gt;

&lt;p&gt;In practice, it's not that simple.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;quality&lt;/code&gt; is the primary driver of file size. &lt;code&gt;effort&lt;/code&gt; controls how much time the encoder spends searching for better compression decisions. Depending on the image, higher effort can slightly reduce or even increase output size. The effect is unpredictable and usually marginal.&lt;/p&gt;

&lt;p&gt;This is documented behavior in Sharp's issue tracker (#3418): at fixed &lt;code&gt;quality&lt;/code&gt;, increasing &lt;code&gt;effort&lt;/code&gt; sometimes produces larger files.&lt;/p&gt;

&lt;p&gt;Practical starting point for AVIF in Sharp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sharp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;avif&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quality&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="na"&gt;effort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;effort: 5-6&lt;/code&gt; hits a reasonable balance. Going to &lt;code&gt;effort: 9&lt;/code&gt; adds seconds of encoding for marginal and unpredictable gains.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use which format
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Best format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User uploads processed on-the-fly&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static assets, pre-generated at build&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time transforms&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum compression for photos&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-memory servers&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screenshots, UI, flat graphics&lt;/td&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photography, gradients, textures&lt;/td&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule I follow: &lt;strong&gt;AVIF for static, WebP for dynamic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 4-second encoding time is fine in a CI pipeline. It's not fine when a user is waiting for their converted file.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this looks like in a converter pipeline
&lt;/h2&gt;

&lt;p&gt;In my converter pipeline, the challenge with AVIF isn't throughput  it's memory spikes under concurrent load. If multiple users upload large images simultaneously, AVIF jobs can spike memory hard across all cores.&lt;/p&gt;

&lt;p&gt;Current approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Concurrency limits on AVIF jobs&lt;/li&gt;
&lt;li&gt;Default &lt;code&gt;effort: 4&lt;/code&gt; for interactive conversions fast enough for real-time use&lt;/li&gt;
&lt;li&gt;SVT-AV1 encoder is worth watching: roughly 2× faster than libaom, 5× faster than rav1e at comparable quality&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Effectively solved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebP&lt;/strong&gt;: ~95.6% global support (Safari 14+, all modern browsers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AVIF&lt;/strong&gt;: ~94.3% global support (Safari 16.4+, Chrome 85+, Firefox 93+)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard fallback pattern still makes sense:&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;"..."&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;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;"Better compression" and "better for your use case" aren't the same thing.&lt;/p&gt;

&lt;p&gt;AVIF is technically superior for compression. But if you're encoding on-the-fly, the encoding overhead is a real operational cost not a benchmark footnote.&lt;/p&gt;

&lt;p&gt;WebP is fast, lightweight, and produces files dramatically smaller than JPEG. It's not a consolation prize.&lt;/p&gt;

&lt;p&gt;The best setup: pre-generate AVIF for static assets, encode WebP dynamically, serve both via &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; or CDN content negotiation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; a free image converter powered by Rust + libvips. Week 9 of building in public.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sources: Sharp GitHub issues #2597, #3418 · libheif AVIF Encoder Benchmark · SpeedVitals WebP vs AVIF · W3Techs image format statistics (May 2026) · Can I Use&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>rust</category>
      <category>avif</category>
    </item>
    <item>
      <title>Building in public, week 8: impressions exploded 71% and I think I know why</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 10 May 2026 12:21:24 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-8-impressions-exploded-71-and-i-think-i-know-why-54ab</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-8-impressions-exploded-71-and-i-think-i-know-why-54ab</guid>
      <description>&lt;p&gt;So this week something actually happened.&lt;/p&gt;

&lt;p&gt;I've been building Convertify - a free image converter (Rust + libvips backend, Next.js frontend) - for about 2 months now. Writing content, fixing schema, submitting to directories, watching Google mostly ignore me.&lt;/p&gt;

&lt;p&gt;This week the numbers moved.&lt;/p&gt;

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

&lt;p&gt;Google Search Console, 3-month view:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Impressions: 271 -&amp;gt; &lt;strong&gt;463&lt;/strong&gt; (+71%)&lt;/li&gt;
&lt;li&gt;Indexed pages: 59 -&amp;gt; &lt;strong&gt;72&lt;/strong&gt; (+13)&lt;/li&gt;
&lt;li&gt;External links: 40 -&amp;gt; &lt;strong&gt;65&lt;/strong&gt; (+25)&lt;/li&gt;
&lt;li&gt;Average position: 40.8 -&amp;gt; &lt;strong&gt;43.8&lt;/strong&gt; (worse, but I'll explain)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That impressions jump is the biggest I've ever had.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think caused it
&lt;/h2&gt;

&lt;p&gt;I don't think it was one thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 7&lt;/strong&gt; I updated 19 pages - added content sections, cross-links, fixed a schema bug where 189 FAQ entries were missing from JSON-LD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 8&lt;/strong&gt; I did Semrush-driven fixes - put target keywords in H1s and titles, simplified sentences for readability, added semantic terms that competitors use but I wasn't. Also brought 16 pages up to 8+ FAQ each.&lt;/p&gt;

&lt;p&gt;My theory is Google re-crawled the batch of updated pages and suddenly saw: "oh wait, these pages actually have real content now, not just a converter widget and two paragraphs."&lt;/p&gt;

&lt;h2&gt;
  
  
  The position thing
&lt;/h2&gt;

&lt;p&gt;Average position went from 40.8 to 43.8 which looks bad. But 13 new pages got indexed this week, and they all start at positions 50-80. That drags the average down even though the older pages are actually climbing.&lt;/p&gt;

&lt;p&gt;One page - jpg-to-webp - is at position &lt;strong&gt;5.8&lt;/strong&gt; with 39 impressions. Position 5.8! That's basically page one. Zero clicks though. Working on the title/description to fix CTR.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built this week
&lt;/h2&gt;

&lt;p&gt;Shipped batch image-to-PDF. You drop up to 10 images, pick page size (A4/Letter) and orientation, get one combined PDF. Built the PDF from raw bytes in Rust — no PDF library, just manually constructing xref tables and DCTDecode streams. Was it overkill? Probably. Was it fun? Absolutely.&lt;/p&gt;

&lt;p&gt;Also refactored the entire Rust backend - split a 1200-line main.rs into proper modules (db, errors, image, notification, path, pdf). Should've done this weeks ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backlinks update
&lt;/h2&gt;

&lt;p&gt;Got on StackShare (DR ~89) and submitted to Capterra (DR ~91, pending review). Capterra was interesting - one submission potentially gets you on Capterra + Software Advice + GetApp, all three owned by Gartner.&lt;/p&gt;

&lt;p&gt;Tried a few other directories from my backlink plan. Turns out FeedMyApp is dead (domain expired in 2022), Go2Web20 is dead, VentureBeat Profiles doesn't exist as a free thing anymore, and BetaList wants $39 minimum. So that saved me some future time at least.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organic search is now #1 traffic source
&lt;/h2&gt;

&lt;p&gt;This is the thing I'm most excited about. search.google.com has been the top traffic source for two weeks straight now.  It's small numbers but the trend is clear.&lt;/p&gt;

&lt;p&gt;The new /images-to-pdf page immediately became the #2 most viewed page on the site. Google picked it up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next (week 9)
&lt;/h2&gt;

&lt;p&gt;Biggest gap right now: 24 out of 64 pages have no cross-links in their body text. Plan is to add 2-3 natural cross-links per page across all 24 this week.&lt;/p&gt;

&lt;p&gt;Also rewriting titles and meta descriptions for pages that are ranking but not getting clicks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want to try it: &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt;. 40+ format pairs, batch conversion, image-to-PDF. Free, no signup.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Previous weeks: &lt;a href="https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-7-19-pages-189-missing-faq-entries-and-organic-search-finally-showing-up-22c"&gt;week 7&lt;/a&gt; | &lt;a href="https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-6-pdf-support-is-live-but-i-had-to-sacrifice-content-for-it-2ch3"&gt;week 6&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>seo</category>
      <category>buildinpublic</category>
      <category>showdev</category>
    </item>
    <item>
      <title>From 0 to 72 indexed pages in 2 months: what Google actually does with new sites</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Fri, 08 May 2026 19:57:15 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/from-0-to-72-indexed-pages-in-2-months-what-google-actually-does-with-new-sites-3kca</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/from-0-to-72-indexed-pages-in-2-months-what-google-actually-does-with-new-sites-3kca</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 in Rust + Next.js — for about 2 months. Here's where I am in GSC right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;72 indexed pages&lt;/li&gt;
&lt;li&gt;Average position: 40.8&lt;/li&gt;
&lt;li&gt;Impressions: ~90/day&lt;/li&gt;
&lt;li&gt;Clicks: basically 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not glamorous. But I spent time researching what's actually happening under the hood, and it changed how I think about the whole thing.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The "Google Sandbox" thing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everyone talks about it. New site, no traffic, must be the sandbox right?&lt;/p&gt;

&lt;p&gt;John Mueller said it directly in 2019: &lt;em&gt;"There is no sandbox."&lt;/em&gt; Google doesn't penalize new sites — it just hasn't collected enough trust signals yet. There's a difference.&lt;/p&gt;

&lt;p&gt;What actually happens is closer to this timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Week 1: pages get crawled and indexed&lt;/li&gt;
&lt;li&gt;Weeks 2–6: Google tests your pages on low-volume queries&lt;/li&gt;
&lt;li&gt;Month 2–3: first stable positions for long-tail keywords&lt;/li&gt;
&lt;li&gt;Month 4–6: rankings start to stabilize&lt;/li&gt;
&lt;li&gt;Month 6–12: meaningful traffic growth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm at month 2. Right on schedule for "still basically nothing."&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What actually moved the needle for me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few things that made a real difference early:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static site generation.&lt;/strong&gt; I migrated from a Vite React SPA to Next.js SSG specifically because Google wasn't indexing my pages. Dynamic JS rendering was killing crawlability. After the migration: 186 static pages, all indexable. Impressions jumped within 2 weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal linking.&lt;/strong&gt; Added &lt;code&gt;RelatedConversions&lt;/code&gt; component — every page links to 10-14 related conversion pages. Before this, new pages were getting zero impressions. After: slow but steady crawl pickup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAQ schema.&lt;/strong&gt; Every landing page has FAQPage JSON-LD with 8-10 real questions. Not for rich snippets (those don't show for this type) — for entity clarity and content depth signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content depth over breadth.&lt;/strong&gt; Instead of 5 shallow pages, I focused on making each conversion page actually useful — real technical comparisons, format history, use cases. Some pages now have 10+ sections.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What didn't work (yet)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backlinks. I spent time on directory submissions, Reddit posts, forum comments. Honest assessment: the directories that matter have 2-4 month queues. The "best image converter" articles are written by competitors. Forum comments get removed.&lt;/p&gt;

&lt;p&gt;The only backlink strategy that feels real right now is writing content that people actually want to link to. Which is why I'm writing this.&lt;/p&gt;

&lt;p&gt;CTR is also rough — 1 click total at position 40. That's expected. You can't get clicks from page 4. The only fix is ranking higher, which takes time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What I'm watching now&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GSC shows a spike in impressions at end of April — new pages entering the index. Then a drop. Mueller warned about this: &lt;em&gt;"a site may get a spike, then return to normal — that's when you need patience."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm in the patience phase.&lt;/p&gt;

&lt;p&gt;Month 4-6 is supposedly when things stabilize. I'll report back.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The honest summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you launched a site recently and have basically no traffic — you're probably fine. Google isn't punishing you. It's just waiting to see if you're serious.&lt;/p&gt;

&lt;p&gt;The things that seem to matter most in the first 3 months: static rendering, internal linking, content depth, and schema markup. Not backlinks. Not social media. Just making the site technically solid and giving Google something worth indexing.&lt;/p&gt;

&lt;p&gt;We'll see if that holds at month 6.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building Convertify in public — free image converter, Rust + libvips backend, Next.js SSG. Week 8 of the build.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building in public, week 7: 19 pages, 189 missing FAQ entries, and organic search finally showing up</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 03 May 2026 17:28:38 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-7-19-pages-189-missing-faq-entries-and-organic-search-finally-showing-up-22c</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-7-19-pages-189-missing-faq-entries-and-organic-search-finally-showing-up-22c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: I shipped image → PDF conversion but spent most of the week on SEO content instead of the planned batch UI and landing page. The numbers say that was the right call. Organic search became the #1 traffic source for the first time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Convertify is a free image converter I'm building solo: Rust + Axum + libvips on the backend, Next.js 16.2 SSG on the frontend, PostgreSQL for landing page content. Every week I publish what worked, what didn't, and the real situation behind it.&lt;/p&gt;

&lt;p&gt;Here's week 7.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened vs. the plan
&lt;/h2&gt;

&lt;p&gt;The plan was clear: ship image → PDF with full batch UI (page size options, orientation picker) plus a dedicated &lt;code&gt;/images-to-pdf&lt;/code&gt; landing page, and catch up on community engagement.&lt;/p&gt;

&lt;p&gt;What actually happened: image → PDF conversion works — you can convert JPG, PNG, WebP to PDF right now. But the batch UI (combining multiple images into one PDF with layout controls) and the dedicated landing page didn't get built. Instead I went deep on content.&lt;/p&gt;

&lt;p&gt;What I did instead of the planned batch work: mass content expansion. Created 9 new landing pages, updated 5 existing ones to full content, improved 5 more that were lagging behind, and found a critical bug in my structured data that had been silently hurting SEO for weeks.&lt;/p&gt;

&lt;p&gt;The reason was simple. I opened the database, looked at the gaps, and realized that getting 19 pages from thin/empty to complete would compound for months. The batch UI and &lt;code&gt;/images-to-pdf&lt;/code&gt; page would polish one feature. Content would give me 19 new entry points into Google. I picked the compounding bet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema_faq bug — 189 missing questions
&lt;/h2&gt;

&lt;p&gt;This was the real find of the week.&lt;/p&gt;

&lt;p&gt;I have &lt;code&gt;faq&lt;/code&gt; fields on every landing page (the visible FAQ accordion) and &lt;code&gt;schema_faq&lt;/code&gt; fields (the JSON-LD FAQPage markup that Google reads for rich results). They're supposed to match. They didn't.&lt;/p&gt;

&lt;p&gt;When I checked: 55 out of 62 pages had fewer questions in &lt;code&gt;schema_faq&lt;/code&gt; than in &lt;code&gt;faq&lt;/code&gt;. That's 189 questions Google couldn't see in structured data — almost a third of my entire FAQ content was invisible to rich results.&lt;/p&gt;

&lt;p&gt;The cause was boring: &lt;code&gt;schema_faq&lt;/code&gt; was written manually when I first created each page, and never updated when I added more FAQ entries later. Classic data sync bug.&lt;/p&gt;

&lt;p&gt;The fix had two parts. First, a SQL migration that synced all 56 pages. Second, a frontend fix in &lt;code&gt;FAQ.tsx&lt;/code&gt; — I removed the &lt;code&gt;landing?.schema_faq ??&lt;/code&gt; fallback so the component now always generates schema from &lt;code&gt;faqItems&lt;/code&gt; directly. The desync can't happen again.&lt;/p&gt;

&lt;p&gt;This is one of those bugs that costs you nothing visibly but bleeds SEO silently. You don't notice it until you audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content expansion: three clusters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TIFF cluster — 5 new pages.&lt;/strong&gt; &lt;a href="https://convertifyapp.net/jpg-to-tiff" rel="noopener noreferrer"&gt;JPG→TIFF&lt;/a&gt;, &lt;a href="https://convertifyapp.net/png-to-tiff" rel="noopener noreferrer"&gt;PNG→TIFF&lt;/a&gt;, &lt;a href="https://convertifyapp.net/pdf-to-tiff" rel="noopener noreferrer"&gt;PDF→TIFF&lt;/a&gt;, HEIF→TIFF, AVIF→TIFF. These target print/archival workflows — people who need lossless containers for prepress, medical imaging, GIS. Low volume keywords but almost zero competition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GIF cluster — 4 new pages.&lt;/strong&gt; &lt;a href="https://convertifyapp.net/webp-to-gif" rel="noopener noreferrer"&gt;WebP→GIF&lt;/a&gt;, &lt;a href="https://convertifyapp.net/avif-to-gif" rel="noopener noreferrer"&gt;AVIF→GIF&lt;/a&gt;, HEIF→GIF, HEIC→GIF. The use case is mostly "I have a modern format and need an animated GIF for legacy platforms." Slack, older email clients, forums that only accept GIF — it's still a thing in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HEIF pairs — 5 existing pages rewritten.&lt;/strong&gt; &lt;a href="https://convertifyapp.net/heif-to-jpg" rel="noopener noreferrer"&gt;HEIF→JPG&lt;/a&gt;, &lt;a href="https://convertifyapp.net/heif-to-png" rel="noopener noreferrer"&gt;HEIF→PNG&lt;/a&gt;, HEIF→WebP, HEIF→AVIF, HEIF→HEIC. These existed but had minimal content. Now each has 7+ sections explaining the difference between HEIF (the container) and HEIC (HEIF + HEVC codec), with comparison tables and full FAQ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial pages filled — 5 more.&lt;/strong&gt; &lt;a href="https://convertifyapp.net/png-to-heic" rel="noopener noreferrer"&gt;PNG→HEIC&lt;/a&gt;, &lt;a href="https://convertifyapp.net/heic-to-webp" rel="noopener noreferrer"&gt;HEIC→WebP&lt;/a&gt;, GIF→JPG, GIF→BMP, GIF→HEIC all got expanded to full content.&lt;/p&gt;

&lt;p&gt;Every page also got cross-links embedded in the content body — teal-styled &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags pointing to related conversions. The &lt;code&gt;ContentSections.tsx&lt;/code&gt; component now supports &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; for these, plus paragraph splitting on &lt;code&gt;\n\n&lt;/code&gt; so content doesn't render as walls of text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend work: RelatedConversions upgrade
&lt;/h2&gt;

&lt;p&gt;The internal linking component got a proper overhaul. Before, it was semi-random. Now it uses a &lt;code&gt;VALID_SLUGS&lt;/code&gt; set (no more 404 links), reverse-route boosting (if you're on A→B, the B→A page ranks high), family overlap scoring, and a HEIC/HEIF sibling bonus. Max 14 links per page, all validated against actual database slugs.&lt;/p&gt;

&lt;p&gt;Small thing, but internal linking is one of those levers where the improvement compounds across every page simultaneously.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Google Search Console&lt;/strong&gt; is moving in the right direction. Indexed pages went from 52 to 59. Impressions had the biggest single-week jump since launch. Average position dropped below 41 for the first time — slow grind but it's grinding the right way. GSC also unfroze after the data lag from week 6, so the dashboard is fresh again.&lt;/p&gt;

&lt;p&gt;Clicks are still minimal. The math is straightforward: at position ~40, CTR is basically zero. I need to crack top 20 on at least a few queries before clicks become meaningful. That's not a content problem — that's a time-and-authority problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Analytics&lt;/strong&gt; showed the signal I've been waiting for: &lt;strong&gt;search.google.com became the #1 traffic source this week&lt;/strong&gt; — beating Reddit, Threads, and everything else. First time organic search has been on top. Active users grew by 40% week over week, and new users by nearly 70%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.semrush.com/" rel="noopener noreferrer"&gt;Semrush&lt;/a&gt; Site Audit&lt;/strong&gt; came back clean: 98% health score, zero errors. No broken links, no redirect chains, no orphan pages. The remaining warnings are minor — some pages flagged for low text-to-HTML ratio (expected with Next.js SSG where the JS bundle is heavy relative to static content), and two pages with low word count that I'll bulk up in the next round. The site also passed all Core Web Vitals checks, and the crawl depth stays within 3 clicks for nearly everything. For a solo project with 60+ landing pages, I'm pretty happy with that baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm noticing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Content × schema = compound effect.&lt;/strong&gt; Every page I fix works 24/7 for months. That's fundamentally different from a Reddit post that peaks in 48 hours. The schema_faq fix alone means Google now sees 189 more questions across the site. That's not a one-time bump — it's a permanent improvement to how the site appears in search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Organic traffic is real now.&lt;/strong&gt; It's still small. But seven weeks ago it was literally zero. The trend line matters more than the absolute number. And when you see Google outpace your referral channels for the first time — that's a signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Community engagement suffered again.&lt;/strong&gt; Two weeks in a row with minimal Reddit/dev.to activity. Karma flatlined, HN karma still stuck. This is becoming a pattern I need to break in week 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest moment
&lt;/h2&gt;

&lt;p&gt;I planned to ship a polished feature with batch UI and a landing page. I shipped the core conversion and then pivoted to content instead. It feels less exciting to write about — "I updated 19 database rows" doesn't have the same energy as "I built a multi-image PDF composer."&lt;/p&gt;

&lt;p&gt;But looking at the analytics, this was probably the highest-ROI week so far. Every page I created or fixed is now a permanent surface area for organic discovery. The schema bug fix alone was worth the entire week.&lt;/p&gt;

&lt;p&gt;Sometimes the boring work is the important work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 8 plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Primary:&lt;/strong&gt; finish the image → PDF experience — batch upload (multiple images into one PDF), page size/orientation options, and the &lt;code&gt;/images-to-pdf&lt;/code&gt; landing page. The conversion works, now it needs the polish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secondary:&lt;/strong&gt; re-engage community channels. Daily Reddit/dev.to comments, one new article, one Reddit post. Two weeks of silence is too long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not doing:&lt;/strong&gt; new SEO clusters, big redesigns, or chasing new platforms.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Convertify is a free, ad-free image converter. &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt; — weekly updates every Sunday.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>buildinpublic</category>
      <category>seo</category>
    </item>
    <item>
      <title>Building in public, week 6: PDF support is live — but I had to sacrifice content for it</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 26 Apr 2026 17:53:18 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-6-pdf-support-is-live-but-i-had-to-sacrifice-content-for-it-2ch3</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-6-pdf-support-is-live-but-i-had-to-sacrifice-content-for-it-2ch3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: I shipped PDF → image conversion this week. The cost? Three days redesigning the converter UI, and falling behind on community content. Solo dev tradeoffs are real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Convertify is a free image converter I'm building solo: Rust + Axum + libvips on the backend, Next.js SSG on the frontend, PostgreSQL for landing page content. Every week I publish what worked, what didn't, and the numbers behind it.&lt;/p&gt;

&lt;p&gt;Here's week 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped: PDF support (Wednesday)
&lt;/h2&gt;

&lt;p&gt;The big one. Convertify now converts PDFs to images — JPG, PNG, WebP, and so on. Multi-page PDFs are bundled into a ZIP with one image per page.&lt;/p&gt;

&lt;p&gt;What's &lt;strong&gt;not&lt;/strong&gt; shipped yet: the reverse direction (image → PDF). That's the headline feature for week 7.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I integrated PDF in libvips
&lt;/h3&gt;

&lt;p&gt;libvips itself doesn't speak PDF natively — you need a backend. I'm using a libvips extension that wraps the heavy lifting (I'll dig into the exact crate / build flag in a follow-up post once I'm sure I'm describing it correctly — there are a few moving parts and I don't want to misinform).&lt;/p&gt;

&lt;p&gt;The flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload PDF → temp file&lt;/li&gt;
&lt;li&gt;Page count via the PDF backend&lt;/li&gt;
&lt;li&gt;Loop through pages, render each to a raster at the requested format/quality&lt;/li&gt;
&lt;li&gt;If single page → return the image directly&lt;/li&gt;
&lt;li&gt;If multi-page → stream a ZIP containing all rendered pages&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ZIP path was the part I spent the most time on. Stream-zipping in Rust without buffering everything in memory took some plumbing — but it pays off the first time someone uploads a 50-page PDF and the server doesn't OOM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Landing pages
&lt;/h3&gt;

&lt;p&gt;PDF launched with the full set of landing pages already drafted: &lt;code&gt;/pdf-to-jpg&lt;/code&gt;, &lt;code&gt;/pdf-to-png&lt;/code&gt;, &lt;code&gt;/pdf-to-webp&lt;/code&gt;, etc. They're in the same SSG pipeline as the rest of the site (Postgres → static build → Caddy).&lt;/p&gt;

&lt;p&gt;The interesting signal: the &lt;code&gt;PDF to PNG&lt;/code&gt; page is already the &lt;strong&gt;#2 most-visited page&lt;/strong&gt; on the site after the homepage, three days after launch. No marketing, no posts about it yet. Just organic discovery.&lt;/p&gt;

&lt;p&gt;That's the kind of thing that makes you want to ship faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't ship: content + image→PDF
&lt;/h2&gt;

&lt;p&gt;Thursday, Friday, and Saturday I spent redesigning the converter UI itself — the format picker, the upload area, the buttons component system (migrating off MUI to a custom CSS-modules-based system to drop bundle size).&lt;/p&gt;

&lt;p&gt;That redesign was overdue. But it cost me three days of community content work I'd planned: replies on Reddit, comments on dev.to articles in my space, follow-ups in subreddits I've been growing in. All pushed to week 7.&lt;/p&gt;

&lt;p&gt;This is the solo dev squeeze. You can ship features, &lt;strong&gt;or&lt;/strong&gt; you can grow distribution. Doing both well in the same week is hard. I picked features this week. Next week the math has to flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack Overflow update
&lt;/h2&gt;

&lt;p&gt;Quick note for anyone following the SO experiment from earlier weeks: I got a moderator warning. Not a ban — but a clear signal about the rules. The takeaway: &lt;strong&gt;you can use SO for self-promotion in narrow, well-defined cases&lt;/strong&gt; (genuinely answering a question where your tool is one of several legitimate solutions, with full disclosure), but the bar is high and the risk/reward isn't great for backlinks specifically.&lt;/p&gt;

&lt;p&gt;For now I'm not pursuing SO further as a backlink channel. Reddit, dev.to, Hashnode, and Indie Hackers continue to be the better mix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers, week 6
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indexed pages in GSC&lt;/strong&gt;: 52 (up from 40+ last week)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average position&lt;/strong&gt;: 40+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clicks from search&lt;/strong&gt;: still climbing toward meaningful numbers — early indexing, niche keywords, give it time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top traffic sources this week&lt;/strong&gt;: SourceForge, Reddit, Threads (referral traffic is starting to compound from earlier posts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top countries&lt;/strong&gt;: Ukraine, US, China, Germany&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GSC dashboard has been stuck on April 21 for five days, which is annoying — Google's lag is usually 2-3 days, not 5. I'll re-check next week before drawing conclusions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm noticing
&lt;/h2&gt;

&lt;p&gt;A few things worth writing down so I remember them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Niche landing pages get found.&lt;/strong&gt; The PDF→PNG page didn't need a single backlink to start pulling traffic. SSG + clean URLs + targeted titles are doing the work. This validates the programmatic SEO bet — keep adding pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Referral traffic compounds slowly.&lt;/strong&gt; Reddit comments I made weeks ago are still bringing people in. Threads posts from earlier in the buildinpublic series are now a source. None of this happens fast — but it does add up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Redesigning is never "small".&lt;/strong&gt; Three days for a converter UI redesign sounds like overkill until you realize that bad UX kills retention faster than missing features. The new format picker handles 15 → 40+ formats gracefully (it'll matter once I add more). The new buttons system dropped a lot of bundle weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest moment
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this week was full of breakthrough energy. I was heads-down and on autopilot for most of it. PDF integration was hard, the redesign ate the rest of the week, and I shipped instead of celebrating.&lt;/p&gt;

&lt;p&gt;Sometimes building in public means publishing a week that was just &lt;em&gt;work&lt;/em&gt;. No story arc, no aha moment. Just: kept moving, shipped the thing, paid the cost, here's what's next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 7 plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One goal:&lt;/strong&gt; ship image → PDF and catch up on content.&lt;/p&gt;

&lt;p&gt;That's it. No new initiatives, no shiny experiments. Close the PDF loop in both directions, and re-engage the community channels I dropped this week.&lt;/p&gt;

&lt;p&gt;If you're a solo dev reading this — what's your current ratio of build vs grow? I'd love to compare notes.&lt;/p&gt;




&lt;p&gt;*Convertify is a free, ad-free image converter built solo. If you've got files to convert, give it a try &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt; — and if you want to follow the build, I post weekly.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>indiehackers</category>
      <category>rust</category>
      <category>seo</category>
    </item>
    <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>
  </channel>
</rss>
