<?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>Forem: Adam Grenier</title>
    <description>The latest articles on Forem by Adam Grenier (@aimatey).</description>
    <link>https://forem.com/aimatey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F705989%2Fa8f4f900-f02c-4a9d-b59d-64669e77e336.png</url>
      <title>Forem: Adam Grenier</title>
      <link>https://forem.com/aimatey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/aimatey"/>
    <language>en</language>
    <item>
      <title>The SEO Fix Your Vibe‑Coded Vite/React Site Needs (Without Next.js)</title>
      <dc:creator>Adam Grenier</dc:creator>
      <pubDate>Fri, 27 Mar 2026 07:18:42 +0000</pubDate>
      <link>https://forem.com/aimatey/the-seo-fix-your-vibe-coded-vitereact-site-needs-without-nextjs-4144</link>
      <guid>https://forem.com/aimatey/the-seo-fix-your-vibe-coded-vitereact-site-needs-without-nextjs-4144</guid>
      <description>&lt;p&gt;If you’ve ever vibe‑coded your way to a “pretty good” SaaS or landing page, shipped it, and then checked Google Search Console… you’ve probably seen it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovered – currently not indexed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I was reminded of that wall with &lt;a href="https://smileplease.app" rel="noopener noreferrer"&gt;SmilePlease&lt;/a&gt;, an AI tool that generates professional school portraits from everyday photos. I built it the way a lot of modern apps get built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vite + React
&lt;/li&gt;
&lt;li&gt;Supabase on the backend
&lt;/li&gt;
&lt;li&gt;Deployed to Railway in an afternoon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site was fast. The UX felt great. But Googlebot? Googlebot basically pretended it didn’t exist.&lt;/p&gt;

&lt;p&gt;Out of 36 known URLs, exactly &lt;strong&gt;one&lt;/strong&gt; page was indexed.&lt;/p&gt;

&lt;p&gt;This post walks through how to fix that by adding build‑time prerendering to a Vite/React SPA—without rewriting everything in Next.js or Remix. If you’ve vibe‑coded a Vite app and are only now realizing SEO matters, this is for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Google Ghosted My SPA
&lt;/h2&gt;

&lt;p&gt;A classic Vite/React SPA ships an almost‑empty &lt;code&gt;index.html&lt;/code&gt; that looks something like this:&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;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/src/main.tsx"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All your real content only exists &lt;strong&gt;after&lt;/strong&gt; the JavaScript bundle runs, mounts React, fetches data, and renders the app.&lt;/p&gt;

&lt;p&gt;Google &lt;em&gt;can&lt;/em&gt; render JavaScript, but it doesn’t do that up front for every URL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, it fetches the HTML and parses whatever it finds.
&lt;/li&gt;
&lt;li&gt;Then, if the page looks “worth it,” it gets scheduled for a second pass where Google executes your JS and sees the full DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your domain is brand new and has basically no authority (no backlinks, no history), those JS‑heavy pages sit in a low‑priority queue. Some of them never get rendered at all.&lt;/p&gt;

&lt;p&gt;In my case, I made things worse with a robots.txt own‑goal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Disallow: /assets/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite was outputting all of my JS and CSS into &lt;code&gt;/assets/&lt;/code&gt;. By disallowing that folder, I was literally telling Google:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“ Please don’t download the code you’d need to see what’s on my site.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML files with no meaningful content
&lt;/li&gt;
&lt;li&gt;JavaScript that Googlebot wasn’t allowed to fetch
&lt;/li&gt;
&lt;li&gt;A brand new domain with zero authority&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect recipe for “Discovered – currently not indexed.”&lt;/p&gt;




&lt;h2&gt;
  
  
  The Standard Advice (That I Didn’t Want to Take)
&lt;/h2&gt;

