Skip to content

SciTee/pagepulse

Repository files navigation

PagePulse

Turn any web page into an RSS feed by watching it for changes.

A lot of pages change quietly — a price drops, an out-of-stock item comes back, a new job shows up on a board, opening hours get edited — but they never offer a feed or a notification to tell you. So you end up reloading the page by hand.

PagePulse does that for you: point it at a URL, tell it which part of the page matters (via a CSS selector), and it emits an RSS/Atom feed entry whenever that part actually changes. Hang the feed in any reader, or have it fire a webhook.

What it's good for

  • Price watching — get an entry when a product's price changes.
  • Back in stock — watch an availability label flip from "Sold out".
  • Job boards — notice a new posting on a careers page that has no feed.
  • Anything that quietly edits a page — opening hours, version numbers, status banners, a changing list.

Features

  • Watch any number of pages, all configured in one YAML file.
  • Extract just the relevant region with a CSS selector (not the whole page, so you get far fewer false alarms).
  • Generate an RSS or Atom feed of the detected changes.
  • Optional webhook per watcher (Discord, Slack, your own endpoint).
  • Snapshot history kept in a local SQLite file.
  • Ignore patterns to drop volatile noise (timestamps, view counters).
  • Run once (run) or continuously (watch).

Install

PagePulse needs Node.js 20 or newer.

# global
npm install -g pagepulse

# or one-off, without installing
npx pagepulse run

Quickstart

# 1. scaffold a config
pagepulse init

# 2. edit pagepulse.yaml: set the url and the CSS selector you care about

# 3. run a pass — writes feeds into ./feeds
pagepulse run

The first run records an initial snapshot for each watcher; from then on you only get an entry when the selected content actually changes.

Configuration

A config is a YAML file (default pagepulse.yaml). Top-level keys:

Key Default Meaning
database ./data/pagepulse.db SQLite file for snapshots and the change log.
feedDir ./feeds Directory the feed files are written to.
interval 1h Default poll interval for watch.
userAgent a PagePulse UA string Sent on every request.
watchers The list of pages to watch (see below).

Each watcher:

Field Required Meaning
id yes Stable id; used for the SQLite rows and the feed file name.
name no Label shown in the feed and in pagepulse list.
url yes The page to fetch.
selector yes CSS selector for the part of the page that matters.
attr no Read this attribute instead of the element's text.
interval no Per-watcher poll interval, overrides the global one.
ignore no List of regexes stripped before comparing (volatile noise).
webhook no { url, method?, headers? } fired on every detected change.

Durations accept s, m, h suffixes: 90s, 30m, 2h.

Finding the right CSS selector

The selector is what keeps PagePulse from crying wolf on every layout tweak.

  1. Open the page, right-click the bit you care about, Inspect.
  2. Look for a stable hook: an id (#price), a class (.availability), or a data- attribute.
  3. Prefer something specific and stable over a long auto-generated path. In your browser console, document.querySelector("#price")?.textContent should return exactly the text you want to watch.
  4. If the value lives in an attribute (e.g. data-price="19.99"), set attr on the watcher to read that instead of the visible text.

Using the feed

pagepulse run writes one feed per watcher plus a combined all.xml into feedDir:

feeds/
├── example-price.xml
├── example-stock.xml
└── all.xml

Serve that directory (or sync it somewhere your reader can reach) and subscribe to the file. Your reader will then receive a new entry each time the watched content changes. Prefer Atom? Pass --format atom.

Webhooks

Give a watcher a webhook and PagePulse POSTs a JSON payload on every change:

watchers:
  - id: widget-price
    url: https://shop.example.com/widget
    selector: "#price"
    webhook:
      url: https://discord.com/api/webhooks/xxx/yyy

The payload looks like:

{
  "watcherId": "widget-price",
  "name": "Widget price",
  "url": "https://shop.example.com/widget",
  "previous": "$19.99",
  "current": "$17.49",
  "detectedAt": "2026-04-28T18:20:00.000Z"
}

Failed deliveries are retried a couple of times with a short backoff.

Recipes

A few starting points — adjust the selectors to the actual page.

Price drop

watchers:
  - id: widget-price
    name: "Widget price"
    url: https://shop.example.com/widget
    selector: "#price"
    attr: data-price # often cleaner than the formatted text
    interval: 2h

Back in stock

watchers:
  - id: console-stock
    name: "Console availability"
    url: https://shop.example.com/console
    selector: ".availability"
    interval: 30m
    webhook:
      url: https://discord.com/api/webhooks/xxx/yyy

New job posting

watchers:
  - id: careers
    name: "Careers page"
    url: https://example.com/careers
    # watch the list container; a new item changes its text
    selector: "#openings"
    interval: 6h
    ignore:
      - "Last updated.*" # the page stamps an update time we don't care about

Running continuously

watch

pagepulse watch keeps polling each watcher on its interval and rewrites the feeds in place. Stop it with Ctrl+C — it finishes the current pass first.

pagepulse watch -c pagepulse.yaml

cron

Prefer cron? Run a single pass on a schedule and let cron be the loop:

# every 30 minutes
*/30 * * * * cd /srv/pagepulse && /usr/bin/pagepulse run >> pagepulse.log 2>&1

Docker

A Dockerfile is included. Build the image, then mount your config and the data/feed directories:

docker build -t pagepulse .
docker run --rm \
  -v "$PWD/pagepulse.yaml:/app/pagepulse.yaml:ro" \
  -v "$PWD/data:/app/data" \
  -v "$PWD/feeds:/app/feeds" \
  pagepulse

By default the container runs watch; override the command to run a single pass instead (docker run ... pagepulse run).

Roadmap

  • JavaScript rendering for pages that build their content client-side (v1 only reads the static HTML the server returns).
  • More notifier targets out of the box.

License

MIT — see LICENSE.

About

Turn any web page into an RSS/Atom feed by watching it for changes — CSS-selector based, with webhooks. Self-hosted CLI.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors