🎨 Sysprovider Code
Sysprovider LogoWiki
🇪🇸Hosting español para ecommerce

Implementación de Headless PrestaShop con Next.js

Actualizado el 29 de octubre de 2025

[INFO] Este artículo está dirigido a desarrolladores y responsables técnicos de tiendas PrestaShop que buscan modernizar su arquitectura. Asumimos conocimientos previos de React, APIs REST y administración básica de PrestaShop.

La industria del ecommerce está viviendo una transformación radical. Los sistemas monolíticos, donde el frontend y el backend están acoplados, están dando paso a arquitecturas desacopladas y flexibles. En este contexto, Headless PrestaShop emerge como una solución potente para quienes desean mantener la robustez de PrestaShop como motor de comercio, pero liberar el frontend de sus limitaciones tradicionales. Combinarlo con Next.js, el framework React para producción, permite crear experiencias de usuario ultrarrápidas, altamente personalizables y con un rendimiento SEO excepcional.

Este artículo te guiará a través de la implementación práctica de una tienda Headless PrestaShop con Next.js, aprovechando al máximo la PrestaShop API REST. Exploraremos desde la configuración inicial hasta la optimización para motores de búsqueda, pasando por la gestión de catálogo, carrito y checkout.

¿Por qué Headless PrestaShop con Next.js?

Antes de sumergirnos en el código, entendamos el valor de esta combinación.

Ventajas de la arquitectura Headless

  • Rendimiento superior: Next.js ofrece renderizado del lado del servidor (SSR) y generación de sitios estáticos (SSG). Esto significa que las páginas de producto y categoría se sirven como HTML pre-renderizado, reduciendo drásticamente los tiempos de carga. Un Next.js ecommerce es inherentemente más rápido que uno basado en el frontend nativo de PrestaShop.
  • Experiencia de usuario (UX) moderna: Libertad total para diseñar interfaces complejas, interactivas y personalizadas sin las restricciones del sistema de plantillas de PrestaShop. Puedes implementar animaciones, micro-interacciones y lógicas de frontend avanzadas con React.
  • Separación de preocupaciones: El equipo de frontend (React/Next.js) y el de backend (PrestaShop/PHP) pueden trabajar de forma independiente. Se acelera el desarrollo y se facilita el mantenimiento.
  • Escalabilidad y omnicanalidad: El mismo backend de PrestaShop puede alimentar un frontend web (Next.js), una app móvil (React Native), un quiosco interactivo o cualquier otro dispositivo. La API es el único punto de contacto.
  • SEO mejorado: Next.js está diseñado para SEO. Con SSR y SSG, los motores de búsqueda indexan el contenido completo de tu tienda sin necesidad de JavaScript. Esto es crítico para cualquier Next.js ecommerce.

¿Qué necesitas?

  • Una instalación de PrestaShop 1.7+ (preferiblemente 8.x) con el módulo de API REST habilitado por defecto.
  • Node.js 18+ y npm/yarn/pnpm.
  • Conocimientos básicos de React y Next.js (App Router).
  • Claves de API de PrestaShop (generadas desde el panel de administración: Servicio Web).

Configuración del Proyecto Next.js

Comenzaremos creando un proyecto Next.js limpio y configurando la comunicación con la API.

1. Crear el proyecto

npx create-next-app@latest prestashop-headless --typescript --tailwind --app
cd prestashop-headless

Elegimos TypeScript y Tailwind CSS para un desarrollo más robusto y estilizado rápido.

2. Configurar el cliente API

Crearemos un módulo para centralizar las llamadas a la PrestaShop API REST. La API de PrestaShop utiliza autenticación basada en clave API (a través de un parámetro ?ws_key=... o un header Authorization: Basic).

Crea el archivo lib/api.ts:

// lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_PRESTASHOP_API_URL || 'http://tudominio.com/api';
const API_KEY = process.env.PRESTASHOP_API_KEY;

if (!API_KEY) {
  throw new Error('PRESTASHOP_API_KEY no está definida en las variables de entorno');
}

const headers = new Headers();
headers.set('Authorization', 'Basic ' + Buffer.from(API_KEY + ':').toString('base64'));
headers.set('Output-Format', 'JSON'); // ¡Importante! PrestaShop devuelve XML por defecto.
headers.set('Content-Type', 'application/json'); // Para peticiones POST/PUT

interface ApiResponse<T> {
  data: T;
  // ... otros campos de error si es necesario
}

async function fetchApi<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
  const url = `${API_BASE_URL}${endpoint}`;
  const response = await fetch(url, {
    ...options,
    headers: {
      ...headers,
      ...options.headers,
    },
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`API Error: ${response.status} - ${errorText}`);
  }

  // La API de PrestaShop envuelve los resultados en un objeto con la clave del recurso (ej: products, categories)
  const result = await response.json();
  // Asumimos que la respuesta tiene una clave principal, ej: result.products, result.categories
  // Para simplificar, devolvemos el objeto completo y se accede según el endpoint.
  return result as T;
}

