Skip to content

Phase 10 - Code Examples

Cloudflare Pages is the recommended deployment platform for this project due to its fast global edge network, free analytics, and seamless integration with Cloudflare Workers for any dynamic functionalities or server-side rendering (SSR) needs. This makes it an excellent choice for high-performance static sites with the option to scale to more complex applications.

Key Advantages:

  • Performance: Leverages Cloudflare’s extensive CDN for fast content delivery worldwide.
  • Analytics: Provides built-in, privacy-focused web analytics.
  • Scalability with Workers: Easily add server-side logic, API endpoints, or SSR for specific components/pages using Cloudflare Workers.

Watch-outs & Tweaks:

  • SSR Islands & Cloudflare Functions: If using SSR for Astro islands or implementing API routes via Cloudflare Functions (either through Pages Functions or dedicated Workers), be mindful of potential cold starts. These can impact the initial response time for dynamic parts of your site.
  • Performance Budgeting: Factor in potential cold start times into your performance targets, especially for critical user interactions relying on server-side execution. Optimize functions for quick boot-up where possible.
  • Configuration: Static Pages deploys need no wrangler.toml — manage redirects via public/_redirects, custom headers via public/_headers (Pages reads both natively), and environment variables in the Pages dashboard.
.github/workflows/deploy-cloudflare.yml
name: Deploy to Cloudflare Pages
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version-file: '.nvmrc'
# No `version` input: pnpm/action-setup reads the pinned version from the
# `packageManager` field in package.json (and errors if both are set)
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
run_install: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build site
run: pnpm run build
env:
NODE_ENV: production
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=your-project-name

Cloudflare Pages needs no wrangler.toml for a static deploy. Redirects live in public/_redirects and custom headers in public/_headers — Pages reads both natively — and environment variables are managed in the Pages dashboard.

vercel.json
{
"buildCommand": "pnpm run build",
"outputDirectory": "dist",
"framework": "astro",
"rewrites": [
{
"source": "/(.*)",
"destination": "/$1"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
}
]
},
{
"source": "/_astro/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"functions": {
"api/*.ts": {
"maxDuration": 10
}
}
}
netlify.toml
[build]
command = "pnpm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "24"
[[redirects]]
from = "/old-path"
to = "/new-path"
status = 301
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
[[headers]]
for = "/_astro/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
# Edge Functions
[[edge_functions]]
path = "/api/*"
function = "api"
# Deploy contexts
[context.production]
environment = { NODE_ENV = "production" }
[context.deploy-preview]
environment = { NODE_ENV = "preview" }
[context.branch-deploy]
environment = { NODE_ENV = "development" }

The template’s real .env.example (abridged comments — see the file for the full guidance). SITE_URL is required for production builds: the build fails if it is unset or still contains a placeholder value.

.env.example
# Copy this file to .env and fill in your values
# DO NOT commit .env to version control
# Site Configuration
# SITE_URL (required for production builds): the canonical origin URL, no trailing slash.
# SITE_URL=https://your-username.github.io
# Alternatively, use PUBLIC_SITE_URL (exposed to client-side code via Astro):
PUBLIC_SITE_URL=http://localhost:4321
# Deployment Target
# Set to "gh-pages" when deploying to GitHub Pages (derives the base path from
# the package.json "name" field). Leave unset for root deployments.
# DEPLOY_TARGET=gh-pages
# Analytics (optional) — not yet implemented; uncomment when adding analytics support.
# PUBLIC_PLAUSIBLE_DOMAIN="your-domain.com"
# PUBLIC_FATHOM_SITE_ID="YOUR_FATHOM_SITE_ID"
# Contact Information
PUBLIC_CONTACT_EMAIL=hello@example.com
PUBLIC_CONTACT_PHONE=+1234567890
PUBLIC_CONTACT_PHONE_DISPLAY="+1 (234) 567-890"
PUBLIC_CONTACT_LOCATION="San Francisco, CA"
PUBLIC_CONTACT_TIMEZONE="Mon-Fri, 9AM-6PM PST"
# Social Media Links
PUBLIC_SOCIAL_GITHUB=https://github.com/example
PUBLIC_SOCIAL_LINKEDIN=https://linkedin.com/company/example
PUBLIC_SOCIAL_TWITTER=https://twitter.com/example

Type-safe environment handling is split across three places (there is no src/config/env.ts):

  • astro.config.mjs — the env.schema block declares every PUBLIC_* variable via envField (astro:env, ADR-050) with demo defaults; components import typed values from astro:env/client.
  • scripts/src/validate-env.ts — the env:validate prebuild step (part of pnpm run build) fails the build when SITE_URL/PUBLIC_SITE_URL is missing or still a placeholder, the one check astro:env cannot express.
  • src/config.ts — hand-edited site metadata and links (siteMetadata, siteLinks, socialLinks) that you update when cloning, separate from environment variables.

