E2E Test Suite / Smoke Tests (P0) (push) Has been cancelled
E2E Test Suite / Critical User Journeys (push) Has been cancelled
E2E Test Suite / API Integration Tests (push) Has been cancelled
E2E Test Suite / Form Interaction Tests (push) Has been cancelled
E2E Test Suite / Destructive & Chaos Tests (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (chromium) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (firefox) (push) Has been cancelled
E2E Test Suite / Cross-Browser Regression (webkit) (push) Has been cancelled
E2E Test Suite / Mobile Device Tests (push) Has been cancelled
E2E Test Suite / Security Header Tests (push) Has been cancelled
E2E Test Suite / Test Report Summary (push) Has been cancelled
Ping Search Engines / Notify Search Engines (push) Successful in 3s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 6m1s
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Has been cancelled
- Add explicit mimeTypes map and setHeaders callback to the /_assets express.static handler to guarantee correct Content-Type for .css, .js, .mjs, .json, .svg, .png, .jpg, .webp, and .woff2 files - Rename the blog post hero <header> to <section> to eliminate duplicate <header> elements and fix Playwright strict-mode selector failures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
97 lines
3.0 KiB
JavaScript
97 lines
3.0 KiB
JavaScript
import { config } from 'dotenv';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Load .env from the project root (same dir as server.mjs)
|
|
config({ path: join(__dirname, '.env') });
|
|
console.log('[server] Loaded .env from:', join(__dirname, '.env'));
|
|
console.log('[server] SMTP_HOST:', process.env.SMTP_HOST || 'NOT SET');
|
|
console.log('[server] CONTACT_EMAIL_TO:', process.env.CONTACT_EMAIL_TO || 'NOT SET');
|
|
|
|
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
|
import express from 'express';
|
|
import compression from 'compression';
|
|
|
|
const app = express();
|
|
|
|
// Server configuration
|
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
const PORT = process.env.PORT || 10000;
|
|
|
|
// Gzip/Brotli compression - reduces transfer size by ~70%
|
|
app.use(compression({
|
|
level: 6, // balanced speed/compression ratio
|
|
threshold: 1024, // only compress responses > 1KB
|
|
filter: (req, res) => {
|
|
// Don't compress already-compressed formats
|
|
if (req.headers['x-no-compression']) return false;
|
|
return compression.filter(req, res);
|
|
},
|
|
}));
|
|
|
|
// MIME type map for explicit Content-Type headers
|
|
const mimeTypes = {
|
|
'.css': 'text/css',
|
|
'.js': 'application/javascript',
|
|
'.mjs': 'application/javascript',
|
|
'.json': 'application/json',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.webp': 'image/webp',
|
|
'.woff2': 'font/woff2',
|
|
};
|
|
|
|
// Serve static assets with long-term caching
|
|
// Hashed assets (_assets/) get 1-year immutable cache
|
|
// Non-hashed assets get 1-hour cache with revalidation
|
|
app.use('/_assets', express.static(join(__dirname, 'dist', 'client', '_assets'), {
|
|
maxAge: '1y',
|
|
immutable: true,
|
|
etag: false,
|
|
setHeaders: (res, filePath) => {
|
|
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
if (mimeTypes[ext]) {
|
|
res.setHeader('Content-Type', mimeTypes[ext]);
|
|
}
|
|
},
|
|
}));
|
|
|
|
app.use(express.static(join(__dirname, 'dist', 'client'), {
|
|
maxAge: '1h',
|
|
setHeaders: (res, filePath) => {
|
|
// Fonts: long-term cache since they're versioned by Google Fonts
|
|
if (/\.(woff2?|ttf|otf|eot)$/.test(filePath)) {
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
}
|
|
// Images: 1 week cache
|
|
if (/\.(jpg|jpeg|png|gif|webp|avif|svg|ico)$/.test(filePath)) {
|
|
res.setHeader('Cache-Control', 'public, max-age=604800, stale-while-revalidate=86400');
|
|
}
|
|
},
|
|
}));
|
|
|
|
// Use the Astro SSR handler for all routes
|
|
app.use(ssrHandler);
|
|
|
|
// Start the server
|
|
app.listen(PORT, HOST, () => {
|
|
console.log(`🚀 Server running at http://${HOST}:${PORT}`);
|
|
console.log(`📁 Serving static files from: ${join(__dirname, 'dist', 'client')}`);
|
|
console.log(`⚙️ Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
console.log('SIGTERM received, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('SIGINT received, shutting down gracefully...');
|
|
process.exit(0);
|
|
});
|