// Tipos básicos (puedes expandirlos con más campos)
export interface PrestaProduct {
  id: string;
  id_default_image: string;
  name: string;
  description: string;
  price: string;
  // ... muchos más campos
}

export interface PrestaCategory {
  id: string;
  name: string;
  description: string;
  // ...
}

// Funciones helper para recursos comunes
export const getProducts = async (params?: string): Promise<{ products: PrestaProduct[] }> => {
  return fetchApi(`/products?${params || ''}&display=full&limit=20`);
};

export const getProductById = async (id: string): Promise<{ product: PrestaProduct }> => {
  return fetchApi(`/products/${id}`);
};

export const getCategories = async (): Promise<{ categories: PrestaCategory[] }> => {
  return fetchApi('/categories?display=full&limit=50');
};

export default fetchApi;

[WARNING] Asegúrate de que la URL de tu API sea accesible desde el frontend. Si tu PrestaShop está en un servidor local o con restricciones CORS, deberás configurar un proxy en Next.js (en next.config.js) o usar una variable de entorno para la URL completa. Nunca expongas tu PRESTASHOP_API_KEY en el cliente.

Construyendo las Páginas Principales

Ahora implementaremos las vistas clave de un Next.js ecommerce.

Página de Listado de Productos (SSG)

Usaremos Generación Estática (SSG) para que las páginas de categoría se construyan en tiempo de build, ofreciendo velocidad máxima.

Crea app/category/[slug]/page.tsx:

// app/category/[slug]/page.tsx
import { getCategories, getProducts, PrestaProduct } from '@/lib/api';

interface Props {
  params: { slug: string };
}

// Generar rutas estáticas para cada categoría
export async function generateStaticParams() {
  const { categories } = await getCategories();
  return categories.map((cat) => ({
    slug: cat.id, // O usa cat.link_rewrite si lo prefieres
  }));
}

async function CategoryPage({ params }: Props) {
  const { slug } = params;
  // Obtener productos de la categoría (necesitarías filtrar por id_category_default)
  const { products } = await getProducts(`filter[id_category_default]=${slug}`);

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Categoría {slug}</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
        {products.map((product: PrestaProduct) => (
          <div key={product.id} className="border rounded-lg p-4 shadow hover:shadow-lg transition">
            <img
              src={`http://tudominio.com/img/p/${product.id_default_image}-home_default.jpg`}
              alt={product.name}
              className="w-full h-48 object-cover mb-4"
            />
            <h2 className="text-xl font-semibold">{product.name}</h2>
            <p className="text-gray-600">{product.price} €</p>
            <a
              href={`/product/${product.id}`}
              className="mt-2 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
            >
              Ver producto
            </a>
          </div>
        ))}
      </div>
    </div>
  );
}

export default CategoryPage;

Página de Detalle de Producto (SSR)

Para el detalle del producto, usaremos SSR para tener datos siempre frescos (precios, stock). Crea app/product/[id]/page.tsx:

// app/product/[id]/page.tsx
import { getProductById } from '@/lib/api';
import { notFound } from 'next/navigation';

interface Props {
  params: { id: string };
}

export default async function ProductPage({ params }: Props) {
  let product;
  try {
    const response = await getProductById(params.id);
    product = response.product;
  } catch (error) {
    notFound(); // Muestra la página 404 si no existe
  }

  return (
    <div className="container mx-auto p-4 flex flex-col md:flex-row gap-8">
      <div className="md:w-1/2">
        <img
          src={`http://tudominio.com/img/p/${product.id_default_image}-large_default.jpg`}
          alt={product.name}
          className="w-full rounded-lg shadow"
        />
      </div>
      <div className="md:w-1/2">
        <h1 className="text-4xl font-bold mb-4">{product.name}</h1>
        <p className="text-2xl text-green-600 font-semibold mb-4">{product.price} €</p>
        <div
          className="prose max-w-none mb-6"
          dangerouslySetInnerHTML={{ __html: product.description }}
        />
        <button className="bg-black text-white px-6 py-3 rounded-lg text-lg hover:bg-gray-800 transition">
          Añadir al carrito
        </button>
      </div>
    </div>
  );
}

[TIP] Para manejar el carrito de forma headless, necesitarás implementar la lógica de carrito de PrestaShop a través de su API. Normalmente, se crea un carrito anónimo (con un id_cart) y se gestiona mediante endpoints como /carts, /cart_products, etc. Esta lógica puede residir en un contexto de React o en una librería de estado como Zustand.

