Strapi Headless CMS Beispiele

Umfassende Strapi Beispiele mit Content Modeling, API Integration, Authentifizierung, Plugins und Deployment Patterns

💻 Strapi Content Modeling javascript

🟡 intermediate ⭐⭐⭐

Vollständige Content-Type-Definitionen, Beziehungen und Feldkonfigurationen für verschiedene Anwendungsfälle

⏱️ 35 min 🏷️ strapi, content modeling, cms
Prerequisites: Strapi basics, Content modeling concepts, JSON schema, Database concepts
// Strapi Content Modeling Examples
// File: config/api/article/content-types/article/schema.json

{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article",
    "description": "Blog articles with rich content and media"
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {
    "i18n": {
      "localized": true
    }
  },
  "attributes": {
    "title": {
      "type": "string",
      "required": true,
      "maxLength": 255,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "slug": {
      "type": "uid",
      "targetField": "title",
      "required": true
    },
    "excerpt": {
      "type": "text",
      "maxLength": 500,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "content": {
      "type": "richtext",
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "featured_image": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "gallery": {
      "type": "media",
      "multiple": true,
      "required": false,
      "allowedTypes": ["images"]
    },
    "category": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::category.category"
    },
    "tags": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::tag.tag"
    },
    "author": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::author.author"
    },
    "seo": {
      "type": "component",
      "repeatable": false,
      "component": "shared.seo"
    },
    "published_at": {
      "type": "datetime",
      "configurable": false,
      "writable": false,
      "default": "now"
    },
    "reading_time": {
      "type": "integer",
      "min": 0
    },
    "view_count": {
      "type": "integer",
      "default": 0,
      "private": true
    }
  }
}

// File: config/api/category/content-types/category/schema.json

