Elysia Tools

Parcourez outils, exemples, explications visuelles et guides thématiques dans un espace clair.

Catégories populaires

Dossiers

Langue
ZHDERUPTENFRES

Elysia Tools

Navigation mobile

CMS

Exemples Contentful CMS Cloud-based

Exemples complets de Contentful couvrant le modelage de contenu, l'intégration d'API, les webhooks et les patterns d'intégration frontend

Exemples

Entrées de cette collection

Code sourcejavascriptIntermédiaire

Modelage de Contenu Contentful

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

Difficulté

6/10

Temps estimé

30 min

Étiquettes

contentful, content modeling, cms

Prérequis

Contentful basics, Content modeling concepts, JSON schema, Headless CMS

javascript
// Contentful Content Modeling Examples
// File: content-types/blog-post.json

{
  "name": "Blog Post",
  "description": "Blog posts with rich content and metadata",
  "displayField": "title",
  "fields": [
    {
      "id": "title",
      "name": "Title",
      "type": "Text",
      "required": true,
      "localized": true,
      "validations": {
        "unique": true,
        "size": {
          "max": 100
        }
      }
    },
    {
      "id": "slug",
      "name": "Slug",
      "type": "Text",
      "required": true,
      "unique": true,
      "validations": {
        "regexp": {
          "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
          "flags": null
        }
      }
    },
    {
      "id": "excerpt",
      "name": "Excerpt",
      "type": "Text",
      "localized": true,
      "validations": {
        "size": {
          "max": 200
        }
      }
    },
    {
      "id": "content",
      "name": "Content",
      "type": "RichText",
      "required": true,
      "localized": true,
      "validations": {
        "enabledNodeTypes": [
          "heading-1",
          "heading-2",
          "heading-3",
          "heading-4",
          "heading-5",
          "heading-6",
          "ordered-list",
          "unordered-list",
          "hr",
          "blockquote",
          "embedded-entry-block",
          "embedded-asset-block",
          "hyperlink",
          "entry-hyperlink",
          "asset-hyperlink"
        ],
        "nodes": {}
      }
    },
    {
      "id": "featuredImage",
      "name": "Featured Image",
      "type": "Link",
      "validations": {
        "linkContentType": ["media"],
        "required": false
      }
    },
    {
      "id": "gallery",
      "name": "Gallery",
      "type": "Array",
      "items": {
        "type": "Link",
        "validations": {
          "linkContentType": ["media"],
          "required": false
        }
      }
    },
    {
      "id": "author",
      "name": "Author",
      "type": "Link",
      "validations": {
        "linkContentType": ["author"],
        "required": true
      }
    },
    {
      "id": "category",
      "name": "Category",
      "type": "Link",
      "validations": {
        "linkContentType": ["category"],
        "required": true
      }
    },
    {
      "id": "tags",
      "name": "Tags",
      "type": "Array",
      "items": {
        "type": "Link",
        "validations": {
          "linkContentType": ["tag"],
          "required": false
        }
      }
    },
    {
      "id": "seo",
      "name": "SEO",
      "type": "Link",
      "validations": {
        "linkContentType": ["seo"],
        "required": false
      }
    },
    {
      "id": "publishedAt",
      "name": "Published Date",
      "type": "Date",
      "required": true
    },
    {
      "id": "readingTime",
      "name": "Reading Time",
      "type": "Number",
      "validations": {
        "range": {
          "min": 1
        }
      }
    },
    {
      "id": "isFeatured",
      "name": "Is Featured",
      "type": "Boolean",
      "defaultValue": false
    }
  ]
}

// File: content-types/product.json

{
  "name": "Product",
  "description": "E-commerce product with variants and pricing",
  "displayField": "name",
  "fields": [
    {
      "id": "name",
      "name": "Name",
      "type": "Text",
      "required": true,
      "localized": true,
      "validations": {
        "unique": true
      }
    },
    {
      "id": "slug",
      "name": "Slug",
      "type": "Text",
      "required": true,
      "unique": true
    },
    {
      "id": "description",
      "name": "Description",
      "type": "RichText",
      "required": true,
      "localized": true
    },
    {
      "id": "shortDescription",
      "name": "Short Description",
      "type": "Text",
      "localized": true,
      "validations": {
        "size": {
          "max": 500
        }
      }
    },
    {
      "id": "sku",
      "name": "SKU",
      "type": "Text",
      "required": true,
      "unique": true,
      "validations": {
        "regexp": {
          "pattern": "^[A-Z0-9-]+$"
        }
      }
    },
    {
      "id": "price",
      "name": "Price",
      "type": "Number",
      "required": true,
      "validations": {
        "range": {
          "min": 0
        }
      }
    },
    {
      "id": "comparePrice",
      "name": "Compare Price",
      "type": "Number",
      "validations": {
        "range": {
          "min": 0
        }
      }
    },
    {
      "id": "currency",
      "name": "Currency",
      "type": "Symbol",
      "required": true,
      "defaultValue": "USD"
    },
    {
      "id": "images",
      "name": "Images",
      "type": "Array",
      "items": {
        "type": "Link",
        "validations": {
          "linkContentType": ["media"]
        }
      }
    },
    {
      "id": "variants",
      "name": "Variants",
      "type": "Array",
      "items": {
        "type": "Link",
        "validations": {
          "linkContentType": ["productVariant"]
        }
      }
    },
    {
      "id": "category",
      "name": "Category",
      "type": "Link",
      "validations": {
        "linkContentType": ["productCategory"]
      }
    },
    {
      "id": "brand",
      "name": "Brand",
      "type": "Link",
      "validations": {
        "linkContentType": ["brand"]
      }
    },
    {
      "id": "attributes",
      "name": "Attributes",
      "type": "Array",
      "items": {
        "type": "Link",
        "validations": {
          "linkContentType": ["productAttribute"]
        }
      }
    },
    {
      "id": "inventory",
      "name": "Inventory",
      "type": "Object",
      "required": false,
      "fields": [
        {
          "id": "quantity",
          "name": "Quantity",
          "type": "Number",
          "required": true,
          "validations": {
            "range": {
              "min": 0
            }
          }
        },
        {
          "id": "sku",
          "name": "SKU",
          "type": "Text",
          "required": true
        },
        {
          "id": "trackQuantity",
          "name": "Track Quantity",
          "type": "Boolean",
          "defaultValue": true
        },
        {
          "id": "allowBackorder",
          "name": "Allow Backorder",
          "type": "Boolean",
          "defaultValue": false
        }
      ]
    },
    {
      "id": "shipping",
      "name": "Shipping",
      "type": "Object",
      "required": false,
      "fields": [
        {
          "id": "weight",
          "name": "Weight",
          "type": "Number",
          "validations": {
            "range": {
              "min": 0
            }
          }
        },
        {
          "id": "length",
          "name": "Length",
          "type": "Number",
          "validations": {
            "range": {
              "min": 0
            }
          }
        },
        {
          "id": "width",
          "name": "Width",
          "type": "Number",
          "validations": {
            "range": {
              "min": 0
            }
          }
        },
        {
          "id": "height",
          "name": "Height",
          "type": "Number",
          "validations": {
            "range": {
              "min": 0
            }
          }
        }
      ]
    },
    {
      "id": "seo",
      "name": "SEO",
      "type": "Link",
      "validations": {
        "linkContentType": ["seo"]
      }
    },
    {
      "id": "isActive",
      "name": "Is Active",
      "type": "Boolean",
      "defaultValue": true
    }
  ]
}