&lt;p&gt;The common advice for “React SPA SEO” is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Just migrate to Next.js or Remix and use SSR.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is good advice &lt;em&gt;if&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’re early in the project, or
&lt;/li&gt;
&lt;li&gt;You want the framework features and are ready to refactor routing, data fetching, and deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wasn’t there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app worked.
&lt;/li&gt;
&lt;li&gt;The stack has some baked complexity.
&lt;/li&gt;
&lt;li&gt;I didn’t want to rebuild the routing layer or re‑thread my Supabase client through SSR entry points.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kept my Vite/React SPA as‑is
&lt;/li&gt;
&lt;li&gt;Required minimal code changes
&lt;/li&gt;
&lt;li&gt;Gave Google fully rendered HTML &lt;em&gt;at crawl time&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of switching frameworks, I went with &lt;strong&gt;build‑time prerendering&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Strategy: Build‑Time Prerendering
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build your SPA as usual with Vite (this is a go-to for most of the current vibe-coding tools like Lovable, Gemini Studio, Replit, Bolt, etc.).&lt;/li&gt;
&lt;li&gt;Spin up a tiny web server that serves the built &lt;code&gt;dist&lt;/code&gt; folder.
&lt;/li&gt;
&lt;li&gt;Use a headless browser (Puppeteer) to visit each important route.
&lt;/li&gt;
&lt;li&gt;Grab the fully rendered HTML from the page.
&lt;/li&gt;
&lt;li&gt;Save that HTML back into &lt;code&gt;dist&lt;/code&gt; as static &lt;code&gt;.html&lt;/code&gt; files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When Googlebot (or any non‑JS client) requests &lt;code&gt;/pricing&lt;/code&gt;, your host serves the static HTML we prerendered. The browser can still load your JS and hydrate the page into a full SPA once it’s loaded.&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static‑site behavior for crawlers and no‑JS users
&lt;/li&gt;
&lt;li&gt;SPA behavior for everyone else
&lt;/li&gt;
&lt;li&gt;No framework rewrite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I wired it up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Make React Hydrate Instead of Re‑Render
&lt;/h2&gt;

&lt;p&gt;If you dump fully rendered HTML into &lt;code&gt;index.html&lt;/code&gt; but keep doing a normal &lt;code&gt;createRoot().render(&amp;lt;App /&amp;gt;)&lt;/code&gt;, React will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throw away the HTML you prerendered
&lt;/li&gt;
&lt;li&gt;Rebuild the DOM from scratch on the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We want React to &lt;strong&gt;hydrate&lt;/strong&gt; the existing DOM instead.&lt;/p&gt;

&lt;p&gt;In your Vite entry file (often &lt;code&gt;main.tsx&lt;/code&gt; or &lt;code&gt;main.jsx&lt;/code&gt;), swap the mount logic to preferred hydration when the root already has content:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hydrateRoot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootElement&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&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="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If the root element has children, the page was prerendered&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;rootElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasChildNodes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;hydrateRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Standard SPA mounting for non-prerendered pages&lt;/span&gt;
    &lt;span class="nf"&gt;createRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the prerendered HTML intact and layers React’s event system on top of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Write a Puppeteer Prerender Script
&lt;/h2&gt;

&lt;p&gt;Next, we’ll write a Node script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts an Express server to serve &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Uses Puppeteer to open each route
&lt;/li&gt;
&lt;li&gt;Waits for network requests to finish
&lt;/li&gt;
&lt;li&gt;Saves the final HTML to disk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create something like &lt;code&gt;scripts/prerender.ts&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dirname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DIST_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3005&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The routes you care about for SEO.&lt;/span&gt;
&lt;span class="c1"&gt;// Keep this focused: home, pricing, features, blog posts, etc.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&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;/&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;/pricing&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;/about&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;/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;prerender&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Start a local server that serves the built assets&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// SPA fallback: any unknown route serves index.html&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&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;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[prerender] Server listening on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Launch headless Chrome&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Optional: set a consistent viewport / user agent&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setViewport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Visit each route and save its HTML&lt;/span&gt;
    &lt;span class="k"&gt;for &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;route&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[prerender] Visiting &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Wait until network is idle to ensure data has loaded&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&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;isRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isRoot&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DIST_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[prerender] Wrote &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;prerender&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="nx"&gt;err&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[prerender] Error during prerender:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;ROUTES&lt;/code&gt; small and focused on your SEO‑critical pages; every route adds build time.
