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ą.
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.
Połączenie Next.js ze Strapi tworzy kompletny stack do budowania nowoczesnych aplikacji webowych:
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.W panelu admina Strapi możesz utworzyć struktury danych (Content Types) używając intuicyjnego Content-Type Builder:
title (Text)content (Rich text)slug (UID)featuredImage (Media)author (Relation to Users)publishedAt (Datetime)Domyślnie Strapi blokuje publiczny dostęp do API. Aby umożliwić odczyt danych:
find i findOne)1# W nowym terminalu, utwórz projekt Next.js
2npx create-next-app@latest my-nextjs-frontend --typescript --tailwind --app
3
4cd my-nextjs-frontendUtwórz plik
.env.local:1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-hereStwó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}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}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}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}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}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}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}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-secretWykorzystaj Next.js caching:
1// Cache na 60 sekund
2export const revalidate = 60;
3
4// Lub użyj cache tags
5fetch(url, { next: { tags: ['articles'] } });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}Generuj typy ze Strapi schema:
1# Zainstaluj generator typów
2npm install -D @strapi/typesPołą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:
W następnych modułach zgłębimy bardziej zaawansowane tematy, takie jak multi-tenancy, internationalization i custom plugins w Strapi.