{
  "kind": "collectionType",
  "collectionName": "categories",
  "info": {
    "singularName": "category",
    "pluralName": "categories",
    "displayName": "Category",
    "description": "Article categories for content organization"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {
    "i18n": {
      "localized": true
    }
  },
  "attributes": {
    "name": {
      "type": "string",
      "required": true,
      "maxLength": 100,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "slug": {
      "type": "uid",
      "targetField": "name",
      "required": true
    },
    "description": {
      "type": "text",
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "color": {
      "type": "string",
      "regex": "^#[0-9A-Fa-f]{6}$"
    },
    "icon": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "parent": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::category.category",
      "nullable": true
    },
    "children": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::category.category",
      "mappedBy": "parent",
      "private": true
    },
    "articles_count": {
      "type": "integer",
      "default": 0,
      "private": true
    }
  }
}

// File: config/api/product/content-types/product/schema.json

{
  "kind": "collectionType",
  "collectionName": "products",
  "info": {
    "singularName": "product",
    "pluralName": "products",
    "displayName": "Product",
    "description": "E-commerce products with variants and pricing"
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {
    "i18n": {
      "localized": true
    }
  },
  "attributes": {
    "name": {
      "type": "string",
      "required": true,
      "maxLength": 255,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "slug": {
      "type": "uid",
      "targetField": "name",
      "required": true
    },
    "description": {
      "type": "richtext",
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "short_description": {
      "type": "text",
      "maxLength": 500,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "sku": {
      "type": "string",
      "required": true,
      "unique": true,
      "regex": "^[A-Z0-9-]+$"
    },
    "price": {
      "type": "decimal",
      "required": true,
      "min": 0
    },
    "compare_price": {
      "type": "decimal",
      "min": 0
    },
    "cost": {
      "type": "decimal",
      "min": 0,
      "private": true
    },
    "images": {
      "type": "media",
      "multiple": true,
      "required": false,
      "allowedTypes": ["images"]
    },
    "variants": {
      "type": "component",
      "repeatable": true,
      "component": "product.product-variant"
    },
    "inventory": {
      "type": "component",
      "repeatable": false,
      "component": "product.inventory"
    },
    "shipping": {
      "type": "component",
      "repeatable": false,
      "component": "product.shipping"
    },
    "seo": {
      "type": "component",
      "repeatable": false,
      "component": "shared.seo"
    },
    "categories": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::product-category.product-category"
    },
    "brand": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::brand.brand"
    },
    "tags": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::tag.tag"
    },
    "attributes": {
      "type": "relation",
      "relation": "manyToMany",
      "target": "api::product-attribute.product-attribute"
    },
    "reviews": {
      "type": "relation",
      "relation": "oneToMany",
      "target": "api::review.review",
      "mappedBy": "product"
    },
    "featured": {
      "type": "boolean",
      "default": false
    },
    "available": {
      "type": "boolean",
      "default": true
    }
  }
}

// File: config/components/product/product-variant.json

{
  "collectionName": "components_product_variants",
  "info": {
    "displayName": "Product Variant",
    "icon": "cube",
    "description": "Product variants with different options and pricing"
  },
  "options": {},
  "attributes": {
    "name": {
      "type": "string",
      "required": true,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "sku": {
      "type": "string",
      "required": true,
      "unique": true
    },
    "price": {
      "type": "decimal",
      "required": true
    },
    "compare_price": {
      "type": "decimal"
    },
    "weight": {
      "type": "decimal"
    },
    "inventory": {
      "type": "integer",
      "default": 0
    },
    "options": {
      "type": "json",
      "default": {}
    },
    "image": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    }
  }
}

// File: config/components/shared/seo.json

{
  "collectionName": "components_shared_seos",
  "info": {
    "displayName": "SEO",
    "icon": "search",
    "description": "SEO metadata for search engine optimization"
  },
  "options": {},
  "pluginOptions": {
    "i18n": {
      "localized": true
    }
  },
  "attributes": {
    "meta_title": {
      "type": "string",
      "maxLength": 60,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "meta_description": {
      "type": "text",
      "maxLength": 160,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "keywords": {
      "type": "text"
    },
    "canonical_url": {
      "type": "string"
    },
    "og_image": {
      "type": "media",
      "multiple": false,
      "required": false,
      "allowedTypes": ["images"]
    },
    "structured_data": {
      "type": "json"
    },
    "meta_robots": {
      "type": "enumeration",
      "enum": ["index", "noindex", "follow", "nofollow"],
      "default": "index,follow"
    }
  }
}

// File: database/seeds/01-categories.js

module.exports = {
  async up(strapi) {
    // Create main categories
    const categories = [
      {
        name: 'Technology',
        slug: 'technology',
        description: 'Articles about technology and programming',
        color: '#3B82F6'
      },
      {
        name: 'Design',
        slug: 'design',
        description: 'Design articles and tutorials',
        color: '#8B5CF6'
      },
      {
        name: 'Business',
        slug: 'business',
        description: 'Business and entrepreneurship articles',
        color: '#10B981'
      },
      {
        name: 'Marketing',
        slug: 'marketing',
        description: 'Marketing strategies and tips',
        color: '#F59E0B'
      }
    ];

    for (const category of categories) {
      await strapi.query('api::category.category').create({
        data: category
      });
    }
  },

  async down(strapi) {
    // Remove seeded categories
    await strapi.query('api::category.category').deleteMany({
      where: {
        slug: ['technology', 'design', 'business', 'marketing']
      }
    });
  }
};

// File: database/seeds/02-tags.js

module.exports = {
  async up(strapi) {
    const tags = [
      { name: 'JavaScript', slug: 'javascript' },
      { name: 'React', slug: 'react' },
      { name: 'Node.js', slug: 'nodejs' },
      { name: 'CSS', slug: 'css' },
      { name: 'HTML', slug: 'html' },
      { name: 'TypeScript', slug: 'typescript' },
      { name: 'Vue.js', slug: 'vuejs' },
      { name: 'Angular', slug: 'angular' },
      { name: 'Python', slug: 'python' },
      { name: 'Machine Learning', slug: 'machine-learning' },
      { name: 'UI/UX', slug: 'ui-ux' },
      { name: 'Startup', slug: 'startup' },
      { name: 'SEO', slug: 'seo' },
      { name: 'Analytics', slug: 'analytics' },
      { name: 'E-commerce', slug: 'ecommerce' }
    ];

    for (const tag of tags) {
      await strapi.query('api::tag.tag').create({
        data: tag
      });
    }
  },

  async down(strapi) {
    const slugs = tags.map(tag => tag.slug);
    await strapi.query('api::tag.tag').deleteMany({
      where: {
        slug: slugs
      }
    });
  }
};

// File: database/seeds/03-authors.js

module.exports = {
  async up(strapi) {
    const authors = [
      {
        name: 'John Doe',
        email: '[email protected]',
        bio: 'Full-stack developer with 10+ years of experience',
        avatar: null,
        social: {
          twitter: 'johndoe',
          github: 'johndoe',
          linkedin: 'john-doe'
        }
      },
      {
        name: 'Jane Smith',
        email: '[email protected]',
        bio: 'UX designer passionate about user experience',
        avatar: null,
        social: {
          twitter: 'janesmith',
          dribbble: 'janesmith',
          behance: 'jane-smith'
        }
      }
    ];

    for (const author of authors) {
      await strapi.query('api::author.author').create({
        data: author
      });
    }
  },

  async down(strapi) {
    await strapi.query('api::author.author').deleteMany();
  }
};

💻 Strapi API Integration javascript

🔴 complex ⭐⭐⭐⭐

Vollständige API-Integrationsbeispiele mit REST und GraphQL Abfragen, Authentifizierung und Frontend-Implementierung

⏱️ 40 min 🏷️ strapi, api, integration, frontend
Prerequisites: Strapi basics, REST/GraphQL APIs, React hooks, JavaScript ES6+, Authentication concepts
// Strapi API Integration Examples
// File: lib/strapi.js - API Client Setup

class StrapiClient {
  constructor(baseURL, token = null) {
    this.baseURL = baseURL;
    this.token = token;
    this.cache = new Map();
  }

  setToken(token) {
    this.token = token;
  }

  async request(endpoint, options = {}) {
    const url = new URL(endpoint, this.baseURL);

    const config = {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    };

    if (this.token) {
      config.headers.Authorization = `Bearer ${this.token}`;
    }

    try {
      const response = await fetch(url.toString(), config);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.error('Strapi API Error:', error);
      throw error;
    }
  }

  async get(endpoint, params = {}) {
    const searchParams = new URLSearchParams();

    Object.keys(params).forEach(key => {
      if (Array.isArray(params[key])) {
        params[key].forEach(value => {
          searchParams.append(`${key}[]`, value);
        });
      } else if (params[key] !== undefined && params[key] !== null) {
        searchParams.append(key, params[key]);
      }
    });

    const queryString = searchParams.toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;

    // Check cache first
    const cacheKey = `GET_${url}`;
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }

    const data = await this.request(url);

    // Cache for 5 minutes
    this.cache.set(cacheKey, data);
    setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);

    return data;
  }

  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint) {
    return this.request(endpoint, {
      method: 'DELETE',
    });
  }

  clearCache() {
    this.cache.clear();
  }
}

// Create and export client instances
export const strapiClient = new StrapiClient(
  process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337'
);

export const strapiAdminClient = new StrapiClient(
  process.env.STRAPI_ADMIN_URL || 'http://localhost:1337',
  process.env.STRAPI_ADMIN_TOKEN
);

// File: lib/strapi-queries.js - Predefined Queries

export const strapiQueries = {
  // Articles with population
  getArticles: (params = {}) => ({
    endpoint: '/articles',
    params: {
      populate: ['*', 'featured_image', 'category', 'tags', 'author.avatar'],
      sort: 'publishedAt:desc',
      publicationState: 'live',
      ...params,
    },
  }),

  getArticleBySlug: (slug, params = {}) => ({
    endpoint: '/articles',
    params: {
      filters: { slug: { $eq: slug } },
      populate: ['*', 'featured_image', 'category', 'tags', 'author.avatar'],
      publicationState: 'live',
      ...params,
    },
  }),

  getRelatedArticles: (articleId, categoryId, limit = 3) => ({
    endpoint: '/articles',
    params: {
      filters: {
        id: { $ne: articleId },
        category: { id: { $eq: categoryId } },
      },
      populate: ['featured_image', 'category'],
      sort: 'publishedAt:desc',
      pagination: { limit, start: 0 },
      publicationState: 'live',
    },
  }),

  getCategories: () => ({
    endpoint: '/categories',
    params: {
      populate: ['parent'],
      sort: 'name:asc',
    },
  }),

  getProducts: (params = {}) => ({
    endpoint: '/products',
    params: {
      populate: ['*', 'images', 'variants.image', 'categories', 'brand', 'reviews'],
      sort: 'createdAt:desc',
      publicationState: 'live',
      ...params,
    },
  }),

  getProductsByCategory: (categoryId, params = {}) => ({
    endpoint: '/products',
    params: {
      filters: {
        categories: { id: { $eq: categoryId } },
      },
      populate: ['*', 'images', 'categories'],
      sort: 'createdAt:desc',
      publicationState: 'live',
      ...params,
    },
  }),

  searchProducts: (searchTerm, params = {}) => ({
    endpoint: '/products',
    params: {
      filters: {
        $or: [
          { name: { $containsi: searchTerm } },
          { description: { $containsi: searchTerm } },
          { short_description: { $containsi: searchTerm } },
          { sku: { $containsi: searchTerm } },
        ],
      },
      populate: ['images', 'categories'],
      sort: 'name:asc',
      publicationState: 'live',
      ...params,
    },
  }),

  // User-specific queries
  getUserOrders: (userId, params = {}) => ({
    endpoint: '/orders',
    params: {
      filters: { user: { id: { $eq: userId } } },
      populate: ['items.product', 'items.variant', 'shipping_address', 'billing_address'],
      sort: 'createdAt:desc',
      ...params,
    },
  }),

  createUserCart: (userId) => ({
    endpoint: '/carts',
    params: {
      filters: { user: { id: { $eq: userId } } },
      populate: ['items.product', 'items.variant'],
    },
  }),
};

// File: hooks/useStrapi.js - React Hooks

import { useState, useEffect, useCallback } from 'react';
import { strapiClient } from '../lib/strapi';

export function useStrapiQuery(query, dependencies = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await strapiClient.get(query.endpoint, query.params);
      setData(response);
    } catch (err) {
      setError(err);
    } finally {
      setLoading(false);
    }
  }, [query.endpoint, query.params]);

  useEffect(() => {
    fetchData();
  }, [fetchData, ...dependencies]);

  const refetch = useCallback(() => {
    return fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch };
}

export function useStrapiMutation(endpoint, method = 'POST') {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const mutate = useCallback(async (data) => {
    try {
      setLoading(true);
      setError(null);

      let response;
      switch (method) {
        case 'POST':
          response = await strapiClient.post(endpoint, data);
          break;
        case 'PUT':
          response = await strapiClient.put(endpoint, data);
          break;
        case 'DELETE':
          response = await strapiClient.delete(endpoint);
          break;
        default:
          throw new Error(`Unsupported method: ${method}`);
      }

      return response;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [endpoint, method]);

  return { mutate, loading, error };
}

// File: services/article-service.js - Service Layer

import { strapiClient, strapiQueries } from '../lib/strapi';

export class ArticleService {
  static async getArticles(params = {}) {
    const query = strapiQueries.getArticles(params);
    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      articles: response.data,
      pagination: response.meta.pagination,
    };
  }

  static async getArticleBySlug(slug) {
    const query = strapiQueries.getArticleBySlug(slug);
    const response = await strapiClient.get(query.endpoint, query.params);

    return response.data?.[0] || null;
  }

  static async getRelatedArticles(articleId, categoryId, limit = 3) {
    const query = strapiQueries.getRelatedArticles(articleId, categoryId, limit);
    const response = await strapiClient.get(query.endpoint, query.params);

    return response.data;
  }

  static async incrementViewCount(articleId) {
    return strapiClient.put(`/articles/${articleId}/view-count`);
  }

  static async searchArticles(searchTerm, params = {}) {
    const query = {
      endpoint: '/articles',
      params: {
        filters: {
          $or: [
            { title: { $containsi: searchTerm } },
            { content: { $containsi: searchTerm } },
            { excerpt: { $containsi: searchTerm } },
          ],
        },
        populate: ['featured_image', 'category', 'author'],
        sort: 'publishedAt:desc',
        publicationState: 'live',
        ...params,
      },
    };

    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      articles: response.data,
      pagination: response.meta.pagination,
    };
  }

  static async getArticlesByTag(tagSlug, params = {}) {
    const query = {
      endpoint: '/articles',
      params: {
        filters: {
          tags: { slug: { $eq: tagSlug } },
        },
        populate: ['featured_image', 'category', 'tags', 'author'],
        sort: 'publishedAt:desc',
        publicationState: 'live',
        ...params,
      },
    };

    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      articles: response.data,
      pagination: response.meta.pagination,
    };
  }
}

// File: services/product-service.js - Product Service

export class ProductService {
  static async getProducts(params = {}) {
    const query = strapiQueries.getProducts(params);
    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      products: response.data,
      pagination: response.meta.pagination,
    };
  }

  static async getProductBySlug(slug) {
    const query = {
      endpoint: '/products',
      params: {
        filters: { slug: { $eq: slug } },
        populate: ['*', 'images', 'variants.image', 'categories', 'brand', 'reviews'],
        publicationState: 'live',
      },
    };

    const response = await strapiClient.get(query.endpoint, query.params);

    return response.data?.[0] || null;
  }

  static async searchProducts(searchTerm, params = {}) {
    const query = strapiQueries.searchProducts(searchTerm, params);
    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      products: response.data,
      pagination: response.meta.pagination,
    };
  }

  static async getProductsByCategory(categorySlug, params = {}) {
    const query = {
      endpoint: '/products',
      params: {
        filters: {
          categories: { slug: { $eq: categorySlug } },
        },
        populate: ['images', 'categories'],
        sort: 'createdAt:desc',
        publicationState: 'live',
        ...params,
      },
    };

    const response = await strapiClient.get(query.endpoint, query.params);

    return {
      products: response.data,
      pagination: response.meta.pagination,
    };
  }

  static async getFeaturedProducts(limit = 8) {
    const query = strapiQueries.getProducts({
      filters: { featured: true },
      pagination: { limit },
    });

    const response = await strapiClient.get(query.endpoint, query.params);

    return response.data;
  }
}

// File: components/ArticleList.js - Frontend Component Example

import React, { useState } from 'react';
import { ArticleService } from '../services/article-service';

