<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Root</title>
    <description>The latest articles on DEV Community by Daniel Root (@daniel_root_5c360ddb87563).</description>
    <link>https://dev.to/daniel_root_5c360ddb87563</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144817%2Fe518d3af-9559-41b6-b204-072e06677138.jpg</url>
      <title>DEV Community: Daniel Root</title>
      <link>https://dev.to/daniel_root_5c360ddb87563</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_root_5c360ddb87563"/>
    <language>en</language>
    <item>
      <title>Monitor your robots.txt and sitemap for accidental changes</title>
      <dc:creator>Daniel Root</dc:creator>
      <pubDate>Sat, 26 Sep 2026 20:21:15 +0000</pubDate>
      <link>https://dev.to/daniel_root_5c360ddb87563/monitor-your-robotstxt-and-sitemap-for-accidental-changes-45ac</link>
      <guid>https://dev.to/daniel_root_5c360ddb87563/monitor-your-robotstxt-and-sitemap-for-accidental-changes-45ac</guid>
      <description>&lt;p&gt;Two SEO accidents come up again and again:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A staging &lt;code&gt;robots.txt&lt;/code&gt; with &lt;code&gt;Disallow: /&lt;/code&gt; gets deployed to production.&lt;/li&gt;
&lt;li&gt;A migration or plugin update changes the sitemap generator, and whole sections disappear from &lt;code&gt;sitemap.xml&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither breaks the site for visitors, so nobody notices. Search engines do notice, just slowly: Google says it &lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt" rel="noopener noreferrer"&gt;generally caches robots.txt for up to 24 hours&lt;/a&gt;, and the effect on indexing and traffic can take much longer to show up in reports. By the time someone asks "why is organic down?", the cause is weeks old.&lt;/p&gt;

&lt;p&gt;The fix is boring: watch the few files that control crawling, and add a smoke test to your deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/robots.txt&lt;/code&gt;&lt;/strong&gt;: plain text, small, and should almost never change. Any change is worth a human look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your sitemap index and the child sitemaps that matter.&lt;/strong&gt; If your sitemap is split (&lt;code&gt;sitemap-pages.xml&lt;/code&gt;, &lt;code&gt;sitemap-products.xml&lt;/code&gt;...), watch the index plus the ones for your most important sections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visible text of 3-5 money pages&lt;/strong&gt; (home, pricing, top landing pages), which catches a template change that wipes copy.&lt;/li&gt;
&lt;li&gt;Other plain-text control files you rely on, such as &lt;code&gt;/ads.txt&lt;/code&gt; or &lt;code&gt;/.well-known/security.txt&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a text-change monitor will not catch
&lt;/h2&gt;

&lt;p&gt;Most change monitors, ours included, compare a page's visible text. That's great for robots.txt and sitemaps, but several indexing controls live in markup or headers, not in text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;meta name="robots" content="noindex"&amp;gt;&lt;/code&gt; (an attribute, not visible text)&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; HTTP header&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rel="canonical"&lt;/code&gt; and &lt;code&gt;hreflang&lt;/code&gt; changes&lt;/li&gt;
&lt;li&gt;tags injected by JavaScript after load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those, use Search Console's indexing reports, a scheduled crawl (Screaming Frog, Sitebulb and similar tools can run on a schedule), or better, a check that runs on every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A post-deploy SEO smoke test
&lt;/h2&gt;

&lt;p&gt;Drop this into your CI after deploy (or run it from cron). It exits non-zero if production looks de-indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# seo-smoke.sh: fail loudly if production is telling crawlers to go away&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;site&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;
&lt;span class="nv"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;/ /pricing /blog&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="nv"&gt;robots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="s2"&gt;/robots.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"robots.txt not reachable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eiq&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*disallow:[[:space:]]*/[[:space:]]*$'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$robots&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"robots.txt contains 'Disallow: /'. Check which user-agent group it's in"&lt;/span&gt;
  &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi

for &lt;/span&gt;path &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSI&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: not reachable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qi&lt;/span&gt; &lt;span class="s1"&gt;'^x-robots-tag:.*noindex'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$headers&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: X-Robots-Tag noindex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
  if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eiq&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta[^&amp;gt;]*name=.?robots[^&amp;gt;]*noindex'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;: meta robots noindex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done

&lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$site&lt;/span&gt;&lt;span class="s2"&gt;/sitemap.xml"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;loc&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sitemap.xml lists &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; URLs"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 10 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sitemap looks too small"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust before trusting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The robots check flags &lt;code&gt;Disallow: /&lt;/code&gt; anywhere in the file, including groups for specific bots you meant to block. Treat a hit as "look at this", not proof.&lt;/li&gt;
&lt;li&gt;The meta check assumes &lt;code&gt;name&lt;/code&gt; comes before &lt;code&gt;content&lt;/code&gt;, which is the common order but not guaranteed.&lt;/li&gt;
&lt;li&gt;Set the sitemap threshold to something meaningful for your site, e.g. 80% of last week's count. If you use a sitemap index, count the child sitemaps instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Between deploys: change alerts
&lt;/h2&gt;