// File: content-types/author.json

{
  "name": "Author",
  "description": "Author information and bio",
  "displayField": "name",
  "fields": [
    {
      "id": "name",
      "name": "Name",
      "type": "Text",
      "required": true,
      "localized": true
    },
    {
      "id": "email",
      "name": "Email",
      "type": "Text",
      "required": true,
      "validations": {
        "unique": true,
        "email": true
      }
    },
    {
      "id": "bio",
      "name": "Bio",
      "type": "RichText",
      "localized": true
    },
    {
      "id": "avatar",
      "name": "Avatar",
      "type": "Link",
      "validations": {
        "linkContentType": ["media"]
      }
    },
    {
      "id": "social",
      "name": "Social Media",
      "type": "Object",
      "fields": [
        {
          "id": "twitter",
          "name": "Twitter",
          "type": "Text"
        },
        {
          "id": "github",
          "name": "GitHub",
          "type": "Text"
        },
        {
          "id": "linkedin",
          "name": "LinkedIn",
          "type": "Text"
        },
        {
          "id": "website",
          "name": "Website",
          "type": "Text"
        }
      ]
    }
  ]
}

// File: content-types/product-variant.json

{
  "name": "Product Variant",
  "description": "Product variants with different options",
  "displayField": "name",
  "fields": [
    {
      "id": "name",
      "name": "Name",
      "type": "Text",
      "required": true,
      "localized": true
    },
    {
      "id": "sku",
      "name": "SKU",
      "type": "Text",
      "required": true,
      "unique": true
    },
    {
      "id": "price",
      "name": "Price",
      "type": "Number",
      "required": true,
      "validations": {
        "range": {
          "min": 0
        }
      }
    },
    {
      "id": "comparePrice",
      "name": "Compare Price",
      "type": "Number",
      "validations": {
        "range": {
          "min": 0
        }
      }
    },
    {
      "id": "weight",
      "name": "Weight",
      "type": "Number",
      "validations": {
        "range": {
          "min": 0
        }
      }
    },
    {
      "id": "inventory",
      "name": "Inventory",
      "type": "Number",
      "defaultValue": 0
    },
    {
      "id": "options",
      "name": "Options",
      "type": "Object",
      "fields": [
        {
          "id": "color",
          "name": "Color",
          "type": "Text"
        },
        {
          "id": "size",
          "name": "Size",
          "type": "Text"
        },
        {
          "id": "material",
          "name": "Material",
          "type": "Text"
        }
      ]
    },
    {
      "id": "image",
      "name": "Image",
      "type": "Link",
      "validations": {
        "linkContentType": ["media"]
      }
    }
  ]
}

// File: content-types/seo.json

{
  "name": "SEO",
  "description": "SEO metadata for search engine optimization",
  "fields": [
    {
      "id": "metaTitle",
      "name": "Meta Title",
      "type": "Text",
      "localized": true,
      "validations": {
        "size": {
          "max": 60
        }
      }
    },
    {
      "id": "metaDescription",
      "name": "Meta Description",
      "type": "Text",
      "localized": true,
      "validations": {
        "size": {
          "max": 160
        }
      }
    },
    {
      "id": "keywords",
      "name": "Keywords",
      "type": "Text"
    },
    {
      "id": "canonicalUrl",
      "name": "Canonical URL",
      "type": "Text"
    },
    {
      "id": "ogImage",
      "name": "OG Image",
      "type": "Link",
      "validations": {
        "linkContentType": ["media"]
      }
    },
    {
      "id": "structuredData",
      "name": "Structured Data",
      "type": "Object",
      "fields": [
        {
          "id": "type",
          "name": "Type",
          "type": "Text",
          "defaultValue": "Article"
        },
        {
          "id": "data",
          "name": "Data",
          "type": "RichText"
        }
      ]
    }
  ]
}

// File: locales.json - Localization Configuration

{
  "defaultLocale": "en-US",
  "locales": [
    {
      "code": "en-US",
      "name": "English (United States)",
      "default": true,
      "fallbackCode": null
    },
    {
      "code": "es-ES",
      "name": "Spanish (Spain)",
      "default": false,
      "fallbackCode": "en-US"
    },
    {
      "code": "fr-FR",
      "name": "French (France)",
      "default": false,
      "fallbackCode": "en-US"
    },
    {
      "code": "de-DE",
      "name": "German (Germany)",
      "default": false,
      "fallbackCode": "en-US"
    },
    {
      "code": "it-IT",
      "name": "Italian (Italy)",
      "default": false,
      "fallbackCode": "en-US"
    },
    {
      "code": "pt-BR",
      "name": "Portuguese (Brazil)",
      "default": false,
      "fallbackCode": "en-US"
    },
    {
      "code": "zh-CN",
      "name": "Chinese (Simplified)",
      "default": false,
      "fallbackCode": "en-US"
    }
  ]
}

// File: environment-variables.json - Environment Configuration

[
  {
    "name": "CONTENTFUL_SPACE_ID",
    "description": "Contentful Space ID"
  },
  {
    "name": "CONTENTFUL_ACCESS_TOKEN",
    "description": "Contentful Delivery API Access Token"
  },
  {
    "name": "CONTENTFUL_MANAGEMENT_TOKEN",
    "description": "Contentful Management API Access Token"
  },
  {
    "name": "CONTENTFUL_PREVIEW_ACCESS_TOKEN",
    "description": "Contentful Preview API Access Token"
  },
  {
    "name": "CONTENTFUL_ENVIRONMENT",
    "description": "Contentful Environment (master, staging, development)",
    "defaultValue": "master"
  },
  {
    "name": "CONTENTFUL_HOST",
    "description": "Contentful API Host",
    "defaultValue": "cdn.contentful.com"
  },
  {
    "name": "CONTENTFUL_WEBHOOK_SECRET",
    "description": "Contentful Webhook Secret"
  }
]