export default function ArticleList({ category, tag, limit = 10 }) {
  const [articles, setArticles] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  const [pagination, setPagination] = useState(null);

  useEffect(() => {
    fetchArticles();
  }, [category, tag, page]);

  const fetchArticles = async () => {
    try {
      setLoading(true);
      setError(null);

      const params = {
        pagination: { limit, page },
      };

      if (category) {
        params.filters = { category: { slug: { $eq: category } } };
      }

      if (tag) {
        params.filters = {
          ...params.filters,
          tags: { slug: { $eq: tag } },
        };
      }

      const result = await ArticleService.getArticles(params);
      setArticles(result.articles);
      setPagination(result.pagination);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handlePageChange = (newPage) => {
    setPage(newPage);
  };

  if (loading) {
    return <div>Loading articles...</div>;
  }

  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <div>
      <h1>Articles</h1>
      <div className="article-grid">
        {articles.map((article) => (
          <ArticleCard key={article.id} article={article} />
        ))}
      </div>

      {pagination && (
        <Pagination
          currentPage={page}
          totalPages={pagination.pageCount}
          onPageChange={handlePageChange}
        />
      )}
    </div>
  );
}

// File: lib/strapi-graphql.js - GraphQL Client

import { GraphQLClient, gql } from 'graphql-request';

class StrapiGraphQLClient {
  constructor(baseURL, token = null) {
    this.client = new GraphQLClient(`${baseURL}/graphql`, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
  }

  async query(query, variables = {}) {
    try {
      return await this.client.request(query, variables);
    } catch (error) {
      console.error('Strapi GraphQL Error:', error);
      throw error;
    }
  }

  async mutation(mutation, variables = {}) {
    try {
      return await this.client.request(mutation, variables);
    } catch (error) {
      console.error('Strapi GraphQL Error:', error);
      throw error;
    }
  }
}

export const strapiGraphQL = new StrapiGraphQLClient(
  process.env.NEXT_PUBLIC_STRAPI_URL || 'http://localhost:1337',
  process.env.STRAPI_API_TOKEN
);

// GraphQL Queries
export const GET_ARTICLES = gql`
  query GetArticles(
    $pagination: PaginationArg = { page: 1, pageSize: 10 }
    $filters: ArticleFiltersInput = {}
    $sort: [String] = ["publishedAt:desc"]
    $publicationState: PublicationState = LIVE
  ) {
    articles(pagination: $pagination, filters: $filters, sort: $sort, publicationState: $publicationState) {
      data {
        id
        attributes {
          title
          slug
          excerpt
          publishedAt
          featured_image {
            data {
              attributes {
                url
                alternativeText
              }
            }
          }
          category {
            data {
              attributes {
                name
                slug
                color
              }
            }
          }
          tags {
            data {
              attributes {
                name
                slug
              }
            }
          }
          author {
            data {
              attributes {
                name
                avatar {
                  data {
                    attributes {
                      url
                    }
                  }
                }
              }
            }
          }
        }
      }
      meta {
        pagination {
          page
          pageSize
          pageCount
          total
        }
      }
    }
  }
`;

export const GET_ARTICLE_BY_SLUG = gql`
  query GetArticleBySlug($slug: String!, $publicationState: PublicationState = LIVE) {
    articles(filters: { slug: { eq: $slug } }, publicationState: $publicationState) {
      data {
        id
        attributes {
          title
          slug
          content
          publishedAt
          featured_image {
            data {
              attributes {
                url
                alternativeText
                caption
              }
            }
          }
          category {
            data {
              attributes {
                name
                slug
                description
              }
            }
          }
          tags {
            data {
              attributes {
                name
                slug
              }
            }
          }
          author {
            data {
              attributes {
                name
                bio
                avatar {
                  data {
                    attributes {
                      url
                    }
                  }
                }
              }
            }
          }
          seo {
            meta_title
            meta_description
            keywords
          }
        }
      }
    }
  }
`;

💻 Strapi Plugins & Deployment javascript

🔴 complex ⭐⭐⭐⭐⭐

Vollständige Plugin-Entwicklung, Anpassung und Deployment-Strategien für Produktionsumgebungen

⏱️ 50 min 🏷️ strapi, plugins, deployment, production
Prerequisites: Strapi basics, Plugin development, Docker, DevOps, Database concepts
// Strapi Plugins & Deployment Examples
// File: plugins/custom-controllers/server/controllers/custom.js

'use strict';

/**
 * Custom controller example
 */

module.exports = {
  // Custom route handler
  async customAction(ctx) {
    try {
      const { param } = ctx.params;
      const { query } = ctx.query;

      // Custom business logic
      const result = await strapi.service('api::custom.custom').processCustomData(param, query);

      ctx.send({
        message: 'Custom action completed successfully',
        data: result,
        timestamp: new Date().toISOString()
      });
    } catch (error) {
      ctx.status = 500;
      ctx.send({
        message: 'Custom action failed',
        error: error.message
      });
    }
  },

  // Bulk operations
  async bulkUpdate(ctx) {
    try {
      const { ids, data } = ctx.request.body;

      // Validate input
      if (!Array.isArray(ids) || ids.length === 0) {
        return ctx.badRequest('Invalid IDs array');
      }

      // Process bulk update
      const results = await strapi.service('api::custom.custom').bulkUpdate(ids, data);

      ctx.send({
        message: `Successfully updated ${results.updated} items`,
        updated: results.updated,
        failed: results.failed
      });
    } catch (error) {
      ctx.status = 500;
      ctx.send({
        message: 'Bulk update failed',
        error: error.message
      });
    }
  },

  // Export data
  async export(ctx) {
    try {
      const { format = 'json', filters = {} } = ctx.query;

      const data = await strapi.service('api::custom.custom').exportData(filters);

      if (format === 'csv') {
        ctx.set('Content-Type', 'text/csv');
        ctx.set('Content-Disposition', 'attachment; filename="export.csv"');
        return ctx.send(data.csv);
      } else {
        ctx.set('Content-Type', 'application/json');
        ctx.set('Content-Disposition', 'attachment; filename="export.json"');
        return ctx.send(data.json);
      }
    } catch (error) {
      ctx.status = 500;
      ctx.send({
        message: 'Export failed',
        error: error.message
      });
    }
  }
};

// File: plugins/custom-controllers/server/services/custom.js

'use strict';

/**
 * Custom service example
 */

module.exports = ({ strapi }) => ({
  // Process custom data
  async processCustomData(param, query = {}) {
    // Custom business logic
    const entities = await strapi.query('api::entity.entity').findMany({
      where: {
        customField: param,
        ...query.filters
      },
      populate: ['relations']
    });

    // Transform data
    const processedData = entities.map(entity => ({
      ...entity,
      processedValue: this.calculateProcessedValue(entity),
      metadata: this.generateMetadata(entity)
    }));

    return {
      count: processedData.length,
      data: processedData
    };
  },

  // Calculate processed value
  calculateProcessedValue(entity) {
    // Example calculation
    return entity.field1 * entity.field2 + entity.field3;
  },

  // Generate metadata
  generateMetadata(entity) {
    return {
      createdAt: entity.createdAt,
      updatedAt: entity.updatedAt,
      version: this.getEntityVersion(entity)
    };
  },

  // Get entity version
  getEntityVersion(entity) {
    return `v${entity.id}.${entity.updatedAt.getTime()}`;
  },

  // Bulk update implementation
  async bulkUpdate(ids, data) {
    const results = {
      updated: 0,
      failed: []
    };

    for (const id of ids) {
      try {
        await strapi.query('api::entity.entity').update({
          where: { id },
          data: {
            ...data,
            updatedBy: 1 // Current user ID
          }
        });
        results.updated++;
      } catch (error) {
        results.failed.push({
          id,
          error: error.message
        });
      }
    }

    return results;
  },

  // Export data implementation
  async exportData(filters = {}) {
    const entities = await strapi.query('api::entity.entity').findMany({
      where: filters,
      populate: ['relations']
    });

    // Transform to CSV
    const csv = this.convertToCSV(entities);

    return {
      json: entities,
      csv: csv
    };
  },

  // Convert to CSV
  convertToCSV(data) {
    if (data.length === 0) return '';

    const headers = Object.keys(data[0]);
    const csvRows = [headers.join(',')];

    for (const row of data) {
      const values = headers.map(header => {
        const value = row[header];
        if (value === null || value === undefined) return '';
        if (typeof value === 'object') return JSON.stringify(value);
        return `"${String(value).replace(/"/g, '\"')}"`;
      });
      csvRows.push(values.join(','));
    }

    return csvRows.join('\n');
  }
});

// File: plugins/webhooks/server/register.js

module.exports = {
  register({ strapi }) {
    // Register webhook events
    strapi webhook.addEvent('entry.create');
    strapi webhook.addEvent('entry.update');
    strapi.websocket.addEvent('entry.delete');
  },

  async bootstrap({ strapi }) {
    // Bootstrap logic
    console.log('Webhook plugin loaded');
  },

  async destroy({ strapi }) {
    // Cleanup logic
    console.log('Webhook plugin destroyed');
  }
};

// File: plugins/webhooks/server/webhooks.js

module.exports = {
  'entry.create': async (event) => {
    const { model, uid, entry } = event;

    // Send webhook notification
    await strapi.plugin('webhooks').service('webhook').trigger({
      event: 'entry.create',
      data: {
        model,
        uid,
        entry,
        timestamp: new Date().toISOString()
      }
    });

    // Log event
    strapi.log.info(`Webhook triggered for ${model} creation: ${entry.id}`);
  },

  'entry.update': async (event) => {
    const { model, uid, entry, changes } = event;

    // Only send webhook if there are actual changes
    if (changes && Object.keys(changes).length > 0) {
      await strapi.plugin('webhooks').service('webhook').trigger({
        event: 'entry.update',
        data: {
          model,
          uid,
          entry,
          changes,
          timestamp: new Date().toISOString()
        }
      });
    }
  },

  'entry.delete': async (event) => {
    const { model, uid, entry } = event;

    await strapi.plugin('webhooks').service('webhook').trigger({
      event: 'entry.delete',
      data: {
        model,
        uid,
        entry,
        timestamp: new Date().toISOString()
      }
    });
  }
};

// File: plugins/analytics/server/services/analytics.js

'use strict';

module.exports = ({ strapi }) => ({
  // Track page view
  async trackPageView(data) {
    const { url, referrer, userAgent, ip } = data;

    const pageView = await strapi.query('api::page-view.page-view').create({
      data: {
        url,
        referrer,
        userAgent,
        ip,
        timestamp: new Date(),
        date: new Date().toISOString().split('T')[0]
      }
    });

    // Update daily stats
    await this.updateDailyStats('pageViews', pageView.date);

    return pageView;
  },

  // Track event
  async trackEvent(data) {
    const { type, category, action, label, value, url, userAgent, ip } = data;

    const event = await strapi.query('api::analytics-event.analytics-event').create({
      data: {
        type,
        category,
        action,
        label,
        value,
        url,
        userAgent,
        ip,
        timestamp: new Date(),
        date: new Date().toISOString().split('T')[0]
      }
    });

    // Update daily stats
    await this.updateDailyStats('events', event.date);

    return event;
  },

  // Update daily statistics
  async updateDailyStats(type, date) {
    const existingStat = await strapi.query('api::daily-stat.daily-stat').findOne({
      where: { date, type }
    });

    if (existingStat) {
      await strapi.query('api::daily-stat.daily-stat').update({
        where: { id: existingStat.id },
        data: {
          count: existingStat.count + 1,
          updatedAt: new Date()
        }
      });
    } else {
      await strapi.query('api::daily-stat.daily-stat').create({
        data: {
          date,
          type,
          count: 1
        }
      });
    }
  },

  // Get analytics data
  async getAnalytics(filters = {}) {
    const { startDate, endDate, type } = filters;

    const whereClause = {};
    if (startDate && endDate) {
      whereClause.date = {
        $gte: startDate,
        $lte: endDate
      };
    }
    if (type) {
      whereClause.type = type;
    }

    const stats = await strapi.query('api::daily-stat.daily-stat').findMany({
      where: whereClause,
      orderBy: { date: 'asc' }
    });

    return stats;
  },

  // Get popular content
  async getPopularContent(limit = 10) {
    const pageViews = await strapi.query('api::page-view.page-view').findMany({
      where: {
        date: {
          $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Last 30 days
        }
      }
    });

    // Group by URL and count
    const urlCounts = pageViews.reduce((acc, view) => {
      acc[view.url] = (acc[view.url] || 0) + 1;
      return acc;
    }, {});

    // Sort by count and limit
    const popularUrls = Object.entries(urlCounts)
      .sort(([,a], [,b]) => b - a)
      .slice(0, limit)
      .map(([url, count]) => ({ url, count }));

    return popularUrls;
  }
});

// File: config/plugins.js - Plugin Configuration

module.exports = {
  // Enable plugins
  'users-permissions': {
    enabled: true,
    config: {
      jwtSecret: process.env.JWT_SECRET || 'default-secret',
      register: {
        allowedFields: ['username', 'email', 'password'],
        uniqueFields: ['email', 'username'],
      },
      confirmEmail: false,
    },
  },

  // Upload configuration
  upload: {
    enabled: true,
    config: {
      provider: 'local', // or 'aws-s3'
      providerOptions: {
        local: {
          path: './public/uploads'
        },
        awsS3: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
          region: process.env.AWS_REGION,
          bucket: process.env.AWS_S3_BUCKET,
        },
      },
    },
  },

  // Email configuration
  'email': {
    enabled: true,
    config: {
      provider: 'sendgrid', // or 'nodemailer'
      providerOptions: {
        sendgrid: {
          apiKey: process.env.SENDGRID_API_KEY,
        },
        nodemailer: {
          host: process.env.SMTP_HOST,
          port: process.env.SMTP_PORT,
          auth: {
            user: process.env.SMTP_USER,
            pass: process.env.SMTP_PASS,
          },
        },
      },
      settings: {
        defaultFrom: process.env.DEFAULT_FROM_EMAIL,
        defaultReplyTo: process.env.DEFAULT_REPLY_TO_EMAIL,
      },
    },
  },

  // i18n configuration
  i18n: {
    enabled: true,
    config: {
      defaultLocale: 'en',
      locales: ['en', 'es', 'fr', 'de', 'it', 'pt', 'zh'],
      fallbackLocale: 'en',
    },
  },

  // GraphQL configuration
  graphql: {
    enabled: true,
    config: {
      endpoint: '/graphql',
      shadowCRUD: true,
      playgroundAlways: process.env.NODE_ENV !== 'production',
      depthLimit: 7,
      apolloServer: {
        introspection: process.env.NODE_ENV !== 'production',
      },
    },
  },

  // Documentation configuration
  'documentation': {
    enabled: true,
    config: {
      openapi: {
        version: '1.0.0',
        info: {
          title: 'My API Documentation',
          description: 'Comprehensive API documentation',
          contact: {
            name: 'API Support',
            email: '[email protected]',
          },
        },
        servers: [
          {
            url: 'http://localhost:1337/api',
            description: 'Development server',
          },
        ],
        security: [
          {
            bearerAuth: [],
          },
        ],
      },
    },
  },

  // Webhook configuration
  webhook: {
    enabled: true,
    config: {
      allowedOrigins: ['https://example.com', 'https://app.example.com'],
    },
  },

  // Redis cache configuration
  'redis': {
    enabled: true,
    config: {
      connections: {
        default: {
          host: process.env.REDIS_HOST || 'localhost',
          port: process.env.REDIS_PORT || 6379,
          password: process.env.REDIS_PASSWORD,
          db: 0,
        },
      },
    },
  },
};

// File: docker-compose.prod.yml - Production Deployment

version: '3.8'

services:
  # Strapi application
  strapi:
    build:
      context: .
      dockerfile: Dockerfile.prod
    image: strapi-app:latest
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_CLIENT: postgres
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: ${DATABASE_NAME}
      DATABASE_USERNAME: ${DATABASE_USERNAME}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
      APP_KEYS: ${APP_KEYS}
      API_TOKEN_SALT: ${API_TOKEN_SALT}
      ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
      TRANSFER_TOKEN_SALT: ${TRANSFER_TOKEN_SALT}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      SENDGRID_API_KEY: ${SENDGRID_API_KEY}
      AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
      AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
      AWS_S3_BUCKET: ${AWS_S3_BUCKET}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    ports:
      - "1337:1337"
    volumes:
      - ./public/uploads:/app/public/uploads
    networks:
      - strapi-network

  # PostgreSQL database
  postgres:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DATABASE_NAME}
      POSTGRES_USER: ${DATABASE_USERNAME}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./config/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USERNAME} -d ${DATABASE_NAME}"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    networks:
      - strapi-network

  # Redis cache
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - strapi-network

  # Nginx reverse proxy
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - strapi
    networks:
      - strapi-network

  # Monitoring with Prometheus
  prometheus:
    image: prom/prometheus:latest
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks:
      - strapi-network

  # Grafana for visualization
  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana:/etc/grafana/provisioning:ro
    networks:
      - strapi-network

