Kurs Next.js · Moduł 9: Integracje i zaawansowane funkcje
Next.js i Strapi - Potężne połączenie frontend i headless CMS
W tej lekcji9
W Metropolii Quantum, gdzie technologie frontend i backend łączą się w harmonijną całość, poznasz jedno z najpotężniejszych połączeń współczesnego web developmentu - Next.js ze Strapi. To duet, który pozwala budować skalowalne, wydajne aplikacje z pełną kontrolą nad treścią.
Czym jest Strapi?
Strapi to open-source'owy headless CMS (Content Management System), który dostarcza RESTful lub GraphQL API do zarządzania treścią. W przeciwieństwie do tradycyjnych CMS-ów jak WordPress, Strapi nie narzuca żadnej struktury frontend - to ty decydujesz, jak wyświetlać dane.
Kluczowe cechy Strapi:
- 100% JavaScript - napisany w Node.js
- Customizable - pełna kontrola nad strukturą danych
- Self-hosted lub Cloud - wybierz sposób hostingu
- RESTful & GraphQL API - elastyczny dostęp do danych
- Admin Panel - intuicyjny interfejs zarządzania
- Role-Based Access Control - zaawansowane zarządzanie uprawnieniami
Dlaczego Next.js + Strapi?
Połączenie Next.js ze Strapi tworzy kompletny stack do budowania nowoczesnych aplikacji webowych:
- Separacja concerns - frontend i backend są niezależne
- Performance - statyczne generowanie z dynamiczną treścią
- Developer Experience - oba narzędzia są przyjazne dla programistów
- Scalability - łatwe skalowanie każdej warstwy osobno
- TypeScript support - pełne wsparcie dla typowania
Instalacja i konfiguracja Strapi
Krok 1: Tworzenie nowego projektu Strapi
1# Utwórz nowy projekt Strapi
2npx create-strapi-app@latest my-strapi-backend --quickstart
3
4# Lub z TypeScript
5npx create-strapi-app@latest my-strapi-backend --typescriptPo instalacji Strapi automatycznie uruchomi się na http://localhost:1337. Pierwsza wizyta przekieruje cię do panelu tworzenia konta administratora.
Krok 2: Konfiguracja Content Types
W panelu admina Strapi możesz utworzyć struktury danych (Content Types) używając intuicyjnego Content-Type Builder:
- Przejdź do Content-Type Builder
- Kliknij Create new collection type
- Nazwij swoją kolekcję (np. "Article")
- Dodaj pola:
title(Text)content(Rich text)slug(UID)featuredImage(Media)author(Relation to Users)publishedAt(Datetime)
Krok 3: Konfiguracja API permissions
Domyślnie Strapi blokuje publiczny dostęp do API. Aby umożliwić odczyt danych:
- Przejdź do Settings → Roles → Public
- Dla swojego Content Type zaznacz odpowiednie uprawnienia (np.
findifindOne) - Zapisz zmiany
Integracja Next.js ze Strapi
Krok 1: Przygotowanie projektu Next.js
1# W nowym terminalu, utwórz projekt Next.js
2npx create-next-app@latest my-nextjs-frontend --typescript --tailwind --app
3
4cd my-nextjs-frontendKrok 2: Konfiguracja zmiennych środowiskowych
Utwórz plik .env.local:
1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-hereKrok 3: Tworzenie klienta API
Stwórz pomocnicze funkcje do komunikacji ze Strapi:
1// lib/strapi.ts
2interface StrapiResponse<T> {
3 data: T;
4 meta: {
5 pagination?: {
6 page: number;
7 pageSize: number;
8 pageCount: number;
9 total: number;
10 };
11 };
12}
13
14const strapiUrl = process.env.NEXT_PUBLIC_STRAPI_API_URL || 'http://localhost:1337';
15
16export async function fetchAPI<T>(
17 path: string,
18 urlParamsObject: Record<string, any> = {},
19 options: RequestInit = {}
20): Promise<T> {
21 // Merge default and user options
22 const mergedOptions: RequestInit = {
23 headers: {
24 "Content-Type": "application/json",
25 ...(process.env.STRAPI_API_TOKEN && {
26 Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}`,
27 }),
28 },
29 ...options,
30 };
31
32 // Build request URL
33 const queryString = new URLSearchParams(urlParamsObject).toString();
34 const requestUrl = `${strapiUrl}${path}${queryString ? `?${queryString}` : ""}`;
35
36 // Trigger API call
37 const response = await fetch(requestUrl, mergedOptions);
38 const data = await response.json();
39
40 if (!response.ok) {
41 throw new Error(`API call failed: ${response.status}`);
42 }
43
44 return data;
45}
46
47// Helper to get media URL
48export function getStrapiMedia(url: string | null) {
49 if (url == null) return null;
50 if (url.startsWith("http") || url.startsWith("//")) return url;
51 return `${strapiUrl}${url}`;
52}Pobieranie danych ze Strapi w Next.js
Static Site Generation (SSG)
Wykorzystaj generateStaticParams i funkcje asynchroniczne w komponentach:
1// app/articles/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3
4interface Article {
5 id: number;
6 attributes: {
7 title: string;
8 slug: string;
9 content: string;
10 publishedAt: string;
11 featuredImage: {
12 data: {
13 attributes: {
14 url: string;
15 alternativeText: string;
16 };
17 };
18 };
19 };
20}
21
22async function getArticles() {
23 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
24 populate: {
25 featuredImage: {
26 fields: ['url', 'alternativeText']
27 },
28 author: {
29 fields: ['name', 'email']
30 }
31 },
32 sort: ['publishedAt:desc'],
33 pagination: {
34 pageSize: 10,
35 },
36 });
37
38 return articlesRes.data;
39}
40
41export default async function ArticlesPage() {
42 const articles = await getArticles();
43
44 return (
45 <div className="container mx-auto px-4 py-8">
46 <h1 className="text-4xl font-bold mb-8">Artykuły</h1>
47 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
48 {articles.map((article) => (
49 <article key={article.id} className="border rounded-lg overflow-hidden shadow-lg">
50 {article.attributes.featuredImage?.data && (
51 <img
52 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
53 alt={article.attributes.featuredImage.data.attributes.alternativeText}
54 className="w-full h-48 object-cover"
55 />
56 )}
57 <div className="p-4">
58 <h2 className="text-xl font-semibold mb-2">
59 {article.attributes.title}
60 </h2>
61 <p className="text-gray-600">
62 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
63 </p>
64 <a
65 href={`/articles/${article.attributes.slug}`}
66 className="text-blue-500 hover:underline mt-2 inline-block"
67 >
68 Czytaj więcej →
69 </a>
70 </div>
71 </article>
72 ))}
73 </div>
74 </div>
75 );
76}Dynamic Routes
Dla pojedynczych artykułów użyj dynamic routes:
1// app/articles/[slug]/page.tsx
2import { fetchAPI } from '@/lib/strapi';
3import { notFound } from 'next/navigation';
4import ReactMarkdown from 'react-markdown';
5
6interface ArticlePageProps {
7 params: Promise<{
8 slug: string;
9 }>;
10}
11
12async function getArticleBySlug(slug: string) {
13 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
14 filters: {
15 slug: {
16 $eq: slug,
17 },
18 },
19 populate: {
20 featuredImage: {
21 fields: ['url', 'alternativeText']
22 },
23 author: {
24 populate: {
25 avatar: {
26 fields: ['url']
27 }
28 }
29 }
30 },
31 });
32
33 if (!articlesRes.data || articlesRes.data.length === 0) {
34 return null;
35 }
36
37 return articlesRes.data[0];
38}
39
40export async function generateStaticParams() {
41 const articlesRes = await fetchAPI<StrapiResponse<Article[]>>('/api/articles', {
42 fields: ['slug'],
43 pagination: {
44 pageSize: 100,
45 },
46 });
47
48 return articlesRes.data.map((article) => ({
49 slug: article.attributes.slug,
50 }));
51}
52
53export default async function ArticlePage({ params }: ArticlePageProps) {
54 const article = await getArticleBySlug((await params).slug);
55
56 if (!article) {
57 notFound();
58 }
59
60 return (
61 <article className="container mx-auto px-4 py-8 max-w-4xl">
62 {article.attributes.featuredImage?.data && (
63 <img
64 src={getStrapiMedia(article.attributes.featuredImage.data.attributes.url)}
65 alt={article.attributes.featuredImage.data.attributes.alternativeText}
66 className="w-full h-96 object-cover rounded-lg mb-8"
67 />
68 )}
69
70 <h1 className="text-4xl font-bold mb-4">{article.attributes.title}</h1>
71
72 <div className="flex items-center gap-4 mb-8 text-gray-600">
73 <span>Autor: {article.attributes.author?.data?.attributes.name}</span>
74 <span>•</span>
75 <time>
76 {new Date(article.attributes.publishedAt).toLocaleDateString('pl-PL')}
77 </time>
78 </div>
79
80 <div className="prose prose-lg max-w-none">
81 <ReactMarkdown>{article.attributes.content}</ReactMarkdown>
82 </div>
83 </article>
84 );
85}Zaawansowane funkcjonalności
Real-time updates z webhooks
Strapi może wysyłać webhooks przy zmianach w contencie. Skonfiguruj endpoint w Next.js:
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath, revalidateTag } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const body = await request.json();
7 const secret = request.headers.get('x-strapi-signature');
8
9 // Weryfikuj webhook signature
10 if (secret !== process.env.STRAPI_WEBHOOK_SECRET) {
11 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12 }
13
14 try {
15 // Rewaliduj odpowiednie ścieżki na podstawie eventu
16 switch (body.event) {
17 case 'entry.create':
18 case 'entry.update':
19 if (body.model === 'article') {
20 revalidatePath('/articles');
21 revalidatePath(`/articles/${body.entry.slug}`);
22 }
23 break;
24 case 'entry.delete':
25 if (body.model === 'article') {
26 revalidatePath('/articles');
27 }
28 break;
29 }
30
31 return NextResponse.json({ revalidated: true });
32 } catch (error) {
33 return NextResponse.json({ error: 'Error revalidating' }, { status: 500 });
34 }
35}GraphQL integration
Strapi oferuje również GraphQL API. Zainstaluj plugin:
1# W folderze Strapi
2npm install @strapi/plugin-graphqlNastępnie użyj GraphQL w Next.js:
1// lib/graphql.ts
2import { GraphQLClient } from 'graphql-request';
3
4const client = new GraphQLClient(`${process.env.NEXT_PUBLIC_STRAPI_API_URL}/graphql`, {
5 headers: {
6 authorization: process.env.STRAPI_API_TOKEN
7 ? `Bearer ${process.env.STRAPI_API_TOKEN}`
8 : '',
9 },
10});
11
12export async function queryStrapi<T>(query: string, variables?: any): Promise<T> {
13 return client.request<T>(query, variables);
14}Optymalizacja obrazów
Integruj Strapi z Next.js Image Optimization:
1// components/StrapiImage.tsx
2import Image from 'next/image';
3import { getStrapiMedia } from '@/lib/strapi';
4
5interface StrapiImageProps {
6 src: string;
7 alt: string;
8 width: number;
9 height: number;
10 className?: string;
11}
12
13export default function StrapiImage({ src, alt, ...props }: StrapiImageProps) {
14 const imageUrl = getStrapiMedia(src);
15
16 if (!imageUrl) {
17 return null;
18 }
19
20 return (
21 <Image
22 src={imageUrl}
23 alt={alt}
24 {...props}
25 loader={({ src, width, quality }) => {
26 const params = new URLSearchParams();
27 if (width) params.set('width', width.toString());
28 if (quality) params.set('quality', quality.toString());
29 return `${src}?${params.toString()}`;
30 }}
31 />
32 );
33}Preview mode
Implementuj podgląd nieopublikowanych treści:
1// app/api/preview/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { draftMode } from 'next/headers';
4
5export async function GET(request: NextRequest) {
6 const secret = request.nextUrl.searchParams.get('secret');
7 const slug = request.nextUrl.searchParams.get('slug');
8
9 if (secret !== process.env.STRAPI_PREVIEW_SECRET || !slug) {
10 return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
11 }
12
13 // Enable Draft Mode
14 (await draftMode()).enable();
15
16 // Redirect to the preview page
17 return NextResponse.redirect(new URL(`/articles/${slug}`, request.url));
18}
19
20// app/api/exit-preview/route.ts
21export async function GET() {
22 (await draftMode()).disable();
23 return NextResponse.redirect('/');
24}Deployment i najlepsze praktyki
1. Environment Variables
Zawsze używaj zmiennych środowiskowych dla wrażliwych danych:
1# .env.production
2NEXT_PUBLIC_STRAPI_API_URL=https://your-strapi-instance.com
3STRAPI_API_TOKEN=your-production-token
4STRAPI_WEBHOOK_SECRET=your-webhook-secret
5STRAPI_PREVIEW_SECRET=your-preview-secret2. Caching Strategy
Wykorzystaj Next.js caching:
1// Cache na 60 sekund
2export const revalidate = 60;
3
4// Lub użyj cache tags
5fetch(url, { next: { tags: ['articles'] } });3. Error Handling
Implementuj solidną obsługę błędów:
1export async function getArticles() {
2 try {
3 const data = await fetchAPI<StrapiResponse<Article[]>>('/api/articles');
4 return data;
5 } catch (error) {
6 console.error('Error fetching articles:', error);
7 return { data: [], meta: {} };
8 }
9}4. TypeScript Types
Generuj typy ze Strapi schema:
1# Zainstaluj generator typów
2npm install -D @strapi/typesPodsumowanie
Połączenie Next.js ze Strapi tworzy potężny stack do budowania nowoczesnych aplikacji webowych. Strapi dostarcza elastyczne API i intuicyjny system zarządzania treścią, podczas gdy Next.js zapewnia wydajny frontend z SSG/SSR i optymalizacjami.
Ta kombinacja jest idealna dla:
- Blogów i portali informacyjnych
- E-commerce
- Portfolio i stron korporacyjnych
- Aplikacji SaaS
- Platform edukacyjnych
W następnych modułach zgłębimy bardziej zaawansowane tematy, takie jak multi-tenancy, internationalization i custom plugins w Strapi.
Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3interface Article {
4 id: number;
5 title: string;
6 slug: string;
7 content: string;
8 author: string;
9 publishedAt: string;
10 category: string;
11}
12
13const mockArticles: Article[] = [
14 { id: 1, title: 'Wprowadzenie do Headless CMS', slug: 'intro-headless-cms', content: 'Headless CMS oddziela backend od frontendu, dając pełną kontrolę nad prezentacją treści...', author: 'Admin', publishedAt: '2025-01-15', category: 'CMS' },
15 { id: 2, title: 'Strapi i Next.js - Pierwsze kroki', slug: 'strapi-nextjs-start', content: 'Integracja Strapi z Next.js pozwala na budowanie wydajnych aplikacji z dynamiczną treścią...', author: 'Developer', publishedAt: '2025-02-10', category: 'Tutorial' },
16 { id: 3, title: 'REST vs GraphQL w Strapi', slug: 'rest-vs-graphql', content: 'Strapi oferuje zarówno REST API jak i GraphQL. Każde podejście ma swoje zalety...', author: 'Admin', publishedAt: '2025-03-05', category: 'API' },
17 { id: 4, title: 'Typowanie odpowiedzi Strapi w TypeScript', slug: 'strapi-typescript', content: 'Dzięki TypeScript możemy typować odpowiedzi z API Strapi dla lepszego DX...', author: 'Developer', publishedAt: '2025-03-20', category: 'TypeScript' },
18];
19
20const StrapiDemo = () => {
21 const [articles] = useState<Article[]>(mockArticles);
22 const [selected, setSelected] = useState<Article | null>(null);
23 const [endpoint, setEndpoint] = useState('/api/articles');
24 const [response, setResponse] = useState<string>('');
25
26 const simulateFetch = () => {
27 const isDetail = endpoint.includes('/api/articles/');
28 const slug = endpoint.split('/api/articles/')[1];
29
30 if (isDetail && slug) {
31 const found = articles.find(a => a.slug === slug);
32 if (found) {
33 setResponse(JSON.stringify({ data: { id: found.id, attributes: { title: found.title, content: found.content, slug: found.slug, author: found.author, publishedAt: found.publishedAt } }, meta: {} }, null, 2));
34 setSelected(found);
35 } else {
36 setResponse(JSON.stringify({ error: { status: 404, message: 'Not found' } }, null, 2));
37 }
38 } else {
39 setResponse(JSON.stringify({ data: articles.map(a => ({ id: a.id, attributes: { title: a.title, slug: a.slug, category: a.category, publishedAt: a.publishedAt } })), meta: { pagination: { page: 1, pageSize: 25, total: articles.length } } }, null, 2));
40 setSelected(null);
41 }
42 };
43
44 return (
45 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
46 <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Next.js + Strapi CMS</h1>
47 <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Symulacja headless CMS API</p>
48
49 <div style={{ maxWidth: '800px', margin: '0 auto' }}>
50 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '16px', borderRadius: '10px', marginBottom: '16px', border: '1px solid #333' }}>
51 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>API Explorer</h3>
52 <div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
53 <input value={endpoint} onChange={e => setEndpoint(e.target.value)}
54 style={{ flex: 1, padding: '10px', background: '#16213e', border: '1px solid #444', borderRadius: '6px', color: '#fff', fontFamily: 'monospace' }} />
55 <button onClick={simulateFetch} style={{ padding: '10px 20px', background: '#7c4dff', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
56 GET
57 </button>
58 </div>
59 <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
60 <button onClick={() => setEndpoint('/api/articles')} style={{ padding: '4px 10px', background: 'rgba(100,255,218,0.1)', border: '1px solid #64ffda', borderRadius: '4px', color: '#64ffda', cursor: 'pointer', fontSize: '12px' }}>/api/articles</button>
61 {articles.map(a => (
62 <button key={a.id} onClick={() => setEndpoint('/api/articles/' + a.slug)} style={{ padding: '4px 10px', background: 'rgba(124,77,255,0.1)', border: '1px solid #7c4dff', borderRadius: '4px', color: '#7c4dff', cursor: 'pointer', fontSize: '12px' }}>
63 /articles/{a.slug}
64 </button>
65 ))}
66 </div>
67 </div>
68
69 {response && (
70 <div style={{ background: '#000', padding: '16px', borderRadius: '10px', marginBottom: '16px' }}>
71 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Response:</h4>
72 <pre style={{ color: '#0f0', fontSize: '12px', overflow: 'auto', maxHeight: '250px', whiteSpace: 'pre-wrap' }}>{response}</pre>
73 </div>
74 )}
75
76 {selected && (
77 <div style={{ background: 'rgba(255,255,255,0.05)', padding: '20px', borderRadius: '10px', border: '1px solid #333' }}>
78 <span style={{ color: '#7c4dff', fontSize: '12px' }}>{selected.category}</span>
79 <h2 style={{ color: '#64ffda', margin: '8px 0' }}>{selected.title}</h2>
80 <p style={{ color: '#aaa', fontSize: '13px' }}>By {selected.author} | {selected.publishedAt}</p>
81 <p style={{ color: '#ccc', marginTop: '12px', lineHeight: '1.6' }}>{selected.content}</p>
82 </div>
83 )}
84 </div>
85 </div>
86 );
87};
88
89export default StrapiDemo;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jakie dane Strapi domyślnie zwraca przez REST API?
2. Która z poniższych cech NAJLEPIEJ opisuje Headless CMS?
Zadania praktyczne w grze
- Klikanie w kolejności
Uporządkuj kroki integracji Next.js ze Strapi.
- Edytor kodu
Stwórz komponent pobierający dane z headless CMS Strapi w Next.js
- Edytor kodu
Stwórz Article content type z polami title, content, slug.