Ejemplos Contentful CMS Basado en la Nube

Ejemplos completos de Contentful cubriendo modelado de contenido, integración de API, webhooks y patrones de integración frontend

💻 Modelado de Contenido Contentful javascript

🟡 intermediate ⭐⭐⭐

Definiciones completas de content types, configuraciones de campos y modelado de relaciones para varios casos de uso

⏱️ 30 min 🏷️ contentful, content modeling, cms
Prerequisites: Contentful basics, Content modeling concepts, JSON schema, Headless CMS
// 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"
      }
    }
  }
}

💻 Integración de API Contentful javascript

🔴 complex ⭐⭐⭐⭐

Ejemplos completos de integración de API incluyendo consultas REST y GraphQL, webhooks e implementación frontend

⏱️ 40 min 🏷️ contentful, api, integration, frontend
Prerequisites: Contentful basics, REST/GraphQL APIs, React hooks, JavaScript ES6+, Headless CMS concepts
// 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' });

💻 Migraciones y Webhooks de Contentful javascript

🔴 complex ⭐⭐⭐⭐⭐

Scripts completos de migración de contenido, manejo de webhooks y flujos de trabajo de despliegue automatizado

⏱️ 50 min 🏷️ contentful, migrations, webhooks, deployment
Prerequisites: Contentful basics, Migration concepts, Webhook concepts, Node.js, Deployment
// 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: '[email protected]',
        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');
  }
}