<?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: Jakub</title>
    <description>The latest articles on DEV Community by Jakub (@jakub_inithouse).</description>
    <link>https://dev.to/jakub_inithouse</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%2F3847884%2Fd5cc2611-0246-4150-95e0-c1145fa35d05.png</url>
      <title>DEV Community: Jakub</title>
      <link>https://dev.to/jakub_inithouse</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jakub_inithouse"/>
    <language>en</language>
    <item>
      <title>Be Recommended by Inithouse: 4 Mistakes We Made Building an AI Visibility Checker — and the Fixes That Worked</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Tue, 16 Jun 2026 00:23:16 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/be-recommended-by-inithouse-4-mistakes-we-made-building-an-ai-visibility-checker-and-the-fixes-4m8j</link>
      <guid>https://dev.to/jakub_inithouse/be-recommended-by-inithouse-4-mistakes-we-made-building-an-ai-visibility-checker-and-the-fixes-4m8j</guid>
      <description>&lt;p&gt;At Inithouse — a studio running parallel product experiments — we built &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt;, a tool that checks how visible your brand is across ChatGPT, Perplexity, Claude, and Gemini. The idea sounded simple: query multiple AI models, score the results, show a report.&lt;/p&gt;

&lt;p&gt;It was not simple. Here are four technical mistakes we made shipping v1 — and the fixes that actually survived production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: Rate Limiting Was an Afterthought
&lt;/h2&gt;

&lt;p&gt;We treated rate limits as edge cases. They were not. Every AI provider has different rate-limit headers, different backoff expectations, and different definitions of "too many requests." Our first architecture just retried on 429. That turned a rate limit into a cascade — one provider throttling triggered a retry storm that cascaded to the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Per-provider circuit breakers with exponential backoff. Each provider gets its own state machine. When a circuit opens, we serve cached results for that provider and mark the score as "partial" in the UI. Users see real data, not a spinner that never resolves.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt; — another tool in our portfolio focused on code quality audits — we observed the same pattern in a different domain: external API dependencies need isolation. The lesson transferred directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: The Caching Strategy Was Too Naive
&lt;/h2&gt;

&lt;p&gt;Our first cache key was &lt;code&gt;query + model&lt;/code&gt;. That breaks immediately — AI model responses drift over time, and a cached result from two weeks ago is misleading. We also had no invalidation strategy beyond TTL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Cache by &lt;code&gt;query + model + week_number&lt;/code&gt;. Weekly invalidation with stale-while-revalidate: serve the cached score instantly, trigger a background refresh, update the display when new data arrives. Users get instant feedback and fresh data within the same session.&lt;/p&gt;

&lt;p&gt;We measured the impact across our portfolio: stale-while-revalidate cut perceived load time from 8+ seconds to under 1 second for returning visitors. The background refresh means scores stay current without the user waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3: Model Version Changes Broke Everything Silently
&lt;/h2&gt;

&lt;p&gt;When OpenAI or Anthropic ships a new model version, recommendation patterns shift. We had no way to detect this — scores just quietly changed, and users saw different numbers without understanding why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Track model versions per query. When a model version changes, flag the score delta in the report: "Score changed from 72 to 65 — model updated from GPT-4.1 to GPT-4.5." Transparency here builds trust. Users stop thinking the tool is broken and start understanding the landscape.&lt;/p&gt;

&lt;p&gt;We found this matters even more for niche products. In our portfolio, tools like &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt; — an AI music generator for personalized gifts — saw wild visibility swings between model versions. Tracking those shifts helped us understand which AI models retain context about smaller brands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4: The Scoring Algorithm Was a Black Box
&lt;/h2&gt;

&lt;p&gt;Our v1 score was a weighted average. Users saw "Your score is 47/100" and had no idea what that meant or how to improve it. Support tickets were mostly "why is my score low?" — we had no good answer because the methodology was opaque even to us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Decomposed scoring with per-factor breakdown. The report now shows exactly which AI models mention you, in what context, with what sentiment, and for which prompts. Each factor has its own sub-score. Users can see "Claude recommends you for 3 out of 10 test queries, mostly in the 'alternatives to X' category."&lt;/p&gt;

&lt;p&gt;This was the single biggest improvement for retention. When users can see exactly what to fix, they come back to measure progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;Building &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt; taught us that the hardest part of querying AI models is not the querying — it is everything around it: rate limits, caching, version tracking, and making results interpretable. The AI visibility space moves fast, and a tool that worked last month can silently degrade.&lt;/p&gt;

&lt;p&gt;Across the Inithouse portfolio, we keep finding that transparency compounds — whether in AI visibility reports, code audits, or personalized content. Tools that show their work earn repeat usage.&lt;/p&gt;

&lt;p&gt;If you want to check your own AI visibility score, &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt; runs a free instant analysis across multiple AI models with full methodology transparency.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>startup</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Add Living Photo Effects to Your Web Portfolio</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Sat, 13 Jun 2026 07:19:26 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/how-to-add-living-photo-effects-to-your-web-portfolio-2oif</link>
      <guid>https://dev.to/jakub_inithouse/how-to-add-living-photo-effects-to-your-web-portfolio-2oif</guid>
      <description>&lt;p&gt;Static portfolios blend together. Every designer's site has the same grid of JPEGs. We wanted something different for our own product pages at Inithouse, a studio shipping a growing portfolio of products in parallel, so we started experimenting with living photos: short AI-generated animations that make a still image breathe.&lt;/p&gt;

&lt;p&gt;Here's how we did it, what we learned about performance, and the code you need to do it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What living photos actually are
&lt;/h2&gt;

&lt;p&gt;You upload a regular photo. An AI model generates a short video loop where parts of the image move naturally: hair blows, water ripples, eyes blink. The output is a 2-4 second clip that loops cleanly.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://alivephoto.online" rel="noopener noreferrer"&gt;alivephoto.online&lt;/a&gt; for exactly this. No signup, no account. Upload, wait about 30 seconds, download. The tool deletes your photo after processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pick the right source photo
&lt;/h2&gt;

&lt;p&gt;Not every photo works equally well. From our testing across thousands of uploads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portraits work best.&lt;/strong&gt; Faces, hair, clothing give the model clear motion targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product shots with texture.&lt;/strong&gt; Fabric, liquid, smoke, reflections all animate convincingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landscapes with water or sky.&lt;/strong&gt; Clouds, waves, leaves in wind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat graphics don't work.&lt;/strong&gt; Logos, icons, UI screenshots produce artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a portfolio hero section, pick your strongest portrait or a textured product shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Generate the animation
&lt;/h2&gt;

&lt;p&gt;Head to &lt;a href="https://alivephoto.online" rel="noopener noreferrer"&gt;alivephoto.online&lt;/a&gt;, drop your image, and hit generate. You'll get a short video clip back. Download it.&lt;/p&gt;

&lt;p&gt;For production use, you want the video format (MP4/WebM), not the GIF. Here's why:&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;Typical size (1080p, 3s)&lt;/th&gt;
&lt;th&gt;Browser support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GIF&lt;/td&gt;
&lt;td&gt;8-15 MB&lt;/td&gt;
&lt;td&gt;Universal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebM&lt;/td&gt;
&lt;td&gt;200-600 KB&lt;/td&gt;
&lt;td&gt;Chrome, Firefox, Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MP4&lt;/td&gt;
&lt;td&gt;300-800 KB&lt;/td&gt;
&lt;td&gt;Universal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GIFs are 20-40x larger. Nobody wants a 12 MB hero image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Embed it as a looping background
&lt;/h2&gt;

