Skip to content

feat(rss-reader): migrate and redesign RSS reader app#780

Draft
nicomiguelino wants to merge 12 commits intomasterfrom
feat/migrate-and-redesign-rss-reader
Draft

feat(rss-reader): migrate and redesign RSS reader app#780
nicomiguelino wants to merge 12 commits intomasterfrom
feat/migrate-and-redesign-rss-reader

Conversation

@nicomiguelino
Copy link
Copy Markdown
Contributor

@nicomiguelino nicomiguelino commented Apr 14, 2026

User description

Summary

  • Rename old app to rss-reader-old and scaffold new app with Bun/Vite/TypeScript
  • Replace vendored JS libs (rss-parser, moment, Alpine, offline geocode) with @rowanmanning/feed-parser and @screenly/edge-apps utilities
  • Redesign UI with glassmorphism card layout (landscape: 3-col, portrait: 2-col) based on Figma designs
  • Add CORS proxy server to dev workflow

PR Type

Enhancement, Tests, Documentation


Description

  • Scaffold Bun/Vite TypeScript RSS app

  • Render cached, localized RSS feed cards

  • Add responsive glassmorphism layout and header

  • Archive legacy app and add screenshots


Diagram Walkthrough

flowchart LR
  A["Screenly settings"]
  B["CORS-aware feed fetch"]
  C["Parsed RSS entries"]
  D["Local cache fallback"]
  E["Responsive card UI"]
  F["Screenshot tests"]
  A -- "configure" --> B
  B -- "parse into" --> C
  C -- "cache for failures" --> D
  C -- "render as" --> E
  E -- "validated by" --> F
Loading

File Walkthrough

Relevant files
Tests
1 files
screenshots.spec.ts
Capture app screenshots across supported resolutions         
+41/-0   
Enhancement
3 files
main.ts
Fetch, cache, and render RSS entries                                         
+159/-0 
style.css
Add glassmorphism responsive feed card styles                       
+347/-0 
index.html
Replace static layout with template-driven shell                 
+44/-81 
Miscellaneous
4 files
main.js
Archive legacy Alpine RSS reader logic                                     
[link]   
common.css
Archive shared legacy RSS layout styles                                   
[link]   
style.css
Archive legacy responsive card styling                                     
[link]   
index.html
Preserve legacy Alpine-based RSS markup                                   
+82/-0   
Configuration changes
8 files
.ignore
Add archived QC manifest ignore rule                                         
+1/-0     
deployed-apps.yml
Add archived deployed RSS app definitions                               
[link]   
screenly.yml
Preserve legacy Screenly manifest settings                             
+82/-0   
screenly_qc.yml
Preserve legacy QC manifest settings                                         
+82/-0   
.ignore
Ignore `node_modules` in migrated app                                       
+1/-1     
screenly.yml
Update manifest categories and settings                                   
+7/-17   
screenly_qc.yml
Sync QC manifest with new settings                                             
+7/-17   
tsconfig.json
Add TypeScript config for new source                                         
+8/-0     
Documentation
3 files
DEPLOYMENT.md
Document multi-instance legacy deployment workflow             
[link]   
README.md
Preserve legacy setup and configuration guide                       
+95/-0   
README.md
Rewrite README for Bun development workflow                           
+25/-68 
Dependencies
1 files
package.json
Add Bun tooling and RSS dependencies                                         
+39/-0   
Additional files
1 files
bg.webp [link]   

- Rename old app directory to rss-reader-old
- Scaffold new app using edge-app-template with Bun/Vite/TypeScript
- Replace vendored JS libs with @rowanmanning/feed-parser and @screenly/edge-apps utilities
- Redesign UI with glassmorphism card layout from Figma (landscape 3-col, portrait 2-col)
- Add CORS proxy server to dev workflow via run-p
@github-actions
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Invalid interval

cache_interval is parsed without any fallback or validation. If the setting is empty or non-numeric, the computed delay becomes NaN, which setInterval treats like 0, so the app starts refetching the feed in a tight loop. A misconfigured instance would continuously hammer the RSS endpoint/CORS proxy and rerender the page.

const cacheInterval =
  parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000

