<?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>Building in public, week 11: 143 pages indexed, a TinyPNG alternative in Rust</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 31 May 2026 11:15:26 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-11-143-pages-indexed-a-tinypng-alternative-in-rust-4n35</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-11-143-pages-indexed-a-tinypng-alternative-in-rust-4n35</guid>
      <description>&lt;p&gt;Week 11 of building &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt; a free image converter (Rust + Axum + libvips, Next.js SSG frontend) in public. Solo, no funding, 52-week run.&lt;/p&gt;

&lt;p&gt;Here's the honest headline: **indexing jumped from 100 to 143 pages, I shipped image compression in Rust, and my clicks are still stuck.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Internal linking between blog and converter (the debt I kept dodging)
&lt;/h3&gt;

&lt;p&gt;I launched a blog in week 10 (3 posts, full schema, the works) and immediately got 18 views/week on one post. Great except that traffic had nowhere to go. The blog talked &lt;em&gt;about&lt;/em&gt; HEIC; it never pointed at the page that actually &lt;em&gt;converts&lt;/em&gt; HEIC.&lt;/p&gt;

&lt;p&gt;So week 11 I built a &lt;code&gt;RelatedArticle&lt;/code&gt; component and wired it both ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blog -&amp;gt; converter:&lt;/strong&gt; inline contextual links in the body + a CTA block at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converter -&amp;gt; blog:&lt;/strong&gt; a "Learn more" card driven by a &lt;code&gt;related_blog_slug&lt;/code&gt; column on each page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The component went through a small evolution. First version showed one article. Then I made it multi-article it pulls 2–3 related posts, matches them by topic via slug, caps at 3, and (the part that bit me) guards against the literal string &lt;code&gt;"NULL"&lt;/code&gt; sneaking in from a missing DB value. Nothing fancy, but it closes the loop: informational traffic can finally flow to the transactional pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Image compression a free TinyPNG alternative, in Rust.
&lt;/h3&gt;

&lt;p&gt;This was the fun one. TinyPNG does ~3M visits/month. That demand is enormous and I was leaving it on the table. libvips already gives me everything I need, so the backend was fast to stand up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG -&amp;gt; mozjpeg.&lt;/strong&gt; libvips can hand JPEG encoding to mozjpeg, which does trellis quantization and smarter Huffman tables. Same visual quality, meaningfully smaller files than baseline libjpeg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG -&amp;gt; imagequant.&lt;/strong&gt; This is the same lossy-PNG approach pngquant (and TinyPNG) use: quantize a 24-bit PNG down to an optimized palette. Huge size drops on PNGs with limited color, transparency preserved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UX is a quality slider (1-100%) and a before/after size readout, so you actually &lt;em&gt;see&lt;/em&gt; the reduction. I shipped three landing pages &lt;code&gt;/compress-jpg&lt;/code&gt;, &lt;code&gt;/compress-png&lt;/code&gt;, &lt;code&gt;/compress-webp&lt;/code&gt; positioned as the free alternative.&lt;/p&gt;

&lt;p&gt;The interesting signal: &lt;code&gt;Compress JPG Free&lt;/code&gt; pulled &lt;strong&gt;24 views in its first week&lt;/strong&gt;, landing straight in my top pages. Different user intent than conversion (people &lt;em&gt;converting&lt;/em&gt; a format vs people &lt;em&gt;shrinking&lt;/em&gt; a file), and it showed up immediately. That alone made the cluster worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Blog post #4
&lt;/h3&gt;

&lt;p&gt;"Why HEIC Files Won't Open" published, 8 FAQs, FAQ schema embedded in the &lt;code&gt;BlogPosting&lt;/code&gt;. Continuing the content momentum and feeding the new internal-linking machine.&lt;/p&gt;

