NewsScope is a real-time news search engine: search a topic, filter by language, category and country, get live results from the NewsData.io API. No React, no bundler, no npm dependencies — just HTML, CSS and vanilla ES2020+.
This post is about a few specific decisions in the architecture that I think are worth sharing.
The module structure
The JS is 9 files, each with a single responsibility, loaded in dependency order directly in index.html:
config.js → i18n.js → data.js
↓ ↓ ↓
helpers.js → geo.js → ui.js
↓ ↓ ↓
render.js → api.js → main.js
Every module only uses things defined in modules loaded before it. main.js registers all event listeners and calls init() — it's the only file that touches everything. config.js is the smallest file in the project, since it only defines the state object and two constants.
All app state lives in a single flat object in config.js, accessed as a global:
const S = {
apiKey: '', query: '', activeQuery: '',
language: 'es', category: '', country: '',
results: [], nextPage: null,
loading: false, hasSearched: false, error: null,
};
No state management library. When something changes, the relevant render function gets called explicitly. Simple, and easy to trace.
Translating search intent, not just the UI
Most i18n stops at labels and button text. NewsScope has 10 predefined topic shortcuts (AI, Climate, Economy, Cybersecurity…) that trigger a search. If a user picks "Cybersecurity" while the app is set to Japanese, the keyword sent to the API should be in Japanese — not a transliteration of the English word.
The solution is a TOPIC_KEYWORDS map in data.js:
const TOPIC_KEYWORDS = {
ai: { es: 'inteligencia artificial', en: 'artificial intelligence', ja: '人工知能', ar: 'الذكاء الاصطناعي', /* 7 more */ },
cyber: { es: 'ciberseguridad', en: 'cybersecurity', ja: 'サイバーセキュリティ', /* 8 more */ },
// 8 more topics
};
One string per language, per topic. Switching the UI language and then selecting a topic sends the right keyword for that language. The results are genuinely different.
Geo-restriction detection
Some news sources (NYT, WSJ, The Times, Le Monde…) block readers outside certain countries. Finding out after you've already clicked the link is annoying.
NewsScope checks the article URL before opening it. If the domain matches a known geo-restricted list, it shows a warning modal first with the option to continue anyway — or to disable the warning permanently (saved in localStorage, so it persists across sessions until the user clears their storage).
function isLikelyGeoRestricted(url) {
try {
const host = new URL(url).hostname.replace(/^www\./, '');
return GEO_RESTRICTED.some(d => host === d || host.endsWith('.' + d));
} catch { return false; }
}
It's a static list, so it won't catch every case. But it handles the most common ones and the pattern is easy to extend.
Error handling that's actually useful
A search app with a generic "Something went wrong" message on every failure is frustrating to use. api.js maps every known failure to a specific, translated message with recovery actions:
function parseError(err) {
if (err.name === 'AbortError') return t('errTimeout'); // 12s hard limit
if (!navigator.onLine) return t('errOffline');
if (err.type === 'auth') return t('errAuth'); // bad or expired key
if (err.type === 'limit') return t('errLimit'); // rate limited
if (err.type === 'server') return t('errServer');
if (err.code === 'ParametersMissing' || ...) return t('errParams', err.msg);
return t('errGeneric', err.msg);
}
Each error type renders with contextual action buttons ("Change key", "Try again") so the user always has a clear next step. And since the error messages go through the same t() function as everything else, they're translated too.
Tradeoffs and future improvements
ES modules — Loading 9 scripts in a fixed order works, but native type="module" would make the dependency graph explicit and statically analyzable. The reason for not using them: opening index.html directly from the filesystem (file://) blocks module imports in some browsers, and keeping zero-server-required as a constraint felt worthwhile. A future version could drop that constraint and adopt ES modules without changing any other part of the architecture.
API key explanation in the UI — The key is stored in localStorage and requests go directly from your browser to newsdata.io — no intermediary server involved. The README covers this, but surfacing that context inside the app itself would be a better experience for users who don't read documentation before using a tool.
Try it
Free API key at newsdata.io/register — takes about a minute, no credit card.
Any suggestions for improving this would be greatly appreciated. 🙌
Top comments (0)