Add /news/ page: live news aggregator across the political spectrum#4
Add /news/ page: live news aggregator across the political spectrum#4
Conversation
Creates news/index.html — a client-side news aggregator that fetches live RSS feeds from 6 sources (The Guardian, NPR, BBC, AP, Fox News, Washington Times) via rss2json.com, rendered in a three-column Left / Center / Right layout. Requests are staggered 700ms apart to respect the free-tier rate limit. Each source renders independently with loading skeletons and error states with retry. Styled to match the existing dark portfolio theme with column-color-coded hover accents. Also adds "News" nav links to index.html (desktop nav, mobile nav, footer nav). https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
📝 WalkthroughWalkthroughAdds a “News” navigation link (header, mobile menu, footer) and a new Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser Client
participant API as RSS-to-JSON API
participant Feeds as RSS Feed Sources
Browser->>Browser: Build 3-column UI and per-source stubs
loop For each RSS source (staggered)
Browser->>Browser: Show skeleton loader
Browser->>API: Request feed JSON
API->>Feeds: Fetch RSS XML
Feeds-->>API: Return RSS XML
API-->>Browser: Return JSON feed items
Browser->>Browser: Render headline cards and update lean counts
end
Browser->>Browser: Update "last updated" timestamp
alt User triggers Refresh (refreshAll)
Browser->>Browser: Clear columns, rebuild stubs, increment generation
loop Staggered per-source
Browser->>API: Re-fetch feed
API-->>Browser: JSON items
Browser->>Browser: Render refreshed cards (only if generation matches)
end
Browser->>Browser: Update timestamp
end
alt Fetch error for a source
Browser->>Browser: Replace source area with error panel + retry button
Browser->>API: retrySingle(srcId)
API-->>Browser: JSON items or error
Browser->>Browser: Update or persist error state
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@news/index.html`:
- Around line 351-356: The current truncate() and feed rendering logic treat
feed payloads as HTML and write msg/item.link directly into markup, which
enables XSS or javascript: URL injection; fix by creating DOM nodes
programmatically (use document.createElement for anchors and containers), assign
text via textContent (not innerHTML), and set href only after validating with
the URL constructor to allow only http: or https: schemes (otherwise omit or
replace with a safe placeholder); update all usages of truncate(), item.link and
msg in the rendering code to use this safe node-building and URL validation
approach so feed text is never inserted as HTML and links cannot be javascript:
or data: URLs.
- Around line 485-499: The current fetchAll/fetchSource flow lets responses from
earlier refreshes overwrite later UI and double-increment leanCounts; fix by
introducing a refresh generation token (e.g., a numeric currentRefreshGen)
incremented at start of fetchAll, capture localGen in each spawned fetch, and
pass it into fetchSource or check it when handling responses so any response
with gen !== currentRefreshGen is ignored; also collect per-source promises (map
SOURCES to fetchSourcePromise(localGen)) and await Promise.all on them before
updating the timestamp and removing the spinner (manipulate elements
'refresh-btn' and 'last-refresh' only when the awaited generation completes) so
UI flips only after that generation finishes.
- Around line 98-99: The page uses custom media queries targeting .spectrum-grid
to switch from multi-column to two-column and single-column layouts; remove
those `@media` rules and switch the layout to W3.CSS responsive classes instead:
replace the .spectrum-grid styling by using W3.CSS grid helpers (e.g., wrap
items in a w3-row and give each grid item classes like w3-third / w3-half /
w3-full or combine w3-col variants such as w3-third w3-half-small w3-full to
achieve desktop: 3 cols, <=1050px: 2 cols, <=680px: 1 col) and use w3-hide-small
/ w3-hide-medium if you need to hide/show elements at breakpoints; update the
markup where .spectrum-grid items are rendered (and the duplicated rules noted
around lines 186-190) and delete the two `@media` blocks so all responsive
behavior comes from W3.CSS classes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7ffe5d59-32a3-483d-9a3b-53caacf3bc14
📒 Files selected for processing (2)
index.htmlnews/index.html
- renderItems/renderError: build DOM nodes programmatically (no innerHTML for external data); set text via textContent; validate item.link with URL constructor, allowing only http:/https: schemes - fetchAll/fetchSource: introduce currentRefreshGen counter incremented on each refresh; pass localGen into fetchSource and discard responses where gen !== currentRefreshGen; use Promise.all on staggered fetches so spinner and timestamp update only after the generation fully completes - Layout: remove custom .spectrum-grid CSS grid rules and @media breakpoints; add W3.CSS CDN and use w3-row / w3-col l4 m6 s12 for responsive 3→2→1 column layout https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
news/index.html (2)
177-181:⚠️ Potential issue | 🟡 MinorReplace this remaining width breakpoint with W3.CSS classes.
This block still hard-codes responsive spacing in CSS. Please move these adjustments to W3.CSS classes/utilities so the page stays aligned with the repo’s HTML responsiveness rule.
As per coding guidelines,
**/*.html: Use W3.CSS responsive classes (such as w3-hide-small) for responsive design instead of writing custom CSS media queries.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@news/index.html` around lines 177 - 181, The `@media` block hard-coding responsive spacing should be removed and replaced with W3.CSS responsive utility classes: remove the media query that targets `#nav`, .container, and .page-meta and instead apply appropriate W3.CSS classes directly in the HTML (for example add w3-padding or w3-padding-small to `#nav`, use w3-container or w3-padding on .container, and use w3-row/w3-bar or spacing utilities for .page-meta to control gap across breakpoints); update the HTML elements to include these classes and delete the CSS selectors in the diff so responsiveness is driven by W3.CSS utilities rather than custom media queries.
521-523:⚠️ Potential issue | 🟠 MajorMake refresh clicks a no-op while a batch is already running.
Line 523 is cosmetic only; it does not stop a second click from launching another six-source batch. Two quick clicks already schedule 12 rss2json requests, so the page can blow past the “10 req/min” budget even though older results are discarded later.
Also applies to: 532-539
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@news/index.html` around lines 521 - 523, The fetchAll function and its associated refresh click handler currently only add a spinning class but do not prevent concurrent executions; introduce a module-level/in-scope boolean flag (e.g., isFetching) used by fetchAll to return immediately if true, set isFetching = true at start and false in all completion paths (success or error) so multiple rapid clicks cannot launch overlapping batches, and ensure the refresh button's class toggling (element id 'refresh-btn') and any promise rejection paths clear the flag so the UI and state remain consistent; apply the same guard to the code referenced around lines 532-539 (the click handler) so clicks become a no-op while a batch is running.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@news/index.html`:
- Around line 558-560: The retrySingle function is calling fetchSource without
the current generation token, causing the stale-response guards in fetchSource
to bail; update retrySingle to accept or obtain the active generation token and
call fetchSource with it (i.e., invoke fetchSource(src, gen) or fetchSource(src,
currentGen)), and ensure the fetchSource signature and callers support the
generation/token parameter so the stale-response checks (in fetchSource's
guards) receive the active token during retries.
---
Duplicate comments:
In `@news/index.html`:
- Around line 177-181: The `@media` block hard-coding responsive spacing should be
removed and replaced with W3.CSS responsive utility classes: remove the media
query that targets `#nav`, .container, and .page-meta and instead apply
appropriate W3.CSS classes directly in the HTML (for example add w3-padding or
w3-padding-small to `#nav`, use w3-container or w3-padding on .container, and use
w3-row/w3-bar or spacing utilities for .page-meta to control gap across
breakpoints); update the HTML elements to include these classes and delete the
CSS selectors in the diff so responsiveness is driven by W3.CSS utilities rather
than custom media queries.
- Around line 521-523: The fetchAll function and its associated refresh click
handler currently only add a spinning class but do not prevent concurrent
executions; introduce a module-level/in-scope boolean flag (e.g., isFetching)
used by fetchAll to return immediately if true, set isFetching = true at start
and false in all completion paths (success or error) so multiple rapid clicks
cannot launch overlapping batches, and ensure the refresh button's class
toggling (element id 'refresh-btn') and any promise rejection paths clear the
flag so the UI and state remain consistent; apply the same guard to the code
referenced around lines 532-539 (the click handler) so clicks become a no-op
while a batch is running.
retrySingle called fetchSource(src) without the generation token introduced in the previous commit, so gen was undefined and the gen !== currentRefreshGen guard always returned early. Pass currentRefreshGen so retries render correctly. https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@news/index.html`:
- Around line 474-487: The fetch in fetchSource can hang; wrap each request with
an AbortController and a timeout: create an AbortController before calling
fetch(apiUrl, { signal }), start a setTimeout that calls controller.abort()
after a configurable timeout (e.g., 10s), and clear that timeout after fetch
resolves; catch AbortError in the catch block and call renderError(src, 'Request
timed out') (or append e.message) while still checking gen !== currentRefreshGen
as already done; ensure the timeout/abort logic references fetchSource, API,
ITEMS_PER_SOURCE, currentRefreshGen, renderSkeleton and renderError so the
per-source error state surfaces on timeout.
- Around line 501-515: The source blocks are appended without initial skeletons
so later columns remain blank until staggered network loaders run; in
buildColumns() (the loop over SOURCES / usage of LEAN_COLS and the created
element with id `src-${src.id}`) insert a skeleton/placeholder markup into the
`div id="src-${src.id}"` when creating the stub (e.g., basic skeleton DOM or
loader HTML/CSS classes) so every source shows a loader immediately, and update
the stagger logic to only delay the network/fetch calls (leave the DOM skeletons
rendered instantly).
- Around line 521-552: The refresh path currently allows concurrent batches;
prevent overlap by adding a simple in-flight guard: introduce a boolean flag
(e.g., fetchInProgress) or reuse currentRefreshGen semantics to detect an active
batch and early-return from refreshAll if a batch is running; set the flag
before calling buildColumns()/fetchAll(), clear it after fetchAll finishes
(await its promise) or when the batch is canceled, and also disable the refresh
button (refresh-btn) while in-flight so the UI cannot trigger additional
batches; ensure fetchAll still increments/uses currentRefreshGen and that the
flag is cleared in all completion/error paths.
| async function fetchSource(src, gen) { | ||
| renderSkeleton('src-' + src.id); | ||
| try { | ||
| const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`; | ||
| const res = await fetch(apiUrl); | ||
| if (!res.ok) throw new Error('HTTP ' + res.status); | ||
| const data = await res.json(); | ||
| if (data.status !== 'ok') throw new Error(data.message || 'Feed error'); | ||
| if (gen !== currentRefreshGen) return; | ||
| renderItems(src, data.items); | ||
| } catch (e) { | ||
| if (gen !== currentRefreshGen) return; | ||
| renderError(src, 'Could not load ' + src.name + '. ' + (e.message || '')); | ||
| } |
There was a problem hiding this comment.
Add a timeout to each feed request.
If rss2json hangs instead of erroring, this fetch() never settles and Promise.all() leaves the page spinning forever. Time out the request and surface it through the existing per-source error state.
🛠️ Minimal fix
async function fetchSource(src, gen) {
renderSkeleton('src-' + src.id);
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`;
- const res = await fetch(apiUrl);
+ const res = await fetch(apiUrl, { signal: controller.signal });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (data.status !== 'ok') throw new Error(data.message || 'Feed error');
if (gen !== currentRefreshGen) return;
renderItems(src, data.items);
} catch (e) {
if (gen !== currentRefreshGen) return;
renderError(src, 'Could not load ' + src.name + '. ' + (e.message || ''));
+ } finally {
+ clearTimeout(timeoutId);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function fetchSource(src, gen) { | |
| renderSkeleton('src-' + src.id); | |
| try { | |
| const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`; | |
| const res = await fetch(apiUrl); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| if (data.status !== 'ok') throw new Error(data.message || 'Feed error'); | |
| if (gen !== currentRefreshGen) return; | |
| renderItems(src, data.items); | |
| } catch (e) { | |
| if (gen !== currentRefreshGen) return; | |
| renderError(src, 'Could not load ' + src.name + '. ' + (e.message || '')); | |
| } | |
| async function fetchSource(src, gen) { | |
| renderSkeleton('src-' + src.id); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 10000); | |
| try { | |
| const apiUrl = `${API}?rss_url=${encodeURIComponent(src.rss)}&count=${ITEMS_PER_SOURCE}`; | |
| const res = await fetch(apiUrl, { signal: controller.signal }); | |
| if (!res.ok) throw new Error('HTTP ' + res.status); | |
| const data = await res.json(); | |
| if (data.status !== 'ok') throw new Error(data.message || 'Feed error'); | |
| if (gen !== currentRefreshGen) return; | |
| renderItems(src, data.items); | |
| } catch (e) { | |
| if (gen !== currentRefreshGen) return; | |
| renderError(src, 'Could not load ' + src.name + '. ' + (e.message || '')); | |
| } finally { | |
| clearTimeout(timeoutId); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@news/index.html` around lines 474 - 487, The fetch in fetchSource can hang;
wrap each request with an AbortController and a timeout: create an
AbortController before calling fetch(apiUrl, { signal }), start a setTimeout
that calls controller.abort() after a configurable timeout (e.g., 10s), and
clear that timeout after fetch resolves; catch AbortError in the catch block and
call renderError(src, 'Request timed out') (or append e.message) while still
checking gen !== currentRefreshGen as already done; ensure the timeout/abort
logic references fetchSource, API, ITEMS_PER_SOURCE, currentRefreshGen,
renderSkeleton and renderError so the per-source error state surfaces on
timeout.
| SOURCES.forEach(src => { | ||
| const colEl = document.getElementById(LEAN_COLS[src.lean]); | ||
| if (!colEl) return; | ||
|
|
||
| // Create stub for this source | ||
| const block = document.createElement('div'); | ||
| block.className = 'source-block'; | ||
| block.innerHTML = ` | ||
| <div class="source-name"> | ||
| <i class="${src.icon}"></i> ${src.name} | ||
| </div> | ||
| <div id="src-${src.id}"></div> | ||
| `; | ||
| colEl.appendChild(block); | ||
| }); |
There was a problem hiding this comment.
Render all source skeletons before the stagger starts.
Right now the stagger delays the loaders too, so later sources sit blank for up to 3.5 seconds. Seed each src-* stub with a skeleton in buildColumns() and keep the stagger only on the network calls.
💡 Minimal fix
block.innerHTML = `
<div class="source-name">
<i class="${src.icon}"></i> ${src.name}
</div>
<div id="src-${src.id}"></div>
`;
colEl.appendChild(block);
+ renderSkeleton(`src-${src.id}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SOURCES.forEach(src => { | |
| const colEl = document.getElementById(LEAN_COLS[src.lean]); | |
| if (!colEl) return; | |
| // Create stub for this source | |
| const block = document.createElement('div'); | |
| block.className = 'source-block'; | |
| block.innerHTML = ` | |
| <div class="source-name"> | |
| <i class="${src.icon}"></i> ${src.name} | |
| </div> | |
| <div id="src-${src.id}"></div> | |
| `; | |
| colEl.appendChild(block); | |
| }); | |
| SOURCES.forEach(src => { | |
| const colEl = document.getElementById(LEAN_COLS[src.lean]); | |
| if (!colEl) return; | |
| // Create stub for this source | |
| const block = document.createElement('div'); | |
| block.className = 'source-block'; | |
| block.innerHTML = ` | |
| <div class="source-name"> | |
| <i class="${src.icon}"></i> ${src.name} | |
| </div> | |
| <div id="src-${src.id}"></div> | |
| `; | |
| colEl.appendChild(block); | |
| renderSkeleton(`src-${src.id}`); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@news/index.html` around lines 501 - 515, The source blocks are appended
without initial skeletons so later columns remain blank until staggered network
loaders run; in buildColumns() (the loop over SOURCES / usage of LEAN_COLS and
the created element with id `src-${src.id}`) insert a skeleton/placeholder
markup into the `div id="src-${src.id}"` when creating the stub (e.g., basic
skeleton DOM or loader HTML/CSS classes) so every source shows a loader
immediately, and update the stagger logic to only delay the network/fetch calls
(leave the DOM skeletons rendered instantly).
| async function fetchAll() { | ||
| const btn = document.getElementById('refresh-btn'); | ||
| if (btn) btn.classList.add('spinning'); | ||
|
|
||
| const localGen = ++currentRefreshGen; | ||
|
|
||
| // Stagger fetches ~700ms apart (safe for free tier: 10 req/min), then await all | ||
| const promises = SOURCES.map((src, i) => | ||
| delay(i * 700).then(() => fetchSource(src, localGen)) | ||
| ); | ||
|
|
||
| await Promise.all(promises); | ||
|
|
||
| // Only update UI if no newer refresh has started | ||
| if (localGen !== currentRefreshGen) return; | ||
|
|
||
| const el = document.getElementById('last-refresh'); | ||
| if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`; | ||
| if (btn) btn.classList.remove('spinning'); | ||
| } | ||
|
|
||
| // ───────────────────────────────────────── | ||
| // Public: refresh all (rebuild + fetch) | ||
| // ───────────────────────────────────────── | ||
| function refreshAll() { | ||
| // Clear column content | ||
| Object.values(LEAN_COLS).forEach(id => { | ||
| const el = document.getElementById(id); | ||
| if (el) el.innerHTML = ''; | ||
| }); | ||
| buildColumns(); | ||
| fetchAll(); |
There was a problem hiding this comment.
Do not start a new batch while one is already in flight.
The generation token protects the DOM, but it does not protect the API quota. The initial load already schedules six requests, so one quick click on Refresh sends six more and can exceed the stated 10 req/min budget. Ignore/disable refreshes until the current batch finishes, or cancel the older batch before starting the new one.
🛠️ One straightforward guard
+ let isRefreshing = false;
+
async function fetchAll() {
+ if (isRefreshing) return;
+ isRefreshing = true;
const btn = document.getElementById('refresh-btn');
- if (btn) btn.classList.add('spinning');
+ if (btn) {
+ btn.classList.add('spinning');
+ btn.disabled = true;
+ }
- const localGen = ++currentRefreshGen;
-
- // Stagger fetches ~700ms apart (safe for free tier: 10 req/min), then await all
- const promises = SOURCES.map((src, i) =>
- delay(i * 700).then(() => fetchSource(src, localGen))
- );
-
- await Promise.all(promises);
-
- // Only update UI if no newer refresh has started
- if (localGen !== currentRefreshGen) return;
-
- const el = document.getElementById('last-refresh');
- if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`;
- if (btn) btn.classList.remove('spinning');
+ try {
+ const localGen = ++currentRefreshGen;
+ const promises = SOURCES.map((src, i) =>
+ delay(i * 700).then(() => fetchSource(src, localGen))
+ );
+
+ await Promise.all(promises);
+
+ if (localGen === currentRefreshGen) {
+ const el = document.getElementById('last-refresh');
+ if (el) el.innerHTML = `<i class="fas fa-check-circle" style="color:var(--secondary)"></i> Updated ${new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}`;
+ }
+ } finally {
+ isRefreshing = false;
+ if (btn) {
+ btn.classList.remove('spinning');
+ btn.disabled = false;
+ }
+ }
}
function refreshAll() {
+ if (isRefreshing) return;
// Clear column content
Object.values(LEAN_COLS).forEach(id => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@news/index.html` around lines 521 - 552, The refresh path currently allows
concurrent batches; prevent overlap by adding a simple in-flight guard:
introduce a boolean flag (e.g., fetchInProgress) or reuse currentRefreshGen
semantics to detect an active batch and early-return from refreshAll if a batch
is running; set the flag before calling buildColumns()/fetchAll(), clear it
after fetchAll finishes (await its promise) or when the batch is canceled, and
also disable the refresh button (refresh-btn) while in-flight so the UI cannot
trigger additional batches; ensure fetchAll still increments/uses
currentRefreshGen and that the flag is cleared in all completion/error paths.
Creates news/index.html — a client-side news aggregator that fetches live RSS feeds from 6 sources (The Guardian, NPR, BBC, AP, Fox News, Washington Times) via rss2json.com, rendered in a three-column Left / Center / Right layout. Requests are staggered 700ms apart to respect the free-tier rate limit. Each source renders independently with loading skeletons and error states with retry. Styled to match the existing dark portfolio theme with column-color-coded hover accents. Also adds "News" nav links to index.html (desktop nav, mobile nav, footer nav).
https://claude.ai/code/session_016goywvdJ4VkKQa8SmLTQu9
Summary by CodeRabbit