&lt;p&gt;Here's a clean, responsive implementation:&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;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-bg"&lt;/span&gt;
    &lt;span class="na"&gt;autoplay&lt;/span&gt;
    &lt;span class="na"&gt;loop&lt;/span&gt;
    &lt;span class="na"&gt;muted&lt;/span&gt;
    &lt;span class="na"&gt;playsinline&lt;/span&gt;
    &lt;span class="na"&gt;preload=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;
    &lt;span class="na"&gt;poster=&lt;/span&gt;&lt;span class="s"&gt;"/img/hero-still.jpg"&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;src=&lt;/span&gt;&lt;span class="s"&gt;"/video/hero-living.webm"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/webm"&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;src=&lt;/span&gt;&lt;span class="s"&gt;"/video/hero-living.mp4"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&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;"/img/hero-still.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Portfolio hero"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Your headline here&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;poster&lt;/code&gt; attribute shows the static frame while the video loads. The &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; fallback covers edge cases where video fails entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hero-bg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-50%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;-50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;object-fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.hero-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4rem&lt;/span&gt; &lt;span class="m"&gt;2rem&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;
  
  
  Step 4: Handle mobile properly
&lt;/h2&gt;

&lt;p&gt;Autoplay video on mobile eats bandwidth. Respect your users:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hero&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hero-bg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(max-width: 768px)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;autoplay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;playBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Play animation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;playBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero-play-btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;playBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;playBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playBtn&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;Alternative approach: use &lt;code&gt;preload="none"&lt;/code&gt; universally and trigger load with Intersection Observer:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unobserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;video&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;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.hero-bg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Measure the impact
&lt;/h2&gt;

&lt;p&gt;We ran A/B tests on our own product landing pages. On &lt;a href="https://petimagination.com" rel="noopener noreferrer"&gt;Pet Imagination&lt;/a&gt;, where we generate AI pet portraits, we tested a static hero vs. a living photo hero.&lt;/p&gt;

&lt;p&gt;The animated version held attention longer. Time-on-page went up. Whether that moves your specific conversion needle depends on your layout and CTA placement.&lt;/p&gt;

&lt;p&gt;Key thing: don't let the animation distract from your call to action. Subtle movement beats dramatic. If the photo moves so much that visitors watch it instead of reading your copy, you've lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes we've seen
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Too much motion.&lt;/strong&gt; The AI can produce dramatic effects. Dial it back for hero sections. You want "huh, is that photo moving?" not "whoa what's happening."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the poster frame.&lt;/strong&gt; Without &lt;code&gt;poster&lt;/code&gt;, visitors see a blank rectangle until the video loads. Always include a static fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serving GIFs in production.&lt;/strong&gt; We measured this across multiple product pages at Inithouse. Switching from GIF to WebM cut load times by 3-4 seconds on mobile connections. There's no reason to ship GIF for looping animations in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No reduced-motion support.&lt;/strong&gt; Some users have &lt;code&gt;prefers-reduced-motion&lt;/code&gt; enabled. Respect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.hero-bg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&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;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/img/hero-still.jpg')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cover&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;
  
  
  The full picture
&lt;/h2&gt;

&lt;p&gt;Living photos work when used with restraint. One animated hero section per page. Keep the clip short. Serve WebM with MP4 fallback. Give mobile users a choice. Respect accessibility preferences.&lt;/p&gt;

&lt;p&gt;We use this technique across several products at Inithouse, a studio building a growing portfolio of tools. If you want to try generating a living photo from your own portfolio shot, &lt;a href="https://alivephoto.online" rel="noopener noreferrer"&gt;alivephoto.online&lt;/a&gt; is free and requires no signup.&lt;/p&gt;

&lt;p&gt;The source photo stays yours. We delete it after processing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>Common mistakes when building a multi-domain photo app (CZ/SK/PL/EN/DE from one codebase)</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Fri, 12 Jun 2026 04:20:10 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/common-mistakes-when-building-a-multi-domain-photo-app-czskplende-from-one-codebase-kg8</link>
      <guid>https://dev.to/jakub_inithouse/common-mistakes-when-building-a-multi-domain-photo-app-czskplende-from-one-codebase-kg8</guid>
      <description>&lt;p&gt;At Inithouse, a studio shipping a growing portfolio of products in parallel, we run an AI photo animation tool across five country domains from a single codebase. The product turns a static photo into a short living video: &lt;a href="https://zivafotka.cz" rel="noopener noreferrer"&gt;zivafotka.cz&lt;/a&gt; for Czech users, &lt;a href="https://zivafotka.sk" rel="noopener noreferrer"&gt;zivafotka.sk&lt;/a&gt; for Slovak, &lt;a href="https://zywafotka.pl" rel="noopener noreferrer"&gt;zywafotka.pl&lt;/a&gt; for Polish, &lt;a href="https://alivephoto.online" rel="noopener noreferrer"&gt;alivephoto.online&lt;/a&gt; for English, and &lt;a href="https://lebendigfoto.de" rel="noopener noreferrer"&gt;lebendigfoto.de&lt;/a&gt; for German.&lt;/p&gt;

&lt;p&gt;Sounds clean on paper. One repo, five builds, five domains. In practice, we walked into every trap the setup could offer. Here are five that cost us the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: One sitemap for all five domains
&lt;/h2&gt;

&lt;p&gt;We started with a single &lt;code&gt;sitemap.xml&lt;/code&gt; generated at build time, served identically on every domain. Google indexed the Czech pages fine, then ignored almost everything else. The crawl budget went to whichever domain Google hit first, and hreflang tags pointed in circles because every sitemap referenced every other domain with no clear canonical signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we changed:&lt;/strong&gt; Each domain now gets its own sitemap listing only its own URLs, with hreflang pointing to the matching pages on sibling domains. Crawl distribution improved within two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: Hardcoding locale strings in components
&lt;/h2&gt;

&lt;p&gt;Early on, we had Czech strings scattered across React components. Adding Slovak was easy (close enough to copy-paste), but Polish broke our assumptions about string length, and German broke our layout. The codebase turned into a maze of ternary expressions checking &lt;code&gt;window.location.hostname&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we changed:&lt;/strong&gt; We extracted all strings into per-locale JSON files and built a thin domain-to-locale resolver that runs at app init. Components just call &lt;code&gt;t('key')&lt;/code&gt;. Adding the German domain took a day instead of a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3: One analytics property, no hostname segmentation
&lt;/h2&gt;

&lt;p&gt;We pointed all five domains at a single GA4 property. The numbers looked great in aggregate. Then we tried to answer "which market converts best?" and realized we had no clean way to split traffic. Hostname as a secondary dimension worked in theory, but half our custom events were missing the hostname parameter entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we changed:&lt;/strong&gt; We kept one GA4 property (managing five would be worse), but enforced hostname as a required parameter on every event. We also built a lightweight stats endpoint that segments by domain automatically. We took the same approach at &lt;a href="https://petimagination.com" rel="noopener noreferrer"&gt;Pet Imagination&lt;/a&gt;, our AI pet portrait tool, where the stats API now segments by referral source instead of domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4: Translating meta descriptions instead of localizing them
&lt;/h2&gt;

&lt;p&gt;We ran the Czech meta descriptions through a translation layer and called it done. The Polish description for the homepage literally said "enliven your photo" in a phrasing no Polish speaker would use for this context. Click-through rates on the Polish domain were 40% lower than Czech for the same ranking positions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we changed:&lt;/strong&gt; We hired native speakers to rewrite (not translate) every meta title and description. Cultural context matters more than literal accuracy. The Polish homepage description now references "rodzinne zdjecia" (family photos) because that turned out to be the dominant use case in Poland, different from the Czech audience that skews toward pet and travel photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 5: Deploying everywhere at once
&lt;/h2&gt;

&lt;p&gt;We shipped to all five domains simultaneously. A layout bug in the German version (long compound words breaking the card grid) went live on a Friday afternoon. By the time we noticed, Google had crawled the broken pages and our Core Web Vitals tanked for &lt;code&gt;lebendigfoto.de&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we changed:&lt;/strong&gt; We now deploy to the smallest-traffic domain first (currently Slovak), wait 24 hours, check error logs and Clarity recordings, then roll out to the rest. We apply the same staged-rollout thinking at &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt;, where we test audit report changes on internal projects before pushing them to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would do differently from day zero
&lt;/h2&gt;

