Strapi
Next.js
CMS
Headless
TypeScript
API

Costruire un CMS headless personalizzato con Strapi e Next.js

Costruire un CMS headless personalizzato con Strapi e Next.js

Nel 2025, la domanda di CMS headless continua a crescere, con le aziende alla ricerca di soluzioni flessibili e scalabili per gestire i contenuti. In questa guida, costruiremo un CMS headless completo utilizzando Strapi come backend e Next.js come frontend, incorporando le pratiche e le funzionalità più recenti di entrambe le tecnologie.

Architettura del sistema

L'architettura del nostro CMS headless sarà basata su microservizi, con una netta separazione tra backend (Strapi) e frontend (Next.js). Ciò consente una maggiore flessibilità, scalabilità e manutenibilità.

Componenti principali

  1. Backend (Strapi)

    • API REST
    • Autenticazione e autorizzazione
    • Gestione dei contenuti
    • Caricamento multimediale
    • Plugin personalizzati
  2. Frontend (Next.js)

    • Componenti del server
    • Rendering ibrido
    • Cache e riconvalida
    • Modalità di anteprima -SEO ottimizzato
  3. Infrastrutture

    • PostgreSQL
    • Cache Redis
    • CDN
    • Orchestrazione dei contenitori

Implementazione del back-end

1. Configurazione Strapi

// config/database.ts export default ({ env }) => ({ connection: { client: 'postgres', connection: { host: env('DATABASE_HOST'), port: env.int('DATABASE_PORT'), database: env('DATABASE_NAME'), user: env('DATABASE_USERNAME'), password: env('DATABASE_PASSWORD'), ssl: env.bool('DATABASE_SSL'), }, pool: { min: 0, max: 10, }, }, }); // config/plugins.ts export default { upload: { config: { provider: 'aws-s3', providerOptions: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_ACCESS_SECRET, region: process.env.AWS_REGION, params: { Bucket: process.env.AWS_BUCKET, }, }, }, }, email: { config: { provider: 'sendgrid', providerOptions: { apiKey: process.env.SENDGRID_API_KEY, }, settings: { defaultFrom: 'no-reply@example.com', defaultReplyTo: 'contact@example.com', }, }, }, 'users-permissions': { config: { jwt: { expiresIn: '7d', }, }, }, };

2. Tipi di contenuto

// api/article/content-types/article/schema.json { "kind": "collectionType", "collectionName": "articles", "info": { "singularName": "article", "pluralName": "articles", "displayName": "Article" }, "options": { "draftAndPublish": true, "versions": true }, "attributes": { "title": { "type": "string", "required": true, "unique": true }, "slug": { "type": "uid", "targetField": "title" }, "content": { "type": "richtext", "required": true }, "coverImage": { "type": "media", "multiple": false, "required": true }, "categories": { "type": "relation", "relation": "manyToMany", "target": "api::category.category", "inversedBy": "articles" }, "author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user" }, "seo": { "type": "component", "component": "shared.seo", "required": true } } }

3. API personalizzata

// api/article/controllers/article.ts export default factories.createCoreController( 'api::article.article', ({ strapi }) => ({ async findBySlug(ctx) { const { slug } = ctx.params; const entity = await strapi.db.query('api::article.article').findOne({ where: { slug }, populate: ['coverImage', 'categories', 'author', 'seo'], }); if (!entity) { return ctx.notFound('Article not found'); } return this.transformResponse(entity); }, async findFeatured(ctx) { const entities = await strapi.db.query('api::article.article').findMany({ where: { featured: true, publishedAt: { $notNull: true }, }, limit: 5, orderBy: { publishedAt: 'desc' }, populate: ['coverImage', 'categories'], }); return this.transformResponse(entities); }, }) );

Implementazione del frontend

1. Installazione di Next.js

// app/layout.tsx import { Inter } from 'next/font/google'; import { Analytics } from '@vercel/analytics/react'; import { Providers } from './providers'; const inter = Inter({ subsets: ['latin'] }); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" suppressHydrationWarning> <body className={inter.className}> <Providers> {children} </Providers> <Analytics /> </body> </html> ); }

2. Client API

// lib/api/client.ts import qs from 'qs'; const API_URL = process.env.NEXT_PUBLIC_STRAPI_API_URL; async function fetchAPI( path: string, urlParamsObject = {}, options = {} ) { try { const mergedOptions = { next: { revalidate: 60 }, headers: { 'Content-Type': 'application/json', }, ...options, }; const queryString = qs.stringify(urlParamsObject, { encodeValuesOnly: true, }); const requestUrl = `${API_URL}/api${path}${queryString ? `?${queryString}` : ''}`; const response = await fetch(requestUrl, mergedOptions); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { console.error(error); throw error; } } export async function getArticles(params = {}) { const data = await fetchAPI('/articles', { populate: ['coverImage', 'categories', 'author'], sort: ['publishedAt:desc'], ...params, }); return data; } export async function getArticle(slug: string) { const data = await fetchAPI(`/articles/${slug}`, { populate: ['coverImage', 'categories', 'author', 'seo'], }); return data; }

