Switch to static output and add Azure SWA deployment

- Set output: 'static' in astro.config.mjs; remove Vercel/Netlify adapters
- Remove API routes (contact, newsletter) incompatible with static mode
- Add Azure SWA deploy workflow using @azure/static-web-apps-cli via npx
- Add public/staticwebapp.config.json for SWA routing and 404 fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel Krähenbühl
2026-04-13 21:58:47 +02:00
parent d668aa0fdf
commit c043b2373b
7 changed files with 112 additions and 4016 deletions
+15 -43
View File
@@ -1,13 +1,15 @@
name: Deploy
name: Deploy to Azure Static Web Apps
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, closed]
branches: [main]
jobs:
build:
build_and_deploy:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
steps:
@@ -15,9 +17,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run: corepack enable pnpm
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -28,46 +28,18 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
- name: Type check
run: pnpm run check
- name: Build
run: pnpm run build
env:
SITE_URL: ${{ secrets.SITE_URL }}
# Uncomment and configure for your deployment platform
#
# Vercel deployment:
# - name: Deploy to Vercel
# if: github.ref == 'refs/heads/main'
# uses: amondnet/vercel-action@v25
# with:
# vercel-token: ${{ secrets.VERCEL_TOKEN }}
# vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
# vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
# vercel-args: '--prod'
#
# Netlify deployment:
# - name: Deploy to Netlify
# if: github.ref == 'refs/heads/main'
# uses: nwtgck/actions-netlify@v2
# with:
# publish-dir: './dist'
# production-branch: main
# github-token: ${{ secrets.GITHUB_TOKEN }}
# env:
# NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
# NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
#
# Cloudflare Pages deployment:
# - name: Deploy to Cloudflare Pages
# if: github.ref == 'refs/heads/main'
# uses: cloudflare/wrangler-action@v3
# with:
# apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# command: pages deploy dist --project-name=astro-rocket
- name: Deploy to Azure Static Web Apps
run: npx --yes @azure/static-web-apps-cli@latest deploy dist --deployment-token "${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}" --env production
close_pull_request:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Close preview environment
run: npx --yes @azure/static-web-apps-cli@latest deploy --deployment-token "${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}" --env close
+1 -9
View File
@@ -4,13 +4,9 @@ import sitemap from '@astrojs/sitemap';
import react from '@astrojs/react';
import icon from 'astro-icon';
import tailwindcss from '@tailwindcss/vite';
import vercel from '@astrojs/vercel';
import netlify from '@astrojs/netlify';
const isNetlify = process.env.DEPLOY_TARGET === 'netlify';
export default defineConfig({
adapter: isNetlify ? netlify() : vercel(),
output: 'static',
site: process.env.SITE_URL || 'https://example.com',
env: {
@@ -52,10 +48,6 @@ export default defineConfig({
},
},
security: {
checkOrigin: true,
},
markdown: {
shikiConfig: {
theme: 'github-dark',
-2
View File
@@ -40,10 +40,8 @@
},
"dependencies": {
"@astrojs/mdx": "5.0.0",
"@astrojs/netlify": "^7.0.2",
"@astrojs/react": "5.0.0",
"@astrojs/sitemap": "^3.7.1",
"@astrojs/vercel": "^10.0.0",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@fontsource-variable/manrope": "^5.2.8",
"@fontsource-variable/outfit": "^5.2.8",
+81 -3759
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"navigationFallback": {
"rewrite": "/404.html",
"exclude": ["/assets/*", "/fonts/*", "/_astro/*", "*.ico", "*.png", "*.jpg", "*.svg", "*.webp", "*.webmanifest", "*.xml", "*.txt"]
},
"trailingSlash": "auto",
"mimeTypes": {
".json": "application/json"
},
"responseOverrides": {
"404": {
"rewrite": "/404.html"
}
}
}
-108
View File
@@ -1,108 +0,0 @@
export const prerender = false;
import type { APIRoute } from 'astro';
import { z } from 'astro/zod';
import { Resend } from 'resend';
import siteConfig from '@/config/site.config';
const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters').max(100),
email: z.email('Please enter a valid email address'),
subject: z.string().max(200).optional(),
message: z.string().min(10, 'Message must be at least 10 characters').max(5000),
honeypot: z.string().max(0), // Anti-spam: must be empty
});
export const POST: APIRoute = async ({ request }) => {
try {
const formData = await request.formData();
const data = {
name: formData.get('name')?.toString() || '',
email: formData.get('email')?.toString() || '',
subject: formData.get('subject')?.toString() || '',
message: formData.get('message')?.toString() || '',
honeypot: formData.get('honeypot')?.toString() || '',
};
// Validate
const result = contactSchema.safeParse(data);
if (!result.success) {
const fieldErrors: Record<string, string[]> = {};
for (const error of result.error.issues) {
const field = error.path[0] as string;
if (!fieldErrors[field]) {
fieldErrors[field] = [];
}
fieldErrors[field].push(error.message);
}
return new Response(
JSON.stringify({ success: false, errors: fieldErrors }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Honeypot check (bot detection)
if (result.data.honeypot) {
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// Send email via Resend
const apiKey = import.meta.env.RESEND_API_KEY;
if (!apiKey) {
console.error('RESEND_API_KEY is not set');
return new Response(
JSON.stringify({ success: false, errors: { form: ['Email service is not configured'] } }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
const resend = new Resend(apiKey);
const toEmail = siteConfig.email;
const fromEmail = import.meta.env.RESEND_FROM_EMAIL || toEmail;
const siteLabel = siteConfig.name;
const subject = result.data.subject
? `[${siteLabel}] ${result.data.subject}`
: `[${siteLabel}] New contact from ${result.data.name}`;
const { error } = await resend.emails.send({
from: `Contact Form <${fromEmail}>`,
to: toEmail,
replyTo: result.data.email,
subject,
html: `
<p><strong>Name:</strong> ${result.data.name}</p>
<p><strong>Email:</strong> ${result.data.email}</p>
<p><strong>Message:</strong></p>
<p>${result.data.message.replace(/\n/g, '<br>')}</p>
`,
});
if (error) {
console.error('Resend error:', error);
return new Response(
JSON.stringify({ success: false, errors: { form: [error.message || 'Failed to send email'] } }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Contact form error:', error);
return new Response(
JSON.stringify({ success: false, errors: { form: ['An unexpected error occurred'] } }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
-95
View File
@@ -1,95 +0,0 @@
import type { APIRoute } from 'astro';
import { z } from 'astro/zod';
import { Resend } from 'resend';
const newsletterSchema = z.object({
email: z.email('Please enter a valid email address'),
honeypot: z.string().max(0).optional(),
});
export const POST: APIRoute = async ({ request }) => {
try {
const formData = await request.formData();
const email = formData.get('email')?.toString() || '';
const honeypot = formData.get('website')?.toString() || '';
// Check honeypot - if filled, it's likely a bot
if (honeypot) {
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
const result = newsletterSchema.safeParse({ email, honeypot });
if (!result.success) {
return new Response(
JSON.stringify({
success: false,
error: result.error.issues[0]?.message || 'Please enter a valid email address',
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
const apiKey = import.meta.env.RESEND_API_KEY;
const audienceId = import.meta.env.RESEND_AUDIENCE_ID;
if (!apiKey || !audienceId) {
console.error('Newsletter: RESEND_API_KEY or RESEND_AUDIENCE_ID is not configured');
return new Response(
JSON.stringify({
success: false,
error: 'Newsletter service is not configured.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
const resend = new Resend(apiKey);
const { error } = await resend.contacts.create({
audienceId,
email: result.data.email,
unsubscribed: false,
});
if (error) {
console.error('Resend newsletter error:', error);
return new Response(
JSON.stringify({
success: false,
error: 'Subscription failed. Please try again.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Newsletter error:', error);
return new Response(
JSON.stringify({
success: false,
error: 'Subscription failed. Please try again.',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
};