Commit Graph

4 Commits

Author SHA1 Message Date
davidherran 8d80cbbc27 perf(seo): image sizes, semantic HTML, X-Robots-Tag headers
- Add `sizes` prop to 8 <Image> components across news, heritage, and
  application pages — tells the browser which srcset variant to download,
  improving LCP and reducing bandwidth
- Replace date <span> with <time dateTime={ISO}> on news pages —
  Google uses datetime for article freshness signals
- Wrap news cards and article content in <article> tags — semantic
  boundary for crawlers
- Add X-Robots-Tag: noindex, nofollow header to all /hq-command
  responses in proxy.ts — defense-in-depth alongside meta robots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 18:04:40 -05:00
davidherran 7fe5108f66 feat: HTTP shared cache for public marketing pages
Deploy to VPS / deploy (push) Has been cancelled
Pages got fast again. Public marketing routes are still rendered
per-request by Next.js (force-dynamic, until the ISR bug gets isolated),
but their HTML is now cached at the Nginx layer for 60s with a 5-minute
stale-while-revalidate window. Result: only the first hit on a URL
inside a 60s window pays the SSR cost; every other visitor in that
window gets a sub-10ms cached response. While a cached entry is
revalidating, peers keep getting the stale copy — no cold starts, no
thundering herds.

NEXT.JS MIDDLEWARE (src/proxy.ts)
- isCacheablePublicPath() identifies routes safe to share-cache:
  /, /<locale>, /<locale>/applications, /<locale>/news,
  /<locale>/heritage. Excludes /<locale>/parts (auth-gated B2B portal)
  and /hq-command/*, /api/*, /_next/*.
- hasAuthCookie() short-circuits caching when the request carries a
  flux_session (admin CMS) or flux_b2b_session (client portal) cookie.
  Authenticated users always get a fresh per-account render.
- When both checks pass, the response gets:
    Cache-Control: public, s-maxage=60, stale-while-revalidate=300

NGINX (nginx/nginx.conf)
- New shared zone:
    proxy_cache_path /var/cache/nginx/flux levels=1:2
                     keys_zone=flux_html:50m max_size=1g inactive=24h
                     use_temp_path=off;
- Access log gets a `cache=$upstream_cache_status` field so we can
  audit hit/miss ratios in the live logs.

NGINX (nginx/conf.d/flux.conf — location /)
- proxy_cache flux_html + proxy_cache_revalidate on
- proxy_cache_use_stale: serves stale on backend errors / timeout /
  during update, so 502s during a Next.js restart never reach users.
- proxy_cache_background_update + proxy_cache_lock: only one upstream
  request fires when a cached entry expires; others keep getting stale.
- proxy_cache_bypass / proxy_no_cache wired to flux_session +
  flux_b2b_session cookies — admin and B2B traffic skips the shared
  cache entirely.
- X-Cache-Status response header (HIT/MISS/EXPIRED/STALE/UPDATING/BYPASS)
  for live debugging — open dev tools, refresh, watch the value flip.

WHAT YOU'LL FEEL
- First visitor on /en within a 60s window: ~150-300ms (SSR + DB).
- Second through Nth visitors in the same window: <10ms.
- Editor publishes a change in HQ Command → revalidatePath() inside
  the existing actions invalidates the Next.js cache; the next
  marketing-page request rebuilds and primes Nginx fresh. The 60s
  TTL bounds how long stale content can linger if revalidation is
  ever skipped.

NO BREAKING CHANGES
- Auth flows untouched (cookies bypass cache).
- HQ Command + API endpoints untouched (separate Nginx locations).
- Static assets (cases/, applications/, /branding/, /_next/static)
  unaffected — they had their own cache headers already.
- Server-side cache invalidation via revalidatePath() still works.

DEPLOY (David)
  cd /opt/flux-srl
  git pull
  docker compose up -d --build app
  docker compose exec nginx nginx -t
  docker compose exec nginx nginx -s reload
2026-05-05 12:20:39 -05:00
davidherran 62506f10b4 fix: strip internal container port from redirect URLs
Deploy to VPS / deploy (push) Has been cancelled
The site was redirecting / -> https://rf-flux.com:3000/en, where :3000
is the container's internal port (only "expose"d, not published) — so
the browser saw ERR_CONNECTION_REFUSED.

Root cause: when running behind Nginx in standalone mode, Next.js (via
next-intl in this case) can build absolute redirect URLs that leak the
container's internal PORT/HOSTNAME env into the Location header.

TWO LAYERS OF DEFENCE
1. Nginx (nginx/conf.d/flux.conf)
   - Adds X-Forwarded-Host + X-Forwarded-Port so the upstream knows
     the public port (443) and host
   - proxy_redirect rewrites any Location header that still slips
     through with :3000 back to the public https://$host

2. Middleware (src/proxy.ts)
   - sanitizeRedirectLocation() runs after handleI18nRouting and
     scrubs Location headers that point at internal hostnames (app /
     localhost / 0.0.0.0) or the container port :3000, replacing them
     with the public host derived from x-forwarded-host / host header.

Either layer alone would fix the immediate symptom; together they
also prevent the same class of bug from showing up in any future
redirect path.
2026-05-04 16:32:45 -05:00
davidherran fc24313f15 production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled
2026-03-20 13:46:05 -05:00