<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NewsAPI → RSS Controller</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; }
.card { @apply rounded-2xl shadow-sm border border-slate-200 bg-white; }
.label { @apply block text-sm font-semibold text-slate-700 mb-1; }
.input { @apply w-full rounded-xl border border-slate-300 px-3 py-2 outline-none focus:ring-2 focus:ring-indigo-500; }
.select { @apply w-full rounded-xl border border-slate-300 px-3 py-2 outline-none focus:ring-2 focus:ring-indigo-500 bg-white; }
.btn { @apply inline-flex items-center gap-2 rounded-xl bg-indigo-600 text-white px-4 py-2 font-semibold hover:bg-indigo-700 active:bg-indigo-800 disabled:opacity-50; }
.btn-secondary { @apply inline-flex items-center gap-2 rounded-xl bg-slate-200 text-slate-800 px-3 py-2 font-semibold hover:bg-slate-300; }
.pill { @apply inline-flex items-center gap-2 rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-700; }
code.url { @apply break-all text-sm text-indigo-700; }
</style>
</head>
<body class="min-h-screen bg-slate-50">
<main class="max-w-5xl mx-auto p-6 space-y-6">
<header class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-slate-900">NewsAPI → RSS Controller</h1>
<a class="pill" target="_blank" href="https://newsapi.org/">Powered by NewsAPI</a>
</header>
<section class="card p-5">
<div class="grid md:grid-cols-2 gap-4">
<div>
<label class="label">Worker Base URL</label>
<input id="baseUrl" class="input" placeholder="https://YOUR-SUBDOMAIN.workers.dev" />
<p class="text-xs text-slate-500 mt-1">Example: <span class="text-slate-700">https://lingering-voice-be3f.abhijit-acharya.workers.dev</span></p>
</div>
<div>
<label class="label">Mode</label>
<div class="flex gap-4 mt-2">
<label class="inline-flex items-center gap-2"><input type="radio" name="mode" value="top" class="accent-indigo-600" checked> <span>Top Headlines</span></label>
<label class="inline-flex items-center gap-2"><input type="radio" name="mode" value="search" class="accent-indigo-600"> <span>Keyword Search</span></label>
</div>
</div>
</div>
<div class="grid md:grid-cols-3 gap-4 mt-4" id="topFields">
<div>
<label class="label">Country</label>
<select id="country" class="select">
<option value="">— any —</option>
<option value="in">India</option>
<option value="us">United States</option>
<option value="gb">United Kingdom</option>
<option value="au">Australia</option>
<option value="ca">Canada</option>
<option value="de">Germany</option>
<option value="fr">France</option>
<option value="jp">Japan</option>
<option value="sg">Singapore</option>
</select>
</div>
<div>
<label class="label">Category</label>
<select id="category" class="select">
<option value="">— any —</option>
<option>business</option>
<option>entertainment</option>
<option>general</option>
<option>health</option>
<option>science</option>
<option>sports</option>
<option>technology</option>
</select>
</div>
<div>
<label class="label">Page Size (1–50)</label>
<input id="pageSize" type="number" min="1" max="50" value="5" class="input" />
</div>
</div>
<div class="grid md:grid-cols-3 gap-4 mt-4 hidden" id="searchFields">
<div class="md:col-span-2">
<label class="label">Keyword (q)</label>
<input id="q" class="input" placeholder="e.g. ai india, crypto, climate" />
</div>
<div>
<label class="label">Language</label>
<select id="language" class="select">
<option>en</option>
<option>hi</option>
<option>de</option>
<option>fr</option>
<option>es</option>
<option>it</option>
<option>ja</option>
<option>ru</option>
<option>zh</option>
</select>
</div>
</div>
<div class="flex flex-wrap items-center gap-3 mt-6">
<button id="build" class="btn">Build RSS URL</button>
<button id="copy" class="btn-secondary">Copy URL</button>
<a id="open" class="btn" target="_blank" href="#">Open in new tab</a>
<span id="status" class="text-sm text-slate-500"></span>
</div>
<div class="mt-4 p-3 bg-slate-50 rounded-xl border border-slate-200">
<div class="text-xs uppercase tracking-wide text-slate-500">RSS URL</div>
<code id="urlOut" class="url"></code>
</div>
</section>
<section class="card p-5">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Preview (first 10 items)</h2>
<button id="previewBtn" class="btn-secondary">Refresh Preview</button>
</div>
<ol id="preview" class="mt-4 space-y-3 list-decimal list-inside"></ol>
</section>
<footer class="text-center text-xs text-slate-500 py-6">Client-side UI only. Your Worker does the heavy lifting.</footer>
</main>
<script>
const els = {
baseUrl: document.getElementById('baseUrl'),
mode: () => document.querySelector('input[name="mode"]:checked')?.value,
topFields: document.getElementById('topFields'),
searchFields: document.getElementById('searchFields'),
country: document.getElementById('country'),
category: document.getElementById('category'),
pageSize: document.getElementById('pageSize'),
q: document.getElementById('q'),
language: document.getElementById('language'),
build: document.getElementById('build'),
copy: document.getElementById('copy'),
open: document.getElementById('open'),
urlOut: document.getElementById('urlOut'),
status: document.getElementById('status'),
previewBtn: document.getElementById('previewBtn'),
preview: document.getElementById('preview'),
};
// Prefill base URL if we know it
const saved = JSON.parse(localStorage.getItem('newsui') || '{}');
if (saved.baseUrl) els.baseUrl.value = saved.baseUrl;
if (saved.country) els.country.value = saved.country;
if (saved.category) els.category.value = saved.category;
if (saved.pageSize) els.pageSize.value = saved.pageSize;
if (saved.mode) document.querySelector(`input[name="mode"][value="${saved.mode}"]`)?.click();
if (saved.q) els.q.value = saved.q;
if (saved.language) els.language.value = saved.language;
function saveState() {
localStorage.setItem('newsui', JSON.stringify({
baseUrl: els.baseUrl.value.trim(),
country: els.country.value,
category: els.category.value,
pageSize: els.pageSize.value,
mode: els.mode(),
q: els.q.value,
language: els.language.value,
}));
}
function toggleModeUI() {
const m = els.mode();
if (m === 'top') { els.topFields.classList.remove('hidden'); els.searchFields.classList.add('hidden'); }
else { els.topFields.classList.add('hidden'); els.searchFields.classList.remove('hidden'); }
}
document.querySelectorAll('input[name="mode"]').forEach(r => r.addEventListener('change', () => { toggleModeUI(); buildUrl(); saveState(); }));
[els.baseUrl, els.country, els.category, els.pageSize, els.q, els.language].forEach(el => el.addEventListener('input', () => { buildUrl(); saveState(); }));
function buildUrl() {
const base = els.baseUrl.value.trim().replace(/\/$/, '');
if (!base) { els.urlOut.textContent = ''; return ''; }
const m = els.mode();
const ps = Math.max(1, Math.min(parseInt(els.pageSize.value || '5', 10), 50));
let url = base + '/rss?';
if (m === 'top') {
const params = new URLSearchParams();
if (els.country.value) params.set('country', els.country.value);
if (els.category.value) params.set('category', els.category.value);
params.set('pageSize', String(ps));
url += params.toString();
} else {
const q = els.q.value.trim();
const params = new URLSearchParams();
if (q) params.set('q', q);
params.set('language', els.language.value);
params.set('pageSize', String(ps));
url += params.toString();
}
els.urlOut.textContent = url;
els.open.href = url;
return url;
}
async function preview() {
els.status.textContent = 'Fetching…';
els.preview.innerHTML = '';
const url = buildUrl();
if (!url) { els.status.textContent = 'Enter your Worker base URL first.'; return; }
try {
const res = await fetch(url, { headers: { 'Accept': 'application/rss+xml' } });
if (!res.ok) throw new Error('HTTP ' + res.status);
const text = await res.text();
const doc = new window.DOMParser().parseFromString(text, 'application/xml');
const items = Array.from(doc.querySelectorAll('item')).slice(0, 10);
if (items.length === 0) {
els.status.textContent = 'No items in feed (try another combo or keyword).';
return;
}
const frag = document.createDocumentFragment();
items.forEach((it, i) => {
const li = document.createElement('li');
const title = it.querySelector('title')?.textContent || '(no title)';
const link = it.querySelector('link')?.textContent || '#';
li.innerHTML = `<a class="text-indigo-700 hover:underline" target="_blank" href="${link}">${title}</a>`;
frag.appendChild(li);
});
els.preview.appendChild(frag);
els.status.textContent = '';
} catch (err) {
els.status.textContent = 'Preview failed: ' + (err.message || err);
}
}
els.build.addEventListener('click', (e) => { e.preventDefault(); buildUrl(); });
els.copy.addEventListener('click', async (e) => {
e.preventDefault();
const url = buildUrl();
if (!url) return;
try { await navigator.clipboard.writeText(url); els.status.textContent = 'Copied!'; setTimeout(() => els.status.textContent = '', 1500); } catch {}
});
els.previewBtn.addEventListener('click', (e) => { e.preventDefault(); preview(); });
// Initial paint
toggleModeUI();
buildUrl();
</script>
</body>
</html>