Skip to content

Seo/sitemap filter orphan entries - #1662

Closed
pedrobernardina wants to merge 50 commits into
deco-cx:mainfrom
oficina-dev:seo/sitemap-filter-orphan-entries
Closed

Seo/sitemap filter orphan entries#1662
pedrobernardina wants to merge 50 commits into
deco-cx:mainfrom
oficina-dev:seo/sitemap-filter-orphan-entries

Conversation

@pedrobernardina

@pedrobernardina pedrobernardina commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What is this Contribution About?

Please provide a brief description of the changes or enhancements you are proposing in this pull request.

Issue Link

Please link to the relevant issue that this pull request addresses:

Loom Video

Record a quick screencast describing your changes to help the team understand and review your contribution. This will greatly assist in the review process.

Demonstration Link

Provide a link to a branch or environment where this pull request can be tested and seen in action.


Summary by cubic

Overhauls VTEX authentication and session flows so impersonated profiles resolve correctly, and stops the proxied sitemap from announcing URLs that answer 404. Also adds a structured ?renderJson page mode and explicit control over SEO structured data.

Session and authentication

  • Auth actions now forward the full request cookie, including vtex_session, so profile, address, and payment calls resolve session-scoped profiles.
  • Password reset uses the startlogin flow with expireSessions and returns structured authStatus instead of a generic failure.
  • Refresh token cookie path is now /api/vtexid/refreshtoken/webstore; adds a validateSession action and a Logout section with a safe returnUrl.

Sitemap and SEO

  • Sitemap entries no route answers are removed (they would 404) and reappear once a page exists; rewritten responses no longer carry stale content-length, content-encoding, or etag headers.
  • structuredDataControl replaces the deprecated ignoreStructuredData; bots no longer receive JSON-LD when it is disabled for users.
  • ?renderJson renders the page as structured JSON; @deco/deco is pinned to the oficina-dev/deco-runtime fork to support it.

Written for commit 4fcb56e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added configurable structured JSON rendering for page sections.
    • Added enhanced sitemap controls, including custom routes, exclusions, and route validation.
    • Added product SEO image optimization and structured-data controls.
    • Added full product review retrieval with resilient fallback behavior.
    • Added session validation, improved logout, and expanded authentication flows.
    • Added optional Google Tag Manager URL parameters.
  • Bug Fixes
    • Improved cookie forwarding and session handling across account, cart, and checkout operations.
    • Improved sitemap response handling and invalid-entry filtering.

marcoferreiradev and others added 30 commits May 12, 2026 15:55
…shtoken/webstore” in refreshToken and setLoginCookies.
…ate cookie management for sign-in and logout
- Replace boolean `ignoreStructuredData` with explicit `structuredDataControl` options
- Fix bug where bots still received JSON-LD even when `ignoreStructuredData=true`
- Add "always include" (default), "disable for users", and "disable for all" modes
- Deprecate `ignoreStructuredData` with backward compatibility
- Extract shared logic to `shouldIncludeStructuredData` utility
* feat(proxy): add prop to exclude entries from final sitemap

* feat(vtex-proxy): make possible to add custom handlers to proxy

---------

Co-authored-by: Pedro Bernardina <pedro.guidance@gmail.com>
Co-authored-by: Pedro Bernardina <pedro.guidance@gmail.com>
classic/setpassword now requires the session minted by POST /authentication/startlogin (sent via the _vss cookie); the older GET /authentication/start token passed in the request body is rejected with an empty-body HTTP 500 on VTEX ID Authenticator-migrated accounts, breaking password change. Switch resetPassword to the startlogin flow used by vtex.react-vtexid and FastStore useSetPassword (startlogin -> _vss cookie -> setpassword?expireSessions=true, no authenticationToken in the body). Add startlogin + expireSessions to the VTEXCommerceStable client type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VTEX returns auth rejections (WrongCredentials, BlockedUser) from setpassword as a non-2xx whose body is a valid AuthResponse. fetchSafe wrapped that in an HttpError, so resetPassword/recoveryPassword bubbled it up and the client choked on a JSON.parse SyntaxError — showing a broken/generic screen instead of the real message ("Senha atual incorreta").

