From e3907074f53999a13b98f301c023459270d237fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 19:30:27 +0100 Subject: [PATCH] Redirect the one GSC URL that 404ed; pin every URL Google has shown GSC Performance export, 23 Oct 2026: 61 URLs with impressions. 60 already land on a sitemap page in one 301. /blog/how-to-reduce-shopify-apps- (48 impressions, a truncated pre-April URL) returned 404; it now goes to the kill-or-keep auditor. tests/gsc-known-urls.test.js replays the middleware for every URL in data/gsc-known-urls.json. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JF72ETBMPEJFo8F25FMa6d --- data/gsc-known-urls.json | 249 +++++++++++++++++++++++++++++++++ functions/_legacy-redirects.js | 10 ++ public/_redirects | 4 + tests/gsc-known-urls.test.js | 43 ++++++ 4 files changed, 306 insertions(+) create mode 100644 data/gsc-known-urls.json create mode 100644 tests/gsc-known-urls.test.js diff --git a/data/gsc-known-urls.json b/data/gsc-known-urls.json new file mode 100644 index 0000000..3eb6375 --- /dev/null +++ b/data/gsc-known-urls.json @@ -0,0 +1,249 @@ +{ + "_note": "Every URL Google Search Console showed in search results (Performance export, 23 Oct 2026, host + path). tests/gsc-known-urls.test.js holds each one to a single redirect onto a sitemap page. Add rows from future exports; never remove one.", + "urls": [ + { + "host": "audit.stackarchitect.xyz", + "path": "/" + }, + { + "host": "stackarchitect.xyz", + "path": "/" + }, + { + "host": "stackarchitect.xyz", + "path": "/analyzify-alternative/" + }, + { + "host": "stackarchitect.xyz", + "path": "/app-audit/" + }, + { + "host": "stackarchitect.xyz", + "path": "/elevar-alternative/" + }, + { + "host": "stackarchitect.xyz", + "path": "/klaviyo-vs-systeme-io/" + }, + { + "host": "stackarchitect.xyz", + "path": "/northbeam-alternative/" + }, + { + "host": "stackarchitect.xyz", + "path": "/pro/" + }, + { + "host": "stackarchitect.xyz", + "path": "/stack" + }, + { + "host": "stackarchitect.xyz", + "path": "/stack/" + }, + { + "host": "stackarchitect.xyz", + "path": "/tools/" + }, + { + "host": "stackarchitect.xyz", + "path": "/triple-whale-alternative/" + }, + { + "host": "tools.stackarchitect.xyz", + "path": "/" + }, + { + "host": "tools.stackarchitect.xyz", + "path": "/make-vs-zapier-cost-calculator" + }, + { + "host": "tools.stackarchitect.xyz", + "path": "/meta-emq-score-estimator" + }, + { + "host": "tools.stackarchitect.xyz", + "path": "/shopify-agentic-storefront-readiness-checker" + }, + { + "host": "tools.stackarchitect.xyz", + "path": "/stocky-migration-risk-scorer" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/autocrat-quota-fix" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/best-ai-tools-shopify" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/automate-shopify-inventory-operations-without-apps" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/best-alternatives-to-zapier-for-shopify" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/google-apps-script-quotas-explained-how-to-avoid-limits-and-scale-your-automations" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-much-shopify-apps-really-cost-and-how-to-cut-your-app-bill-in-half" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-bypass-ios-privacy-restrictions-in-shopify-ads" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-fix-service-invoked-too-many-times-in-google-apps-script" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-fix-shopify-conversion-tracking-after-ios-14" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-fix-shopify-conversion-tracking-after-ios-updates" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-reduce-shopify-apps-" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/how-to-track-shopify-inventory-in-google-sheets" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/klaviyo-vs-getresponse-shopify-2026" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/make-com-shopify-automation-guide" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/recover-lost-shopify-conversions-capi-shield" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/recover-lost-shopify-conversions-with-capi-shield" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/replace-shopify-stocky-with-a-0-inventory-system" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/scalable-google-sheets-automation-for-high-volume-workflows" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-agentic-storefronts-setup-guide-2026" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-ai-playbook-2026" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-apps-that-are-a-waste-of-money" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-automation-stack-for-small-stores" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-inventory-management-without-apps-automate-your-workflow-for-0" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-meta-roas-dropped-2026-fix" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/shopify-server-side-tracking-complete-setup-guide" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/the-complete-guide-to-reliable-shopify-automations" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/the-lean-shopify-tech-stack-2026" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/the-ultimate-guide-to-shopify-inventory-management" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/tidio-for-shopify-complete-setup-guide" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/tidio-vs-gorgias-shopify" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/why-meta-ads-show-fewer-purchases-than-shopify" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/blog/zapier-vs-shopify-flow-vs-make" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/capi-shield" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/shopify-app-cost-calculator" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/shopify-automation-guides" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/shopify-automation-scanner" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/shopify-optimization-guides" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/shopify-profit-loss-automation" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/sitemap" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/stocky-swap" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/tiktok-events-api-shopify" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/tools" + }, + { + "host": "www.stackarchitect.xyz", + "path": "/ultimate-shopify-automation-guide" + } + ] +} \ No newline at end of file diff --git a/functions/_legacy-redirects.js b/functions/_legacy-redirects.js index b4b7475..51b0928 100644 --- a/functions/_legacy-redirects.js +++ b/functions/_legacy-redirects.js @@ -1042,6 +1042,16 @@ export const LEGACY_RULES = [ "to": "/best-free-shopify-apps-2026/", "status": 301 }, + { + "from": "/blog/how-to-reduce-shopify-apps-", + "to": "/shopify-app-stack-kill-or-keep-auditor/", + "status": 301 + }, + { + "from": "/blog/how-to-reduce-shopify-apps-/", + "to": "/shopify-app-stack-kill-or-keep-auditor/", + "status": 301 + }, { "from": "/blog/free-shopify-apps-replace-paid-2026", "to": "/best-free-shopify-apps-2026/", diff --git a/public/_redirects b/public/_redirects index f647252..ba44e90 100644 --- a/public/_redirects +++ b/public/_redirects @@ -251,6 +251,10 @@ /blog/how-much-shopify-apps-really-cost-and-how-to-cut-your-app-bill-in-half/ /best-free-shopify-apps-2026/ 301 /blog/shopify-apps-that-are-a-waste-of-money /best-free-shopify-apps-2026/ 301 /blog/shopify-apps-that-are-a-waste-of-money/ /best-free-shopify-apps-2026/ 301 +# Truncated pre-April URL (trailing hyphen). GSC Performance, 23 Oct 2026: 48 impressions, +# and it 404ed. Sent to the tool that answers "which apps can I remove?". +/blog/how-to-reduce-shopify-apps- /shopify-app-stack-kill-or-keep-auditor/ 301 +/blog/how-to-reduce-shopify-apps-/ /shopify-app-stack-kill-or-keep-auditor/ 301 /blog/free-shopify-apps-replace-paid-2026 /best-free-shopify-apps-2026/ 301 /blog/free-shopify-apps-replace-paid-2026/ /best-free-shopify-apps-2026/ 301 diff --git a/tests/gsc-known-urls.test.js b/tests/gsc-known-urls.test.js new file mode 100644 index 0000000..23564aa --- /dev/null +++ b/tests/gsc-known-urls.test.js @@ -0,0 +1,43 @@ +// tests/gsc-known-urls.test.js: every URL Google has shown in search lands on +// a live page. +// +// GSC's Performance export of 23 Oct 2026 listed 61 URLs with impressions, +// most of them www, slashless or retired paths. One, a truncated +// "/blog/how-to-reduce-shopify-apps-" with 48 impressions, resolved to a 404. +// This replays the middleware's order (host to apex, legacy map first, then +// the trailing slash) for every URL in data/gsc-known-urls.json and fails +// unless it ends on a sitemap URL. Subdomains tools./audit. are dashboard +// redirect rules; for those only the path is checked, as on the apex. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { resolveLegacy } from '../functions/_legacy-redirects.js'; + +const KNOWN = JSON.parse(fs.readFileSync('data/gsc-known-urls.json', 'utf8')).urls; +const SITEMAP = fs.existsSync('dist/sitemap-0.xml') + ? new Set([...fs.readFileSync('dist/sitemap-0.xml', 'utf8').matchAll(/https:\/\/stackarchitect\.xyz([^<]*)<\/loc>/g)].map((m) => m[1])) + : null; +// Deliberately indexable pages outside the sitemap (see CLAUDE.md). +const ALSO_LIVE = new Set(['/sitemap-page/']); + +function land(path) { + const hit = resolveLegacy(path); + if (hit && hit.to !== path) return { to: hit.to, hops: 1 }; + if (!path.endsWith('/') && !/\.[a-z0-9]+$/i.test(path)) return { to: path + '/', hops: 1 }; + return { to: path, hops: 0 }; +} + +test('every URL Google has shown lands on a live page in at most one redirect', { skip: !SITEMAP && 'needs a build' }, () => { + const bad = []; + for (const { host, path } of KNOWN) { + const { to } = land(path); + const settled = land(to); + if (settled.hops) bad.push(`${host}${path} -> ${to} -> ${settled.to} (chain)`); + else if (!SITEMAP.has(to) && !ALSO_LIVE.has(to)) bad.push(`${host}${path} -> ${to} (not a live page)`); + } + assert.deepEqual(bad, [], bad.join('\n')); +}); + +test('the 23 Oct 2026 404 is covered', () => { + assert.equal(resolveLegacy('/blog/how-to-reduce-shopify-apps-')?.to, '/shopify-app-stack-kill-or-keep-auditor/'); +});