&lt;p&gt;The smoke test covers your deploys. It doesn't cover a CMS plugin update at 2am, a teammate editing robots.txt in an admin panel, or a hosting platform "helpfully" changing defaults. That's where a change monitor on the files above earns its keep.&lt;/p&gt;

&lt;p&gt;Some practical notes for sitemaps specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your generator rewrites &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; on every build, a text monitor will alert on every deploy. Full ISO timestamps like &lt;code&gt;2026-09-26T10:00:00+00:00&lt;/code&gt; are ignored by monitors that mask timestamps (PingWhen does), but date-only values like &lt;code&gt;2026-09-26&lt;/code&gt; are not. Watch a child sitemap that changes rarely, or accept a deploy-time alert.&lt;/li&gt;
&lt;li&gt;Very large sitemaps (tens of thousands of URLs) are better handled by the count check above than by a diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  With PingWhen
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pingwhen.app" rel="noopener noreferrer"&gt;PingWhen&lt;/a&gt; watches public URLs and emails you, or POSTs a signed JSON webhook, when their text changes, when they go down, and when they come back. Because robots.txt is plain text, every edit produces an alert with a short excerpt of the added text. Point the webhook at your CI (anything that can take a JSON POST) and you can re-run the smoke test automatically whenever robots.txt or a sitemap changes.&lt;/p&gt;

&lt;p&gt;It's $9/month for 10 URLs checked every 15 minutes, and there's no free plan. You can check whether your files are reachable and readable with our free &lt;a href="https://pingwhen.app/check" rel="noopener noreferrer"&gt;page check&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://pingwhen.app/blog/monitor-robots-txt-and-sitemap-changes" rel="noopener noreferrer"&gt;PingWhen blog&lt;/a&gt;. Written with AI assistance for PingWhen, the page-change and uptime alert tool we make.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>How to get notified when a web page changes (without writing a scraper)</title>
      <dc:creator>Daniel Root</dc:creator>
      <pubDate>Sat, 26 Sep 2026 20:21:11 +0000</pubDate>
      <link>https://dev.to/daniel_root_5c360ddb87563/how-to-get-notified-when-a-web-page-changes-without-writing-a-scraper-531c</link>
      <guid>https://dev.to/daniel_root_5c360ddb87563/how-to-get-notified-when-a-web-page-changes-without-writing-a-scraper-531c</guid>
      <description>&lt;p&gt;You want to know when a page changes: a "registration opens soon" notice, a competitor's pricing table, a changelog with no RSS, a government page that says "updates will be posted here". Most guides jump straight to a tool. It's worth spending five minutes first, because the right URL matters more than the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: check whether a feed already exists
&lt;/h2&gt;

&lt;p&gt;A feed is the most reliable change notification there is, and it's free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look in the page source for &lt;code&gt;&amp;lt;link rel="alternate" type="application/rss+xml"&amp;gt;&lt;/code&gt; or &lt;code&gt;application/atom+xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Try the usual paths: &lt;code&gt;/feed&lt;/code&gt;, &lt;code&gt;/rss&lt;/code&gt;, &lt;code&gt;/atom.xml&lt;/code&gt;, &lt;code&gt;/index.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;GitHub has Atom feeds for releases (&lt;code&gt;https://github.com/OWNER/REPO/releases.atom&lt;/code&gt;), tags (&lt;code&gt;/tags.atom&lt;/code&gt;) and commits (&lt;code&gt;/commits/main.atom&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Subreddits have one at &lt;code&gt;https://www.reddit.com/r/NAME/.rss&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's a feed, put it in any feed reader (or an automation tool that emails you new items) and stop here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the view-source test
&lt;/h2&gt;

&lt;p&gt;Open the page, then open its source: &lt;code&gt;Ctrl+U&lt;/code&gt; / &lt;code&gt;Cmd+Option+U&lt;/code&gt;, or prefix the address with &lt;code&gt;view-source:&lt;/code&gt;. Search for the words you care about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The text is in the source.&lt;/strong&gt; The page is server-rendered. Any text-based monitor will see what you see, and a simple, cheap tool is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The source is mostly &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags and an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt;.&lt;/strong&gt; The page is rendered by JavaScript in the browser. A plain HTTP fetch sees an empty shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For JavaScript pages you have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the data URL.&lt;/strong&gt; Open DevTools, go to the Network tab, filter by Fetch/XHR, and reload. The page is usually loading JSON from an API. If that JSON URL works when you open it in a new tab (no login cookie needed), watch the JSON instead of the page. It's smaller, it changes only when the data changes, and many monitors handle JSON well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a monitor that runs a real browser&lt;/strong&gt; (Visualping, PageCrawl, or self-hosted changedetection.io with its Playwright fetcher). These are heavier and usually cost more per check, but they work on single-page apps.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: pick the narrowest URL you can
&lt;/h2&gt;

