No description
  • TypeScript 72.9%
  • Svelte 26.2%
  • CSS 0.3%
  • Dockerfile 0.3%
  • HTML 0.2%
  • Other 0.1%
Find a file
2026-07-04 15:37:32 +02:00
.github/workflows ci: bump GitHub Actions to node24 majors 2026-06-06 17:46:34 +02:00
.vscode feat: initialize SvelteKit project with dependencies 2026-03-26 18:29:43 +01:00
docs docs: implementation plan for live-blog handling 2026-06-07 19:27:26 +02:00
scripts fix: delete action also clears diffs, flushes syndicate queue from Redis 2026-03-28 16:08:23 +01:00
src fix(live-blog): scope LiveBlogPosting detection to JSON-LD; share snapshot const 2026-06-07 19:59:57 +02:00
static feat: dynamic footer with AP + Bluesky handles, instance picker, favicon fixes 2026-03-29 12:39:46 +02:00
.dockerignore feat: add Cloudron deployment packaging 2026-03-26 19:00:06 +01:00
.env.example feat: Internet Archive SPN2 API with auth, disabled until credentials configured 2026-03-27 21:34:33 +01:00
.gitignore chore: prepare for public release, separate Cloudron packaging 2026-03-27 09:19:48 +01:00
.npmrc feat: initialize SvelteKit project with dependencies 2026-03-26 18:29:43 +01:00
CLAUDE.md docs: add test coverage section to README and CLAUDE.md (64 tests) 2026-03-29 15:42:19 +02:00
Dockerfile build: pin npm@11 in builder (npm 10 mis-resolves @noble/hashes lock) 2026-07-04 15:37:32 +02:00
drizzle.config.ts feat: add Drizzle schema with feeds, articles, versions, diffs, social_posts 2026-03-26 18:31:40 +01:00
package-lock.json fix(deps): regenerate lockfile so npm ci resolves @noble/hashes 2026-07-04 15:00:54 +02:00
package.json fix: mf2-aware extraction stops page chrome leaking into diffs 2026-06-06 15:58:46 +02:00
README.md docs: add test coverage section to README and CLAUDE.md (64 tests) 2026-03-29 15:42:19 +02:00
svelte.config.js feat: initialize SvelteKit project with dependencies 2026-03-26 18:29:43 +01:00
tsconfig.json feat: initialize SvelteKit project with dependencies 2026-03-26 18:29:43 +01:00
vite.config.ts feat: initialize SvelteKit project with dependencies 2026-03-26 18:29:43 +01:00

NewsDiff

Monitor RSS feeds for article changes and display visual diffs. When a news article silently updates its headline or content, NewsDiff catches it, stores every version, and shows you exactly what changed.

Draws inspiration from newsdiffs, diffengine, and NYTdiff — rebuilt with modern tooling.

Features

  • Monitor any RSS/Atom/JSON Feed for article changes
  • Automatic content extraction via Defuddle with Readability fallback (no per-site parsers)
  • Word-level diffs with inline highlighting
  • Visual diff card images for social sharing + full-height diff image download
  • ActivityPub bot (Fediverse/Mastodon-compatible) via Botkit with threaded replies
  • Bluesky syndication with image embeds, rich link cards, and threaded replies
  • Internet Archive integration — each version archived to the Wayback Machine
  • Atom output feeds: /feed.xml (all diffs), /feed/{feedId}.xml (per source), /article/{id}/feed.xml (per article)
  • Web Share API on mobile, share dropdown on desktop
  • "Boring" diff detection — skips timestamp-only changes and minor numeric updates
  • WebSub support — instant push updates from hubs, with polling as fallback
  • Sitemap import — seed baseline versions for an entire site so future edits are detected
  • Rate-limited syndication (configurable, default: 1 post per 5 minutes)
  • OIDC-protected feed management, bot profile editor, and sitemap importer

Tech Stack

Component Library
Framework SvelteKit (adapter-node)
Database PostgreSQL + Drizzle ORM
Job Queue BullMQ (Redis-backed)
Feed Parsing rss-parser + JSON Feed
Content Extraction Defuddle (primary) + @mozilla/readability (fallback) + JSDOM
Diffing diff (jsdiff) — word-level
ActivityPub @fedify/botkit
Bluesky @atproto/api
Diff Images satori + sharp (no browser needed)
Runtime Node.js 22

