
On September 7, 2026 a one-line commit to hsvagi.com triggered a Vercel build from git that removed trailingSlash: true from production. Six days later my own crawler found 164 canonical mismatches and 121 sitemap entries answering with a redirect.
The setting had been live since July 19, but only in deployments made from my laptop with uncommitted changes. Git never had it. This post covers the slash itself, how I proved what reverted it, and the check that stops it happening twice.
What did the crawler find on September 13?
The crawler is a 157-line Python script in the repo, scripts/audit-seo.py, using only the standard library. It reads the sitemap, follows internal links, and does not follow redirects. For every page that returns 200, it compares the canonical tag with the URL the page was actually served at.
I ran it against production at 22:17 UTC on September 13, then again at 01:18 UTC on September 14, after the fix went out. Both reports are saved in docs/seo/.
| Measure | Sep 13 (before) | Sep 14 (after) |
|---|---|---|
| Sitemap entries | 122 | 132 |
| URLs crawled | 316 | 329 |
| Canonical mismatches | 164 | 0 |
| Sitemap entries not returning 200 | 121 | 0 |
| Served pages missing from the sitemap | 164 | 0 |
| Broken URLs (404) | 17 | 0 |
| Broken image assets | 3 | 0 |
| Total problems | 305 | 0 |
I sorted all 164 mismatches. Every one differed from the served URL by a trailing slash and nothing else. On 163 pages the canonical said /about/ and the page was served at /about. The 164th was the homepage, where the canonical omitted the slash after the domain. Those two homepage forms are the same URL, so that one is my crawler being strict. The real count is 163.
The damage was sitewide, not one template. The guides section had 36 affected pages, the launch pages 35, services 25 and industries 16. A third-party audit export from the same afternoon agreed: 163 pages with a canonical pointing at a redirect, and 120 redirecting sitemap URLs. My 121 counts one guide URL that the sitemap listed twice.
Why does one slash send Google two opposite signals?
Because Next.js and the code disagreed about which URL was real. The Next.js trailingSlash docs say: “By default Next.js will redirect URLs with trailing slashes to their counterpart without a trailing slash. For example /about/ will redirect to /about.”
Since the first build in December 2025, every canonical, internal link and sitemap entry on hsvagi.com has used the slashed form. Without the setting, the server sent each of those slashed URLs to the unslashed one with a 308.
Google’s canonicalization guide calls a redirect “a strong signal that the target of the redirect should become canonical.” It calls rel="canonical" “a strong signal that the specified URL should become canonical.” It also says: “Don’t specify different URLs as canonical for the same page using different canonicalization techniques.”
So on 163 pages, two strong signals pointed in opposite directions. The sitemap, which Google calls “a weak signal,” sided with the canonical tag. Google’s sitemap guide says to “include the URLs in your sitemap that you want to see in Google’s search results.” Every URL I listed answered with a redirect.
Google’s guide does not say which signal wins in that conflict. That choice is Google’s. What you control is making sure the conflict never exists: the served URL, the canonical, the sitemap entry and every internal link must match character for character, slash included.
How did a fix that was already live disappear?
The fix only ever existed in my working tree. I pulled the production deployment history for the project from the Vercel API and lined it up against git.
| Production deploy (UTC) | Source | Commit label | Uncommitted changes | trailingSlash |
|---|---|---|---|---|
| Apr 7, 2026 | git | b3cdbf2 | No | Off |
| Jul 19 and Jul 21 (three deploys) | CLI | b3cdbf2 | Yes | On |
| Sep 7, 22:11 | git | e1dd16a | No | Off |
| Sep 13, 22:34 | CLI | e1dd16a | Yes | On |
| Oct 10, 20:11 | git | 73754b3 | No | On |
In July I deployed to production from the command line three times. Those deployments carried the slash setting, retired-URL redirects and robots changes that were never committed. The last July deploy differed from its labelled commit in 11 source files and carried 3 more files that git did not have.
On September 7 I fixed a different bug, the root-layout canonical. That commit changed one line in one file, and I pushed it. Vercel’s git docs say “A Production deployment will be created each time you merge to the production branch.” That build came from the repository, and the repository had never contained trailingSlash: true.
Vercel’s deployment docs say the CLI method “works whether your project is connected to Git or not.” Read together, the two pages describe a trap neither one states. A CLI deploy from a dirty tree creates a production site that exists nowhere in git, and the next push to main replaces it without warning.
The crawl confirms which build it measured. The image URLs in the September 13 report carry the deployment ID of the September 7 git build. The slash setting was live for 49 days, gone for 6, then live again but outside git for another 26, until I committed the exact deployed tree on October 10.
Doesn’t the Vercel dashboard show which commit is live?
It shows a commit, and for a CLI deploy that commit can be wrong about the code. This is the assumption that let the drift survive two months. The September 13 deployment was labelled e1dd16a, the same as the git build it replaced.
I checked the label against the files. Vercel’s API lists each uploaded file with a SHA-1 of its contents, so I hashed the same paths at each commit and compared. The September 13 deployment differed from e1dd16a in 21 source files and carried 6 files that commit did not contain. Against 73754b3, the commit I made on October 10, it matched in every file except two that the build generates.
The only marker is in the deployment metadata: gitDirty: "1". In the last eight production deploys, every CLI deploy has it and no git deploy does. Here is the read-only check I now run:
$ vercel api "/v6/deployments?projectId=<project-id>&target=production&limit=10"
# For each deployment, read:
# source "git" or "cli"
# meta.gitDirty "1" means uncommitted files went to production
If the newest production deployment says cli and gitDirty, your repo is behind your live site. The next push will roll production back to whatever git has.
What does the site serve today?
On October 10 I fetched every URL in the live sitemap. All 132 return 200, and all 132 carry a canonical that matches their own address exactly. None of the 132 lacks the trailing slash, and none is listed twice.
The 17 broken URLs from September now redirect: 11 retired service-and-city pages go to their service page, two old industry slugs go to the current ones, and four legacy service slugs keep their city. Next.js sends these as 308s. Google’s redirect guide says “The 301 and 308 status codes mean that a page has permanently moved.”
$ curl -sIL https://www.hsvagi.com/services/lead-intake-systems/huntsville-al | grep -iE "^HTTP|^location"
HTTP/2 308
location: /services/lead-intake-systems/huntsville-al/
HTTP/2 308
location: /services/ai-lead-intake-systems/huntsville-al/
HTTP/2 200
That is two hops: the slash first, then the move. Old /blog/ URLs took three, because the redirect’s destination, /ai-guides/:slug, had no trailing slash, so Next.js added a third hop. Google’s HTTP status code guide says its crawlers “follow up to 10 redirect hops.” Three is inside that. It was still one hop more than it needed. On October 10 I added the slash to that destination, and an old /blog/ post URL with its slash now lands in one hop.
Are the private pages kept out of the index?
Half of it works. The same fix made /portal/, /admin/, /dashboard/ and /account/ send noindex, nofollow, and I confirmed all four live on October 10. It also added all four to robots.txt as disallowed.
Those two measures cancel out for Google. Its noindex guide says the page “must not be blocked by a robots.txt file,” because otherwise “the crawler will never see the noindex rule” and “the page can still appear in search results.” The stakes here are low: these are login screens. On October 10 I dropped the four disallow lines, so the noindex can do its job. /api/ stays disallowed.
What did the six days cost in search?
I don’t know yet. I have no Search Console data for this period in the files I worked from, and I am not going to estimate it. I also have no crawl from the July to September window, so the 49 clean days are what the deployed config implies, not something I measured.
How do I stop this on my own Next.js site?
- Run
curl -sIon one sitemap URL. Anything other than 200 means your sitemap suggests URLs you do not serve. - View the source of that page. The canonical must equal the address bar after redirects, slash included.
- If your canonicals end in a slash, set
trailingSlash: trueinnext.config. If they don’t, remove the slash from the canonicals. Pick one convention and write it down. - Write every redirect destination in that same convention, so a move costs one hop and not two.
- Run
git statusbefore any CLI production deploy. If it is not clean, commit first. - If you hot-fix from the CLI anyway, commit that exact tree the same day, before anyone pushes anything else.
- After each deploy, check the newest production deployment for
source: "cli"withgitDirty: "1". - Rerun your crawler after every production deploy and compare the problem counts to the last run.
- Never block in
robots.txta page whose job is to saynoindex.
The same signal conflict shows up in two other rebuilds: fixing canonical signals after a www flip, where 53 pages carried two canonical tags, and fixing canonicals that point at redirects on an expired domain rebuild.
Sources
- Google Search Central, How to specify a canonical URL (page updated 2026-07-10).
- Google Search Central, Build and submit a sitemap (page updated 2026-07-08).
- Google Search Central, Redirects and Google Search (page updated 2026-04-14).
- Google Search Central, HTTP status codes, network and DNS errors (page updated 2026-02-04).
- Google Search Central, Block search indexing with noindex (page updated 2025-12-10).
- Next.js, trailingSlash (v16.4.0, page updated 2025-06-16).
- Vercel, Deploying Git Repositories with Vercel (page updated 2026-09-18).
- Vercel, Deploying to Vercel (page updated 2026-09-03).
- The hsvagi.com repository, its crawl reports in
docs/seo/, its Vercel production deployment history, and the live site, checked October 10, 2026.
