Strapi
Next.js
CMS
Headless
TypeScript
API

Erstellen eines benutzerdefinierten Headless CMS mit Strapi und Next.js

Erstellen eines benutzerdefinierten Headless CMS mit Strapi und Next.js

Im Jahr 2025 wächst die Nachfrage nach Headless-CMSs weiter, da Unternehmen nach flexiblen und skalierbaren Lösungen für die Verwaltung von Inhalten suchen. In diesem Leitfaden erstellen wir ein vollständiges Headless-CMS mit Strapi als Backend und Next.js als Frontend und integrieren die neuesten Praktiken und Funktionen beider Technologien.

Systemarchitektur

Die Architektur unseres Headless CMS wird auf Microservices basieren, mit einer klaren Trennung zwischen Backend (Strapi) und Frontend (Next.js). Dies ermöglicht eine größere Flexibilität, Skalierbarkeit und Wartbarkeit.

Hauptkomponenten

  1. Backend (Strapi)

    • RESTful-API
    • Authentifizierung und Autorisierung
    • Content-Management
    • Medien-Upload
    • Benutzerdefinierte Plugins
  2. Frontend (Next.js)

    • Serverkomponenten
    • Hybrid-Rendering
    • Cache und Revalidierung
    • Vorschaumodus
    • Optimierte SEO
  3. Infrastruktur

    • PostgreSQL
    • Redis-Cache
    • CDN
    • Container-Orchestrierung

Backend-Implementierung

1. Strapi-Konfiguration

// 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. Arten von Inhalten

// 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. Benutzerdefinierte API

// 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); }, }) );

Frontend-Implementierung

1. Next.js-Setup

// 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. API-Client

// 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. Komponenten

// 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. Seiten und Routen

// 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> ); }

Bereitstellung und Infrastruktur

1. Docker Compose

## 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. GitHub-Aktionen

## .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/

Best Practices

1. Leistung

  • Implementieren Sie Caching in mehreren Ebenen
  • Optimieren Sie Assets und Bilder
  • Verwenden Sie CDN
  • Lazy Loading implementieren
  • Überwachen Sie Leistungskennzahlen

2. Sicherheit

  • Konfigurieren Sie CORS entsprechend
  • Ratenbegrenzung implementieren
  • Verwenden Sie Umgebungsvariablen
  • Eingänge desinfizieren
  • Halten Sie Abhängigkeiten auf dem neuesten Stand

3. SEO

  • Implementieren Sie dynamische Meta-Tags
  • Sitemap.xml generieren
  • Verwenden Sie benutzerfreundliche URLs
  • Leistung optimieren
  • Implementieren Sie schema.org

Fazit

Der Aufbau eines Headless CMS mit Strapi und Next.js bietet:

  1. Flexibilität: Modulare und erweiterbare Architektur
  2. Leistung: Optimierung der Inhaltsbereitstellung
  3. Sicherheit: Volle Kontrolle über die Implementierung
  4. Skalierbarkeit: Verteilte Architektur
  5. Wartbarkeit: Organisierter und gut dokumentierter Code

Nächste Schritte

  1. Implementieren Sie erweiterte Funktionen
  2. Datenanalyse hinzufügen
  3. Erweitern Sie die Integrationen
  4. Optimieren Sie die Größe
  5. Sammeln Sie Benutzerfeedback

Entwickeln Sie ein Headless-CMS? Teilen Sie Ihre Erfahrungen und Fragen in den Kommentaren unten!

Lesen Sie auch