The deployment-relevant excerpt of the template’s real astro.config.mjs. The sitemap comes from the @astrojs/sitemap integration (there is no build.sitemap option), fonts are served by the Astro Fonts API (ADR-053), and the PUBLIC_* surface is validated by the env.schema block (astro:env, ADR-050) — elided below.

// astro.config.mjs (excerpt)
import { readFileSync } from "node:fs";
import mdx from "@astrojs/mdx";
import preact from "@astrojs/preact";
import sitemap from "@astrojs/sitemap";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig, envField, fontProviders } from "astro/config";
// Read package name to derive GitHub Pages base path automatically
const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf-8"));
const isGhPages = process.env.DEPLOY_TARGET === "gh-pages";
// Site URL: require explicit configuration for builds, default to localhost for dev.
// The validate-env.ts prebuild script catches misconfiguration before we get here.
const envSite = process.env.SITE_URL || process.env.PUBLIC_SITE_URL;
const isDev = process.argv.slice(2).includes("dev");
const site = envSite || (isDev ? "http://localhost:4321" : undefined);
if (!site) {
throw new Error(
"SITE_URL is required for production builds. " +
"Set SITE_URL or PUBLIC_SITE_URL in your environment or .env file.",
);
}
// Base path: derive from package.json name for GH Pages, root for all others
const base = isGhPages ? `/${pkg.name}` : "/";
export default defineConfig({
site,
base,
trailingSlash: "always",
compressHTML: "jsx",
prefetch: true,
// fonts: [...] — self-hosted local providers via the Astro Fonts API (ADR-053)
// env: { schema: { ... } } — typed PUBLIC_* variables via envField (ADR-050)
integrations: [mdx(), sitemap(), preact()],
vite: {
plugins: [tailwindcss()],
build: {
cssMinify: "lightningcss",
},
},
output: "static",
build: {
inlineStylesheets: "auto",
},
});

Note: An API health-check endpoint like this requires an SSR adapter — the starter ships static output. For a static site, point uptime checks at a real page URL instead.

