Files
route-commerce/next.config.ts
T
Nora 5a72705e71
Deploy to route.crispygoat.com / deploy (push) Successful in 4m14s
fix(ops): switch pm2 to standalone server + fix wholesale_settings query
Three problems were silently breaking the prod Tuxedo redesign and
the offline DB error swallowed all visibility into them:

1. `next start` against `output: "standalone"` is unsupported. Next.js
   prints "next start does not work with output: standalone" and
   silently disables the image optimizer — every `/_next/image?url=...`
   returned "url parameter is not allowed". pm2 was previously
   started as `pm2 start npm -- start -- -p 3100`. Switched to
   `node /home/tyler/route-commerce/scripts/start-standalone.cjs`.

2. The standalone server reads `.next/static/` relative to itself, so
   deploy must `cp -r .next/static .next/standalone/.next/static` on
   every sync. Without this, every `_next/static/chunks/*.js` returns
   404 and the page hydration dies. Added to the deploy workflow.

3. bash's `set -a; . ./.env` truncates DATABASE_URL at the `&` in
   `&channel_binding=require`, so the standalone server boots with
   empty DB env. `getBrandSettingsPublic` then caught the empty-pool
   error and returned `{success:false, error:'Failed to fetch brand settings'}`,
   so `state.heroImageUrl` stayed null and the hero poster never
   rendered. The new `scripts/start-standalone.cjs` parses .env in
   Node (which handles `&` correctly) and exec's the server with the
   loaded env.

4. `getBrandSettingsPublic` queried `ws.wholesale_enabled`, which
   no longer exists in `wholesale_settings`. The schema migration
   renamed it to `online_payment_enabled`. Switched the column
   reference and added a console.error log so future drift surfaces
   instead of being swallowed by the empty catch.

Plus `next.config.ts` had `hostname: "s3.crispygoat.com"` added
under `images.remotePatterns` so the optimizer accepts MinIO URLs.
2026-07-06 12:50:43 -06:00

179 lines
5.5 KiB
TypeScript

import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Enable standalone output for Docker/PM2 deployment
output: "standalone",
// Lock the file-tracing root to the project directory. Without this,
// Next.js 16 walks up from package.json looking for a lockfile, finds
// the homelab runner's stale `act` cache at
// /home/tyler/.cache/act/.../package-lock.json, and warns:
// "We detected multiple lockfiles and selected the directory of
// /home/tyler/package-lock.json as the root directory."
// The deploy runner's APP_DIR is /home/tyler/route-commerce, so
// resolving relative to the project root is correct both locally and
// in CI. We resolve to an absolute path to avoid the warning in
// Next.js 16 which prefers absolute paths here.
outputFileTracingRoot: __dirname,
// Enable strict mode
reactStrictMode: true,
// Optimize images
images: {
remotePatterns: [
{
protocol: "https",
hostname: "*.supabase.co",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "picsum.photos",
},
{
// Brand media hosted on the crispygoat MinIO bucket
// (e.g. s3.crispygoat.com/videos/wp-import/...)
protocol: "https",
hostname: "s3.crispygoat.com",
},
],
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Security headers
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
],
},
{
// API routes - more restrictive
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization, X-API-Key",
},
{
key: "Access-Control-Max-Age",
value: "86400",
},
],
},
{
source: "/_next/static/:path*",
headers: [
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
],
},
{
source: "/icons/:path*",
headers: [
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
],
},
{
source: "/screenshots/:path*",
headers: [
{ key: "Cache-Control", value: "public, max-age=604800" },
],
},
];
},
// Redirects
async redirects() {
return [
// Cutover (PR 6, Task 6.1): mobile-first admin lives at /admin/v2/*.
// Redirect the v1 list/detail paths to their v2 equivalents.
// Use `permanent: false` (307) so we can revert easily if a
// regression is caught in monitoring before the v1 files are
// removed in Task 6.2.
// PR 8, Task 8.1: redirect the bare /admin path to the v2 dashboard.
// The dashboard itself still lives at /admin (as the v1 page)
// until Task 8.2 removes it after a 3-day monitoring window —
// this redirect just points the v1 entry point at v2.
{ source: "/admin", destination: "/admin/v2", permanent: false },
{ source: "/admin/orders", destination: "/admin/v2/orders", permanent: false },
{ source: "/admin/orders/:id", destination: "/admin/v2/orders/:id", permanent: false },
{ source: "/admin/stops", destination: "/admin/v2/stops", permanent: false },
{ source: "/admin/products", destination: "/admin/v2/products", permanent: false },
];
},
// Rewrites for API proxy
async rewrites() {
return [
// Add any necessary rewrites here
];
},
// Experimental features
experimental: {
// Enable optimizePackageImports for better bundle size
optimizePackageImports: ["lucide-react", "@radix-ui/react-icons", "framer-motion"],
// Enable React's <ViewTransition> and Next.js' automatic route
// transitions. Combined with the smooth-transition wrappers around
// page content (see src/components/transitions), navigation feels
// like a single continuous app rather than a sequence of page loads.
viewTransition: true,
},
// Compiler options
compiler: {
// Remove console.log in production
removeConsole: process.env.NODE_ENV === "production"
? { exclude: ["error", "warn"] }
: false,
},
// Logging
logging: {
fetches: {
fullUrl: process.env.NODE_ENV === "development",
},
},
// Allow cross-origin requests to dev resources (HMR, dev manifest, etc.)
// from non-localhost hosts (e.g. LAN IPs, dev tunnels). Read from
// `NEXT_ALLOWED_DEV_ORIGINS` as a comma-separated list. Empty in production.
allowedDevOrigins: process.env.NEXT_ALLOWED_DEV_ORIGINS
? process.env.NEXT_ALLOWED_DEV_ORIGINS.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [],
};
export default nextConfig;