Repository navigation
perf(sites): built-in page cache with publish-time invalidation (#72) - #143
Merged
Merged
Conversation
#72) Public pages are now rendered once and served from the cache. Repeat views of pages, blog and docs no longer call the API, and a publish shows up within a fraction of a second. Sites: - Cache Components: every public read is a "use cache" function tagged team/shell/page/docs/blog/changelog/sitemap. Pages inherit the tags of the data they read and are cached as a whole. A _build placeholder satisfies generateStaticParams, so a build needs no API. - Nothing rendered reads request headers anymore. /embed has its own layout outside the new (site) group. The canonical host and hideFromSearch come from cached public settings, and a non-canonical host gets X-Robots-Tag from the proxy. Docs are cached like everything else. - Docs articles and blog posts read only the shared header and footer (shell tag), so a homepage publish doesn't re-render them. - Ticket threads, pull request previews and the chat embed render their token/query part inside Suspense and stay private, no-store. - cache-handler.cjs: Next's in-memory handler, plus Redis when REDIS_URL is set. Entries are shared, and tag invalidations are published and applied to every replica's tag manifest, so full-page hits refresh too. Redis never blocks or fails a request. - Host lookups use Redis as a second tier, share concurrent misses and invalidate on every replica. - The revalidate webhook takes { eventId, teamId, tags[], invalidateHosts? }, accepts only the team's tag families and ignores replays. The docs webhook expires docs + sitemap only. - Links prefetch on intent: viewport prefetch of fully cached pages downloaded every linked page and its hero image. - Without REVALIDATE_WEBHOOK_SECRET, pages refresh every minute. API: - site_cache_outbox replaces the best-effort revalidateTeam POST. Events are written by the query layer, so REST, MCP and Git sync are all covered. Release activation diffs the outgoing release to find the changed pages, and the shell when the site snapshot changed. - SITES_REVALIDATE_URL lets containers post to the internal address. - Public settings carry canonicalHost and hideFromSearch. - Queuing a docs publication wakes the publication worker.
This was referenced Oct 25, 2026
IMAGE_REMOTE_HOSTS and DEPLOYMENT_ID are read by next build, so the sites Dockerfile now takes them as build args and compose passes IMAGE_REMOTE_HOSTS through. DEPLOYMENT_ID only matters for separately built replicas.
next build prerenders the sitemap route with a '-' team. Its cached read called the API, so building the image without a running backend failed. Any team ID that isn't team_… is now a placeholder.
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.
Part of #75. Closes #72. Design:
docs/site-delivery.md§4 (updated to match what was built, including measurements in §9.1).Public pages are now rendered once and served from FrontLit's own cache. Repeat views don't call the API, and a publish shows up in well under a second. It works with a plain
docker compose up(no Redis, no CDN). WithREDIS_URL, every replica shares the cache and every invalidation.Results (local, same sample tenant and harness as #142)
mainScoped invalidation, checked by warming 6 pages and then making each change:
/re-renders./blog./changelog./docs/*.Lighthouse (mobile, median of 5):
Sites
lib/api.tsis a"use cache"function taggedteam/shell/page/docs/blog/changelog/sitemap(siteprofile: stale 5 min, revalidate 1 h, expire 30 d). Pages inherit those tags and are cached whole (x-nextjs-cache: HIT).generateStaticParamsreturns a_buildplaceholder, so a build needs no API./embedhas its own layout outside the new(site)group. Canonical host andhideFromSearchcome from cached public settings. A non-canonical host getsX-Robots-Tag: noindexfrom the proxy.teamIdcomes from params, and docs are no longerno-store.getSiteChrome,shelltag), so a homepage publish doesn't re-render them.?layout=part inside<Suspense>and sendprivate, no-store. Side effect: a bad ticket token now shows the noindex not-found page with status 200 instead of 404. The duplicated site chrome on ticket pages is gone.cache-handler.cjs. Next's in-memory handler, plus Redis whenREDIS_URLis set. Entries are shared across replicas. Tag invalidations are stored, published, and applied to each replica's tag manifest, which is what full-page hits are checked against. Redis never blocks a request: no offline queue, 500 ms timeouts, background reconnects. I tested it down at boot, stopped mid-run, and restarted./api/webhooks/revalidatetakes{ eventId, teamId, tags[], invalidateHosts? }. It accepts only that team's tag families and acknowledges replays without repeating them. The docs webhook now expires onlydocs+sitemap.SiteLinkprefetches on hover, focus or touch. With fully static pages, viewport prefetch downloaded every linked page plus its preloaded hero image; a docs sidebar alone would fetch every article.REVALIDATE_WEBHOOK_SECRETnothing can invalidate, so pages refresh every minute (siteUnmanaged). That matches today's behaviour.API
site_cache_outbox(migration0011) replaces the best-effortrevalidateTeam. Events are written in the query layer, so REST, MCP and Git sync invalidate the same way. MCP blog and settings changes never invalidated before.shellwhen header, footer, theme or redirects changed. Delivery runs within ~50 ms of the write, retries with backoff, and records per-destination acknowledgments.SITES_REVALIDATE_URLlets the API post to an internal address. Compose now setshttp://frontlit-sites:3002; before, it posted to the publiclocalhost:3002, which doesn't resolve inside the container.canonicalHostandhideFromSearch.Testing
site-cache.test.ts). Sites: 59 tests pass (webhook tag validation and replay, host lookup dedupe, proxy forwards no request headers). Typecheck and lint are clean./nope,/blog/nopeand/docs/nopereturn 404.Notes
DEPLOYMENT_IDon every instance. The public site has no Server Actions, so it needs no shared encryption key.