Skip to main content
The project uses Astro’s output: 'static' globally, then opts specific routes into server-side rendering with export const prerender = false. This keeps the public site fast (everything from CDN) while still supporting dynamic portal pages that need authentication.

Rendering modes at a glance

ModeCountRoutesRendering trigger
SSG8 pagesPublic site, sponsors, projects, eventsBuild time
SSR9 pagesPortal, student dashboard, authRequest time
API5 endpointsBetter Auth, events API, subscribeRequest time

Static generation (SSG) — public pages

All public pages are pre-rendered during the Cloudflare Pages build. Content is fetched from Sanity via GROQ at build time and baked into static HTML. There are no runtime API calls from a browser visiting the public site.
FileRouteStatic path source
index.astro/Hardcoded
sponsors/index.astro/sponsors/Hardcoded
sponsors/[slug].astro/sponsors/[slug]ALL_SPONSOR_SLUGS_QUERY
projects/index.astro/projects/Hardcoded
projects/[slug].astro/projects/[slug]ALL_PROJECT_SLUGS_QUERY
events/index.astro/events/Hardcoded
events/[slug].astro/events/[slug]ALL_EVENT_SLUGS_QUERY
[...slug].astro/<any-cms-slug>ALL_PAGE_SLUGS_QUERY
Slug-based pages export a getStaticPaths() function that queries Sanity for all published slugs. Each slug becomes a separate pre-rendered HTML file on the CDN.

Server-side rendering (SSR) — portal and auth

Portal pages export export const prerender = false, which tells the @astrojs/cloudflare adapter to route those requests through a Cloudflare Worker instead of the CDN. The Worker runs the Astro render pipeline on every request.
FileRouteRequired role
portal/index.astro/portal/sponsor
portal/[sponsorSlug].astro/portal/[sponsorSlug]sponsor
portal/events.astro/portal/eventssponsor
portal/progress.astro/portal/progresssponsor
portal/login.astro/portal/loginnone (public)
portal/denied.astro/portal/deniednone (public)
student/index.astro/student/student
auth/login.astro/auth/loginnone (public)
api/auth/[...all].ts/api/auth/*none (Better Auth)

Middleware pipeline

Every request to an SSR route passes through the middleware pipeline before the page handler runs:

Auth check detail

  1. Middleware extracts the session token from the request cookie.
  2. It checks SESSION_CACHE KV for a cached session (5-minute TTL).
  3. On cache miss, it queries D1 via Better Auth.
  4. If the user’s email is in sponsor.allowedEmails (checked against Sanity), the role is escalated to sponsor.
  5. The validated user is placed in Astro.locals.user for the page handler.
  6. Non-whitelisted users receive the student role and are redirected if they try to access /portal/*.

Rate limit detail

  • Implemented as a Cloudflare Durable Object (SlidingWindowRateLimiter).
  • Limit: 100 requests per 60 seconds per IP address.
  • Uses SQLite storage within the DO for the sliding window counter.
  • Alarm-based cleanup removes stale rate limit records.
  • Fail-open: if the Durable Object is unavailable, the request proceeds normally.

SSR on the preview branch — Visual Editing

The preview Git branch deploys to Cloudflare Pages with these additional environment variables:
PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true
PUBLIC_SANITY_LIVE_CONTENT_ENABLED=true
With Visual Editing enabled:
  • The Sanity client switches from the CDN (useCdn: true) to the live API (useCdn: false).
  • Stega encoding embeds click-to-edit metadata in rendered strings.
  • The Presentation tool in Sanity Studio loads the preview URL as an iframe with an overlay that maps visible elements back to their Studio fields.
  • Draft content (unpublished changes) is visible on the preview URL.
Visual Editing is never active on main. The production build always uses useCdn: true and bakes published content only.

How webhooks trigger rebuilds

1

Editor publishes content in Sanity Studio

Publishing a document (or unpublishing, deleting) triggers a Sanity webhook.
2

Webhook hits Cloudflare Pages deploy hook

Sanity sends a POST request to the Cloudflare Pages deploy hook URL configured in the project dashboard.
3

Cloudflare Pages triggers a new build

The build runs npm run build --workspace=astro-app, which fetches all content via GROQ and pre-renders every SSG page.
4

New static assets are deployed to the CDN

Cloudflare atomically swaps the CDN cache with the new build output. Visitors immediately see updated content.
Because every publish triggers a full rebuild, content editors do not need to know which page a block appears on. The build always fetches everything fresh.