🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Примеры Contentful Облачного CMS
Комплексные примеры Contentful, охватывающие моделирование контента, интеграцию API, вебхуки и паттерны фронтенд-интеграции
💻 Моделирование контента в Contentful javascript
🟡 intermediate
⭐⭐⭐
Полные определения контент-типов, конфигурации полей и моделирование отношений для различных вариантов использования
⏱️ 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"
}
}
}
}
💻 Интеграция API Contentful javascript
🔴 complex
⭐⭐⭐⭐
Полные примеры интеграции API, включая запросы REST и GraphQL, вебхуки и фронтенд-реализацию
⏱️ 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' });
💻 Миграции и вебхуки Contentful javascript
🔴 complex
⭐⭐⭐⭐⭐
Полные скрипты миграции контента, обработка вебхуков и автоматизированные рабочие процессы развертывания
⏱️ 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');
}
}