Cloudflare Pages Samples
Cloudflare Pages full-stack web application examples with Functions, edge deployment, and modern web frameworks integration
Key Facts
- Category
- Cloud Computing
- Items
- 3
- Format Families
- sample
Sample Overview
Cloudflare Pages full-stack web application examples with Functions, edge deployment, and modern web frameworks integration This sample set belongs to Cloud Computing and can be used to test related workflows inside Elysia Tools.
💻 Cloudflare Pages Hello World javascript
🟢 simple
Getting started with Cloudflare Pages including static site deployment and serverless functions
// Cloudflare Pages Hello World
// Complete setup with static site and functions
// 1. Public/index.html - Main HTML file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare Pages Hello World</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
border-radius: 10px;
padding: 2rem;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
max-width: 500px;
width: 90%;
text-align: center;
}
h1 {
color: #667eea;
margin-bottom: 1rem;
font-size: 2rem;
}
.logo {
width: 80px;
height: 80px;
margin: 0 auto 1rem;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 2rem;
font-weight: bold;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.feature {
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.feature:hover {
border-color: #667eea;
transform: translateY(-2px);
}
.btn {
display: inline-block;
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
text-decoration: none;
border-radius: 6px;
margin: 0.5rem;
transition: transform 0.2s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn:hover {
transform: translateY(-2px);
}
.btn-secondary {
background: #6c757d;
}
.info {
background: #e3f2fd;
border-left: 4px solid #667eea;
padding: 1rem;
margin: 1rem 0;
text-align: left;
}
.loading {
display: none;
color: #667eea;
}
.response {
margin-top: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 6px;
text-align: left;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="logo">☁️</div>
<h1>Cloudflare Pages</h1>
<p>Deploy static sites and serverless functions at the edge</p>
<div class="feature-grid">
<div class="feature">
<h3>⚡ Lightning Fast</h3>
<p>Global CDN with automatic HTTPS</p>
</div>
<div class="feature">
<h3>🚀 Zero Config</h3>
<p>Deploy from Git in seconds</p>
</div>
<div class="feature">
<h3>📦 Functions</h3>
<p>Serverless edge computing</p>
</div>
<div class="feature">
<h3>🔒 Secure</h3>
<p>DDoS protection and WAF</p>
</div>
</div>
<div class="info">
<strong>Try the serverless functions:</strong><br>
This page includes several API endpoints powered by Cloudflare Pages Functions.
</div>
<button class="btn" onclick="testAPI('/api/hello')">
Test Hello API
</button>
<button class="btn" onclick="testAPI('/api/time')">
Get Server Time
</button>
<button class="btn" onclick="testAPI('/api/info')">
Get Request Info
</button>
<button class="btn btn-secondary" onclick="testAPI('/api/echo', {message: 'Hello from Cloudflare Pages!'})">
Test Echo API
</button>
<div class="loading" id="loading">Loading...</div>
<div class="response" id="response" style="display: none;"></div>
</div>
<script>
async function testAPI(endpoint, data = null) {
const loading = document.getElementById('loading');
const response = document.getElementById('response');
loading.style.display = 'block';
response.style.display = 'none';
try {
const options = {
method: data ? 'POST' : 'GET',
headers: {
'Content-Type': 'application/json',
},
};
if (data) {
options.body = JSON.stringify(data);
}
const res = await fetch(endpoint, options);
const result = await res.json();
response.textContent = JSON.stringify(result, null, 2);
response.style.display = 'block';
} catch (error) {
response.textContent = `Error: ${error.message}`;
response.style.display = 'block';
} finally {
loading.style.display = 'none';
}
}
// Get initial info
window.addEventListener('load', () => {
testAPI('/api/info');
});
</script>
</body>
</html>
// 2. Functions/api/hello.js - Simple hello function
export function onRequest(context) {
return new Response(
JSON.stringify({
message: "Hello from Cloudflare Pages Functions! 🎉",
method: context.request.method,
url: context.request.url,
timestamp: new Date().toISOString(),
environment: context.env.CF_PAGES ? 'production' : 'preview'
}),
{
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60"
}
}
);
}
// 3. Functions/api/time.js - Server time function
export function onRequest() {
const now = new Date();
return new Response(
JSON.stringify({
serverTime: now.toISOString(),
unixTimestamp: Math.floor(now.getTime() / 1000),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
formatted: {
date: now.toLocaleDateString(),
time: now.toLocaleTimeString(),
datetime: now.toLocaleString()
}
}),
{
headers: {
"Content-Type": "application/json"
}
}
);
}
// 4. Functions/api/info.js - Request information
export function onRequest(context) {
const request = context.request;
const url = new URL(request.url);
return new Response(
JSON.stringify({
request: {
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
cf: request.cf
},
environment: {
pages: context.env.CF_PAGES ? true : false,
branch: context.env.CF_PAGES_BRANCH || 'unknown',
commitSha: context.env.CF_PAGES_COMMIT_SHA || 'unknown'
},
location: {
country: request.cf.country,
city: request.cf.city,
colo: request.cf.colo,
timezone: request.cf.timezone
},
device: {
mobile: request.cf.device.mobile,
model: request.cf.device.model,
brand: request.cf.device.brand
}
}),
{
headers: {
"Content-Type": "application/json"
}
}
);
}
// 5. Functions/api/echo.js - Echo function
export async function onRequest(context) {
const request = context.request;
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { "Content-Type": "application/json" } }
);
}
try {
const body = await request.json();
return new Response(
JSON.stringify({
echo: body,
receivedAt: new Date().toISOString(),
requestInfo: {
method: request.method,
contentType: request.headers.get('content-type'),
userAgent: request.headers.get('user-agent')
}
}),
{
headers: {
"Content-Type": "application/json"
}
}
);
} catch (error) {
return new Response(
JSON.stringify({
error: 'Invalid JSON',
message: error.message
}),
{
status: 400,
headers: { "Content-Type": "application/json" }
}
);
}
}
// 6. Functions/api/proxy.js - API proxy example
export async function onRequest(context) {
const url = new URL(context.request.url);
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response(
JSON.stringify({
error: 'Missing url parameter',
usage: 'GET /api/proxy?url=https://api.example.com/data'
}),
{
status: 400,
headers: { "Content-Type": "application/json" }
}
);
}
try {
const response = await fetch(targetUrl);
const data = await response.text();
return new Response(data, {
status: response.status,
headers: {
"Content-Type": response.headers.get('content-type') || 'text/plain',
"X-Proxy-For": targetUrl
}
});
} catch (error) {
return new Response(
JSON.stringify({
error: 'Proxy request failed',
message: error.message,
targetUrl
}),
{
status: 500,
headers: { "Content-Type": "application/json" }
}
);
}
}
// 7. Functions/api/image.js - Image processing
export async function onRequest(context) {
const request = context.request;
const url = new URL(request.url);
const imageUrl = url.searchParams.get('url');
const width = parseInt(url.searchParams.get('width')) || 800;
const quality = parseInt(url.searchParams.get('quality')) || 80;
if (!imageUrl) {
return new Response(
JSON.stringify({
error: 'Missing url parameter',
usage: 'GET /api/image?url=https://example.com/image.jpg&width=800&quality=80'
}),
{
status: 400,
headers: { "Content-Type": "application/json" }
}
);
}
try {
// Fetch original image
const imageResponse = await fetch(imageUrl);
if (!imageResponse.ok) {
throw new Error(`Failed to fetch image: ${imageResponse.status}`);
}
const originalImage = await imageResponse.arrayBuffer();
// For now, just pass through the image
// In a real application, you could use Cloudflare's Image Resizing
return new Response(originalImage, {
headers: {
"Content-Type": imageResponse.headers.get('content-type') || 'image/jpeg',
"Cache-Control": "public, max-age=31536000" // 1 year
}
});
} catch (error) {
return new Response(
JSON.stringify({
error: 'Image processing failed',
message: error.message,
imageUrl
}),
{
status: 500,
headers: { "Content-Type": "application/json" }
}
);
}
}
💻 Advanced Pages Functions javascript
🟡 intermediate
Complex serverless functions with middleware, database integration, and authentication
// Cloudflare Pages Advanced Functions
// Complete API with middleware, auth, and database integration
// 1. Functions/_middleware.js - Global middleware
import { getCookie, setCookie } from 'https://deno.land/[email protected]/http/cookie.ts';
export async function onRequest(context) {
const { request, env, next, params } = context;
const url = new URL(request.url);
const pathname = url.pathname;
// CORS middleware
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Header',
};
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// Authentication middleware for protected routes
if (pathname.startsWith('/api/protected')) {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response(
JSON.stringify({ error: 'Missing or invalid authorization header' }),
{
status: 401,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
}
);
}
const token = authHeader.replace('Bearer ', '');
try {
const user = await verifyJWT(token, env.JWT_SECRET);
context.user = user;
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid or expired token' }),
{
status: 401,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
}
);
}
}
// Rate limiting middleware
if (pathname.startsWith('/api/')) {
const clientIP = request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For');
const key = `rate_limit:${clientIP}:${pathname}`;
const limit = await env.RATE_LIMITER.get(key);
const current = parseInt(limit?.value || '0');
if (current > 100) { // 100 requests per minute
return new Response(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': '60',
...corsHeaders
}
}
);
}
await env.RATE_LIMITER.put(key, (current + 1).toString(), { expirationTtl: 60 });
}
// Continue to the next handler
const response = await next();
// Add CORS headers to response
Object.entries(corsHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
}
// 2. Functions/api/auth/login.js - Authentication
export async function onRequest(context) {
const { request, env } = context;
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
try {
const { email, password } = await request.json();
// Validate input
if (!email || !password) {
return new Response(
JSON.stringify({ error: 'Email and password are required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Find user in database
const user = await env.DB.prepare(
'SELECT id, email, password_hash, name FROM users WHERE email = ?'
).bind(email).first();
if (!user) {
return new Response(
JSON.stringify({ error: 'Invalid credentials' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// Verify password (using a simple hash for demo - use bcrypt in production)
const passwordHash = await hashString(password);
if (passwordHash !== user.password_hash) {
return new Response(
JSON.stringify({ error: 'Invalid credentials' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// Generate JWT
const token = await generateJWT(
{ userId: user.id, email: user.email },
env.JWT_SECRET,
'24h'
);
// Update last login
await env.DB.prepare(
'UPDATE users SET last_login = ? WHERE id = ?'
).bind(new Date().toISOString(), user.id).run();
return new Response(
JSON.stringify({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
name: user.name
}
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Set-Cookie': `auth_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`
}
}
);
} catch (error) {
return new Response(
JSON.stringify({
error: 'Internal server error',
message: error.message
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
// 3. Functions/api/posts/index.js - Posts CRUD API
export async function onRequest(context) {
const { request, env, user } = context;
if (request.method === 'GET') {
// Get posts with pagination
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page')) || 1;
const limit = parseInt(url.searchParams.get('limit')) || 10;
const offset = (page - 1) * limit;
try {
const posts = await env.DB.prepare(`
SELECT p.*, u.name as author_name, u.email as author_email
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.published = true
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
`).bind(limit, offset).all();
const totalResult = await env.DB.prepare(
'SELECT COUNT(*) as total FROM posts WHERE published = true'
).first();
return new Response(
JSON.stringify({
posts: posts.results,
pagination: {
page,
limit,
total: totalResult.total,
pages: Math.ceil(totalResult.total / limit)
}
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to fetch posts' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
if (request.method === 'POST') {
// Create new post (protected route)
if (!user) {
return new Response(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
try {
const { title, content, excerpt, tags } = await request.json();
// Validate input
if (!title || !content) {
return new Response(
JSON.stringify({ error: 'Title and content are required' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const postId = crypto.randomUUID();
const now = new Date().toISOString();
await env.DB.prepare(`
INSERT INTO posts (id, title, content, excerpt, tags, author_id, created_at, updated_at, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
postId,
title,
content,
excerpt || null,
tags ? JSON.stringify(tags) : null,
user.userId,
now,
now,
false // Draft by default
).run();
const post = await env.DB.prepare(
'SELECT * FROM posts WHERE id = ?'
).bind(postId).first();
return new Response(
JSON.stringify({
message: 'Post created successfully',
post
}),
{
status: 201,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to create post' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
// 4. Functions/api/posts/[id].js - Individual post operations
export async function onRequest(context) {
const { request, env, params, user } = context;
const postId = params.id;
if (request.method === 'GET') {
try {
const post = await env.DB.prepare(`
SELECT p.*, u.name as author_name, u.email as author_email
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.id = ? AND (p.published = true OR p.author_id = ?)
`).bind(postId, user?.userId).first();
if (!post) {
return new Response(
JSON.stringify({ error: 'Post not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
// Increment view count
await env.DB.prepare(
'UPDATE posts SET views = views + 1 WHERE id = ?'
).bind(postId).run();
return new Response(
JSON.stringify({ post }),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to fetch post' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
if (request.method === 'PUT') {
// Update post (protected route)
if (!user) {
return new Response(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
try {
const existingPost = await env.DB.prepare(
'SELECT author_id FROM posts WHERE id = ?'
).bind(postId).first();
if (!existingPost) {
return new Response(
JSON.stringify({ error: 'Post not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
if (existingPost.author_id !== user.userId) {
return new Response(
JSON.stringify({ error: 'Unauthorized to edit this post' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
const { title, content, excerpt, tags, published } = await request.json();
await env.DB.prepare(`
UPDATE posts
SET title = ?, content = ?, excerpt = ?, tags = ?, published = ?, updated_at = ?
WHERE id = ?
`).bind(
title,
content,
excerpt || null,
tags ? JSON.stringify(tags) : null,
published || false,
new Date().toISOString(),
postId
).run();
const updatedPost = await env.DB.prepare(
'SELECT * FROM posts WHERE id = ?'
).bind(postId).first();
return new Response(
JSON.stringify({
message: 'Post updated successfully',
post: updatedPost
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to update post' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
if (request.method === 'DELETE') {
// Delete post (protected route)
if (!user) {
return new Response(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
try {
const existingPost = await env.DB.prepare(
'SELECT author_id FROM posts WHERE id = ?'
).bind(postId).first();
if (!existingPost) {
return new Response(
JSON.stringify({ error: 'Post not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
if (existingPost.author_id !== user.userId) {
return new Response(
JSON.stringify({ error: 'Unauthorized to delete this post' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
await env.DB.prepare('DELETE FROM posts WHERE id = ?').bind(postId).run();
return new Response(
JSON.stringify({ message: 'Post deleted successfully' }),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to delete post' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
// 5. Functions/api/upload.js - File upload with image processing
export async function onRequest(context) {
const { request, env, user } = context;
if (!user) {
return new Response(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
if (request.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{ status: 405, headers: { 'Content-Type': 'application/json' } }
);
}
try {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return new Response(
JSON.stringify({ error: 'No file provided' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return new Response(
JSON.stringify({ error: 'Invalid file type. Only images are allowed.' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Validate file size (10MB max)
if (file.size > 10 * 1024 * 1024) {
return new Response(
JSON.stringify({ error: 'File too large. Maximum size is 10MB.' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Generate unique filename
const fileExtension = file.name.split('.').pop();
const fileName = `${user.userId}_${Date.now()}.${fileExtension}`;
// Upload to R2 (or any storage service)
const uploadUrl = `https://${env.R2_BUCKET_NAME}.${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`;
await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD'
},
body: file
});
// Create database record
const fileId = crypto.randomUUID();
await env.DB.prepare(`
INSERT INTO files (id, user_id, filename, original_name, file_type, file_size, upload_url, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
fileId,
user.userId,
fileName,
file.name,
file.type,
file.size,
uploadUrl,
new Date().toISOString()
).run();
return new Response(
JSON.stringify({
message: 'File uploaded successfully',
file: {
id: fileId,
filename: fileName,
originalName: file.name,
fileType: file.type,
fileSize: file.size,
uploadUrl
}
}),
{
status: 201,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
return new Response(
JSON.stringify({ error: 'Failed to upload file' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
// 6. Utility functions
async function generateJWT(payload, secret, expiresIn = '1h') {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const exp = now + (expiresIn === '24h' ? 86400 : 3600);
const encodedHeader = btoa(JSON.stringify(header));
const encodedPayload = btoa(JSON.stringify({ ...payload, iat: now, exp }));
const signature = await hmacSha256(
`${encodedHeader}.${encodedPayload}`,
secret
);
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
async function verifyJWT(token, secret) {
const [encodedHeader, encodedPayload, signature] = token.split('.');
const expectedSignature = await hmacSha256(
`${encodedHeader}.${encodedPayload}`,
secret
);
if (signature !== expectedSignature) {
throw new Error('Invalid signature');
}
const payload = JSON.parse(atob(encodedPayload));
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token expired');
}
return payload;
}
async function hmacSha256(message, secret) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(message);
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, messageData);
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
async function hashString(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hash = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
💻 Static Site with Modern Frameworks
🟡 intermediate
Static site generation with Astro, React, Vue, and optimized build processes
// Cloudflare Pages Static Site with Astro
// astro.config.mjs - Astro configuration
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import { imagetools } from 'astro-imagetools';
export default defineConfig({
// Build options
output: 'static',
trailingSlash: 'never',
// Integrations
integrations: [
react({
jsxImportSource: 'react'
}),
tailwind({
applyBaseStyles: false
}),
sitemap({
changefreq: 'weekly',
priority: 0.7,
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'fr', 'de', 'pt', 'ru']
}
}),
imagetools({
defaultDirectives: (url) => {
// Optimize images by default
return new URLSearchParams({
format: 'webp',
quality: '80'
});
}
})
],
// Site configuration
site: 'https://your-site.pages.dev',
base: '/',
// Markdown options
markdown: {
shikiConfig: {
theme: 'github-dark',
langs: ['javascript', 'typescript', 'jsx', 'tsx', 'css', 'html', 'json', 'bash']
},
remarkPlugins: [
'remark-gfm',
'remark-toc'
],
rehypePlugins: [
'rehype-slug',
'rehype-autolink-headings'
]
},
// Vite configuration
vite: {
build: {
minify: 'terser',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
astro: ['astro']
}
}
}
},
optimizeDeps: {
exclude: ['@astroimage/tailwindcss']
}
},
// Experimental features
experimental: {
assets: true,
buildElapsed: true
}
});
// src/pages/index.astro - Homepage
---
import Layout from '../layouts/Layout.astro';
import Hero from '../components/Hero.astro';
import Features from '../components/Features.astro';
import RecentPosts from '../components/RecentPosts.astro';
import Newsletter from '../components/Newsletter.astro';
const pageTitle = 'Welcome to My Blog';
const pageDescription = 'A modern blog built with Astro and Cloudflare Pages';
// Get recent posts
const recentPosts = await Astro.glob('../content/blog/*.md');
const sortedPosts = recentPosts
.filter(post => post.frontmatter.published)
.sort((a, b) => new Date(b.frontmatter.pubDate) - new Date(a.frontmatter.pubDate))
.slice(0, 3);
---
<Layout title={pageTitle} description={pageDescription}>
<main>
<Hero />
<Features />
<RecentPosts posts={sortedPosts} />
<Newsletter />
</main>
</Layout>
<style>
main {
min-height: 100vh;
}
/* Ensure smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
// src/layouts/Layout.astro - Main layout
---
export interface Props {
title: string;
description?: string;
image?: string;
noIndex?: boolean;
}
const {
title,
description = 'A modern blog built with Astro and Cloudflare Pages',
image = '/images/og-default.jpg',
noIndex = false
}: Props = Astro.props;
// Get site data
const { site } = Astro.config;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
// Social media
const socialLinks = [
{ name: 'Twitter', href: 'https://twitter.com/yourusername', icon: '🐦' },
{ name: 'GitHub', href: 'https://github.com/yourusername', icon: '🐙' },
{ name: 'LinkedIn', href: 'https://linkedin.com/in/yourusername', icon: '💼' }
];
// Navigation
const navigation = [
{ name: 'Home', href: '/' },
{ name: 'Blog', href: '/blog' },
{ name: 'About', href: '/about' },
{ name: 'Contact', href: '/contact' }
];
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- SEO Meta -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
{noIndex && <meta name="robots" content="noindex, nofollow" />}
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.site)} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalURL} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(image, Astro.site)} />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<!-- Tailwind CSS -->
<style>
:root {
--font-family: 'Inter', system-ui, sans-serif;
}
</style>
</head>
<body class="bg-gray-50 text-gray-900 antialiased">
<!-- Skip to content link -->
<a href="#main" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-blue-600 text-white px-4 py-2 rounded-md">
Skip to content
</a>
<!-- Navigation -->
<header class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<!-- Logo -->
<div class="flex-shrink-0">
<a href="/" class="flex items-center space-x-2">
<div class="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
<span class="text-white font-bold text-sm">M</span>
</div>
<span class="text-xl font-bold text-gray-900">MyBlog</span>
</a>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
{navigation.map(item => (
<a
href={item.href}
class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium transition-colors duration-200"
>
{item.name}
</a>
))}
</div>
<!-- Social Links -->
<div class="flex items-center space-x-4">
{socialLinks.map(link => (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
class="text-gray-400 hover:text-gray-600 transition-colors duration-200"
aria-label={link.name}
>
<span class="text-xl">{link.icon}</span>
</a>
))}
<!-- Mobile menu button -->
<button
type="button"
class="md:hidden p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100"
aria-label="Toggle menu"
id="mobile-menu-button"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
<!-- Mobile Navigation -->
<div class="hidden md:hidden" id="mobile-menu">
<div class="px-2 pt-2 pb-3 space-y-1 bg-white border-t border-gray-200">
{navigation.map(item => (
<a
href={item.href}
class="block px-3 py-2 rounded-md text-base font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50"
>
{item.name}
</a>
))}
</div>
</div>
</nav>
</header>
<!-- Main Content -->
<main id="main">
<slot />
</main>
<!-- Footer -->
<footer class="bg-gray-900 text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Company Info -->
<div class="col-span-1 md:col-span-2">
<div class="flex items-center space-x-2 mb-4">
<div class="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
<span class="text-white font-bold text-sm">M</span>
</div>
<span class="text-xl font-bold">MyBlog</span>
</div>
<p class="text-gray-300 max-w-md">
A modern blog platform built with Astro and deployed on Cloudflare Pages.
Fast, secure, and sustainable web hosting at the edge.
</p>
</div>
<!-- Quick Links -->
<div>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
Quick Links
</h3>
<ul class="space-y-2">
{navigation.map(item => (
<li>
<a href={item.href} class="text-gray-300 hover:text-white transition-colors">
{item.name}
</a>
</li>
))}
</ul>
</div>
<!-- Legal -->
<div>
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
Legal
</h3>
<ul class="space-y-2">
<li>
<a href="/privacy" class="text-gray-300 hover:text-white transition-colors">
Privacy Policy
</a>
</li>
<li>
<a href="/terms" class="text-gray-300 hover:text-white transition-colors">
Terms of Service
</a>
</li>
</ul>
</div>
</div>
<!-- Bottom bar -->
<div class="mt-8 pt-8 border-t border-gray-800">
<div class="flex flex-col md:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
© {new Date().getFullYear()} MyBlog. All rights reserved.
</p>
<p class="text-gray-400 text-sm mt-2 md:mt-0">
Powered by <a href="https://astro.build" target="_blank" class="hover:text-white transition-colors">Astro</a> &
<a href="https://pages.cloudflare.com" target="_blank" class="hover:text-white transition-colors">Cloudflare Pages</a>
</p>
</div>
</div>
</div>
</footer>
<!-- Scripts -->
<script>
// Mobile menu toggle
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
// Close menu when clicking outside
document.addEventListener('click', (event) => {
if (!mobileMenuButton.contains(event.target) && !mobileMenu.contains(event.target)) {
mobileMenu.classList.add('hidden');
}
});
}
// Theme toggle (optional)
const initTheme = () => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.documentElement.classList.add('dark');
}
};
initTheme();
// Service Worker registration for PWA
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
}
</script>
</body>
</html>
// src/pages/blog/[...slug].astro - Dynamic blog routes
---
import Layout from '../../layouts/Layout.astro';
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post }: { post: CollectionEntry<'blog'> } = Astro.props;
// Get related posts
const relatedPosts = await getCollection('blog', ({ slug }) => slug !== post.slug);
const filteredRelated = relatedPosts
.filter(relatedPost => {
// Find posts with similar tags
const commonTags = post.data.tags?.filter(tag =>
relatedPost.data.tags?.includes(tag)
);
return (commonTags?.length || 0) > 0;
})
.slice(0, 3);
// Table of contents
const { Content, headings } = await post.render();
---
<Layout
title={post.data.title}
description={post.data.description}
image={post.data.image}
>
<article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Article Header -->
<header class="mb-12">
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-4">
<time datetime={post.data.pubDate}>
{new Date(post.data.pubDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
<span>•</span>
<span>{Math.ceil(post.data.readingTime || 5)} min read</span>
{post.data.updatedDate && (
<>
<span>•</span>
<span>Updated {new Date(post.data.updatedDate).toLocaleDateString()}</span>
</>
)}
</div>
<h1 class="text-4xl font-bold text-gray-900 mb-6 leading-tight">
{post.data.title}
</h1>
{post.data.description && (
<p class="text-xl text-gray-600 mb-8 leading-relaxed">
{post.data.description}
</p>
)}
{/* Author and tags */}
<div class="flex items-center justify-between flex-wrap gap-4">
<div class="flex items-center space-x-3">
{post.data.authorImage && (
<img
src={post.data.authorImage}
alt={post.data.author}
class="w-12 h-12 rounded-full object-cover"
/>
)}
<div>
<p class="font-medium text-gray-900">{post.data.author}</p>
<p class="text-sm text-gray-500">{post.data.authorTitle || 'Author'}</p>
</div>
</div>
{post.data.tags && post.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2">
{post.data.tags.map(tag => (
<span class="px-3 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full">
{tag}
</span>
))}
</div>
)}
</div>
</header>
{/* Table of Contents */}
{headings.length > 0 && (
<aside class="mb-12 p-6 bg-gray-50 rounded-lg">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Table of Contents</h2>
<nav>
<ul class="space-y-2">
{headings.map(heading => (
<li>
<a
href={`#${heading.slug}`}
class="text-gray-600 hover:text-gray-900 text-sm transition-colors"
style={`padding-left: ${(heading.depth - 1) * 16}px`}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
</aside>
)}
{/* Article Content */}
<div class="prose prose-lg max-w-none prose-headings:scroll-mt-20">
<Content />
</div>
{/* Article Footer */}
<footer class="mt-16 pt-8 border-t border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div class="text-sm text-gray-500">
Published on {new Date(post.data.pubDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
{/* Share buttons */}
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-500">Share:</span>
{[
{
name: 'Twitter',
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(post.data.title)}&url=${encodeURIComponent(Astro.url.pathname)}`,
icon: '🐦'
},
{
name: 'LinkedIn',
href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(Astro.url.pathname)}`,
icon: '💼'
},
{
name: 'Copy Link',
href: '#',
icon: '🔗',
action: 'copy'
}
].map(social => (
<a
href={social.href}
target={social.action !== 'copy' ? '_blank' : undefined}
rel={social.action !== 'copy' ? 'noopener noreferrer' : undefined}
class="text-gray-400 hover:text-gray-600 transition-colors"
onClick={social.action === 'copy' ? 'navigator.clipboard.writeText(window.location.href); return false;' : undefined}
aria-label={social.name}
>
<span class="text-lg">{social.icon}</span>
</a>
))}
</div>
</div>
</footer>
{/* Related Posts */}
{filteredRelated.length > 0 && (
<section class="mt-16">
<h2 class="text-2xl font-bold text-gray-900 mb-8">Related Posts</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRelated.map(relatedPost => (
<article class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200">
{relatedPost.data.image && (
<a href={`/blog/${relatedPost.slug}`} class="block aspect-video overflow-hidden">
<img
src={relatedPost.data.image}
alt={relatedPost.data.title}
class="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
/>
</a>
)}
<div class="p-6">
<time datetime={relatedPost.data.pubDate} class="text-sm text-gray-500 block mb-2">
{new Date(relatedPost.data.pubDate).toLocaleDateString()}
</time>
<h3 class="text-lg font-semibold text-gray-900 mb-2">
<a href={`/blog/${relatedPost.slug}`} class="hover:text-blue-600 transition-colors">
{relatedPost.data.title}
</a>
</h3>
{relatedPost.data.description && (
<p class="text-gray-600 text-sm line-clamp-2">
{relatedPost.data.description}
</p>
)}
</div>
</article>
))}
</div>
</section>
)}
</article>
</Layout>
<style>
/* Custom prose styles */
.prose h2 {
@apply text-2xl font-bold text-gray-900 mt-12 mb-6;
}
.prose h3 {
@apply text-xl font-semibold text-gray-900 mt-8 mb-4;
}
.prose p {
@apply mb-6 leading-relaxed;
}
.prose code {
@apply bg-gray-100 text-gray-800 px-2 py-1 rounded text-sm font-mono;
}
.prose pre {
@apply bg-gray-900 text-gray-100 p-6 rounded-lg overflow-x-auto;
}
.prose pre code {
@apply bg-transparent text-inherit p-0;
}
.prose blockquote {
@apply border-l-4 border-blue-500 pl-6 italic text-gray-600;
}
.prose a {
@apply text-blue-600 hover:text-blue-800 underline;
}
.prose ul, .prose ol {
@apply mb-6 pl-8;
}
.prose li {
@apply mb-2;
}
/* Line clamp utility */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
// public/sw.js - Service Worker for PWA
const CACHE_NAME = 'myblog-v1';
const STATIC_ASSETS = [
'/',
'/about',
'/contact',
'/styles/global.css',
'/scripts/main.js',
'/images/logo.png',
'/favicon.ico'
];
// Install event
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(STATIC_ASSETS);
})
);
});
// Activate event
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
// Fetch event
self.addEventListener('fetch', (event) => {
const { request } = event;
// Skip non-GET requests
if (request.method !== 'GET') return;
// Try cache first
event.respondWith(
caches.match(request)
.then(cachedResponse => {
// Return cached version if found
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(request)
.then(response => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone response for caching
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(request, responseToCache);
});
return response;
})
.catch(() => {
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});