Architecture decision ADR-009

Use ISR for Category and Filter Pages; SSR for Comparison Pages

Accepted · 2026-05-10

Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.

Date: 2026-05-10
Status: Accepted
Deciders: Daniel Meza


Context and Problem Statement

Urza's Wallet's SEO (Search Engine Optimization) strategy is the primary driver of organic user acquisition. Google's crawling and indexing of financial comparison pages is the revenue-generating engine. We need to decide — at the level of individual page types — whether pages are statically generated (SSG), statically generated with incremental revalidation (ISR), server-rendered per-request (SSR), or client-side rendered (CSR).

This decision has direct consequences for:


Decision Drivers


Considered Options

For each page type, the options are:

  1. SSG (build-time generation) — Pre-rendered at next build, served as static HTML from Railway's edge cache. Does not update until the next build.
  2. SSG + ISR (static with incremental revalidation) — Pre-rendered at build time; refreshed in the background when a request arrives after the TTL expires OR when revalidatePath is called on-demand.
  3. SSR (server-side rendering per-request) — Rendered on the server for every request. Always fresh. Higher server resource usage.
  4. CSR (client-side rendering) — Rendered in the browser using JavaScript. Not SEO-friendly for public pages.

Decision Outcome

The rendering strategy is assigned per page type as follows:

Page Type Strategy Revalidation Rationale
Homepage SSG On-demand only Rarely changes; full CDN caching; maximum LCP performance
Category hubs (/tarjetas-de-credito/) SSG + ISR revalidate: 3600 + on-demand High SEO value; product list changes ≤ hourly; CDN-cached between updates
Filter pages (/tarjetas-de-credito/sin-anualidad/) SSG + ISR revalidate: 3600 + on-demand Same as category hub; pre-generated for common filters
Issuer pages (/tarjetas-de-credito/bac-credomatic/) SSG + ISR revalidate: 3600 + on-demand Issuer product lists stable; CDN-cached
Product detail pages SSG + ISR revalidate: 14400 + on-demand Highest conversion value; pre-rendered for LCP; rates change at most daily
Editorial / guide pages SSG On-demand only Content changes rarely; full CDN caching
Comparison pages (/comparar/tarjetas/?ids=...) SSR Per-request Dynamic product set; canonical control; noindex
User dashboard CSR — Authenticated; personal data; noindex
Admin UI CSR — Internal; authenticated; noindex

ISR revalidation mechanics:

There are two revalidation triggers:

  1. TTL-based: Next.js automatically revalidates pages in the background when a request arrives after the configured revalidate seconds have elapsed. The user who triggers the revalidation receives the cached (previous) version; subsequent users receive the fresh version.
  2. On-demand: The Data Ingestion Service calls POST /api/revalidate with the affected vertical and product ID(s) after a successful write. Next.js calls revalidatePath('/tarjetas-de-credito/') and revalidatePath('/tarjetas-de-credito/bac-credomatic/bac-platinum/') for the affected pages. This means product pages are updated within ~10 seconds of a successful ingestion run, regardless of the TTL.

The on-demand revalidation endpoint is authenticated with an HMAC (Hash-based Message Authentication Code)-signed token (REVALIDATE_SECRET) shared between the ingestion service and the Next.js app. It is not a public endpoint.

Comparison page SSR — canonical strategy:

Comparison pages use SSR with the following behaviors:

  1. The ids query parameter is sorted and deduplicated server-side before rendering. ?ids=def,abc and ?ids=abc,def render identically.
  2. A <meta name="robots" content="noindex, follow"> tag is injected in the page <head>.
  3. A <link rel="canonical" href="/tarjetas-de-credito/"> points to the parent category hub, not to the comparison URL.
  4. Comparison pages are excluded from the sitemap.
  5. No internal links on the platform link to comparison URLs — they exist only as user-generated, shareable URLs (e.g., "compartir esta comparación" copies the URL to clipboard).

Build-time pre-generation scope:

generateStaticParams is limited to avoid excessively long build times:


Positive Consequences

Negative Consequences / Trade-offs


Pros and Cons of the Options (for product detail pages)

Option LCP Data Freshness Server Load SEO Risk
SSG only Best (< 1.5 s) Stale until next build Minimal Low
SSG + ISR (chosen) Best (< 1.5 s from cache) Fresh within ingestion cycle Low (cached responses) Low
SSR Good (< 2.5 s) Always fresh High (per-request render) Low
CSR Poor (JS-dependent) Always fresh Minimal (server) High

References