const loadAndRender = async () => {
  try {
    const entries = await fetchFeed(rssUrl)
    renderCards(entries, rssTitle)
    saveCache(entries)
    showGrid()
  } catch (err) {
    console.error('RSS fetch failed:', err)
    const cached = loadCache()
    if (cached.length > 0) {
      renderCards(cached, rssTitle)
      showGrid()
    } else {
      showError()
    }
  }
}

await loadAndRender()
signalReady()
setInterval(loadAndRender, cacheInterval)
Portrait layout

The portrait breakpoint switches the grid to a single column with two rows, so cards stack vertically instead of rendering in the intended two-column layout. On portrait displays this leaves half the width unused and does not match the redesign described for portrait mode.

@media (orientation: portrait) {
  .feed-grid {
    grid-template-columns: 1fr;
    grid-template-rows: repeat(2, 1fr);
    padding: 0 1rem 1rem;
  }

@github-actions
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Validate refresh interval

Validate cache_interval before passing it to setInterval. If the setting is
non-numeric or 0, the delay becomes NaN/0 and the app can enter a near-tight refresh
loop that floods the feed endpoint.

edge-apps/rss-reader/src/main.ts [135-136]

+const cacheIntervalSeconds = Number.parseInt(
+  getSettingWithDefault<string>('cache_interval', '1800'),
+  10,
+)
 const cacheInterval =
-  parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000
+  Number.isFinite(cacheIntervalSeconds) && cacheIntervalSeconds > 0
+    ? cacheIntervalSeconds * 1000
+    : 1800 * 1000
Suggestion importance[1-10]: 8

__

Why: This is a valid safeguard: an invalid or non-positive cache_interval can make setInterval run effectively as fast as possible and hammer the feed endpoint. The proposed change is accurate for cacheInterval and materially improves resilience against bad configuration.

Medium
Guard concurrent refreshes

Prevent overlapping refreshes when a fetch takes longer than cacheInterval.
Concurrent loadAndRender executions can race, overwrite newer data with older
results, and multiply outbound requests under slow network conditions.

edge-apps/rss-reader/src/main.ts [156-158]

 await loadAndRender()
 signalReady()
-setInterval(loadAndRender, cacheInterval)
 
+let isRefreshing = false
+setInterval(async () => {
+  if (isRefreshing) return
+  isRefreshing = true
+  try {
+    await loadAndRender()
+  } finally {
+    isRefreshing = false
+  }
+}, cacheInterval)
+
Suggestion importance[1-10]: 7

__

Why: This is a sound improvement because setInterval(loadAndRender, cacheInterval) can start a new async refresh before the prior one finishes. Adding an isRefreshing guard reduces duplicate requests and prevents stale renderCards/saveCache updates from racing each other.

Medium
Reject empty feed results

Treat an empty parsed feed as a failure instead of rendering an empty grid. Right
now a valid but empty response leaves the screen blank and skips the cache fallback
path entirely.

edge-apps/rss-reader/src/main.ts [140-143]

 const entries = await fetchFeed(rssUrl)
+if (entries.length === 0) {
+  throw new Error('Feed contains no entries')
+}
 renderCards(entries, rssTitle)
 saveCache(entries)
 showGrid()
Suggestion importance[1-10]: 5

__

Why: This suggestion is consistent with the current fallback design, since an empty entries array currently shows a blank feed-grid instead of using cached data or the error state. However, an empty feed can be a legitimate result, so this is more of a product-behavior improvement than a clear correctness bug.

Low

- Remove rss-reader-old/ directory
- Move deployed-apps.yml and DEPLOYMENT.md to rss-reader/
- Rename static/images/ to static/img/ to match old structure
- Sync screenly.yml and screenly_qc.yml from old app
- Reformat SVG attributes in index.html
- Reformat long line in main.ts
- Move feed card template outside #feed-grid to prevent it being wiped by innerHTML reset
- Add high-specificity [hidden] rules to prevent .feed-error display:flex from overriding hidden attribute
- Fix background image path from static/images/ to static/img/
- Replace background with sunny image
- Redesign cards with glass-morphism style
- Add locale/TZ-aware date above the feed grid
- Fix portrait layout to position cards at the bottom
- Fix hidden element CSS specificity conflict
- Update e2e screenshots with mock RSS data
- Remove unused rssTitle parameter
- Split error message onto two lines for better visual balance
- Increase text-muted opacity to 0.8
- Add line-height, padding-bottom, and text-align to center error text
- Add display_errors setting for panic-overlay error handling
- Remove unused theme setting and setupTheme call
- Update README to reflect current settings
- Extract stripHtml, loadCache, and saveCache into utils.ts
- Replace local formatDate with formatLocalizedDate from @screenly/edge-apps
- Add unit tests for stripHtml, loadCache, and saveCache
- Pass locale and timezone into fetchFeed to avoid repeated async calls
- Return early in catch block to eliminate duplicate renderCards/showGrid
- Add feed-card-source element to card template
- Display rss_title as source label on each card
- Style source label as small, muted, uppercase text
- Group title, date, and source in feed-card-meta wrapper
- Update docs to reflect rss_title usage
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the RSS Reader edge app from the legacy static/Alpine implementation to a new Bun/Vite/TypeScript app using @screenly/edge-apps utilities and @rowanmanning/feed-parser, with a redesigned responsive “glassmorphism” card UI and updated deployment/testing workflow.

Changes:

  • Replaced legacy Alpine-based runtime (vendored libs + static JS/CSS) with a TypeScript app that fetches/parses RSS, formats localized dates, and renders template-based cards.
  • Added caching utilities + unit tests and Playwright screenshot coverage.
  • Updated manifests, docs, and tooling for the new Bun dev/build workflow (including a dev CORS proxy).

Reviewed changes

Copilot reviewed 17 out of 40 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
edge-apps/rss-reader/src/main.ts New TS app entry: fetch/parse/render, caching fallback, localization, ready signal
edge-apps/rss-reader/src/utils.ts HTML stripping + localStorage cache read/write helpers
edge-apps/rss-reader/src/utils.test.ts Unit tests for utils (stripHtml + cache load/save)
edge-apps/rss-reader/src/css/style.css New responsive glassmorphism layout and typography
edge-apps/rss-reader/index.html New template-driven shell using auto-scaler + @screenly/edge-apps/components
edge-apps/rss-reader/package.json Bun/Vite/TS tooling, scripts, dependencies
edge-apps/rss-reader/tsconfig.json TS config extending @screenly/edge-apps baseline
edge-apps/rss-reader/e2e/screenshots.spec.ts Playwright screenshot test with mocked Screenly + RSS
edge-apps/rss-reader/screenly.yml Updated manifest settings (adds display_errors, removes legacy theme)
edge-apps/rss-reader/screenly_qc.yml QC manifest kept in sync with manifest settings
edge-apps/rss-reader/README.md Updated dev/test/build/deploy instructions + settings table
edge-apps/rss-reader/DEPLOYMENT.md Updated deployment notes (RSS Title meaning)
edge-apps/rss-reader/deployed-apps.yml Registry of deployed RSS Reader instances/IDs
edge-apps/rss-reader/.ignore Deployment ignore rules (now ignores node_modules)
edge-apps/rss-reader/.gitignore Git ignore rules including build outputs and screenshot PNGs
edge-apps/rss-reader/screenshots/*.webp New/updated reference screenshots for supported resolutions
edge-apps/rss-reader/static/js/* Removes legacy vendored JS + Alpine app runtime
edge-apps/rss-reader/static/css/* Removes legacy static CSS styling
edge-apps/rss-reader/static/img/Screenly.svg Removes legacy logo asset (no longer referenced by new UI)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread edge-apps/rss-reader/src/main.ts Outdated
): Promise<RssEntry[]> {
const bypassCors =
getSettingWithDefault<string>('bypass_cors', 'true') === 'true'
const url = bypassCors ? `${getCorsProxyUrl()}/${rssUrl}` : rssUrl
Comment thread edge-apps/rss-reader/src/main.ts Outdated
Comment on lines +95 to +96
const cacheInterval =
parseInt(getSettingWithDefault<string>('cache_interval', '1800')) * 1000
Comment thread edge-apps/rss-reader/src/utils.ts Outdated
Comment on lines +34 to +35
const data: AppCache = { entries, timestamp: Date.now() }
localStorage.setItem(CACHE_KEY, JSON.stringify(data))
Comment on lines +26 to +27
const parsed: AppCache = JSON.parse(raw)
return parsed.entries ?? []
- Only apply CORS proxy to absolute http/https URLs
- Use number type for cache_interval to avoid parseInt edge cases
- Wrap saveCache in try/catch to prevent storage errors from breaking renders
- Document legacy cache format behaviour in loadCache
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants