Tutorials

WordPress als Headless CMS: Die moderne Art, Websites zu bauen

Erfahre, wie du WordPress als Headless CMS nutzen und mit modernen Frontend-Frameworks wie Next.js oder Nuxt kombinieren kannst.

16 Minuten Lesezeit
Maurice Naef
WordPress als Headless CMS: Die moderne Art, Websites zu bauen

WordPress als Headless CMS: Die moderne Art, Websites zu bauen

WordPress betreibt über 40% des Internets, aber wusstest du, dass du es auch als modernes Headless CMS nutzen kannst? In diesem Tutorial zeige ich dir, wie du WordPress mit modernen Frontend-Frameworks kombinierst und das Beste aus beiden Welten rausholst.

Was ist ein Headless CMS?

Ein Headless CMS trennt das Backend (Content Management) vom Frontend (Präsentation). WordPress wird dabei nur für die Inhaltsverwaltung genutzt, während die Darstellung über eine separate Anwendung erfolgt.

Vorteile des Headless-Ansatzes

Performance: Statische Seiten sind blitzschnell ✅ Sicherheit: Kein direkter Zugriff auf WordPress ✅ Flexibilität: Jedes Frontend-Framework möglich ✅ Skalierbarkeit: CDN-Distribution einfach möglich ✅ Developer Experience: Moderne Tools und Workflows

WordPress für Headless vorbereiten

1. REST API aktivieren

WordPress hat eine eingebaute REST API. Prüfe die Verfügbarkeit:

# Teste deine WordPress REST API
curl https://deine-domain.ch/wp-json/wp/v2/posts

2. Nützliche Plugins installieren

// Empfohlene Plugins für Headless WordPress
$headless_plugins = [
    'acf-to-rest-api' => 'Advanced Custom Fields in API',
    'wp-graphql' => 'GraphQL Alternative zu REST',
    'jwt-authentication' => 'Sichere API-Authentifizierung',
    'headless-mode' => 'Deaktiviert WordPress Frontend'
];

3. Custom Post Types und Fields

// functions.php - Custom Post Type für Projekte
function create_project_post_type() {
    register_post_type('project',
        array(
            'labels' => array(
                'name' => __('Projekte'),
                'singular_name' => __('Projekt')
            ),
            'public' => true,
            'has_archive' => true,
            'show_in_rest' => true, // Wichtig für REST API!
            'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
            'menu_icon' => 'dashicons-portfolio'
        )
    );
}
add_action('init', 'create_project_post_type');

// REST API erweitern
add_action('rest_api_init', function() {
    register_rest_field('project', 'featured_image_url', array(
        'get_callback' => function($post) {
            return get_the_post_thumbnail_url($post['id'], 'full');
        }
    ));
});

Frontend mit Next.js aufbauen

1. Next.js Projekt erstellen

npx create-next-app@latest wordpress-frontend
cd wordpress-frontend
npm install axios swr

2. WordPress API Service

// lib/wordpress.js
const API_URL = process.env.NEXT_PUBLIC_WORDPRESS_API_URL;

export async function getAllPosts() {
  const response = await fetch(`${API_URL}/posts?_embed`);
  const posts = await response.json();
  return posts;
}

export async function getPost(slug) {
  const response = await fetch(
    `${API_URL}/posts?slug=${slug}&_embed`
  );
  const posts = await response.json();
  return posts[0];
}

export async function getAllCategories() {
  const response = await fetch(`${API_URL}/categories`);
  const categories = await response.json();
  return categories;
}

3. Blog-Übersicht implementieren

// pages/blog/index.js
import { getAllPosts } from '../../lib/wordpress';
import Link from 'next/link';
import Image from 'next/image';

export default function BlogIndex({ posts }) {
  return (
    <div className="container mx-auto px-4">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {posts.map((post) => (
          <article key={post.id} className="border rounded-lg overflow-hidden">
            {post._embedded?.['wp:featuredmedia']?.[0] && (
              <Image
                src={post._embedded['wp:featuredmedia'][0].source_url}
                alt={post.title.rendered}
                width={400}
                height={250}
                className="w-full h-48 object-cover"
              />
            )}
            <div className="p-6">
              <h2 className="text-xl font-semibold mb-2">
                <Link href={`/blog/${post.slug}`}>
                  {post.title.rendered}
                </Link>
              </h2>
              <div 
                className="text-gray-600 line-clamp-3"
                dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
              />
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

export async function getStaticProps() {
  const posts = await getAllPosts();
  return {
    props: { posts },
    revalidate: 60 // ISR: Seite alle 60 Sekunden neu generieren
  };
}

4. Einzelne Blog-Posts

// pages/blog/[slug].js
import { getPost, getAllPosts } from '../../lib/wordpress';

export default function BlogPost({ post }) {
  if (!post) return <div>Loading...</div>;

  return (
    <article className="container mx-auto px-4 max-w-4xl">
      <h1 className="text-4xl font-bold mb-4">
        {post.title.rendered}
      </h1>
      <div className="text-gray-600 mb-8">
        {new Date(post.date).toLocaleDateString('de-DE')}
      </div>
      <div 
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content.rendered }}
      />
    </article>
  );
}

export async function getStaticPaths() {
  const posts = await getAllPosts();
  const paths = posts.map((post) => ({
    params: { slug: post.slug }
  }));

  return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
  const post = await getPost(params.slug);
  return {
    props: { post },
    revalidate: 60
  };
}

GraphQL mit WPGraphQL

1. GraphQL Setup

# Im WordPress Plugin-Verzeichnis
wp plugin install wp-graphql --activate

2. GraphQL Queries

// lib/graphql.js
const GRAPHQL_URL = process.env.NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL;

export async function fetchAPI(query, { variables } = {}) {
  const headers = { 'Content-Type': 'application/json' };

  const res = await fetch(GRAPHQL_URL, {
    method: 'POST',
    headers,
    body: JSON.stringify({ query, variables }),
  });

  const json = await res.json();
  if (json.errors) {
    console.error(json.errors);
    throw new Error('Failed to fetch API');
  }
  return json.data;
}

export async function getAllPostsWithSlug() {
  const data = await fetchAPI(`
    {
      posts(first: 10000) {
        edges {
          node {
            slug
          }
        }
      }
    }
  `);
  return data?.posts;
}

export async function getPostAndMorePosts(slug) {
  const data = await fetchAPI(
    `
    query PostBySlug($id: ID!) {
      post(id: $id, idType: SLUG) {
        title
        slug
        content
        date
        featuredImage {
          node {
            sourceUrl
          }
        }
        categories {
          edges {
            node {
              name
            }
          }
        }
      }
      posts(first: 3, where: { notIn: [$id] }) {
        edges {
          node {
            title
            slug
            featuredImage {
              node {
                sourceUrl
              }
            }
          }
        }
      }
    }
  `,
    {
      variables: {
        id: slug,
      },
    }
  );
  return data;
}

Advanced Custom Fields (ACF) integrieren

1. ACF Felder erstellen

// ACF Feldgruppe für Projekte
if( function_exists('acf_add_local_field_group') ):

acf_add_local_field_group(array(
    'key' => 'group_project_details',
    'title' => 'Projekt Details',
    'fields' => array(
        array(
            'key' => 'field_client',
            'label' => 'Kunde',
            'name' => 'client',
            'type' => 'text',
        ),
        array(
            'key' => 'field_technologies',
            'label' => 'Technologien',
            'name' => 'technologies',
            'type' => 'repeater',
            'sub_fields' => array(
                array(
                    'key' => 'field_tech_name',
                    'label' => 'Technologie',
                    'name' => 'name',
                    'type' => 'text',
                ),
            ),
        ),
        array(
            'key' => 'field_gallery',
            'label' => 'Galerie',
            'name' => 'gallery',
            'type' => 'gallery',
        ),
    ),
    'location' => array(
        array(
            array(
                'param' => 'post_type',
                'operator' => '==',
                'value' => 'project',
            ),
        ),
    ),
    'show_in_rest' => true, // Wichtig für REST API
));

endif;

2. ACF Daten im Frontend nutzen

// components/ProjectDetail.js
export default function ProjectDetail({ project }) {
  return (
    <div className="project-detail">
      <h1>{project.title.rendered}</h1>
      
      {project.acf?.client && (
        <p className="client">Kunde: {project.acf.client}</p>
      )}
      
      {project.acf?.technologies && (
        <div className="technologies">
          <h3>Verwendete Technologien:</h3>
          <ul>
            {project.acf.technologies.map((tech, index) => (
              <li key={index}>{tech.name}</li>
            ))}
          </ul>
        </div>
      )}
      
      {project.acf?.gallery && (
        <div className="gallery grid grid-cols-3 gap-4">
          {project.acf.gallery.map((image) => (
            <Image
              key={image.id}
              src={image.url}
              alt={image.alt}
              width={400}
              height={300}
              className="rounded-lg"
            />
          ))}
        </div>
      )}
    </div>
  );
}

Preview-Funktionalität implementieren

1. Preview API Route

// pages/api/preview.js
export default async function preview(req, res) {
  const { secret, slug } = req.query;

  // Check the secret and next parameters
  if (secret !== process.env.WORDPRESS_PREVIEW_SECRET || !slug) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  // Fetch the headless CMS to check if the provided slug exists
  const post = await getPostBySlug(slug);

  // If the slug doesn't exist prevent preview mode from being enabled
  if (!post) {
    return res.status(401).json({ message: 'Invalid slug' });
  }

  // Enable Preview Mode
  res.setPreviewData({
    post: {
      slug: post.slug,
      status: post.status,
    },
  });

  // Redirect to the path from the fetched post
  res.redirect(`/blog/${post.slug}`);
}

Deployment und Performance

1. Umgebungsvariablen

# .env.local
NEXT_PUBLIC_WORDPRESS_API_URL=https://api.ihre-domain.ch/wp-json/wp/v2
NEXT_PUBLIC_WORDPRESS_GRAPHQL_URL=https://api.ihre-domain.ch/graphql
WORDPRESS_PREVIEW_SECRET=ihr-geheimer-preview-key

2. Build-Optimierungen

// next.config.js
module.exports = {
  images: {
    domains: ['api.ihre-domain.ch'],
  },
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'X-DNS-Prefetch-Control',
            value: 'on'
          },
          {
            key: 'X-Frame-Options',
            value: 'SAMEORIGIN'
          }
        ],
      },
    ];
  },
};

