4.8 KiB
4.8 KiB
Stack Configuration
astro.config.mjs
// @ts-check
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react';
export default defineConfig({
site: '{{SITE_URL}}', // e.g. 'https://example.com'
integrations: [
mdx(),
sitemap({
filter: (page) =>
!['/404', '/privacy', '/terms', '/shop'].some((p) => page.includes(p)),
}),
react(),
],
adapter: cloudflare({
platformProxy: { enabled: true },
imageService: "compile"
}),
vite: {
plugins: [tailwindcss()],
},
});
Key notes:
siteis used for canonical URLs, OG tags, sitemap, RSS- Sitemap filter excludes legal, 404, and shop pages
platformProxy: { enabled: true }gives access to Cloudflare bindings in devimageService: "compile"uses Astro's built-in image optimization- No separate tailwind.config or postcss.config — Tailwind 4 uses Vite plugin
tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true,
"types": ["./worker-configuration.d.ts", "node"],
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] },
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "{{PROJECT_NAME}}",
"compatibility_date": "2025-12-05",
"compatibility_flags": ["nodejs_compat"],
"pages_build_output_dir": "./dist",
"observability": { "enabled": true },
"vars": {
"CONTACT_EMAIL": "{{CONTACT_EMAIL}}"
}
}
Critical: Pages does NOT support account_id or ratelimits in config. Wrangler will reject the deploy. Set account ID via env var, configure rate limiting in dashboard.
package.json scripts
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro build && wrangler pages dev",
"astro": "astro",
"deploy": "astro build && wrangler pages deploy",
"cf-typegen": "wrangler types",
"convert:avif": "node src/utils/convert-to-avif.js",
"convert:avif:all": "node src/utils/convert-to-avif.js --all",
"convert:avif:jpeg": "node src/utils/convert-to-avif.js --jpeg",
"convert:avif:png": "node src/utils/convert-to-avif.js --png",
"commit": "node src/utils/git-commit.js"
}
}
Critical: bun build invokes bun's bundler, not astro's. Always use bun run build.
Dependencies
{
"dependencies": {
"@astrojs/cloudflare": "^12.6.12",
"@astrojs/mdx": "^4.3.12",
"@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.14",
"@astrojs/sitemap": "^3.6.0",
"@react-email/components": "^1.0.8",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"astro": "^5.16.4",
"clsx": "^2.1.1",
"fuse.js": "^7.1.0",
"gsap": "^3.14.2",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-icons": "^5.5.0",
"resend": "^6.9.2",
"sharp": "^0.34.3",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17"
},
"devDependencies": {
"@types/node": "^24.10.1",
"wrangler": "^4.53.0"
}
}
src/env.d.ts
type Runtime = import("@astrojs/cloudflare").Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {}
}
interface Env {
RESEND_API_KEY: string;
CONTACT_RATE_LIMITER: RateLimit;
}
src/consts.ts
export const SITE_TITLE = '{{COMPANY_NAME}} — {{TAGLINE}}';
export const SITE_DESCRIPTION = '{{SEO_DESCRIPTION}}';
export const HTML_MARKER = "Built by {{COMPANY_NAME}}";
export const SOCIAL_LINKS = {
email: '{{CONTACT_EMAIL}}',
website: '{{SITE_URL}}',
// Include only the ones the client has:
// steam: 'https://store.steampowered.com/...',
// twitter: 'https://twitter.com/...',
// discord: 'https://discord.gg/...',
// instagram: 'https://instagram.com/...',
// github: 'https://github.com/...',
};
src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
src/utils/reading-time.ts
export function calculateReadingTime(content: string | undefined, wordsPerMinute: number = 200): string {
const wordCount = content?.split(/\s+/).length || 0;
const readingTime = Math.max(1, Math.ceil(wordCount / wordsPerMinute));
return `${readingTime} min read`;
}
worker-configuration.d.ts
Generated by bun cf-typegen. Run this after initial setup to generate Cloudflare Worker runtime types.