3. Componenti

// components/ArticleCard.tsx import Image from 'next/image'; import Link from 'next/link'; import { format } from 'date-fns'; interface ArticleCardProps { article: { attributes: { title: string; slug: string; excerpt: string; publishedAt: string; coverImage: { data: { attributes: { url: string; alternativeText: string; }; }; }; }; }; } export function ArticleCard({ article }: ArticleCardProps) { const { attributes } = article; return ( <Link href={`/articles/${attributes.slug}`} className="group block" > <article className="space-y-4"> <div className="aspect-w-16 aspect-h-9 overflow-hidden rounded-lg"> <Image src={attributes.coverImage.data.attributes.url} alt={attributes.coverImage.data.attributes.alternativeText} fill className="object-cover transition group-hover:scale-105" /> </div> <div className="space-y-2"> <h3 className="text-xl font-semibold line-clamp-2"> {attributes.title} </h3> <p className="text-gray-600 line-clamp-3"> {attributes.excerpt} </p> <time dateTime={attributes.publishedAt} className="text-sm text-gray-500" > {format( new Date(attributes.publishedAt), 'dd MMM, yyyy' )} </time> </div> </article> </Link> ); }

4. Pagine e Percorsi

// app/articles/page.tsx import { Suspense } from 'react'; import { getArticles } from '@/lib/api/client'; import { ArticleCard } from '@/components/ArticleCard'; import { Pagination } from '@/components/Pagination'; interface ArticlesPageProps { searchParams: { page?: string; category?: string; }; } export default async function ArticlesPage({ searchParams, }: ArticlesPageProps) { const page = Number(searchParams.page) || 1; const { data: articles, meta } = await getArticles({ pagination: { page, pageSize: 9, }, filters: searchParams.category ? { categories: { slug: searchParams.category, }, } : undefined, }); return ( <div className="space-y-8"> <h1 className="text-4xl font-bold">Articles</h1> <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3"> {articles.map((article) => ( <ArticleCard key={article.id} article={article} /> ))} </div> <Pagination currentPage={page} totalPages={meta.pagination.pageCount} baseUrl="/articles" /> </div> ); }

Distribuzione e infrastruttura

1. Finestra mobile Componi

## docker-compose.yml version: '3' services: strapi: build: ./backend environment: DATABASE_CLIENT: postgres DATABASE_HOST: postgres DATABASE_NAME: strapi DATABASE_USERNAME: strapi DATABASE_PASSWORD: strapi NODE_ENV: production depends_on: - postgres ports: - '1337:1337' postgres: image: postgres:15 environment: POSTGRES_DB: strapi POSTGRES_USER: strapi POSTGRES_PASSWORD: strapi volumes: - postgres-data:/var/lib/postgresql/data redis: image: redis:7 ports: - '6379:6379' nextjs: build: ./frontend environment: NEXT_PUBLIC_STRAPI_API_URL: http://strapi:1337 ports: - '3000:3000' depends_on: - strapi volumes: postgres-data:

2. Azioni GitHub

## .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '20' - name: Install dependencies run: | cd backend && yarn install cd ../frontend && yarn install - name: Build run: | cd backend && yarn build cd ../frontend && yarn build - name: Deploy env: DIGITALOCEAN_ACCESS_TOKEN: $ run: | doctl kubernetes cluster kubeconfig save cms-cluster kubectl apply -f k8s/

Migliori pratiche

1. Prestazioni

  • Implementare la memorizzazione nella cache su più livelli
  • Ottimizza risorse e immagini
  • Usa CDN
  • Implementare il caricamento lento
  • Monitorare le metriche delle prestazioni

2. Sicurezza

  • Configurare CORS in modo appropriato
  • Implementare la limitazione della velocità
  • Utilizzare variabili di ambiente
  • Sanificare gli ingressi
  • Mantieni aggiornate le dipendenze

3. SEO

  • Implementare meta tag dinamici
  • Genera sitemap.xml
  • Utilizza URL amichevoli
  • Ottimizzare le prestazioni
  • Implementare schema.org

Conclusione

Costruire un CMS headless con Strapi e Next.js offre:

  1. Flessibilità: architettura modulare ed estensibile
  2. Prestazioni: ottimizzazione della distribuzione dei contenuti
  3. Sicurezza: controllo completo sull'implementazione
  4. Scalabilità: architettura distribuita
  5. Manutenibilità: codice organizzato e ben documentato

Passaggi successivi

  1. Implementa funzionalità avanzate
  2. Aggiungi l'analisi dei dati
  3. Espandi le integrazioni
  4. Ottimizza per la scala
  5. Raccogli il feedback degli utenti

Stai costruendo un CMS headless? Condividi le tue esperienze e domande nei commenti qui sotto!

Leggi anche