Deployment & Optimization
Deploy to Vercel, optimize images, fonts, and improve Core Web Vitals
Deploying Next.js
Next.js can be deployed to any hosting platform that supports Node.js, or as a static site. Vercel (the creators of Next.js) offers the most seamless deployment experience with zero configuration.
🚀 Deploy to Vercel
# Install Vercel CLI
npm i -g vercel
# Deploy (from project root)
vercel
# Deploy to production
vercel --prod
# Or connect to Git for automatic deployments
# 1. Push to GitHub/GitLab/Bitbucket
# 2. Import project at vercel.com/new
# 3. Every push deploys automatically!
Image Optimization
import Image from 'next/image';
// Basic usage
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately (above the fold)
/>
);
}
// Responsive images
function ResponsiveImage() {
return (
<Image
src="/photo.jpg"
alt="Photo"
fill // Fill parent container
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
);
}
// External images (configure in next.config.js)
// next.config.js
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '*.amazonaws.com',
},
],
},
};
// Placeholder blur
<Image
src="/large-image.jpg"
alt="Large image"
width={800}
height={400}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // Or use static import
/>
Font Optimization
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
// Load Google Fonts
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
lang="en"
className={`${inter.variable} ${robotoMono.variable}`}
>
<body className={inter.className}>{children}</body>
</html>
);
}
// Use in Tailwind CSS
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
};
// Local fonts
import localFont from 'next/font/local';
const myFont = localFont({
src: './fonts/MyFont.woff2',
display: 'swap',
});
Metadata & SEO
// app/layout.tsx - Global metadata
import { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | My Site',
default: 'My Site',
},
description: 'Welcome to my website',
metadataBase: new URL('https://mysite.com'),
openGraph: {
title: 'My Site',
description: 'Welcome to my website',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
},
robots: {
index: true,
follow: true,
},
};
// app/blog/[slug]/page.tsx - Dynamic metadata
import { Metadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
// JSON-LD structured data
export default function Page() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'My Article',
author: { '@type': 'Person', name: 'John Doe' },
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>...</article>
</>
);
}
Performance Optimization
// next.config.js
const nextConfig = {
// Bundle analyzer
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
// Compression
compress: true,
// Production source maps (optional)
productionBrowserSourceMaps: false,
};
// Dynamic imports for code splitting
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Client-side only
});
// Lazy load below-the-fold content
const Comments = dynamic(() => import('./Comments'));
export default function Post() {
return (
<article>
<h1>Post Title</h1>
<p>Content...</p>
{/* Only loads when scrolled into view */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</article>
);
}
Sitemap & Robots
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
const blogUrls = posts.map(post => ({
url: `https://mysite.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{
url: 'https://mysite.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://mysite.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
},
...blogUrls,
];
}
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/'],
},
],
sitemap: 'https://mysite.com/sitemap.xml',
};
}
Other Hosting Options
# Docker deployment
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
# Static export (no server features)
# next.config.js
const nextConfig = {
output: 'export', // Generates static HTML
};
# Build and deploy to any static host
npm run build
# Upload 'out' folder to Netlify, GitHub Pages, etc.
✅ Deployment Checklist
- ☑️ Use Image component for all images
- ☑️ Optimize fonts with next/font
- ☑️ Add metadata to all pages
- ☑️ Generate sitemap and robots.txt
- ☑️ Enable compression
- ☑️ Use dynamic imports for large components
- ☑️ Test Core Web Vitals with Lighthouse
- ☑️ Set up environment variables