From 756e749ab71e843360403c040e6437a2c9704a7d Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 8 Apr 2026 17:22:16 +0300 Subject: [PATCH 1/5] Add cases indexing controls and Apostrophe sitemap/robots setup --- website/app.js | 2 ++ .../modules/@apostrophecms/sitemap/index.js | 5 +++ website/modules/case-studies-page/index.js | 34 +++++++++++++++++++ .../case-studies-page/views/index.html | 5 +++ .../modules/case-studies-page/views/show.html | 19 ++++++++++- website/modules/robots/index.js | 25 ++++++++++++++ website/package-lock.json | 10 ++++++ website/package.json | 1 + 8 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 website/modules/@apostrophecms/sitemap/index.js create mode 100644 website/modules/robots/index.js diff --git a/website/app.js b/website/app.js index 0ff482f1..e8192e1d 100644 --- a/website/app.js +++ b/website/app.js @@ -67,6 +67,7 @@ function createAposConfig() { }, // Enable local SEO module with GTM integration '@apostrophecms/seo': {}, + '@apostrophecms/sitemap': {}, '@apostrophecms/global': {}, // Make getEnv function available to templates '@apostrophecms/template': { @@ -79,6 +80,7 @@ function createAposConfig() { // Add global data module 'global-data': {}, + 'robots': {}, // Shared constants module '@apostrophecms/shared-constants': {}, diff --git a/website/modules/@apostrophecms/sitemap/index.js b/website/modules/@apostrophecms/sitemap/index.js new file mode 100644 index 00000000..b3eaa530 --- /dev/null +++ b/website/modules/@apostrophecms/sitemap/index.js @@ -0,0 +1,5 @@ +module.exports = { + options: { + cacheLifetime: 60 * 60, + }, +}; diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index 1758a6ae..350d344d 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -165,6 +165,36 @@ const runSetupIndexData = async function (self, req) { } }; +const buildIndexSeoData = function (req) { + const query = req.query || {}; + const hasFilterParams = + Boolean(query.search) || + Boolean(query.industry) || + Boolean(query.stack) || + Boolean(query.caseStudyType) || + Boolean(query.partner); + const pageNumber = Number(query.page || 1); + const hasPaginationParam = Number.isFinite(pageNumber) && pageNumber > 1; + const shouldNoindex = hasFilterParams || hasPaginationParam; + let pageUrl = '/cases'; + if (req.data && req.data.page && req.data.page.slug) { + pageUrl = req.data.page.slug; + } + let robots = 'index,follow'; + if (shouldNoindex) { + robots = 'noindex,follow'; + } + return { + canonicalUrl: pageUrl, + robots, + }; +}; + +const runSetupIndexSeoData = function (req) { + req.data ||= {}; + req.data.caseListingSeo = buildIndexSeoData(req); +}; + const runSetupShowData = async function (self, req) { try { const navigation = await NavigationService.getNavigationDataForPage( @@ -219,6 +249,7 @@ module.exports = { await self.resolveSearchRelationships(req); await self.applyEnhancedSearchResults(req); await self.setupIndexData(req); + self.setupIndexSeoData(req); }; const superBeforeShow = self.beforeShow; @@ -244,6 +275,9 @@ module.exports = { setupIndexData(req) { return runSetupIndexData(self, req); }, + setupIndexSeoData(req) { + return runSetupIndexSeoData(req); + }, setupShowData(req) { return runSetupShowData(self, req); }, diff --git a/website/modules/case-studies-page/views/index.html b/website/modules/case-studies-page/views/index.html index 9680b986..663a9098 100644 --- a/website/modules/case-studies-page/views/index.html +++ b/website/modules/case-studies-page/views/index.html @@ -1,6 +1,11 @@ {# modules/case-studies-page/views/index.html #} {% extends "layout.html" %} {% import '@apostrophecms/pager:macros.html' as pager with context %} +{% block extraHead %} + {{ super() }} + + +{% endblock %} {% block main %}
+ +{% endblock %} +{% block main %}
diff --git a/website/modules/robots/index.js b/website/modules/robots/index.js new file mode 100644 index 00000000..a226ee28 --- /dev/null +++ b/website/modules/robots/index.js @@ -0,0 +1,25 @@ +module.exports = { + routes(self) { + return { + get: { + '/robots.txt': (req, res) => { + const baseUrl = self.apos.baseUrl || ''; + const isDevHost = baseUrl.includes('://dv.'); + const isProduction = process.env.NODE_ENV === 'production'; + if (!isProduction || isDevHost) { + return res.type('text/plain').send('User-agent: *\nDisallow: /\n'); + } + + const robotsContent = + 'User-agent: *\n' + + 'Allow: /\n\n' + + '# Block indexation of huge filter/search combinations under /cases\n' + + 'Disallow: /cases?*\n' + + 'Disallow: /cases/*?*\n\n' + + `Sitemap: ${baseUrl}/sitemap.xml\n`; + return res.type('text/plain').send(robotsContent); + }, + }, + }; + }, +}; diff --git a/website/package-lock.json b/website/package-lock.json index 52d6f083..99427fba 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -12,6 +12,7 @@ "@apostrophecms/form": "^1.4.2", "@apostrophecms/import-export": "^3.2.0", "@apostrophecms/security-headers": "^1.0.2", + "@apostrophecms/sitemap": "^1.2.0", "@barba/core": "^2.10.3", "abort-controller": "^3.0.0", "apostrophe": "^4.17.0", @@ -119,6 +120,15 @@ "integrity": "sha512-BoBIRdWkXSxZMt8CFlKUevOm2BU73wRHACsQTjFiNBfUso2MWy2UuufId3akR5WAnCsaRRUSUCgg8CbaOiSxPA==", "license": "MIT" }, + "node_modules/@apostrophecms/sitemap": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apostrophecms/sitemap/-/sitemap-1.2.0.tgz", + "integrity": "sha512-NmlZ+2+XM9hDKQgk2TQM6BPKSLAQJzGSwPNvc5lN3X3C/DPW/e6KqsuPfnZ0/65O9yKN8CbGYsUCQyEGAT+Caw==", + "license": "MIT", + "dependencies": { + "common-tags": "^1.8.0" + } + }, "node_modules/@apostrophecms/vue-material-design-icons": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@apostrophecms/vue-material-design-icons/-/vue-material-design-icons-1.0.0.tgz", diff --git a/website/package.json b/website/package.json index 81f940d3..0c0ce836 100644 --- a/website/package.json +++ b/website/package.json @@ -41,6 +41,7 @@ "@apostrophecms/form": "^1.4.2", "@apostrophecms/import-export": "^3.2.0", "@apostrophecms/security-headers": "^1.0.2", + "@apostrophecms/sitemap": "^1.2.0", "@barba/core": "^2.10.3", "abort-controller": "^3.0.0", "apostrophe": "^4.17.0", From 20d9dc39dfc185c22b88e35566985ef42a34a5e9 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 8 Apr 2026 17:47:52 +0300 Subject: [PATCH 2/5] fix(seo): harden robots env gating and query-variant noindex behavior --- .../case-studies-page/services/UrlService.js | 1 + website/modules/case-studies-page/views/show.html | 10 +--------- website/modules/robots/index.js | 14 +++++++++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/website/modules/case-studies-page/services/UrlService.js b/website/modules/case-studies-page/services/UrlService.js index f18963a9..f4b15c96 100644 --- a/website/modules/case-studies-page/services/UrlService.js +++ b/website/modules/case-studies-page/services/UrlService.js @@ -153,6 +153,7 @@ class UrlService { reqCopy.data.backUrl = UrlService.buildCaseStudyUrl('/cases', queryParams); reqCopy.data.query = queryParams; + reqCopy.data.hasQueryParams = Object.keys(req.query || {}).length > 0; } } diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index 962665d4..cda9a8f1 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -1,18 +1,10 @@ {% extends "layout.html" %} {% block extraHead %} {{ super() }} - {% set hasQueryParams = - data.query.search or - data.query.industry or - data.query.stack or - data.query.caseStudyType or - data.query.partner or - data.query.page - %} {% endblock %} {% block main %} diff --git a/website/modules/robots/index.js b/website/modules/robots/index.js index a226ee28..d6703af2 100644 --- a/website/modules/robots/index.js +++ b/website/modules/robots/index.js @@ -4,18 +4,22 @@ module.exports = { get: { '/robots.txt': (req, res) => { const baseUrl = self.apos.baseUrl || ''; - const isDevHost = baseUrl.includes('://dv.'); + let baseHost = ''; + try { + baseHost = new URL(baseUrl).hostname; + } catch (error) { + self.apos.util.warn('Invalid baseUrl for robots.txt route', error); + baseHost = ''; + } const isProduction = process.env.NODE_ENV === 'production'; - if (!isProduction || isDevHost) { + const isProductionHost = baseHost === 'www.speedandfunction.com'; + if (!isProduction || !isProductionHost) { return res.type('text/plain').send('User-agent: *\nDisallow: /\n'); } const robotsContent = 'User-agent: *\n' + 'Allow: /\n\n' + - '# Block indexation of huge filter/search combinations under /cases\n' + - 'Disallow: /cases?*\n' + - 'Disallow: /cases/*?*\n\n' + `Sitemap: ${baseUrl}/sitemap.xml\n`; return res.type('text/plain').send(robotsContent); }, From 39250065b1229a9783321dcb709abcf01afde7f1 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 8 Apr 2026 17:58:51 +0300 Subject: [PATCH 3/5] fix(robots): allow both apex and www production hosts --- website/modules/robots/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/modules/robots/index.js b/website/modules/robots/index.js index d6703af2..10405727 100644 --- a/website/modules/robots/index.js +++ b/website/modules/robots/index.js @@ -12,7 +12,11 @@ module.exports = { baseHost = ''; } const isProduction = process.env.NODE_ENV === 'production'; - const isProductionHost = baseHost === 'www.speedandfunction.com'; + const productionHosts = [ + 'speedandfunction.com', + 'www.speedandfunction.com', + ]; + const isProductionHost = productionHosts.includes(baseHost); if (!isProduction || !isProductionHost) { return res.type('text/plain').send('User-agent: *\nDisallow: /\n'); } From c2d1869c0f735a6a96299f482a3af4284d73f345 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 8 Apr 2026 18:05:05 +0300 Subject: [PATCH 4/5] fix(robots): normalize baseUrl origin for sitemap link --- website/modules/robots/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/modules/robots/index.js b/website/modules/robots/index.js index 10405727..718eecf5 100644 --- a/website/modules/robots/index.js +++ b/website/modules/robots/index.js @@ -17,6 +17,8 @@ module.exports = { 'www.speedandfunction.com', ]; const isProductionHost = productionHosts.includes(baseHost); + const parsedBaseUrl = new URL(baseUrl); + const normalizedBaseUrl = parsedBaseUrl.origin; if (!isProduction || !isProductionHost) { return res.type('text/plain').send('User-agent: *\nDisallow: /\n'); } @@ -24,7 +26,7 @@ module.exports = { const robotsContent = 'User-agent: *\n' + 'Allow: /\n\n' + - `Sitemap: ${baseUrl}/sitemap.xml\n`; + `Sitemap: ${normalizedBaseUrl}/sitemap.xml\n`; return res.type('text/plain').send(robotsContent); }, }, From f4583871f8a920dd7c88a125ae1dbc974d9ddf18 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Thu, 9 Apr 2026 15:09:33 +0300 Subject: [PATCH 5/5] fix cases query nofollow --- website/modules/case-studies-page/index.js | 2 +- website/modules/case-studies-page/views/show.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/modules/case-studies-page/index.js b/website/modules/case-studies-page/index.js index 350d344d..fca7e6d7 100644 --- a/website/modules/case-studies-page/index.js +++ b/website/modules/case-studies-page/index.js @@ -182,7 +182,7 @@ const buildIndexSeoData = function (req) { } let robots = 'index,follow'; if (shouldNoindex) { - robots = 'noindex,follow'; + robots = 'noindex,nofollow'; } return { canonicalUrl: pageUrl, diff --git a/website/modules/case-studies-page/views/show.html b/website/modules/case-studies-page/views/show.html index cda9a8f1..8df752f1 100644 --- a/website/modules/case-studies-page/views/show.html +++ b/website/modules/case-studies-page/views/show.html @@ -4,7 +4,7 @@ {% endblock %} {% block main %}