<?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: Macan</title>
    <description>The latest articles on DEV Community by Macan (@brian_wijnandts_54811758d).</description>
    <link>https://dev.to/brian_wijnandts_54811758d</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%2F2344664%2F915df865-c79b-4b18-9f9d-37518cac6e49.png</url>
      <title>DEV Community: Macan</title>
      <link>https://dev.to/brian_wijnandts_54811758d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brian_wijnandts_54811758d"/>
    <language>en</language>
    <item>
      <title>I built a shopping search engine in Rust that you talk to in plain words</title>
      <dc:creator>Macan</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:54:09 +0000</pubDate>
      <link>https://dev.to/brian_wijnandts_54811758d/i-built-a-shopping-search-engine-in-rust-that-you-talk-to-in-plain-words-4iih</link>
      <guid>https://dev.to/brian_wijnandts_54811758d/i-built-a-shopping-search-engine-in-rust-that-you-talk-to-in-plain-words-4iih</guid>
      <description>&lt;p&gt;Keyword search is bad at specific products. I'd know &lt;em&gt;exactly&lt;/em&gt; what I wanted — "dark green waxed cotton jacket, under €200, not from a giant marketplace" —&lt;br&gt;
  and every engine buried me in Amazon listings and content-farm "best of" lists.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://hubje.nl" rel="noopener noreferrer"&gt;Hubje&lt;/a&gt;: you describe a product in plain language and it returns real, buyable products from independent shops, ranked by how&lt;br&gt;
  well they match what you actually said. Here's the interesting engineering.&lt;/p&gt;

&lt;p&gt;## Server-rendered Rust, no SPA&lt;/p&gt;

&lt;p&gt;The whole site is Rust (&lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;axum&lt;/a&gt;) rendering HTML with &lt;a href="https://maud.lambda.xyz/" rel="noopener noreferrer"&gt;Maud&lt;/a&gt;, a compile-time template macro. No&lt;br&gt;
  JS framework, no hydration, no API layer — handlers return HTML strings.&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;product_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;Product&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;Markup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nd"&gt;html!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded-2xl border border-slate-200 bg-white"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;img_url&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;p&lt;/span&gt;&lt;span class="py"&gt;.image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="n"&gt;srcset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;img_srcset&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;p&lt;/span&gt;&lt;span class="py"&gt;.image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
                  &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CARD_SIZES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="n"&gt;h3&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-sm font-semibold text-ink"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why: a shopping site lives or dies by SEO, and SSR is trivially crawlable + fast. htmx handles the few interactive bits (live search results swap in-place)&lt;br&gt;
  as progressive enhancement — URLs stay real and shareable. The "framework" is the type system.&lt;/p&gt;

&lt;p&gt;## Plain-language search&lt;/p&gt;

&lt;p&gt;Search isn't keyword matching. It pulls live listings (via &lt;a href="https://exa.ai/" rel="noopener noreferrer"&gt;Exa&lt;/a&gt;) and an LLM ranks/filters them against your &lt;em&gt;sentence&lt;/em&gt; — including soft&lt;br&gt;
  constraints like "under €200" or "minimalist". Same pipeline writes the one-line "why this pick" rationales and a top-3.&lt;/p&gt;

&lt;p&gt;The genuinely fun part is the &lt;strong&gt;failure mode as a feature&lt;/strong&gt;: when a description returns nothing buyable, that zero-result query is logged. Recurring gaps&lt;br&gt;
  auto-generate a curated buying guide page — so unmet demand turns into indexable content. The content engine feeds itself.&lt;/p&gt;

&lt;p&gt;## CWV obsession (self-host everything)&lt;/p&gt;

&lt;p&gt;Organic is the only growth channel I can afford, so Core Web Vitals matter. I ended up removing every third-party request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fonts&lt;/strong&gt; — vendored the variable woff2s, &lt;code&gt;@font-face&lt;/code&gt; with &lt;code&gt;font-display: swap&lt;/code&gt; + &lt;code&gt;unicode-range&lt;/code&gt; so &lt;code&gt;latin-ext&lt;/code&gt; only loads on an accented glyph. No
Google Fonts round-trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;htmx&lt;/strong&gt; — self-hosted + &lt;code&gt;defer&lt;/code&gt;, so zero render-blocking external JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt; — a Rust &lt;code&gt;/img&lt;/code&gt; proxy that downscales and &lt;strong&gt;content-negotiates&lt;/strong&gt; the format:
&lt;/li&gt;
&lt;/ul&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;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ImgFmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accept_header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// webp if offered, else jpeg&lt;/span&gt;
  &lt;span class="c1"&gt;// cache key + Vary: Accept so caches don't cross-serve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebP (libwebp via the &lt;code&gt;webp&lt;/code&gt; crate) is ~30% smaller than JPEG and builds fine for a distroless image. Cloudflare's field data now shows LCP 100% "good".&lt;/p&gt;

&lt;p&gt;Plus the boring-but-required stuff: canonical URLs, full OpenGraph/Twitter, JSON-LD (Product with price/availability, ItemList, FAQ, Breadcrumb, Dataset),&lt;br&gt;
  noindex on thin pages, a real 404, trailing-slash 301s.&lt;/p&gt;

&lt;p&gt;## Ad-blocker-proof analytics&lt;/p&gt;

&lt;p&gt;GA and even Cloudflare's beacon get blocked by uBlock/AdGuard — which undercounts exactly this audience. So pageviews are counted &lt;strong&gt;server-side&lt;/strong&gt; in the&lt;br&gt;
  request middleware (cookieless, aggregate-only), with day-salted hashes for unique visitors and a curated datacenter-IP list to flag scrapers spoofing&lt;br&gt;
  browser UAs as bots, not humans.&lt;/p&gt;

&lt;p&gt;## Deploy&lt;/p&gt;

&lt;p&gt;&lt;code&gt;include_str!&lt;/code&gt;/&lt;code&gt;include_bytes!&lt;/code&gt; embed the CSS, fonts, htmx, and favicon straight into the binary, so the runtime image is just distroless + one static-ish&lt;br&gt;
  executable. Built with kaniko, deployed to a small k3s homelab cluster. The whole thing is one ~170MB binary.&lt;/p&gt;

&lt;p&gt;## Honest tradeoffs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's new — catalog coverage is thin, so niche searches sometimes return nothing.&lt;/li&gt;
&lt;li&gt;Affiliate-funded (disclosed); commission never affects ranking.&lt;/li&gt;
&lt;li&gt;LLM-in-the-loop search is a latency/cost tradeoff vs. a pure index; caching + a top-3-only LLM pass keeps it sane.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with the most oddly-specific thing you've failed to find online and tell me if it surfaces anything sane: &lt;strong&gt;&lt;a href="https://hubje.nl" rel="noopener noreferrer"&gt;https://hubje.nl&lt;/a&gt;&lt;/strong&gt; — feedback welcome,&lt;br&gt;
  especially on search quality.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
