<?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.us-east-2.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>I Benchmarked 17 Image Conversions on My Production Server. Some Results Were Not What I Expected.</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sat, 20 Jun 2026 06:01:01 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/i-benchmarked-17-image-conversions-on-my-production-server-some-results-were-not-what-i-expected-1j4f</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/i-benchmarked-17-image-conversions-on-my-production-server-some-results-were-not-what-i-expected-1j4f</guid>
      <description>&lt;p&gt;I run &lt;a href="https://convertifyapp.net" rel="noopener noreferrer"&gt;Convertify&lt;/a&gt;, a free image converter built on Rust and libvips. Last week I decided to stop guessing about format performance and actually measure it. I took 50 real images (26 PNGs, 24 iPhone HEIC photos), ran 17 conversions through the production pipeline, and recorded every file size and encode time.&lt;/p&gt;

&lt;p&gt;Some results confirmed what everyone says. Others did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three results that surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Converting HEIC to JPG makes files 14% &lt;em&gt;bigger&lt;/em&gt;, not smaller.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one hurt. "Convert iPhone photos to JPG" is probably the most common advice on the internet. But HEIC wraps the HEVC codec, which compresses roughly 2x better than JPEG. Going from a better codec to a worse one means the file grows. Every time.&lt;/p&gt;

&lt;p&gt;If you actually want smaller iPhone photos: HEIC to WebP saves 43%, HEIC to AVIF saves 57%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AVIF encodes 7x slower than WebP for 10% more compression.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AVIF Q63: 55 KB, 1.30s per image.&lt;br&gt;
WebP Q80: 61 KB, 0.19s per image.&lt;/p&gt;

&lt;p&gt;That is a 10% size difference for a 7x speed penalty. For a single hero image, nobody cares. For a batch pipeline processing thousands of product photos, that is the difference between 3 minutes and 21 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. PNG at 600 DPI is &lt;em&gt;smaller&lt;/em&gt; than PNG at 300 DPI when rasterizing PDFs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was the weirdest one. I was benchmarking PDF-to-image and noticed PNG output &lt;em&gt;shrank&lt;/em&gt; from 2,221 KB at 300 DPI to 1,660 KB at 600 DPI. I spent an hour convinced I had a bug.&lt;/p&gt;

&lt;p&gt;Turns out it is a real property of PNG encoding. Higher DPI renders smoother gradients between adjacent pixels, and PNG's prediction filters (Paeth, sub, up) compress smooth gradients dramatically better than the sharp edges you get at lower resolutions. Not a bug. Just PNG being PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick reference table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Conversion&lt;/th&gt;
&lt;th&gt;Size change&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPG to WebP Q80&lt;/td&gt;
&lt;td&gt;-64%&lt;/td&gt;
&lt;td&gt;0.19s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPG to AVIF Q63&lt;/td&gt;
&lt;td&gt;-68%&lt;/td&gt;
&lt;td&gt;1.30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG to WebP Q80&lt;/td&gt;
&lt;td&gt;-92%&lt;/td&gt;
&lt;td&gt;0.21s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG to JPG Q85&lt;/td&gt;
&lt;td&gt;-86%&lt;/td&gt;
&lt;td&gt;0.07s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC to JPG Q85&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+14%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.90s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC to WebP Q80&lt;/td&gt;
&lt;td&gt;-43%&lt;/td&gt;
&lt;td&gt;5.64s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEIC to AVIF Q63&lt;/td&gt;
&lt;td&gt;-57%&lt;/td&gt;
&lt;td&gt;14.52s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP to JPG Q85&lt;/td&gt;
&lt;td&gt;+60%&lt;/td&gt;
&lt;td&gt;0.09s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF to JPG Q85&lt;/td&gt;
&lt;td&gt;+80%&lt;/td&gt;
&lt;td&gt;0.15s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;For most websites: &lt;strong&gt;WebP Q80&lt;/strong&gt;. 64% smaller than JPG, fast to encode, universal browser support since 2020.&lt;/p&gt;