// File: webhooks.json - Webhook Configuration

{
  "name": "Content Webhooks",
  "description": "Webhooks for content changes",
  "url": "https://your-app.com/api/webhooks/contentful",
  "topics": [
    "Entry.create",
    "Entry.save",
    "Entry.delete",
    "Entry.publish",
    "Entry.unpublish",
    "Asset.create",
    "Asset.save",
    "Asset.delete",
    "Asset.publish",
    "Asset.unpublish",
    "ContentType.create",
    "ContentType.save",
    "ContentType.delete"
  ],
  "headers": {
    "Content-Type": "application/json",
    "X-Webhook-Secret": "{{webhook_secret}}"
  },
  "filters": [
    {
      "field": "contentType.sys.id",
      "in": ["blogPost", "product", "author"]
    }
  ],
  "transformation": {
    "contentType": {
      "type": "lookup",
      "lookup": {
        "blogPost": "Blog Post",
        "product": "Product",
        "author": "Author"
      }
    }
  }
}
Code sourcejavascriptComplexe

Intégration d'API Contentful

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

Difficulté

8/10

Temps estimé

40 min

Étiquettes

contentful, api, integration, frontend

Prérequis

Contentful basics, REST/GraphQL APIs, React hooks, JavaScript ES6+, Headless CMS concepts

javascript
// Contentful API Integration Examples
// File: lib/contentful.js - API Client Setup

import { createClient } from 'contentful';
import { createClient as createManagementClient } from 'contentful-management';

// Delivery API Client
export const contentfulClient = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
  host: process.env.CONTENTFUL_HOST || 'cdn.contentful.com',
  adapter: 'fetch',
});

// Preview API Client
export const contentfulPreviewClient = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
  environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
  host: 'preview.contentful.com',
  adapter: 'fetch',
});

// Management API Client
export const contentfulManagementClient = createManagementClient({
  accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});

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

export const contentfulQueries = {
  // Blog posts with population
  getBlogPosts: (limit = 10, skip = 0, locale = 'en-US') => ({
    content_type: 'blogPost',
    order: '-fields.publishedAt',
    limit,
    skip,
    locale,
    include: 10,
  }),

  getBlogPostBySlug: (slug, locale = 'en-US') => ({
    content_type: 'blogPost',
    'fields.slug': slug,
    locale,
    include: 10,
    limit: 1,
  }),

  getRelatedBlogPosts: (categoryId, limit = 3, currentPostId, locale = 'en-US') => ({
    content_type: 'blogPost',
    'fields.category.sys.id': categoryId,
    'sys.id[ne]': currentPostId,
    order: '-fields.publishedAt',
    limit,
    locale,
    include: 2,
  }),

  getFeaturedBlogPosts: (limit = 5, locale = 'en-US') => ({
    content_type: 'blogPost',
    'fields.isFeatured': true,
    order: '-fields.publishedAt',
    limit,
    locale,
    include: 5,
  }),

  getBlogPostsByCategory: (categoryId, limit = 10, locale = 'en-US') => ({
    content_type: 'blogPost',
    'fields.category.sys.id': categoryId,
    order: '-fields.publishedAt',
    limit,
    locale,
    include: 5,
  }),

  getBlogPostsByTag: (tagId, limit = 10, locale = 'en-US') => ({
    content_type: 'blogPost',
    'fields.tags.sys.id': tagId,
    order: '-fields.publishedAt',
    limit,
    locale,
    include: 5,
  }),

  // Products with population
  getProducts: (limit = 20, skip = 0, locale = 'en-US') => ({
    content_type: 'product',
    order: 'fields.name',
    limit,
    skip,
    locale,
    include: 10,
  }),

  getProductBySlug: (slug, locale = 'en-US') => ({
    content_type: 'product',
    'fields.slug': slug,
    locale,
    include: 10,
    limit: 1,
  }),

  getProductsByCategory: (categoryId, limit = 20, locale = 'en-US') => ({
    content_type: 'product',
    'fields.category.sys.id': categoryId,
    order: 'fields.name',
    limit,
    locale,
    include: 5,
  }),

  getProductsByBrand: (brandId, limit = 20, locale = 'en-US') => ({
    content_type: 'product',
    'fields.brand.sys.id': brandId,
    order: 'fields.name',
    limit,
    locale,
    include: 5,
  }),

  searchProducts: (searchTerm, locale = 'en-US') => ({
    content_type: 'product',
    query: searchTerm,
    locale,
    include: 5,
  }),

  // Authors
  getAuthors: (locale = 'en-US') => ({
    content_type: 'author',
    order: 'fields.name',
    locale,
    include: 1,
  }),

  getAuthorByEmail: (email, locale = 'en-US') => ({
    content_type: 'author',
    'fields.email': email,
    locale,
    include: 1,
    limit: 1,
  }),

  // Categories
  getCategories: (locale = 'en-US') => ({
    content_type: 'category',
    order: 'fields.name',
    locale,
  }),

  // Tags
  getTags: (locale = 'en-US') => ({
    content_type: 'tag',
    order: 'fields.name',
    locale,
  }),

  // Entries by content type
  getEntriesByContentType: (contentType, options = {}) => ({
    content_type: contentType,
    ...options,
  }),
};

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

import { useState, useEffect, useCallback } from 'react';
import { contentfulClient, contentfulPreviewClient } from '../lib/contentful';

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

  const client = preview ? contentfulPreviewClient : contentfulClient;

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

      const entries = await client.getEntries(query);
      setData(entries);
    } catch (err) {
      setError(err);
      console.error('Contentful query error:', err);
    } finally {
      setLoading(false);
    }
  }, [client, query]);

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

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

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

export function useContentfulEntry(id, query = {}, preview = false) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const client = preview ? contentfulPreviewClient : contentfulClient;

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

        const entry = await client.getEntry(id, query);
        setData(entry);
      } catch (err) {
        setError(err);
        console.error('Contentful entry error:', err);
      } finally {
        setLoading(false);
      }
    };

    fetchEntry();
  }, [id, query, client]);

  return { data, loading, error };
}

export function useContentfulAssets(query = {}) {
  const [assets, setAssets] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

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

        const result = await contentfulClient.getAssets(query);
        setAssets(result.items);
      } catch (err) {
        setError(err);
        console.error('Contentful assets error:', err);
      } finally {
        setLoading(false);
      }
    };

    fetchAssets();
  }, [query]);

  return { assets, loading, error };
}

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

export class ContentfulService {
  constructor(client = contentfulClient) {
    this.client = client;
  }

