<?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: K. Bear</title>
    <description>The latest articles on Forem by K. Bear (@xkbear).</description>
    <link>https://forem.com/xkbear</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%2F3860344%2Fb84a43eb-e790-47dd-978a-4c8bb1df9d29.png</url>
      <title>Forem: K. Bear</title>
      <link>https://forem.com/xkbear</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/xkbear"/>
    <language>en</language>
    <item>
      <title>Making ChatGPT, Perplexity and Claude actually cite your SPA — a GEO field report</title>
      <dc:creator>K. Bear</dc:creator>
      <pubDate>Mon, 06 Apr 2026 09:41:26 +0000</pubDate>
      <link>https://forem.com/xkbear/making-chatgpt-perplexity-and-claude-actually-cite-your-spa-a-geo-field-report-4h2b</link>
      <guid>https://forem.com/xkbear/making-chatgpt-perplexity-and-claude-actually-cite-your-spa-a-geo-field-report-4h2b</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody warned me about
&lt;/h2&gt;

&lt;p&gt;I shipped a single-HTML-file global conflict monitor (&lt;a href="https://crisispulse.org" rel="noopener noreferrer"&gt;crisispulse.org&lt;/a&gt;) last week. Classic SPA: one &lt;code&gt;index.html&lt;/code&gt;, D3 map, Netlify Functions for the backend, ~100KB total. It works beautifully in browsers.&lt;/p&gt;

&lt;p&gt;Then I asked ChatGPT: &lt;em&gt;"What does crisispulse.org do?"&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I don't have information about that specific website."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perplexity: &lt;em&gt;"I couldn't find reliable sources about crisispulse.org."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Claude: &lt;em&gt;"This URL doesn't appear in my training data or available tools."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three of the biggest answer engines in the world, and none of them could describe a site that literally exists and has a comprehensive &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, meta description, and Open Graph tags. Why?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because SPAs are invisible to AI crawlers in ways traditional SEO never had to worry about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is a field report on what I changed, in what order, and what actually moved the needle. Everything here is open â€” you can diff the commit yourself: &lt;a href="https://github.com/xkbear/crisispulse/commit/2d94a57" rel="noopener noreferrer"&gt;&lt;code&gt;2d94a57&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI crawlers are different from Googlebot
&lt;/h2&gt;

&lt;p&gt;Two inconvenient truths I had to internalize:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Most AI crawlers don't execute JavaScript
&lt;/h3&gt;

&lt;p&gt;Googlebot renders JS (mostly). GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Bytespider, CCBot â€” most of them just fetch the raw HTML, parse it, and move on. That means for a pure SPA:&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;"app"&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;src=&lt;/span&gt;&lt;span class="s"&gt;"bundle.js"&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;â€¦what the crawler sees is literally a div and a script tag. No content. No headings. No context. Zero signal to feed into an embedding.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI crawlers are citation-optimizing, not click-optimizing
&lt;/h3&gt;

&lt;p&gt;Traditional SEO is a ranking game: &lt;em&gt;show my link first&lt;/em&gt;. GEO (Generative Engine Optimization) is a &lt;strong&gt;citation game&lt;/strong&gt;: &lt;em&gt;when the model answers a user's question, have it actually name-check and link to my site&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The optimization targets are different. Google wants to show you the best ten results. ChatGPT wants to write one paragraph that correctly attributes sources. To get cited, you need to be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Easy to fetch&lt;/strong&gt; (no JS, no login walls, no Cloudflare challenges on bot UAs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to parse&lt;/strong&gt; (structured data, headings, lists, not walls of text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to summarize&lt;/strong&gt; (short factual statements, not marketing prose)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to cite&lt;/strong&gt; (a canonical URL and a clear "what this is in one sentence")&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that happens by default on a modern SPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things I changed
&lt;/h2&gt;

&lt;p&gt;Here's the stack, in order of ROI (highest first):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; SEO block
&lt;/h3&gt;

