<?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: dundun sun</title>
    <description>The latest articles on DEV Community by dundun sun (@dundunup).</description>
    <link>https://dev.to/dundunup</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%2F3949859%2Ff91f1c89-5cb2-4554-921f-eafd99a804d1.jpg</url>
      <title>DEV Community: dundun sun</title>
      <link>https://dev.to/dundunup</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dundunup"/>
    <language>en</language>
    <item>
      <title>Title: Building a Zero-Backend AI Stock Bubble Tracker: Architecture, Web Scraping, and Performance Lessons</title>
      <dc:creator>dundun sun</dc:creator>
      <pubDate>Thu, 23 Jul 2026 02:17:36 +0000</pubDate>
      <link>https://dev.to/dundunup/title-building-a-zero-backend-ai-stock-bubble-tracker-architecture-web-scraping-and-performance-4o7k</link>
      <guid>https://dev.to/dundunup/title-building-a-zero-backend-ai-stock-bubble-tracker-architecture-web-scraping-and-performance-4o7k</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdwsm9m5aqm6cz5qp6zt.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdwsm9m5aqm6cz5qp6zt.jpg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every time I open X (formerly Twitter) or Hacker News, I see developers arguing about the AI ecosystem. One camp claims we’re in a permanent super-cycle, while the other insists that massive GPU Capex without proportional software revenue will trigger a dot-com-style market crash.&lt;/p&gt;

&lt;p&gt;Instead of taking sides on social media, I decided to do what any sane software engineer would do: &lt;strong&gt;build a lightweight, real-time metrics dashboard to track the data myself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;&lt;a href="https://aistockbubbleindex.com" rel="noopener noreferrer"&gt;AI Stock Bubble Index&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I want to walk you through the architectural decisions, client-side performance tricks, data pipeline challenges (like crawling Google News RSS via local proxies without getting rate-limited), and why I chose a completely zero-backend / JAMstack approach for a data-heavy utility site.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal: Low Latency, Zero Server Costs, Max Scannability
&lt;/h2&gt;

&lt;p&gt;When I set out to build this dashboard, I had a strict set of architectural rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero Database Overheads:&lt;/strong&gt; I didn't want to maintain a Postgres or MongoDB instance just to serve static or semi-static analytical metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-second Page Loads:&lt;/strong&gt; Financial dashboards are notoriously bloated. Most stock portals load 40MB of tracking scripts and take five seconds to render a single chart. My site needed to feel instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flawless Core Web Vitals:&lt;/strong&gt; Cumulative Layout Shift (CLS) needed to be zero, and First Contentful Paint (FCP) had to stay under 500ms on low-end mobile devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Data Discovery:&lt;/strong&gt; News headlines, P/E ratio shifts, and sentiment metrics needed to be updated asynchronously via simple, build-time or client-side JSON hydration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s how the architecture looks and the engineering lessons learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural Overview
&lt;/h2&gt;

&lt;p&gt;The stack is deceptively simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React (SPA) with component-driven state management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component Library:&lt;/strong&gt; Ant Design, custom-shaken to ensure minimal CSS and JS bundle footprints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Layer:&lt;/strong&gt; Asynchronously generated, statically served JSON schemas hosted via CDN endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrapers &amp;amp; Aggregators:&lt;/strong&gt; Node.js (ESM) automation scripts that fetch RSS discovery candidate streams, parse XML, score relevance via term-matching algorithms, and generate structured data payloads.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│ Google News / Financial Feeds │
└───────────────┬───────────────┘
                │
                ▼ (Node.js ESM Scraper + Proxy)
┌───────────────────────────────┐
│ Term Matching &amp;amp; Weighting     │
└───────────────┬───────────────┘
                │
                ▼
┌───────────────────────────────┐
│ JSON Static Assets (Data CDN) │
└───────────────┬───────────────┘
                │
                ▼ (React Hydration)
┌───────────────────────────────┐
│  AI Stock Bubble UI Dashboard │
└───────────────────────────────┘

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Technical Challenge 1: Fetching External Feeds Without Bottlenecks or Proxy Failures
&lt;/h2&gt;

&lt;p&gt;One of the first features of the platform was an automated news discovery tool that aggregates headlines regarding AI Capex, valuations, revenue, and model releases.&lt;/p&gt;