&lt;/li&gt;
&lt;li&gt;If your app relies on auth, feature flags, or environment‑specific data, you can inject cookies or query params before calling &lt;code&gt;page.goto&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Wire It Into Your Build
&lt;/h2&gt;

&lt;p&gt;Now we want this script to run automatically whenever we build for production.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build &amp;amp;&amp;amp; tsx scripts/prerender.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite preview"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vite build&lt;/code&gt; runs first and outputs your SPA into &lt;code&gt;dist&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsx scripts/prerender.ts&lt;/code&gt; runs next, starts the local server, prerenders routes, and overwrites/creates HTML files in &lt;code&gt;dist&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;On Railway, set your build command to &lt;code&gt;\pnpm run build&lt;/code&gt; in the project settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re deploying somewhere else (Vercel, Netlify, Render, plain CI + S3), the idea is the same: make sure this script runs in your build step, &lt;em&gt;not&lt;/em&gt; at request time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Fix Your robots.txt (If You Messed It Up Like I Did)
&lt;/h2&gt;

&lt;p&gt;Finally, make sure you’re not accidentally blocking the very assets Google needs to render your app.&lt;/p&gt;

&lt;p&gt;A safe starting point is something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /

Sitemap: https://your-domain.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to block specific URLs or paths, target those explicitly, but don’t broad‑brush disallow your JS/CSS folders.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed After Prerendering
&lt;/h2&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removing the &lt;code&gt;/assets/&lt;/code&gt; block from &lt;code&gt;robots.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding the prerender step to the Vite build
&lt;/li&gt;
&lt;li&gt;Letting Google recrawl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I went from “one lonely indexed page” to seeing my key marketing URLs actually appear in the index. New URLs stopped getting stuck in “Discovered – currently not indexed” purgatory.&lt;/p&gt;

&lt;p&gt;Just as importantly, this worked &lt;strong&gt;without&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrating to Next.js/Remix
&lt;/li&gt;
&lt;li&gt;Rewriting routing
&lt;/li&gt;
&lt;li&gt;Changing my hosting setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Google and non‑JS clients, SmilePlease behaves like a static site. For users with JS, it behaves like the vibe‑coded SPA I originally shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  When This Approach Makes Sense
&lt;/h2&gt;

&lt;p&gt;Build‑time prerendering is a great fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have a Vite/React SPA in production.
&lt;/li&gt;
&lt;li&gt;You’re seeing indexing issues or thin‑content warnings.
&lt;/li&gt;
&lt;li&gt;You can afford slightly slower builds in exchange for better SEO.
&lt;/li&gt;
&lt;li&gt;You don’t want to overhaul your stack just to get server‑rendered HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s less ideal if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a ton of highly dynamic, user‑specific pages.
&lt;/li&gt;
&lt;li&gt;Your content changes on every request.
&lt;/li&gt;
&lt;li&gt;You need real‑time personalized HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, SSR or a hybrid framework may be the better long‑term path.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR for Vibe Coders
&lt;/h2&gt;

&lt;p&gt;If you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clicked a few buttons in an AI builder
&lt;/li&gt;
&lt;li&gt;Ended up with a Vite/React app on Railway
&lt;/li&gt;
&lt;li&gt;Are now staring at “Discovered – currently not indexed” in Search Console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t have to rewrite everything in Next.js to fix it.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make React hydrate pre‑rendered HTML instead of re‑rendering.
&lt;/li&gt;
&lt;li&gt;Add a Puppeteer prerender step after &lt;code&gt;vite build&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Focus that prerender on your key marketing routes.
&lt;/li&gt;
&lt;li&gt;Make sure robots.txt isn’t blocking your JS/CSS.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And suddenly, your vibe‑coded Vite site behaves like a real, crawlable website in the eyes of Google—without killing the vibes that got you shipping in the first place.&lt;/p&gt;




&lt;p&gt;If you end up wiring this into your own Vite app, I’d love to hear which platform you built on (v0, Bolt, Lovable, hand‑rolled, etc.) and whether Google started indexing your pages after the change.&lt;/p&gt;

