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
-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' },
}
);
}
};