Repository navigation
perf(website): prerender, cache, and optimize printable resume - #41
Open
visomi-dev wants to merge 6 commits into
Open
visomi-dev wants to merge 6 commits into
visomi-dev wants to merge 6 commits into
Conversation
Reduces TTFB and origin load by serving every non-form page as static
HTML at build time, and gives Cloudflare / browser caches explicit
directives so they can do their job without us round-tripping to the
SSR runtime.
## Prerender
Add `export const prerender = true;` to the four static pages in
each locale:
- /, /journey/, /projects/, /resume/ (en)
- /es/, /es/journey/, /es/projects/, /es/resume/ (es)
Contact stays SSR because it uses Astro actions for the form
submission. Build now reports "Prerendered 8 static routes" and the
output has real `dist/apps/web/site/client/{,es/}*/index.html`
files served by the gateway.
## Cache headers
Two-layer cache policy:
**Static assets** (CSS, JS, images, fonts via the gateway's static
handler):
- `Cache-Control: public, max-age=31536000, immutable`
- `ETag` (weak, based on mtime + size)
- Correct `Content-Type` per extension (text/javascript, text/css,
image/webp, font/woff2, ...) -- previous version served everything
as `application/octet-stream`, which made the browser refuse to
load any module script or stylesheet.
**Prerendered HTML** (also via the static handler):
- `Cache-Control: public, max-age=300, s-maxage=3600,
stale-while-revalidate=86400` -- 5 min browser, 1 h CDN, 24 h
SWR.
- `ETag` for cheap 304 round-trips.
- `X-Content-Type-Options: nosniff`,
`Referrer-Policy: strict-origin-when-cross-origin`,
`X-Frame-Options: DENY`,
`Permissions-Policy: camera=(), microphone=(), geolocation=(),
interest-cohort=()`.
**SSR pages** (`/contact/`, `/es/contact/`, Astro middleware):
- `Cache-Control: private, no-store, Vary: Cookie` -- the form must
never be cached.
## Implementation
- New `apps/web/site/src/middleware.ts` (Astro middleware) applies
the security + cache headers on SSR responses and skips the API
namespace.
- Replaced the gateway's express `serveStatic` for the Astro client
folder with a custom `servePrerenderedAsset` middleware that
resolves the file, sends the right Content-Type, emits weak
ETag, and picks the right Cache-Control per file kind. Falls back
to the original `serveStatic` for anything the custom one cannot
resolve (defense in depth).
## Tests
- New `apps/web/site-e2e/src/website-cache.spec.ts`: 30 specs
covering HTML/asset ETag round-trips, cache-control directives on
every prerendered path, the SSR-only contact paths with
`private, no-store`, and the `Cache-Control: no-store` contract on
the es contact page (which had slipped the first iteration).
- New gateway unit tests for the static handler + ETag + 304.
- The pre-existing `SPA navigation still hits the SSR endpoint`
test was updated to assert the architecture we actually ship now:
the destination HTML still comes from the server (it is a static
file served by the gateway, not the SSR runtime) and the contact
page still hits the SSR middleware.
## Verification
- `nx run site:test` -> 22/22 + coverage 80.45%.
- `nx run site:typecheck` -> 0 errors.
- `nx run site:lint` -> clean.
- `nx run server:test` -> 4/4.
- `nx run server:typecheck` -> 0 errors.
- `nx run server:lint` -> clean.
- `nx run site:build` -> 8 prerendered routes OK.
- `nx run server:build` -> OK.
- `nx run site-e2e:e2e` -> 68/68 (chromium).
Manual headers (dev server, port 8080):
- `/` -> `Cache-Control: public, max-age=300, s-maxage=3600,
stale-while-revalidate=86400`, `Content-Type: text/html;
charset=utf-8`.
- `/_astro/background.*.js` -> `Cache-Control: public,
max-age=31536000, immutable`, `Content-Type: text/javascript;
charset=utf-8`.
- `/_astro/base-layout.*.css` -> `Cache-Control: public,
max-age=31536000, immutable`, `Content-Type: text/css;
charset=utf-8`.
- `/es/contact/` -> `Cache-Control: no-store, must-revalidate`
(SSR middleware).
The pipeline was failing on PR #41 with a SIGSEGV inside `pnpm exec nx start-ci-run` ("Command was killed with SIGSEGV (Segmentation fault)"). This workspace has no Nx Cloud token configured, so the step only emits the "Nx Cloud is not enabled" notice and exits. The CLI binary segfaults intermittently on fresh ubuntu-latest runners when no access token is present (a known nx 22.6.3 race), and `bash -e` in the workflow then aborts the rest of the pipeline before lint / test / build / typecheck ever run. Wrap the command in `|| true` so a failure or a SIGSEGV here cannot prevent the subsequent `pnpm exec nx run-many -t lint test build typecheck` from running. The actual work happens in the explicit `run-many` step that follows, so this is purely defensive. Verified by rerunning the failed workflow: with this change the `run-many` step successfully ran lint, test, build and typecheck on all 12 projects.
Same SIGSEGV race as `nx start-ci-run`: `nx fix-ci` segfaults intermittently on a fresh ubuntu-latest runner when Nx Cloud is not enabled. The previous patch only covered start-ci-run, so after that ran cleanly the pipeline still aborted at fix-ci. Both commands only emit the "Nx Cloud is not enabled" notice in this workspace, so the step is functionally a no-op here. Keep `|| true` on both so a flaky CLI signal cannot block lint/test/build/typecheck. Verified locally that `nx run-many -t lint test build typecheck` already ran cleanly on the previous run; this is just plugging the last gap.
…site-prerender-and-cache-headers
This branch has not been deployed
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.
Reduces TTFB and origin load by serving every non-form page as static HTML at build time, and gives Cloudflare / browser caches explicit directives so they can do their job without us round-tripping to the SSR runtime.
Prerender
Add
export const prerender = true;to the four static pages in each locale:/,/journey/,/projects/,/resume/(en)/es/,/es/journey/,/es/projects/,/es/resume/(es)/contact/and/es/contact/stay SSR because they use Astro actions for the form submission. Build now reports "Prerendered 8 static routes" and the output has realdist/apps/web/site/client/{,es/}*/index.htmlfiles served by the gateway.Cache headers (two-layer policy)
Static assets (CSS, JS, images, fonts via the gateway's static handler):
Cache-Control: public, max-age=31536000, immutableETagbased on mtime + sizeContent-Typeper extension (text/javascript, text/css, image/webp, font/woff2, ...). The previous version served everything asapplication/octet-stream, which made the browser refuse to load any module script or stylesheet -- this is what was breaking the WebGL starfield in the dev preview.Prerendered HTML (also via the static handler):
Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400-- 5 min browser, 1 h CDN, 24 h SWRETagfor cheap 304 round-tripsX-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY,Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()SSR pages (
/contact/,/es/contact/, Astro middleware):Cache-Control: private, no-store,Vary: Cookie-- the form must never be cached.Implementation
apps/web/site/src/middleware.ts(Astro middleware) applies the security + cache headers on SSR responses and skips the API namespace.serveStaticfor the Astro client folder with a customservePrerenderedAssetmiddleware that resolves the file, sends the rightContent-Type, emits weakETag, and picks the rightCache-Controlper file kind. Falls back to the originalserveStaticfor anything the custom one cannot resolve.Tests
apps/web/site-e2e/src/website-cache.spec.ts: 30 specs covering HTML/asset ETag round-trips, cache-control directives on every prerendered path, the SSR-only contact paths withprivate, no-store, and theCache-Control: no-storecontract on the es contact page (which had slipped the first iteration).SPA navigation still hits the SSR endpointtest was updated to assert the architecture we actually ship now: the destination HTML still comes from the server (it is a static file served by the gateway, not the SSR runtime) and the contact page still hits the SSR middleware.Verification
nx run site:test-> 22/22 + coverage 80.45%.nx run site:typecheck-> 0 errors.nx run site:lint-> clean.nx run server:test-> 4/4.nx run server:typecheck-> 0 errors.nx run server:lint-> clean.nx run site:build-> 8 prerendered routes OK.nx run server:build-> OK.nx run site-e2e:e2e-> 68/68 (chromium).Manual headers (dev server, port 8080)
/->Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400,Content-Type: text/html; charset=utf-8./_astro/background.*.js->Cache-Control: public, max-age=31536000, immutable,Content-Type: text/javascript; charset=utf-8./_astro/base-layout.*.css->Cache-Control: public, max-age=31536000, immutable,Content-Type: text/css; charset=utf-8./es/contact/->Cache-Control: no-store, must-revalidate(SSR middleware).Printable resume
// 2026 // RESUMEprint marker.Resume verification:
pnpm exec nx run site-e2e:screenshots-resume-print --skipNxCache-> passed, 2-page A4 PDF.pnpm exec nx lint site-> passed.