</description>
      <category>vite</category>
      <category>seo</category>
      <category>vibecoding</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Built a Synthetic Goat Scream Pipeline (And the Tools Along the Way)</title>
      <dc:creator>Adam Grenier</dc:creator>
      <pubDate>Wed, 18 Feb 2026 23:29:58 +0000</pubDate>
      <link>https://forem.com/aimatey/how-i-built-a-synthetic-goat-scream-pipeline-and-the-tools-along-the-way-1aln</link>
      <guid>https://forem.com/aimatey/how-i-built-a-synthetic-goat-scream-pipeline-and-the-tools-along-the-way-1aln</guid>
      <description>&lt;h2&gt;
  
  
  Goats?
&lt;/h2&gt;

&lt;p&gt;I needed goat screams. Hundreds of them. High quality. Labeled with musical notes.&lt;/p&gt;

&lt;p&gt;This is the story of the tools I built to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Origin: Build Something Weird Enough to Ship
&lt;/h2&gt;

&lt;p&gt;I wanted to learn API development. Not another todo app. Something fun enough to keep me motivated.&lt;/p&gt;

&lt;p&gt;In a hunt for silly APIs, I came across the &lt;a href="https://owen-wilson-wow-api.onrender.com/" rel="noopener noreferrer"&gt;Owen Wilson Whoa API&lt;/a&gt;, and it inspired me. Truly. I remembered the goat scream meme era from YouTube. Those viral compilations of goats screaming like humans. Perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: YouTube Scraping
&lt;/h2&gt;

&lt;p&gt;First attempt: find videos, download them, and agents clip the screams.&lt;/p&gt;

&lt;p&gt;This was slow. And the results were rough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Background music (Taylor Swift goat remixes, anyone?)&lt;/li&gt;
&lt;li&gt;People laughing over the screams&lt;/li&gt;
&lt;li&gt;Cuts in weird places&lt;/li&gt;
&lt;li&gt;Duplicate screams across videos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool #1: The Extract-o-matic
&lt;/h2&gt;

&lt;p&gt;I built a Streamlit app that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes a list of YouTube URLs&lt;/li&gt;
&lt;li&gt;Downloads the audio&lt;/li&gt;
&lt;li&gt;Detects audio patterns that match goat screams (amplitude spikes, frequency ranges)&lt;/li&gt;
&lt;li&gt;Auto-clips them into individual MP3s&lt;/li&gt;
&lt;li&gt;Generates basic metadata (source video, timestamp, duration)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;`python&lt;/p&gt;

&lt;h3&gt;
  
  
  Simplified detection logic
&lt;/h3&gt;

&lt;p&gt;def detect_screams(audio_path):&lt;br&gt;
    y, sr = librosa.load(audio_path)&lt;br&gt;
    # Look for sudden amplitude spikes in goat frequency range&lt;br&gt;
    onset_frames = librosa.onset.onset_detect(y=y, sr=sr)&lt;br&gt;
    # Filter for goat-like frequency characteristics&lt;br&gt;
    # ... (frequency analysis)&lt;br&gt;
    return scream_timestamps`&lt;/p&gt;

&lt;p&gt;This got me from hours of manual work to minutes. But the quality was still inconsistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Synthetic Screams
&lt;/h2&gt;

&lt;p&gt;The YouTube clips had too much noise. I needed cleaner source material.&lt;/p&gt;

&lt;p&gt;Stock audio sites had some goat sounds, but not enough variety. So I turned to AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ElevenLabs Discovery
&lt;/h3&gt;

