La maggior parte delle guide "deploy su Vercel" si ferma a premere il pulsante. Il Vercel in produzione è una config che progetti. I quattro file qui sotto sono quello che rilasciamo: un vercel.json con header, redirect e cron; un next.config.ts con la strategia di cache scritta nero su bianco; una route in edge che fa auth in meno di 50ms di first byte; e una Server Action che fa revalidate della path giusta quando il contenuto cambia.
1. La config vercel.json
La config di produzione vive nel source control. Header, redirect, cron e rewrite sono rivedibili, diff-abili e legati al commit che li ha introdotti.
// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=63072000; includeSubDomains; preload"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
}
]
},
{
"source": "/_next/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"redirects": [
{
"source": "/blog/old-slug",
"destination": "/blog/new-slug",
"permanent": true
}
],
"crons": [
{
"path": "/api/cron/reconcile-stripe",
"schedule": "0 3 * * *"
},
{
"path": "/api/cron/refresh-sitemap",
"schedule": "0 4 * * *"
}
]
}
2. La strategia di cache di Next.js
Il next.config.ts rende esplicita la strategia di cache: ISR per le pagine di contenuto, nessuna cache per le route autenticate, ottimizzazione immagini tarata sui breakpoint reali che il design system usa.
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [360, 640, 768, 1024, 1280, 1536, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/uploads/**',
},
],
},
experimental: {
// Cache ISR di lunga durata per il blog; flush on-demand via webhook.
isrFlushToDisk: true,
},
async headers() {
return [
{
source: '/api/:path*',
headers: [{ key: 'Cache-Control', value: 'no-store' }],
},
]
},
}
export default config
3. La route in edge
L'auth check gira in edge in millisecondi. La route legge il cookie di sessione, lo valida contro una chiave pubblica JWT (niente round-trip al database) e o rewrite verso l'applicazione o redirect a login.
// app/api/auth/check/route.ts
import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
export const runtime = 'edge'
const PUBLIC_KEY = process.env.SESSION_PUBLIC_KEY!
export async function GET(request: Request) {
const cookie = request.headers.get('cookie')
const token = cookie?.match(/session=([^;]+)/)?.[1]
if (!token) return NextResponse.json({ ok: false }, { status: 401 })
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(PUBLIC_KEY),
)
return NextResponse.json({ ok: true, userId: payload.sub })
} catch {
return NextResponse.json({ ok: false }, { status: 401 })
}
}
4. Revalidation on-demand da una Server Action
Quando un editor di contenuti pubblica un post, la Server Action scrive nel database e chiama revalidatePath per la route esatta. La versione in cache cade; la prossima richiesta ricostruisce e ri-mette in cache. Niente rebuild notturno.
// app/admin/posts/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { createServerClient } from '@/lib/supabase/server'
export async function publishPost(postId: string): Promise<{ ok: true } | { ok: false; error: string }> {
const supabase = await createServerClient()
const { data, error } = await supabase
.from('posts')
.update({ status: 'published', published_at: new Date().toISOString() })
.eq('id', postId)
.select('slug, locale')
.single()
if (error) return { ok: false, error: error.message }
// Droppa la cache per questo post esatto e per l'indice del locale.
revalidatePath(`/${data.locale}/blog/${data.slug}`)
revalidatePath(`/${data.locale}/blog`)
return { ok: true }
}
5. Cosa ti compra questo
Il deploy è un git push. La preview è una URL che salta fuori sulla PR. L'auth check risponde in 30 millisecondi. La cache del blog si rigenera nel momento in cui un editor preme publish, non in un cron notturno. La fattura atterra dove te l'aspetti perché ogni decisione di cache vive nel source control dove puoi verificarla.
Il Vercel che si prende la cattiva reputazione è quello configurato per caso. Il Vercel che merita il suo posto è quello progettato, con intenzione, tre o quattro file alla volta.