Skip to content

fix(seo): make the trailing-slash redirect relative - #10476

Merged
MarkusNeusinger merged 2 commits into
mainfrom
fix/seo-relative-slash-redirect
Aug 18, 2026
Merged

fix(seo): make the trailing-slash redirect relative#10476
MarkusNeusinger merged 2 commits into
mainfrom
fix/seo-relative-slash-redirect

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

What I shipped wrong

#10473 removed a redirect that leaked the internal proxy URL and replaced it with a smaller version of the same defect. Verified live after the deploy:

$ curl -A Googlebot -sSI https://anyplot.ai/box-basic/ | grep -i '^http\|^location'
HTTP/2 301
location: http://anyplot.ai:8080/box-basic

Internal port, plain http. The api.anyplot.ai/seo-proxy/… leak is genuinely gone — this is not a regression to the old state — but nginx builds a permanent rewrite's Location from $scheme://$host:$server_port, and behind Cloud Run that is http on 8080.

Fix

absolute_redirect off makes the Location relative (/box-basic). The client resolves it against the URL it actually requested, which is the only value in this setup guaranteed to be correct. Hardcoding https://anyplot.ai would work today and rot the next time the scheme, host or port changes.

The guard nearly earned its keep

bot-serving-check already rejects a trailing-slash target starting with http://, so it would have failed on its next daily run — the check written in #10473 was correct, it just runs daily rather than at deploy. It now also rejects an internal port explicitly, so the next variant of this does not need the scheme to be wrong before it fails:

case "$slash_target" in
  *"/seo-proxy"*|http://*|*:8080/*)

Note on verification

nginx changes are only observable after deploy — this repo has no local loop for them, which is stated in CLAUDE.md as a known gap and is exactly how this got out. The live curl above is the verification; the guard is what makes the next one cheaper to catch.

🤖 Generated with Claude Code

#10473 removed a redirect that leaked http://api.anyplot.ai/seo-proxy/…
and shipped one that leaks http://anyplot.ai:8080/… instead. Smaller, but
the same defect: nginx builds a `permanent` rewrite's Location from
$scheme://$host:$server_port, and behind Cloud Run that is plain http on
port 8080. Verified live after the deploy — /box-basic/ answered

    301 Location: http://anyplot.ai:8080/box-basic

absolute_redirect off makes the Location relative. The client resolves it
against the URL it actually requested, which is the only value in this
setup guaranteed to be correct — hardcoding a scheme and host would work
today and rot the next time either changes.

The daily bot-serving-check would have caught it on its next run: the
guard added in #10473 already rejects an http:// target. It now rejects
an internal port explicitly as well, so the next variant of this does not
need the scheme to be wrong before it fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 19:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an SEO-related redirect bug in the anyplot-app nginx layer where the trailing-slash canonicalization was emitting an absolute Location header derived from Cloud Run’s internal scheme/port, producing http://…:8080/... for crawlers. It also tightens the scheduled bot-serving monitor to catch internal-port leaks.

Changes:

  • Configure nginx to emit a relative Location for the trailing-slash redirect via absolute_redirect off.
  • Add an explicit guard in bot-serving-check to reject redirects that include :8080.
  • Document the incident and fix in CHANGELOG.md under Fixed.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
app/nginx.conf Turns off absolute redirects so the trailing-slash 301 produces a relative Location instead of http://$host:8080/... behind Cloud Run.
.github/workflows/bot-serving-check.yml Extends the redirect-leak guard to also fail on :8080 in the redirect target.
CHANGELOG.md Adds a Fixed entry describing the internal-port redirect leak and the guard update.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 108 to 112
slash_target=$(curl -sS --max-time 30 -o /dev/null -A "$GOOGLEBOT" \
-w '%{redirect_url}' "$ORIGIN/scatter-basic/")
case "$slash_target" in
*"/seo-proxy"*|http://*)
*"/seo-proxy"*|http://*|*:8080/*)
echo "::error::trailing-slash redirect leaks or downgrades: $slash_target"
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

