Elysia Tools

在一个清晰的工作台里浏览工具、案例、可视化和主题指南。

热门分类

专题

语言
ZHDERUPTENFRES

Elysia Tools

导航

CMS

Contentful 基于云的CMS 示例

全面的Contentful示例,涵盖内容建模、API集成、Webhook、迁移和前端集成模式

案例

这个案例集合中的条目

代码javascript中等

Contentful 内容建模

完整的内容类型定义、字段配置和各种用例的关系建模

难度

6/10

预计时间

30 min

标签

contentful, content modeling, cms

前置条件

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"
      }
    }
  }
}
代码javascript复杂

Contentful API 集成

完整的API集成示例,包含REST、GraphQL、Webhook和前端实现

难度

8/10

预计时间

40 min

标签

contentful, api, integration, frontend

前置条件

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' });
代码javascript复杂

Contentful 迁移和Webhook

完整的内容迁移脚本、Webhook处理和自动化部署工作流

难度

9/10

预计时间

50 min

标签

contentful, migrations, webhooks, deployment

前置条件

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

工具

经常与该案例搭配使用的工具

相关内容

相关案例集合

相关内容

相关专题

专题

本地习惯记录的备份、恢复演练与分享前检查

保留可恢复的习惯记录 JSON,在隔离环境演练导入并核对日期与备注,再准备不泄露私人信息的 CSV 或 PNG 分享副本。

专题

已有译文字幕的同步修复与双语交付

区分已有译文字幕的固定偏移、持续漂移与剪辑差异,核对语义配对,合并双语字幕,并验收可读性和可切换播放。

专题

域名上线前的 DNS、DNSSEC 与 HTTPS 验收

先拟定 DNS 区域记录,再核对在线解析、DNSSEC 委派和实际提供的 HTTPS 证书,为域名上线留下可复核的依据。

专题

修复被数据库拒绝的 CSV 导入并完成入库

定位 CSV 导入反复失败的结构原因,把混乱表头对齐到目标表结构,按声明的策略清除重复行,最终产出带类型的 INSERT 语句或供批量装载的 TSV。

首页
工具3212
工作台
案例593
可视化471
文件查看器103
专题239
标签333
图像、音频与视频657

图像、音频、视频处理、转换与优化工具

数学、日期与金融647

计算器、数值计算、日期逻辑、统计与金融工具

设计与颜色298

颜色、版式、图形、视觉样式与设计辅助工具

文本与写作209

写作、清洗、格式化、提取与文本分析工具

开发与 Web193

开发者工具、网络处理、Web 调试与自动化辅助工具

转换与编码175

格式、文件、压缩包、单位与编码转换工具

潜客测验发布与联系人交接
讣告、悼词与纪念卡的家属复核及交付
本地习惯记录的备份、恢复演练与分享前检查
长文改编 X 串文与 LinkedIn 帖子的事实复核与交付
Markdown 互动课件审校、离线演示与交付

© 2026 Elysia Tools. 保留所有权利。

文档价格关于我们服务条款隐私政策Cookie 政策联系我们

Health

碳排放核算计算器(按活动量)

按活动量逐年核算个人碳排放(kg CO₂e):饮食模式(高肉食 2.6 → 纯素 1.1 t/年)×用电量×电网碳强度(煤电 0.82 → 低碳 0.05 kg/kWh)、天然气(2.02 kg/m³)、汽油(2.31 kg/L)、柴油(2.68 kg/L)、短途航班(0.15 kg/人·km)、长途航班(0.11)、公交(0.04)、混合垃圾(0.5 kg/kg)。输出分项排放与占比、总量(t CO₂e/年)、与世界平均 4.7 t 及 1.5 °C 目标 2.3 t 的对照、最大减排杠杆识别;航班因子未含约 1.9 倍辐射强迫上浮(输出中有提示)。因子为教学用近似值,正式报告请替换为本国官方因子。

Development

一次性云沙箱成本计算与清理审计器

在 AWS、GCP、Azure、Vercel、Fly.io、Railway、Replit、Codespaces、Gitpod 九家之间为一次性沙箱定价,并审计「忘记清理」每一天的真实成本。

Fitness

HRV 恢复与准备度评分器

把晨间 HRV(rMSSD/SDNN)与静息心率对个人滚动基线做标准化 z 评分,结合昨夜睡眠与昨日训练 RPE 加权为 0-100 准备度分数,输出绿/黄/红三档、0.5 SD 最小有意义变化判定、基线噪声(CV)检查与强度上限训练建议。

Media

PNG转调色板PNG

将PNG图像转换为基于调色板的格式,使用索引颜色以获得更小的文件大小

Data Visualization

词云生成器

从文本生成美观的词云,支持自定义字体、颜色和大小

CMS

Strapi 无头CMS 示例

全面的Strapi示例,涵盖内容建模、API集成、身份验证、插件和部署模式

CMS

Sanity 实时CMS 示例

全面的Sanity示例,涵盖实时内容管理、Studio自定义、API集成和部署模式

Web 框架

Fastify 示例

Fastify 高性能 Node.js Web 框架示例,包括插件、钩子、验证和优化技术