// api/health.ts - Health check endpoint
export async function GET() {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
checks: {
database: await checkDatabase(),
cache: await checkCache(),
storage: await checkStorage(),
},
};
const isHealthy = Object.values(checks.checks).every(check => check.status === 'ok');
return new Response(JSON.stringify(checks), {
status: isHealthy ? 200 : 503,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
},
});
}
async function checkDatabase() {
try {
// Your database check logic
return { status: 'ok', latency: 10 };
} catch (error) {
return { status: 'error', message: error.message };
}
}
async function checkCache() {
try {
// Your cache check logic
return { status: 'ok', latency: 5 };
} catch (error) {
return { status: 'error', message: error.message };
}
}
async function checkStorage() {
try {
// Your storage check logic
return { status: 'ok', available: '10GB' };
} catch (error) {
return { status: 'error', message: error.message };
}
}
src/lib/error-tracking.ts
import * as Sentry from '@sentry/browser';
import { BrowserTracing } from '@sentry/tracing';
// Initialize Sentry
if (import.meta.env.PUBLIC_SENTRY_DSN && import.meta.env.PROD) {
Sentry.init({
dsn: import.meta.env.PUBLIC_SENTRY_DSN,
environment: import.meta.env.PUBLIC_ENVIRONMENT || 'production',
integrations: [
new BrowserTracing(),
],
tracesSampleRate: 0.1, // 10% of transactions
// Filter out known issues
beforeSend(event, hint) {
// Ignore specific errors
if (event.exception?.values?.[0]?.type === 'NetworkError') {
return null;
}
// Remove sensitive data
if (event.request?.cookies) {
delete event.request.cookies;
}
return event;
},
});
}
// Error boundary component
export function captureError(error: Error, context?: Record<string, any>) {
console.error('Application error:', error);
if (import.meta.env.PROD) {
Sentry.captureException(error, {
contexts: {
custom: context,
},
});
}
}
// Performance monitoring
export function measurePerformance(name: string, fn: () => void | Promise<void>) {
const transaction = Sentry.startTransaction({ name });
try {
const result = fn();
if (result instanceof Promise) {
return result.finally(() => transaction.finish());
}
transaction.finish();
return result;
} catch (error) {
transaction.setStatus('internal_error');
transaction.finish();
throw error;
}
}
src/components/Analytics.astro
---
const { analyticsId, gtmId, enableAnalytics } = Astro.props;
---
{enableAnalytics && (
<>
<!-- Google Tag Manager -->
{gtmId && (
<>
<script is:inline>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{gtmId}');
</script>
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
height="0"
width="0"
style="display:none;visibility:hidden"
></iframe>
</noscript>
</>
)}
<!-- Plausible Analytics (Privacy-focused alternative) -->
{analyticsId && !gtmId && (
<script
defer
data-domain={analyticsId}
src="https://plausible.io/js/script.js"
></script>
)}
<!-- Custom analytics events -->
<script>
// Track Core Web Vitals
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
// Google Analytics
if (window.gtag) {
gtag('event', metric.name, {
value: Math.round(metric.value),
metric_id: metric.id,
metric_value: metric.value,
metric_delta: metric.delta,
});
}
// Plausible
if (window.plausible) {
plausible('Web Vitals', {
props: {
metric: metric.name,
value: Math.round(metric.value),
},
});
}
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
</script>
</>
)}
// src/lib/rum.ts - Real User Monitoring
class RUM {
private metrics: Record<string, any> = {};
private observer: PerformanceObserver | null = null;
constructor(private endpoint: string) {
this.initializeObservers();
}
private initializeObservers() {
// Navigation timing
if ('performance' in window && 'PerformanceObserver' in window) {
// Observe long tasks
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.track('long-task', {
duration: entry.duration,
startTime: entry.startTime,
});
}
}
});
this.observer.observe({ entryTypes: ['longtask'] });
// Track navigation timing
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
this.track('navigation', {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
domInteractive: navigation.domInteractive - navigation.fetchStart,
ttfb: navigation.responseStart - navigation.requestStart,
});
});
}
}
track(eventName: string, data: Record<string, any>) {
const event = {
event: eventName,
data,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
connection: (navigator as any).connection?.effectiveType,
};
// Batch events
this.metrics[eventName] = this.metrics[eventName] || [];
this.metrics[eventName].push(event);
// Send in batches
if (this.metrics[eventName].length >= 10) {
this.flush(eventName);
}
}
private flush(eventName?: string) {
const eventsToSend = eventName
? { [eventName]: this.metrics[eventName] }
: this.metrics;
if (Object.keys(eventsToSend).length === 0) return;
navigator.sendBeacon(this.endpoint, JSON.stringify(eventsToSend));
// Clear sent events
if (eventName) {
this.metrics[eventName] = [];
} else {
this.metrics = {};
}
}
destroy() {
// Flush remaining events
this.flush();
// Clean up observer
if (this.observer) {
this.observer.disconnect();
}
}
}
// Initialize RUM
export const rum = new RUM('/api/rum');
// Ensure events are sent before page unload
window.addEventListener('beforeunload', () => {
rum.destroy();
});
.github/workflows/backup.yml
name: Automated Backup
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
workflow_dispatch:
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0 # Get full history
- name: Create backup archive
run: |
BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S)"
tar -czf "${BACKUP_NAME}.tar.gz" \
--exclude=node_modules \
--exclude=.git \
--exclude=dist \
.
- name: Upload to backup storage
uses: actions/upload-artifact@v7
with:
name: site-backup-${{ github.run_id }}
path: backup-*.tar.gz
retention-days: 30
# Optional: Upload to external storage
- name: Upload to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 cp backup-*.tar.gz s3://your-backup-bucket/backups/
scripts/backup-content.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { createGzip } from 'zlib';
const execAsync = promisify(exec);
async function backupContent() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = `backups/${timestamp}`;
// Create backup directory
await execAsync(`mkdir -p ${backupDir}`);
// Backup content files
await execAsync(`cp -r src/content ${backupDir}/`);
// Backup images
await execAsync(`cp -r public/images ${backupDir}/`);
// Backup configuration
await execAsync(`cp -r *.config.* ${backupDir}/`);
// Create compressed archive
const tarFile = `backup-${timestamp}.tar.gz`;
await execAsync(`tar -czf ${tarFile} ${backupDir}`);
// Upload to cloud storage
if (process.env.BACKUP_BUCKET) {
console.log('Uploading to cloud storage...');
// Your cloud storage upload logic
}
// Clean up old backups (keep last 7)
const { stdout } = await execAsync('ls -1 backup-*.tar.gz | sort -r | tail -n +8');
const oldBackups = stdout.trim().split('\n').filter(Boolean);
for (const backup of oldBackups) {
await execAsync(`rm ${backup}`);
}
console.log(`✅ Backup created: ${tarFile}`);
}
backupContent().catch(console.error);