🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
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
💻 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 } 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"