🎯 Ejemplos recomendados
Balanced sample collections from various categories for you to explore
Ejemplos Contentful CMS Basado en la Nube
Ejemplos completos de Contentful cubriendo modelado de contenido, integración de API, webhooks y patrones de integración frontend
💻 Modelado de Contenido Contentful javascript
🟡 intermediate
⭐⭐⭐
Definiciones completas de content types, configuraciones de campos y modelado de relaciones para varios casos de uso
⏱️ 30 min
🏷️ contentful, content modeling, cms
Prerequisites:
Contentful basics, Content modeling concepts, JSON schema, Headless CMS
// Contentful Content Modeling Examples
// File: content-types/blog-post.json
{
"name": "Blog Post",
"description": "Blog posts with rich content and metadata",
"displayField": "title",
"fields": [
{
"id": "title",
"name": "Title",
"type": "Text",
"required": true,
"localized": true,
"validations": {
"unique": true,
"size": {
"max": 100
}
}
},
{
"id": "slug",
"name": "Slug",
"type": "Text",
"required": true,
"unique": true,
"validations": {
"regexp": {
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
"flags": null
}
}
},
{
"id": "excerpt",
"name": "Excerpt",
"type": "Text",
"localized": true,
"validations": {
"size": {
"max": 200
}
}
},
{
"id": "content",
"name": "Content",
"type": "RichText",
"required": true,
"localized": true,
"validations": {
"enabledNodeTypes": [
"heading-1",
"heading-2",
"heading-3",
"heading-4",
"heading-5",
"heading-6",
"ordered-list",
"unordered-list",
"hr",
"blockquote",
"embedded-entry-block",
"embedded-asset-block",
"hyperlink",
"entry-hyperlink",
"asset-hyperlink"
],
"nodes": {}
}
},
{
"id": "featuredImage",
"name": "Featured Image",
"type": "Link",
"validations": {
"linkContentType": ["media"],
"required": false
}
},
{
"id": "gallery",
"name": "Gallery",
"type": "Array",
"items": {
"type": "Link",
"validations": {
"linkContentType": ["media"],
"required": false
}
}
},
{
"id": "author",
"name": "Author",
"type": "Link",
"validations": {
"linkContentType": ["author"],
"required": true
}
},
{
"id": "category",
"name": "Category",
"type": "Link",
"validations": {
"linkContentType": ["category"],
"required": true
}
},
{
"id": "tags",
"name": "Tags",
"type": "Array",
"items": {
"type": "Link",
"validations": {
"linkContentType": ["tag"],
"required": false
}
}
},
{
"id": "seo",
"name": "SEO",
"type": "Link",
"validations": {
"linkContentType": ["seo"],
"required": false
}
},
{
"id": "publishedAt",
"name": "Published Date",
"type": "Date",
"required": true
},
{
"id": "readingTime",
"name": "Reading Time",
"type": "Number",
"validations": {
"range": {
"min": 1
}
}
},
{
"id": "isFeatured",
"name": "Is Featured",
"type": "Boolean",
"defaultValue": false
}
]
}
// File: content-types/product.json
{
"name": "Product",
"description": "E-commerce product with variants and pricing",
"displayField": "name",
"fields": [
{
"id": "name",
"name": "Name",
"type": "Text",
"required": true,
"localized": true,
"validations": {
"unique": true
}
},
{
"id": "slug",
"name": "Slug",
"type": "Text",
"required": true,
"unique": true
},
{
"id": "description",
"name": "Description",
"type": "RichText",
"required": true,
"localized": true
},
{
"id": "shortDescription",
"name": "Short Description",
"type": "Text",
"localized": true,
"validations": {
"size": {
"max": 500
}
}
},
{
"id": "sku",
"name": "SKU",
"type": "Text",
"required": true,
"unique": true,
"validations": {
"regexp": {
"pattern": "^[A-Z0-9-]+$"
}
}
},
{
"id": "price",
"name": "Price",
"type": "Number",
"required": true,
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "comparePrice",
"name": "Compare Price",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "currency",
"name": "Currency",
"type": "Symbol",
"required": true,
"defaultValue": "USD"
},
{
"id": "images",
"name": "Images",
"type": "Array",
"items": {
"type": "Link",
"validations": {
"linkContentType": ["media"]
}
}
},
{
"id": "variants",
"name": "Variants",
"type": "Array",
"items": {
"type": "Link",
"validations": {
"linkContentType": ["productVariant"]
}
}
},
{
"id": "category",
"name": "Category",
"type": "Link",
"validations": {
"linkContentType": ["productCategory"]
}
},
{
"id": "brand",
"name": "Brand",
"type": "Link",
"validations": {
"linkContentType": ["brand"]
}
},
{
"id": "attributes",
"name": "Attributes",
"type": "Array",
"items": {
"type": "Link",
"validations": {
"linkContentType": ["productAttribute"]
}
}
},
{
"id": "inventory",
"name": "Inventory",
"type": "Object",
"required": false,
"fields": [
{
"id": "quantity",
"name": "Quantity",
"type": "Number",
"required": true,
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "sku",
"name": "SKU",
"type": "Text",
"required": true
},
{
"id": "trackQuantity",
"name": "Track Quantity",
"type": "Boolean",
"defaultValue": true
},
{
"id": "allowBackorder",
"name": "Allow Backorder",
"type": "Boolean",
"defaultValue": false
}
]
},
{
"id": "shipping",
"name": "Shipping",
"type": "Object",
"required": false,
"fields": [
{
"id": "weight",
"name": "Weight",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "length",
"name": "Length",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "width",
"name": "Width",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "height",
"name": "Height",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
}
]
},
{
"id": "seo",
"name": "SEO",
"type": "Link",
"validations": {
"linkContentType": ["seo"]
}
},
{
"id": "isActive",
"name": "Is Active",
"type": "Boolean",
"defaultValue": true
}
]
}
// File: content-types/author.json
{
"name": "Author",
"description": "Author information and bio",
"displayField": "name",
"fields": [
{
"id": "name",
"name": "Name",
"type": "Text",
"required": true,
"localized": true
},
{
"id": "email",
"name": "Email",
"type": "Text",
"required": true,
"validations": {
"unique": true,
"email": true
}
},
{
"id": "bio",
"name": "Bio",
"type": "RichText",
"localized": true
},
{
"id": "avatar",
"name": "Avatar",
"type": "Link",
"validations": {
"linkContentType": ["media"]
}
},
{
"id": "social",
"name": "Social Media",
"type": "Object",
"fields": [
{
"id": "twitter",
"name": "Twitter",
"type": "Text"
},
{
"id": "github",
"name": "GitHub",
"type": "Text"
},
{
"id": "linkedin",
"name": "LinkedIn",
"type": "Text"
},
{
"id": "website",
"name": "Website",
"type": "Text"
}
]
}
]
}
// File: content-types/product-variant.json
{
"name": "Product Variant",
"description": "Product variants with different options",
"displayField": "name",
"fields": [
{
"id": "name",
"name": "Name",
"type": "Text",
"required": true,
"localized": true
},
{
"id": "sku",
"name": "SKU",
"type": "Text",
"required": true,
"unique": true
},
{
"id": "price",
"name": "Price",
"type": "Number",
"required": true,
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "comparePrice",
"name": "Compare Price",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "weight",
"name": "Weight",
"type": "Number",
"validations": {
"range": {
"min": 0
}
}
},
{
"id": "inventory",
"name": "Inventory",
"type": "Number",
"defaultValue": 0
},
{
"id": "options",
"name": "Options",
"type": "Object",
"fields": [
{
"id": "color",
"name": "Color",
"type": "Text"
},
{
"id": "size",
"name": "Size",
"type": "Text"
},
{
"id": "material",
"name": "Material",
"type": "Text"
}
]
},
{
"id": "image",
"name": "Image",
"type": "Link",
"validations": {
"linkContentType": ["media"]
}
}
]
}
// File: content-types/seo.json
{
"name": "SEO",
"description": "SEO metadata for search engine optimization",
"fields": [
{
"id": "metaTitle",
"name": "Meta Title",
"type": "Text",
"localized": true,
"validations": {
"size": {
"max": 60
}
}
},
{
"id": "metaDescription",
"name": "Meta Description",
"type": "Text",
"localized": true,
"validations": {
"size": {
"max": 160
}
}
},
{
"id": "keywords",
"name": "Keywords",
"type": "Text"
},
{
"id": "canonicalUrl",
"name": "Canonical URL",
"type": "Text"
},
{
"id": "ogImage",
"name": "OG Image",
"type": "Link",
"validations": {
"linkContentType": ["media"]
}
},
{
"id": "structuredData",
"name": "Structured Data",
"type": "Object",
"fields": [
{
"id": "type",
"name": "Type",
"type": "Text",
"defaultValue": "Article"
},
{
"id": "data",
"name": "Data",
"type": "RichText"
}
]
}
]
}
// File: locales.json - Localization Configuration
{
"defaultLocale": "en-US",
"locales": [
{
"code": "en-US",
"name": "English (United States)",
"default": true,
"fallbackCode": null
},
{
"code": "es-ES",
"name": "Spanish (Spain)",
"default": false,
"fallbackCode": "en-US"
},
{
"code": "fr-FR",
"name": "French (France)",
"default": false,
"fallbackCode": "en-US"
},
{
"code": "de-DE",
"name": "German (Germany)",
"default": false,
"fallbackCode": "en-US"
},
{
"code": "it-IT",
"name": "Italian (Italy)",
"default": false,
"fallbackCode": "en-US"
},
{
"code": "pt-BR",
"name": "Portuguese (Brazil)",
"default": false,
"fallbackCode": "en-US"
},
{
"code": "zh-CN",
"name": "Chinese (Simplified)",
"default": false,
"fallbackCode": "en-US"
}
]
}
// File: environment-variables.json - Environment Configuration
[
{
"name": "CONTENTFUL_SPACE_ID",
"description": "Contentful Space ID"
},
{
"name": "CONTENTFUL_ACCESS_TOKEN",
"description": "Contentful Delivery API Access Token"
},
{
"name": "CONTENTFUL_MANAGEMENT_TOKEN",
"description": "Contentful Management API Access Token"
},
{
"name": "CONTENTFUL_PREVIEW_ACCESS_TOKEN",
"description": "Contentful Preview API Access Token"
},
{
"name": "CONTENTFUL_ENVIRONMENT",
"description": "Contentful Environment (master, staging, development)",
"defaultValue": "master"
},
{
"name": "CONTENTFUL_HOST",
"description": "Contentful API Host",
"defaultValue": "cdn.contentful.com"
},
{
"name": "CONTENTFUL_WEBHOOK_SECRET",
"description": "Contentful Webhook Secret"
}
]
// File: webhooks.json - Webhook Configuration
{
"name": "Content Webhooks",
"description": "Webhooks for content changes",
"url": "https://your-app.com/api/webhooks/contentful",
"topics": [
"Entry.create",
"Entry.save",
"Entry.delete",
"Entry.publish",
"Entry.unpublish",
"Asset.create",
"Asset.save",
"Asset.delete",
"Asset.publish",
"Asset.unpublish",
"ContentType.create",
"ContentType.save",
"ContentType.delete"
],
"headers": {
"Content-Type": "application/json",
"X-Webhook-Secret": "{{webhook_secret}}"
},
"filters": [
{
"field": "contentType.sys.id",
"in": ["blogPost", "product", "author"]
}
],
"transformation": {
"contentType": {
"type": "lookup",
"lookup": {
"blogPost": "Blog Post",
"product": "Product",
"author": "Author"
}
}
}
}
💻 Integración de API Contentful javascript
🔴 complex
⭐⭐⭐⭐
Ejemplos completos de integración de API incluyendo consultas REST y GraphQL, webhooks e implementación frontend
⏱️ 40 min
🏷️ contentful, api, integration, frontend
Prerequisites:
Contentful basics, REST/GraphQL APIs, React hooks, JavaScript ES6+, Headless CMS concepts
// Contentful API Integration Examples
// File: lib/contentful.js - API Client Setup
import { createClient } from 'contentful';
import { createClient as createManagementClient } from 'contentful-management';
// Delivery API Client
export const contentfulClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
host: process.env.CONTENTFUL_HOST || 'cdn.contentful.com',
adapter: 'fetch',
});
// Preview API Client
export const contentfulPreviewClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
host: 'preview.contentful.com',
adapter: 'fetch',
});
// Management API Client
export const contentfulManagementClient = createManagementClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});
// File: lib/contentful-queries.js - Predefined Queries
export const contentfulQueries = {
// Blog posts with population
getBlogPosts: (limit = 10, skip = 0, locale = 'en-US') => ({
content_type: 'blogPost',
order: '-fields.publishedAt',
limit,
skip,
locale,
include: 10,
}),
getBlogPostBySlug: (slug, locale = 'en-US') => ({
content_type: 'blogPost',
'fields.slug': slug,
locale,
include: 10,
limit: 1,
}),
getRelatedBlogPosts: (categoryId, limit = 3, currentPostId, locale = 'en-US') => ({
content_type: 'blogPost',
'fields.category.sys.id': categoryId,
'sys.id[ne]': currentPostId,
order: '-fields.publishedAt',
limit,
locale,
include: 2,
}),
getFeaturedBlogPosts: (limit = 5, locale = 'en-US') => ({
content_type: 'blogPost',
'fields.isFeatured': true,
order: '-fields.publishedAt',
limit,
locale,
include: 5,
}),
getBlogPostsByCategory: (categoryId, limit = 10, locale = 'en-US') => ({
content_type: 'blogPost',
'fields.category.sys.id': categoryId,
order: '-fields.publishedAt',
limit,
locale,
include: 5,
}),
getBlogPostsByTag: (tagId, limit = 10, locale = 'en-US') => ({
content_type: 'blogPost',
'fields.tags.sys.id': tagId,
order: '-fields.publishedAt',
limit,
locale,
include: 5,
}),
// Products with population
getProducts: (limit = 20, skip = 0, locale = 'en-US') => ({
content_type: 'product',
order: 'fields.name',
limit,
skip,
locale,
include: 10,
}),
getProductBySlug: (slug, locale = 'en-US') => ({
content_type: 'product',
'fields.slug': slug,
locale,
include: 10,
limit: 1,
}),
getProductsByCategory: (categoryId, limit = 20, locale = 'en-US') => ({
content_type: 'product',
'fields.category.sys.id': categoryId,
order: 'fields.name',
limit,
locale,
include: 5,
}),
getProductsByBrand: (brandId, limit = 20, locale = 'en-US') => ({
content_type: 'product',
'fields.brand.sys.id': brandId,
order: 'fields.name',
limit,
locale,
include: 5,
}),
searchProducts: (searchTerm, locale = 'en-US') => ({
content_type: 'product',
query: searchTerm,
locale,
include: 5,
}),
// Authors
getAuthors: (locale = 'en-US') => ({
content_type: 'author',
order: 'fields.name',
locale,
include: 1,
}),
getAuthorByEmail: (email, locale = 'en-US') => ({
content_type: 'author',
'fields.email': email,
locale,
include: 1,
limit: 1,
}),
// Categories
getCategories: (locale = 'en-US') => ({
content_type: 'category',
order: 'fields.name',
locale,
}),
// Tags
getTags: (locale = 'en-US') => ({
content_type: 'tag',
order: 'fields.name',
locale,
}),
// Entries by content type
getEntriesByContentType: (contentType, options = {}) => ({
content_type: contentType,
...options,
}),
};
// File: hooks/useContentful.js - React Hooks
import { useState, useEffect, useCallback } from 'react';
import { contentfulClient, contentfulPreviewClient } from '../lib/contentful';
export function useContentfulQuery(query, dependencies = [], preview = false) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const client = preview ? contentfulPreviewClient : contentfulClient;
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const entries = await client.getEntries(query);
setData(entries);
} catch (err) {
setError(err);
console.error('Contentful query error:', err);
} finally {
setLoading(false);
}
}, [client, query]);
useEffect(() => {
fetchData();
}, [fetchData, ...dependencies]);
const refetch = useCallback(() => {
return fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
export function useContentfulEntry(id, query = {}, preview = false) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const client = preview ? contentfulPreviewClient : contentfulClient;
useEffect(() => {
const fetchEntry = async () => {
try {
setLoading(true);
setError(null);
const entry = await client.getEntry(id, query);
setData(entry);
} catch (err) {
setError(err);
console.error('Contentful entry error:', err);
} finally {
setLoading(false);
}
};
fetchEntry();
}, [id, query, client]);
return { data, loading, error };
}
export function useContentfulAssets(query = {}) {
const [assets, setAssets] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchAssets = async () => {
try {
setLoading(true);
setError(null);
const result = await contentfulClient.getAssets(query);
setAssets(result.items);
} catch (err) {
setError(err);
console.error('Contentful assets error:', err);
} finally {
setLoading(false);
}
};
fetchAssets();
}, [query]);
return { assets, loading, error };
}
// File: services/contentful-service.js - Service Layer
export class ContentfulService {
constructor(client = contentfulClient) {
this.client = client;
}
// Blog posts
async getBlogPosts(params = {}) {
const { limit = 10, skip = 0, locale = 'en-US', category, tag, featured } = params;
const query = {
content_type: 'blogPost',
order: '-fields.publishedAt',
limit,
skip,
locale,
include: 10,
};
if (category) {
query['fields.category.sys.id'] = category;
}
if (tag) {
query['fields.tags.sys.id'] = tag;
}
if (featured) {
query['fields.isFeatured'] = true;
}
const entries = await this.client.getEntries(query);
return {
posts: entries.items,
total: entries.total,
limit: entries.limit,
skip: entries.skip,
};
}
async getBlogPostBySlug(slug, locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
locale,
include: 10,
limit: 1,
});
return entries.items[0] || null;
}
async getRelatedBlogPosts(categoryId, currentPostId, limit = 3, locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'blogPost',
'fields.category.sys.id': categoryId,
'sys.id[ne]': currentPostId,
order: '-fields.publishedAt',
limit,
locale,
include: 2,
});
return entries.items;
}
// Products
async getProducts(params = {}) {
const { limit = 20, skip = 0, locale = 'en-US', category, brand, search } = params;
const query = {
content_type: 'product',
order: 'fields.name',
limit,
skip,
locale,
include: 10,
};
if (category) {
query['fields.category.sys.id'] = category;
}
if (brand) {
query['fields.brand.sys.id'] = brand;
}
if (search) {
query.query = search;
}
const entries = await this.client.getEntries(query);
return {
products: entries.items,
total: entries.total,
limit: entries.limit,
skip: entries.skip,
};
}
async getProductBySlug(slug, locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'product',
'fields.slug': slug,
locale,
include: 10,
limit: 1,
});
return entries.items[0] || null;
}
// Categories
async getCategories(locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'category',
order: 'fields.name',
locale,
});
return entries.items;
}
// Tags
async getTags(locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'tag',
order: 'fields.name',
locale,
});
return entries.items;
}
// Authors
async getAuthors(locale = 'en-US') {
const entries = await this.client.getEntries({
content_type: 'author',
order: 'fields.name',
locale,
include: 1,
});
return entries.items;
}
// Assets
async getAssets(params = {}) {
const { limit = 100, skip = 0, contentType } = params;
const query = {
limit,
skip,
include: 1,
};
if (contentType) {
query['mimetype_group'] = contentType; // 'image', 'video', 'audio', 'text'
}
const assets = await this.client.getAssets(query);
return {
assets: assets.items,
total: assets.total,
limit: assets.limit,
skip: assets.skip,
};
}
// Search
async searchAll(query, options = {}) {
const { limit = 50, locale = 'en-US' } = options;
const searchQuery = {
query,
limit,
locale,
include: 5,
};
const result = await this.client.getEntries(searchQuery);
return {
items: result.items,
total: result.total,
};
}
}
// File: components/BlogPostCard.js - Frontend Component
import React from 'react';
import { documentToReactComponents } from '@contentful/rich-text-react-renderer';
import { contentfulClient } from '../lib/contentful';
const BlogPostCard = ({ post, className = '' }) => {
if (!post) return null;
const {
title,
slug,
excerpt,
featuredImage,
author,
category,
publishedAt,
tags,
readingTime,
} = post.fields;
const imageUrl = featuredImage?.fields?.file?.url;
const authorName = author?.fields?.name;
const categoryName = category?.fields?.name;
return (
<article className={`blog-post-card ${className}`}>
{imageUrl && (
<div className="blog-post-card__image">
<img
src={imageUrl}
alt={featuredImage.fields.title || title}
loading="lazy"
/>
</div>
)}
<div className="blog-post-card__content">
{categoryName && (
<span className="blog-post-card__category">
{categoryName}
</span>
)}
<h3 className="blog-post-card__title">
<a href={`/blog/${slug}`}>
{title}
</a>
</h3>
{excerpt && (
<p className="blog-post-card__excerpt">
{excerpt}
</p>
)}
<div className="blog-post-card__meta">
{authorName && (
<span className="blog-post-card__author">
By {authorName}
</span>
)}
{publishedAt && (
<time className="blog-post-card__date">
{new Date(publishedAt).toLocaleDateString()}
</time>
)}
{readingTime && (
<span className="blog-post-card__reading-time">
{readingTime} min read
</span>
)}
</div>
{tags && tags.length > 0 && (
<div className="blog-post-card__tags">
{tags.map(tag => (
<span key={tag.sys.id} className="blog-post-card__tag">
{tag.fields.name}
</span>
))}
</div>
)}
</div>
</article>
);
};
export default BlogPostCard;
// File: lib/contentful-graphql.js - GraphQL Client
import { createClient } from 'contentful';
export const contentfulGraphQLClient = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENVIRONMENT || 'master',
});
// GraphQL Queries
export const GET_BLOG_POSTS = `
query GetBlogPosts(
$limit: Int = 10
$skip: Int = 0
$locale: String = "en-US"
$order: [BlogPostOrder!] = "publishedAt_DESC"
) {
blogPostCollection(
limit: $limit
skip: $skip
locale: $locale
order: $order
preview: false
) {
items {
sys {
id
}
title
slug
excerpt
publishedAt
readingTime
isFeatured
author {
... on Author {
name
bio
avatar {
url
title
description
width
height
}
}
}
category {
... on Category {
name
slug
description
}
}
tags {
... on Tag {
name
slug
}
}
featuredImage {
... on Asset {
url
title
description
width
height
}
}
seo {
... on SEO {
metaTitle
metaDescription
keywords
}
}
}
total
}
}
`;
export const GET_BLOG_POST_BY_SLUG = `
query GetBlogPostBySlug(
$slug: String!
$locale: String = "en-US"
$preview: Boolean = false
) {
blogPostCollection(
limit: 1
where: { slug: $slug }
locale: $locale
preview: $preview
) {
items {
sys {
id
publishedAt
}
title
slug
content {
json
links {
asset {
block {
sys {
id
}
}
hyperlink {
sys {
id
}
}
}
entry {
block {
sys {
id
}
}
inline {
sys {
id
}
}
}
}
}
publishedAt
readingTime
author {
... on Author {
name
bio
avatar {
url
title
description
width
height
}
social {
twitter
github
linkedin
website
}
}
}
category {
... on Category {
name
slug
description
}
}
tags {
... on Tag {
name
slug
}
}
featuredImage {
... on Asset {
url
title
description
width
height
}
}
seo {
... on SEO {
metaTitle
metaDescription
keywords
ogImage {
url
title
description
width
height
}
structuredData {
json
}
}
}
}
}
}
`;
export const GET_PRODUCTS = `
query GetProducts(
$limit: Int = 20
$skip: Int = 0
$locale: String = "en-US"
$order: [ProductOrder!] = "name_ASC"
$where: ProductFilter = {}
) {
productCollection(
limit: $limit
skip: $skip
locale: $locale
order: $order
where: $where
preview: false
) {
items {
sys {
id
}
name
slug
shortDescription
sku
price
comparePrice
currency
isActive
category {
... on ProductCategory {
name
slug
}
}
brand {
... on Brand {
name
slug
logo {
url
title
description
width
height
}
}
}
images {
... on Asset {
url
title
description
width
height
}
}
}
total
}
}
`;
export async function fetchGraphQL(query, variables = {}) {
try {
const response = await contentfulGraphQLClient.graphqlRequest(query, variables);
return response;
} catch (error) {
console.error('GraphQL query error:', error);
throw error;
}
}
// Usage example:
// const posts = await fetchGraphQL(GET_BLOG_POSTS, { limit: 5, locale: 'es-ES' });
💻 Migraciones y Webhooks de Contentful javascript
🔴 complex
⭐⭐⭐⭐⭐
Scripts completos de migración de contenido, manejo de webhooks y flujos de trabajo de despliegue automatizado
⏱️ 50 min
🏷️ contentful, migrations, webhooks, deployment
Prerequisites:
Contentful basics, Migration concepts, Webhook concepts, Node.js, Deployment
// Contentful Migrations & Webhooks Examples
// File: scripts/migrate-content.js
import { createClient } from 'contentful-management';
const managementClient = createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});
class ContentfulMigrator {
constructor() {
this.client = managementClient;
}
async migrate() {
try {
console.log('Starting Contentful migration...');
// Create content types
await this.createContentTypes();
// Create entries
await this.createEntries();
// Create assets
await this.createAssets();
console.log('Migration completed successfully!');
} catch (error) {
console.error('Migration failed:', error);
throw error;
}
}
async createContentTypes() {
console.log('Creating content types...');
// Blog Post Content Type
const blogPostContentType = await this.client.contentType.create({
name: 'Blog Post',
description: 'Blog posts with rich content',
displayField: 'title',
});
await blogPostContentType.fields.create({
id: 'title',
name: 'Title',
type: 'Text',
required: true,
localized: true,
});
await blogPostContentType.fields.create({
id: 'slug',
name: 'Slug',
type: 'Text',
required: true,
unique: true,
});
await blogPostContentType.fields.create({
id: 'content',
name: 'Content',
type: 'RichText',
required: true,
localized: true,
});
await blogPostContentType.fields.create({
id: 'author',
name: 'Author',
type: 'Link',
linkType: 'Entry',
required: true,
validations: [
{
linkContentType: ['author'],
},
],
});
console.log('Created blog post content type');
}
async createEntries() {
console.log('Creating entries...');
// Get existing content types
const authorContentType = await this.client.contentType.get('author');
if (!authorContentType) {
throw new Error('Author content type not found');
}
// Create author entry
const authorEntry = await this.client.entry.create({
contentTypeId: authorContentType.sys.id,
fields: {
name: {
'en-US': 'John Doe',
},
email: '[email protected]',
bio: {
'en-US': 'Software developer and tech blogger.',
},
},
});
// Publish author
await authorEntry.publish();
console.log('Created author entry:', authorEntry.sys.id);
}
async createAssets() {
console.log('Creating assets...');
// Create example image asset
const asset = await this.client.asset.createFromFile({
file: {
name: 'example-image.jpg',
mimeType: 'image/jpeg',
url: 'https://example.com/image.jpg',
},
});
// Process the asset
await asset.processForLocale({
locale: 'en-US',
processingOptions: {
format: 'webp',
width: 1200,
height: 800,
},
});
await asset.publish();
console.log('Created asset:', asset.sys.id);
}
}
// File: scripts/import-from-wordpress.js
import fs from 'fs';
import path from 'path';
import { createClient } from 'contentful-management';
class WordPressImporter {
constructor() {
this.client = createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});
this.importedAuthors = new Map();
this.importedCategories = new Map();
this.importedTags = new Map();
}
async import(exportPath) {
try {
console.log('Starting WordPress import...');
// Load WordPress export
const exportData = JSON.parse(fs.readFileSync(exportPath, 'utf8'));
// Import authors
await this.importAuthors(exportData.users);
// Import categories
await this.importCategories(exportData.categories);
// Import tags
await this.importTags(exportData.tags);
// Import posts
await this.importPosts(exportData.posts);
console.log('WordPress import completed!');
} catch (error) {
console.error('WordPress import failed:', error);
throw error;
}
}
async importAuthors(users) {
console.log(`Importing ${users.length} authors...`);
const authorContentType = await this.client.contentType.get('author');
for (const user of users) {
try {
const authorEntry = await this.client.entry.create({
contentTypeId: authorContentType.sys.id,
fields: {
name: {
'en-US': user.name,
},
email: user.email,
bio: {
'en-US': user.description || '',
},
social: {
'en-US': {
website: user.url || '',
twitter: '',
github: '',
},
},
},
});
await authorEntry.publish();
this.importedAuthors.set(user.id, authorEntry.sys.id);
console.log(`Imported author: ${user.name}`);
} catch (error) {
console.error(`Failed to import author ${user.name}:`, error);
}
}
}
async importCategories(categories) {
console.log(`Importing ${categories.length} categories...`);
const categoryContentType = await this.client.contentType.get('category');
for (const category of categories) {
try {
const categoryEntry = await this.client.entry.create({
contentTypeId: categoryContentType.sys.id,
fields: {
name: {
'en-US': category.name,
},
slug: {
'en-US': category.slug,
},
description: {
'en-US': category.description || '',
},
},
});
await categoryEntry.publish();
this.importedCategories.set(category.id, categoryEntry.sys.id);
console.log(`Imported category: ${category.name}`);
} catch (error) {
console.error(`Failed to import category ${category.name}:`, error);
}
}
}
async importTags(tags) {
console.log(`Importing ${tags.length} tags...`);
const tagContentType = await this.client.contentType.get('tag');
for (const tag of tags) {
try {
const tagEntry = await this.client.entry.create({
contentTypeId: tagContentType.sys.id,
fields: {
name: {
'en-US': tag.name,
},
slug: {
'en-US': tag.slug,
},
},
});
await tagEntry.publish();
this.importedTags.set(tag.id, tagEntry.sys.id);
console.log(`Imported tag: ${tag.name}`);
} catch (error) {
console.error(`Failed to import tag ${tag.name}:`, error);
}
}
}
async importPosts(posts) {
console.log(`Importing ${posts.length} posts...`);
const blogPostContentType = await this.client.contentType.get('blogPost');
for (const post of posts) {
try {
// Map WordPress author to Contentful author
const authorId = this.importedAuthors.get(post.author);
// Map WordPress categories to Contentful categories
const categoryIds = post.categories
.map(catId => this.importedCategories.get(catId))
.filter(Boolean);
// Map WordPress tags to Contentful tags
const tagIds = post.tags
.map(tagId => this.importedTags.get(tagId))
.filter(Boolean);
const postEntry = await this.client.entry.create({
contentTypeId: blogPostContentType.sys.id,
fields: {
title: {
'en-US': post.title.rendered,
},
slug: {
'en-US': post.slug,
},
content: {
'en-US': this.convertHtmlToRichText(post.content.rendered),
},
excerpt: {
'en-US': post.excerpt.rendered,
},
author: {
'en-US': authorId ? { sys: { type: 'Link', linkType: 'Entry', id: authorId } } : null,
},
publishedAt: post.date_gmt,
isFeatured: post.sticky === true,
},
});
// Add categories and tags after entry creation
if (categoryIds.length > 0) {
await postEntry.patch({
fields: {
category: {
'en-US': {
sys: {
type: 'Link',
linkType: 'Entry',
id: categoryIds[0],
},
},
},
},
});
}
if (tagIds.length > 0) {
await postEntry.patch({
fields: {
tags: {
'en-US': tagIds.map(id => ({
sys: {
type: 'Link',
linkType: 'Entry',
id,
},
})),
},
},
});
}
await postEntry.publish();
console.log(`Imported post: ${post.title.rendered}`);
} catch (error) {
console.error(`Failed to import post ${post.title.rendered}:`, error);
}
}
}
convertHtmlToRichText(html) {
// Simple HTML to Rich Text conversion
// In a real implementation, you'd use a proper HTML parser
return {
nodeType: 'document',
data: {},
content: [
{
nodeType: 'paragraph',
data: {},
content: [
{
nodeType: 'text',
value: html.replace(/<[^>]*>/g, ''),
marks: [],
},
],
},
],
};
}
}
// File: api/webhooks/contentful.js - Webhook Handler
import crypto from 'crypto';
class ContentfulWebhookHandler {
constructor(secret) {
this.secret = secret;
}
verifySignature(body, signature) {
const expectedSignature = crypto
.createHmac('sha256', this.secret)
.update(body)
.digest('hex');
return signature === expectedSignature;
}
async handleWebhook(req, res) {
try {
const signature = req.headers['x-contentful-webhook-secret'];
const body = JSON.stringify(req.body);
if (!this.verifySignature(body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.body;
console.log(`Received webhook event: ${event.sys.type}`);
switch (event.sys.type) {
case 'Entry.create':
await this.handleEntryCreate(event);
break;
case 'Entry.save':
await this.handleEntryUpdate(event);
break;
case 'Entry.delete':
await this.handleEntryDelete(event);
break;
case 'Entry.publish':
await this.handleEntryPublish(event);
break;
case 'Entry.unpublish':
await this.handleEntryUnpublish(event);
break;
default:
console.log(`Unhandled event type: ${event.sys.type}`);
}
res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
async handleEntryCreate(event) {
const { contentType, fields } = event;
console.log(`New entry created in ${contentType.sys.id}`);
// Trigger revalidation for Next.js
if (contentType.sys.id === 'blogPost') {
await this.revalidateBlogPosts();
}
// Send notification
await this.sendNotification('content_created', {
contentType: contentType.sys.id,
entryId: event.sys.id,
});
}
async handleEntryUpdate(event) {
const { contentType, fields } = event;
console.log(`Entry updated in ${contentType.sys.id}`);
// Trigger revalidation
if (contentType.sys.id === 'blogPost') {
await this.revalidateBlogPost(fields.slug);
}
}
async handleEntryDelete(event) {
const { contentType } = event;
console.log(`Entry deleted from ${contentType.sys.id}`);
// Send notification
await this.sendNotification('content_deleted', {
contentType: contentType.sys.id,
entryId: event.sys.id,
});
}
async handleEntryPublish(event) {
const { contentType, fields } = event;
console.log(`Entry published in ${contentType.sys.id}`);
// Trigger revalidation
if (contentType.sys.id === 'blogPost') {
await this.revalidateBlogPost(fields.slug);
await this.revalidateBlogPosts();
}
}
async handleEntryUnpublish(event) {
const { contentType, fields } = event;
console.log(`Entry unpublished from ${contentType.sys.id}`);
// Trigger revalidation
if (contentType.sys.id === 'blogPost') {
await this.revalidateBlogPost(fields.slug);
await this.revalidateBlogPosts();
}
}
async revalidateBlogPost(slug) {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'blog-post',
slug,
}),
});
if (!res.ok) {
console.error(`Revalidation failed for blog post: ${slug}`);
return;
}
console.log(`Revalidated blog post: ${slug}`);
} catch (error) {
console.error(`Error revalidating blog post ${slug}:`, error);
}
}
async revalidateBlogPosts() {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/revalidate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'blog-posts',
}),
});
if (!res.ok) {
console.error('Revalidation failed for blog posts');
return;
}
console.log('Revalidated blog posts');
} catch (error) {
console.error('Error revalidating blog posts:', error);
}
}
async sendNotification(type, data) {
try {
// Send to Slack
await this.sendSlackNotification(type, data);
// Send email
await this.sendEmailNotification(type, data);
} catch (error) {
console.error('Error sending notification:', error);
}
}
async sendSlackNotification(type, data) {
if (!process.env.SLACK_WEBHOOK_URL) {
return;
}
const message = this.formatSlackMessage(type, data);
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: message }),
});
}
async sendEmailNotification(type, data) {
// Implementation depends on your email service
console.log(`Email notification: ${type}`, data);
}
formatSlackMessage(type, data) {
const emojis = {
'content_created': '✨',
'content_updated': '✏️',
'content_deleted': '🗑️',
};
return `${emojis[type]} Contentful ${type.replace('_', ' ')}
` +
`Content Type: ${data.contentType}
` +
`Entry ID: ${data.entryId}`;
}
}
// File: scripts/deploy-contentful.js
import { createClient } from 'contentful-management';
import { Client as AppClient } from '@contentful/app-sdk';
class ContentfulDeployer {
constructor() {
this.managementClient = createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});
this.appClient = new AppClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
});
}
async deploy() {
try {
console.log('Starting Contentful deployment...');
// Validate configuration
await this.validateConfiguration();
// Deploy content types
await this.deployContentTypes();
// Deploy entries
await this.deployEntries();
// Deploy assets
await this.deployAssets();
// Run post-deployment checks
await this.runPostDeploymentChecks();
console.log('Contentful deployment completed successfully!');
} catch (error) {
console.error('Contentful deployment failed:', error);
throw error;
}
}
async validateConfiguration() {
console.log('Validating Contentful configuration...');
const space = await this.managementClient.getSpace();
if (!space) {
throw new Error('Contentful space not found');
}
console.log(`Connected to space: ${space.name}`);
}
async deployContentTypes() {
console.log('Deploying content types...');
// Load content type definitions from files
const contentTypesDir = './content-types';
const contentTypes = fs.readdirSync(contentTypesDir);
for (const file of contentTypes) {
if (file.endsWith('.json')) {
const contentTypeDef = JSON.parse(
fs.readFileSync(path.join(contentTypesDir, file), 'utf8')
);
await this.deployContentType(contentTypeDef);
}
}
}
async deployContentType(contentTypeDef) {
try {
console.log(`Deploying content type: ${contentTypeDef.name}`);
// Check if content type already exists
let contentType;
try {
contentType = await this.managementClient.contentType.get(contentTypeDef.name.toLowerCase());
} catch (error) {
// Content type doesn't exist, create it
contentType = await this.managementClient.contentType.create({
name: contentTypeDef.name,
description: contentTypeDef.description,
displayField: contentTypeDef.displayField,
});
}
// Update fields
for (const fieldDef of contentTypeDef.fields) {
try {
await contentType.fields.create(fieldDef);
} catch (error) {
// Field might already exist, try to update it
console.log(`Field ${fieldDef.id} might already exist, skipping...`);
}
}
console.log(`Deployed content type: ${contentTypeDef.name}`);
} catch (error) {
console.error(`Failed to deploy content type ${contentTypeDef.name}:`, error);
}
}
async deployEntries() {
console.log('Deploying entries...');
// Load entries from data files
const entriesDir = './data/entries';
const entryDirs = fs.readdirSync(entriesDir);
for (const dir of entryDirs) {
const entriesPath = path.join(entriesDir, dir);
if (fs.statSync(entriesPath).isDirectory()) {
await this.deployEntriesForContentType(dir, entriesPath);
}
}
}
async deployEntriesForContentType(contentTypeName, entriesPath) {
try {
const contentType = await this.managementClient.contentType.get(contentTypeName);
const entryFiles = fs.readdirSync(entriesPath);
const entries = [];
for (const file of entryFiles) {
if (file.endsWith('.json')) {
const entryData = JSON.parse(
fs.readFileSync(path.join(entriesPath, file), 'utf8')
);
const entry = await this.managementClient.entry.create({
contentTypeId: contentType.sys.id,
fields: entryData.fields,
});
await entry.publish();
entries.push(entry);
}
}
console.log(`Deployed ${entries.length} entries for ${contentTypeName}`);
} catch (error) {
console.error(`Failed to deploy entries for ${contentTypeName}:`, error);
}
}
async deployAssets() {
console.log('Deploying assets...');
const assetsDir = './data/assets';
const assetFiles = fs.readdirSync(assetsDir);
for (const file of assetFiles) {
const assetPath = path.join(assetsDir, file);
const stats = fs.statSync(assetPath);
if (stats.isFile() && this.isImageFile(file)) {
await this.deployAsset(assetPath, file);
}
}
}
async deployAsset(assetPath, fileName) {
try {
console.log(`Deploying asset: ${fileName}`);
const fileBuffer = fs.readFileSync(assetPath);
const mimeType = this.getMimeType(fileName);
const asset = await this.managementClient.asset.createFromFile({
file: {
name: fileName,
mimeType,
data: fileBuffer,
},
});
// Process for web
await asset.processForLocale({
locale: 'en-US',
processingOptions: {
format: 'webp',
quality: 85,
},
});
await asset.publish();
console.log(`Deployed asset: ${fileName}`);
} catch (error) {
console.error(`Failed to deploy asset ${fileName}:`, error);
}
}
async runPostDeploymentChecks() {
console.log('Running post-deployment checks...');
// Check if required content types exist
const requiredContentTypes = ['blogPost', 'product', 'author', 'category', 'tag'];
for (const contentTypeName of requiredContentTypes) {
try {
await this.managementClient.contentType.get(contentTypeName);
console.log(`✓ Content type ${contentTypeName} exists`);
} catch (error) {
console.error(`✗ Content type ${contentTypeName} not found`);
throw new Error(`Missing required content type: ${contentTypeName}`);
}
}
// Check if entries exist
const blogPostContentType = await this.managementClient.contentType.get('blogPost');
const posts = await this.managementClient.getEntries({
content_type: blogPostContentType.sys.id,
});
console.log(`Found ${posts.total} blog posts`);
if (posts.total === 0) {
console.warn('Warning: No blog posts found');
}
}
isImageFile(filename) {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
return imageExtensions.includes(path.extname(filename).toLowerCase());
}
getMimeType(filename) {
const ext = path.extname(filename).toLowerCase();
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.bmp': 'image/bmp',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
};
return mimeTypes[ext] || 'application/octet-stream';
}
}
// Usage examples
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'migrate':
const migrator = new ContentfulMigrator();
migrator.migrate().catch(console.error);
break;
case 'import-wordpress':
const importer = new WordPressImporter();
importer.import('./wordpress-export.json').catch(console.error);
break;
case 'deploy':
const deployer = new ContentfulDeployer();
deployer.deploy().catch(console.error);
break;
default:
console.log('Available commands:');
console.log(' migrate - Run migration');
console.log(' import-wordpress - Import from WordPress');
console.log(' deploy - Deploy content');
}
}