diff --git a/.gitignore b/.gitignore index a31ee31..10fc9f9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ public/news/ public/parts/ public/operations-inbox/ public/footage/ + +# Local Claude Code / MCP config — agent-specific, not project +.mcp.json +.claude/ diff --git a/next.config.ts b/next.config.ts index 10d5b70..03bf14d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,14 +4,20 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); const nextConfig = { output: "standalone" as const, - images: { + images: { qualities: [75, 90, 100], + // Image Optimizer cache TTL — keeps optimized variants for 5 min, + // matching Nginx max-age. Picks up replaced source files quickly. + minimumCacheTTL: 300, + formats: ['image/avif', 'image/webp'] as ('image/avif' | 'image/webp')[], }, reactStrictMode: true, serverExternalPackages: ['nodemailer'], experimental: { serverActions: { - bodySizeLimit: '500mb' as const, + // 50MB cap — large enough for hero images and CMS uploads, + // small enough to limit DoS surface. Use /api/assets for big files. + bodySizeLimit: '50mb' as const, }, }, }; diff --git a/nginx/conf.d/flux.conf b/nginx/conf.d/flux.conf index 7a352ab..83bf6c7 100644 --- a/nginx/conf.d/flux.conf +++ b/nginx/conf.d/flux.conf @@ -36,6 +36,7 @@ server { add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + # Next.js bundles use content hashing — safe to cache forever location /_next/static/ { proxy_pass http://nextjs; expires 365d; @@ -43,6 +44,17 @@ server { access_log off; } + # Next.js image optimizer — short cache, browser revalidates + location /_next/image { + proxy_pass http://nextjs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + add_header Cache-Control "public, max-age=300, must-revalidate" always; + } + location /hq-command/login { limit_req zone=login burst=10 nodelay; proxy_pass http://nextjs; @@ -104,46 +116,50 @@ server { access_log off; } - # Serve uploaded assets directly from disk (bypass Next.js) + # ───────────────────────────────────────────────────────────────── + # User-uploaded assets — served directly from disk (bypass Next.js) + # + # Cache strategy: short max-age + must-revalidate. + # Browser caches for 5 minutes, then asks Nginx "did this change?" + # via If-Modified-Since. Nginx auto-replies 304 (Not Modified) if the + # file's mtime is unchanged, or serves the new file if it changed. + # This means new CMS uploads appear within ~5 min without rebuild + # AND saved bandwidth on unchanged files. + # ───────────────────────────────────────────────────────────────── location /cases/ { alias /srv/cases/; - expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, max-age=300, must-revalidate" always; access_log off; } location /applications/ { alias /srv/applications/; - expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, max-age=300, must-revalidate" always; access_log off; } location /news/ { alias /srv/news/; - expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, max-age=300, must-revalidate" always; access_log off; } location /parts/ { alias /srv/parts/; - expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, max-age=300, must-revalidate" always; access_log off; } location /operations-inbox/ { alias /srv/operations-inbox/; - expires 7d; + add_header Cache-Control "private, max-age=60, must-revalidate" always; access_log off; } location /footage/ { alias /srv/footage/; - expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, max-age=300, must-revalidate" always; access_log off; } diff --git a/src/app/[locale]/applications/[slug]/page.tsx b/src/app/[locale]/applications/[slug]/page.tsx index 9b80035..c3cbf36 100644 --- a/src/app/[locale]/applications/[slug]/page.tsx +++ b/src/app/[locale]/applications/[slug]/page.tsx @@ -1,4 +1,5 @@ -export const dynamic = "force-dynamic"; +// ISR: revalidates every 60s + on-demand via revalidatePath after CMS uploads. +export const revalidate = 60; import Link from "next/link"; import fs from "fs"; @@ -46,8 +47,6 @@ export async function generateStaticParams() { } } -export const revalidate = 60; - // 🔥 AHORA RECIBIMOS EL LOCALE DESDE LA URL export default async function ApplicationPage({ params }: { params: Promise<{ slug: string, locale: string }> }) { const resolvedParams = await params; diff --git a/src/app/[locale]/heritage/page.tsx b/src/app/[locale]/heritage/page.tsx index 6d35fd1..b79b262 100644 --- a/src/app/[locale]/heritage/page.tsx +++ b/src/app/[locale]/heritage/page.tsx @@ -1,4 +1,5 @@ -export const dynamic = "force-dynamic"; +// ISR: revalidates every 60s + on-demand via revalidatePath after CMS uploads. +export const revalidate = 60; import Link from "next/link"; import Image from "next/image"; @@ -28,21 +29,21 @@ const renderMarkdown = (text: string) => { if (inTable) { elements.push(
- +
- + {tableHeaders.map((th, i) => ( - ))} - + {tableRows.map((row, rIdx) => ( - + {row.map((cell, cIdx) => ( - ))} @@ -62,11 +63,11 @@ const renderMarkdown = (text: string) => { if (listItems.length > 0) { elements.push( isOrderedList ? ( -
    +
      {listItems}
    ) : ( -
+ {parseInline(th)}
+ {parseInline(cell)}