Exemples Strapi Headless CMS

Exemples complets de Strapi couvrant le modelage de contenu, l'intégration d'API, l'authentification, les plugins et les patterns de déploiement

Key Facts

Category
CMS
Items
3
Format Families
sample

Sample Overview

Exemples complets de Strapi couvrant le modelage de contenu, l'intégration d'API, l'authentification, les plugins et les patterns de déploiement This sample set belongs to CMS and can be used to test related workflows inside Elysia Tools.

💻 Modelage de Contenu Strapi javascript

🟡 intermediate ⭐⭐⭐

Définitions complètes de content types, relations et configurations de champs pour divers cas d'usage

⏱️ 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();
  }
};

💻 Intégration d'API Strapi javascript

🔴 complex ⭐⭐⭐⭐

Exemples complets d'intégration d'API incluant les requêtes REST et GraphQL, l'authentification et l'implémentation frontend

⏱️ 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, useEffect } 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
          }
        }
      }
    }
  }
`;

💻 Plugins et Déploiement Strapi javascript

🔴 complex ⭐⭐⭐⭐⭐

Développement complet de plugins, personnalisation et stratégies de déploiement pour la production

⏱️ 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"