Catch the HttpError, recover the AuthResponse via authResponseFromHttpError, and return it so the client maps it to a message. Unexpected failures are logged and rethrown as a typed HttpError. Guard validateSession so a post-success session failure isn't mistaken for a password-change failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fresh.ts gains a structured-JSON exit alongside the existing ?asJson one:
?renderJson (legacy alias ?appJson — remove once consumers switch) resolves
the page with hooks that short-circuit sections opted out of JSON rendering
(their loaders never run), serializes the tree via @deco/deco
serializeResolvedSection honoring each section's `renderJson` export, and
responds { name, path, sections } with lazy sections as
{ component, lazyUrl } placeholders. One-shot JSON responses never use
async render (firstByteThreshold guards extended); renderJson takes
precedence when both params are sent; legacy ?asJson is untouched.

The website app gains `renderJson.sectionsToIgnore` (admin-configurable):
app-owned sections excluded by resolveType suffix — site-owned sections
should prefer `export const renderJson = false` in their own file.

Replaces the site-level wrapper pattern (handler + pages-loader fork) that
oficina-reserva ran as v1 — the default website/loaders/pages.ts now works
unchanged for renderJson consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marcoferreiradev and others added 20 commits June 10, 2026 14:58
The fresh.ts renderJson branch imports symbols (sectionModuleLookup,
serializeResolvedSection, computeRenderCb) that only exist in our runtime
fork — upstream jsr:@deco/deco doesn't have them, so standalone deno check
broke. Raw URLs don't resolve subpaths via the package exports map, hence
one entry per subpath used, plus @deco/deno-ast-wasm which the runtime
graph self-references. Pre-existing vtex/mod.ts type-constraint errors
remain untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
renderJson is the replacement, not a sibling — consumers adopt ?renderJson
directly (validation happens on PR previews, nothing in production speaks
?appJson). Only the legacy ?asJson remains as a separate, untouched mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repin @deco/deco from d7f9cc2c (render-json tip) to c0228316, the
deco-runtime main-alt merge commit, so apps tracks the full main-alt
(render-json + daemon/cache/version fixes) instead of just render-json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings 31 upstream commits (no-store on personalizing loaders, no-cache on
extension wrappers, image quality context, userSegment matcher signature,
spire removal, version 0.158.0). Our @deco/deco fork pin (deco-runtime
@c0228316) and the render-json work are preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… it)

The render-json fork-pin (34931e7) replaced jsr:@deco/deco with 6 explicit
jsdelivr subpaths but omitted scripts/bundle, so scripts/start.ts's
import("@deco/deco/scripts/bundle") no longer resolved and CI (ci.yaml -> start.ts)
broke. Map it to jsr like the bundle task and the site already do: the bundle is
a build tool untouched by render-json, and jsr resolves its transitive deps
(codemod-toolkit, std) automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greens the apps CI after the render-json + upstream consolidation:
- website/handlers/fresh.ts (render-json edit) was committed without
  deno fmt -> failed the `deno fmt --check` in the Check step.
- the manifest generation (start.ts -> bundle) pulls @deco/deno-ast-wasm,
  which was missing from deno.lock -> would fail "Check if there are changes".