&lt;p&gt;For maximum compression with no time pressure: &lt;strong&gt;AVIF Q63&lt;/strong&gt;. 68% smaller than JPG but 7x slower to encode.&lt;/p&gt;

&lt;p&gt;For iPhone photos: &lt;strong&gt;Do not convert to JPG.&lt;/strong&gt; Go straight to WebP or AVIF. JPG is a downgrade in both efficiency and file size.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full benchmark
&lt;/h2&gt;

&lt;p&gt;The complete data with PDF rasterization benchmarks, DPI comparisons, and methodology is in the &lt;a href="https://convertifyapp.net/blogs/image-format-benchmark-2026" rel="noopener noreferrer"&gt;full benchmark post on Convertify&lt;/a&gt;. Every number was measured on June 18, 2026 on the production Rust + libvips server. No theoretical figures, no borrowed data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Week 14 of building Convertify in public. Previous posts in the series cover the &lt;a href="https://convertifyapp.net/blogs/image-format-benchmark-2026" rel="noopener noreferrer"&gt;SSG migration&lt;/a&gt;, libvips segfaults, and why I stopped trusting my own landing page copy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>img</category>
      <category>performance</category>
      <category>webperf</category>
    </item>
    <item>
      <title>Building in public, week 13: the debt week nobody posts about</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 14 Jun 2026 07:16:50 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-13-the-debt-week-nobody-posts-about-2j4a</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/building-in-public-week-13-the-debt-week-nobody-posts-about-2j4a</guid>
      <description>&lt;p&gt;I'm building Convertify, a free image converter, in public over 52 weeks. It's a solo project on Rust + Axum + libvips with a Next.js frontend. This is week 13, and I want to write about it honestly because it was a week with no shiny new feature, and those weeks rarely get posts.&lt;/p&gt;

&lt;p&gt;Here's what actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  I chased a double-free instead of shipping
&lt;/h2&gt;

&lt;p&gt;My "images to PDF" path had been crashing in production, intermittently, with a GObject assertion failure. Convert a few files, fine. Convert a few more, segfault. The worst kind of bug.&lt;/p&gt;

&lt;p&gt;Root cause: a derived &lt;code&gt;Clone&lt;/code&gt; on the libvips image type was doing a bitwise pointer copy of a C object without taking a reference. Two Rust values ended up owning one underlying GObject, and when both dropped, &lt;code&gt;g_object_unref&lt;/code&gt; ran twice on the same thing. A textbook double-free, wearing a &lt;code&gt;.clone()&lt;/code&gt; as a disguise.&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="c1"&gt;// the bug&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// copies the pointer, no g_object_ref&lt;/span&gt;

&lt;span class="c1"&gt;// the fix&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;copy&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;image&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="c1"&gt;// real new GObject with its own refcount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. A couple of evenings to be sure it was that line. I wrote the full story up as a separate dev.to article this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  I fixed a UX dead-end I'd been ignoring
&lt;/h2&gt;

&lt;p&gt;If you went over the 20 MB upload limit, the convert button disabled itself, and there was no obvious way out. You couldn't remove a single file from the list. People were quietly getting stuck.&lt;/p&gt;

&lt;p&gt;So I rebuilt the file list: each file is a card with a remove button, there's a size-limit toast that fires when you cross the limit, and the dropzone goes red.&lt;/p&gt;