  // Blog posts
  async getBlogPosts(params = {}) {
    const { limit = 10, skip = 0, locale = 'en-US', category, tag, featured } = params;

    const query = {
      content_type: 'blogPost',
      order: '-fields.publishedAt',
      limit,
      skip,
      locale,
      include: 10,
    };

    if (category) {
      query['fields.category.sys.id'] = category;
    }

    if (tag) {
      query['fields.tags.sys.id'] = tag;
    }

    if (featured) {
      query['fields.isFeatured'] = true;
    }

    const entries = await this.client.getEntries(query);

    return {
      posts: entries.items,
      total: entries.total,
      limit: entries.limit,
      skip: entries.skip,
    };
  }

  async getBlogPostBySlug(slug, locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'blogPost',
      'fields.slug': slug,
      locale,
      include: 10,
      limit: 1,
    });

    return entries.items[0] || null;
  }

  async getRelatedBlogPosts(categoryId, currentPostId, limit = 3, locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'blogPost',
      'fields.category.sys.id': categoryId,
      'sys.id[ne]': currentPostId,
      order: '-fields.publishedAt',
      limit,
      locale,
      include: 2,
    });

    return entries.items;
  }

  // Products
  async getProducts(params = {}) {
    const { limit = 20, skip = 0, locale = 'en-US', category, brand, search } = params;

    const query = {
      content_type: 'product',
      order: 'fields.name',
      limit,
      skip,
      locale,
      include: 10,
    };

    if (category) {
      query['fields.category.sys.id'] = category;
    }

    if (brand) {
      query['fields.brand.sys.id'] = brand;
    }

    if (search) {
      query.query = search;
    }

    const entries = await this.client.getEntries(query);

    return {
      products: entries.items,
      total: entries.total,
      limit: entries.limit,
      skip: entries.skip,
    };
  }

  async getProductBySlug(slug, locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'product',
      'fields.slug': slug,
      locale,
      include: 10,
      limit: 1,
    });

    return entries.items[0] || null;
  }

  // Categories
  async getCategories(locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'category',
      order: 'fields.name',
      locale,
    });

    return entries.items;
  }

  // Tags
  async getTags(locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'tag',
      order: 'fields.name',
      locale,
    });

    return entries.items;
  }

  // Authors
  async getAuthors(locale = 'en-US') {
    const entries = await this.client.getEntries({
      content_type: 'author',
      order: 'fields.name',
      locale,
      include: 1,
    });

    return entries.items;
  }

  // Assets
  async getAssets(params = {}) {
    const { limit = 100, skip = 0, contentType } = params;

    const query = {
      limit,
      skip,
      include: 1,
    };

    if (contentType) {
      query['mimetype_group'] = contentType; // 'image', 'video', 'audio', 'text'
    }

    const assets = await this.client.getAssets(query);

    return {
      assets: assets.items,
      total: assets.total,
      limit: assets.limit,
      skip: assets.skip,
    };
  }

  // Search
  async searchAll(query, options = {}) {
    const { limit = 50, locale = 'en-US' } = options;

    const searchQuery = {
      query,
      limit,
      locale,
      include: 5,
    };

    const result = await this.client.getEntries(searchQuery);

    return {
      items: result.items,
      total: result.total,
    };
  }
}

// File: components/BlogPostCard.js - Frontend Component

import React from 'react';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { contentfulClient } from '../lib/contentful';

const BlogPostCard = ({ post, className = '' }) => {
  if (!post) return null;

  const {
    title,
    slug,
    excerpt,
    featuredImage,
    author,
    category,
    publishedAt,
    tags,
    readingTime,
  } = post.fields;

  const imageUrl = featuredImage?.fields?.file?.url;
  const authorName = author?.fields?.name;
  const categoryName = category?.fields?.name;

  return (
    <article className={`blog-post-card ${className}`}>
      {imageUrl && (
        <div className="blog-post-card__image">
          <img
            src={imageUrl}
            alt={featuredImage.fields.title || title}
            loading="lazy"
          />
        </div>
      )}

      <div className="blog-post-card__content">
        {categoryName && (
          <span className="blog-post-card__category">
            {categoryName}
          </span>
        )}

        <h3 className="blog-post-card__title">
          <a href={`/blog/${slug}`}>
            {title}
          </a>
        </h3>

        {excerpt && (
          <p className="blog-post-card__excerpt">
            {excerpt}
          </p>
        )}

        <div className="blog-post-card__meta">
          {authorName && (
            <span className="blog-post-card__author">
              By {authorName}
            </span>
          )}

          {publishedAt && (
            <time className="blog-post-card__date">
              {new Date(publishedAt).toLocaleDateString()}
            </time>
          )}

          {readingTime && (
            <span className="blog-post-card__reading-time">
              {readingTime} min read
            </span>
          )}
        </div>

        {tags && tags.length > 0 && (
          <div className="blog-post-card__tags">
            {tags.map(tag => (
              <span key={tag.sys.id} className="blog-post-card__tag">
                {tag.fields.name}
              </span>
            ))}
          </div>
        )}
      </div>
    </article>
  );
};

export default BlogPostCard;

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

import { createClient } from 'contentful';

export const contentfulGraphQLClient = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
});

// GraphQL Queries
export const GET_BLOG_POSTS = `
  query GetBlogPosts(
    $limit: Int = 10
    $skip: Int = 0
    $locale: String = "en-US"
    $order: [BlogPostOrder!] = [publishedAt_DESC]
  ) {
    blogPostCollection(
      limit: $limit
      skip: $skip
      locale: $locale
      order: $order
      preview: false
    ) {
      items {
        sys {
          id
        }
        title
        slug
        excerpt
        publishedAt
        readingTime
        isFeatured
        author {
          ... on Author {
            name
            bio
            avatar {
              url
              title
              description
              width
              height
            }
          }
        }
        category {
          ... on Category {
            name
            slug
            description
          }
        }
        tags {
          ... on Tag {
            name
            slug
          }
        }
        featuredImage {
          ... on Asset {
            url
            title
            description
            width
            height
          }
        }
        seo {
          ... on SEO {
            metaTitle
            metaDescription
            keywords
          }
        }
      }
      total
    }
  }
`;

