Kurs Next.js · Moduł 8: Deployment i produkcja
Projekt: Pełne wdrożenie aplikacji Next.js do produkcji
W tej lekcji10
Witaj w finalnym projekcie modułu wdrażania! W Metropolii Quantum 2150 każdy system przechodzi przez rygorystyczny proces przygotowania do produkcji - od budowania i optymalizacji, przez konfigurację SEO, aż po monitoring i testowanie. W tym projekcie połączymy wszystkie umiejętności zdobyte w całym module, aby przejść przez pełny cykl wdrażania aplikacji Next.js.
Cel projektu
Twoim zadaniem jest przygotowanie kompletnej aplikacji Next.js do wdrożenia produkcyjnego. Projekt obejmuje wszystkie kluczowe aspekty, które omówiliśmy w tym module:
- Budowanie i optymalizacja (z lekcji o build i analyze)
- SEO i metadane (z lekcji o metadanych dynamicznych)
- Open Graph i social sharing (z lekcji o OG)
- Sitemap i robots.txt (z lekcji o mapach witryn)
- Web Analytics (z lekcji o analityce)
- Wdrażanie na platformy hostingowe (z lekcji o Vercel/Netlify)
- Zmienne środowiskowe i konfiguracja produkcyjna (z lekcji o env vars)
- CI/CD (z lekcji o automatyzacji)
- Monitoring (z lekcji o monitoringu)
- PWA i Service Workers (z lekcji o PWA)
- Preview Deployments i staging (z lekcji o środowiskach)
Krok 1: Konfiguracja next.config.js
Rozpocznij od pełnej konfiguracji Next.js z optymalizacjami produkcyjnymi:
1// next.config.ts
2import type { NextConfig } from 'next';
3import withBundleAnalyzer from '@next/bundle-analyzer';
4
5const config: NextConfig = {
6 // Optymalizacja obrazów
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 dni cache
17 },
18
19 // Kompresja
20 compress: true,
21
22 // Optymalizacja bundla
23 experimental: {
24 optimizePackageImports: ['lucide-react', '@quantum/ui'],
25 },
26
27 // Nagłówki bezpieczeństwa
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 // Przekierowania
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);Krok 2: SEO i dynamiczne metadane
Skonfiguruj metadane dla każdej strony z wykorzystaniem 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 - Portal Metropolii',
8 template: '%s | Quantum City',
9 },
10 description: 'Oficjalny portal Metropolii Quantum 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 - dynamiczne metadane
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}Krok 3: Sitemap i robots.txt
Wygeneruj dynamiczną mapę witryny:
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 // Pobierz dynamiczne strony
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}Krok 4: Web Analytics i monitoring
Zintegruj system analityki i monitoringu błędów:
1// lib/analytics.ts
2export function trackEvent(name: string, properties?: Record<string, any>) {
3 if (typeof window === 'undefined') return;
4
5 // Własna analityka
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}Krok 5: CI/CD Pipeline
Skonfiguruj automatyczny pipeline wdrażania:
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_TOKENKrok 6: PWA i offline support
Dodaj funkcjonalność Progressive Web App:
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: 'Oficjalny portal Metropolii Quantum 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}Krok 7: Testy E2E z Cypress
Napisz testy pokrywające krytyczne ścieżki użytkownika:
1// cypress/e2e/production-readiness.cy.ts
2describe('Production Readiness Check', () => {
3 it('strona główna ładuje się poniżej 3 sekund', () => {
4 cy.visit('/', { timeout: 3000 });
5 cy.get('h1').should('be.visible');
6 });
7
8 it('SEO meta tagi są poprawne', () => {
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('nagłówki bezpieczeństwa są ustawione', () => {
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});Krok 8: Checklist przed wdrożeniem
Przed każdym wdrożeniem produkcyjnym przejdź przez te punkty kontrolne:
Wydajność
- Komenda
next buildkończy się bez błędów - Rozmiar bundla JS poniżej 200KB (gzipped)
- LCP poniżej 2.5 sekundy, CLS poniżej 0.1
- Obrazy zoptymalizowane (WebP/AVIF)
SEO
- Każda strona ma unikalne title i description
- Open Graph tagi są poprawne
- Sitemap.xml i robots.txt są wygenerowane
Bezpieczeństwo
- Nagłówki bezpieczeństwa (HSTS, CSP, X-Frame-Options)
- CSRF protection i rate limiting
- Walidacja inputu z Zod
Monitoring
- Error tracking skonfigurowany
- Web Vitals monitorowane
- Alerty na krytyczne błędy
CI/CD
- Testy jednostkowe i E2E przechodzą
- Linting i type-checking bez błędów
- Automatyczny deploy po merge do main
Podsumowanie
W tym projekcie połączyłeś wszystkie kluczowe umiejętności wdrażania aplikacji Next.js do produkcji. Każdy z tych elementów jest niezbędny do stworzenia profesjonalnej, wydajnej i bezpiecznej aplikacji webowej.
Pamiętaj - w Metropolii Quantum 2150 nie ma miejsca na półśrodki. Każdy system musi być w pełni zoptymalizowany, zabezpieczony i monitorowany. Teraz jesteś gotowy, aby wdrożyć swoje aplikacje Next.js na najwyższym poziomie!
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Projekt: Pelne wdrozenie aplikacji Next.js do produkcji
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 - optymalizacja obrazow i kompresja', category: 'Build', done: false },
14 { id: '2', label: 'Naglowki bezpieczenstwa (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: 'Dynamiczne metadane dla podstron', category: 'SEO', done: false },
17 { id: '5', label: 'Sitemap.xml wygenerowany', category: 'SEO', done: false },
18 { id: '6', label: 'Robots.txt skonfigurowany', category: 'SEO', done: false },
19 { id: '7', label: 'Web Analytics zintegrowany', category: 'Monitoring', done: false },
20 { id: '8', label: 'Error monitoring skonfigurowany', category: 'Monitoring', done: false },
21 { id: '9', label: 'Web Vitals monitorowane', category: 'Monitoring', done: false },
22 { id: '10', label: 'CI/CD pipeline skonfigurowany', category: 'Deploy', done: false },
23 { id: '11', label: 'Testy E2E (Cypress) przechodza', category: 'Deploy', done: false },
24 { id: '12', label: 'PWA manifest i Service Worker', category: 'Deploy', done: false },
25 { id: '13', label: 'Wdrozenie na 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' }}>Postep: {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 }}>Aplikacja gotowa do wdrozenia!</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}Widzisz błąd w tej lekcji?