The Automation Wing
Sitemap diff monitor: know the moment a page appears or disappears
A daily n8n workflow that fetches your sitemap, compares the URL list against yesterday's run held in workflow static data, and emails you when pages appear or disappear.
A sitemap is a quiet record of what a site thinks it publishes. When a page silently drops out, or a batch of new URLs appears that nobody told you about, the sitemap changes before anyone files a ticket. Watching it is one of the cheapest early-warning signals in SEO, and almost nobody does it because it means remembering to diff a file by hand.
This workflow remembers for you. Once a day it fetches your sitemap, pulls out the URLs, and compares them with the set it saw on the previous run, which it keeps in the workflow's own static data. If nothing changed it does nothing. If URLs appeared or disappeared it emails you the two lists. It only reads the sitemap and sends one email; it touches nothing on the site.
Before you start
- Find the sitemap you want to watch. A single
sitemap.xmlof page URLs is the simplest case; a sitemap index that points at other sitemaps needs an extra fetch step this starter does not include. - The email step needs an SMTP credential, attached after import. No credentials live inside the file.
- The first run has nothing to compare against, so it records the current URLs as the baseline and sends no alert. Changes are reported from the second run onward.
The workflow
Import the JSON below. The parsing keeps it dependency-free: the Code node reads the raw XML text and extracts everything inside <loc> tags, then diffs against the stored set via getWorkflowStaticData.
{
"name": "Sitemap diff monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "days",
"triggerAtHour": 6
}
]
}
},
"name": "Every morning",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [220, 300]
},
{
"parameters": {
"url": "https://example.com/sitemap.xml",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"name": "Fetch the sitemap",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
},
{
"parameters": {
"jsCode": "// Extract <loc> URLs from the raw sitemap XML, no XML library needed.\nconst xml = $input.first().json.data || $input.first().json.body || '';\nconst urls = [...String(xml).matchAll(/<loc>\\s*([^<]+?)\\s*<\\/loc>/g)].map(m => m[1].trim());\nconst current = [...new Set(urls)];\n\n// Static data survives between runs (but resets if the workflow is re-imported).\nconst store = $getWorkflowStaticData('global');\nconst previous = store.sitemapUrls || null;\nstore.sitemapUrls = current;\n\nif (previous === null) {\n // First run: record the baseline, report nothing.\n return [{ json: { firstRun: true, changed: false, added: [], removed: [], count: current.length } }];\n}\n\nconst prevSet = new Set(previous);\nconst currSet = new Set(current);\nconst added = current.filter(u => !prevSet.has(u));\nconst removed = previous.filter(u => !currSet.has(u));\n\nreturn [{ json: { firstRun: false, changed: added.length + removed.length > 0, added, removed, count: current.length } }];"
},
"name": "Diff against last run",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [700, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.changed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"name": "Only if something changed",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [940, 300]
},
{
"parameters": {
"fromEmail": "monitor@example.com",
"toEmail": "you@example.com",
"subject": "=Sitemap changed: {{ $json.added.length }} added, {{ $json.removed.length }} removed",
"emailFormat": "text",
"text": "=Added:\n{{ $json.added.join('\\n') || '(none)' }}\n\nRemoved:\n{{ $json.removed.join('\\n') || '(none)' }}",
"options": {}
},
"name": "Email the changes",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [1180, 220]
}
],
"connections": {
"Every morning": {
"main": [[{ "node": "Fetch the sitemap", "type": "main", "index": 0 }]]
},
"Fetch the sitemap": {
"main": [[{ "node": "Diff against last run", "type": "main", "index": 0 }]]
},
"Diff against last run": {
"main": [[{ "node": "Only if something changed", "type": "main", "index": 0 }]]
},
"Only if something changed": {
"main": [[{ "node": "Email the changes", "type": "main", "index": 0 }]]
}
}
}
Set it up
- Import the JSON and open the workflow.
- In "Fetch the sitemap", replace the URL with your own
sitemap.xml. - In "Email the changes", attach your SMTP credential and set the from and to addresses.
- Run the workflow once by hand. This first run records the baseline and sends nothing, which is expected.
- Activate it. From the next run onward, you are emailed only on the days the URL list actually moves.
How to read the alert, and the limitation to keep in mind
The email gives you two lists: URLs added since yesterday, and URLs removed. Added URLs are usually new content or a template change; removed URLs are the ones to look at first, because a page dropping out of the sitemap can mean it was unpublished, deleted, or accidentally excluded. The alert tells you what moved, not why, so treat it as a prompt to go and check, not a diagnosis.
The honest limitation is the state itself. The baseline lives in the workflow's static data, which is convenient but not permanent: if you re-import the workflow, duplicate it, or reset it, the stored URL set is lost and the next run starts a fresh baseline (recording the current URLs and reporting nothing, exactly like a first run). So a re-import will swallow one day's changes. If you need the comparison to survive that, keep the URL list somewhere external instead, a database node or a Google Sheet, which is a heavier build than this starter is meant to be.