PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Next.js dla PHP developera – SSR, Server Components, GraphQL z Magento, porównanie z MVC

by Henryk Tews / wtorek, 27 lutego 2024 / Opublikowano w JavaScript

Next.js to React framework który rozwiązuje problem SEO i wydajności aplikacji SPA – renderuje strony po stronie serwera (SSR) lub generuje statycznie (SSG), tak jak klasyczne PHP aplikacje. Dla PHP developera Next.js ma znajomą strukturę: routing oparty na plikach, server-side rendering, API routes jako własny backend. Pokazuję podstawy z perspektywy kogoś kto myśli w PHP i Magento.

Dlaczego Next.js a nie czysty React?

Czysty React (Create React App, Vite) renderuje stronę w przeglądarce – robot Google widzi pustą stronę HTML i czeka na JavaScript. Next.js renderuje HTML po stronie serwera – robot dostaje gotową stronę. To fundamentalna różnica dla SEO sklepu e-commerce:

// Czysty React - przeglądarka dostaje:
<html>
  <body>
    <div id="root"></div>  // puste - JavaScript renderuje treść
    <script src="bundle.js"></script>
  </body>
</html>

// Next.js - przeglądarka i robot Google dostają:
<html>
  <body>
    <h1>Widget Pro - Najlepszy widget na rynku</h1>
    <p>Cena: 29.99 PLN</p>
    // ... pełna treść strony gotowa przy pierwszym ładowaniu
  </body>
</html>

Instalacja i struktura projektu

# Nowy projekt Next.js (App Router - zalecany od Next.js 13+)
npx create-next-app@latest my-magento-frontend \
    --typescript \
    --tailwind \
    --app

cd my-magento-frontend
npm run dev
my-magento-frontend/
  app/                    # App Router - routing oparty na katalogach
    layout.tsx            # Globalny layout - jak layout.xml w Magento
    page.tsx              # Strona główna - /
    catalog/
      category/
        [slug]/
          page.tsx        # /catalog/category/[slug]
      product/
        [sku]/
          page.tsx        # /catalog/product/[sku]
    api/                  # API Routes - własny backend
      cart/
        route.ts          # POST /api/cart
      search/
        route.ts          # GET /api/search?q=...
  components/             # Komponenty React
    Header.tsx
    ProductCard.tsx
    AddToCartButton.tsx
  lib/
    magento.ts            # Klient Magento GraphQL/REST API
  types/
    product.ts            # TypeScript interfejsy

Pobieranie danych z Magento GraphQL – Server Component

App Router Next.js 13+ ma Server Components – komponenty które renderują się na serwerze i mogą bezpośrednio fetchować dane. Dla PHP developera to jak metoda kontrolera która pobiera dane z bazy:

// lib/magento.ts - klient GraphQL dla Magento
const MAGENTO_URL = process.env.NEXT_PUBLIC_MAGENTO_URL || 'https://shop.example.com';

interface GraphQLResponse<T> {
    data: T;
    errors?: Array<{ message: string }>;
}

export async function magentoQuery<T>(
    query: string,
    variables?: Record<string, unknown>,
    token?: string
): Promise<T> {
    const headers: HeadersInit = {
        'Content-Type': 'application/json',
    };

    if (token) {
        headers['Authorization'] = `Bearer ${token}`;
    }

    const response = await fetch(`${MAGENTO_URL}/graphql`, {
        method: 'POST',
        headers,
        body: JSON.stringify({ query, variables }),
        next: { revalidate: 60 }, // cache 60 sekund - jak Varnish TTL
    });

    const json: GraphQLResponse<T> = await response.json();

    if (json.errors) {
        throw new Error(json.errors[0].message);
    }

    return json.data;
}
// app/catalog/product/[sku]/page.tsx - Server Component (domyślnie)
// Renderuje się na serwerze - jak PHP controller + template

import { magentoQuery } from '@/lib/magento';
import { notFound } from 'next/navigation';
import AddToCartButton from '@/components/AddToCartButton';

interface Product {
    id: number;
    sku: string;
    name: string;
    price_range: {
        minimum_price: {
            final_price: { value: number; currency: string };
        };
    };
    description: { html: string };
    media_gallery: Array<{ url: string; label: string }>;
}

const PRODUCT_QUERY = `
    query GetProduct($sku: String!) {
        products(filter: { sku: { eq: $sku } }) {
            items {
                id
                sku
                name
                description { html }
                price_range {
                    minimum_price {
                        final_price { value currency }
                    }
                }
                media_gallery { url label }
            }
        }
    }
`;