&lt;p&gt;Also did a cannibalization audit across blog + landing pages. Came back clean: intent is cleanly split (blog = informational, landing = transactional), so they're not fighting each other in search.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Google Search Console (3 months):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Indexed: 100 -&amp;gt; &lt;strong&gt;143&lt;/strong&gt; (+43, second-biggest jump ever)&lt;/li&gt;
&lt;li&gt;Not indexed: 65 -&amp;gt; &lt;strong&gt;39&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Avg position: 40 -&amp;gt; &lt;strong&gt;39.5&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Google Analytics (7 days):&lt;/strong&gt; 31 active users (+29%), 24 new (+33%), 52 sessions (+11%).&lt;/p&gt;

&lt;p&gt;Traffic sources had a surprise: &lt;code&gt;l.threads.com&lt;/code&gt; sent 13 sessions in a week Threads quietly became a real referrer. &lt;code&gt;chatgpt.com&lt;/code&gt; has been a steady trickle for over a month now too (LLMs citing the blog posts).&lt;/p&gt;

&lt;h2&gt;
  
  
  Housekeeping wins
&lt;/h2&gt;

&lt;p&gt;Cleared a stack of small debts that were quietly rotting: fixed the 404 source (broken internal links), cleared the redirect errors flagged in GSC, and consolidated my project docs into a single source-of-truth file instead of three drifting documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next week (12)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two new tools that are low-effort / high-ROI: &lt;strong&gt;resize + crop&lt;/strong&gt; and &lt;strong&gt;background removal&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A compression-focused blog post to feed the new cluster.&lt;/li&gt;
&lt;li&gt;The real work: on-page pushes on my top-impression pages to &lt;em&gt;finally&lt;/em&gt; break something into the top 20.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped a programmatic-SEO site and watched impressions climb while clicks flatlined I'd genuinely like to hear how (or whether) you broke out of it. That's the wall I'm at.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Convertify is built with Rust + libvips + Next.js. Following along week by week.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>rust</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Image Compression in Rust with libvips Real Benchmarks, Real Tradeoffs</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Fri, 29 May 2026 13:00:32 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-image-compression-in-rust-with-libvips-real-benchmarks-real-tradeoffs-4j2</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-image-compression-in-rust-with-libvips-real-benchmarks-real-tradeoffs-4j2</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 for 11 weeks now. Last week I added compression support (JPG, PNG, WebP) using Rust + libvips. Here's what I learned, with actual numbers.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Rust + Axum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image processing&lt;/strong&gt;: libvips 8.15.1 via the &lt;code&gt;libvips&lt;/code&gt; crate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG encoder&lt;/strong&gt;: libjpeg (bundled with libvips)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG quantization&lt;/strong&gt;: imagequant (same algorithm as pngquant / TinyPNG)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG parser&lt;/strong&gt;: libspng (faster than libpng)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing worth knowing upfront: libvips does &lt;strong&gt;not&lt;/strong&gt; include mozjpeg by default. You get standard libjpeg. I'll come back to why this matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the API Works
&lt;/h2&gt;

&lt;p&gt;The compression endpoint is the same &lt;code&gt;/api/upload&lt;/code&gt; used for conversion — just pass &lt;code&gt;format_to=jpg&lt;/code&gt; (or &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;webp&lt;/code&gt;) with a &lt;code&gt;quality&lt;/code&gt; parameter (1–100):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://convertifyapp.net/api/upload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"files=@photo.jpg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"format_to=jpg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"quality=80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Rust side, &lt;code&gt;quality&lt;/code&gt; flows into libvips save options via &lt;code&gt;build_output_path&lt;/code&gt;, which appends &lt;code&gt;[Q=N]&lt;/code&gt; to the output filename — libvips picks it up automatically:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;build_output_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&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="s"&gt;"jpg"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"jpeg"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"webp"&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;q&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;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;"{}[Q={}]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"png"&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;q&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;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;"{}[Q={}]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="nf"&gt;.to_string&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;Clean. No separate code path for compression vs conversion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Benchmarks
&lt;/h2&gt;

&lt;p&gt;Test image: 1920×1080 synthetic photo, &lt;strong&gt;1.5 MB&lt;/strong&gt; original JPG (Q=95 source).&lt;/p&gt;

&lt;h3&gt;
  
  
  JPG → JPG (re-compression)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Output size&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q=90&lt;/td&gt;