&lt;p&gt;Whatever tool you use, noise usually comes from the page, not the tool. Before you set anything up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer the specific page (one product, one notice, one job posting) over a homepage or listing.&lt;/li&gt;
&lt;li&gt;Look for a print view, a "text only" version, or a filtered URL (&lt;code&gt;?category=...&lt;/code&gt;) that shows only what you need.&lt;/li&gt;
&lt;li&gt;Watch out for text that changes on every load: "posted 5 minutes ago", rotating testimonials, view counters, "people are looking at this now". A monitor that compares the whole page will alert on every one of those. Either pick a different URL or use a tool that lets you select a region.&lt;/li&gt;
&lt;li&gt;Markup-only churn (cache-busting file names, CSRF tokens in attributes, analytics snippets) is harmless for monitors that compare &lt;em&gt;visible text&lt;/em&gt;. It's a problem for ones that compare raw HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: choose how you want to be alerted
&lt;/h2&gt;

&lt;p&gt;There are three broad families. None is "best"; they trade money for effort differently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Good at&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser extension, local checks&lt;/td&gt;
&lt;td&gt;Distill (local monitors)&lt;/td&gt;
&lt;td&gt;Free, can see pages you're logged into&lt;/td&gt;
&lt;td&gt;Only checks while your browser is running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;urlwatch, changedetection.io&lt;/td&gt;
&lt;td&gt;Free and flexible (selectors, filters, JS rendering)&lt;/td&gt;
&lt;td&gt;You run and update it, and it's down when your box is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted service&lt;/td&gt;
&lt;td&gt;Visualping, PageCrawl, ChangeTower, PingWhen&lt;/td&gt;
&lt;td&gt;Checks while your laptop is closed, no maintenance&lt;/td&gt;
&lt;td&gt;Monthly cost; free tiers are usually small&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're comfortable with a terminal, the smallest possible self-hosted version is a cron job. This one hashes the page's text and emails you when the hash changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# watch.sh URL: run from cron, e.g. */15 * * * * /home/me/watch.sh https://example.com/notice&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.pagewatch/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-16&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s1"&gt;'pagewatch/1.0'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;script[^&amp;gt;]*&amp;gt;.*&amp;lt;\/script&amp;gt;//g'&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;[^&amp;gt;]*&amp;gt;/ /g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt; changed"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Page changed: &lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; you@example.com
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Honest caveats: the &lt;code&gt;sed&lt;/code&gt; is crude (a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that spans several lines survives it), it needs a working &lt;code&gt;mail&lt;/code&gt; command, and because of &lt;code&gt;curl -f&lt;/code&gt; it silently skips a run when the site is down instead of telling you. It's a fine starting point for one or two pages. Past that, a real tool pays for itself in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: expect a baseline, then tune
&lt;/h2&gt;

&lt;p&gt;Every monitor has to store a first copy before it can tell you anything changed, so the first check is silent. If you then get alerts that don't matter, go back to Step 2 before you blame the tool. A narrower URL fixes most noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where PingWhen fits
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://pingwhen.app" rel="noopener noreferrer"&gt;PingWhen&lt;/a&gt; for the simple case: a public, server-rendered page (or a JSON URL) where you just want an email or a webhook when the text changes, plus a "down" and "back up" alert on the same URL. It compares the page's visible text (scripts and styles removed, ISO timestamps masked, JSON keys sorted) and sends one alert with a short excerpt of what's new. It doesn't render JavaScript or let you pick a CSS selector, and there's no free plan: it's $9/month for 10 pages checked every 15 minutes.&lt;/p&gt;

&lt;p&gt;Not sure whether a page is a good candidate? Our free &lt;a href="https://pingwhen.app/check" rel="noopener noreferrer"&gt;page check&lt;/a&gt; fetches it once, the way our watcher would, and tells you whether it's up and whether we can see its text.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://pingwhen.app/blog/get-notified-when-a-web-page-changes" rel="noopener noreferrer"&gt;PingWhen blog&lt;/a&gt;. Written with AI assistance for PingWhen, the page-change and uptime alert tool we make.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>automation</category>
      <category>monitoring</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
