About

Based on the previous notes on automating websites with node-red and JavaScript, it would be nice if there existed a flow that would be able to monitor any number of websites for changes and that would just require a minimal set of inputs without using changedetection.io and socketpuppetbrowser.

Flow

The flow for this project does not need any external nodes and can be created using the set of features built-in to node-red.

In large the flow works by defining a set of URLs and a CSS selector for a HTML node to be found at the URL. When the flow starts, the URLs are read and accessed one-by-one by loading the website, grabbing the HTML code under the provided CSS selector and then building a SHA has from the result and storing it persistently.

After that, once the contents selected by the CSS selector changes, the hash will change as well and the flow will send a notification via Gotify. Gotify was chosen because we develop Winify which seems to be a convenient front-end.

Code

[{"id":"378eb00a5c78d42a","type":"tab","label":"Website Monitoring","disabled":false,"info":"","env":[]},{"id":"b1d771894423190b","type":"http request","z":"378eb00a5c78d42a","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":390,"y":240,"wires":[["0c9603a9b45e4418","5d5de1e989240be3"]]},{"id":"daa05d19034f1794","type":"split","z":"378eb00a5c78d42a","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":550,"y":180,"wires":[["f1a09dc9d317616f"]]},{"id":"415f93158c6f5628","type":"debug","z":"378eb00a5c78d42a","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":540,"y":360,"wires":[]},{"id":"f1a09dc9d317616f","type":"change","z":"378eb00a5c78d42a","name":"","rules":[{"t":"move","p":"payload.url","pt":"msg","to":"url","tot":"msg"},{"t":"move","p":"payload.css","pt":"msg","to":"css","tot":"msg"},{"t":"move","p":"payload.sha","pt":"msg","to":"sha","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":180,"wires":[["b1d771894423190b"]]},{"id":"5d5de1e989240be3","type":"switch","z":"378eb00a5c78d42a","name":"","property":"statusCode","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":550,"y":240,"wires":[["f72eb4841c3dbda1"],[]]},{"id":"f72eb4841c3dbda1","type":"function","z":"378eb00a5c78d42a","name":"function 1","func":"const url = msg.url\nconst $ = cheerio.load(msg.payload)\nconst data = $(`${msg.css}`).html()\nconst shasum = crypto.createHash('sha1')\nshasum.update(data)\nconst sha = shasum.digest('hex')\n\nvar store = flow.get('store')\nif (store === null || typeof store === undefined) {\n    store = {}\n}\nstore[url].sha = sha\nflow.set('store', store)\n\nmsg.payload = url\n\nif(msg.sha !== sha) {\n    msg.sha = sha\n    node.send([msg, null])\n    return\n}\nmsg.sha = sha\nnode.send([null, msg])\n\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"cheerio","module":"cheerio"},{"var":"crypto","module":"crypto"}],"x":700,"y":240,"wires":[["ee06f5465c323ad0"],[]]},{"id":"0c9603a9b45e4418","type":"debug","z":"378eb00a5c78d42a","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":620,"y":120,"wires":[]},{"id":"a490817b34ca17ec","type":"function","z":"378eb00a5c78d42a","name":"function 2","func":"var store = flow.get('store')\nif(store === null || typeof store === 'undefined') {\n    store = {}\n}\n\nvar sites = flow.get('sites')\nif(store === null || typeof store === 'undefined') {\n    sites = {}\n}\n\nObject.keys(sites).forEach(url => {\n    if(store.hasOwnProperty(url)) {\n        return\n    }\n    store[url] = sites[url]\n})\n\nflow.set('store', store)\n\nlet payload = []\nObject.keys(store).forEach(url => {\n    payload.push({\n        \"url\": url,\n        \"css\": store[url].css,\n        \"sha\": store[url].sha || \"\"\n    })\n})\n\nmsg.payload = payload\nreturn msg","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":180,"wires":[["daa05d19034f1794"]]},{"id":"1744248aad9a08bc","type":"inject","z":"378eb00a5c78d42a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"43200","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":180,"wires":[["a490817b34ca17ec"]]},{"id":"c0b50d93857cc561","type":"http request","z":"378eb00a5c78d42a","name":"","method":"POST","ret":"txt","paytoqs":"ignore","url":"https://gotify.tld/message?token=","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":710,"y":300,"wires":[[]]},{"id":"6a44bd5ccb682d1f","type":"function","z":"378eb00a5c78d42a","name":"function 3","func":"var sites = {\n    \"https://store.steampowered.com/app/1632060/Terminal_Earth/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/1442530/Outbreak_Island/\" : {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/2868080/ICEBOUND/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/2387670/As_One_We_Survive/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/2979810/POLYARIS/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/3179350/Long_Drive_North_CoOp_RV_Simulator/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/3517740/Frostrail/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/998400/TransSiberian_Railway_Simulator/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    },\n    \"https://store.steampowered.com/app/3645160/Frostpunk_1886/\": {\n        \"css\": \"#game_area_purchase\",\n        \"sha\": \"\"\n    }\n}\n\nflow.set(\"sites\", sites)\nreturn msg\n","outputs":0,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":120,"wires":[]},{"id":"eb3685f5741d6c62","type":"inject","z":"378eb00a5c78d42a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.1","topic":"","payload":"","payloadType":"date","x":230,"y":120,"wires":[["6a44bd5ccb682d1f"]]},{"id":"ee06f5465c323ad0","type":"template","z":"378eb00a5c78d42a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n  \"extras\": {\n    \"client::display\": {\n      \"contentType\": \"text/markdown\"\n    }\n  },\n  \"title\": \"Website update\",\n  \"message\": \"[{{{url}}}]({{url}}) has been updated.\"\n}\n","output":"json","x":400,"y":300,"wires":[["79c6021f077ec733"]]},{"id":"79c6021f077ec733","type":"function","z":"378eb00a5c78d42a","name":"function 4","func":"const payload = msg.payload\nmsg = {}\nmsg.payload = payload\nreturn msg","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":300,"wires":[["c0b50d93857cc561","415f93158c6f5628"]]},{"id":"2c5911da4639c03e","type":"template","z":"378eb00a5c78d42a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n  \"extras\": {\n    \"client::display\": {\n      \"contentType\": \"text/markdown\"\n    }\n  },\n  \"title\": \"Website update test\",\n  \"message\": \"[{https://grimore.org}](https://grimore.org) has been updated\"\n}\n","output":"json","x":540,"y":420,"wires":[["c0b50d93857cc561"]]},{"id":"ba7a72e6919663fb","type":"inject","z":"378eb00a5c78d42a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":400,"y":420,"wires":[["2c5911da4639c03e"]]}]

Connections to Other Projects

Using changedetection.io and sockpuppetbrowser is an alternative when "live" websites have to be monitored or when having to log-in to websites where the mechanism is more complicated than stored cookies. However, for most websites that are public, the node-red alternative consumes much less resources than running a stale browser.


fuss/node-red/flows/website_monitoring.txt ยท Last modified: by office

Wizardry and Steamworks

© 2025 Wizardry and Steamworks

Access website using Tor Access website using i2p Wizardry and Steamworks PGP Key


For the contact, copyright, license, warranty and privacy terms for the usage of this website please see the contact, license, privacy, copyright.