&lt;p&gt;If we started over, we would set up i18n, per-domain sitemaps, and hostname-segmented analytics before writing a single feature component. The multi-domain architecture is worth it for local SEO and trust signals. But the infrastructure tax is real, and paying it upfront is cheaper than retrofitting.&lt;/p&gt;

&lt;p&gt;At Inithouse, a lab building many products at once, this was one of our more complex setups. Most products in the portfolio run on a single domain. The five-domain experiment taught us that internationalization is less about translation and more about treating each market as its own product.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Team Inithouse&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>i18n</category>
      <category>devops</category>
      <category>react</category>
    </item>
    <item>
      <title>How to Generate Royalty-Free Background Music for Your App</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Sat, 06 Jun 2026 23:33:51 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/how-to-generate-royalty-free-background-music-for-your-app-de1</link>
      <guid>https://dev.to/jakub_inithouse/how-to-generate-royalty-free-background-music-for-your-app-de1</guid>
      <description>&lt;p&gt;At Inithouse, a studio shipping a growing portfolio of products in parallel, we deal with audio assets across several apps. One problem keeps coming back: finding background music that fits the mood, costs nothing in royalties, and doesn't require a lawyer.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt; to generate studio-quality custom songs from a text prompt. Real vocals, professional production, ready in minutes. Price per track: $1.90. While it was designed for personalized gift songs (birthdays, weddings), we've been using it internally to prototype audio for our other products too.&lt;/p&gt;

&lt;p&gt;Here's how to add AI-generated background music to your web app or game, step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define your audio requirements
&lt;/h2&gt;

&lt;p&gt;Before generating anything, answer three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mood&lt;/strong&gt;: ambient, upbeat, dramatic, lo-fi?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tempo&lt;/strong&gt;: slow background hum or energetic gameplay loop?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration&lt;/strong&gt;: short intro jingle or continuous ambient track?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For background music, instrumental tracks work best. Vocal melodies pull attention away from your UI. When we prototyped ambient audio for &lt;a href="https://petimagination.com" rel="noopener noreferrer"&gt;Pet Imagination&lt;/a&gt;, our AI pet portrait generator, we learned this the hard way: a catchy vocal hook made users forget they were uploading a photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Generate 3 to 5 variants
&lt;/h2&gt;

&lt;p&gt;Head to &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;magicalsong.com&lt;/a&gt; and describe the mood you need. Be specific: "calm piano ambient for a productivity app, no vocals, 90 BPM" works better than "nice background music."&lt;/p&gt;

&lt;p&gt;Generate multiple variants. Audio is subjective; what sounds right in your headphones might feel wrong in context. We typically generate 3 to 5 options and test each against the actual UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Download and prepare the file
&lt;/h2&gt;

&lt;p&gt;Download your chosen track as MP3. For web apps, MP3 gives you the best balance of quality and file size. If you're building a native game with an engine that prefers WAV or OGG, convert accordingly.&lt;/p&gt;

&lt;p&gt;Compression matters for mobile users. A 128kbps MP3 is usually enough for background audio. No one notices the difference between 128 and 320 when it's playing behind UI interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Implement the audio player
&lt;/h2&gt;

&lt;p&gt;Here's a minimal HTML5 Audio API setup for looping background music:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bgMusic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/audio/background.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Browsers block autoplay without user interaction&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to watch:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Autoplay policy&lt;/strong&gt;: every modern browser blocks audio autoplay until the user interacts with the page. The snippet above handles this by starting playback on first click. Don't fight the policy; work with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume&lt;/strong&gt;: 0.3 is a good starting point. Background music that's too loud competes with your content. Let users adjust it if your app has settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make it loop cleanly
&lt;/h2&gt;

&lt;p&gt;AI-generated tracks don't always loop perfectly. The tail might fade out or the intro might have a beat of silence. Trim both ends for a clean loop:&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="c1"&gt;// Trim approach: skip first 0.5s, stop 0.5s before end&lt;/span&gt;
&lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeupdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bgMusic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;For production, use a tool like ffmpeg to trim the actual file. The JavaScript approach works for prototyping but adds a micro-gap on each loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; background.mp3 &lt;span class="nt"&gt;-ss&lt;/span&gt; 0.5 &lt;span class="nt"&gt;-to&lt;/span&gt; 180 &lt;span class="nt"&gt;-c&lt;/span&gt; copy background-loop.mp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Test with real users
&lt;/h2&gt;

&lt;p&gt;We run this check across our portfolio at Inithouse: play the app with and without music, then ask 5 people which version feels better. Music affects retention more than most developers expect. On &lt;a href="https://voicetables.com" rel="noopener noreferrer"&gt;Voice Tables&lt;/a&gt;, our voice-first AI workspace, we tested ambient audio during voice input sessions and found it reduced the "empty room" feeling users reported.&lt;/p&gt;

&lt;p&gt;Not every app needs music. But if yours has idle states, loading screens, or immersive flows, a well-chosen background track makes the experience feel finished rather than hollow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Licensing confusion&lt;/strong&gt;: most AI-generated music is royalty-free by the generator's ToS, but always read the fine print. Magical Song tracks are yours to use commercially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One track for everything&lt;/strong&gt;: generate separate tracks for different moods. A menu screen and a gameplay scene shouldn't share the same vibe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting mobile&lt;/strong&gt;: test on real phones. Background audio on mobile eats battery and data. Give users a mute toggle and respect their choice.&lt;/p&gt;




&lt;p&gt;At Inithouse, a studio building a growing portfolio of products, we've been solving audio challenges like this across many of our apps. If your project needs a soundtrack, give &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt; a try. $1.90 per track, no subscription, no royalty worries.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>How We Ship a Growing Portfolio of AI Products at Inithouse</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Sat, 06 Jun 2026 20:20:13 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/how-we-ship-a-growing-portfolio-of-ai-products-at-inithouse-260i</link>
      <guid>https://dev.to/jakub_inithouse/how-we-ship-a-growing-portfolio-of-ai-products-at-inithouse-260i</guid>
      <description>&lt;p&gt;At Inithouse, we run a lab that ships a growing portfolio of AI products in parallel. Not one product at a time. Not a pivot-heavy path from idea to idea. A deliberate strategy: build multiple MVPs, measure what sticks, double down on what works.&lt;/p&gt;

&lt;p&gt;Here is how we actually do it, and what we learned shipping products like &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt; (studio-quality custom songs from your story), &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt; (AI Visibility Reports for brands), and &lt;a href="https://zivafotka.cz" rel="noopener noreferrer"&gt;Ziva Fotka&lt;/a&gt; (AI photo-to-video tool, multi-domain across 5 languages).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with one product, then fan out
&lt;/h2&gt;

&lt;p&gt;We did not start with a portfolio. We started with a single MVP, validated the build pipeline, then replicated it. The key lesson: don't scale the number of products until you can ship one in under 3 weeks. If your first product takes 3 months, your tenth will too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Standardize the tech stack
&lt;/h2&gt;

&lt;p&gt;Every product in our portfolio runs on the same foundation: React SPA, Supabase backend, shared analytics layer. When we build &lt;a href="https://petimagination.com" rel="noopener noreferrer"&gt;Pet Imagination&lt;/a&gt; (AI pet portrait generator with 9 styles), the deploy pipeline is identical to what we use for &lt;a href="https://verdictbuddy.com" rel="noopener noreferrer"&gt;Verdict Buddy&lt;/a&gt; (AI conflict resolver using established psychology frameworks). Same CI, same monitoring, same hosting config. A new product spins up in hours, not days.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Share infrastructure across products
&lt;/h2&gt;

&lt;p&gt;We built a shared analytics and reporting layer that covers all products at once. One dashboard shows signups, conversions, and engagement across the entire portfolio. We track Google Search Console, GA4, and Clarity for every product from a single config file. When something breaks on one product, we usually catch it because the same pattern broke elsewhere first.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Automate the repetitive work
&lt;/h2&gt;