`gcloud builds list --project=anyplot` returns three builds from early
2026 and nothing since, because the deploy triggers run in europe-west4
and regional builds are absent from the global list. Read at face value
that says nothing has deployed for months, which is what happened in this
session: I reported the SEO work as merged-but-not-live, and the owner had
to point at a successful build to correct me. Every merge had in fact
deployed within about five minutes.

The guide now carries the `--region` form of both commands, notes that
notify-deployment.yml only records a GitHub deployment rather than
performing one, and says plainly that the trustworthy check is probing
production — the only method that also catches a build succeeding while
shipping the wrong thing, which is precisely what this PR fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 18, 2026 19:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

agentic/docs/project-guide.md:906

  • The gcloud builds list example uses createTime.date('%H:%M'), which is not a valid gcloud --format field transform (the date() transform doesn’t accept strftime-style format strings). As written, the command is likely to fail, which undermines the goal of making deploy checks easy.
gcloud builds list --region=europe-west4 --project=anyplot --limit=10 \
  --format="table(id.slice(0,8),status,createTime.date('%H:%M'),substitutions.TRIGGER_NAME,substitutions.SHORT_SHA)"
gcloud builds describe <build-id> --region=europe-west4 --project=anyplot

@MarkusNeusinger
MarkusNeusinger merged commit 1100140 into main Aug 18, 2026
10 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the fix/seo-relative-slash-redirect branch August 18, 2026 19:43
MarkusNeusinger added a commit that referenced this pull request Aug 18, 2026
## The finding

```
$ gh run list --workflow=bot-serving-check.yml --limit 10
2026-08-18  failure (schedule)
2026-08-17  failure (schedule)
2026-08-16  failure (schedule)
...
2026-08-09  failure (schedule)
```

Ten consecutive scheduled runs, all red, on **one line**:

```
expects: <title>anyplot.ai</title>
actual:  <title>anyplot.ai — AI-generated plot catalog for 15 libraries</title>
```

Nothing else was broken. `grep -qF` on the full title, copy changed,
check died.

## Why it matters more than a stale string

This run of SEO work edited this workflow **twice** and cited it
repeatedly as the regression cover for precisely the class of change
that has no local verification loop — nginx behaviour, bot serving. It
was failing daily throughout.

The trailing-slash port leak that shipped today is the proof: the guard
existed, it would have caught it, and it was already red so nobody would
have seen it. A monitor nobody reads protects nothing.

Found by an independent audit, not by me — I had asserted the guard's
value in three PR bodies without once checking whether it was green.

## Changes

**Home check matches on the prefix.** `<title>anyplot.ai` still
separates the prerendered page from the SPA shell, whose title is
`any.plot() — any library.` — that separation *is* the property under
test. The full title was never the point and made the check break on
copy edits.

**Empty redirect is now a failure.** `%{redirect_url}` is empty when
there is no redirect, which fell through to the success branch and
printed `OK: trailing slash -> `. So the rewrite disappearing entirely
would have read as a pass. Copilot raised this on #10473 and again on
#10476; neither addressed it.

## After merge

The workflow needs a manual `workflow_dispatch` to confirm the first
green run in ten days — the schedule alone would leave it unverified
until tomorrow. Note it will only pass once #10476 has deployed, since
the trailing-slash assertion is currently correct to fail.

## Not fixed here

Nothing alerted on ten days of failure. The workflow reports into GitHub
Actions and that is the whole notification path. Worth a decision
separately — a monitor that fails silently is only marginally better
than none.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request Aug 18, 2026
Merging main brought the five entries that landed after 3.1.0 was cut
(#10476, #10477, #10479, #10482, #10483) into the version section, where
they belong -- the image work and the sitemap fix are part of this
release. Dates the release to the day it is published.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants