Elysia Tools

Explora herramientas, ejemplos, explicaciones visuales y guías temáticas en un solo espacio claro.

Categorías populares

Temas

Idioma
ZHDERUPTENFRES

Elysia Tools

Navegación

CMS

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

Ejemplos

Entradas de esta colección

CódigojavascriptIntermedio

Modelado de Contenido Contentful

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

Dificultad

6/10

Tiempo estimado

30 min

Etiquetas

contentful, content modeling, cms

Requisitos previos

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"
      }
    }
  }
}
CódigojavascriptComplejo

Integración de API Contentful

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

Dificultad

8/10

Tiempo estimado

40 min

Etiquetas

contentful, api, integration, frontend

Requisitos previos

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' });
CódigojavascriptComplejo

Migraciones y Webhooks de Contentful

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

Dificultad

9/10

Tiempo estimado

50 min

Etiquetas

contentful, migrations, webhooks, deployment

Requisitos previos

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');
  }
}

Herramientas

Herramientas que suelen usarse con este ejemplo

Relacionado

Colecciones de ejemplos relacionadas

Relacionado

Temas relacionados

Tema

Conversión de unidades de tamaño de datos sin la confusión entre KB y KiB

Convierte entre bits, bytes y los prefijos decimales SI KB, MB, GB, TB además de los prefijos binarios IEC KiB, MiB, GiB, TiB, dejando fijado el sistema de cada número antes de calcular.

Tema

Revisión de muebles en una habitación pequeña y entrega para la mudanza

Concilia medidas, compara distribuciones y entrega JSON, CSV y planos coherentes con comprobaciones pendientes de impresión y visita.

Tema

De una imagen y música a un vídeo de partículas revisado

Prepara una imagen y un fragmento musical, ajusta partículas que reaccionan al audio y entrega el MP4 revisado con póster, parámetros y registro de aprobación.

Tema

Revisión de enlaces de campaña y códigos QR antes del lanzamiento

Prepara enlaces con UTM, concilia parámetros, despliega un enlace corto opcional y verifica el QR final con un registro de publicación revisado.

Inicio
Herramientas3212
Banco de trabajo
Ejemplos593
Visualizaciones471
Visores de archivos103
Temas239
Etiquetas333
Imágenes, audio y video657

Herramientas de procesamiento, conversión y optimización de imágenes, audio y video

Matemáticas, fechas y finanzas647

Calculadoras, herramientas numéricas, lógica de fechas, estadística y finanzas

Diseño y color298

Herramientas de color, maquetación, gráficos, estilo visual y apoyo al diseño

Texto y redacción209

Herramientas de redacción, limpieza, formato, extracción y análisis de texto

Desarrollo y Web193

Utilidades para desarrolladores, red, depuración web y automatización

Conversión y codificación175

Herramientas de conversión de formatos, archivos, unidades, archivos comprimidos y codificación

Lanzamiento de un cuestionario de captación y entrega de contactos
Textos conmemorativos: revisión familiar y entrega para impresión
Hábitos privados: copia, restauración y revisión antes de compartir
De artículo a borradores de X y LinkedIn: revisión factual y entrega
Revisión de una lección Markdown interactiva y entrega sin conexión

© 2026 Elysia Tools. Todos los derechos reservados.

DocumentaciónPreciosAcerca deTérminos de servicioPolítica de privacidadCookiesContacto

Health

Calculadora de Huella de Carbono (por actividades)

Contabiliza las emisiones personales anuales (kg CO₂e) por actividades: patrón alimentario (de 2.6 t mucho carne a 1.1 t vegano/año), electricidad × intensidad de la red (0.82 carbón → 0.05 baja en carbono kg/kWh), gas natural (2.02 kg/m³), gasolina (2.31 kg/L), diésel (2.68 kg/L), vuelos cortos (0.15 kg/pasajero-km), vuelos largos (0.11), transporte público (0.04) y residuos mixtos (0.5 kg/kg). Devuelve el desglose por categoría con porcentajes, el total (t CO₂e/año), la comparación con la media mundial de 4.7 t y el objetivo de 2.3 t para 1.5 °C, e identifica la mayor palanca de reducción; los factores de vuelo excluyen el factor de forzamiento radiativo ~1.9 (se avisa en la salida). Factores aproximados de uso docente — use factores oficiales nacionales para informes formales.

Development

Calculadora de costos de sandbox efímero y auditor de limpieza

Precia un sandbox desechable en AWS, GCP, Azure, Vercel, Fly.io, Railway, Replit, Codespaces y Gitpod, y audita cuánto cuesta olvidar desmantelarlo cada día.

Fitness

Puntuación de Recuperación y Disponibilidad basada en HRV

Compara el HRV matinal (rMSSD/SDNN) y la FC en reposo con tu propia base móvil mediante z-scores estandarizados, los combina con el sueño de anoche y el RPE de ayer en una puntuación de disponibilidad 0-100, con niveles verde/ámbar/rojo, detección de cambio significativo (0,5 DE), control de ruido de la base (CV) y una recomendación de techo de intensidad.

Media

Convertir PNG a PNG con Paleta

Convierte imágenes PNG a formato basado en paleta con colores indexados para tamaño de archivo más pequeño

Data Visualization

Generador de Nube de Palabras

Generar hermosas nubes de palabras desde texto con fuentes, colores y tamaños personalizables

CMS

Ejemplos Strapi Headless CMS

Ejemplos completos de Strapi cubriendo modelado de contenido, integración de API, autenticación, plugins y patrones de despliegue

CMS

Ejemplos Sanity CMS en Tiempo Real

Ejemplos completos de Sanity cubriendo el modelado de contenido, integración de API y patrones de despliegue

Frameworks web

Ejemplos de Fastify

Ejemplos del framework web Node.js de alto rendimiento Fastify incluyendo plugins, hooks, validación y técnicas de optimización