Skip to content

Commit e82c952

Browse files
jr-rkclaude
andcommitted
fix(ssr): drop dead scoped-search patterns, add robots.txt test
Copilot review (and an independent verification pass) found that the two scoped-search SSR excludes and their matching robots.txt lines never match anything on this 7.6.5 branch: collection/community routing has no `:id/search` child route here (that shape is DSpace 9.x, where the mendelu source commit came from), and scoped search on 7.6.5 is `/search?scope=<uuid>&f.*` — already covered by the pre-existing `Disallow: /search` and `^/search` SSR exclusion. - Remove the dead `^/(communities|collections)/[uuid]/search` patterns from config.yml, config.example.yml, environment.ts, environment.production.ts, environment.test.ts, and the matching dead robots.txt Disallow lines. - Keep the effective, new parts: the generic facet trap (Disallow: /*?f. and /*&f.) and the three opt-in toggles — those are unaffected by the routing question. - Extract the toggle-line assembly into buildOptionalRobotsDisallows() (src/config/robots.util.ts), rendered as a single EJS local from server.ts, and add a unit test (robots.util.spec.ts) covering each toggle and the no-blank-line-in-group invariant Copilot flagged as untested. Not applied: rewriting isExcludedFromSsr to inspect facet query keys. The documented incident (scoped-search facet enumeration) is already covered by the existing /search SSR exclusion; changing core SSR routing logic for every request is out of scope for this fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 001de25 commit e82c952

9 files changed

Lines changed: 75 additions & 67 deletions

File tree

config/config.example.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@ universal:
3131
flag: "i"
3232
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
3333
flag: "i"
34-
# Scoped search inside a community/collection is the most expensive route, and its facet
35-
# links are effectively unbounded; crawlers walking them saturated SSR in production
36-
# (HTTP 504). Serve these as a plain CSR shell. src/robots.txt.ejs asks well-behaved
37-
# crawlers to stay out of the same paths; this handles the ones that ignore robots.txt.
38-
- pattern: "^/communities/[a-f0-9-]{36}/search(/.*)?$"
39-
flag: "i"
40-
- pattern: "^/collections/[a-f0-9-]{36}/search(/.*)?$"
41-
flag: "i"
4234
- pattern: "^/browse/"
4335
- pattern: "^/search$"
4436
- pattern: "^/community-list$"

config/config.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@ universal:
1919
flag: "i"
2020
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
2121
flag: "i"
22-
# Scoped search inside a community/collection is the most expensive route, and its facet
23-
# links are effectively unbounded; crawlers walking them saturated SSR in production
24-
# (HTTP 504). Serve these as a plain CSR shell. src/robots.txt.ejs asks well-behaved
25-
# crawlers to stay out of the same paths; this handles the ones that ignore robots.txt.
26-
- pattern: "^/communities/[a-f0-9-]{36}/search(/.*)?$"
27-
flag: "i"
28-
- pattern: "^/collections/[a-f0-9-]{36}/search(/.*)?$"
29-
flag: "i"
3022
- pattern: "^/browse/"
3123
- pattern: "^/search$"
3224
- pattern: "^/community-list$"

server.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { extendEnvironmentWithAppConfig } from './src/config/config.util';
5656
import { logStartupMessage } from './startup-message';
5757
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
5858
import { SsrExcludePatterns } from './src/config/universal-config.interface';
59+
import { buildOptionalRobotsDisallows } from './src/config/robots.util';
5960

6061

6162
/*
@@ -163,15 +164,13 @@ export function app() {
163164

164165
/**
165166
* Serve the robots.txt ejs template, filling in the origin variable and the
166-
* per-instance optional-block toggles (see config `robots`).
167+
* per-instance optional Disallow blocks (see config `robots`).
167168
*/
168169
server.get('/robots.txt', (req, res) => {
169170
res.setHeader('content-type', 'text/plain');
170171
res.render('assets/robots.txt.ejs', {
171172
'origin': req.protocol + '://' + req.headers.host,
172-
'disallowHandle': environment.robots.disallowHandle,
173-
'disallowBrowse': environment.robots.disallowBrowse,
174-
'disallowBitstreams': environment.robots.disallowBitstreams
173+
'optionalDisallows': buildOptionalRobotsDisallows(environment.robots)
175174
});
176175
});
177176

src/config/robots.util.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { buildOptionalRobotsDisallows } from './robots.util';
2+
import { RobotsConfig } from './robots-config.interface';
3+
4+
describe('buildOptionalRobotsDisallows', () => {
5+
const allOff: RobotsConfig = {
6+
disallowHandle: false,
7+
disallowBrowse: false,
8+
disallowBitstreams: false,
9+
};
10+
11+
it('emits nothing when every toggle is off', () => {
12+
expect(buildOptionalRobotsDisallows(allOff)).toBe('');
13+
});
14+
15+
it('emits only /browse when disallowBrowse is on', () => {
16+
expect(buildOptionalRobotsDisallows({ ...allOff, disallowBrowse: true }))
17+
.toBe('\nDisallow: /browse');
18+
});
19+
20+
it('emits only /handle when disallowHandle is on', () => {
21+
expect(buildOptionalRobotsDisallows({ ...allOff, disallowHandle: true }))
22+
.toBe('\nDisallow: /handle');
23+
});
24+
25+
it('emits both bitstream rules when disallowBitstreams is on', () => {
26+
const out = buildOptionalRobotsDisallows({ ...allOff, disallowBitstreams: true });
27+
expect(out).toContain('Disallow: /bitstream/');
28+
expect(out).toContain('Disallow: /bitstreams/');
29+
});
30+
31+
it('leads with a newline and never a blank line, so the block stays inside the group', () => {
32+
const out = buildOptionalRobotsDisallows({
33+
disallowHandle: true,
34+
disallowBrowse: true,
35+
disallowBitstreams: true,
36+
});
37+
expect(out.startsWith('\n')).toBeTrue();
38+
expect(out).not.toContain('\n\n');
39+
});
40+
});

src/config/robots.util.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { RobotsConfig } from './robots-config.interface';
2+
3+
/**
4+
* Build the optional, per-instance `Disallow` lines for the served robots.txt
5+
* (see `src/robots.txt.ejs`) from config. The always-on facet-trap rules live in
6+
* the template itself; only these switchable blocks are assembled here so the
7+
* toggle logic has a unit-testable seam.
8+
*
9+
* Returns a string that already starts with a newline when non-empty, so it can
10+
* be appended directly after the last static rule without leaving a blank line
11+
* inside the `User-agent: *` group record (blank lines end a group).
12+
*/
13+
export function buildOptionalRobotsDisallows(robots: RobotsConfig): string {
14+
const lines: string[] = [];
15+
if (robots?.disallowBrowse) {
16+
lines.push('Disallow: /browse');
17+
}
18+
if (robots?.disallowHandle) {
19+
lines.push('Disallow: /handle');
20+
}
21+
if (robots?.disallowBitstreams) {
22+
// Bitstream content is reachable by handle path and by UUID; block both.
23+
lines.push('Disallow: /bitstream/');
24+
lines.push('Disallow: /bitstreams/');
25+
}
26+
return lines.length ? '\n' + lines.join('\n') : '';
27+
}

src/environments/environment.production.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,6 @@ export const environment: Partial<BuildConfig> = {
2020
pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$',
2121
flag: 'i',
2222
},
23-
// Scoped search + its facet links: the crawler trap that saturated SSR in production.
24-
{
25-
pattern: '^/communities/[a-f0-9-]{36}/search(/.*)?$',
26-
flag: 'i',
27-
},
28-
{
29-
pattern: '^/collections/[a-f0-9-]{36}/search(/.*)?$',
30-
flag: 'i',
31-
},
3223
{ pattern: '^/browse/' },
3324
{ pattern: '^/search' },
3425
{ pattern: '^/community-list$' },

src/environments/environment.test.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,6 @@ export const environment: BuildConfig = {
2323
pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$',
2424
flag: 'i',
2525
},
26-
// Scoped search + its facet links: the crawler trap that saturated SSR in production.
27-
{
28-
pattern: '^/communities/[a-f0-9-]{36}/search(/.*)?$',
29-
flag: 'i',
30-
},
31-
{
32-
pattern: '^/collections/[a-f0-9-]{36}/search(/.*)?$',
33-
flag: 'i',
34-
},
3526
{ pattern: '^/browse/' },
3627
{ pattern: '^/search' },
3728
{ pattern: '^/community-list$' },

src/environments/environment.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,6 @@ export const environment: Partial<BuildConfig> = {
2525
pattern: '^/collections/[a-f0-9-]{36}/browse(/.*)?$',
2626
flag: 'i',
2727
},
28-
// Scoped search + its facet links: the crawler trap that saturated SSR in production.
29-
{
30-
pattern: '^/communities/[a-f0-9-]{36}/search(/.*)?$',
31-
flag: 'i',
32-
},
33-
{
34-
pattern: '^/collections/[a-f0-9-]{36}/search(/.*)?$',
35-
flag: 'i',
36-
},
3728
{ pattern: '^/browse/' },
3829
{ pattern: '^/search' },
3930
{ pattern: '^/community-list$' },

src/robots.txt.ejs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,12 @@ Disallow: /profile
1818
Disallow: /workflowitems
1919
# Crawlers should be able to access entity pages, but not the facet search links present on entity pages
2020
Disallow: /entities/*?f
21-
# "Disallow: /search" above only matches paths that START with /search, so scoped
22-
# search inside a community or collection stayed crawlable. Enumerating its facet
23-
# links saturated SSR in production (HTTP 504); block it and every facet URL.
24-
Disallow: /collections/*/search
25-
Disallow: /communities/*/search
26-
# Any URL carrying a Discovery facet filter (f.author, f.subject, ...). Two rules
27-
# because the facet can be the first query parameter (?f.) or a later one (&f.),
28-
# and robots.txt cannot express "either".
21+
# Discovery facet links (f.author, f.subject, ...) multiply into an effectively
22+
# unbounded set of crawlable URLs; enumerating them saturated SSR in production
23+
# (HTTP 504). Two rules because the facet can be the first query parameter (?f.)
24+
# or a later one (&f.), and robots.txt cannot express "either".
2925
Disallow: /*?f.
30-
Disallow: /*&f.
31-
<% if (disallowBrowse) { -%>
32-
Disallow: /browse
33-
<% } -%>
34-
<% if (disallowHandle) { -%>
35-
Disallow: /handle
36-
<% } -%>
37-
<% if (disallowBitstreams) { -%>
38-
# Bitstream content is reachable by handle path and by UUID; block both.
39-
Disallow: /bitstream/
40-
Disallow: /bitstreams/
41-
<% } -%>
26+
Disallow: /*&f.<%- optionalDisallows %>
4227

4328
# Optionally uncomment the following line ONLY if sitemaps are working
4429
# and you have verified that your site is being indexed correctly.

0 commit comments

Comments
 (0)