volumes:
  postgres_data:
  redis_data:
  prometheus_data:
  grafana_data:

networks:
  strapi-network:
    driver: bridge

# File: Dockerfile.prod - Production Dockerfile

FROM node:18-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Build the app
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Install dumb-init for signal handling
RUN apk add --no-cache dumb-init

# Create app user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S strapi -u 1001

WORKDIR /app

# Copy built app
COPY --from=builder --chown=strapi:nodejs /app .

# Create uploads directory
RUN mkdir -p public/uploads && \
    chown -R strapi:nodejs public/uploads

# Switch to app user
USER strapi

# Expose port
EXPOSE 1337

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:1337/health || exit 1

# Start the app
ENTRYPOINT ["dumb-init", "--"]
CMD ["npm", "start"]

# File: scripts/backup.sh - Backup Script

#!/bin/bash

# Configuration
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
STRAPI_CONTAINER="strapi_strapi_1"
DB_CONTAINER="strapi_postgres_1"

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup database
echo "Backing up database..."
docker exec $DB_CONTAINER pg_dump -U $DATABASE_USERNAME $DATABASE_NAME > $BACKUP_DIR/db_backup_$DATE.sql

# Backup uploads
echo "Backing up uploads..."
docker cp $STRAPI_CONTAINER:/app/public/uploads $BACKUP_DIR/uploads_$DATE

# Compress backups
echo "Compressing backups..."
tar -czf $BACKUP_DIR/strapi_backup_$DATE.tar.gz -C $BACKUP_DIR db_backup_$DATE.sql uploads_$DATE

# Clean up
rm -f $BACKUP_DIR/db_backup_$DATE.sql
rm -rf $BACKUP_DIR/uploads_$DATE

# Remove old backups (keep last 7 days)
find $BACKUP_DIR -name "strapi_backup_*.tar.gz" -mtime +7 -delete

echo "Backup completed: $BACKUP_DIR/strapi_backup_$DATE.tar.gz"