&lt;p&gt;If you’ve ever tried scripting Node.js to fetch Google News RSS feeds (&lt;code&gt;[https://news.google.com/rss/search?q=](https://news.google.com/rss/search?q=)...&lt;/code&gt;), you know it is a minefield.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson A: Node.js &lt;code&gt;fetch&lt;/code&gt; (Undici) Ignores Native Proxy Environment Variables
&lt;/h3&gt;

&lt;p&gt;If you run Node 18+ and try using the native &lt;code&gt;fetch()&lt;/code&gt; method with standard &lt;code&gt;export HTTPS_PROXY&lt;/code&gt; variables in your local environment, &lt;strong&gt;it will fail with `UND_ERR_CONNECT_TIMEOUT&lt;/strong&gt;`.&lt;/p&gt;

&lt;p&gt;Why? Because Node's global &lt;code&gt;fetch&lt;/code&gt; is built on top of &lt;code&gt;undici&lt;/code&gt;, which intentionally bypasses standard system environment proxy flags to maintain spec compliance with Web APIs.&lt;/p&gt;

&lt;p&gt;To fix this, you have to explicitly pass a &lt;code&gt;dispatcher&lt;/code&gt; or use &lt;code&gt;ProxyAgent&lt;/code&gt; directly inside your request logic:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node: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;XMLParser&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="s2"&gt;fast-xml-parser&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;ProxyAgent&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="s2"&gt;undici&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Explicitly route requests through local proxy when testing from restrictive networks&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxyAgent&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;ProxyAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1:7890&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;fetchFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedUrl&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proxyAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Mandatory for undici to route through local tunnels&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User-Agent&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="s2"&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0.0.0 Safari/537.36&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="s2"&gt;Accept&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="s2"&gt;application/rss+xml, application/xml;q=0.9, */*;q=0.8&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="s2"&gt;Accept-Language&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="s2"&gt;en-US,en;q=0.9&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;h3&gt;
  
  
  Lesson B: Custom User-Agents Will Get You Instantly Blocked
&lt;/h3&gt;

&lt;p&gt;Do not set custom User-Agents like &lt;code&gt;"My-Cool-Crawler/1.0"&lt;/code&gt;. Google’s edge security will identify the non-browser headers and instantly drop your connections with a 403 Forbidden or 503 Service Unavailable. Always mirror standard modern Chrome/Safari User-Agents and include &lt;code&gt;Accept-Language&lt;/code&gt; headers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Challenge 2: Client-Side Scoring &amp;amp; Data Normalization
&lt;/h2&gt;

&lt;p&gt;Instead of sending unstructured text to expensive LLM endpoints on every scrape iteration, I implemented a lightweight, term-matching relevance engine directly in the ingestion script.&lt;/p&gt;

&lt;p&gt;We define two primary arrays:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Company Entities:&lt;/strong&gt; Hardware, cloud, and foundation model providers (Nvidia, Microsoft, TSMC, OpenAI, DeepSeek, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relevance Keywords:&lt;/strong&gt; Financial &amp;amp; risk terms (bubble, valuation, Capex, data center, debt, inference cost, ROI, guidance).
&lt;/li&gt;
&lt;/ol&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;companyTerms&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="s2"&gt;Nvidia&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="s2"&gt;Microsoft&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="s2"&gt;TSMC&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="s2"&gt;OpenAI&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="s2"&gt;DeepSeek&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="s2"&gt;Broadcom&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;relevanceTerms&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="s2"&gt;bubble&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="s2"&gt;valuation&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="s2"&gt;capex&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="s2"&gt;debt&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="s2"&gt;inference cost&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="s2"&gt;margin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluateHeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headline&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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;matchedCompanies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;companyTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;matchedTerms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;relevanceTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Weighted scoring logic: Companies are weighted higher than generic financial buzzwords&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relevanceScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matchedCompanies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;matchedTerms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;matchedCompanies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;matchedTerms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;relevanceScore&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 simple algorithm filters out noisy headlines and retains high-signal candidates without requiring a heavy Python NLP server or paying $50/month in LLM token fees.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Challenge 3: Eliminating Cumulative Layout Shift (CLS) on Financial Charts
&lt;/h2&gt;

&lt;p&gt;If you are rendering interactive line charts or data tables on client devices, layout thrashing is your biggest enemy. Users hate clicking a button only for the layout to jump 200 pixels down because a chart canvas finished rendering a second late.&lt;/p&gt;

&lt;p&gt;Here is how I kept CLS down to &lt;strong&gt;0.00&lt;/strong&gt; on &lt;strong&gt;&lt;a href="https://aistockbubbleindex.com" rel="noopener noreferrer"&gt;aistockbubbleindex.com&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Explicit Aspect-Ratio Wrappers
&lt;/h3&gt;

&lt;p&gt;Never let your charting component dictate container height dynamically upon data arrival. Always wrap canvases in CSS grid/flex containers with fixed or aspect-ratio locked placeholders:&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;.chart-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;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;380px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="n"&gt;intrinsic-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Prevents browser reflow during dynamic canvas mounting */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Decoupled UI State from Canvas Rendering
&lt;/h3&gt;

&lt;p&gt;When users toggle between different metrics (e.g., P/E ratios vs. Capex spending trends), the DOM metadata (headers, index numbers, legend text) updates immediately in React state, while heavy canvas re-draws are deferred using standard requestIdleCallback or microtask queuing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Challenge 4: SPA SEO for Niche Keywords
&lt;/h2&gt;

&lt;p&gt;Single Page Applications are historically terrible at ranking for specific search terms like "AI stock bubble index" unless you handle DOM structure carefully.&lt;/p&gt;

&lt;p&gt;Since I opted against a full server-side Node instance (like Next.js) to keep hosting costs at absolute zero, I focused heavily on &lt;strong&gt;semantic HTML optimization&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic Landmarks:&lt;/strong&gt; No generic &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup for critical data blocks. I used &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; strictly for structured content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured JSON-LD:&lt;/strong&gt; Injecting Schema.org data schemas directly into the head tag during static generation so search engine bots can parse metric definitions instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Build-time Sitemaps:&lt;/strong&gt; Every time news or index candidate files update, a build script updates the &lt;code&gt;sitemap.xml&lt;/code&gt; and pings Search Console endpoints automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Results &amp;amp; Performance Benchmarks
&lt;/h2&gt;

&lt;p&gt;By eliminating backend database queries and keeping the frontend architecture razor-thin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse Performance Score:&lt;/strong&gt; 98/100&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First Contentful Paint (FCP):&lt;/strong&gt; ~0.4s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cumulative Layout Shift (CLS):&lt;/strong&gt; 0.00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly Server Costs:&lt;/strong&gt; $0 (served directly via CDN static edge networks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, I have a functional, noise-free utility that I actually use every week to check the temperature of the AI market without wading through social media hype.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways for Developers
&lt;/h2&gt;

&lt;p&gt;If you want to build data-driven tools or side projects in 2026, &lt;strong&gt;you don't always need a complex server infrastructure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before you launch a Docker cluster or pay for database instances:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask if your data can be pre-parsed and served as static, edge-cached JSON.&lt;/li&gt;
&lt;li&gt;Keep your frontend component bundles minimal.&lt;/li&gt;
&lt;li&gt;Optimize your scraping logic with simple term-matching algorithms before reaching for heavy AI models.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check out the live platform here: &lt;strong&gt;&lt;a href="https://aistockbubbleindex.com" rel="noopener noreferrer"&gt;https://aistockbubbleindex.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’d love to hear your thoughts! How are you handling external API feeds in Node.js ESM setups? How do you keep client-side dashboard rendering fast? Let’s chat in the comments below!&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Low-Cost, Zero-Backend AI Stock Bubble Index Tracker</title>
      <dc:creator>dundun sun</dc:creator>
      <pubDate>Thu, 16 Jul 2026 02:22:05 +0000</pubDate>
      <link>https://dev.to/dundunup/building-a-low-cost-zero-backend-ai-stock-bubble-index-tracker-16n3</link>
      <guid>https://dev.to/dundunup/building-a-low-cost-zero-backend-ai-stock-bubble-index-tracker-16n3</guid>
      <description>&lt;p&gt;As the discussion around Artificial Intelligence shifts from generative hype to financial sustainability, I wanted to build a dedicated tool to track the "bubble status" of AI-related equities.&lt;/p&gt;

&lt;p&gt;Instead of spinning up a heavy Kubernetes cluster or paying for expensive enterprise data pipelines, my goal was to build a highly responsive, SEO-optimized, and low-latency metrics dashboard.&lt;/p&gt;

&lt;p&gt;Here is the architectural breakdown of how I built AI Stock Bubble Index.&lt;/p&gt;

&lt;p&gt;The Architecture: Keeping it Lightweight&lt;br&gt;
To handle real-time financial sentiment and market data without breaking the bank, I adopted a modern JAMstack / Zero-Backend inspired architecture.&lt;/p&gt;

&lt;p&gt;Frontend Framework: Built entirely with a components-driven framework (React) to ensure instant UI state updates when users toggle different tracking metrics.&lt;/p&gt;

&lt;p&gt;Data Ingestion: Instead of keeping an active database connection open for every single visitor, market volatility metrics and structural data schemas are processed asynchronously and served via highly optimized JSON endpoints.&lt;/p&gt;

&lt;p&gt;Styling &amp;amp; Interactivity: Leveraged highly responsive UI kits alongside custom CSS logic to ensure smooth rendering of data charts across both desktop and mobile viewports.&lt;/p&gt;

&lt;p&gt;Technical Challenge 1: Micro-Frontend SEO &amp;amp; Indexing&lt;br&gt;
One of the biggest hurdles for data-heavy, single-page web applications is ensuring that search engine bots can effectively index dynamic keywords like "AI stock bubble".&lt;/p&gt;

&lt;p&gt;To tackle this:&lt;/p&gt;

&lt;p&gt;Semantic HTML Layouts: I structured the FAQs and dashboard summaries using strict semantic markup rather than nested &lt;/p&gt; soup.

&lt;p&gt;Sitemap &amp;amp; Index Management: Implemented an automated build pipeline that updates the indexation status and alerts search consoles whenever market thresholds trigger significant index shifts.&lt;/p&gt;

&lt;p&gt;Technical Challenge 2: Client-Side Data Rendering Efficiency&lt;br&gt;
Rendering multi-asset financial charts on low-end mobile devices can easily choke the main thread.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A snippet of how data structural layers are evaluated before canvas rendering&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluateBubbleIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metrics&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;peRatio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sentimentScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;capitalInflow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Weighted algorithmic formula to determine the current market exposure&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peRatio&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sentimentScore&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capitalInflow&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&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;p&gt;By decoupling the layout state from the heavy charting canvas, the page achieves an exceptionally high performance score, keeping cumulative layout shifts (CLS) near zero.&lt;/p&gt;

&lt;p&gt;Key Takeaways&lt;br&gt;
Building this index proved that you don't need a complex microservice architecture to launch a functional, data-driven utility platform. By prioritizing clean semantic markup, lightweight state management, and strict build-time optimizations, you can achieve enterprise-grade responsiveness on a hobbyist budget.&lt;/p&gt;

&lt;p&gt;Check out the live dashboard here: &lt;a href="https://aistockbubbleindex.com/" rel="noopener noreferrer"&gt;AI Stock Bubble Index&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts on the metric weights! How are you handling real-time data streaming on client-side dashboards? Let's discuss in the comments below.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Sandbox for Neal.fun's Cursor Camp — Here's What Happened</title>
      <dc:creator>dundun sun</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:35:59 +0000</pubDate>
      <link>https://dev.to/dundunup/i-built-a-sandbox-for-nealfuns-cursor-camp-heres-what-happened-276k</link>
      <guid>https://dev.to/dundunup/i-built-a-sandbox-for-nealfuns-cursor-camp-heres-what-happened-276k</guid>
      <description>&lt;p&gt;A few months ago I opened Neal Agarwal's Cursor Camp for the first time. You know that feeling when a website completely derails your afternoon? That was me. I spent two hours just watching dozens of cursors wander around a pixel campground, kicking a soccer ball, sitting by the fire.&lt;/p&gt;

&lt;p&gt;And then I thought: what if this world kept going even after I closed the tab?&lt;/p&gt;

&lt;p&gt;That question eventually turned into &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;CursorCamp Sandbox&lt;/a&gt;, a fully interactive browser companion built from scratch. No game engines, just Canvas 2D, SVG, and TypeScript. Here's how a moment of curiosity became a 6,000-line obsession.&lt;/p&gt;

&lt;p&gt;The Moment It Clicked&lt;br&gt;
The original Cursor Camp is a genius piece of ambient design. There are no objectives, no tutorials, no signups. You just move your cursor and discover things. The campers (other cursors) wander around doing their own thing. It feels alive, but it's ephemeral — close the tab and it's gone.&lt;/p&gt;

&lt;p&gt;I wanted to make a version that felt permanent. A place you could return to. Same whimsical energy, but with persistence, more interactivity, and deeper secrets to uncover.&lt;/p&gt;

&lt;p&gt;What I Actually Built&lt;br&gt;
The sandbox ended up with:&lt;/p&gt;

&lt;p&gt;55 AI campers, each with a 5-phase movement state machine (idle → accelerate → cruise → decelerate → overshoot). They wander between points of interest, occasionally chasing the soccer ball, randomly joining and leaving throughout the day&lt;br&gt;
An 8,000×5,000 pixel procedurally-generated world with oceans, rivers, roads, a soccer field, running track, vegetable garden, and scattered decorations — all generated from math, no external assets&lt;br&gt;
Dynamic weather — rain and snow with particle systems, wind that drifts over time&lt;br&gt;
Spatial audio — 11 independent sound sources that fade as you move around. The DJ booth has a 4-track playlist that crossfades&lt;br&gt;
17 hidden seashells, a soccer goal, interactive houses you can enter, and benches you can rest on&lt;br&gt;
The entire thing runs at 60fps in a browser with zero dependencies beyond React and Next.js.&lt;/p&gt;

&lt;p&gt;The Hardest Part Wasn't the Code&lt;br&gt;
It was resisting the urge to add objectives. I kept wanting to add quests, levels, progression systems. But that would've missed the point. The magic of Cursor Camp is that nothing asks anything of you. You're just... there.&lt;/p&gt;

&lt;p&gt;The hardest technical challenge was the camera system. With an 8,000×5,000 world, you can't just center on the player — that would be disorienting. I ended up with a dead-zone approach: the camera only pans when your cursor approaches the screen edges, giving the feel of looking around a physical space.&lt;/p&gt;

&lt;p&gt;Why I'm Sharing This&lt;br&gt;
Fan projects don't need to be commercial to be worth building. This one taught me more about Canvas rendering, procedural generation, and game feel than any tutorial ever could. And it brought a small community of people who just wanted to hang out at a virtual campfire.&lt;/p&gt;

&lt;p&gt;If you've ever been inspired by someone else's creative work, I encourage you to build something in response. Not a clone, not a competitor — just your own take. It's the most fun you'll have coding.&lt;/p&gt;

&lt;p&gt;The sandbox is free and open at &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;https://www.cursorcamp-sandbox.com&lt;/a&gt;. No signup, no downloads, no objectives. Just a camp that's always open.&lt;/p&gt;

&lt;p&gt;Built with React, Next.js, TypeScript, and exactly zero game engines.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Building a Game with Zero Game Libraries — The Architecture Behind CursorCamp Sandbox</title>
      <dc:creator>dundun sun</dc:creator>
      <pubDate>Mon, 25 May 2026 03:26:47 +0000</pubDate>
      <link>https://dev.to/dundunup/building-a-game-with-zero-game-libraries-the-architecture-behind-cursorcamp-sandbox-20hn</link>
      <guid>https://dev.to/dundunup/building-a-game-with-zero-game-libraries-the-architecture-behind-cursorcamp-sandbox-20hn</guid>
      <description>&lt;p&gt;You'd think building a browser game requires a stack of dependencies. Phaser, PixiJS, maybe Three.js. A physics engine, an audio library, a state manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CursorCamp Sandbox&lt;/strong&gt; does none of that. Zero game engines. Zero physics libraries. Just Next.js, TypeScript, raw Canvas 2D, and inline SVG. Under 6,000 lines of hand-rolled code.&lt;/p&gt;

&lt;p&gt;Here's how the architecture works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is It?
&lt;/h2&gt;

&lt;p&gt;An interactive fan-made companion for Neal.fun's Cursor Camp. You explore a procedurally-detailed world, collect seashells, kick soccer balls, and share the space with 55 AI campers. Weather changes. Music fades as you move around. Everything runs in the browser at &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;cursorcamp-sandbox.com&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack (Spoiler: It's Empty)
&lt;/h2&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;"dependencies"&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;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"react-dom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^18.3.0"&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;That's it. Every extra dependency — &lt;code&gt;next-sitemap&lt;/code&gt;, &lt;code&gt;tailwindcss&lt;/code&gt;, &lt;code&gt;postcss&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt; — is either build tooling or a sitemap generator. The runtime ships with Next.js and React only.&lt;/p&gt;

&lt;p&gt;No Phaser. No Matter.js. No Howler.js. No Redux. No Framer Motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual-Canvas Rendering Pipeline
&lt;/h2&gt;

&lt;p&gt;The most interesting architectural decision: &lt;strong&gt;two HTML Canvas elements overlaid on top of each other&lt;/strong&gt;, separated by z-index, each responsible for a different set of concerns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1 (canvasRef)     → terrain, trees, structures, entities, bots
Layer 2 (cursorCanvasRef) → weather particles, cursor pointers, accessories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids &lt;code&gt;globalCompositeOperation&lt;/code&gt; tricks and makes the render loop cleaner. Each canvas gets fully cleared and redrawn every frame via &lt;code&gt;requestAnimationFrame&lt;/code&gt;. No dirty-rect tracking, no spatial partitioning — just brute-force, draw-everything, every tick.&lt;/p&gt;

&lt;p&gt;Between the two canvas layers sit &lt;strong&gt;inline React SVG components&lt;/strong&gt; positioned absolutely over the world. The DJ stage, the lake, the gym equipment, the banner — all rendered as SVG elements with CSS animations (rotating spotlights, rippling water). They share the same viewBox coordinate system as the canvas, so positioning is consistent.&lt;/p&gt;

&lt;p&gt;Why SVG for these? Because CSS animations on SVG elements are GPU-accelerated and don't need to be re-drawn in the canvas loop. The DJ's moving-head spotlights rotate with a few lines of CSS &lt;code&gt;@keyframes&lt;/code&gt; instead of recalculating angles in JavaScript every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Split That Makes It Work
&lt;/h2&gt;

&lt;p&gt;React isn't built for game loops. If you put framerate-critical state into &lt;code&gt;useState&lt;/code&gt;, you'll trigger re-renders 60 times per second and kill performance.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;a deliberate split between &lt;code&gt;useRef&lt;/code&gt; and &lt;code&gt;useState&lt;/code&gt;&lt;/strong&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="c1"&gt;// Game state → useRef (no re-renders)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;camRef&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Vec2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1050&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;botsRef&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Bot&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="nf"&gt;createBots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&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;ballRef&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ball&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FIELD_CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// UI state → useState (triggers re-renders for UI updates)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;shellsCollected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShellsCollected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNotification&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;showJournal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setShowJournal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The game loop reads and mutates refs 60 times per second without touching React's render cycle. Only UI-relevant state — collected shell counts, notification popups, journal visibility — lives in &lt;code&gt;useState&lt;/code&gt; and triggers the normal React pipeline.&lt;/p&gt;

&lt;p&gt;This is embarrassingly simple compared to ECS (Entity Component System) architectures, but for a single-developer project with ~200 entities, it works perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bot AI State Machine
&lt;/h2&gt;

&lt;p&gt;55 campers wander the map simultaneously. Each one runs a &lt;strong&gt;5-phase movement state machine&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle → accel → cruise → decel → overshoot → idle (loop)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;idle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stands still for 1-4 seconds, fidgets slightly, picks a random destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;accel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eases into movement along a quadratic Bezier curve toward the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cruise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moves at full speed with sub-pixel jitter for natural-looking motion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;decel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eases out as it approaches the target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;overshoot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If it passes the target, corrects back with a small counter-movement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A couple bots also keep an eye on the soccer ball. If the ball is kicked near them, they enter a chase mode and move toward it. When the ball is idle too long, a random nearby bot kicks it.&lt;/p&gt;

&lt;p&gt;The result looks organic — not like pathfinding soldiers, more like actual people wandering a campsite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spatial Proximity Audio
&lt;/h2&gt;

&lt;p&gt;The audio system has no external dependencies. It uses raw &lt;code&gt;HTMLAudioElement&lt;/code&gt; objects with proximity-based volume:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateDJProximity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;djAudio&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="nx"&gt;vol&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;Each sound source — DJ booth, campfire, ocean, river, car horn, football — has its own init and update function. The audio elements are created once at startup and their volumes are modulated every frame based on the player's distance to the sound source.&lt;/p&gt;

&lt;p&gt;The DJ system is the most complex: a 4-track playlist that auto-advances on the &lt;code&gt;ended&lt;/code&gt; event with crossfading. Walk toward the DJ booth, the music fades in. Walk away, it fades out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Procedural Terrain, Deterministically
&lt;/h2&gt;

&lt;p&gt;An 8,000 × 5,000 pixel world with oceans, rivers, beaches, roads, soccer fields, running tracks, vegetable gardens, and scattered decorations — all generated at runtime from math.&lt;/p&gt;

&lt;p&gt;The secret weapon is a &lt;code&gt;hash2d&lt;/code&gt; function that takes two integers and returns a deterministic pseudo-random number:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hash2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;374761393&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;668265263&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1274126177&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1274126177&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4294967296&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;Every flower, grass tuft, bush, and stone is positioned using this hash. The result looks organic but is completely deterministic — every render produces the same layout. No random seed, no saved state, just math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isometric 2.5D Without a 3D Engine
&lt;/h2&gt;

&lt;p&gt;All SVG components use "fake isometric" rendering: explicit 2D coordinates that simulate depth. Building roofs are parallelograms. Shadows are dark ellipses under entities. The DJ stage platform surface is computed using bilinear interpolation. Everything is top-down isometric drawn in pure 2D — no projection matrices, no 3D transforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  SVG Data URIs as Sprite Sheets
&lt;/h2&gt;

&lt;p&gt;Every game asset — trees, the campfire, houses, the soccer ball, shells — is an &lt;strong&gt;inline SVG stored as a data URI&lt;/strong&gt; in &lt;code&gt;assets.ts&lt;/code&gt;. These are loaded into &lt;code&gt;Image&lt;/code&gt; objects once at startup, then drawn with &lt;code&gt;ctx.drawImage()&lt;/code&gt; in the game loop.&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;TREE_ROUND_SVG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:image/svg+xml,...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 2KB of hand-crafted SVG&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&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;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TREE_ROUND_SVG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Later, in the render loop:&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates all external image requests. The entire game loads as a single HTML page with inline assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weather That Works
&lt;/h2&gt;

&lt;p&gt;Rain and snow are particle systems running on the cursor canvas layer. Rain drops are angled streaks with wind variation. Snow particles float with multi-frequency wobble. Splash particles spawn when rain hits the "ground" at the bottom of the viewport.&lt;/p&gt;

&lt;p&gt;The weather system transitions on a timer — 50-100 seconds of clear sky, then 25-35 seconds of rain or snow, with a smooth fade transition in between. Wind direction drifts slowly over time using a sine function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Water Physics
&lt;/h2&gt;

&lt;p&gt;Wading into the ocean or river doesn't just change the visual — it changes how you move. Ocean depth uses a quadratic resistance curve: ankle-deep gives light drag, while 200px deep makes you nearly immovable. The river applies a consistent 85% drag plus directional current force. Diving in triggers a splash sound via edge-triggered audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The takeaway isn't "you should build games without engines." It's that &lt;strong&gt;the browser platform is incredibly capable on its own&lt;/strong&gt;. Canvas 2D, SVG, CSS animations, and the Web Audio API can handle far more than most developers assume — especially when you're one person building something for the love of it.&lt;/p&gt;

&lt;p&gt;No build pipeline for sprite sheets. No WebGL shader debugging. No physics engine configuration. Just TypeScript, math, and the raw browser.&lt;/p&gt;

&lt;p&gt;If you want to see it in action, it's at &lt;a href="https://www.cursorcamp-sandbox.com" rel="noopener noreferrer"&gt;cursorcamp-sandbox.com&lt;/a&gt;. The source is all there in your browser's dev tools — no minification, no obfuscation, just readable code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Next.js 14, TypeScript, and precisely zero game libraries.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
