Skip to content

Commit bb857df

Browse files
authored
Add cases indexing controls and Apostrophe sitemap/robots setup (#270)
## Summary - Add SEO indexing controls for `/cases` listing pages to prevent indexing of filter/search/pagination URL combinations. - Keep clean case detail URLs indexable and canonicalized, while query-param detail variants are noindex. - Integrate `@apostrophecms/sitemap` and configure robots handling for dev/prod behavior, including sitemap publication. ## What changed - Enabled sitemap module in `website/app.js`. - Added sitemap config in `website/modules/@apostrophecms/sitemap/index.js`. - Added cases listing SEO logic (`index/noindex` + canonical) in: - `website/modules/case-studies-page/index.js` - `website/modules/case-studies-page/views/index.html` - Added detail page canonical/robots behavior for query-param variants in: - `website/modules/case-studies-page/views/show.html` - Added dynamic `robots.txt` route in `website/modules/robots/index.js`: - Dev/non-prod: `Disallow: /` - Prod: allow crawl, block `/cases` query combinations, publish sitemap URL from `baseUrl` - Installed compatible sitemap package version in `website/package.json`: - `@apostrophecms/sitemap@^1.2.0` ## Why - Prevent crawler/index bloat caused by massive filter/search URL combinations on case listings. - Preserve SEO value on canonical case detail pages. - Provide a clean, machine-readable sitemap for search engines and AI crawlers. ## Validation - Verified `http://localhost:3000/sitemap.xml` returns sitemap entries for public pages and case detail pages. - Verified `robots.txt` behavior in local/dev mode returns `Disallow: /`. - Verified listing/detail pages output expected canonical and robots tags for clean vs query-param URLs. ## Notes - Production correctness depends on `baseUrl` being set to `https://www.speedandfunction.com`. - Dev environment remains non-indexable by design.
1 parent 2d19c8f commit bb857df

9 files changed

Lines changed: 103 additions & 1 deletion

File tree

website/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ function createAposConfig() {
6767
},
6868
// Enable local SEO module with GTM integration
6969
'@apostrophecms/seo': {},
70+
'@apostrophecms/sitemap': {},
7071
'@apostrophecms/global': {},
7172
// Make getEnv function available to templates
7273
'@apostrophecms/template': {
@@ -79,6 +80,7 @@ function createAposConfig() {
7980

8081
// Add global data module
8182
'global-data': {},
83+
'robots': {},
8284

8385
// Shared constants module
8486
'@apostrophecms/shared-constants': {},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
options: {
3+
cacheLifetime: 60 * 60,
4+
},
5+
};

website/modules/case-studies-page/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,36 @@ const runSetupIndexData = async function (self, req) {
165165
}
166166
};
167167

168+
const buildIndexSeoData = function (req) {
169+
const query = req.query || {};
170+
const hasFilterParams =
171+
Boolean(query.search) ||
172+
Boolean(query.industry) ||
173+
Boolean(query.stack) ||
174+
Boolean(query.caseStudyType) ||
175+
Boolean(query.partner);
176+
const pageNumber = Number(query.page || 1);
177+
const hasPaginationParam = Number.isFinite(pageNumber) && pageNumber > 1;
178+
const shouldNoindex = hasFilterParams || hasPaginationParam;
179+
let pageUrl = '/cases';
180+
if (req.data && req.data.page && req.data.page.slug) {
181+
pageUrl = req.data.page.slug;
182+
}
183+
let robots = 'index,follow';
184+
if (shouldNoindex) {
185+
robots = 'noindex,follow';
186+
}
187+
return {
188+
canonicalUrl: pageUrl,
189+
robots,
190+
};
191+
};
192+
193+
const runSetupIndexSeoData = function (req) {
194+
req.data ||= {};
195+
req.data.caseListingSeo = buildIndexSeoData(req);
196+
};
197+
168198
const runSetupShowData = async function (self, req) {
169199
try {
170200
const navigation = await NavigationService.getNavigationDataForPage(
@@ -219,6 +249,7 @@ module.exports = {
219249
await self.resolveSearchRelationships(req);
220250
await self.applyEnhancedSearchResults(req);
221251
await self.setupIndexData(req);
252+
self.setupIndexSeoData(req);
222253
};
223254

224255
const superBeforeShow = self.beforeShow;
@@ -244,6 +275,9 @@ module.exports = {
244275
setupIndexData(req) {
245276
return runSetupIndexData(self, req);
246277
},
278+
setupIndexSeoData(req) {
279+
return runSetupIndexSeoData(req);
280+
},
247281
setupShowData(req) {
248282
return runSetupShowData(self, req);
249283
},

website/modules/case-studies-page/services/UrlService.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class UrlService {
153153

154154
reqCopy.data.backUrl = UrlService.buildCaseStudyUrl('/cases', queryParams);
155155
reqCopy.data.query = queryParams;
156+
reqCopy.data.hasQueryParams = Object.keys(req.query || {}).length > 0;
156157
}
157158
}
158159

website/modules/case-studies-page/views/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{# modules/case-studies-page/views/index.html #}
22
{% extends "layout.html" %}
33
{% import '@apostrophecms/pager:macros.html' as pager with context %}
4+
{% block extraHead %}
5+
{{ super() }}
6+
<link rel="canonical" href="{{ data.caseListingSeo.canonicalUrl | e }}" />
7+
<meta name="robots" content="{{ data.caseListingSeo.robots | e }}" />
8+
{% endblock %}
49
{% block main %}
510
<div
611
class="cs_container"

website/modules/case-studies-page/views/show.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
{% extends "layout.html" %} {% block main %}
1+
{% extends "layout.html" %}
2+
{% block extraHead %}
3+
{{ super() }}
4+
<link rel="canonical" href="{{ data.piece._url | e }}" />
5+
<meta
6+
name="robots"
7+
content="{% if data.hasQueryParams %}noindex,follow{% else %}index,follow{% endif %}"
8+
/>
9+
{% endblock %}
10+
{% block main %}
211

312
<div>
413
<div class="cs_nav-container">

website/modules/robots/index.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module.exports = {
2+
routes(self) {
3+
return {
4+
get: {
5+
'/robots.txt': (req, res) => {
6+
const baseUrl = self.apos.baseUrl || '';
7+
let baseHost = '';
8+
try {
9+
baseHost = new URL(baseUrl).hostname;
10+
} catch (error) {
11+
self.apos.util.warn('Invalid baseUrl for robots.txt route', error);
12+
baseHost = '';
13+
}
14+
const isProduction = process.env.NODE_ENV === 'production';
15+
const productionHosts = [
16+
'speedandfunction.com',
17+
'www.speedandfunction.com',
18+
];
19+
const isProductionHost = productionHosts.includes(baseHost);
20+
const parsedBaseUrl = new URL(baseUrl);
21+
const normalizedBaseUrl = parsedBaseUrl.origin;
22+
if (!isProduction || !isProductionHost) {
23+
return res.type('text/plain').send('User-agent: *\nDisallow: /\n');
24+
}
25+
26+
const robotsContent =
27+
'User-agent: *\n' +
28+
'Allow: /\n\n' +
29+
`Sitemap: ${normalizedBaseUrl}/sitemap.xml\n`;
30+
return res.type('text/plain').send(robotsContent);
31+
},
32+
},
33+
};
34+
},
35+
};

website/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@apostrophecms/form": "^1.4.2",
4242
"@apostrophecms/import-export": "^3.2.0",
4343
"@apostrophecms/security-headers": "^1.0.2",
44+
"@apostrophecms/sitemap": "^1.2.0",
4445
"@barba/core": "^2.10.3",
4546
"abort-controller": "^3.0.0",
4647
"apostrophe": "^4.17.0",

0 commit comments

Comments
 (0)