The Lebanon–Israel conflict generates hundreds of articles daily across dozens of outlets in multiple languages. Keeping up means juggling browser tabs, Telegram channels, and Twitter threads — or missing critical updates entirely.
LEB Monitor solves this. It aggregates 45 RSS feeds from war correspondents, UN agencies, humanitarian orgs, and major news networks — in Arabic and English — into a single real-time stream. Articles arrive progressively via NDJSON as each source resolves. No waiting. No refreshing. No API keys.
|
Feeds arrive via NDJSON as each source resolves — you see articles in under 3 seconds, not after all 45 sources finish. Built on native Automatic RTL/LTR detection via Unicode range analysis. Arabic headlines render right-to-left with Tajawal; English content stays left-to-right with Poppins. Zero configuration. No database. No Redis. No API keys. No authentication. Just public RSS feeds parsed server-side and streamed to the browser. Deploy in 30 seconds. |
Each feed is fetched independently via Client-side merge algorithm deduplicates by composite ID ( Toggle sources on/off, reorder feeds, switch between grid/list layouts, cycle through 4 font sizes, flip light/dark themes — all persisted in localStorage via |
git clone https://github.com/ramyatrouny/leb-news-monitor.git
cd leb-news-monitor
npm install
npm run devOpen localhost:3000 — articles start streaming immediately.
Requirements: Node.js 18+ | No environment variables needed | No database setup
┌─────────────────────────────────────────┐
│ Browser │
│ │
┌─────────────┐ │ LiveFeed (orchestrator) │
│ 44 RSS │ NDJSON stream │ ├── FeedHeader (logo, stats, tools)│
│ Sources │ ◄─── fetch() ────► │ ├── FeedFilterBar (categories, chips) │
│ │ │ ├── FeedContent (cards, scroll) │
│ Al Jazeera │ ┌────────────┐ │ │ └── FeedCard (article, RTL) │
│ BBC │ │ /api/feeds │ │ └── FeedSettings (toggle, reorder) │
│ UN OCHA │ │ │ │ │
│ MSF │──►│ Parse XML │──►│ Hooks: │
│ LBC │ │ Sanitize │ │ ├── useFeedStream (NDJSON + merge) │
│ Bellingcat │ │ Extract │ │ ├── useFeedPrefs (source prefs) │
│ ...38 more │ │ Stream │ │ ├── useLayout (grid / list) │
└─────────────┘ └────────────┘ │ ├── useFontSize (4 presets) │
│ └── useTheme (light / dark) │
└─────────────────────────────────────────┘
| Step | What Happens |
|---|---|
| 1. Mount | useFeedStream() fires GET /api/feeds |
| 2. Fetch | API route fetches 45 feeds in parallel via Promise.allSettled |
| 3. Stream | Each resolved feed is sanitized, parsed, and streamed as an NDJSON batch |
| 4. Merge | Client deduplicates by ID, sorts by pubDate, marks new items for animation |
| 5. Render | Responsive card grid with category/source filtering + infinite scroll |
| 6. Poll | Every 30s the cycle repeats; new articles merge in with CSS transitions |
| Feature | Implementation |
|---|---|
| Real-time streaming | NDJSON over ReadableStream — articles appear as each source resolves |
| 45 RSS sources | War coverage, breaking news, analysis — Arabic + English |
| Auto-refresh | 30s polling with client-side deduplication and merge |
| Infinite scroll | IntersectionObserver with 30-item pages |
| Category filtering | War Focused / Breaking News / General tabs with live counts |
| Source filtering | Chip-based source selector with active state toggling |
| Preference | Control |
|---|---|
| Theme | Light / Dark with system preference detection |
| Font size | Small / Medium / Large / XL — applied via CSS class scaling |
| Layout | Grid (1-3 columns) / List (single column, centered) |
| Feed sources | Toggle visibility, reorder — syncs automatically when new sources appear |
| Capability | Detail |
|---|---|
| RTL/LTR detection | Unicode range \u0600–\u06FF analysis per title/snippet |
| Error resilience | Promise.allSettled — one feed failure never blocks others |
| Hydration-safe state | All preferences use useSyncExternalStore (not useState + useEffect) |
| SEO | Metadata API, JSON-LD (WebApplication + WebSite), sitemap, robots.txt |
| Security headers | X-Content-Type-Options, X-Frame-Options, Referrer-Policy |
| PWA manifest | Standalone display, themed icons, installable on mobile |
| Build-time changelog | Git log parsed at build via prebuild script, served at /changelog |
Streams newline-delimited JSON. Each line is a self-contained JSON object.
FeedItem Schema
interface FeedItem {
id: string; // `${source}-${link || guid || title}`
title: string; // Article headline
link: string; // Original article URL
snippet: string; // First 200 chars, HTML stripped
pubDate: string; // ISO 8601
source: string; // Feed display name
sourceColor?: string; // Brand color (OKLCh)
sourceCategory: string; // "war" | "breaking" | "general"
image?: string; // Article thumbnail URL
}Response Headers
| Header | Value | Purpose |
|---|---|---|
Content-Type |
text/plain; charset=utf-8 |
NDJSON stream |
Cache-Control |
no-cache, no-store |
Always fresh |
X-Content-Type-Options |
nosniff |
Prevent MIME sniffing |
X-Frame-Options |
DENY |
Block clickjacking |
Referrer-Policy |
strict-origin-when-cross-origin |
Privacy |
War Focused — 17 sources
| Source | Coverage |
|---|---|
| LBC War | Lebanese conflict updates |
| ReliefWeb LB | Humanitarian situation reports |
| OCHA Lebanon | UN humanitarian coordination |
| UN Middle East | United Nations regional desk |
| UN Peace | Peace & security dispatches |
| Middle East Monitor | Regional conflict reporting |
| Middle East Eye | Investigative journalism |
| Al Manar | Lebanese perspective |
| +972 Magazine | Israeli–Palestinian coverage |
| Mondoweiss | Palestinian rights reporting |
| Electronic Intifada | Palestinian news & analysis |
| Crisis Group | Conflict resolution analysis |
| Bellingcat | Open-source investigations |
| MSF | Doctors Without Borders field reports |
| Amnesty International | Human rights documentation |
| Defense News | Military & defense industry |
| War on the Rocks | National security analysis |
Breaking News — 11 sources
| Source | Coverage |
|---|---|
| LBC Breaking / Latest | Lebanese breaking news |
| NNA | National News Agency of Lebanon |
| The961 | Lebanese news & culture |
| Times of Israel | Israeli news coverage |
| Jerusalem Post (Headlines + Defense) | Israeli perspective |
| Al Jazeera EN | Pan-Arab English news |
| BBC Middle East | British international coverage |
| Al Hurra | US-funded Arabic news |
| Anadolu Agency | Turkish state news agency |
General / Analysis — 17 sources
Arabic: Al Jazeera AR, Sky News Arabia, BBC Arabic, BBC AR Middle East, France 24 AR, DW Arabic, Annahar, Al Arabiya, Asharq Al-Awsat, Lebanon Debate, Al Quds, Rai Al Youm, Lebanese Forces
English: Al-Monitor, The New Arab, Guardian Middle East, Foreign Policy
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 16 App Router | Server components, route handlers, Metadata API |
| UI | React 19 | useSyncExternalStore for hydration-safe localStorage |
| Language | TypeScript 5 strict | End-to-end type safety |
| Styling | Tailwind CSS 4 | CSS-based config, OKLCh color tokens |
| Components | shadcn/ui + Radix | Accessible primitives (Sheet, Switch, Tooltip) |
| Fonts | Poppins + Tajawal | Latin + Arabic via next/font |
| RSS | rss-parser | XML parsing with media extraction |
| Streaming | NDJSON via ReadableStream |
Native, no libraries |
| Analytics | Vercel Analytics + Speed Insights + GA4 | Performance + usage tracking |
| Deployment | Vercel | Edge network, zero-config |
src/
├── app/
│ ├── layout.tsx # Root layout: fonts, SEO, JSON-LD, theme script
│ ├── page.tsx # Home → renders <LiveFeed />
│ ├── changelog/page.tsx # Auto-generated from git history
│ ├── globals.css # Tailwind v4 + OKLCh tokens + font size presets
│ ├── manifest.ts # PWA manifest
│ ├── robots.ts / sitemap.ts # SEO
│ ├── icon.tsx / apple-icon.tsx # Dynamic favicons
│ ├── opengraph-image.tsx # Dynamic OG image
│ └── api/feeds/route.ts # NDJSON streaming RSS API
│
├── components/
│ ├── live-feed.tsx # Orchestrator: data + state → child components
│ ├── feed-header.tsx # Logo, stats, streaming indicator, toolbar
│ ├── feed-filter-bar.tsx # Category tabs + source chips
│ ├── feed-content.tsx # Card grid/list + infinite scroll + empty states
│ ├── feed-card.tsx # Article card (RTL-aware, memoized)
│ ├── feed-settings.tsx # Source management side panel
│ ├── announcement-banner.tsx # Dismissible banner
│ ├── font-size-toggle.tsx # 4-preset font size cycler
│ ├── layout-toggle.tsx # Grid / List switcher
│ ├── theme-toggle.tsx # Light / Dark toggle
│ └── ui/ # shadcn/ui primitives
│
├── hooks/
│ ├── use-feed-stream.ts # NDJSON streaming + merge + 30s poll
│ ├── use-feed-prefs.ts # Source visibility + order (localStorage)
│ ├── use-layout.ts # Grid / List preference
│ ├── use-font-size.ts # Font size preference
│ └── use-theme.ts # Light / Dark preference
│
├── config/
│ └── feeds.ts # 45 RSS feed definitions + categories
│
└── scripts/
└── generate-changelog.ts # Build-time git log → JSON
Edit src/config/feeds.ts:
{
name: "Your Source",
url: "https://example.com/rss",
color: "#hex", // Brand accent color
category: "war", // "war" | "breaking" | "general"
},The source appears automatically on next page load. No other changes needed.
- Add to
FeedCategorytype infeeds.ts - Add entries to
CATEGORY_LABELS,CATEGORY_COLORS,CATEGORY_ORDER - Assign feeds — the UI picks it up automatically
Connect your GitHub repo — it deploys on every push. No env vars needed.
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Enable
output: "standalone"innext.config.tsfor Docker builds.
npm run build && NODE_ENV=production npm startPort 3000 by default. Use nginx/Caddy for HTTPS.
| Command | Description |
|---|---|
npm run dev |
Development server with Turbopack |
npm run build |
Production build (runs changelog generation via prebuild) |
npm start |
Serve production build |
npm run lint |
ESLint checks |
Some feeds show errors — is that normal?
Yes. RSS feeds are third-party services. Sources go down, rate-limit, or block IPs temporarily. LEB Monitor handles this gracefully — failed feeds are reported in the status bar, but all others continue streaming.
Why NDJSON instead of SSE or WebSockets?
NDJSON over fetch() is the lightest approach — no client libraries, no connection management, full HTTP proxy compatibility. Each line is self-contained JSON, trivially parsed with ReadableStream. SSE adds reconnection complexity we don't need; WebSockets are overkill for 30s polling.
Why useSyncExternalStore instead of useState + useEffect?
useState + useEffect for localStorage causes hydration mismatches (server renders default, client reads localStorage, then re-renders). useSyncExternalStore with getServerSnapshot handles this correctly — React knows the server snapshot is a placeholder and reconciles without tearing.
Can I deploy this publicly?
Yes. No API keys, no database, no auth. It fetches public RSS feeds server-side. Deploy to Vercel, a VPS, or any Node.js host.
A feed I added shows no articles
Common causes:
- URL returns HTML, not XML — verify:
curl -s "URL" | head -5 - Source blocks server requests (403/401 in terminal)
- Items lack
<title>or<link>— the parser needs at least a title - Non-standard XML encoding — check the terminal for parse errors
How do I reset all preferences?
// Browser console:
Object.keys(localStorage).filter(k => k.startsWith("lebmon-")).forEach(k => localStorage.removeItem(k));
location.reload();- Full-text search across all sources
- Bookmarks / saved articles (localStorage)
- Browser push notifications for breaking news
- Article clustering — group related coverage across sources
- Timeline view — chronological event visualization
- Telegram channel ingestion
- Twitter/X feed support
- Source credibility indicators
- Historical archive with database persistence
- Feed health dashboard (uptime, latency per source)
- Light / Dark theme with system detection
- Grid / List layout toggle
- Font size presets (4 levels)
- Tooltip system on all controls
- Build-time changelog at
/changelog - Map view — geotagged articles on interactive map
- Reading mode — inline article preview
- Keyboard shortcuts
- Offline support (PWA service worker)
- Server-side caching layer (Redis)
- i18n (Arabic, French, English UI)
- Unified RSS/Atom feed export
- Mobile app (React Native)
192 more ideas in the Feature Ideas catalog with priority matrices and implementation phases.
Contributions welcome — from adding a feed source to building features.
git clone https://github.com/ramyatrouny/leb-news-monitor.git
cd leb-news-monitor && npm install && npm run dev
# make changes...
npm run lint && npm run build # both must pass| I want to... | How |
|---|---|
| Report a bug | Bug report |
| Request a feature | Feature request |
| Suggest a source | Feed source request |
| Find something to build | Feature Ideas catalog |
| Submit code | Read CONTRIBUTING.md, open a PR |
MIT — use it however you want.
Built with data from independent journalists, humanitarian organizations, and international news agencies covering the Lebanon–Israel conflict. This tool aggregates publicly available RSS feeds and does not generate, editorialize, or modify content.
LEB MONITOR · Conflict Intelligence, Real-Time