Next.js course Β· Module 8: Deployment and Production

Project: Full deployment of a Next.js application to production

7 min read
In this lesson10

Welcome to the final project of the deployment module! In Quantum Metropolis 2150, every system goes through a rigorous process of preparing for production - from building and optimization, through SEO configuration, all the way to monitoring and testing. In this project, we will combine all the skills gained throughout the module to go through the full deployment cycle of a Next.js application.

Project goal

Your task is to prepare a complete Next.js application for a production deployment. The project covers all the key aspects we discussed in this module:

  1. Building and optimization (from the build and analyze lesson)
  2. SEO and metadata (from the dynamic metadata lesson)
  3. Open Graph and social sharing (from the OG lesson)
  4. Sitemap and robots.txt (from the sitemaps lesson)
  5. Web Analytics (from the analytics lesson)
  6. Deploying to hosting platforms (from the Vercel/Netlify lesson)
  7. Environment variables and production configuration (from the env vars lesson)
  8. CI/CD (from the automation lesson)
  9. Monitoring (from the monitoring lesson)
  10. PWA and Service Workers (from the PWA lesson)
  11. Preview Deployments and staging (from the environments lesson)

Step 1: Configuring next.config.js

Start with a full Next.js configuration with production optimizations:

1// next.config.ts
2import type { NextConfig } from 'next';
3import withBundleAnalyzer from '@next/bundle-analyzer';
4
5const config: NextConfig = {
6  // Image optimization
7  images: {
8    formats: ['image/avif', 'image/webp'],
9    remotePatterns: [
10      {
11        protocol: 'https',
12        hostname: 'cdn.quantum-city.com',
13        pathname: '/images/**',
14      },
15    ],
16    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days cache
17  },
18
19  // Compression
20  compress: true,
21
22  // Bundle optimization
23  experimental: {
24    optimizePackageImports: ['lucide-react', '@quantum/ui'],
25  },
26
27  // Security headers
28  async headers() {
29    return [
30      {
31        source: '/:path*',
32        headers: [
33          { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
34          { key: 'X-Content-Type-Options', value: 'nosniff' },
35          { key: 'X-XSS-Protection', value: '1; mode=block' },
36          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
37          {
38            key: 'Strict-Transport-Security',
39            value: 'max-age=63072000; includeSubDomains; preload',
40          },
41          {
42            key: 'Permissions-Policy',
43            value: 'camera=(), microphone=(), geolocation=()',
44          },
45        ],
46      },
47    ];
48  },
49
50  // Redirects
51  async redirects() {
52    return [
53      {
54        source: '/old-page',
55        destination: '/new-page',
56        permanent: true, // 301 redirect
57      },
58    ];
59  },
60};
61
62const withAnalyzer = withBundleAnalyzer({
63  enabled: process.env.ANALYZE === 'true',
64});
65
66export default withAnalyzer(config);

Step 2: SEO and dynamic metadata

Configure metadata for each page using the Metadata API:

1// app/layout.tsx
2import type { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  metadataBase: new URL('https://quantum-city.com'),
6  title: {
7    default: 'Quantum City - Metropolis Portal',
8    template: '%s | Quantum City',
9  },
10  description: 'The official portal of Quantum Metropolis 2150',
11  openGraph: {
12    type: 'website',
13    locale: 'pl_PL',
14    url: 'https://quantum-city.com',
15    siteName: 'Quantum City',
16    images: [
17      {
18        url: '/og-default.png',
19        width: 1200,
20        height: 630,
21        alt: 'Quantum City',
22      },
23    ],
24  },
25  twitter: {
26    card: 'summary_large_image',
27    creator: '@QuantumCity',
28  },
29  robots: {
30    index: true,
31    follow: true,
32    googleBot: {
33      index: true,
34      follow: true,
35      'max-video-preview': -1,
36      'max-image-preview': 'large',
37      'max-snippet': -1,
38    },
39  },
40};
41
42// app/products/[id]/page.tsx - dynamic metadata
43import type { Metadata } from 'next';
44
45type Props = { params: Promise<{ id: string }> };
46
47export async function generateMetadata({ params }: Props): Promise<Metadata> {
48  const product = await fetch(
49    `https://api.quantum-city.com/products/${(await params).id}`
50  ).then(r => r.json());
51
52  return {
53    title: product.name,
54    description: product.description,
55    openGraph: {
56      title: product.name,
57      description: product.description,
58      images: [{ url: product.imageUrl, width: 800, height: 600 }],
59    },
60  };
61}

Step 3: Sitemap and robots.txt

Generate a dynamic sitemap:

1// app/sitemap.ts
2import { MetadataRoute } from 'next';
3
4export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
5  const baseUrl = 'https://quantum-city.com';
6
7  // Fetch dynamic pages
8  const products = await fetch('https://api.quantum-city.com/products')
9    .then(r => r.json());
10
11  const posts = await fetch('https://api.quantum-city.com/blog')
12    .then(r => r.json());
13
14  const productPages = products.map((product: any) => ({
15    url: `${baseUrl}/products/${product.slug}`,
16    lastModified: new Date(product.updatedAt),
17    changeFrequency: 'weekly' as const,
18    priority: 0.8,
19  }));
20
21  const blogPages = posts.map((post: any) => ({
22    url: `${baseUrl}/blog/${post.slug}`,
23    lastModified: new Date(post.updatedAt),
24    changeFrequency: 'monthly' as const,
25    priority: 0.6,
26  }));
27
28  return [
29    { url: baseUrl, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
30    { url: `${baseUrl}/products`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 },
31    ...productPages,
32    ...blogPages,
33  ];
34}
35
36// app/robots.ts
37import { MetadataRoute } from 'next';
38
39export default function robots(): MetadataRoute.Robots {
40  return {
41    rules: [
42      {
43        userAgent: '*',
44        allow: '/',
45        disallow: ['/admin/', '/api/', '/dashboard/'],
46      },
47    ],
48    sitemap: 'https://quantum-city.com/sitemap.xml',
49  };
50}

Step 4: Web Analytics and monitoring

Integrate an analytics and error-monitoring system:

1// lib/analytics.ts
2export function trackEvent(name: string, properties?: Record<string, any>) {
3  if (typeof window === 'undefined') return;
4
5  // Custom analytics
6  fetch('/api/analytics', {
7    method: 'POST',
8    headers: { 'Content-Type': 'application/json' },
9    body: JSON.stringify({ event: name, properties, timestamp: Date.now() }),
10  }).catch(console.error);
11}
12
13// lib/monitoring.ts
14export function initErrorMonitoring() {
15  if (typeof window === 'undefined') return;
16
17  window.addEventListener('error', (event) => {
18    fetch('/api/errors', {
19      method: 'POST',
20      headers: { 'Content-Type': 'application/json' },
21      body: JSON.stringify({
22        message: event.message,
23        filename: event.filename,
24        lineno: event.lineno,
25        stack: event.error?.stack,
26        url: window.location.href,
27        timestamp: new Date().toISOString(),
28      }),
29    }).catch(console.error);
30  });
31}

Step 5: CI/CD Pipeline

Configure an automatic deployment pipeline:

1# .github/workflows/deploy.yml
2name: Deploy to Production
3
4on:
5  push:
6    branches: [main]
7
8jobs:
9  test:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13      - uses: actions/setup-node@v4
14        with:
15          node-version: '20'
16          cache: 'npm'
17      - run: npm ci
18      - run: npm run lint
19      - run: npm run type-check
20      - run: npm test
21
22  e2e:
23    needs: test
24    runs-on: ubuntu-latest
25    steps:
26      - uses: actions/checkout@v4
27      - uses: actions/setup-node@v4
28        with:
29          node-version: '20'
30      - run: npm ci
31      - run: npm run build
32      - name: Cypress E2E Tests
33        uses: cypress-io/github-action@v6
34        with:
35          start: npm start
36          wait-on: 'http://localhost:3000'
37          browser: chrome
38
39  deploy:
40    needs: [test, e2e]
41    runs-on: ubuntu-latest
42    steps:
43      - uses: actions/checkout@v4
44      - name: Deploy to Vercel
45        run: npx vercel --prod --token=VERCEL_TOKEN

Step 6: PWA and offline support

Add Progressive Web App functionality:

1// app/manifest.ts
2import { MetadataRoute } from 'next';
3
4export default function manifest(): MetadataRoute.Manifest {
5  return {
6    name: 'Quantum City Portal',
7    short_name: 'QC Portal',
8    description: 'The official portal of Quantum Metropolis 2150',
9    start_url: '/',
10    display: 'standalone',
11    background_color: '#0a0a1a',
12    theme_color: '#64ffda',
13    icons: [
14      { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
15      { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
16    ],
17  };
18}

Step 7: E2E tests with Cypress

Write tests covering critical user paths:

1// cypress/e2e/production-readiness.cy.ts
2describe('Production Readiness Check', () => {
3  it('home page loads in under 3 seconds', () => {
4    cy.visit('/', { timeout: 3000 });
5    cy.get('h1').should('be.visible');
6  });
7
8  it('SEO meta tags are correct', () => {
9    cy.visit('/');
10    cy.get('head title').should('contain', 'Quantum City');
11    cy.get('meta[name="description"]').should('have.attr', 'content');
12  });
13
14  it('security headers are set', () => {
15    cy.request('/').then((response) => {
16      expect(response.headers).to.have.property('x-frame-options');
17      expect(response.headers).to.have.property('x-content-type-options');
18    });
19  });
20});

Step 8: Pre-deployment checklist

Before every production deployment, go through these checkpoints:

Performance

  • The next build command finishes without errors
  • JS bundle size below 200KB (gzipped)
  • LCP below 2.5 seconds, CLS below 0.1
  • Images optimized (WebP/AVIF)

SEO

  • Every page has a unique title and description
  • Open Graph tags are correct
  • Sitemap.xml and robots.txt are generated

Security

  • Security headers (HSTS, CSP, X-Frame-Options)
  • CSRF protection and rate limiting
  • Input validation with Zod

Monitoring

  • Error tracking configured
  • Web Vitals monitored
  • Alerts for critical errors

CI/CD

  • Unit and E2E tests pass
  • Linting and type-checking with no errors
  • Automatic deploy after merge to main

Summary

In this project, you combined all the key skills for deploying a Next.js application to production. Each of these elements is essential for creating a professional, performant, and secure web application.

Remember - in Quantum Metropolis 2150 there is no room for half-measures. Every system must be fully optimized, secured, and monitored. Now you are ready to deploy your Next.js applications at the highest level!

Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Project: Full production deployment of Next.js application
4
5interface ChecklistItem {
6  id: string;
7  label: string;
8  category: string;
9  done: boolean;
10}
11
12const initialChecklist: ChecklistItem[] = [
13  { id: '1', label: 'next.config.js - image optimization and compression', category: 'Build', done: false },
14  { id: '2', label: 'Security headers (HSTS, CSP, X-Frame-Options)', category: 'Build', done: false },
15  { id: '3', label: 'Metadata API - title, description, OG tags', category: 'SEO', done: false },
16  { id: '4', label: 'Dynamic metadata for subpages', category: 'SEO', done: false },
17  { id: '5', label: 'Sitemap.xml generated', category: 'SEO', done: false },
18  { id: '6', label: 'Robots.txt configured', category: 'SEO', done: false },
19  { id: '7', label: 'Web Analytics integrated', category: 'Monitoring', done: false },
20  { id: '8', label: 'Error monitoring configured', category: 'Monitoring', done: false },
21  { id: '9', label: 'Web Vitals monitored', category: 'Monitoring', done: false },
22  { id: '10', label: 'CI/CD pipeline configured', category: 'Deploy', done: false },
23  { id: '11', label: 'E2E tests (Cypress) passing', category: 'Deploy', done: false },
24  { id: '12', label: 'PWA manifest and Service Worker', category: 'Deploy', done: false },
25  { id: '13', label: 'Deployment on Vercel/Netlify', category: 'Deploy', done: false },
26];
27
28const categoryColors: Record<string, string> = {
29  Build: '#64ffda',
30  SEO: '#bb86fc',
31  Monitoring: '#ff9800',
32  Deploy: '#4caf50',
33};
34
35export default function DeploymentChecklist() {
36  const [items, setItems] = useState(initialChecklist);
37
38  const toggle = (id: string) => {
39    setItems(prev => prev.map(item => item.id === id ? { ...item, done: !item.done } : item));
40  };
41
42  const total = items.length;
43  const done = items.filter(i => i.done).length;
44  const pct = Math.round((done / total) * 100);
45
46  const categories = Array.from(new Set(items.map(i => i.category)));
47
48  return (
49    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#fff', fontFamily: 'monospace' }}>
50      <h1 style={{ color: '#64ffda', fontSize: 20, marginBottom: 4 }}>Production Deployment Checklist</h1>
51      <p style={{ color: '#888', fontSize: 13, marginBottom: 16 }}>Quantum City - Next.js Deployment</p>
52
53      <div style={{ background: '#151530', borderRadius: 8, padding: 16, marginBottom: 20 }}>
54        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8, fontSize: 13 }}>
55          <span style={{ color: '#aaa' }}>Progress: {done}/{total}</span>
56          <span style={{ color: pct === 100 ? '#4caf50' : '#64ffda' }}>{pct}%</span>
57        </div>
58        <div style={{ height: 10, background: '#1a2744', borderRadius: 5, overflow: 'hidden' }}>
59          <div style={{ width: pct + '%', height: '100%', background: pct === 100 ? '#4caf50' : '#64ffda', borderRadius: 5, transition: 'width 0.4s ease' }} />
60        </div>
61        {pct === 100 && <p style={{ color: '#4caf50', marginTop: 8, fontSize: 14 }}>Application ready for deployment!</p>}
62      </div>
63
64      {categories.map(cat => (
65        <div key={cat} style={{ marginBottom: 16 }}>
66          <h3 style={{ color: categoryColors[cat] || '#fff', fontSize: 14, marginBottom: 8 }}>{cat}</h3>
67          {items.filter(i => i.category === cat).map(item => (
68            <div key={item.id} onClick={() => toggle(item.id)} style={{
69              display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px', marginBottom: 4,
70              background: item.done ? '#1a3a2a' : '#151530', borderRadius: 6, cursor: 'pointer',
71              border: '1px solid ' + (item.done ? '#4caf5044' : '#334'),
72            }}>
73              <span style={{ fontSize: 16 }}>{item.done ? '\u2705' : '\u2B1C'}</span>
74              <span style={{ color: item.done ? '#888' : '#ccc', fontSize: 13, textDecoration: item.done ? 'line-through' : 'none' }}>
75                {item.label}
76              </span>
77            </div>
78          ))}
79        </div>
80      ))}
81    </div>
82  );
83}

Spotted a mistake in this lesson?

Useful articles