Validated end-to-end with deno 2.4.5 (CI's exact version): fmt --check, lint,
and check **/mod.ts all clean; Bundle regenerates manifests with no drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the renderJson review fixes (omit no-op, tightened RenderJson
return, dropped Murmurhash3 re-export, lazy resolveChain comment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sectionsToIgnore is admin config (external JSON), not type-guaranteed at
runtime. Keep only non-empty string suffixes: a blank entry would make
endsWith("") match every section (dropping the whole page), and a non-string
would throw on .trim() (500ing the request).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ad/writes

profile/address/payment/user store-graphql calls forwarded only the VtexId cookies (parseCookie), dropping vtex_session — so @withCurrentProfile could not resolve a session-scoped (e.g. telesales-impersonated) profile and writes misdirected. Forward the full request cookie, matching the cart actions.

updateProfile also injected payload.sub as the profile email; under an operator admin token that is the OPERATOR's email, which would re-key the impersonated customer's account. Derive the target email from the impersonated-customer session cookie when the token audience is admin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The block read `vtex-impersonated-customer-email` as a COOKIE and threw
"Could not resolve the target profile email" when it was absent. Nothing ever
writes that cookie — impersonation sets it as a session public property via
editSession — so the throw fired on every profile update under a telesales
session.

It was also unnecessary: store-graphql discards the field
(node/resolvers/profile/services.ts, `const { email, ...profileWithoutEmail }`,
since 2.173.2) and writes to the session-resolved currentProfile. The forwarded
cookie is what targets the write; this field is only present because
ProfileInput.email is String! in the schema.

So `email: payload.sub` is restored with a one-line why, the client-writable
cookie read is gone, and the block's security argument goes with it — there is
nothing left to defend.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up upstream deco-cx/deco v1.203.0, merged into our main-alt. Clean merge:
none of the 4 upstream commits touch our 7 customized runtime files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream deco-cx/apps v0.160.0 (292aed0), 50 non-merge commits.

Two conflicts, both from d2e9108 ("remove deprecated firstByteThresholdMS
config"), resolved by adopting the upstream removal:

  website/handlers/fresh.ts — kept our renderJson/isJsonOneShot lines, took
    upstream's `?? 0`, dropped `delayFromProps`.
  website/mod.ts — kept the renderJson block, dropped firstByteThresholdMS.

The removal is bit-identical for this site: .deco/blocks/site.json has
`"firstByteThresholdMS": false`, so `false ? 1 : 0` === `?? 0`.

Worth recording, because it is the trap here: the same commit also strips
`| "firstByteThresholdMS"` from the `Pick<AppContext>` in fresh.ts's signature,
and three-way merge resolves THAT silently, with no marker. Keeping the HEAD
side of the marked hunk — the instinct — leaves the body reading a field the
type no longer has, and only `deno check` catches it.

Verified after resolution: zero `firstByteThresholdMS` left in .ts/.tsx, and
deno fmt/lint/check all exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sitemap is proxied from VTEX, so it lists whatever the platform's
category tree holds. Categories are created upstream by a different team
than the one that builds the storefront pages, so an entry can point at a
path no page serves — which answers 404. Announcing such a URL to crawlers
is worse than omitting it.

Behind `removeEntriesWithoutPage`, every <url> is now matched against the
paths of the pages that actually exist, read with the same blockSelector
website/loaders/pages.ts uses. Dropped entries are logged so the missing
page can be created; the entry returns on its own once it is. The catch-all
page is deliberately left out of the matchers: it matches every path, and
counting it would make the filter inert.

Also matches the storefront host instead of the exact configured URL when
rewriting <loc>, so a variation the platform emits — http, or no trailing
slash — cannot leak the platform host into the index.
Three defects from review, none of which the earlier commit could have
shown working:

The flag never reached the handler. The proxy loader builds the sitemap
routes with a fixed set of props, and removeEntriesWithoutPage was not
among them, so the handler always saw undefined and the filter never ran.
It is now threaded through the loader to both sitemap routes.

The matcher only imitated the router. Hand-rolled as a regex, it diverged
on case, on trailing slashes and on percent-encoding: a page at /maçã does
answer /ma%C3%A7%C3%A3 but would have been dropped, while /foo/ and /FOO,
which answer nothing, would have been kept. It now builds the very same
URLPattern the router builds, which also fixes a false positive found
against production data — a page whose path carries a query string was
being dropped, because the regex escaped the template whole.

An empty page list disabled the filter, conflating a valid read with a
failed one, which the catch already covers. The filter now always runs;
what is refused instead is the narrower and unrecoverable case of dropping
every entry, since an empty sitemap withdraws the whole site from the
index and nothing downstream would notice.
Three defects from review.

The rewritten body was returned under the upstream's framing headers. It
is no longer the gzip stream content-encoding claims, nor the length
content-length states — a mismatch that predates the filter, since
rewriting the host already shortens every <loc>. Both headers are dropped
from a clone of the response's.

The guard against emptying a sitemap watched the symptom instead of the
cause. Each child sitemap is handled on its own, so one made entirely of
orphan entries is a legitimate emptying, and refusing it left those URLs
published. What is unrecoverable is having no page to match against at
all — that would empty every sitemap — so the guard now sits there.

A <loc> holds XML and arrives entity-encoded, so a URL written as
"?a=1&amp;b=2" was compared literally against a route the router sees as
"?a=1&b=2", dropping pages whose URL carries more than one parameter.
Entities are decoded, numeric references included, before matching.
The sitemap is no longer rewritten. Removing entries meant reserializing a
body the upstream's headers no longer described, and deciding on the
storefront's behalf which URLs a crawler may see; a warning gives whoever
can create the missing page the same information without either. The
response goes back out exactly as it came in, and the prop says so.

The check itself is now taken from the routes actually being served, read
from the request state the way website/handlers/sitemap.ts does, rather
than from the raw page blocks: a page hidden by hidePagesInDeco answers
only with ?rdc=true, and reading its block would have called it reachable.

Catch-all routes are recognized by probing what they answer instead of by
their spelling, since the router normalizes "/*", "/(.*)" and an absolute
"https://store.test/*" to the same pattern and any of them would have made
the check vacuous.

Routes with no pattern syntax — nearly all of them — are indexed in a set,
so an orphan entry no longer walks every route: 10k orphan URLs against
500 routes went from seconds to 92ms, and the protocol allows 50k URLs per
file. The log carries a count and a bounded sample instead of every URL,
which on a large sitemap was megabytes to the console and the logger both.

Finally, the host rewrite now requires the host to end where the match
does, with an optional port. Unanchored, "shop.example.com" rewrote
through the prefix of "shop.example.com.br" and mangled ":443" URLs.
Restores the removal. A URL the platform lists but no page serves answers
404, and announcing it to crawlers is worse than omitting it; it returns
on its own once the page exists, since nothing here is persisted.

Two defects from review, both of which removal turns from noise into
damage. A catch-all was recognized by probing an improbable path, which
"/:department/:category" also answers — every two-segment route would have
been dropped from the index and every two-segment page erased from the
sitemap. The router normalizes each spelling of a catch-all to the
pathname "/*", so that is what is compared now.

And the scan read every <loc> in the document, including the <sitemap>
entries of an index, which name documents rather than pages: an external
sitemap added through `include` would have been reported, and now removed.
Matching whole <url> blocks confines the check to the urlset, which the
removal needed anyway.
The response is decoded and rewritten before being served: the host is
swapped in every <loc>, entries are dropped, includes are added. What the
upstream sent about its own bytes stops holding — content-length is the
length of a different document, content-encoding announces a gzip stream
that was already decoded, and etag names the platform's version rather
than the one being served, so a client could revalidate its way into a
stale body.

This predates the entry removal: rewriting the host alone already shortens
every <loc>. Deleting the three from a clone lets the server frame the
response it is actually sending. VTEX currently sends no etag for the
sitemap, so that one is a precaution.
@github-actions

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.162.2 update
  • 🎉 for Minor 0.163.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 208e02d0-ed6f-43c2-8ff9-5222695ffc72

📥 Commits

Reviewing files that changed from the base of the PR and between dafcdcf and 4fcb56e.

⛔ Files ignored due to path filters (1)
  • deno.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • commerce/loaders/product/productListingPage.ts
  • commerce/loaders/product/products.ts
  • commerce/sections/Seo/SeoPDPV2.tsx
  • commerce/sections/Seo/SeoPLPV2.tsx
  • commerce/utils/structuredData.ts
  • deno.json
  • verified-reviews/loaders/fullReview.ts
  • verified-reviews/loaders/productDetailsPage.ts
  • verified-reviews/loaders/productList.ts
  • verified-reviews/loaders/productListingPage.ts
  • verified-reviews/manifest.gen.ts
  • vtex/actions/address/create.ts
  • vtex/actions/address/delete.ts
  • vtex/actions/address/update.ts
  • vtex/actions/authentication/accessKeySignIn.ts
  • vtex/actions/authentication/classicSignIn.ts
  • vtex/actions/authentication/logout.ts
  • vtex/actions/authentication/recoveryPassword.ts
  • vtex/actions/authentication/refreshToken.ts
  • vtex/actions/authentication/resetPassword.ts
  • vtex/actions/authentication/sendEmailVerification.ts
  • vtex/actions/authentication/startAuthentication.ts
  • vtex/actions/cart/updateUser.ts
  • vtex/actions/payment/deletePaymentToken.ts
  • vtex/actions/profile/updateProfile.ts
  • vtex/actions/session/createSession.ts
  • vtex/actions/session/editSession.ts
  • vtex/actions/session/validateSession.ts
  • vtex/handlers/sitemap.ts
  • vtex/loaders/address/getUserAddresses.ts
  • vtex/loaders/legacy/relatedProductsLoader.ts
  • vtex/loaders/payment/userPayments.ts
  • vtex/loaders/profile/getCurrentProfile.ts
  • vtex/loaders/proxy.ts
  • vtex/loaders/session/getSession.ts
  • vtex/loaders/user.ts
  • vtex/manifest.gen.ts
  • vtex/mod.ts
  • vtex/sections/Authentication/Logout.tsx
  • vtex/utils/authResponse.ts
  • vtex/utils/client.ts
  • vtex/utils/cookies.ts
  • vtex/utils/login/getLoginCookies.ts
  • vtex/utils/login/setLoginCookies.ts
  • vtex/utils/segment.ts
  • vtex/utils/session.ts
  • vtex/utils/types.ts
  • vtex/utils/vtexId.ts
  • website/components/Analytics.tsx
  • website/components/Image.tsx
  • website/components/Seo.tsx
  • website/handlers/fresh.ts
  • website/mod.ts

📝 Walkthrough

Walkthrough

This change adds structured-data controls, VTEX authentication and session handling, sitemap filtering, Fresh JSON rendering, analytics URL parameters, verified-review loading, and no-cache directives for selected product loaders.

Changes

Commerce SEO and loader behavior

Layer / File(s) Summary
Structured-data control
commerce/utils/structuredData.ts, commerce/sections/Seo/*
Product and listing SEO sections use shared structured-data control modes. The deprecated ignoreStructuredData option remains as a fallback.
Product SEO output
commerce/sections/Seo/SeoPDPV2.tsx, website/components/Image.tsx, website/components/Seo.tsx
Product SEO can optimize VTEX images and emits type="product" metadata.
Commerce loader cache directives
commerce/loaders/product/*
Selected product loaders export "no-cache" directives.

Verified reviews

Layer / File(s) Summary
Full-review loading and caching
verified-reviews/loaders/fullReview.ts, verified-reviews/loaders/productDetailsPage.ts, verified-reviews/loaders/productList.ts, verified-reviews/loaders/productListingPage.ts, verified-reviews/manifest.gen.ts
A full-review loader calls the review client, returns an empty fallback on failure, normalizes cache keys, and is registered in the manifest.

VTEX authentication, sessions, and cookies

Layer / File(s) Summary
Cookie and session contracts
vtex/utils/cookies.ts, vtex/utils/session.ts, vtex/utils/types.ts, vtex/utils/vtexId.ts, vtex/utils/client.ts, vtex/utils/authResponse.ts
Cookie-jar utilities, session types, session item defaults, endpoint contracts, and structured authentication error parsing are added or expanded.
Authentication action flow
vtex/actions/authentication/*, vtex/sections/Authentication/Logout.tsx
Authentication actions preserve cookies, proxy response cookies, refresh tokens, validate sessions, handle password errors, and implement logout redirects.
Session lifecycle actions
vtex/actions/session/*, vtex/loaders/session/getSession.ts, vtex/manifest.gen.ts
Session creation, editing, validation, and loading use shared properties, requested items, cookie jars, and typed responses.
Cookie-consuming actions and loaders
vtex/actions/address/*, vtex/actions/payment/*, vtex/actions/profile/*, vtex/loaders/address/*, vtex/loaders/payment/*, vtex/loaders/profile/*, vtex/loaders/user.ts
Identity payload parsing is separated from forwarding the complete request cookie header.
Cart user-state update
vtex/actions/cart/updateUser.ts
The cart action uses effective cookies and returns a boolean after updating the anonymous user state.

VTEX sitemap processing

Layer / File(s) Summary
Custom sitemap routes
vtex/loaders/proxy.ts
Proxy configuration supports custom sitemap handlers, exclusions, and route-based filtering options.
Sitemap rewriting and filtering
vtex/handlers/sitemap.ts
The sitemap handler rewrites hosts, removes configured or unmatched entries, logs failures, and removes invalid response headers.

Website JSON rendering

Layer / File(s) Summary
Structured JSON rendering contract
deno.json, website/mod.ts, website/handlers/fresh.ts
Fresh receives render configuration and recognizes the renderJson query mode.
Structured JSON rendering execution
website/handlers/fresh.ts
Fresh excludes selected sections, skips their loaders, serializes resolved sections, and returns structured JSON with CORS headers.

Analytics

Layer / File(s) Summary
Google Tag Manager extra parameters
website/components/Analytics.tsx
Analytics forwards valid extra parameters to Google Tag Manager script and noscript URLs while excluding reserved keys.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant Fresh
  participant SectionResolver
  participant JSONResponse
  Request->>Fresh: request renderJson mode
  Fresh->>SectionResolver: resolve sections and apply exclusions
  SectionResolver-->>Fresh: resolved sections
  Fresh->>JSONResponse: serialize page metadata and sections
  JSONResponse-->>Request: return JSON with CORS headers
Loading
sequenceDiagram
  participant Client
  participant AuthenticationAction
  participant VTEX
  participant SessionValidation
  Client->>AuthenticationAction: submit credentials
  AuthenticationAction->>VTEX: send credentials and cookie header
  VTEX-->>AuthenticationAction: return authentication data and Set-Cookie headers
  AuthenticationAction->>SessionValidation: refresh token and validate session
  SessionValidation-->>Client: return authentication result
Loading

Suggested reviewers: guitavano

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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