export const GET_BLOG_POST_BY_SLUG = `
  query GetBlogPostBySlug(
    $slug: String!
    $locale: String = "en-US"
    $preview: Boolean = false
  ) {
    blogPostCollection(
      limit: 1
      where: { slug: $slug }
      locale: $locale
      preview: $preview
    ) {
      items {
        sys {
          id
          publishedAt
        }
        title
        slug
        content {
          json
          links {
            asset {
              block {
                sys {
                  id
                }
              }
              hyperlink {
                sys {
                  id
                }
              }
            }
            entry {
              block {
                sys {
                  id
                }
              }
              inline {
                sys {
                  id
                }
              }
            }
          }
        }
        publishedAt
        readingTime
        author {
          ... on Author {
            name
            bio
            avatar {
              url
              title
              description
              width
              height
            }
            social {
              twitter
              github
              linkedin
              website
            }
          }
        }
        category {
          ... on Category {
            name
            slug
            description
          }
        }
        tags {
          ... on Tag {
            name
            slug
          }
        }
        featuredImage {
          ... on Asset {
            url
            title
            description
            width
            height
          }
        }
        seo {
          ... on SEO {
            metaTitle
            metaDescription
            keywords
            ogImage {
              url
              title
              description
              width
              height
            }
            structuredData {
              json
            }
          }
        }
      }
    }
  }
`;

export const GET_PRODUCTS = `
  query GetProducts(
    $limit: Int = 20
    $skip: Int = 0
    $locale: String = "en-US"
    $order: [ProductOrder!] = "name_ASC"
    $where: ProductFilter = {}
  ) {
    productCollection(
      limit: $limit
      skip: $skip
      locale: $locale
      order: $order
      where: $where
      preview: false
    ) {
      items {
        sys {
          id
        }
        name
        slug
        shortDescription
        sku
        price
        comparePrice
        currency
        isActive
        category {
          ... on ProductCategory {
            name
            slug
          }
        }
        brand {
          ... on Brand {
            name
            slug
            logo {
              url
              title
              description
              width
              height
            }
          }
        }
        images {
          ... on Asset {
            url
            title
            description
            width
            height
          }
        }
      }
      total
    }
  }
`;

export async function fetchGraphQL(query, variables = {}) {
  try {
    const response = await contentfulGraphQLClient.graphqlRequest(query, variables);
    return response;
  } catch (error) {
    console.error('GraphQL query error:', error);
    throw error;
  }
}

// Usage example:
// const posts = await fetchGraphQL(GET_BLOG_POSTS, { limit: 5, locale: 'es-ES' });
Code sourcejavascriptComplexe

Migrations et Webhooks Contentful

Scripts complets de migration de contenu, gestion des webhooks et workflows de déploiement automatisés

Difficulté

9/10

Temps estimé

50 min

Étiquettes

contentful, migrations, webhooks, deployment

Prérequis

Contentful basics, Migration concepts, Webhook concepts, Node.js, Deployment

javascript
// Contentful Migrations & Webhooks Examples
// File: scripts/migrate-content.js

import { createClient } from 'contentful-management';

const managementClient = createClient({
  accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});

class ContentfulMigrator {
  constructor() {
    this.client = managementClient;
  }

  async migrate() {
    try {
      console.log('Starting Contentful migration...');

      // Create content types
      await this.createContentTypes();

      // Create entries
      await this.createEntries();

      // Create assets
      await this.createAssets();

      console.log('Migration completed successfully!');
    } catch (error) {
      console.error('Migration failed:', error);
      throw error;
    }
  }

  async createContentTypes() {
    console.log('Creating content types...');

    // Blog Post Content Type
    const blogPostContentType = await this.client.contentType.create({
      name: 'Blog Post',
      description: 'Blog posts with rich content',
      displayField: 'title',
    });

    await blogPostContentType.fields.create({
      id: 'title',
      name: 'Title',
      type: 'Text',
      required: true,
      localized: true,
    });

    await blogPostContentType.fields.create({
      id: 'slug',
      name: 'Slug',
      type: 'Text',
      required: true,
      unique: true,
    });

    await blogPostContentType.fields.create({
      id: 'content',
      name: 'Content',
      type: 'RichText',
      required: true,
      localized: true,
    });

    await blogPostContentType.fields.create({
      id: 'author',
      name: 'Author',
      type: 'Link',
      linkType: 'Entry',
      required: true,
      validations: [
        {
          linkContentType: ['author'],
        },
      ],
    });

    console.log('Created blog post content type');
  }

  async createEntries() {
    console.log('Creating entries...');

    // Get existing content types
    const authorContentType = await this.client.contentType.get('author');

    if (!authorContentType) {
      throw new Error('Author content type not found');
    }

    // Create author entry
    const authorEntry = await this.client.entry.create({
      contentTypeId: authorContentType.sys.id,
      fields: {
        name: {
          'en-US': 'John Doe',
        },
        email: 'john.doe@example.com',
        bio: {
          'en-US': 'Software developer and tech blogger.',
        },
      },
    });

    // Publish author
    await authorEntry.publish();

    console.log('Created author entry:', authorEntry.sys.id);
  }

  async createAssets() {
    console.log('Creating assets...');

    // Create example image asset
    const asset = await this.client.asset.createFromFile({
      file: {
        name: 'example-image.jpg',
        mimeType: 'image/jpeg',
        url: 'https://example.com/image.jpg',
      },
    });

    // Process the asset
    await asset.processForLocale({
      locale: 'en-US',
      processingOptions: {
        format: 'webp',
        width: 1200,
        height: 800,
      },
    });

    await asset.publish();

    console.log('Created asset:', asset.sys.id);
  }
}

// File: scripts/import-from-wordpress.js

import fs from 'fs';
import path from 'path';
import { createClient } from 'contentful-management';

class WordPressImporter {
  constructor() {
    this.client = createClient({
      accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
    });
    this.importedAuthors = new Map();
    this.importedCategories = new Map();
    this.importedTags = new Map();
  }

  async import(exportPath) {
    try {
      console.log('Starting WordPress import...');

      // Load WordPress export
      const exportData = JSON.parse(fs.readFileSync(exportPath, 'utf8'));

      // Import authors
      await this.importAuthors(exportData.users);

      // Import categories
      await this.importCategories(exportData.categories);

      // Import tags
      await this.importTags(exportData.tags);

      // Import posts
      await this.importPosts(exportData.posts);

      console.log('WordPress import completed!');
    } catch (error) {
      console.error('WordPress import failed:', error);
      throw error;
    }
  }

  async importAuthors(users) {
    console.log(`Importing ${users.length} authors...`);

    const authorContentType = await this.client.contentType.get('author');

    for (const user of users) {
      try {
        const authorEntry = await this.client.entry.create({
          contentTypeId: authorContentType.sys.id,
          fields: {
            name: {
              'en-US': user.name,
            },
            email: user.email,
            bio: {
              'en-US': user.description || '',
            },
            social: {
              'en-US': {
                website: user.url || '',
                twitter: '',
                github: '',
              },
            },
          },
        });

        await authorEntry.publish();

        this.importedAuthors.set(user.id, authorEntry.sys.id);
        console.log(`Imported author: ${user.name}`);
      } catch (error) {
        console.error(`Failed to import author ${user.name}:`, error);
      }
    }
  }