// generateMetadata - odpowiednik meta tagów w PHTML
export async function generateMetadata({ params }: { params: { sku: string } }) {
    const data = await magentoQuery<{ products: { items: Product[] } }>(
        PRODUCT_QUERY,
        { sku: params.sku }
    );

    const product = data.products.items[0];
    if (!product) return {};

    return {
        title: `${product.name} | Mój Sklep`,
        description: product.description.html.replace(/<[^>]*>/g, '').slice(0, 160),
    };
}

// Główny komponent - renderuje się na serwerze
export default async function ProductPage({ params }: { params: { sku: string } }) {
    const data = await magentoQuery<{ products: { items: Product[] } }>(
        PRODUCT_QUERY,
        { sku: params.sku }
    );

    const product = data.products.items[0];

    if (!product) {
        notFound(); // jak 404 w Magento
    }

    const price = product.price_range.minimum_price.final_price;
    const mainImage = product.media_gallery[0];

    return (
        <div className="container mx-auto px-4 py-8">
            <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
                <div>
                    {mainImage && (
                        <img
                            src={mainImage.url}
                            alt={mainImage.label || product.name}
                            className="w-full rounded-lg shadow"
                        />
                    )}
                </div>

                <div>
                    <h1 className="text-3xl font-bold mb-4">{product.name}</h1>
                    <p className="text-2xl text-primary font-semibold mb-6">
                        {price.value.toFixed(2)} {price.currency}
                    </p>

                    <div
                        className="prose mb-6"
                        dangerouslySetInnerHTML={{ __html: product.description.html }}
                    />

                    {/* Client Component - interaktywny */}
                    <AddToCartButton productId={product.id} productName={product.name} />
                </div>
            </div>
        </div>
    );
}

Client Component – interaktywność

// components/AddToCartButton.tsx - Client Component
// "use client" - ten komponent renderuje się w przeglądarce
'use client';

import { useState } from 'react';

interface Props {
    productId: number;
    productName: string;
}

export default function AddToCartButton({ productId, productName }: Props) {
    const [qty, setQty] = useState(1);
    const [isLoading, setIsLoading] = useState(false);
    const [message, setMessage] = useState<string | null>(null);

    async function addToCart() {
        setIsLoading(true);
        setMessage(null);

        try {
            // API Route Next.js jako proxy do Magento REST API
            const response = await fetch('/api/cart', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ productId, qty }),
            });

            const data = await response.json();

            if (response.ok) {
                setMessage('Produkt dodany do koszyka!');
            } else {
                setMessage(data.error || 'Wystąpił błąd');
            }
        } catch {
            setMessage('Błąd połączenia');
        } finally {
            setIsLoading(false);
        }
    }

    return (
        <div>
            <div className="flex items-center gap-3 mb-4">
                <button onClick={() => setQty(Math.max(1, qty - 1))} className="btn-qty">-</button>
                <span className="w-12 text-center font-semibold">{qty}</span>
                <button onClick={() => setQty(qty + 1)} className="btn-qty">+</button>
            </div>

            <button
                onClick={addToCart}
                disabled={isLoading}
                className="w-full bg-primary text-white py-3 rounded disabled:opacity-50"
            >
                {isLoading ? 'Dodaję...' : 'Dodaj do koszyka'}
            </button>

            {message && (
                <p className="mt-2 text-sm text-green-600">{message}</p>
            )}
        </div>
    );
}

Porównanie z klasycznym PHP MVC

Koncepcja PHP/Magento Odpowiednik Next.js
Controller::execute() page.tsx (Server Component)
Repository::getById() fetch() z danymi GraphQL/REST
layout.xml / templates layout.tsx + komponenty
REST API endpoint app/api/…/route.ts
Varnish cache (TTL) next: { revalidate: 60 }
Magento Frontend (Luma) Client Components
generateMetadata() Meta tagi SEO dynamicznie

Podsumowanie

Next.js to naturalny wybór dla headless frontendu przy Magento 2 gdy chcesz SSR i dobrego SEO. Architektura Server Components jest bardziej znajoma dla PHP developera niż czysty React SPA – rendering po stronie serwera, bezpośrednie fetche danych, routing oparty na plikach. Client Components obsługują interaktywność tam gdzie jest potrzebna. Razem z Magento GraphQL API to solidny stack dla nowoczesnego sklepu headless.

About Henryk Tews

Co możesz przeczytać następne

TypeScript dla PHP developera – typy, interface, generics, async/await
React.js – JSX, useState, useEffect, custom hooks, porównanie z Vue
Vue.js dla PHP developera – Options API, Composition API, komunikacja z Magento REST
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}