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:
@@ -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
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+81
-3759
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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' },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user