  async importCategories(categories) {
    console.log(`Importing ${categories.length} categories...`);

    const categoryContentType = await this.client.contentType.get('category');

    for (const category of categories) {
      try {
        const categoryEntry = await this.client.entry.create({
          contentTypeId: categoryContentType.sys.id,
          fields: {
            name: {
              'en-US': category.name,
            },
            slug: {
              'en-US': category.slug,
            },
            description: {
              'en-US': category.description || '',
            },
          },
        });

        await categoryEntry.publish();

        this.importedCategories.set(category.id, categoryEntry.sys.id);
        console.log(`Imported category: ${category.name}`);
      } catch (error) {
        console.error(`Failed to import category ${category.name}:`, error);
      }
    }
  }

  async importTags(tags) {
    console.log(`Importing ${tags.length} tags...`);

    const tagContentType = await this.client.contentType.get('tag');

    for (const tag of tags) {
      try {
        const tagEntry = await this.client.entry.create({
          contentTypeId: tagContentType.sys.id,
          fields: {
            name: {
              'en-US': tag.name,
            },
            slug: {
              'en-US': tag.slug,
            },
          },
        });

        await tagEntry.publish();

        this.importedTags.set(tag.id, tagEntry.sys.id);
        console.log(`Imported tag: ${tag.name}`);
      } catch (error) {
        console.error(`Failed to import tag ${tag.name}:`, error);
      }
    }
  }

  async importPosts(posts) {
    console.log(`Importing ${posts.length} posts...`);

    const blogPostContentType = await this.client.contentType.get('blogPost');

    for (const post of posts) {
      try {
        // Map WordPress author to Contentful author
        const authorId = this.importedAuthors.get(post.author);

        // Map WordPress categories to Contentful categories
        const categoryIds = post.categories
          .map(catId => this.importedCategories.get(catId))
          .filter(Boolean);

        // Map WordPress tags to Contentful tags
        const tagIds = post.tags
          .map(tagId => this.importedTags.get(tagId))
          .filter(Boolean);

        const postEntry = await this.client.entry.create({
          contentTypeId: blogPostContentType.sys.id,
          fields: {
            title: {
              'en-US': post.title.rendered,
            },
            slug: {
              'en-US': post.slug,
            },
            content: {
              'en-US': this.convertHtmlToRichText(post.content.rendered),
            },
            excerpt: {
              'en-US': post.excerpt.rendered,
            },
            author: {
              'en-US': authorId ? { sys: { type: 'Link', linkType: 'Entry', id: authorId } } : null,
            },
            publishedAt: post.date_gmt,
            isFeatured: post.sticky === true,
          },
        });

        // Add categories and tags after entry creation
        if (categoryIds.length > 0) {
          await postEntry.patch({
            fields: {
              category: {
                'en-US': {
                  sys: {
                    type: 'Link',
                    linkType: 'Entry',
                    id: categoryIds[0],
                  },
                },
              },
            },
          });
        }

        if (tagIds.length > 0) {
          await postEntry.patch({
            fields: {
              tags: {
                'en-US': tagIds.map(id => ({
                  sys: {
                    type: 'Link',
                    linkType: 'Entry',
                    id,
                  },
                })),
              },
            },
          });
        }

        await postEntry.publish();

        console.log(`Imported post: ${post.title.rendered}`);
      } catch (error) {
        console.error(`Failed to import post ${post.title.rendered}:`, error);
      }
    }
  }

  convertHtmlToRichText(html) {
    // Simple HTML to Rich Text conversion
    // In a real implementation, you'd use a proper HTML parser
    return {
      nodeType: 'document',
      data: {},
      content: [
        {
          nodeType: 'paragraph',
          data: {},
          content: [
            {
              nodeType: 'text',
              value: html.replace(/<[^>]*>/g, ''),
              marks: [],
            },
          ],
        },
      ],
    };
  }
}

// File: api/webhooks/contentful.js - Webhook Handler

import crypto from 'crypto';

class ContentfulWebhookHandler {
  constructor(secret) {
    this.secret = secret;
  }

  verifySignature(body, signature) {
    const expectedSignature = crypto
      .createHmac('sha256', this.secret)
      .update(body)
      .digest('hex');

    return signature === expectedSignature;
  }

  async handleWebhook(req, res) {
    try {
      const signature = req.headers['x-contentful-webhook-secret'];
      const body = JSON.stringify(req.body);

      if (!this.verifySignature(body, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      const event = req.body;
      console.log(`Received webhook event: ${event.sys.type}`);

      switch (event.sys.type) {
        case 'Entry.create':
          await this.handleEntryCreate(event);
          break;
        case 'Entry.save':
          await this.handleEntryUpdate(event);
          break;
        case 'Entry.delete':
          await this.handleEntryDelete(event);
          break;
        case 'Entry.publish':
          await this.handleEntryPublish(event);
          break;
        case 'Entry.unpublish':
          await this.handleEntryUnpublish(event);
          break;
        default:
          console.log(`Unhandled event type: ${event.sys.type}`);
      }

      res.status(200).json({ success: true });
    } catch (error) {
      console.error('Webhook error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

  async handleEntryCreate(event) {
    const { contentType, fields } = event;
    console.log(`New entry created in ${contentType.sys.id}`);

    // Trigger revalidation for Next.js
    if (contentType.sys.id === 'blogPost') {
      await this.revalidateBlogPosts();
    }

    // Send notification
    await this.sendNotification('content_created', {
      contentType: contentType.sys.id,
      entryId: event.sys.id,
    });
  }

  async handleEntryUpdate(event) {
    const { contentType, fields } = event;
    console.log(`Entry updated in ${contentType.sys.id}`);

    // Trigger revalidation
    if (contentType.sys.id === 'blogPost') {
      await this.revalidateBlogPost(fields.slug);
    }
  }

  async handleEntryDelete(event) {
    const { contentType } = event;
    console.log(`Entry deleted from ${contentType.sys.id}`);

    // Send notification
    await this.sendNotification('content_deleted', {
      contentType: contentType.sys.id,
      entryId: event.sys.id,
    });
  }

  async handleEntryPublish(event) {
    const { contentType, fields } = event;
    console.log(`Entry published in ${contentType.sys.id}`);

    // Trigger revalidation
    if (contentType.sys.id === 'blogPost') {
      await this.revalidateBlogPost(fields.slug);
      await this.revalidateBlogPosts();
    }
  }

  async handleEntryUnpublish(event) {
    const { contentType, fields } = event;
    console.log(`Entry unpublished from ${contentType.sys.id}`);

    // Trigger revalidation
    if (contentType.sys.id === 'blogPost') {
      await this.revalidateBlogPost(fields.slug);
      await this.revalidateBlogPosts();
    }
  }

  async revalidateBlogPost(slug) {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          type: 'blog-post',
          slug,
        }),
      });

      if (!res.ok) {
        console.error(`Revalidation failed for blog post: ${slug}`);
        return;
      }

      console.log(`Revalidated blog post: ${slug}`);
    } catch (error) {
      console.error(`Error revalidating blog post ${slug}:`, error);
    }
  }

