RSS Reader
Browser-based RSS/Atom feed reader. Subscribe to feeds, browse articles, and track read/unread state — all in a single HTML file with no server required.
Layout
Two-panel layout: feed sidebar (left) · article list (right). Each article row links out to the original; no in-app reading pane.
UI Approach
Prefer DaisyUI components wherever there is a natural fit (e.g. badge, menu, input, btn, collapse). Fall back to Tailwind utilities only when no DaisyUI component fits.
Theme follows the OS prefers-color-scheme (dark/light). Set via an inline <script> before any stylesheets load: reads window.matchMedia('(prefers-color-scheme: dark)') and writes data-theme on <html>, with a change listener to keep it in sync if the user switches OS theme at runtime.
UI Components
Feed Sidebar
DaisyUI tabs (tabs tabs-border tabs-sm) under the header switch the grouping dimension: author · platform · topics. The list below shows the distinct values of the active dimension (alphabetical), derived from the slider-filtered feed set. A value can span multiple feeds (e.g. author Greg Hurrell = blog + YouTube; platform youtube = all channels). Topics are tags: a feed carries one or more and appears under each of them.
Single-select: clicking a value filters articles to feeds matching it; clicking again deselects. "All" entry at top is active when nothing is selected (filter() === null). Switching tabs clears the selection back to All. Each value row has a ↻ button that refreshes every feed in that category.
Below the feed list: a search box, then a range slider with a mode label. The label toggles between age and velocity modes on click. Slider steps and labels depend on mode:
- age mode — steps 0–7: 12h, 1d, 2d, 4d, 1w, 2w, 1mo, ∞. Defaults to ∞. Hides feeds whose latest item is older than the limit.
- velocity mode — steps 0–7: 1/mo, 2/mo, 1/w, 2/w, 3/w, 1/d, 2/d, ∞. Defaults to ∞. Hides feeds whose average posting rate exceeds the limit.
Article List
- One row per item: title, author (if available), date, unread dot indicator
- Clicking a row marks it read and opens the link in a new tab
- Empty state: "No articles" / "No results" for search/filter
Feed Configuration
Hardcoded array of feed objects at the top of the script: {url, author, platform, topics}. Author, platform, and topics are set manually in this table — never derived from the feed. topics is a list of tags (one or more per feed). The table below is the authoritative list; the FEEDS array in the HTML mirrors it row by row. FEED_META (Map<url, feed-object>) is derived from FEEDS for per-item lookups.
Platform values: blog · substack · youtube
Topic tags (process-mode taxonomy à la Bonnitta Roy): craft · technosphere · ways-of-knowing · process-metaphysics · interiority · soma · cosmos · planetary
Dependencies
| Library | Version | Purpose |
|---|---|---|
| daisyui | 5 | UI components via CDN link tag |
| @tailwindcss/browser | 4 | Utility CSS via CDN script tag |
| solid-js | latest | Reactive signals, memos, effects, For/Show components |
| solid-js/web | latest | DOM rendering via render() |
| solid-js/html | latest | No-build tagged template literal UI (html\...``) |
State (localStorage)
| Key | Type | Description |
|---|---|---|
rss-feeds |
JSON string of [url, feed][] |
Cached feed data |
rss-search |
string | Current search query |
rss-tab |
"author" | "platform" | "topics" |
Active sidebar tab; defaults to "author" |
rss-filter |
string or absent | Selected value in the active tab's dimension; absent = no filter (All) |
rss-age |
number (ms) or absent | Age limit; absent = ∞ |
rss-velocity |
number (posts/week) or absent | Velocity limit; absent = ∞ |
rss-slider-mode |
"age" | "velocity" |
Active slider mode; defaults to "age" |
State (SolidJS)
Signals via createSignal, derived values via createMemo, side effects via createEffect.
| Name | Type | Description |
|---|---|---|
feeds |
Signal<Map<url, {title, items[]}>> |
All cached feed data |
search |
Signal<string> |
Current search query |
tab |
Signal<"author"|"platform"|"topics"> |
Active sidebar tab dimension |
filter |
Signal<string|null> |
Selected value in the active dimension; null = show all |
ageLimit |
Signal<number|null> |
Max item age in ms; null = ∞ |
velocityLimit |
Signal<number|null> |
Max posts/week; null = ∞ |
sliderMode |
Signal<"age"|"velocity"> |
Which slider mode is active |
feedList |
Memo<[url, feed][]> |
All feeds as array |
activeFeedList |
Memo<[url, feed][]> |
Feeds filtered by current slider mode/value |
categoryValues |
Memo<string[]> |
Distinct values of the active dimension across activeFeedList (topic lists flattened), alphabetical |
allItems |
Memo<Item[]> |
All items across all feeds, sorted newest-first |
ageLimitedItems |
Memo<Item[]> |
allItems filtered by age or velocity mode |
visibleItems |
Memo<Item[]> |
ageLimitedItems filtered by filter (via FEED_META on the active dimension; topics match any tag) and search |
last24hCount |
Memo<number> |
Count of items published in the last 24h |
Key Functions
fetchFeed(url)
Fetches XML via https://proxy.namjul.deno.net/?url=<encoded>, returns parsed {title, items[]}.
parseFeed(xml, url) → parseRSS / parseAtom
Detects RSS vs Atom by root element tag (rss/rdf:rdf → RSS, feed → Atom). Per-item fields: id, title, link, author, topics, pubDate, snippet, wordCount. Atom uses only <published> for pubDate (not <updated>).
refreshFeed(url)
Fetches a single feed and merges new/updated items into the feeds signal, preserving existing items not in the fresh response.
refreshAll()
Calls refreshFeed for every URL in FEEDS concurrently.
refreshCategory(value)
Calls refreshFeed for every feed whose active-dimension category matches value (for topics: any tag matches).
User Interactions
- On load: render from localStorage immediately, then refresh all feeds in the background — UI updates as each feed resolves
- Filter by category: pick a tab (author/platform/topics), click a value row → single-select; click again or "All" to clear; switching tabs clears the selection
- Filter by age/velocity: drag slider; click slider label to toggle mode
- Read: click article row → link opens in new tab, item marked read
- Search: type in search box → filters by title/author in real time
- Refresh: click ↻ on a value row → re-fetch every feed in that category