&lt;p&gt;I tried several platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI voice generation&lt;/li&gt;
&lt;li&gt;Various sound effect models (Adobe SFX Generator, Kling’s AI SFX Generator, PopPop, My Edit, Elevenlabs AI SFX Generator, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ElevenLabs won. Their sound effects tool generates clips that actually sounded indistinguishable from real goats from youtube. It also worked best with hilarious prompts...&lt;/p&gt;

&lt;h3&gt;
  
  
  The Prompting Insight
&lt;/h3&gt;

&lt;p&gt;"Goat scream" as a prompt was inconsistent.&lt;/p&gt;

&lt;p&gt;What worked better: &lt;strong&gt;describing screamable experiences with a goat at the center.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Successful prompts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Goat yells in horror at a spider because it hates spiders"&lt;/li&gt;
&lt;li&gt;"A dramatic goat yelling protest while being ridiculed"&lt;/li&gt;
&lt;li&gt;"A goat stubs its toe and screams like crazy"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Less successful:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Goat scream"&lt;/li&gt;
&lt;li&gt;"Goat noise"&lt;/li&gt;
&lt;li&gt;"Goat sound effect"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The context gave the model something to work with. Emotional state + situation = better variety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: The Sora Hack
&lt;/h2&gt;

&lt;p&gt;Then Sora came out.&lt;/p&gt;

&lt;p&gt;I was playing with video generation when I had an idea: what if I could get 15 seconds of goat screams in one generation?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prompt that worked:&lt;/strong&gt; "Montage of goats screaming"&lt;/p&gt;

&lt;p&gt;Sora would generate a compilation-style video with multiple goats, multiple screams, natural variety. I'd feed the audio through my Extract-o-matic and get 5-10 clean clips per video.&lt;/p&gt;

&lt;p&gt;I tried other video platforms (Veo, Wan, etc.) but Sora's audio quality was best for this use case.&lt;/p&gt;

&lt;p&gt;Soon, I had as many (if not more) synthetic screams than real ones. And when I played them back-to-back, I couldn't reliably tell which were which—unless I had a specific memory attached to a real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quality Problem
&lt;/h2&gt;

&lt;p&gt;I now had hundreds of clips. But many were still rough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weird cutoffs&lt;/li&gt;
&lt;li&gt;Non-goat sounds that slipped through&lt;/li&gt;
&lt;li&gt;Duplicates&lt;/li&gt;
&lt;li&gt;Clips that needed manual trimming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed to audit them. Fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool #2: The Goat Screams Auditor
&lt;/h2&gt;

&lt;p&gt;I needed a faster review workflow, so I built a swipe-style QA interface for audio clips, inspired by a hiring app we built in growth at Uber.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Goat Screams Auditor:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shows one clip at a time with a media player&lt;/li&gt;
&lt;li&gt;Keyboard shortcuts: &lt;code&gt;→&lt;/code&gt; next, &lt;code&gt;G&lt;/code&gt; good, &lt;code&gt;B&lt;/code&gt; bad, &lt;code&gt;N&lt;/code&gt; not a scream&lt;/li&gt;
&lt;li&gt;Auto-hides after rating&lt;/li&gt;
&lt;li&gt;Optional notes field&lt;/li&gt;
&lt;li&gt;Exports clean lists: good files, bad files, needs-editing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I breezed through the entire library. The good ones went to the API. The bad ones got deleted. The "needs editing" pile went to Audacity for manual cleanup when I had time (which I haven't yet, and doesn't matter).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Music Problem
&lt;/h2&gt;

&lt;p&gt;I wanted to build a beatbox (bleatbox, if you will) where you could make beats with goat screams.&lt;/p&gt;

&lt;p&gt;First attempt: random screams on a grid.&lt;/p&gt;

&lt;p&gt;Result: absolute chaos. Not fun. Just noise.&lt;/p&gt;

&lt;p&gt;The screams needed musical structure. I needed to know what note each goat was hitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool #3: Wannableat
&lt;/h2&gt;

&lt;p&gt;(If you wannableat my lover...)&lt;/p&gt;

&lt;p&gt;Wannableat is a pitch analyzer that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes an audio file&lt;/li&gt;
&lt;li&gt;Runs pitch detection (using &lt;code&gt;librosa&lt;/code&gt; and &lt;code&gt;crepe&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Identifies the primary musical note&lt;/li&gt;
&lt;li&gt;Maps the tone sequence throughout the scream&lt;/li&gt;
&lt;li&gt;Outputs metadata for the API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;python&lt;br&gt;
def analyze_pitch(audio_path):&lt;br&gt;
    y, sr = librosa.load(audio_path)&lt;br&gt;
    # Using CREPE for more accurate pitch detection&lt;br&gt;
    time, frequency, confidence, activation = crepe.predict(y, sr)&lt;br&gt;
    # Convert frequencies to musical notes&lt;br&gt;
    notes = [frequency_to_note(f) for f in frequency if confidence &amp;gt; 0.5]&lt;br&gt;
    primary_note = most_common(notes)&lt;br&gt;
    return {&lt;br&gt;
        "primary_note": primary_note,&lt;br&gt;
        "tones_in_order": notes,&lt;br&gt;
        "confidence": avg(confidence)&lt;br&gt;
    }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fun finding:&lt;/strong&gt; Goat screams cluster around G#5 and A5. Something about goat vocal cords, I guess.&lt;/p&gt;

&lt;p&gt;With pitch data, I could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query screams by musical note&lt;/li&gt;
&lt;li&gt;Build an actual playable instrument&lt;/li&gt;
&lt;li&gt;Pitch-shift screams to hit specific notes&lt;/li&gt;
&lt;li&gt;Create melodies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Goat Screams went from chaos to... slightly musical chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flywheel
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting.&lt;/p&gt;

&lt;p&gt;One of the apps I built is &lt;strong&gt;Goat I-Scream&lt;/strong&gt;—it takes a photo of you and generates a video of you transforming into a screaming goat (using Veo).&lt;/p&gt;

&lt;p&gt;The audio from those generations? It's a goat scream.&lt;/p&gt;

&lt;p&gt;So now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User makes a personal Goat Screams video&lt;/li&gt;
&lt;li&gt;Audio gets extracted and stored&lt;/li&gt;
&lt;li&gt;Runs through Wannableat for pitch analysis&lt;/li&gt;
&lt;li&gt;Gets added to the API&lt;/li&gt;
&lt;li&gt;Feeds more beats in Goat Screams&lt;/li&gt;
&lt;li&gt;Attracts more users&lt;/li&gt;
&lt;li&gt;Who make more Goat Screams videos&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The API feeds itself. Network effects, but for goat screams. Which probably doesn't need network effects, but it's kinda my jam.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silly projects &amp;gt; serious projects&lt;/strong&gt; for learning. I actually finished this one, and had a blast building the features and tools along the way. I didn't have a revenue idea for API (yet), so while this annoyed my family, it delighted me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Synthetic data is underrated.&lt;/strong&gt; The AI screams are indistinguishable from real ones. And I could generate variety on demand. This is a hot topic, and I'm fully on board.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context Engineering &amp;gt; Prompt Engineering = Gold.&lt;/strong&gt; "Goat scream" vs "goat screaming when it accidentally touches a cactus" comes from two completely unrelated contexts I had (goats + things that make me scream). Combining the contexts produces more creative results. Think beyond the context in front of you to raise the bar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build tools for your tools.&lt;/strong&gt; The Extract-o-matic, Auditor, and Wannableat each saved hours. The meta-work was worth it. And I never really thought about music analysis and now I have three additional ideas I could fork off of it, just because I built it for goats.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Steal workflows from other domains.&lt;/strong&gt; The Tinder-style auditor came from a hiring tool we built at Uber. The pitch analyzer came from music production. Cross-pollination works.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;I'm cleaning up and will open source the side tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract-o-matic (Streamlit audio clipper)&lt;/li&gt;
&lt;li&gt;Goat Screams Auditor (rapid QA interface)&lt;/li&gt;
&lt;li&gt;Wannableat (pitch analyzer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're goat-themed, but the patterns work for any audio classification/processing pipeline.&lt;/p&gt;

&lt;p&gt;And I'll be launching a more consumer facing app with lots of fun AI micro-apps I built on top of GoatScreams API. &lt;/p&gt;

&lt;p&gt;Hold your butts...&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt;: &lt;a href="https://www.goatscreams.com/developer" rel="noopener noreferrer"&gt;goatscreams.com/developer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://api.goatscreams.com/docs" rel="noopener noreferrer"&gt;api.goatscreams.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AIMateyApps/goat-scream-api" rel="noopener noreferrer"&gt;github.com/AIMateyApps/goat-scream-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waitlist&lt;/strong&gt; (apps launching soon): &lt;a href="https://goatscreams.com/waitlist" rel="noopener noreferrer"&gt;goatscreams.com/waitlist&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>funny</category>
      <category>api</category>
      <category>infrastructure</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Built a VSCode Extension to Learn How VSCode Extensions Work</title>
      <dc:creator>Adam Grenier</dc:creator>
      <pubDate>Tue, 13 Jan 2026 20:24:11 +0000</pubDate>
      <link>https://forem.com/aimatey/i-built-a-vscode-extension-to-learn-how-vscode-extensions-work-3197</link>
      <guid>https://forem.com/aimatey/i-built-a-vscode-extension-to-learn-how-vscode-extensions-work-3197</guid>
      <description>&lt;p&gt;I learn by building. Always have.&lt;/p&gt;

&lt;p&gt;I also think VSCode is an under-the-radar environment ripe for innovation in coding and beyond.&lt;/p&gt;

&lt;p&gt;I'd been wanting to understand how VSCode extensions work for years, but the docs felt scattered. So instead of just reading, I built one and documented everything along the way.&lt;/p&gt;

&lt;p&gt;The extension itself is a bit fun: it generates 5-7-5 haiku commit messages from your staged git changes.&lt;/p&gt;

&lt;p&gt;Stage your code, press &lt;code&gt;Cmd+Shift+H&lt;/code&gt;, and get something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Config paths diverge&lt;br&gt;
Settings flow through typed channels&lt;br&gt;
Defaults guide the lost&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07deuyjp2y46zv8pivik.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07deuyjp2y46zv8pivik.gif" alt=" " width="720" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  But the haiku thing isn't really the point.
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted to learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How VSCode commands work&lt;/li&gt;
&lt;li&gt;How to integrate with the Source Control panel&lt;/li&gt;
&lt;li&gt;How settings and configuration are handled&lt;/li&gt;
&lt;li&gt;How to work with multiple AI providers (Claude, GPT-5, Gemini)&lt;/li&gt;
&lt;li&gt;How to publish to the VS Code Marketplace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reading docs only gets you so far. Building something real forces you to solve actual problems.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;1. Commands are simpler than expected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Registering a command is just:&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;disposable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;575.haikuCommit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="c1"&gt;// your logic here&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complexity is in what happens inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. SCM integration has quirks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting the staged diff requires working with VSCode's git extension API. It's not hard, but it's not obvious either. The repo has examples of how to do this cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Settings are powerful but verbose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;VSCode's configuration system is robust, but defining settings in &lt;code&gt;package.json&lt;/code&gt; gets lengthy fast. I ended up with settings for API keys, provider selection, model overrides, retry limits, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Error handling matters more than you'd think&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI APIs fail. Rate limits hit. Networks timeout. The extension retries with corrective guidance if the haiku syllable count is wrong. This retry logic was more interesting to build than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Repo as a Learning Resource
&lt;/h2&gt;

&lt;p&gt;I structured the codebase so others can learn from it too.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;docs/GUIDES.md&lt;/code&gt;, there are step-by-step guides covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to template this repo into your own extension&lt;/li&gt;
&lt;li&gt;How commands and keyboard shortcuts work&lt;/li&gt;
&lt;li&gt;How to integrate with Source Control&lt;/li&gt;
&lt;li&gt;How settings and configuration management works&lt;/li&gt;
&lt;li&gt;How to abstract multiple API providers&lt;/li&gt;
&lt;li&gt;How to prep for release and publish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've wanted to build a VSCode extension but didn't know where to start, clone the repo and poke around.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Is the First of Many
&lt;/h2&gt;

&lt;p&gt;I believe the best way to understand AI (and most things in life) isn't reading about it — it's getting your hands dirty, and building. &lt;/p&gt;

&lt;p&gt;This is the first "build to learn" project I'm sharing publicly. More coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It / Learn From It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace:&lt;/strong&gt; &lt;a href="https://marketplace.visualstudio.com/items?itemName=AIMatey.575-haiku-commit" rel="noopener noreferrer"&gt;575 Haiku Commit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AIMateyApps/575-haiku-commit" rel="noopener noreferrer"&gt;AIMateyApps/575-haiku-commit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Clone it, break it, build your own thing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's something you've been wanting to build and learn from, but haven't started yet?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
