fix(seo): AI-crawler allow-list, cf-visitor https redirect, HSTS preload, llms-full.txt - #234
Open
BSalaeddin wants to merge 1 commit into
Open
BSalaeddin wants to merge 1 commit into
BSalaeddin wants to merge 1 commit into
Conversation
…oad, llms-full.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
grabcaramel.com is the fleet's second-best organic asset (920 clicks / 16,409 impressions in 90 days). The 2026-09-11/12 SEO audit (
C:\Users\alaed\Documents\Github\caramel-artifact\seo-2026-09-11\—route-inventory.md§10 "Gaps vs the fleet standard",audit-findings.md"Host hygiene",ci-inventory.md§4) measured these host-hygiene gaps; this PR closes the app-side ones and pins each in a test.http://grabcaramel.com/serves 200 with the full page (Cloudflare Always-Use-HTTPS off; GSC already indexes the http twin with clicks)src/middleware.ts: http→https 308 keyed ONLY on Cloudflare'scf-visitor: {"scheme":"http"}. Never onx-forwarded-proto(Next synthesises it, Traefik rewrites it — a fallback loops). No header = serve. Folded into the existing www→apex 308 so ahttp://www.request redirects once to the https apex.max-age=31536000; includeSubDomains(nopreload)next.config.mjsSECURITY_HEADERS →…; includeSubDomains; preload. dev.grabcaramel.com is already https-only, so preload is safe.*group, no AI-crawler allow-listsrc/lib/seo/aiCrawlers.ts(18-agent readonly const, fleet list) + a second prod rule insrc/app/robots.ts{ userAgent: [...AI_CRAWLERS], allow: '/', disallow: DISALLOWED_PATHS }(same disallow set). Non-prod branch unchanged (blanket disallow, one rule)./llms-full.txt404; nothing links/llms.txtsrc/app/llms-full.txt/route.ts(what/how/browsers/FAQ/key pages/install/privacy summary →/privacy). FAQ strings come from the SAME array as the landing accordion + FAQPage JSON-LD, moved verbatim tosrc/lib/faqItems.ts(wording untouched).llms.txtgains a pointer to the full doc. Discoverability:<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt">viaalternates.types(root layout + home page — Next replacesalternateswholesale per level, verified inresolve-metadata.js, so home's canonical block re-states it through one shared constantsrc/lib/seo/llmsTxtAlternate.ts) + a tiny footer<a href="/llms.txt">under Legal.@id/alternateName/sameAsbut nothing pins themparentOrganization: { Organization, "Devino Solutions", https://devino.ca }added (devino.ca → 200 via curl 2026-09-12); shape pinned in e2e. No rating/review markup added./login,/signup,/verifyrely on robots.txt Disallow onlyrobots: { index: false, follow: false }metadata on the three pages./forgot-password,/reset-passworduntouched.Not touched (owned by
seo/index-policy/seo/claim-integrity):sitemap.ts,couponsRepo.ts, coupon pages, marketing copy. Nothing needed from them.Tests
tests/unit/robots-env-contract.test.ts— rewritten: non-prod = exactly ONE rule; prod = exactly TWO rules; the AI group'suserAgentequals a literal 18-agent list (spelled out, not imported, so a dropped/misspelled agent goes red) with the same disallow set;AI_CRAWLERSitself is compared to that literal.tests/unit/middleware.test.ts— new (NextRequest): http cf-visitor → 308 https same path+query; https cf-visitor → served; no header → served; www → 308 apex; www+http → one 308 to https apex;x-forwarded-proto: httpalone → NO redirect; garbage cf-visitor → served; matcher still excludes_next/static,_next/image,favicon.ico.tests/unit/llms-txt-routes.test.ts— new: llms-full.txt carries every FAQ question+answer verbatim, points at /privacy, no review markup; llms.txt links llms-full.txt.e2e/seo-regression.spec.ts— extended (same runner, all deployment-safe, no DATABASE_URL assumptions): (a)/llms-full.txt200 text/plain + 'Caramel' + a FAQ question; (b) home raw HTML has therel=alternate type=text/plainlink + the crawlable footer<a href="/llms.txt">; (c) Organization JSON-LD@idends#organization,alternateNameincludes 'Caramel coupon extension', non-empty httpssameAs,parentOrganization.url === 'https://devino.ca'; (d)strict-transport-securityon/containspreload(set by next.config, so it holds in both contexts); (e)/login,/signup,/verifyraw HTML carriesnoindex+nofollowmeta. The prod robots allow-list is pinned at unit level because e2e only ever sees the non-prod branch.Verification (worktree
C:\wt-caramel-hygiene, freshpnpm install --frozen-lockfile+prisma generate)Not run locally: Playwright e2e and Lighthouse (no local server stood up; the e2e additions are type-checked only via
tsc --noEmit, which includese2e/). CI'se2e-pr(hermetic) runs them on this PR;e2e-pushre-runs them against the deployed site after merge (it waits on/api/versionfirst, so the HSTS/redirect assertions see the new build).What CI covers
checks-app.yml: lint/oxlint/prettier/knip/type-check/unit (the gates above) +e2e-prruns the extendedseo-regression.spec.tsagainst the hermetic build. The footer gains one small link, so the visual-regression job may flag the home/footer snapshot — expected, approve the new baseline.Owner items (not code)
grabcaramel.comat https://hstspreload.org/ once this is live (header now qualifies: 1y + includeSubDomains + preload). Irreversible-ish — only do it if every current/future subdomain will stay https-only.curl -sI http://grabcaramel.com/should show308+location: https://grabcaramel.com/;curl -s https://grabcaramel.com/robots.txtshould show the secondUser-Agent: GPTBot …group.