  async revalidateBlogPosts() {
    try {
      const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          type: 'blog-posts',
        }),
      });

      if (!res.ok) {
        console.error('Revalidation failed for blog posts');
        return;
      }

      console.log('Revalidated blog posts');
    } catch (error) {
      console.error('Error revalidating blog posts:', error);
    }
  }

  async sendNotification(type, data) {
    try {
      // Send to Slack
      await this.sendSlackNotification(type, data);

      // Send email
      await this.sendEmailNotification(type, data);
    } catch (error) {
      console.error('Error sending notification:', error);
    }
  }

  async sendSlackNotification(type, data) {
    if (!process.env.SLACK_WEBHOOK_URL) {
      return;
    }

    const message = this.formatSlackMessage(type, data);

    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text: message }),
    });
  }

  async sendEmailNotification(type, data) {
    // Implementation depends on your email service
    console.log(`Email notification: ${type}`, data);
  }

  formatSlackMessage(type, data) {
    const emojis = {
      'content_created': '✨',
      'content_updated': '✏️',
      'content_deleted': '🗑️',
    };

    return `${emojis[type]} Contentful ${type.replace('_', ' ')}
` +
           `Content Type: ${data.contentType}
` +
           `Entry ID: ${data.entryId}`;
  }
}

// File: scripts/deploy-contentful.js

import { createClient } from 'contentful-management';
import { Client as AppClient } from '@contentful/app-sdk';

class ContentfulDeployer {
  constructor() {
    this.managementClient = createClient({
      accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
    });

    this.appClient = new AppClient({
      accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
    });
  }

  async deploy() {
    try {
      console.log('Starting Contentful deployment...');

      // Validate configuration
      await this.validateConfiguration();

      // Deploy content types
      await this.deployContentTypes();

      // Deploy entries
      await this.deployEntries();

      // Deploy assets
      await this.deployAssets();

      // Run post-deployment checks
      await this.runPostDeploymentChecks();

      console.log('Contentful deployment completed successfully!');
    } catch (error) {
      console.error('Contentful deployment failed:', error);
      throw error;
    }
  }

  async validateConfiguration() {
    console.log('Validating Contentful configuration...');

    const space = await this.managementClient.getSpace();

    if (!space) {
      throw new Error('Contentful space not found');
    }

    console.log(`Connected to space: ${space.name}`);
  }

  async deployContentTypes() {
    console.log('Deploying content types...');

    // Load content type definitions from files
    const contentTypesDir = './content-types';
    const contentTypes = fs.readdirSync(contentTypesDir);

    for (const file of contentTypes) {
      if (file.endsWith('.json')) {
        const contentTypeDef = JSON.parse(
          fs.readFileSync(path.join(contentTypesDir, file), 'utf8')
        );

        await this.deployContentType(contentTypeDef);
      }
    }
  }

  async deployContentType(contentTypeDef) {
    try {
      console.log(`Deploying content type: ${contentTypeDef.name}`);

      // Check if content type already exists
      let contentType;
      try {
        contentType = await this.managementClient.contentType.get(contentTypeDef.name.toLowerCase());
      } catch (error) {
        // Content type doesn't exist, create it
        contentType = await this.managementClient.contentType.create({
          name: contentTypeDef.name,
          description: contentTypeDef.description,
          displayField: contentTypeDef.displayField,
        });
      }

      // Update fields
      for (const fieldDef of contentTypeDef.fields) {
        try {
          await contentType.fields.create(fieldDef);
        } catch (error) {
          // Field might already exist, try to update it
          console.log(`Field ${fieldDef.id} might already exist, skipping...`);
        }
      }

      console.log(`Deployed content type: ${contentTypeDef.name}`);
    } catch (error) {
      console.error(`Failed to deploy content type ${contentTypeDef.name}:`, error);
    }
  }

  async deployEntries() {
    console.log('Deploying entries...');

    // Load entries from data files
    const entriesDir = './data/entries';
    const entryDirs = fs.readdirSync(entriesDir);

    for (const dir of entryDirs) {
      const entriesPath = path.join(entriesDir, dir);
      if (fs.statSync(entriesPath).isDirectory()) {
        await this.deployEntriesForContentType(dir, entriesPath);
      }
    }
  }

  async deployEntriesForContentType(contentTypeName, entriesPath) {
    try {
      const contentType = await this.managementClient.contentType.get(contentTypeName);

      const entryFiles = fs.readdirSync(entriesPath);
      const entries = [];

      for (const file of entryFiles) {
        if (file.endsWith('.json')) {
          const entryData = JSON.parse(
            fs.readFileSync(path.join(entriesPath, file), 'utf8')
          );

          const entry = await this.managementClient.entry.create({
            contentTypeId: contentType.sys.id,
            fields: entryData.fields,
          });

          await entry.publish();
          entries.push(entry);
        }
      }

      console.log(`Deployed ${entries.length} entries for ${contentTypeName}`);
    } catch (error) {
      console.error(`Failed to deploy entries for ${contentTypeName}:`, error);
    }
  }

  async deployAssets() {
    console.log('Deploying assets...');

    const assetsDir = './data/assets';
    const assetFiles = fs.readdirSync(assetsDir);

    for (const file of assetFiles) {
      const assetPath = path.join(assetsDir, file);
      const stats = fs.statSync(assetPath);

      if (stats.isFile() && this.isImageFile(file)) {
        await this.deployAsset(assetPath, file);
      }
    }
  }

  async deployAsset(assetPath, fileName) {
    try {
      console.log(`Deploying asset: ${fileName}`);

      const fileBuffer = fs.readFileSync(assetPath);
      const mimeType = this.getMimeType(fileName);

      const asset = await this.managementClient.asset.createFromFile({
        file: {
          name: fileName,
          mimeType,
          data: fileBuffer,
        },
      });

      // Process for web
      await asset.processForLocale({
        locale: 'en-US',
        processingOptions: {
          format: 'webp',
          quality: 85,
        },
      });

      await asset.publish();

      console.log(`Deployed asset: ${fileName}`);
    } catch (error) {
      console.error(`Failed to deploy asset ${fileName}:`, error);
    }
  }

