Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Next.js i Strapi - Potężne połączenie frontend i headless CMS

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:

  1. Separacja concerns - frontend i backend są niezależne
  2. Performance - statyczne generowanie z dynamiczną treścią
  3. Developer Experience - oba narzędzia są przyjazne dla programistów
  4. Scalability - łatwe skalowanie każdej warstwy osobno
  5. 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 --typescript

Po 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:

  1. Przejdź do Content-Type Builder
  2. Kliknij Create new collection type
  3. Nazwij swoją kolekcję (np. "Article")
  4. 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:

  1. Przejdź do Settings → Roles → Public
  2. Dla swojego Content Type zaznacz odpowiednie uprawnienia (np.
    find
    i
    findOne
    )
  3. 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-frontend

Krok 2: Konfiguracja zmiennych środowiskowych

Utwórz plik

.env.local
:

1NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337
2STRAPI_API_TOKEN=your-api-token-here

Krok 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-graphql

Nastę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-secret

2. 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/types

Podsumowanie

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.

Przejdź do CodeWorlds