&lt;p&gt;The simplest, cheapest win. Right after &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, before the app mount point, I added a block that only exists for non-JS crawlers:&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;noscript&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Crisis Pulse â€” Global Conflict Monitor&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Crisis Pulse is a free, single-file web app that tracks 25+ active
     global conflict zones in real time and generates a personalized
     emergency supply list based on your location.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Currently tracked conflicts&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Russia-Ukraine war â€” Eastern Europe&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Gaza / Israel-Hamas conflict â€” Middle East&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Sudan civil war â€” North Africa&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Myanmar civil war â€” Southeast Asia&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- â€¦21 moreâ€¦ --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Frequently asked questions&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Is Crisis Pulse free?&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Yes, completely free and open source. No sign-up required.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- â€¦7 more Q/A pairsâ€¦ --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; browsers with JS enabled never render it (users see your real UI). But every crawler that doesn't execute JS â€” which is most AI crawlers â€” gets a clean, structured, semantically-tagged summary of what your site is. H1, H2, H3, lists, and eight FAQ pairs in plain HTML. That's eight potential citations every time a user asks a related question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to implement:&lt;/strong&gt; 20 minutes. &lt;strong&gt;Impact:&lt;/strong&gt; massive.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Counter-argument I've seen: "But Google dings you for hidden content!" The &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; element is explicitly allowed â€” it's the standards-defined way to expose content to non-JS agents. Google has confirmed this repeatedly. Don't conflate it with &lt;code&gt;display:none&lt;/code&gt; spam.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. A rich JSON-LD &lt;code&gt;@graph&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The second-biggest lever is structured data. Not the tiny &lt;code&gt;WebSite&lt;/code&gt; snippet most tutorials show â€” a proper &lt;code&gt;@graph&lt;/code&gt; with multiple linked entities:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;@context&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;https://schema.org&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;@graph&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="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;WebApplication&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;@id&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;https://crisispulse.org/#webapp&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;name&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;Crisis Pulse&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;applicationCategory&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;NewsApplication&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;operatingSystem&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;Any&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;offers&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Offer&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;price&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;0&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;featureList&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Real-time global conflict map&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;Daily intensity scoring (0-10)&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;Personalized emergency supply calculator&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;Bilingual EN/ZH support&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;25+ tracked conflict zones&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;inLanguage&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&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;zh&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;isAccessibleForFree&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;softwareVersion&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;1.0.0&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Organization&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;@id&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;https://crisispulse.org/#org&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;name&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;Crisis Pulse&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;url&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;https://crisispulse.org&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;sameAs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.producthunt.com/products/crisis-pulse&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;https://dev.to/xkbear&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="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;FAQPage&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;@id&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;https://crisispulse.org/#faq&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;mainEntity&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="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Question&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;name&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;What is Crisis Pulse?&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;acceptedAnswer&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Answer&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;text&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;Crisis Pulse is a free, single-file web application that tracks 25+ active global conflicts in real time...&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="c1"&gt;// â€¦7 more questionsâ€¦&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@graph&lt;/code&gt; with multiple entities&lt;/strong&gt;, not one lonely &lt;code&gt;WebApplication&lt;/code&gt;. Each entity has its own &lt;code&gt;@id&lt;/code&gt;, so crawlers can deduplicate across pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;FAQPage&lt;/code&gt; with 8 entries&lt;/strong&gt;, directly in the schema, plus the same Q/A text in the &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; block. Belt and suspenders â€” the schema gives machine-readable intent, the noscript gives human-readable content. Both say the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sameAs&lt;/code&gt; linking to Product Hunt and Dev.to&lt;/strong&gt;. This is the identity graph â€” it tells AI models "this site, this Product Hunt listing, this Dev.to profile are all the same project." Worth the 30 seconds.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;llms.txt&lt;/code&gt; and &lt;code&gt;llms-full.txt&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the one that surprised me by mattering. &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; is a proposed standard (by Jeremy Howard) that's rapidly becoming the &lt;code&gt;robots.txt&lt;/code&gt; equivalent for AI crawlers: a single, human-readable markdown file at your root that tells LLMs exactly what you are, in the format they want to consume.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;/llms.txt&lt;/code&gt; is ~40 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Crisis Pulse&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; A free, single-HTML-file global conflict monitor and emergency&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; supply calculator. Tracks 25+ active conflict zones with daily&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; intensity scoring and personalized prep recommendations.&lt;/span&gt;

&lt;span class="gu"&gt;## Core features&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Real-time global conflict map (D3 + TopoJSON)
&lt;span class="p"&gt;-&lt;/span&gt; Daily intensity scoring algorithm (0-10 scale)
&lt;span class="p"&gt;-&lt;/span&gt; Emergency supply calculator based on geolocation
&lt;span class="p"&gt;-&lt;/span&gt; Bilingual English / Simplified Chinese
&lt;span class="p"&gt;-&lt;/span&gt; 100% free, no sign-up, no tracking

&lt;span class="gu"&gt;## Key facts&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Launched: 2026
&lt;span class="p"&gt;-&lt;/span&gt; Open source: yes
&lt;span class="p"&gt;-&lt;/span&gt; Architecture: single HTML file + Netlify Functions
&lt;span class="p"&gt;-&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then a &lt;code&gt;/llms-full.txt&lt;/code&gt; with the long version â€” every tracked conflict listed, the intensity scoring methodology, the tech stack, the philosophy, the roadmap.&lt;/p&gt;