&lt;td&gt;1,172 KB&lt;/td&gt;
&lt;td&gt;22.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=80&lt;/td&gt;
&lt;td&gt;890 KB&lt;/td&gt;
&lt;td&gt;41.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=70&lt;/td&gt;
&lt;td&gt;737 KB&lt;/td&gt;
&lt;td&gt;51.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=60&lt;/td&gt;
&lt;td&gt;627 KB&lt;/td&gt;
&lt;td&gt;58.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=50&lt;/td&gt;
&lt;td&gt;542 KB&lt;/td&gt;
&lt;td&gt;64.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  PNG → PNG (imagequant lossy)
&lt;/h3&gt;

&lt;p&gt;Test image: 1024×768, &lt;strong&gt;678 KB&lt;/strong&gt; original PNG.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Output size&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q=90&lt;/td&gt;
&lt;td&gt;486 KB&lt;/td&gt;
&lt;td&gt;29.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=80&lt;/td&gt;
&lt;td&gt;441 KB&lt;/td&gt;
&lt;td&gt;36.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=70&lt;/td&gt;
&lt;td&gt;396 KB&lt;/td&gt;
&lt;td&gt;42.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=60&lt;/td&gt;
&lt;td&gt;377 KB&lt;/td&gt;
&lt;td&gt;45.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=50&lt;/td&gt;
&lt;td&gt;348 KB&lt;/td&gt;
&lt;td&gt;49.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  JPG → WebP (format switch as compression)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Output size&lt;/th&gt;
&lt;th&gt;Saved vs original JPG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q=90&lt;/td&gt;
&lt;td&gt;1,120 KB&lt;/td&gt;
&lt;td&gt;26.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=80&lt;/td&gt;
&lt;td&gt;872 KB&lt;/td&gt;
&lt;td&gt;42.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q=70&lt;/td&gt;
&lt;td&gt;760 KB&lt;/td&gt;
&lt;td&gt;49.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Surprising Part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;WebP at Q=80 saves almost the same as JPG at Q=80&lt;/strong&gt; — 42.5% vs 41.3%. Not the dramatic 2× improvement you'd expect from the marketing.&lt;/p&gt;

&lt;p&gt;Why? On already-complex photographic content (lots of high-frequency detail), the gap between WebP and JPEG narrows significantly. WebP's advantage is most visible on graphics, flat colors, and images with transparency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other surprise&lt;/strong&gt;: if the source JPG is already compressed at Q=70–75 (common for web photos), re-compressing to the same format gives you almost nothing. You're fighting the JPEG DCT artifacts that are already baked in. This is where converting to WebP actually helps different codec, fresh encode.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not mozjpeg?
&lt;/h2&gt;

&lt;p&gt;mozjpeg typically gives 10–15% better compression than libjpeg at the same quality level. To use it with libvips you need to compile libvips from source with &lt;code&gt;--with-mozjpeg&lt;/code&gt;. On Ubuntu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Not trivial requires building mozjpeg first&lt;/span&gt;
git clone https://github.com/mozilla/mozjpeg.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mozjpeg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;-G&lt;/span&gt;&lt;span class="s2"&gt;"Unix Makefiles"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Then rebuild libvips pointing at mozjpeg&lt;/span&gt;
./configure &lt;span class="nt"&gt;--with-jpeg-includes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/mozjpeg/include &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;--with-jpeg-libraries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/mozjpeg/lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a production VPS running PM2 + Caddy with system libvips, the operational complexity wasn't worth it. libjpeg at Q=80 already delivers 40%+ savings which covers 90% of use cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  PNG: imagequant is the real hero
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;selected quantisation package: imagequant&lt;/code&gt; line in &lt;code&gt;vips --vips-config&lt;/code&gt; is significant. This is the same engine behind pngquant and TinyPNG's PNG compression. It works by reducing the color palette to 256 colors (lossy) while preserving perceptual quality.&lt;/p&gt;

&lt;p&gt;The Q parameter maps to imagequant's quality range. Q=80 gives ~36% savings with barely visible quality loss on photographic PNGs. For logos and flat graphics the savings are even better since there are fewer unique colors to begin with.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sweet Spot
&lt;/h2&gt;