  async runPostDeploymentChecks() {
    console.log('Running post-deployment checks...');

    // Check if required content types exist
    const requiredContentTypes = ['blogPost', 'product', 'author', 'category', 'tag'];

    for (const contentTypeName of requiredContentTypes) {
      try {
        await this.managementClient.contentType.get(contentTypeName);
        console.log(`✓ Content type ${contentTypeName} exists`);
      } catch (error) {
        console.error(`✗ Content type ${contentTypeName} not found`);
        throw new Error(`Missing required content type: ${contentTypeName}`);
      }
    }

    // Check if entries exist
    const blogPostContentType = await this.managementClient.contentType.get('blogPost');
    const posts = await this.managementClient.getEntries({
      content_type: blogPostContentType.sys.id,
    });

    console.log(`Found ${posts.total} blog posts`);

    if (posts.total === 0) {
      console.warn('Warning: No blog posts found');
    }
  }

  isImageFile(filename) {
    const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
    return imageExtensions.includes(path.extname(filename).toLowerCase());
  }

  getMimeType(filename) {
    const ext = path.extname(filename).toLowerCase();
    const mimeTypes = {
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
      '.bmp': 'image/bmp',
      '.webp': 'image/webp',
      '.svg': 'image/svg+xml',
    };

    return mimeTypes[ext] || 'application/octet-stream';
  }
}

// Usage examples
if (require.main === module) {
  const args = process.argv.slice(2);
  const command = args[0];

  switch (command) {
    case 'migrate':
      const migrator = new ContentfulMigrator();
      migrator.migrate().catch(console.error);
      break;

    case 'import-wordpress':
      const importer = new WordPressImporter();
      importer.import('./wordpress-export.json').catch(console.error);
      break;

    case 'deploy':
      const deployer = new ContentfulDeployer();
      deployer.deploy().catch(console.error);
      break;

    default:
      console.log('Available commands:');
      console.log('  migrate - Run migration');
      console.log('  import-wordpress - Import from WordPress');
      console.log('  deploy - Deploy content');
  }
}

Outils

Outils souvent utilisés avec cet exemple

Associé

Collections d’exemples associées

Associé

Dossiers associés

Dossier

Déduire un JSON Schema depuis des exemples et des tableurs

Produisez un brouillon de JSON Schema relisible à partir d'exemples JSON représentatifs ou de lignes XLSX, puis examinez types, caractère obligatoire, valeurs nulles et anomalies avant toute adoption contractuelle.

Dossier

Revue et livraison d’un module de jeu promotionnel auto-hébergé

Contrôlez configuration, exemples de réponses, lisibilité et affichage adaptatif avant de livrer les fichiers du module et les preuves de recette de son intégration.

Dossier

Contrôles de format des identifiants d'infrastructure réseau

Vérifiez la syntaxe des adresses IPv4 et IPv6, des valeurs CIDR, des adresses MAC, des ARN AWS et des références d'images Docker avant leur traitement, en séparant les preuves de format de l'accessibilité, de la propriété, des droits et de la disponibilité de l'image.

Dossier

Modeles pharmacocinetiques pedagogiques et controle des resultats

Explorer les formules pharmacocinetiques, les hypotheses de modele et la coherence des resultats pour l'enseignement ou la QA, sans avis clinique.

Accueil
Outils3212
Atelier
Exemples593
Visualisations471
Visionneuses de fichiers103
Dossiers239
Étiquettes333
Images, audio et vidéo657

Outils de traitement, conversion et optimisation d’images, d’audio et de vidéo

Maths, dates et finance647

Calculateurs, outils numériques, logique de dates, statistiques et finance

Design et couleur298

Outils de couleur, mise en page, graphisme, style visuel et assistance design

Texte et rédaction209

Outils de rédaction, nettoyage, formatage, extraction et analyse de texte

Développement et Web193

Utilitaires développeur, réseau, débogage Web et assistants d’automatisation

Conversion et encodage175

Outils de conversion de formats, fichiers, archives, unités et encodages

Lancement d’un quiz de prospection et transmission des contacts
Textes commémoratifs : validation familiale et remise pour impression
Habitudes privées : sauvegarde, restauration et partage vérifié
D’un article aux brouillons X et LinkedIn : revue factuelle et livraison
Revue de cours Markdown interactif et livraison hors ligne

© 2026 Elysia Tools. Tous droits réservés.

DocumentationTarifsÀ proposConditions d'utilisationPolitique de confidentialitéCookiesContact

Health

Calculateur d'Empreinte Carbone (par activité)

Comptabilise les émissions personnelles annuelles (kg CO₂e) par activité : régime alimentaire (de 2.6 t forte viande à 1.1 t végan/an), électricité × intensité du réseau (0.82 charbon → 0.05 bas carbone kg/kWh), gaz naturel (2.02 kg/m³), essence (2.31 kg/L), diesel (2.68 kg/L), vols courts (0.15 kg/passager-km), vols longs (0.11), transports en commun (0.04) et déchets mixtes (0.5 kg/kg). Fournit la ventilation par catégorie avec pourcentages, le total (t CO₂e/an), la comparaison à la moyenne mondiale de 4.7 t et à l'objectif 2.3 t pour 1.5 °C, et identifie le principal levier de réduction ; les facteurs vols excluent le forçage radiatif ~1.9× (signalé en sortie). Facteurs pédagogiques approximatifs — substituez les facteurs nationaux officiels pour un rapport formel.

Development

Calculateur de coût sandbox éphémère et auditeur de démontage

Chiffre un sandbox jetable sur AWS, GCP, Azure, Vercel, Fly.io, Railway, Replit, Codespaces et Gitpod, puis audite ce que coûte chaque jour d’oubli de démontage.

Fitness

Score de récupération et de disponibilité basé sur la VFC

Compare la VFC matinale (rMSSD/SDNN) et la fréquence de repos à votre propre base glissante via des z-scores standardisés, puis les combine avec le sommeil de la nuit dernière et la RPE d’hier en un score de disponibilité 0-100 — paliers vert/jaune/rouge, détection de changement significatif (0,5 DS), contrôle du bruit de base (CV) et plafond d’intensité recommandé.

Media

Convertir PNG en PNG avec Palette

Convertir les images PNG au format basé sur palette avec couleurs indexées pour une taille de fichier plus petite

Data Visualization

Générateur de Nuage de Mots

Générer de beaux nuages de mots à partir de texte avec des polices, couleurs et tailles personnalisables

CMS

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

CMS

Exemples Sanity CMS Temps Réel

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

Frameworks web

Exemples Fastify

Exemples du framework web Node.js haute performance Fastify incluant les plugins, hooks, validation et techniques d'optimisation