&lt;p&gt;Why two files? The short one is for the model's system prompt injection; the long one is for the crawler's deep index. Both live at the root, both are &lt;code&gt;text/plain&lt;/code&gt;, both are ~10KB total. Zero cost, enormous context gain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; llms.txt isn't a W3C standard and not every AI crawler reads it (yet). But Anthropic, Perplexity, and Cursor have publicly committed. ChatGPT is "exploring." Google hasn't said. The downside of shipping it is basically zero; the upside if it becomes table stakes is large.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A maximally permissive &lt;code&gt;robots.txt&lt;/code&gt; for AI bots
&lt;/h3&gt;

&lt;p&gt;By default, a lot of frameworks ship a &lt;code&gt;robots.txt&lt;/code&gt; that's implicitly "crawl everything." But AI bots are increasingly checking for &lt;em&gt;explicit&lt;/em&gt; allow directives because of the post-2023 opt-out backlash. If you want to be cited by ChatGPT, you probably want to go from "implicit allow" to "explicit allow."&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: GPTBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: OAI-SearchBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Claude-Web
Allow: /

User-agent: anthropic-ai
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Perplexity-User
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: Applebot-Extended
Allow: /

User-agent: CCBot
Allow: /

User-agent: cohere-ai
Allow: /

# â€¦and about ten moreâ€¦

Sitemap: https://crisispulse.org/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The list is long (I put 20+ bots in mine) because the AI crawler ecosystem is fragmented. Some bots fall back to the most restrictive directive they can match; an explicit &lt;code&gt;Allow&lt;/code&gt; resolves ambiguity.&lt;/p&gt;

&lt;p&gt;If you're running analytics on 402 responses, you'll see a surprising number of these UAs showing up the day after you deploy this.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Meta tags per bot
&lt;/h3&gt;

&lt;p&gt;Small but free. In addition to the standard &lt;code&gt;&amp;lt;meta name="robots"&amp;gt;&lt;/code&gt;, you can ship bot-specific meta directives:&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;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"GPTBot"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ChatGPT-User"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ClaudeBot"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"PerplexityBot"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Google-Extended"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"index, follow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"category"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Geopolitics, Emergency Preparedness, News Monitoring, OSINT"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max-snippet:-1&lt;/code&gt; is the one that matters most â€” it tells crawlers "feel free to quote the entire page in an answer," which is exactly the behavior you want for citation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I skipped (and why)
&lt;/h2&gt;

&lt;p&gt;A few things I chose not to do, and the reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-rendering / SSG.&lt;/strong&gt; Would work, but introduces a build step and breaks the "one HTML file" constraint that defines the project. The &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; block covers 90% of the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate &lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/faq&lt;/code&gt;, &lt;code&gt;/features&lt;/code&gt; pages.&lt;/strong&gt; Every "you need 10+ pages to rank" SEO guide recommends this. I disagree for AI crawlers â€” they prefer one authoritative page that's easy to cite over ten thin ones. One URL is one citation target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid AI SEO tools.&lt;/strong&gt; There's a whole class of "GEO dashboards" emerging. They're mostly wrapping &lt;code&gt;curl&lt;/code&gt; + prompt templates. Skip until you have organic signal worth measuring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic HTML restructuring of the entire app.&lt;/strong&gt; Diminishing returns. The &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; block gives crawlers what they need; the interactive app can stay divs-and-JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I'm measuring this
&lt;/h2&gt;

&lt;p&gt;Honest answer: it's early and the feedback loop is slow. Here's what I'm tracking:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Log file analysis&lt;/strong&gt; for the UAs listed in &lt;code&gt;robots.txt&lt;/code&gt;. Did GPTBot show up? ClaudeBot? How often? Netlify makes this trivial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual citation checks&lt;/strong&gt; every few days â€” ask ChatGPT, Perplexity, Claude, and You.com the same five questions about "global conflict tracker" and see whether crisispulse.org surfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referral traffic from &lt;code&gt;chat.openai.com&lt;/code&gt;, &lt;code&gt;perplexity.ai&lt;/code&gt;, &lt;code&gt;you.com&lt;/code&gt;, &lt;code&gt;claude.ai&lt;/code&gt;.&lt;/strong&gt; These show up in analytics once you're indexed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand search growth&lt;/strong&gt; in Google Search Console â€” the boring but reliable leading indicator.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll write a follow-up in 30 days with actual numbers. No hype, whatever the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta point
&lt;/h2&gt;

&lt;p&gt;SEO used to be about ranking in a list. GEO is about being the sentence the model writes. The optimization targets rhyme, but they're not the same â€” and they reward different trade-offs.&lt;/p&gt;

&lt;p&gt;The encouraging thing is that most of it is boringly standards-compliant: use semantic HTML, ship structured data, respect &lt;code&gt;noscript&lt;/code&gt;, write a clear one-sentence description of what your thing is. All of it is free, all of it takes a few hours, all of it is testable. You don't need an agency.&lt;/p&gt;