3. Caching-Strategien

// lib/cache.js
import { unstable_cache } from 'next/cache';

export const getCachedPosts = unstable_cache(
  async () => {
    return await getAllPosts();
  },
  ['posts'],
  {
    revalidate: 3600, // 1 Stunde
    tags: ['posts'],
  }
);

Security Best Practices

1. API-Zugriff absichern

// WordPress: API nur für bestimmte Endpoints öffnen
add_filter('rest_authentication_errors', function($result) {
    if (!empty($result)) {
        return $result;
    }
    
    if (!is_user_logged_in() && 
        strpos($_SERVER['REQUEST_URI'], '/wp/v2/posts') === false &&
        strpos($_SERVER['REQUEST_URI'], '/wp/v2/pages') === false) {
        return new WP_Error('rest_not_logged_in', 
            'You are not currently logged in.', 
            array('status' => 401));
    }
    
    return $result;
});

2. CORS konfigurieren

// WordPress: CORS Headers
add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        $origin = get_http_origin();
        $allowed_origins = ['https://ihre-frontend-domain.ch'];
        
        if ($origin && in_array($origin, $allowed_origins)) {
            header('Access-Control-Allow-Origin: ' . $origin);
            header('Access-Control-Allow-Methods: GET');
            header('Access-Control-Allow-Credentials: true');
        }
        
        return $value;
    });
});

Fazit

WordPress als Headless CMS bietet das Beste aus zwei Welten: Die vertraute Inhaltsverwaltung von WordPress kombiniert mit der Performance und Flexibilität moderner Frontend-Frameworks.

Mit dieser Architektur kannst du:

  • Blitzschnelle Websites bauen
  • Moderne Developer-Tools nutzen
  • Skalierbare Anwendungen erstellen
  • Die Sicherheit erhöhen

Der Umstieg erfordert zwar initial mehr Setup, zahlt sich aber durch bessere Performance, Developer Experience und Zukunftssicherheit aus. Probier es aus - deine User (und Entwickler) werden es dir danken!

Lass uns gemeinsam etwas Grossartiges umsetzen

Ich bin offen für neue Ideen, spannende Projekte und gute Gespräche. Melde dich, wenn du das Gefühl hast, dass es passen könnte.