Fix uppercase content filenames and enforce lowercase via Tina schema#4641
Fix uppercase content filenames and enforce lowercase via Tina schema#4641isaaclombardssw wants to merge 4 commits intomainfrom
Conversation
Issue #4482: pages with uppercase characters in the URL path were returning a 200 not-found fallback at the Next.js layer. Cloudflare was masking this in production by force-lowercasing all paths, which in turn was breaking mixed-case Tina filenames such as /consulting/TenderPortals. Middleware now lowercases the FIRST path segment only when it contains uppercase characters (308 redirect, query string preserved), leaving deeper segments untouched so mixed-case Tina routes keep working. Static asset paths, /_next/, and /api/ are skipped. Adds Playwright regression coverage in ui-tests/uppercase-redirect.spec.ts. Note: the Cloudflare lowercase transformation rule must also be removed or narrowed for this fix to fully take effect in production. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Coverage report
Test suite run success13 tests passing in 1 suite. Report generated by 🧪jest coverage report action from 9b8d8e4 |
There was a problem hiding this comment.
Pull request overview
Adds a Next.js middleware redirect to normalize uppercase characters in only the first URL path segment, fixing origin behavior where /Consulting, /Company, etc. fall into a 200 “not found shell” instead of routing correctly, while preserving mixed-case deep segments needed for Tina/consultingv2 filenames (e.g. /consulting/TenderPortals).
Changes:
- Implement first-segment-only lowercase normalization in
middleware.tswith a 308 redirect, skipping/_next/,/api/, and likely-static-file paths. - Add Playwright coverage for uppercase redirects and mixed-case deep-segment preservation in
ui-tests/uppercase-redirect.spec.ts.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| middleware.ts | Adds first-segment lowercase redirect logic via middleware. |
| ui-tests/uppercase-redirect.spec.ts | Introduces Playwright regression tests for redirect + deep-segment preservation behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return NextResponse.redirect(url, 308); | ||
| } | ||
| } | ||
|
|
||
| const response = NextResponse.next(); | ||
|
|
There was a problem hiding this comment.
The early return on redirect skips the existing header logic (HSTS and X-Robots-Tag / noindex via addNoIndexHeaders). If you rely on these headers being present on all responses (including redirects), build the redirect response first and then apply the same header mutations before returning it (or refactor header-setting into a shared helper used by both redirect and NextResponse.next()).
| return NextResponse.redirect(url, 308); | |
| } | |
| } | |
| const response = NextResponse.next(); | |
| const response = NextResponse.redirect(url, 308); | |
| return applyResponseHeaders(request, response); | |
| } | |
| } | |
| return applyResponseHeaders(request, NextResponse.next()); | |
| } | |
| function applyResponseHeaders( | |
| request: NextRequest, | |
| response: NextResponse, | |
| ): NextResponse { |
There was a problem hiding this comment.
Good catch — applied. Refactored header-setting into a shared applySharedHeaders helper and it now runs on both the 308 redirect and the pass-through NextResponse.next(). The noindex header in particular matters here, because a crawler that indexes the 308 itself (without following) on a PR/staging host would otherwise let the preview leak into search engines. Verified locally — the 308 response now carries strict-transport-security and x-robots-tag. Fixed in 9b8d8e4.
| import { expect, test } from "@playwright/test"; | ||
|
|
||
| test("🔠 Uppercase top-level path is redirected (308) to lowercase", async ({ | ||
| request, | ||
| baseURL, | ||
| }) => { | ||
| const response = await request.get(`${baseURL}/Consulting`, { | ||
| maxRedirects: 0, | ||
| failOnStatusCode: false, | ||
| }); | ||
| expect(response.status()).toBe(308); | ||
| expect(response.headers()["location"]).toMatch(/\/consulting$/); | ||
| }); |
There was a problem hiding this comment.
This new spec won’t run in the current CI workflows because the Playwright job filters tests via tests_to_run (e.g. images seo-noindex), which won’t match uppercase-redirect. To make this regression coverage effective, update the workflow inputs to include this spec (or adjust the selection strategy) so it runs on PR/staging deployments.
There was a problem hiding this comment.
Verified and fixed — template-ui-tests.yml runs npx playwright test ${{ inputs.tests_to_run }} which uses the input as a positional test-file filter, so uppercase-redirect wouldn't match. Added it to the tests_to_run inputs in both pr-push-deploy.yml and main-build-and-deploy.yml. Fixed in 9b8d8e4.
|
Deployed changes to https://app-sswwebsite-9eb3-pr-4641.azurewebsites.net |
…n CI Apply HSTS, X-Robots-Tag, and noindex headers to the 308 redirect response (not just the pass-through), so PR/staging previews don't leak indexable redirects to crawlers that don't follow them. Include uppercase-redirect in the tests_to_run filter for both the PR deploy and main build workflows, since the workflow passes positional args to playwright test and would otherwise skip the new spec. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Deployed changes to https://app-sswwebsite-9eb3-pr-4641.azurewebsites.net |
🚀 Lighthouse score comparison for PR slot and production
|
…a Tina schema Instead of redirecting uppercase URLs in middleware, fix the root cause: - Rename 971 content files with uppercase characters to lowercase - Update all cross-references in content files to match new filenames - Add kebabCaseFilename to all remaining Tina collections (technologies, testimonialCategories, presenter, events-calendar, opportunities, newsletters, payment-details) - Add Jest test to prevent future uppercase content filenames - Revert middleware redirect logic and workflow changes from prior approach This allows the Cloudflare lowercase transform rule to work correctly without breaking any content routes. Closes #4482 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ement The existing parse function alone was not preventing uppercase filenames in the Tina UI. Adding slugify auto-generates lowercase kebab-case filenames from the title/name field on document creation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fixes #4482 — Uppercase URL paths fail to load due to Cloudflare's lowercase transform rule conflicting with mixed-case content filenames.
Root cause: Cloudflare has a transform rule that lowercases all URI paths. Some Tina CMS content files had uppercase characters in their filenames, causing a mismatch when Cloudflare's lowercase URL didn't match the original case on disk.
Fix approach (pivoted from middleware redirect):
kebabCaseFilenameto all remaining Tina collections that were missing it (technologies, testimonialCategories, presenter, events-calendar, opportunities, newsletters, payment-details) — prevents future uppercase filenames at creation timeNote: Tina collection directory names with camelCase (e.g.
testimonialCategories,v2Technologies) were intentionally left as-is — they are Tina collection identifiers tied to the schemanameandpathfields, and renaming them would require updating GraphQL queries and TinaCloud's API.Action item: The Cloudflare "lowercase all paths" transform rule can now be safely left in place, or optionally removed since all content is now lowercase.
Test plan
pnpm lintpasses__tests__/content-filenames.test.tspasses (no uppercase content filenames)🤖 Generated with Claude Code