&lt;p&gt;While refactoring the upload handler I also collapsed three separate file-handling paths (choose, drop, remove) into one gate. That killed a whole class of bugs where the three paths had drifted apart (drop wasn't updating the size map, re-selecting the same file silently did nothing, etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  I audited my own content for honesty
&lt;/h2&gt;

&lt;p&gt;This is the unglamorous one. I went through landing pages and removed claims that didn't match what the backend actually does, checking each against the real Rust code (metadata handling, compression details). I'd rather say less and be correct than pad pages with things that aren't true.&lt;/p&gt;

&lt;p&gt;Funny side effect: an SEO audit I ran this week pointed at exactly this kind of honest, specific content as the thing that sets the site apart from templated competitors. The discipline turned out to be the moat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most useful thing I did wasn't code
&lt;/h2&gt;

&lt;p&gt;I finally set up Bing Webmaster Tools. Took five minutes (you can import straight from Google Search Console).&lt;/p&gt;

&lt;p&gt;It immediately showed me search intent Google had been hiding: around 25 different queries for "pdf to jpg at N dpi" (600, 300, 150), in multiple languages, where I'm already ranking on page one. People who need a specific print resolution. I had no idea that demand existed because Google's console never surfaced it. That's now the top candidate for next week's backend work.&lt;/p&gt;

&lt;p&gt;Lesson: if you only watch Google, you're watching most of the market through one keyhole. Other search consoles are free and show you different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers, honestly
&lt;/h2&gt;

&lt;p&gt;Impressions and indexing are both at period highs (impressions peaked around 225/day this week). Clicks are still small and spiky, which is normal for a 3-month-old domain with almost no backlinks. Nothing regressed, despite a scary-looking week-over-week dip that turned out to be me comparing a 7-day peak against a 3-month total.&lt;/p&gt;

&lt;p&gt;Threads is quietly my best referral source (real sessions every week), and ChatGPT keeps sending people too, which means I'm getting cited in LLM answers even though I haven't seen the AI crawlers hit me directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next week is a real technical week
&lt;/h2&gt;

&lt;p&gt;Backend debt: selectable TIFF compression, multipage TIFF, a CMYK path, and that DPI feature the Bing data justified. Plus refactoring my upload handler properly before I pile more on it, and turning my real benchmark data into a public study people can actually cite (which an audit suggested is a stronger link lever than guest posts).&lt;/p&gt;

&lt;p&gt;No new converter pages. The lever this stretch is depth and correctness, not surface area.&lt;/p&gt;

&lt;p&gt;If you're building something image-heavy, it's at convertifyapp.net. No account, files deleted 6 hours after conversion. Week 13 of 52 done.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>seo</category>
      <category>rust</category>
    </item>
    <item>
      <title>The Rust binding compiled fine. then it started segfaulting in prod</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Thu, 11 Jun 2026 07:59:39 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/the-rust-binding-compiled-fine-then-it-started-segfaulting-in-prod-1o94</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/the-rust-binding-compiled-fine-then-it-started-segfaulting-in-prod-1o94</guid>
      <description>&lt;p&gt;I run a free image converter, its built on rust + axum + libvips. it had been happily running for months. then one feature, images-to-pdf, started falling over in production with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;g_object_unref: assertion 'G_IS_OBJECT (object)' failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the fun part: only sometimes. convert a couple images, fine. convert a few more, boom. the non-deterministic kind of crash, which is the worst kind, because it makes you sit there wondering if you even understand the language you've been writing for years.&lt;/p&gt;

&lt;p&gt;Turns out the bug was mine. the trigger was one tiny innocent &lt;code&gt;.clone()&lt;/code&gt;. heres the whole thing because i'm pretty sure other people doing rust over a C lib will eventually step on the same rake.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But rust is memory safe"
&lt;/h2&gt;

&lt;p&gt;This is what messed with my head for a while. rust is supposed to stop exactly this. no double frees, no use after free, borrow checker has your back. so how am i double freeing anything at all?&lt;/p&gt;

&lt;p&gt;One word makes all those guarantees quietly disappear: &lt;strong&gt;FFI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Libvips is a C library built on GObject, and GObject does its own manual reference counting. the rust crate wraps those C objects in rust types. and the second a rust type is holding a raw pointer into C land, the borrow checker just... cant help you anymore. it sees a struct. it does not see that the struct is actually a handle to a refcounted thing living on the C heap. safety stops right at that line and nobody tells you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual cause: a Clone that doesnt clone
&lt;/h2&gt;

&lt;p&gt;Heres roughly what was blowing up. i was building a multi-page pdf and somewhere in the loop i cloned the image:&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="c1"&gt;// the crash version&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ... use copy ...&lt;/span&gt;
&lt;span class="c1"&gt;// both `image` and `copy` drop here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VipsImage&lt;/code&gt; in the crate derives &lt;code&gt;Clone&lt;/code&gt;. and a derived clone on a thing that holds a pointer does the obvious dumb thing: it copies the pointer. bit for bit. so now the same C object has two rust owners, and here is the kicker, &lt;strong&gt;nobody incremented the GObject refcount&lt;/strong&gt;. libvips still thinks there is exactly one reference to that object.&lt;/p&gt;

&lt;p&gt;Then rust does its job a little too well. both values go out of scope, &lt;code&gt;Drop&lt;/code&gt; runs twice, and each drop calls &lt;code&gt;g_object_unref&lt;/code&gt; on the same object. first unref takes the count to zero and frees it. second unref runs on already-freed memory, libvips checks &lt;code&gt;G_IS_OBJECT&lt;/code&gt;, the assertion blows, and depending on timing you either get a glib critical or a clean segfault.&lt;/p&gt;

&lt;p&gt;So. a textbook double free. wearing a &lt;code&gt;.clone()&lt;/code&gt; as a disguise. the "random" part was just the allocator deciding whether the freed slot had been reused yet or not.&lt;/p&gt;

&lt;p&gt;A correct clone here would have to call &lt;code&gt;g_object_ref&lt;/code&gt; so each owner actually accounts for its own reference. the derived one doesnt. so calling it &lt;code&gt;Clone&lt;/code&gt; is generous. its an aliased pointer with two destructors aimed at it.&lt;/p&gt;

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

&lt;p&gt;Stopped cloning the handle, started asking libvips for a real new object instead:&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="c1"&gt;// the fixed version&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;copy&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;image&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;&lt;code&gt;ops::copy&lt;/code&gt; runs an actual libvips copy and gives you back a fresh GObject with its own refcount. now each rust value owns its own distinct C object, each drop unrefs its own thing, nothing gets freed twice. crash gone.&lt;/p&gt;

&lt;p&gt;one line. took me a couple evenings to be sure it was &lt;em&gt;that&lt;/em&gt; line, which is always how these go.&lt;/p&gt;

&lt;h2&gt;
  
  
  a few more libvips + rust landmines from the same evening
&lt;/h2&gt;

&lt;p&gt;since i was already in there bleeding, a few neighbours showed up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;JpegsaveBufferOptions::default()&lt;/code&gt; hands you a busted &lt;code&gt;keep&lt;/code&gt; value.&lt;/strong&gt; the default came out as something like &lt;code&gt;keep | 32&lt;/code&gt;, which libvips just rejects with a glib critical. fix was to stop trusting the default and set it myself:&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="n"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ForeignKeep&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;the operation cache was a second way to die.&lt;/strong&gt; libvips caches operations by default, and under load that cache plus the lifecycle mess above gave me another crash, this time inside cache trimming. i turned the operation cache off entirely while getting things stable, which killed that path. just know it exists and that its global state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;VipsApp::new(name, false)&lt;/code&gt; is not what it looks like.&lt;/strong&gt; that second bool is &lt;code&gt;detect_leak&lt;/code&gt;, not some concurrency switch, which is exactly what i had lazily assumed it was. read the signature, dont pattern-match on vibes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bumping the crate did not save me.&lt;/strong&gt; i tried going from &lt;code&gt;1.7.x&lt;/code&gt; up to &lt;code&gt;2.1.0&lt;/code&gt; hoping the lifecycle stuff was just fixed upstream. nope, changed nothing for this. reverted to &lt;code&gt;1.7.6&lt;/code&gt; and fixed it properly on my end.&lt;/p&gt;

&lt;h2&gt;
  
  
  what i actually took from this
&lt;/h2&gt;

&lt;p&gt;a derived &lt;code&gt;Clone&lt;/code&gt; on a type that wraps a foreign, refcounted resource is a trap. &lt;code&gt;#[derive(Clone)]&lt;/code&gt; assumes a clone is a cheap copy-the-fields job. for a GObject handle thats the exact wrong assumption: copy the pointer without taking a ref and you've got two owners, one refcount, and rust's own &lt;code&gt;Drop&lt;/code&gt; happily freeing it twice.&lt;/p&gt;

&lt;p&gt;so the rule i walked out with: when a rust type owns a C resource that has its own lifecycle, treat &lt;code&gt;Clone&lt;/code&gt; and &lt;code&gt;Drop&lt;/code&gt; as suspects, not freebies. need another handle? reach for the library's own copy/ref function before you derive &lt;code&gt;Clone&lt;/code&gt; and hope.&lt;/p&gt;

&lt;p&gt;if you've hit something like this with libvips bindings or any GObject FFI, i'd genuinely love to hear how you dealt with it, the comments are the fun part. the tool this was all holding up is at &lt;a href="//convertifyapp.net"&gt;convertifyapp.net&lt;/a&gt; if you want a look. no account, files get deleted after.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>programming</category>
      <category>rust</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Week 12 building Convertify in public (free image converter, Rust + libvips + Next.js SSG)</title>
      <dc:creator>Serhii Kalyna</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:47:13 +0000</pubDate>
      <link>https://dev.to/serhii_kalyna_730b636889c/week-12-building-convertify-in-public-free-image-converter-rust-libvips-nextjs-ssg-1g84</link>
      <guid>https://dev.to/serhii_kalyna_730b636889c/week-12-building-convertify-in-public-free-image-converter-rust-libvips-nextjs-ssg-1g84</guid>
      <description>&lt;p&gt;What I shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two new tools live: Resize Image and Crop Image, both on libvips natively&lt;/li&gt;
&lt;li&gt;A long-form guide, "How to Compress Images Without Losing Quality," written to feed my compress JPG/PNG/WebP pages with real internal links and real benchmarks from 24 of my own iPhone photos&lt;/li&gt;
&lt;li&gt;A site-wide "Image Tools" block (inline section + footer column) so every page now cross-links to the new tools&lt;/li&gt;
&lt;li&gt;Squashed a nasty non-deterministic segfault in the libvips Rust bindings. It was a double-free: the binding derives Clone over the raw image pointer, so cloning copied the pointer without bumping the refcount and Drop freed the same object twice. Replaced the clone with an explicit copy, disabled the op cache, and the crashes plus the G_IS_OBJECT warnings are gone. Happy to write up the war story if anyone wants it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers (last 7 days):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clicks doubled, up 100% week over week. Still small in absolute terms, but real and trending the right way for the first time in weeks.&lt;/li&gt;
&lt;li&gt;Impressions also up 100%, as the new pages finished getting indexed.&lt;/li&gt;
&lt;li&gt;Average position is creeping up, into the mid-40s on the most recent day, from around 51 the week before.&lt;/li&gt;
&lt;li&gt;A brand-new long-tail query, "sony hif file to jpg converter," is already sitting at position 6. That is exactly the kind of low-competition query I am betting the whole strategy on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest read:&lt;br&gt;
Clicks and impressions both doubled, which is great, but the absolute numbers are still small and capped by domain authority. I have around 15 referring domains; the big converters have thousands. On competitive terms like "avif to png" (KD 42) I cannot rank without backlinks, and that is a months-long game. So the play stays low-KD long-tail: pages that can reach the top 10 even at low DR. That position-6 result on a niche query is the early proof it works.&lt;/p&gt;

&lt;p&gt;Next week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Concentrate internal links on the low-KD pages that already have impressions&lt;/li&gt;
&lt;li&gt;Start background removal (rembg sidecar) research&lt;/li&gt;
&lt;li&gt;Keep backlink outreach slow and quality-first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;convertifyapp.net&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>seo</category>
      <category>buildinpublic</category>
    </item>
    <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/compress/png" 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/compress/jpg" 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/pdf-to-jpg&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/pdf-to-jpg"&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/jpg-to-avif" 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>
  </channel>
</rss>