&lt;p&gt;After running these benchmarks, the defaults I settled on for the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPG&lt;/strong&gt;: Q=82 (hits the knee of the quality/size curve, ~40% savings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG&lt;/strong&gt;: Q=80 (36% savings, imagequant artifacts not visible at normal viewing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP&lt;/strong&gt;: Q=80 (consistent with JPG behavior)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are close to what Squoosh and Lighthouse recommend. The difference is users can move the slider themselves the before/after size display makes the tradeoff tangible.&lt;/p&gt;




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

&lt;p&gt;The compression cluster is live at &lt;a href="https://convertifyapp.net/compress/jpg" rel="noopener noreferrer"&gt;convertifyapp.net/compress/jpg&lt;/a&gt;. Next up: RAW format support (CR2/NEF) for photographers, and eventually mozjpeg as an opt-in for maximum compression.&lt;/p&gt;

&lt;p&gt;If you're building something similar libvips in Rust is genuinely great. The &lt;code&gt;libvips&lt;/code&gt; crate is well-maintained, the C library is fast (faster than ImageMagick by a wide margin), and imagequant bundled for PNG is a nice bonus.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public week 11 of 52. Follow along if you're into indie dev + Rust + SEO experiments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building Convertify in Public Week 10: PDF Cluster + Blog Launch</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 24 May 2026 18:17:16 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-convertify-in-public-week-10-pdf-cluster-blog-launch-2pp7</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-convertify-in-public-week-10-pdf-cluster-blog-launch-2pp7</guid>
      <description>&lt;p&gt;Week 10 of building Convertify (&lt;a href="//convertifyapp.net"&gt;convertifyapp.net&lt;/a&gt;)&lt;br&gt;
in public.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;PDF cluster 4 new pages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/heic-to-pdf (33,100 searches/mo, KD 36)&lt;/li&gt;
&lt;li&gt;/pdf-to-png (74,000 searches/mo, KD 42)&lt;/li&gt;
&lt;li&gt;/pdf-to-jpg (90,500 searches/mo, KD 86)&lt;/li&gt;
&lt;li&gt;/png-to-pdf (3,600 searches/mo, KD 43)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backend (poppler) was already there from the &lt;br&gt;
images-to-pdf feature — pure content work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blog 0 to 3 posts in one week:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What Are HEIC Files?" KD 20, lowest KD 
across all 1,192 keywords in my strategy&lt;/li&gt;
&lt;li&gt;"How to Convert PDF to JPG 5 Methods"&lt;/li&gt;
&lt;li&gt;"AVIF vs WebP vs HEIC 2026"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full SEO infrastructure from day one: BlogPosting &lt;br&gt;
schema, BreadcrumbList, OG images, canonical, &lt;br&gt;
sitemap updated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&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;T9&lt;/th&gt;
&lt;th&gt;T10&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;75&lt;/td&gt;
&lt;td&gt;100 (+25) 🔥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impressions (3mo)&lt;/td&gt;
&lt;td&gt;509&lt;/td&gt;
&lt;td&gt;520&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg position&lt;/td&gt;
&lt;td&gt;40.1&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unexpected: chatgpt.com sent 13 sessions this &lt;br&gt;
month. Blog posts are getting picked up by AI.&lt;/p&gt;

&lt;p&gt;First blog post got 18 views in week 1 small &lt;br&gt;
but it's a start.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Internal linking blog &amp;lt;-&amp;gt; converter pages (P0 
for next week without this, blog traffic 
doesn't convert)&lt;/li&gt;
&lt;li&gt;Showoff Saturday post deleted again&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Internal linking blog &amp;lt;-&amp;gt; converter pages&lt;/li&gt;
&lt;li&gt;Blog post #3&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Week 1–10 recap: 0 -&amp;gt; 100 indexed pages, &lt;br&gt;
0 -&amp;gt; 520 impressions, 0 -&amp;gt; 3 blog posts.&lt;/p&gt;

&lt;p&gt;&lt;a href="//convertifyapp.net"&gt;Convertify&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>webdev</category>
      <category>rust</category>
    </item>
    <item>
      <title>Why I Finally Added a Blog to My Converter Tool</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Wed, 20 May 2026 14:32:11 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/why-i-finally-added-a-blog-to-my-converter-tool-37df</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/why-i-finally-added-a-blog-to-my-converter-tool-37df</guid>
      <description>&lt;p&gt;Two months in, 75 pages indexed, zero blog posts. That was the plan — ship converter pages, get them indexed, figure out the informational content later.&lt;/p&gt;

&lt;p&gt;Then one keyword changed my mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  The number that changed things
&lt;/h2&gt;

&lt;p&gt;I've been tracking keywords for Convertify across image conversion queries. Most sit between KD 35 and 86 competitive, slow to move, need real backlinks to crack.&lt;/p&gt;

&lt;p&gt;While reviewing the list this week I noticed &lt;strong&gt;"what are heic files"&lt;/strong&gt;  3,600 searches per month, KD 20.&lt;/p&gt;

&lt;p&gt;For context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;heic to jpg&lt;/code&gt; -&amp;gt; KD 67&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;convert png to pdf&lt;/code&gt; -&amp;gt; KD 43&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;avif vs webp&lt;/code&gt; -&amp;gt; KD 35&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;what are heic files&lt;/code&gt; -&amp;gt; &lt;strong&gt;KD 20&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SERP is surprisingly weak for a query with this volume. Most results are generic explainers rather than tools solving the problem directly. No converter tool has a proper dedicated page ranking for it.&lt;/p&gt;

&lt;p&gt;That felt like the right moment to start.&lt;/p&gt;




&lt;h2&gt;
  
  
  The logic behind it
&lt;/h2&gt;

&lt;p&gt;The user searching "what are heic files" is usually an iPhone owner who just tried to send a photo somewhere and got confused. They're a couple of clicks away from needing a converter.&lt;/p&gt;

&lt;p&gt;The path I'm betting on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Land on &lt;code&gt;/blog/what-are-heic-files&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Understand that HEIC isn't universally supported&lt;/li&gt;
&lt;li&gt;Follow an internal link to &lt;code&gt;/heic-to-jpg&lt;/code&gt; or &lt;code&gt;/heic-to-pdf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Convert&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Informational content converts when the internal linking is tight. A blog post with no clear path to the tool is just a vanity page. The goal here isn't content for content's sake — it's closing the loop between someone learning about a format and actually solving their problem.&lt;/p&gt;




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

&lt;p&gt;The main work was a PDF cluster three new converter pages plus the blog infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HEIC -&amp;gt; PDF&lt;/strong&gt; turned out to be more interesting than expected. PDF has no HEVCDecode filter the spec only defines DCTDecode (JPEG), FlateDecode, and JPXDecode. That means HEIC always requires a transcode to JPEG before embedding. JPG -&amp;gt; PDF by contrast is literally lossless bytes go in untouched. I wrote the full explanation in the page content because this is exactly the kind of depth that thin competitor pages skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PDF -&amp;gt; PNG&lt;/strong&gt; uses pdftocairo for rendering. pdftocairo runs about 3–4x faster than ImageMagick at 300 DPI and produces noticeably better anti-aliasing on text. libvips has a pdfload wrapper that also calls poppler under the hood, so the pipeline stays consistent.&lt;/p&gt;

&lt;p&gt;For the blog itself I went with PostgreSQL instead of MDX files. Every new MDX post requires a deploy for a solo developer writing irregularly, that friction compounds quickly. The blog_posts table uses the same JSONB content structure I already use for landing pages, so the renderer was already built. Adding the blog was mostly schema and new routes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Indexed URLs: 75 -&amp;gt; 82 (target after new pages)&lt;/li&gt;
&lt;li&gt;Impressions last 3 months: 509&lt;/li&gt;
&lt;li&gt;Blog posts live: 0 -&amp;gt; 2 (target by end of week)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cross-links I added in week 9 across 64 converter pages should start appearing in GSC by Thursday. Right now I'm prioritizing site structure and internal linking before investing time into backlink outreach. Structural fixes take a few crawl cycles to surface each one is slow feedback, but it compounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this feels different
&lt;/h2&gt;

&lt;p&gt;The interesting part about SEO at this stage isn't traffic yet it's feedback loops.&lt;/p&gt;

&lt;p&gt;You ship a structural change, wait through a few crawl cycles, and slowly learn which assumptions were right. Most changes take weeks to show up. You're essentially running experiments with a 3-week delay on results.&lt;/p&gt;

&lt;p&gt;The KD 20 HEIC query is the first time the feedback loop feels short enough to actually compound. If a new domain can rank for KD 20 in a reasonable timeframe, that's a signal worth building on.&lt;/p&gt;

&lt;p&gt;Next check is Thursday's GSC analysis.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Convertify is a free image converter built with Rust + libvips and Next.js SSG.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Week 1–10 archive: &lt;a href="https://dev.to/serhii_kalyna_730b636889c"&gt;https://dev.to/serhii_kalyna_730b636889c&lt;/a&gt;&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 9: cross-links, positions going up, and 246 new dev.to followers in 7 days</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 17 May 2026 05:55:34 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-9-cross-links-positions-going-up-and-246-new-devto-followers-in-7-days-5pb</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-9-cross-links-positions-going-up-and-246-new-devto-followers-in-7-days-5pb</guid>
      <description>&lt;p&gt;&lt;strong&gt;tl;dr:&lt;/strong&gt; Week 9 was infrastructure week. Closed the biggest SEO gap internal cross-links on 100% of pages. Average position improved for the first time in a month. &lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers
&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 8&lt;/th&gt;
&lt;th&gt;Week 9&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Indexed URLs&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impressions (3 months)&lt;/td&gt;
&lt;td&gt;463&lt;/td&gt;
&lt;td&gt;509&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg. position&lt;/td&gt;
&lt;td&gt;43.8&lt;/td&gt;
&lt;td&gt;40.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-links coverage&lt;/td&gt;
&lt;td&gt;40/64&lt;/td&gt;
&lt;td&gt;64/64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev.to followers&lt;/td&gt;
&lt;td&gt;156&lt;/td&gt;
&lt;td&gt;402&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GA last 7 days: 36 active users (+56.5%), 61 sessions (+38.6%). Traffic sources: Google organic is now the consistent #1.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually did
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cross-links: 40 -&amp;gt; 64/64 ✅
&lt;/h3&gt;

&lt;p&gt;This was the main P0 for the week. Convertify has 64 landing pages (62 format-pair conversions + home + images-to-pdf). In week 8, only 40 had contextual cross-links  inline &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; inside the body text pointing to related conversions.&lt;/p&gt;

&lt;p&gt;I added 2-3 cross-links per page across the remaining 24 pages, batched over Monday–Wednesday. The links are semantic, not just alphabetical  a page about AVIF-&amp;gt;JPG links to AVIF-&amp;gt;PNG and JPG-&amp;gt;AVIF, not to BMP-&amp;gt;TIFF.&lt;/p&gt;

&lt;p&gt;The effect on positions isn't instant. Internal links take 1-2 crawl cycles to register. But average position went from 43.8 to 40.1 the first improvement in a month. Expecting more in T10–T11 as Googlebot re-crawls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semrush on-page fixes
&lt;/h3&gt;

&lt;p&gt;Four pages got H1/title/meta updates based on Semrush priority scores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;avif-to-webp&lt;/code&gt;  priority 3.35 (highest), added missing semantic terms&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;avif-to-png&lt;/code&gt; priority 0.78, H1 and title now contain "avif to png" exactly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;heif-to-jpg&lt;/code&gt; priority 0.48, body + H1 + title now include "heif image converter"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;avif-to-jpg&lt;/code&gt; priority 0.19, title updated to match "online convert avif to jpg"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also simplified readability on 4 pages (Semrush flagged them as "difficult to read") shorter sentences, fewer compound clauses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content gaps closed
&lt;/h3&gt;

&lt;p&gt;4 pages brought from 5 to 6+ sections, 2 pages got FAQ expanded from 6 to 8 questions. Home page schema_faq synced (was 4 questions in JSON-LD, 7 in the DB now 7 everywhere).&lt;/p&gt;

&lt;p&gt;Also added a comparison table to &lt;code&gt;/images-to-pdf&lt;/code&gt; the only page that was missing one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google confirmed 35 canonical fixes
&lt;/h3&gt;

&lt;p&gt;Got an email from Google Search Console team confirming that the canonical issues I fixed in T8 (35 pages were flagged as "Alternate page with proper canonical tag") have been resolved. The "Variant page with canonical tag" count dropped to 0.&lt;/p&gt;

&lt;h3&gt;
  
  
  SaaSworthy listing
&lt;/h3&gt;

&lt;p&gt;Added Convertify to SaaSworthy full listing (Basic Details + Media + Pricing + FAQs). Free tier was actually free, no "Talk to Us" required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pinterest
&lt;/h3&gt;

&lt;p&gt;Created an account, verified the domain via meta tag, published 3 pins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HEIC-&amp;gt;JPG: "Convert HEIC to JPG Free Online No Signup"&lt;/li&gt;
&lt;li&gt;AVIF-&amp;gt;PNG: "Convert AVIF to PNG Free Preserve Transparency"&lt;/li&gt;
&lt;li&gt;PNG-&amp;gt;WebP: "Convert PNG to WebP Free 30% Smaller Files"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not expecting traffic from this. Image search visibility is the goal AVIF and WebP pages might surface in Pinterest search for people looking for format examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I deliberately didn't do
&lt;/h3&gt;

&lt;p&gt;Researched whether adding "Last updated: May 2026" to pages would help CTR. Found two case studies showing it hurt CTR by 13-22% on tool pages. Probably because it signals the tool is old and was manually touched, not that it's actively maintained. Skipped it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The unexpected thing: dev.to followers +246 in one week
&lt;/h2&gt;

&lt;p&gt;I publish weekly "Building in public" posts on dev.to + a technical article each week. Week 9 posts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Building in public, week 8: impressions exploded 71% and I think I know why"&lt;/li&gt;
&lt;li&gt;"AVIF encoding speed".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AVIF article is the one that seems to have triggered the follower spike. It's pure technical content benchmarks comparing AVIF encoding speed at different quality settings across formats. No promotion, no "check out my tool." Just data.&lt;/p&gt;

&lt;p&gt;My theory: dev.to surfaces content in the weekly digest or the tag feeds, and technical posts about specific formats get picked up by people in the web performance / image optimization community. The followers are probably devs who bookmarked the article and followed for future posts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's not working
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Clicks aren't moving.&lt;/strong&gt; The pages are showing up in Google  &lt;code&gt;avif-to-png&lt;/code&gt; has 85 impressions, &lt;code&gt;avif-to-jpg&lt;/code&gt; has 66, &lt;code&gt;png-to-webp&lt;/code&gt; has 59. But average position is still ~40. Need to be in top 20 for these before clicks appear. The on-page and cross-link work is aimed at exactly this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Plan for week 10
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keyword targeting on top pages&lt;/strong&gt;: &lt;code&gt;avif-to-png&lt;/code&gt;, &lt;code&gt;avif-to-jpg&lt;/code&gt;, &lt;code&gt;png-to-webp&lt;/code&gt; are the three pages with the most impressions. These need to move from position ~40 to position ~20. That means: content depth, more FAQ, better semantic coverage, maybe longer-form sections.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cross-links: was at 62.5% coverage, now 100%&lt;/li&gt;
&lt;li&gt;Content: 0 pages below minimum threshold&lt;/li&gt;
&lt;li&gt;Schema: all mismatches resolved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The position improvement (43.8 -&amp;gt; 40.1) is the first concrete signal that the on-page work is landing. 509 impressions.&lt;br&gt;
Real clicks need top-20 positions. That's the only thing that matters in T10.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Convertify is a free image converter built with Rust + libvips and Next.js SSG. &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;convertifyapp.net&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Week 1–9 archive: &lt;a href="https://dev.to/serhiizahornyi"&gt;dev.to/serhiizahornyi&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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