&lt;p&gt;If you've shipped something and it's invisible to AI search, I'd genuinely love to hear what you tried â€” drop it in the comments and I'll diff our approaches.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with vanilla JS, shipped on Netlify, cited by (hopefully) an LLM near you. The whole thing is &lt;a href="https://github.com/xkbear/crisispulse" rel="noopener noreferrer"&gt;one HTML file on GitHub&lt;/a&gt;. If you want the full GEO commit, it's &lt;a href="https://github.com/xkbear/crisispulse/commit/2d94a57" rel="noopener noreferrer"&gt;2d94a57&lt;/a&gt; â€” 5 files, 369 insertions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a global conflict monitor in a single HTML file — here's how the serverless architecture works</title>
      <dc:creator>K. Bear</dc:creator>
      <pubDate>Sat, 04 Apr 2026 03:13:57 +0000</pubDate>
      <link>https://forem.com/xkbear/i-built-a-global-conflict-monitor-in-a-single-html-file-heres-how-the-serverless-architecture-324i</link>
      <guid>https://forem.com/xkbear/i-built-a-global-conflict-monitor-in-a-single-html-file-heres-how-the-serverless-architecture-324i</guid>
      <description>&lt;p&gt;When I started building CrisisPulse, I had one constraint: it had to work without a backend database, a framework, or a build pipeline. The result is &lt;a href="https://crisispulse.org" rel="noopener noreferrer"&gt;crisispulse.org&lt;/a&gt; — a live global conflict monitor + emergency supply calculator, shipped as a single HTML file.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Pure HTML/CSS/JS with D3.js for the world map. No React, no build step. The entire app ships as one file (~99KB). Zero dependencies to install, zero build times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily news updates:&lt;/strong&gt; A Netlify Scheduled Function runs &lt;code&gt;@daily&lt;/code&gt;, fetching Bing RSS feeds for 25+ conflict zones. It parses article counts to calculate intensity scores and deltas, translates headlines to Chinese via the Google Translate &lt;code&gt;gtx&lt;/code&gt; endpoint, and stores everything to Netlify Blobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistence without a database:&lt;/strong&gt; Netlify Blobs is a built-in KV store included with Netlify. Visitor counts by country, conflict data, subscriber emails — all stored there. No Postgres, no Redis, no external API keys for storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tricky Part — Bing RSS URL Decoding
&lt;/h2&gt;

&lt;p&gt;Bing's RSS feeds double-encode their redirect URLs. The &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.bing.com/news/apiclick.aspx?url=https%3A%2F%2F...&amp;amp;amp;ref=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;amp;amp;&lt;/code&gt; breaks &lt;code&gt;new URL()&lt;/code&gt; parsing. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractRealUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bingUrl&lt;/span&gt;&lt;span class="p"&gt;)&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;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bingUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;amp;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&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;clean&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apiclick.aspx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;clean&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bing.com/news&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;u&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&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;real&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&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;real&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;real&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="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;;&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;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;clean&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;Decode HTML entities &lt;em&gt;before&lt;/em&gt; parsing the URL params. Simple fix, non-obvious bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bilingual Support Without a Translation API Key
&lt;/h2&gt;

&lt;p&gt;A static &lt;code&gt;CONFLICT_ZH&lt;/code&gt; map covers all 25 conflict names, types, and descriptions. Dynamic news descriptions get batch-translated via the free Google Translate &lt;code&gt;gtx&lt;/code&gt; endpoint (no API key required). Language switching re-runs the risk calculation rather than serving stale cached strings — a subtle bug I hit early on where the cache stored already-translated strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visitor Tracking by Country
&lt;/h2&gt;

&lt;p&gt;Each page load hits a Netlify Function that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the visitor's country from &lt;code&gt;context.geo&lt;/code&gt; (Netlify's built-in geolocation)&lt;/li&gt;
&lt;li&gt;Falls back to GPS if available&lt;/li&gt;
&lt;li&gt;Increments a per-country counter in Netlify Blobs&lt;/li&gt;
&lt;li&gt;Returns the updated counts for the left-side visitor panel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No external analytics, no cookies, no tracking scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;The single-file constraint was a feature, not a limitation. It forced every line to justify its existence. But I'd reconsider using the free Google Translate &lt;code&gt;gtx&lt;/code&gt; endpoint in production — it's undocumented and could break without notice.&lt;/p&gt;

&lt;p&gt;Live site: &lt;a href="https://crisispulse.org" rel="noopener noreferrer"&gt;crisispulse.org&lt;/a&gt;&lt;br&gt;
Free, no signup, supports English and Chinese.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>netlify</category>
    </item>
  </channel>
</rss>