Gestión Dinámica del Carrito

La gestión del carrito es uno de los puntos más delicados en una implementación headless PrestaShop. Aquí te mostramos un esqueleto de cómo podrías abordarlo.

Crea un contexto context/CartContext.tsx:

// context/CartContext.tsx
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import fetchApi from '@/lib/api';

interface CartItem {
  id_product: string;
  id_product_attribute?: string;
  quantity: number;
  name: string;
  price: string;
}

interface CartContextType {
  cart: CartItem[];
  addToCart: (productId: string, quantity: number) => Promise<void>;
  removeFromCart: (productId: string) => Promise<void>;
  total: number;
}

const CartContext = createContext<CartContextType | undefined>(undefined);

export function CartProvider({ children }: { children: React.ReactNode }) {
  const [cart, setCart] = useState<CartItem[]>([]);
  const [cartId, setCartId] = useState<string | null>(null);

  // Inicializar carrito (crear uno si no existe)
  useEffect(() => {
    const storedCartId = localStorage.getItem('prestashop_cart_id');
    if (storedCartId) {
      setCartId(storedCartId);
      // Cargar contenido del carrito desde la API
      fetchApi(`/carts/${storedCartId}`).then((data: any) => {
        // Procesar y setear los items
        setCart(data.cart.associations.cart_rows || []);
      });
    } else {
      // Crear un nuevo carrito en PrestaShop
      fetchApi('/carts', {
        method: 'POST',
        body: JSON.stringify({ id_currency: '1', id_lang: '1' }),
      }).then((data: any) => {
        const newCartId = data.cart.id;
        localStorage.setItem('prestashop_cart_id', newCartId);
        setCartId(newCartId);
      });
    }
  }, []);

  const addToCart = async (productId: string, quantity: number) => {
    if (!cartId) return;
    // Llamar a la API para añadir el producto
    await fetchApi(`/carts/${cartId}`, {
      method: 'PATCH',
      body: JSON.stringify({
        product: [{ id: productId, quantity }],
      }),
    });
    // Refrescar el carrito local
    const updatedCart = await fetchApi(`/carts/${cartId}`);
    setCart(updatedCart.cart.associations.cart_rows);
  };

  // ... removeFromCart similar

  const total = cart.reduce((sum, item) => sum + parseFloat(item.price) * item.quantity, 0);

  return (
    <CartContext.Provider value={{ cart, addToCart, removeFromCart, total }}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart debe usarse dentro de CartProvider');
  return context;
};

[WARNING] La API de carrito de PrestaShop puede ser compleja. Asegúrate de leer la documentación oficial sobre cómo gestionar id_product_attribute (combinaciones) y customization (personalizaciones). Además, considera usar tokens de cliente para mantener la sesión.

SEO y Optimización para Motores de Búsqueda

El SEO es un pilar fundamental de cualquier Next.js ecommerce. La arquitectura headless, bien implementada, puede superar al SEO de un PrestaShop tradicional.

Metadatos dinámicos con Next.js

Next.js 13+ permite exportar metadatos desde los componentes de página. En tu página de producto:

// app/product/[id]/page.tsx (añadir al inicio)
import { Metadata } from 'next';

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await getProductById(params.id);
  return {
    title: `${product.product.name} | Tu Tienda`,
    description: product.product.description_short || product.product.description?.substring(0, 160),
    openGraph: {
      title: product.product.name,
      description: product.product.description_short,
      images: [`http://tudominio.com/img/p/${product.product.id_default_image}-large_default.jpg`],
    },
  };
}

Generación de Sitemaps

Crea app/sitemap.ts para generar un sitemap dinámico que incluya todos tus productos y categorías:

// app/sitemap.ts
import { getProducts, getCategories } from '@/lib/api';

export default async function sitemap() {
  const baseUrl = 'https://tudominio.com';

  // Productos
  const { products } = await getProducts('display=[id,date_upd]&limit=10000');
  const productUrls = products.map((product) => ({
    url: `${baseUrl}/product/${product.id}`,
    lastModified: product.date_upd,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }));

  // Categorías
  const { categories } = await getCategories();
  const categoryUrls = categories.map((category) => ({
    url: `${baseUrl}/category/${category.id}`,
    lastModified: new Date().toISOString(),
    changeFrequency: 'daily' as const,
    priority: 0.5,
  }));

  return [...productUrls, ...categoryUrls];
}

Optimización de Imágenes

Usa el componente next/image para servir imágenes optimizadas en formato WebP y con lazy loading:

import Image from 'next/image';
// ...
<Image
  src={`http://t

¿Necesitas ayuda?Son dos de nuestros técnicos, Agustín y Mikel, y están disponibles para resolver cualquier problema.

Hablar con ellos ahora
Agustín y Mikel