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 crisispulse.org — a live global conflict monitor + emergency supply calculator, shipped as a single HTML file.
Here's how the architecture works.
The Stack
Frontend: 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.
Daily news updates: A Netlify Scheduled Function runs @daily, 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 gtx endpoint, and stores everything to Netlify Blobs.
Persistence without a database: 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.
The Tricky Part — Bing RSS URL Decoding
Bing's RSS feeds double-encode their redirect URLs. The <link> tags look like this:
https://www.bing.com/news/apiclick.aspx?url=https%3A%2F%2F...&ref=...
The & breaks new URL() parsing. The fix:
function extractRealUrl(bingUrl) {
let clean = bingUrl.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
try {
if (clean.includes('apiclick.aspx') || clean.includes('bing.com/news')) {
const u = new URL(clean);
const real = u.searchParams.get('url');
if (real) return decodeURIComponent(real);
}
if (clean.startsWith('http')) return clean;
} catch (_) {}
return clean;
}
Decode HTML entities before parsing the URL params. Simple fix, non-obvious bug.
Bilingual Support Without a Translation API Key
A static CONFLICT_ZH map covers all 25 conflict names, types, and descriptions. Dynamic news descriptions get batch-translated via the free Google Translate gtx 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.
Visitor Tracking by Country
Each page load hits a Netlify Function that:
- Reads the visitor's country from
context.geo(Netlify's built-in geolocation) - Falls back to GPS if available
- Increments a per-country counter in Netlify Blobs
- Returns the updated counts for the left-side visitor panel
No external analytics, no cookies, no tracking scripts.
What I'd Do Differently
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 gtx endpoint in production — it's undocumented and could break without notice.
Live site: crisispulse.org
Free, no signup, supports English and Chinese.
Top comments (0)