&lt;p&gt;Our reporting, SEO audits, and content publishing run on automated schedules. We measured the time we spent on manual weekly reports and replaced most of it with scheduled jobs that pull data, flag anomalies, and post summaries. The team reads a digest, not a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Measure early, redirect fast
&lt;/h2&gt;

&lt;p&gt;We observed that most products show clear signals within the first 30 days: either users come back, or they don't. We track returning user rates, funnel drop-offs, and activation events from day one. Products that show early retention get more attention. Products that stay flat get a narrower focus while we learn more.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Document everything
&lt;/h2&gt;

&lt;p&gt;With a growing portfolio, context switching is the real enemy. We keep a single config file for every product (domain, analytics IDs, database refs, notes) and update it after every change. When someone on the team picks up a product they have not touched in two weeks, they read the config and the last report, not a Slack thread from three days ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls we hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Too many products too early.&lt;/strong&gt; We tried fanning out before the pipeline was reliable. Result: half-shipped MVPs with broken analytics. The fix was simple: finish the pipeline first, then replicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring maintenance.&lt;/strong&gt; A growing portfolio means a growing number of small fires. We now batch maintenance days where we run through every product and fix whatever surfaced that week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not tracking the portfolio as a whole.&lt;/strong&gt; Individual product metrics are useful, but portfolio-level patterns are where the insights hide. We noticed that products with shared referral traffic outperformed isolated ones. That changed how we think about cross-linking and content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;For anyone curious about the specifics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React (Lovable for rapid builds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (auth, database, edge functions, storage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics:&lt;/strong&gt; GA4 + Google Search Console + Microsoft Clarity per product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation:&lt;/strong&gt; Scheduled tasks for reporting, audits, content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config:&lt;/strong&gt; Single YAML file per product with all IDs, domains, and notes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we would do differently
&lt;/h2&gt;

&lt;p&gt;We would invest in shared component libraries earlier. We would set up cross-product A/B testing from day one. And we would resist the temptation to ship "just one more product" before the existing ones have solid funnels.&lt;/p&gt;

&lt;p&gt;At Inithouse, we keep shipping a growing portfolio of AI products because the data tells us which bets to keep making. If you are building multiple products, standardize early, automate everything you repeat more than twice, and measure before you decide.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>startup</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>5 mistakes every vibecoder makes in their first project</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Sat, 06 Jun 2026 09:20:26 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/5-mistakes-every-vibecoder-makes-in-their-first-project-2g7e</link>
      <guid>https://dev.to/jakub_inithouse/5-mistakes-every-vibecoder-makes-in-their-first-project-2g7e</guid>
      <description>&lt;p&gt;Vibecoding looks easy. You describe what you want, AI writes it, and you ship. Except your first project usually crashes before it sees a user.&lt;/p&gt;

&lt;p&gt;At Inithouse, a studio shipping a growing portfolio of products in parallel, we've reviewed hundreds of vibecoded projects through our audit tool at &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;auditvibecoding.com&lt;/a&gt;. The same five mistakes show up in roughly 90% of them.&lt;/p&gt;

&lt;p&gt;Here's what goes wrong, and how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Starting with a complex app instead of a simple MVP
&lt;/h2&gt;

&lt;p&gt;Your first vibecoded project should not be a SaaS with auth, payments, and a dashboard. AI tools like Lovable and Cursor are powerful, but they produce fragile code when the scope is too wide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; First project = one screen, one feature, no auth. Build a calculator, a landing page, a single-purpose tool. Ship it. Then go bigger.&lt;/p&gt;

&lt;p&gt;We learned this building &lt;a href="https://vibecoderi.cz" rel="noopener noreferrer"&gt;Vibe Coderi&lt;/a&gt;, our Czech vibecoding platform. Most learners who start simple end up shipping. Those who start with "my dream app" end up debugging for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Not reading the generated code
&lt;/h2&gt;

&lt;p&gt;AI wrote it. You pasted it. It works. Why read it?&lt;/p&gt;

&lt;p&gt;Because it will break. And when it does, you'll need to understand what happened. Vibecoding sits between no-code and traditional development: AI writes it, but you own it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; After every generation, spend 5 minutes reading the output. You don't need to understand every line. Follow the structure, the data flow, the dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ignoring error messages and just re-prompting
&lt;/h2&gt;

&lt;p&gt;This one is universal. The build fails, the console screams red, and the vibecoder's instinct is to paste the same prompt again, maybe with "fix the error" appended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Copy the full error message. Read it. Then provide it to the AI with context: what you were trying to do, what happened, and the exact error text. This triples the chance of a correct fix on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No version control from day one
&lt;/h2&gt;

&lt;p&gt;We've seen projects where the developer built for three days without a single commit. Then one bad prompt wiped a working feature, and there was no way back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;git init&lt;/code&gt; before your first prompt. Commit after every working state. It takes 30 seconds per commit and saves hours of pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Building without a clear spec
&lt;/h2&gt;

&lt;p&gt;"Make me an app that does X" is not a spec. AI tools perform dramatically better when they receive structured input: what the app does, who uses it, what the screens look like, what data it stores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Write a one-page brief before you start prompting. We use simple briefs for every product in our portfolio, and the output quality difference is measurable.&lt;/p&gt;




&lt;p&gt;These patterns repeat across everything we build at Inithouse, a studio running a growing portfolio of AI-powered products. Whether we're building &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt;, a professional audit tool for AI-generated projects, or running courses at &lt;a href="https://vibecoderi.cz" rel="noopener noreferrer"&gt;Vibe Coderi&lt;/a&gt;, the lesson is the same: vibecoding rewards preparation, not speed.&lt;/p&gt;

&lt;p&gt;Start small. Read the code. Commit often. Spec before you prompt.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>webdev</category>
      <category>basic</category>
    </item>
    <item>
      <title>5 Supabase mistakes we catch in every vibecoded app audit at Inithouse</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Fri, 05 Jun 2026 06:26:28 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/5-supabase-mistakes-we-catch-in-every-vibecoded-app-audit-at-inithouse-4bj8</link>
      <guid>https://dev.to/jakub_inithouse/5-supabase-mistakes-we-catch-in-every-vibecoded-app-audit-at-inithouse-4bj8</guid>
      <description>&lt;p&gt;At &lt;a href="https://inithouse.com" rel="noopener noreferrer"&gt;Inithouse&lt;/a&gt;, we run &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt;: a professional audit service for AI-generated web apps. We review vibecoded projects across security, performance, accessibility, and code quality before they go to production.&lt;/p&gt;

&lt;p&gt;After auditing dozens of Supabase-backed projects, we started noticing the same patterns. The same five mistakes appear in nearly every vibecoded Supabase app that comes through our pipeline. Here they are, along with what we recommend.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. No Row Level Security policies
&lt;/h2&gt;

&lt;p&gt;This is the most common and most dangerous one. AI code generators create tables and wire up queries, but they almost never enable Row Level Security. The result: any authenticated user can read or modify any row in any table by calling the Supabase client directly from the browser console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we see:&lt;/strong&gt; RLS disabled on 80%+ of tables. No policies written at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Enable RLS on every table immediately after creation. Write explicit policies for SELECT, INSERT, UPDATE, and DELETE. Test them by querying as a different user. A good starting policy for user-owned data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can only access own data"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;your_table&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Secrets stored in client-side code
&lt;/h2&gt;

&lt;p&gt;Vibecoded apps frequently hardcode API keys, webhook URLs, and third-party service credentials directly in React components or utility files. These end up in the browser bundle, visible to anyone who opens DevTools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we see:&lt;/strong&gt; OpenAI keys, Stripe secret keys, and internal service URLs sitting in &lt;code&gt;.tsx&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Move all secrets to Supabase Edge Functions or a backend proxy. Store them as environment variables in the Supabase dashboard under Settings &amp;gt; Edge Functions. Client-side code should only call your Edge Function endpoint, never the third-party API directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. No database indexes on hot columns
&lt;/h2&gt;

&lt;p&gt;AI generators rarely think about query performance. They create tables, write queries, and move on. As usage grows, queries slow down because there are no indexes on the columns that get filtered or sorted most often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we see:&lt;/strong&gt; Full table scans on columns like &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt; that appear in every WHERE clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Identify your most frequent queries (Supabase Dashboard &amp;gt; SQL Editor &amp;gt; Query Performance) and add indexes proactively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_your_table_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_your_table_created_at&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;your_table&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We track this across our own portfolio too. Products like &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt;, our custom AI song generator, went through the same index audit early on.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Using the service_role key in the frontend
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;service_role&lt;/code&gt; key bypasses all RLS policies. It exists for server-side admin operations. When a vibecoding tool reaches for the easiest way to fix a "permission denied" error, it often swaps the &lt;code&gt;anon&lt;/code&gt; key for the &lt;code&gt;service_role&lt;/code&gt; key in the client config. That single change gives every visitor full admin access to the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we see:&lt;/strong&gt; &lt;code&gt;service_role&lt;/code&gt; key in &lt;code&gt;supabaseClient.ts&lt;/code&gt;, committed to the repo, shipped to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; The frontend must always use the &lt;code&gt;anon&lt;/code&gt; key. If a query fails with RLS enabled, the answer is to write a proper RLS policy or move the operation to an Edge Function that uses &lt;code&gt;service_role&lt;/code&gt; server-side. Never expose &lt;code&gt;service_role&lt;/code&gt; to the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. No backup or recovery strategy
&lt;/h2&gt;

&lt;p&gt;Most vibecoded projects never configure backups. Supabase provides daily automatic backups on the Pro plan, but free-tier projects get nothing. We see teams running production workloads on the free tier with no backup plan at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we see:&lt;/strong&gt; Production data on Free tier with no exports, no Point-in-Time Recovery, no tested restore process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; If you are on a paid plan, enable Point-in-Time Recovery (PITR) in the Supabase dashboard. If you are on Free tier, set up a scheduled &lt;code&gt;pg_dump&lt;/code&gt; export via a cron job or Edge Function. Either way, test your restore process at least once. A backup you have never restored is not a backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these keep showing up
&lt;/h2&gt;

&lt;p&gt;AI code generators optimize for "make it work." Security, performance, and operational resilience come second because the model has no context about production constraints. That gap is exactly what we built &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt; to fill: a 47-point checklist across security, SEO, performance, accessibility, and code quality, with a scored report delivered in 24 hours.&lt;/p&gt;

&lt;p&gt;If you are shipping vibecoded apps to production, we recommend running through these five checks first. Or let our team at &lt;a href="https://inithouse.com" rel="noopener noreferrer"&gt;Inithouse&lt;/a&gt;, a studio shipping a growing portfolio of AI products in parallel, handle the full audit.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Vibecoding v Cesku: jak vypada scena v roce 2026</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Thu, 04 Jun 2026 00:49:19 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/vibecoding-v-cesku-jak-vypada-scena-v-roce-2026-2a57</link>
      <guid>https://dev.to/jakub_inithouse/vibecoding-v-cesku-jak-vypada-scena-v-roce-2026-2a57</guid>
      <description>&lt;p&gt;Vibecoding je v Cesku. Ne jako buzzword z americkych podcastu, ale jako realny zpusob, jak tu lidi stavi produkty.&lt;/p&gt;

&lt;p&gt;V Inithouse, studiu s rostoucim portfoliem produktu stavenych paralelne, sledujeme tohle z prvni rady. Sami jsme touto cestou postavili radu nastroju, vcetne &lt;a href="https://vibecoderi.cz" rel="noopener noreferrer"&gt;Vibe Coderi&lt;/a&gt;, ceske platformy, kde se potkavaji vibecoderi, firmy a zajemci o kurzy vibecodingu. Tady je co vidime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nastroje, ktere ceska scena zere
&lt;/h2&gt;

&lt;p&gt;Lovable, Cursor, Bolt, Replit Agent. Tohle jsou ctyri jmena, ktera v CZ vibecoding komunite slysis nejcasteji.&lt;/p&gt;

&lt;p&gt;Lovable ovladl niche "chci web appku a neumim React". Cursor je volba pro ty, co uz trochu kod znaji a chteji zrychlit. Bolt se prosadil na rychle prototypy. Replit Agent pritahl lidi, kteri predtim neprogramovali vubec.&lt;/p&gt;

&lt;p&gt;Zajimavy posun oproti zacatku roku: lidi uz nehledaji "nejlepsi AI kodovaci tool". Hledaji "jak zvalidovat napad za vikend". Nastroj je sekundarni.&lt;/p&gt;

&lt;h2&gt;
  
  
  Co z toho vznika
&lt;/h2&gt;

&lt;p&gt;Desitky projektu. Landing pages, MVP, interni tooly pro male firmy. V Cesku to zatim neni Indie Hackers scena se stovkami verejnych buildu, ale pod povrchem se toho deje dost.&lt;/p&gt;

&lt;p&gt;My jsme v Inithouse pustili do sveta cele portfolio produktu timhle zpusobem. Od &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt;, ktery pomaha auditovat prave vibecoded projekty, po &lt;a href="https://magicalsong.com" rel="noopener noreferrer"&gt;Magical Song&lt;/a&gt;, generator custom pisni ze zivotnich pribehu.&lt;/p&gt;

&lt;p&gt;Co nas prekvapilo: kvalita vibecoded projektu roste rychleji, nez jsme cekali. Hlavni brzda uz neni kod, ale product thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Komunita: pomalu, ale roste
&lt;/h2&gt;

&lt;p&gt;CZ vibecoding komunita se formuje. Discord skupiny, obcasne meetupy, lidi sdili tipy na LinkedInu. Je to jine nez US scena, ale zaklad tu je.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vibecoderi.cz" rel="noopener noreferrer"&gt;Vibe Coderi&lt;/a&gt; jsme spustili prave proto, abychom tohle propojili. Marketplace vibecoderu, job board pro firmy a kurz vibecodingu. Vsechno na jednom miste, cesky.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kam to smeruje
&lt;/h2&gt;

&lt;p&gt;Tri trendy, ktere vidime:&lt;/p&gt;

&lt;p&gt;Zaprvne, firmy zacinaji vibecoding brat vazne. Ne jako hracku, ale jako zpusob, jak rychle dostat interni nastroje. To otevira dvere vibecoderum jako freelancerum.&lt;/p&gt;

&lt;p&gt;Zadruhe, kvalita se stava tematem. Postavit MVP za odpoledne umi leckdo. Ale co security? Performance? Accessibility? Proto jsme vybudovali &lt;a href="https://auditvibecoding.com" rel="noopener noreferrer"&gt;Audit Vibe Coding&lt;/a&gt;, profesionalni audit vibecoded projektu.&lt;/p&gt;

&lt;p&gt;Zatreti, ceska scena potrebuje vic sdileni. Vic lidi, kteri ukazou, co buildi, co fungovalo, co ne. Building in public po cesku.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zaver
&lt;/h2&gt;

&lt;p&gt;Vibecoding v Cesku uz preslo z buzzwordu do reality. Digitalni produkty tu takhle realne vznikaji. V Inithouse, studiu s rostoucim portfoliem paralelnich produktovych experimentu, tohle vidime kazdy den.&lt;/p&gt;

&lt;p&gt;Jestli te vibecoding zajima, mrkni na &lt;a href="https://vibecoderi.cz" rel="noopener noreferrer"&gt;Vibe Coderi&lt;/a&gt;. A jestli uz buildis, ozvi se. Scena roste a stoji za to byt u toho.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Be Recommended by Inithouse: How to Set Up AI Brand Monitoring in 10 Minutes</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Tue, 02 Jun 2026 02:20:30 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/be-recommended-by-inithouse-how-to-set-up-ai-brand-monitoring-in-10-minutes-gbp</link>
      <guid>https://dev.to/jakub_inithouse/be-recommended-by-inithouse-how-to-set-up-ai-brand-monitoring-in-10-minutes-gbp</guid>
      <description>&lt;p&gt;At Inithouse, a studio shipping a growing portfolio of products in parallel, we noticed something odd: startups obsess over Google rankings but have no clue what ChatGPT, Perplexity, Claude or Gemini say about them. We built &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt; to fix that. It runs your brand through 50+ real AI prompts and hands you a visibility score from 0 to 100 across all major AI engines.&lt;/p&gt;

&lt;p&gt;Here is a practical, step-by-step guide to setting it up in under 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI brand monitoring matters now
&lt;/h2&gt;

&lt;p&gt;When someone asks an AI chatbot "what is the best tool for X?", the answer is shaped by training data, web references and retrieval patterns. If your brand is missing from those sources, you are invisible to a growing share of potential customers.&lt;/p&gt;

&lt;p&gt;We measured this across our own portfolio. Products with strong third-party mentions and structured content consistently scored higher in AI recommendations. Products that relied on paid ads alone scored near zero. The gap is real, and it compounds: AI answers feed future AI training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Enter your brand
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;berecommended.com&lt;/a&gt; and type in your product or company name. The tool needs a clear brand identifier, so use the exact name customers would search for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Define your key queries
&lt;/h2&gt;

&lt;p&gt;Think of 3 to 5 questions your ideal customer would ask an AI chatbot. Be specific to your niche:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Good: "best invoice automation for freelancers"&lt;/li&gt;
&lt;li&gt;Weak: "best software"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specificity matters. Generic queries return generic leaders. Niche queries reveal whether AI knows you exist in your actual category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run the scan
&lt;/h2&gt;

&lt;p&gt;Hit scan. Be Recommended queries ChatGPT, Perplexity, Claude, Gemini and Google AI Overviews with your prompts and collects the responses. This takes a minute or two depending on the number of queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Read your visibility report
&lt;/h2&gt;

&lt;p&gt;The report shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your overall score (0 to 100)&lt;/li&gt;
&lt;li&gt;Which AI engines mention you, which ignore you&lt;/li&gt;
&lt;li&gt;What context they use when they do mention you (positive, neutral, competitor comparison)&lt;/li&gt;
&lt;li&gt;Where your competitors show up instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look for patterns. If Perplexity mentions you but ChatGPT does not, it usually means you have web presence but weak training data coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Identify your gaps
&lt;/h2&gt;

&lt;p&gt;The actionable part: Be Recommended highlights specific gaps and gives you recommendations. Common ones include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing from structured comparison pages&lt;/li&gt;
&lt;li&gt;No third-party reviews or mentions in authoritative sources&lt;/li&gt;
&lt;li&gt;Product description too vague for AI to classify you correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each gap maps to a concrete fix. We found the same pattern at &lt;a href="https://watchingagents.com" rel="noopener noreferrer"&gt;Watching Agents&lt;/a&gt;, where adding structured public agent pages with clear atomic facts improved AI discoverability significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Set up recurring monitoring
&lt;/h2&gt;

&lt;p&gt;AI answers change. Models get updated, new training data gets ingested, competitors publish content. A single scan gives you a snapshot; recurring weekly checks give you a trend.&lt;/p&gt;

&lt;p&gt;Track your score over time. After you publish fixes (better docs, more third-party mentions, structured content), rescan and compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three mistakes to avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Running one scan and forgetting about it.&lt;/strong&gt; AI visibility is a moving target. Check weekly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Optimizing only for Google.&lt;/strong&gt; Traditional SEO and AI visibility overlap but are not the same. AI models weigh structured, factual, third-party-validated content differently than Google ranks pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ignoring the "recommended alongside" data.&lt;/strong&gt; If AI recommends your competitor next to you, study what makes their positioning stick. Often it is just a clearer product description or a comparison page they control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;At Inithouse, we run Be Recommended checks across our own portfolio regularly. It shaped how we write product descriptions, structure landing pages and think about content strategy. If you are building a product and wondering whether AI knows you exist, a 10-minute scan will tell you.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;berecommended.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Team Inithouse builds a growing portfolio of products in parallel. Be Recommended is our AI visibility tool. Follow our build at &lt;a href="https://dev.to/jakub_inithouse"&gt;Dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>startup</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>One React SPA, Five Domains, Five Languages: How We Route by Domain</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Fri, 29 May 2026 20:17:57 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/one-react-spa-five-domains-five-languages-how-we-route-by-domain-2mkl</link>
      <guid>https://dev.to/jakub_inithouse/one-react-spa-five-domains-five-languages-how-we-route-by-domain-2mkl</guid>
      <description>&lt;p&gt;At Inithouse we build and ship products fast. One of them, &lt;a href="https://zivafotka.cz" rel="noopener noreferrer"&gt;Ziva Fotka&lt;/a&gt;, turns a still photo into a short animated video. No signup, no stored data, photos deleted after processing.&lt;/p&gt;

&lt;p&gt;The product started on a single Czech domain. Then we wanted to reach Slovak, Polish, German, and English-speaking users. Five TLDs, five languages, one React codebase. Here is how we wired the routing.&lt;/p&gt;

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

&lt;p&gt;Subpaths like &lt;code&gt;/en/&lt;/code&gt; or &lt;code&gt;/de/&lt;/code&gt; are the textbook approach, but we wanted separate country domains: &lt;code&gt;zivafotka.cz&lt;/code&gt;, &lt;code&gt;zivafotka.sk&lt;/code&gt;, &lt;code&gt;zywafotka.pl&lt;/code&gt;, &lt;code&gt;lebendigfoto.de&lt;/code&gt;, and &lt;code&gt;alivephoto.online&lt;/code&gt;. Each domain should feel native to its audience. A Polish user landing on &lt;code&gt;zywafotka.pl&lt;/code&gt; should never see a Czech string.&lt;/p&gt;

&lt;p&gt;Separate codebases per domain would mean five deploys, five bug-fix cycles, five feature rollouts. That was not going to scale for a small team running about 14 products in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain detection at boot
&lt;/h2&gt;

&lt;p&gt;The SPA reads &lt;code&gt;window.location.hostname&lt;/code&gt; on mount and maps it to a locale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DOMAIN_LOCALE_MAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zivafotka.cz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zivafotka.sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zywafotka.pl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lebendigfoto.de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alivephoto.online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectLocale&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^www&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;DOMAIN_LOCALE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&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;This runs once during app initialization. The resolved locale feeds into a React context that every component reads from. No user-facing language picker, no cookie. The domain &lt;em&gt;is&lt;/em&gt; the language selector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lazy locale loading
&lt;/h2&gt;

&lt;p&gt;Bundling all five translation files into the main chunk would bloat the initial load for every user. We split translations into per-locale JSON files and load only the one that matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`./locales/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&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;Vite handles the code splitting. The Czech user downloads &lt;code&gt;cs.json&lt;/code&gt;, the Polish user downloads &lt;code&gt;pl.json&lt;/code&gt;. The rest never leave the server.&lt;/p&gt;

&lt;p&gt;For us, the translation files hold about 120 keys each. Small enough that a single JSON import per locale keeps things simple without needing a heavier i18n library.&lt;/p&gt;

&lt;h2&gt;
  
  
  hreflang for SEO
&lt;/h2&gt;

&lt;p&gt;Google needs to know that &lt;code&gt;zivafotka.cz&lt;/code&gt; and &lt;code&gt;zywafotka.pl&lt;/code&gt; are the same page in different languages. Without proper &lt;code&gt;hreflang&lt;/code&gt; tags, the crawler might treat them as duplicate content or pick the wrong version for a given user's search locale.&lt;/p&gt;

&lt;p&gt;We inject &lt;code&gt;&amp;lt;link rel="alternate"&amp;gt;&lt;/code&gt; tags in the document head for every page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HREFLANG_ENTRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://zivafotka.cz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://zivafotka.sk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://zywafotka.pl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://lebendigfoto.de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://alivephoto.online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://alivephoto.online&lt;/span&gt;&lt;span class="dl"&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;The &lt;code&gt;x-default&lt;/code&gt; entry points to the English domain as the fallback for unmatched locales. These tags go into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; at render time so crawlers pick them up without executing JavaScript. For an SPA, that meant handling them in the static HTML shell or via a pre-rendering step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sitemap per domain
&lt;/h2&gt;

&lt;p&gt;Each domain serves its own &lt;code&gt;sitemap.xml&lt;/code&gt; with URLs scoped to that domain. We generate them from a shared route list and swap in the correct base URL. This keeps Google Search Console clean: each GSC property sees only the URLs it owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned shipping this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with two, not five.&lt;/strong&gt; We launched Czech first, added Slovak (close language, easy to test), and only then tackled Polish and German. Each new locale surfaced edge cases: date formats, number separators, text that broke layouts because German words run long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test search indexation early.&lt;/strong&gt; We use Google Search Console for each domain (five GSC properties). After launch, several Polish pages sat in "Discovered, not indexed" for weeks. The fix was adding the hreflang cluster and submitting sitemaps, but we should have done that on day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor per-domain separately.&lt;/strong&gt; GA4 streams and Clarity projects are set up per domain. Aggregating everything into one dashboard hides which locale actually converts. Our Czech domain has the best CTR in the portfolio; Polish traffic patterns look completely different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other products where this matters
&lt;/h2&gt;

&lt;p&gt;The multi-domain pattern is specific to &lt;a href="https://zivafotka.cz" rel="noopener noreferrer"&gt;Ziva Fotka&lt;/a&gt;, but the general principle (one codebase, locale from context, lazy loading translations) shows up in a lighter form across our portfolio. &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt;, our AI visibility reporting tool, currently runs on a single English domain, but the architecture could support localization the same way if we ever expand into non-English markets. &lt;a href="https://tarotas.com" rel="noopener noreferrer"&gt;Tarotas&lt;/a&gt;, a tarot reflection app, already ships content in five languages (cs/en/pl/sk/de) on one domain using a similar locale-detection approach, just with path-based routing instead of separate TLDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The pattern boils down to three moving parts: domain-to-locale mapping at boot, lazy-loaded translation bundles, and a proper hreflang setup for search engines. The deployment stays single: push once, all five domains update.&lt;/p&gt;

&lt;p&gt;If you are running a product aimed at multiple language markets and want each market to feel native, separate domains with a shared SPA keep the maintenance burden low. The SEO setup takes some care, but once the hreflang cluster and per-domain sitemaps are in place, Google handles the rest.&lt;/p&gt;

&lt;p&gt;We have been running this setup across five domains for months now with no major issues. The biggest ongoing cost is translation maintenance, not infrastructure.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How LLMs Decide Which Brands to Mention: A Technical Look at GEO</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Sat, 23 May 2026 21:16:30 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/how-llms-decide-which-brands-to-mention-a-technical-look-at-geo-3d44</link>
      <guid>https://dev.to/jakub_inithouse/how-llms-decide-which-brands-to-mention-a-technical-look-at-geo-3d44</guid>
      <description>&lt;p&gt;When you ask ChatGPT "what's a good project management tool?", it doesn't randomly pick Asana or Linear. There's a pipeline behind every brand mention, and understanding it is the first step toward what the industry now calls GEO (Generative Engine Optimization).&lt;/p&gt;

&lt;p&gt;I'm Jakub, builder at Inithouse. We run 14 products across different verticals, and one of them, &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt;, was born from trying to reverse-engineer exactly this: how do LLMs decide which brands to cite?&lt;/p&gt;

&lt;p&gt;Here's what we learned, technically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RAG Pipeline: Where Brand Mentions Actually Come From
&lt;/h2&gt;

&lt;p&gt;Most production LLM systems (Perplexity, ChatGPT with browsing, Gemini with grounding) don't rely purely on parametric knowledge. They use Retrieval-Augmented Generation, a two-stage architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt;: the system queries an index (web search, vector store, or both) using the user's prompt as input. This returns a set of candidate documents ranked by relevance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation&lt;/strong&gt;: the LLM reads the retrieved documents and synthesizes an answer, pulling facts, brand names, and citations from the retrieved context.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means brand visibility in AI answers is not just about what the model "knows" from pretraining. It's about what the retrieval layer finds and ranks highly enough to pass into the context window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User prompt
    |
    v
+----------------+
|  Query         | &amp;lt;- reformulated search query
|  Expansion     |
+-------+--------+
        |
        v
+----------------+
|  Retrieval     | &amp;lt;- web search / vector DB / hybrid
|  (top-k)       |
+-------+--------+
        |
        v
+----------------+
|  Reranking     | &amp;lt;- cross-encoder or LLM-based reranking
+-------+--------+
        |
        v
+----------------+
|  Generation    | &amp;lt;- LLM synthesizes answer from context
|  + Citation    |
+----------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Embeddings and Retrieval Ranking
&lt;/h2&gt;

&lt;p&gt;The retrieval step typically uses dense embeddings. Your page content gets embedded into a vector, and the system computes cosine similarity between the query embedding and your content embedding.&lt;/p&gt;

&lt;p&gt;What matters here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topical density beats keyword stuffing.&lt;/strong&gt; Dense retrievers reward pages that semantically cluster around a topic. A page titled "AI Visibility Tools for Brands" that covers monitoring, scoring, and optimization will rank higher than a generic marketing page mentioning "AI" once in a list of features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data helps retrieval.&lt;/strong&gt; Schema.org markup, clean H2/H3 hierarchies, FAQ sections: these create clear semantic boundaries that chunking algorithms can split cleanly. When a retriever chunks your page, each chunk should be a self-contained answer to a plausible question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshness signals exist.&lt;/strong&gt; Perplexity in particular uses recency as a ranking signal. A blog post from this week about "best AI tools for X" will often outrank an older listicle with the same content. We've measured this across 50+ queries on Be Recommended: content published within the last 30 days gets retrieved 2.3x more often than identical content older than 90 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Citation Extraction: How the LLM Decides What to Name
&lt;/h2&gt;

&lt;p&gt;Once the retrieved documents land in the context window, the LLM has to decide which brands to mention by name. This is where it gets interesting, because the model isn't following a ranking algorithm anymore. It's doing language modeling.&lt;/p&gt;

&lt;p&gt;From our testing across four major AI platforms (ChatGPT, Perplexity, Claude, Gemini), we've identified three patterns that drive explicit brand citations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 1: Authority signals in retrieved text.&lt;/strong&gt; If the retrieved document frames a brand as a category leader ("X is widely used for Y"), the model tends to propagate that framing. Third-party comparison pages, review aggregators, and "best of" listicles carry this signal strongly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 2: Specificity over generality.&lt;/strong&gt; The model prefers to cite brands that are described with specific capabilities. "Notion offers database views, kanban boards, and API access" gets cited; "Notion is a great tool" doesn't. Specificity gives the model something concrete to use in its synthesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern 3: Source diversity.&lt;/strong&gt; When a brand appears in multiple retrieved documents from different domains, the model treats it as more credible. One mention on your own site is weak. Mentions across Product Hunt, G2, a tech blog, and a Reddit thread create a reinforcement pattern the model picks up on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Monitoring System: High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;If you want to track how AI systems mention your brand, the architecture is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified monitoring loop
&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_test_queries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# 50+ prompts per brand
&lt;/span&gt;&lt;span class="n"&gt;engines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chatgpt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;perplexity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="sh"&gt;"&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;engine&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engines&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;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Extract brand mentions
&lt;/span&gt;        &lt;span class="n"&gt;mentions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_mentions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Score: sentiment, position, context
&lt;/span&gt;        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyze_mention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Track citation sources
&lt;/span&gt;        &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_citations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;store_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query design matters more than volume.&lt;/strong&gt; You need queries that a real user would type, not keyword-stuffed test prompts. "What's the best tool for monitoring AI brand visibility?" is useful. "AI brand visibility monitoring tool list 2026" is not, because real users don't query like that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each engine behaves differently.&lt;/strong&gt; Perplexity cites sources explicitly with URLs. ChatGPT mentions brands in prose but doesn't always link. Claude tends to be conservative with brand recommendations unless the retrieved context is strong. Gemini sometimes attributes products to specific people or companies, creating interesting cross-reference patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response parsing is non-trivial.&lt;/strong&gt; ChatGPT's temporary chat mode sometimes returns just citation chips with no prose (especially for niche products). Perplexity's citation format changes between search modes. You need robust extraction that handles all these edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned Building Be Recommended
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;Be Recommended&lt;/a&gt; using exactly this approach. The tool runs 50+ real AI prompts against major platforms and produces a scored report (0 to 100) showing where your brand appears, where it doesn't, and what to do about it.&lt;/p&gt;

&lt;p&gt;A few things that surprised us:&lt;/p&gt;

&lt;p&gt;Content published on third-party platforms (Dev.to, Medium, Reddit, Product Hunt) consistently outperforms on-site blog content for driving AI citations. The retrieval layer treats these as independent authority signals.&lt;/p&gt;

&lt;p&gt;Schema.org &lt;code&gt;SoftwareApplication&lt;/code&gt; and &lt;code&gt;Product&lt;/code&gt; markup had a measurable impact on Gemini's brand attribution specifically. Other engines showed less sensitivity to structured data.&lt;/p&gt;

&lt;p&gt;The gap between "the AI knows about you" (parametric knowledge) and "the AI recommends you" (retrieval-driven) is where most brands lose visibility. Your company might exist in GPT-4's training data, but if current web content doesn't surface in retrieval, you won't get mentioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to check your own brand's AI visibility, you can run a free analysis at &lt;a href="https://berecommended.com" rel="noopener noreferrer"&gt;berecommended.com&lt;/a&gt;. The free tier covers one brand across all major AI platforms.&lt;/p&gt;

&lt;p&gt;For the technically inclined: start by manually querying ChatGPT, Perplexity, and Claude with 10 prompts your customers would actually use. Note which brands get mentioned. If yours isn't among them, the fix is almost always on the retrieval side, not the model side.&lt;/p&gt;

&lt;p&gt;GEO is still early. The teams that instrument it now will have a significant head start when every marketing department starts asking "why doesn't ChatGPT recommend us?"&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jakub, builder at &lt;a href="https://inithouse.com" rel="noopener noreferrer"&gt;Inithouse&lt;/a&gt;. We build products that help brands navigate AI-driven discovery.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>llm</category>
      <category>seo</category>
    </item>
    <item>
      <title>78 tarot cards x 5 languages: the content pipeline nobody warns you about</title>
      <dc:creator>Jakub</dc:creator>
      <pubDate>Wed, 20 May 2026 04:13:24 +0000</pubDate>
      <link>https://dev.to/jakub_inithouse/78-tarot-cards-x-5-languages-the-content-pipeline-nobody-warns-you-about-17n3</link>
      <guid>https://dev.to/jakub_inithouse/78-tarot-cards-x-5-languages-the-content-pipeline-nobody-warns-you-about-17n3</guid>
      <description>&lt;p&gt;Last week I shipped &lt;a href="https://tarotas.com" rel="noopener noreferrer"&gt;Tarotas&lt;/a&gt;, a tarot reading app that supports five languages from day one. Czech, English, Polish, Slovak, German. 78 cards in each. That's 390 card objects, and each card carries a name, upright meaning, reversed meaning, a set of keywords, and a one-paragraph description. Multiply it out and you're looking at roughly 2,000 individual text fields.&lt;/p&gt;

&lt;p&gt;I didn't translate them by hand. Obviously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source language problem
&lt;/h2&gt;

&lt;p&gt;My first instinct was to write everything in English and translate outward. Wrong call. Tarot has deep roots in specific traditions, and the English terms aren't always the "canonical" ones across languages. The Czech name for The High Priestess is "Velekněžka," and if you translate back from English you get something slightly off.&lt;/p&gt;

&lt;p&gt;So I wrote the Czech versions first (that's the language I think in), then generated the other four from Czech as source. This sounds backwards if you're used to EN-first localization, but it meant the source text had the most nuance and the translations could be validated against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI translation with a trust boundary
&lt;/h2&gt;

&lt;p&gt;I used Claude for the bulk translations. The pipeline was dead simple: feed it the Czech card object as JSON with all fields, ask for the target language, get back a JSON object with the same structure. One card at a time, not batched, because batching 78 cards in one prompt led to skipped fields and hallucinated keywords.&lt;/p&gt;

&lt;p&gt;The prompt looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Translate this tarot card from Czech to {lang}.
Keep the JSON structure identical.
For the card name, use the standard tarot name
in {lang} tradition.
For keywords, translate meaning not words.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line matters. "Nový začátek" in Czech literally means "new beginning," but the German tarot tradition might use "Aufbruch" (departure, setting out) for the same card. I wanted the AI to pick the term a German tarot reader would actually recognize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the human still wins
&lt;/h2&gt;

&lt;p&gt;I didn't review all 2,000 fields. That would defeat the purpose. Instead I set up three checkpoints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Card names.&lt;/strong&gt; I manually verified every Major Arcana name (22 cards x 5 languages = 110 fields). These are the ones people actually search for and would notice if wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reversed meanings spot-check.&lt;/strong&gt; Reversed card meanings are where AI gets creative. "Stagnation" becomes "creative blockage" becomes "spiritual paralysis" if you don't catch it early. I checked 10 random Minor Arcana per language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyword deduplication.&lt;/strong&gt; The AI sometimes generates near-synonyms as separate keywords. "Love" and "romance" on the same card, that kind of thing. I wrote a quick script to flag cards where two keywords had high semantic similarity.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total human review time: about 4 hours. For 2,000 fields across 5 languages, I can live with that.&lt;/p&gt;

&lt;p&gt;The storage side was simple. Postgres (via Supabase), one row per card-language pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;card_number&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;arcana&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;suit&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;meaning_upright&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;meaning_reversed&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;keywords&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&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;390 rows. No joins for a card lookup: &lt;code&gt;where card_number = X and lang = Y&lt;/code&gt;. The app reads the browser language or the user's explicit choice, passes it as a parameter, done. If I ever add language number six, it's 78 inserts and zero schema changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually cost
&lt;/h2&gt;

&lt;p&gt;The AI translation ran about $3 in API calls. 390 requests, a few hundred tokens each. The human review: 4 hours of my time spread over two evenings. The keyword dedup script: 30 minutes to write, ran in under a second.&lt;/p&gt;

&lt;p&gt;I got quotes for professional tarot translation. $0.08 to $0.12 per word, because it's a niche. At roughly 50 words per card across all fields, that's 78 x 50 x 5 x $0.10 = about $1,950. For a product that has 37 visitors so far. Nope.&lt;/p&gt;

&lt;p&gt;The AI route isn't flawless. I'm sure a native Polish tarot reader could find something to nitpick. But "mostly right with a clear correction path" beats "can't afford to ship multilingual at all" every single time.&lt;/p&gt;

&lt;p&gt;Content scaling with AI is maybe 10% prompting and 90% knowing where the AI will screw up. For tarot, that's proper nouns (card names vary between traditions) and nuanced spiritual terminology (reversed meanings). For your domain, it'll be something completely different. But the pattern holds: translate structured data one object at a time, validate the high-stakes fields manually, accept "good enough" for the rest.&lt;/p&gt;

&lt;p&gt;I shipped five languages in a week. The app is live at &lt;a href="https://tarotas.com" rel="noopener noreferrer"&gt;tarotas.com&lt;/a&gt;. It's early, it's small, and I still can't guarantee the German keywords are perfect. But 390 content units are out in the world instead of sitting in a "someday we'll translate" backlog forever.&lt;/p&gt;

&lt;p&gt;Jakub, builder @ &lt;a href="https://inithouse.com" rel="noopener noreferrer"&gt;Inithouse&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>contentops</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