Deployment on Cloudron

NewsDiff is packaged as a Cloudron app. It requires the following addons:

  • postgresql — database
  • redis — job queue
  • localstorage — bot profile config and uploaded images
  • oidc — protects /feeds and /bot/profile routes

Build and install

# Build the Docker image
cloudron build

# Install on your Cloudron instance
cloudron install --image <your-registry>/com.newsdiff.app:<tag>

# Update an existing install
cloudron update --app <app-id> --image <your-registry>/com.newsdiff.app:<tag>

Environment variables (injected by Cloudron addons)

Variable Source Purpose
CLOUDRON_POSTGRESQL_URL postgresql addon Database connection
CLOUDRON_REDIS_URL redis addon Job queue
CLOUDRON_OIDC_ISSUER oidc addon Auth provider URL
CLOUDRON_OIDC_CLIENT_ID oidc addon Auth client ID
CLOUDRON_OIDC_CLIENT_SECRET oidc addon Auth client secret
CLOUDRON_APP_ORIGIN Cloudron runtime Public URL of the app
CLOUDRON_DATA_DIR Cloudron runtime Persistent storage path

These are mapped to the app's internal env vars in start.sh.

Self-hosted (without Cloudron)

Copy .env.example to .env and fill in your values:

cp .env.example .env
npm install
npm run migrate
npm run build
node build/index.js

The ActivityPub bot server runs on port 8001. An nginx reverse proxy (see nginx.conf.template) routes ActivityPub paths to Botkit and everything else to SvelteKit on port 3000.

Development

npm install
npm run dev     # SvelteKit dev server on :5173
npm test        # Vitest unit tests
npm run migrate # Run DB migrations

Test coverage

64 tests across 10 test files covering all core services:

Module Tests What's covered
differ 16 Word-level diffing, boring detection (timestamps, relative times, date changes)
feed-parser 10 RSS, Atom, JSON Feed parsing, hub discovery (WebSub)
bluesky 6 Post construction, config check, character limits
auth 5 Session cookies, HMAC signing, tampering, expiry, OIDC check
atom-builder 4 XML generation, escaping, self links
rate-limit 4 Allow/block/reset/key isolation
extractor 3 Content extraction (Defuddle/Readability), feed-listing rejection
card-generator 3 Diff card image generation, alt text
websub 3 Subscription params, error handling
schema 10 Database schema validation

Workers (feed-poller, syndicator) and the AP bot require Redis/Postgres connections and are covered by integration testing against the live deployment.

Architecture

RSS feeds
  └─ feed-poller (BullMQ)
       └─ readability extraction
       └─ word-level diff
       └─ PostgreSQL
            └─ syndicator (BullMQ)
                 ├─ Bluesky (atproto)
                 └─ ActivityPub (Botkit/Fedify)

SvelteKit (:3000) ─┐
                    ├─ nginx (:8000, public)
Botkit (:8001) ────┘

Sitemap import

To detect edits on articles published before NewsDiff was set up, import a sitemap to seed baseline versions:

  1. Go to /feeds → "Import from sitemap"
  2. Enter your sitemap URL (e.g., https://example.com/sitemap.xml)
  3. Select the feed to associate articles with
  4. Click "Start import" — progress is shown on the page

The import stores version 1 for each URL without creating any diffs or social posts. Future edits detected by the feed poller will diff against this baseline.

WebSub

Feeds that advertise a WebSub hub are automatically subscribed for instant push updates. The hub pushes updated feed content to NewsDiff when the source changes, so diffs appear within seconds rather than waiting for the next poll. Polling continues as a fallback for all feeds.

nginx routes ActivityPub paths (/.well-known/webfinger, /users/, /ap/, /nodeinfo/) to Botkit; everything else goes to SvelteKit.

ActivityPub Bot

The bot exposes a Fediverse actor at @<BOT_USERNAME>@<your-domain>. Configure the profile at /bot/profile (login required). Users can follow the bot from any ActivityPub client (Mastodon, Misskey, etc.) to receive diffs in their feed.

Credits

  • newsdiffs — full body diffing, web UI, boring-version filtering
  • diffengine — feed-agnostic design, readability extraction
  • NYTdiff — visual diff images, Bluesky support
  • Botkit — ActivityPub bot framework
  • Fedify — ActivityPub federation library

License

MIT