Skip to content

feat(redirects): honor the query a redirect source was scoped to - #520

Draft
igoramf wants to merge 2 commits into
fix/redirect-csv-query-self-loopfrom
feat/redirect-query-scoped
Draft

feat(redirects): honor the query a redirect source was scoped to#520
igoramf wants to merge 2 commits into
fix/redirect-csv-query-self-loopfrom
feat/redirect-query-scoped

Conversation

@igoramf

@igoramf igoramf commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Empilhado no #519. O base branch é fix/redirect-csv-query-self-loop, então o diff aqui mostra só o incremento. Mergear #519 primeiro.

O problema que sobra depois do #519

O #519 mata o loop descartando a regra. Isso está certo para um fix de incidente, mas cobra um preço: a linha descartada era um redirect legítimo.

normalizePath reduz a origem ao pathname, então uma regra escrita contra uma query legada não tem como dizer "só para essa query":

https://www.montecarlo.com.br/relogios?map=category-1,/relogios/todos,PERMANENT

Hoje ela vira uma regra para /relogios inteiro — redirecionando todo visitante da categoria, inclusive quem nunca teve a query. Quando o destino coincide com o próprio path, isso vira o loop do #519; quando não coincide, o estrago é mais silencioso: a página existe, mas ninguém consegue chegar nela.

O loader Fresh que este SDK substituiu casava a href inteira (isHref: true), então essas regras funcionavam. Todo site que migra de Fresh para TanStack as perde.

O que muda

  • Redirect.search carrega a query da origem. Essas regras vivem em RedirectMap.scoped, indexadas por pathname, inalcançáveis a partir de um path pelado.
  • matchRedirect(pathname, map, search?) — terceiro argumento opcional. Quem não passa mantém o comportamento de hoje, então é aditivo: nenhum call site existente quebra.
  • Regra com query ganha da regra de path puro no mesmo pathname — é o match mais específico.
  • Match por subconjunto, não igualdade: utm_source, gclid, fbclid anexados à request não derrubam a regra.
  • workerEntry.ts passa url.search.

Comportamento

Com a regra /relogios?map=category-1 → /relogios/todos:

Request Resultado
/relogios?map=category-1 301 → /relogios/todos
/relogios?map=category-1&utm_source=news 301 → /relogios/todos
/relogios passa direto
/relogios?map=outra-coisa passa direto

E a linha do incidente volta a fazer sentido em vez de ser descartada:

Request #519 sozinho com este PR
/aliancas passa direto passa direto
/aliancas?map=category-1 passa direto (regra perdida) 301 → /aliancas

Testes

packages/blocks/src/sdk/redirectsQueryScoped.test.ts, 9 casos: query exata, prefixo ?, params extras tolerados, regra multi-param exigindo todos, precedência sobre path puro, várias regras no mesmo pathname, blocos de CMS além de CSV, e regras sem query intocadas.

Suíte do pacote: 756 passed (57 arquivos). Typecheck do monorepo: os 6 erros que aparecem são pré-existentes em blocks-cli/scripts/migrate — mesmo número no main, confirmado com git stash.

Ponto de review

A mudança de API é matchRedirect ganhando um parâmetro e RedirectMap ganhando um campo. Ambos aditivos, mas RedirectMap é um tipo exportado — quem construir um à mão (não via loadRedirects) precisa do campo scoped. Se preferir, dá para deixá-lo opcional e tratar undefined; achei mais honesto exigir, já que loadRedirects sempre preenche.

🤖 Generated with Claude Code


Summary by cubic

Restores the query-scoped redirect rules that the #519 fix was discarding. A source like /relogios?map=category-1 previously lost its query, widening into a rule for the whole path — looping against its own target or silently hijacking the page — and now carries it and matches it at request time.

  • Redirect.search holds the source query; these rules live in RedirectMap.scoped, keyed by pathname and unreachable from a bare path.
  • matchRedirect(pathname, map, search?) gains an optional third argument; existing callers keep today's behavior.
  • workerEntry passes url.search so the rules fire in the worker.
  • Matching is a subset of the request params, so utm_* or gclid appended to the request don't defeat a rule.
  • A scoped rule wins over a bare-path rule on the same pathname.

Migration

  • RedirectMap.scoped is a new required field; maps built by hand, not via loadRedirects, need to add it.

Written for commit 3c86eeb. Summary will update on new commits.

Review in cubic

igoramf and others added 2 commits September 1, 2026 07:55
… loop

A rule whose source and target resolve to the same page answers every request
with a redirect back to the URL just asked for: ERR_TOO_MANY_REDIRECTS.

Bulk migration exports carry these rows, and `normalizePath` manufactures more:
it reduces an absolute `from` to `new URL(p).pathname`, which drops the query.
A row scoped to one legacy query

    https://www.example.com/aliancas?map=category-1  ->  /aliancas

therefore becomes `/aliancas -> /aliancas` and takes the page down. One
production storefront hit this twice from the same CSV — once on `/`, via a
`http://blog.example.com/,/` row, and once on a category page.

The Fresh loader this SDK replaced (deco-cx/apps
`website/loaders/redirectsFromCsv.ts`) had a `from === to` guard for exactly
this. Restore it, comparing after normalization so the collapsed rows above are
caught too.

Host is compared whenever both sides carry one, so a genuine cross-origin
redirect to the same path (`/x -> https://other.example.com/x`) still fires.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stacked on #519, which stops query-scoped rows from looping by discarding them.
This restores what they are supposed to do.

`normalizePath` reduces a source to its pathname, so a rule written against one
legacy query — `https://site/relogios?map=category-1 -> /relogios/todos` — had
no way to express "only for that query". #519 drops such a row when it collapses
onto its own target; every other one silently widens into a rule for the whole
page, redirecting visitors who never had the query.

The Fresh loader this SDK replaced (deco-cx/apps
`website/loaders/redirectsFromCsv.ts`) matched the entire href, so these rules
worked. Sites migrating off it lose them today.

- `Redirect.search` carries the query the source was scoped to. Those rules live
  in `RedirectMap.scoped`, keyed by pathname, unreachable from a bare path.
- `matchRedirect(pathname, map, search?)` — new optional third argument.
  Existing callers keep today's behaviour, so this is additive.
- A scoped rule wins over a bare-path rule on the same pathname: it is the more
  specific match.
- Matching is a subset of params, not equality, so utm_*/gclid appended to the
  request do not defeat the rule.
- `workerEntry` passes `url.search`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant