Cloudflare Pages
How Arcturus-Prime.com is built, deployed, and served as a static+SSR hybrid on Cloudflare Pages
Cloudflare Pages
Arcturus-Prime is deployed as a hybrid static/SSR site on Cloudflare Pages. The static pages are served from Cloudflare’s global CDN edge nodes, and the dynamic routes (API endpoints, admin pages) run as Cloudflare Pages Functions — which are Workers under the hood.
Deployment Pipeline
The deployment pipeline is a three-hop chain: Gitea push triggers a GitHub mirror sync, which triggers the Cloudflare Pages build.
git push (Gitea)
│
▼
git.Arcturus-Prime.com/KeyArgo/Arcturus-Prime ← canonical repo
│
│ mirror webhook
▼
github.com/KeyArgo/Arcturus-Prime ← read-only mirror
│
│ CF Pages integration
▼
Arcturus-Prime.pages.dev ← Cloudflare Pages build
│
▼
Arcturus-Prime.com ← production (custom domain)
Gitea is the source of truth. The GitHub mirror exists solely because Cloudflare Pages requires a GitHub or GitLab repo for its CI/CD integration. A post-receive hook on Gitea pushes to GitHub, which triggers the Pages build automatically.
Build Configuration
| Setting | Value |
|---|---|
| Framework preset | Astro |
| Build command | npm run build |
| Build output directory | dist/ |
| Root directory | / |
| Node.js version | 20.x |
| Package manager | npm |
The build command runs Astro’s build process, which outputs static HTML to dist/ for prerendered pages and bundles Functions for SSR routes into dist/_worker.js.
Framework Details
Arcturus-Prime runs on Astro 5.17 with the @astrojs/cloudflare adapter version 12.x. The adapter handles the translation between Astro’s SSR output and Cloudflare’s Workers runtime.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
platformProxy: {
enabled: true,
},
}),
});
The output: 'server' setting makes SSR the default, with individual pages opting into static generation via export const prerender = true in their frontmatter script. In practice, about 81 pages are statically prerendered and 49 are server-rendered on each request.
Static vs. SSR
Astro’s hybrid rendering model lets each page choose its rendering strategy independently.
Static Pages (prerender = true)
These are generated at build time and served directly from the CDN:
- All blog posts (
/blog/[slug]) - Journal entries (
/journal/[...slug]) - Documentation pages (
/docs/[...slug]) - Learning content (
/learn/[...slug]) - Public pages (
/,/about,/contact,/homelab, etc.)
Static pages load instantly from the edge. No cold starts, no function invocations, no compute costs.
SSR Pages (prerender = false)
These run as Cloudflare Pages Functions on every request:
- Admin pages (
/admin/*) — need auth checks and live data - Dashboard (
/dashboard) — personalized per user role - Auth routes (
/auth/login,/auth/logout) - User portal (
/user/*) - All API endpoints (
/api/*) - Command center (
/command/*)
SSR pages have access to the Cloudflare runtime: KV bindings, environment variables, request headers, and the full Workers API.
Pages Functions (Workers)
Every file in src/pages/api/ becomes a Cloudflare Pages Function. These are Workers scoped to the Pages project — they share the same deployment, bindings, and domain.
Functions have access to:
// Inside any SSR page or API route
const runtime = Astro.locals.runtime;
const env = runtime.env;
// KV namespace
const kv = env.ARGOBOX_CACHE;
// Environment secrets
const teamDomain = env.CF_ACCESS_TEAM_DOMAIN;
const accessAud = env.CF_ACCESS_AUD;
Compatibility Flags
The project requires specific Workers compatibility flags to work correctly:
# wrangler.toml (or CF Pages settings)
compatibility_flags = ["nodejs_compat"]
The nodejs_compat flag enables Node.js API compatibility in Workers (Buffer, crypto, etc.). The disable_nodejs_process_v2 flag was previously needed but may vary by adapter version.
Bundle Exclusions
Some packages are excluded from the SSR bundle because they are Node.js-only and would break in the Workers runtime:
better-sqlite3— native SQLite binding, used only in dev mode@argonaut/core— local package with Node.js dependencies
These are configured as external in the Astro/Vite config so they are never bundled into the Worker.
CSP and Security Headers
Content Security Policy and other security headers are defined in public/_headers, which Cloudflare Pages serves as HTTP response headers.
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
/admin/*
X-Robots-Tag: noindex, nofollow
/api/*
X-Robots-Tag: noindex, nofollow
The CSP policy allows inline scripts (required by Astro’s hydration), connections to the Cloudflare Access endpoints, and fonts from the local domain. Admin and API routes get noindex to keep them out of search engines.
Preview Deployments
Every pull request gets its own preview deployment on a unique subdomain (e.g., abc123.Arcturus-Prime.pages.dev). Preview deployments are functionally identical to production — same build, same bindings, same Functions.
Admin Access in Previews
Preview deployments can access admin functionality by appending ?preview=true to any admin URL. This bypasses the normal Cloudflare Access authentication requirement so PR reviewers can test admin features without being in the Access policy.
https://abc123.Arcturus-Prime.pages.dev/admin?preview=true
This is only available on preview deployments (non-production URLs). The production domain at Arcturus-Prime.com always requires full Cloudflare Access authentication for admin routes.
Environment Variables
Environment variables are set in the Cloudflare Pages dashboard under Settings > Environment Variables. They are available to both the build process and the runtime Functions.
Build-Time Variables
| Variable | Purpose |
|---|---|
NODE_VERSION | Pin Node.js version for builds |
NPM_FLAGS | Additional npm install flags |
Runtime Secrets
| Variable | Purpose |
|---|---|
CF_ACCESS_TEAM_DOMAIN | Cloudflare Access team domain |
CF_ACCESS_AUD | Access application audience tag |
CF_ACCESS_ISSUER | JWT issuer URL |
OPENAI_API_KEY | OpenRouter API key for AI features |
GOOGLE_AI_KEY | Google Generative AI key |
RESEND_API_KEY | Transactional email via Resend |
CACHE_WARMUP_SECRET | Secret for cache warmup cron |
KV Bindings
| Binding Name | Namespace | Purpose |
|---|---|---|
ARGOBOX_CACHE | ARGOBOX_CACHE | Primary data store (roles, cache, sessions) |
Custom Domain
The production site is served at Arcturus-Prime.com with Cloudflare handling DNS, SSL termination, and CDN caching. The Pages project is configured with:
- Custom domain:
Arcturus-Prime.com - SSL mode: Full (strict)
- Always HTTPS: Enabled
- Auto Minify: Disabled (Astro handles this)
The www.Arcturus-Prime.com subdomain redirects to the apex domain via a Cloudflare Page Rule.
Troubleshooting
Build Failures
The most common build failures are:
- Node.js version mismatch — Ensure
NODE_VERSIONmatches what works locally (20.x). - Missing environment variables — Functions that reference
env.SOME_VARwill fail if the variable is not set in the Pages dashboard. - Bundle size limits — Workers have a 1MB compressed limit. If the bundle exceeds this, check for accidentally included native modules.
- TypeScript errors — The build runs
astro checkwhich catches type errors thatnpm run devmight not surface.
Cold Starts
Pages Functions (Workers) have minimal cold starts compared to traditional serverless platforms. First requests after a deployment or after idle periods might see 5-50ms additional latency, but subsequent requests on the same edge location are near-instant.