Claude-skill-registry Headless CMS Integration
Separating content management from presentation by providing content via APIs, enabling omnichannel delivery and developer flexibility with platforms like Contentful, Strapi, and Sanity.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/headless-cms" ~/.claude/skills/majiayu000-claude-skill-registry-headless-cms-integration && rm -rf "$T"
manifest:
skills/data/headless-cms/SKILL.mdsource content
Headless CMS Integration
Current Level: Intermediate
Domain: Content Management / Backend
Overview
Headless CMS separates content management from presentation, providing content via APIs. This guide covers integration patterns, popular platforms, and best practices for building content-driven applications with flexibility and scalability.
Core Concepts
Headless CMS Concepts
Traditional CMS: Content → Template → HTML Headless CMS: Content → API → Any Frontend
Benefits:
- Platform agnostic
- Omnichannel delivery
- Better performance
- Developer flexibility
- Scalability
Popular Headless CMS Comparison
| CMS | Type | API | Hosting | Pricing |
|---|---|---|---|---|
| Contentful | SaaS | REST, GraphQL | Cloud | Free tier, paid plans |
| Strapi | Self-hosted | REST, GraphQL | Self/Cloud | Open source, enterprise |
| Sanity | SaaS | GROQ, GraphQL | Cloud | Free tier, paid plans |
| Prismic | SaaS | REST, GraphQL | Cloud | Free tier, paid plans |
| Directus | Self-hosted | REST, GraphQL | Self/Cloud | Open source |
Content Modeling
// Example content model interface BlogPost { id: string; title: string; slug: string; content: RichText; excerpt: string; author: Reference<Author>; categories: Reference<Category>[]; featuredImage: Asset; publishedAt: Date; metadata: SEOMetadata; } interface Author { id: string; name: string; bio: string; avatar: Asset; socialLinks: SocialLink[]; } interface Category { id: string; name: string; slug: string; description: string; } interface SEOMetadata { title: string; description: string; keywords: string[]; ogImage: Asset; } interface Asset { id: string; url: string; title: string; description: string; width: number; height: number; contentType: string; }
API Integration
REST API
// services/cms-client.service.ts import axios, { AxiosInstance } from 'axios'; export class CMSClient { private client: AxiosInstance; constructor(baseURL: string, apiKey: string) { this.client = axios.create({ baseURL, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); } async getEntries<T>(contentType: string, query?: QueryParams): Promise<T[]> { const response = await this.client.get('/entries', { params: { content_type: contentType, ...query } }); return response.data.items; } async getEntry<T>(id: string): Promise<T> { const response = await this.client.get(`/entries/${id}`); return response.data; } async getAsset(id: string): Promise<Asset> { const response = await this.client.get(`/assets/${id}`); return response.data; } } interface QueryParams { limit?: number; skip?: number; order?: string; locale?: string; include?: number; [key: string]: any; }
GraphQL API
// services/cms-graphql.service.ts import { GraphQLClient } from 'graphql-request'; export class CMSGraphQLClient { private client: GraphQLClient; constructor(endpoint: string, apiKey: string) { this.client = new GraphQLClient(endpoint, { headers: { 'Authorization': `Bearer ${apiKey}` } }); } async getBlogPosts(limit: number = 10): Promise<BlogPost[]> { const query = ` query GetBlogPosts($limit: Int!) { blogPostCollection(limit: $limit, order: publishedAt_DESC) { items { sys { id } title slug excerpt publishedAt author { name avatar { url } } featuredImage { url width height } categoriesCollection { items { name slug } } } } } `; const data = await this.client.request(query, { limit }); return data.blogPostCollection.items; } async getBlogPost(slug: string): Promise<BlogPost> { const query = ` query GetBlogPost($slug: String!) { blogPostCollection(where: { slug: $slug }, limit: 1) { items { sys { id } title slug content { json } excerpt publishedAt author { name bio avatar { url } } featuredImage { url width height } } } } `; const data = await this.client.request(query, { slug }); return data.blogPostCollection.items[0]; } }
Content Preview
// lib/preview.ts export class ContentPreview { async enablePreview(req: any, res: any): Promise<void> { // Check secret if (req.query.secret !== process.env.PREVIEW_SECRET) { return res.status(401).json({ message: 'Invalid token' }); } // Enable preview mode res.setPreviewData({}); // Redirect to the path res.redirect(req.query.slug || '/'); } async disablePreview(req: any, res: any): Promise<void> { res.clearPreviewData(); res.redirect('/'); } async getPreviewContent(id: string, preview: boolean): Promise<any> { const client = new CMSClient( process.env.CMS_API_URL!, preview ? process.env.CMS_PREVIEW_KEY! : process.env.CMS_API_KEY! ); return client.getEntry(id); } } // pages/api/preview.ts export default async function handler(req: any, res: any) { const preview = new ContentPreview(); await preview.enablePreview(req, res); } // pages/api/exit-preview.ts export default async function handler(req: any, res: any) { const preview = new ContentPreview(); await preview.disablePreview(req, res); }
Webhooks
// pages/api/webhooks/cms.ts import crypto from 'crypto'; export default async function handler(req: any, res: any) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } // Verify webhook signature if (!verifyWebhookSignature(req)) { return res.status(401).json({ message: 'Invalid signature' }); } const event = req.body; switch (event.type) { case 'Entry.publish': await handleEntryPublished(event); break; case 'Entry.unpublish': await handleEntryUnpublished(event); break; case 'Entry.delete': await handleEntryDeleted(event); break; case 'Asset.publish': await handleAssetPublished(event); break; } res.json({ received: true }); } function verifyWebhookSignature(req: any): boolean { const signature = req.headers['x-webhook-signature']; const secret = process.env.WEBHOOK_SECRET!; const hash = crypto .createHmac('sha256', secret) .update(JSON.stringify(req.body)) .digest('hex'); return hash === signature; } async function handleEntryPublished(event: any): Promise<void> { const { entryId, contentType } = event; // Revalidate pages await fetch(`${process.env.APP_URL}/api/revalidate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contentType, entryId }) }); } async function handleEntryUnpublished(event: any): Promise<void> { // Implementation } async function handleEntryDeleted(event: any): Promise<void> { // Implementation } async function handleAssetPublished(event: any): Promise<void> { // Implementation }
Image Optimization
// components/OptimizedImage.tsx import Image from 'next/image'; interface OptimizedImageProps { src: string; alt: string; width: number; height: number; quality?: number; priority?: boolean; } export function OptimizedImage({ src, alt, width, height, quality = 75, priority = false }: OptimizedImageProps) { // Transform CMS image URL const optimizedSrc = transformImageUrl(src, { width, quality }); return ( <Image src={optimizedSrc} alt={alt} width={width} height={height} quality={quality} priority={priority} placeholder="blur" blurDataURL={generateBlurDataUrl(src)} /> ); } function transformImageUrl(url: string, options: ImageOptions): string { const params = new URLSearchParams({ w: options.width?.toString() || '', q: options.quality?.toString() || '75', fm: options.format || 'webp' }); return `${url}?${params}`; } function generateBlurDataUrl(url: string): string { // Generate low-quality placeholder return transformImageUrl(url, { width: 10, quality: 10 }); } interface ImageOptions { width?: number; height?: number; quality?: number; format?: 'webp' | 'jpg' | 'png'; }
Multi-language Content
// lib/i18n.ts export class I18nContent { async getLocalizedContent<T>( id: string, locale: string ): Promise<T> { const client = new CMSClient( process.env.CMS_API_URL!, process.env.CMS_API_KEY! ); return client.getEntry<T>(id, { locale }); } async getAllLocales(): Promise<string[]> { return ['en-US', 'th-TH', 'ja-JP']; } async getLocalizedPaths(contentType: string): Promise<LocalizedPath[]> { const locales = await this.getAllLocales(); const paths: LocalizedPath[] = []; for (const locale of locales) { const entries = await this.getEntries(contentType, { locale }); entries.forEach(entry => { paths.push({ params: { slug: entry.slug }, locale }); }); } return paths; } } interface LocalizedPath { params: { slug: string }; locale: string; } // pages/[slug].tsx export async function getStaticPaths() { const i18n = new I18nContent(); const paths = await i18n.getLocalizedPaths('blogPost'); return { paths, fallback: 'blocking' }; } export async function getStaticProps({ params, locale }: any) { const i18n = new I18nContent(); const post = await i18n.getLocalizedContent(params.slug, locale); return { props: { post }, revalidate: 60 }; }
Content Versioning
// lib/versioning.ts export class ContentVersioning { async getVersionHistory(entryId: string): Promise<Version[]> { const response = await fetch( `${process.env.CMS_API_URL}/entries/${entryId}/versions`, { headers: { 'Authorization': `Bearer ${process.env.CMS_API_KEY}` } } ); return response.json(); } async getVersion(entryId: string, versionId: string): Promise<any> { const response = await fetch( `${process.env.CMS_API_URL}/entries/${entryId}/versions/${versionId}`, { headers: { 'Authorization': `Bearer ${process.env.CMS_API_KEY}` } } ); return response.json(); } async compareVersions( entryId: string, version1: string, version2: string ): Promise<VersionDiff> { const [v1, v2] = await Promise.all([ this.getVersion(entryId, version1), this.getVersion(entryId, version2) ]); return this.diff(v1, v2); } private diff(v1: any, v2: any): VersionDiff { // Implementation return { added: [], removed: [], modified: [] }; } } interface Version { id: string; createdAt: Date; createdBy: string; changes: string; } interface VersionDiff { added: string[]; removed: string[]; modified: Array<{ field: string; old: any; new: any }>; }
Caching Strategies
// lib/cache.ts import { Redis } from 'ioredis'; export class CMSCache { private redis: Redis; constructor() { this.redis = new Redis(process.env.REDIS_URL!); } async getCachedContent<T>(key: string): Promise<T | null> { const cached = await this.redis.get(key); return cached ? JSON.parse(cached) : null; } async setCachedContent(key: string, data: any, ttl: number = 3600): Promise<void> { await this.redis.setex(key, ttl, JSON.stringify(data)); } async invalidateCache(pattern: string): Promise<void> { const keys = await this.redis.keys(pattern); if (keys.length > 0) { await this.redis.del(...keys); } } async getOrFetch<T>( key: string, fetcher: () => Promise<T>, ttl: number = 3600 ): Promise<T> { const cached = await this.getCachedContent<T>(key); if (cached) { return cached; } const data = await fetcher(); await this.setCachedContent(key, data, ttl); return data; } } // Usage const cache = new CMSCache(); export async function getBlogPost(slug: string): Promise<BlogPost> { return cache.getOrFetch( `blog:${slug}`, () => cmsClient.getBlogPost(slug), 3600 // 1 hour ); }
Next.js Integration
// lib/cms.ts import { CMSGraphQLClient } from './cms-graphql'; const client = new CMSGraphQLClient( process.env.CMS_GRAPHQL_URL!, process.env.CMS_API_KEY! ); export async function getAllPosts(): Promise<BlogPost[]> { return client.getBlogPosts(100); } export async function getPost(slug: string): Promise<BlogPost> { return client.getBlogPost(slug); } // pages/blog/[slug].tsx import { GetStaticProps, GetStaticPaths } from 'next'; export const getStaticPaths: GetStaticPaths = async () => { const posts = await getAllPosts(); return { paths: posts.map(post => ({ params: { slug: post.slug } })), fallback: 'blocking' }; }; export const getStaticProps: GetStaticProps = async ({ params, preview = false }) => { const post = await getPost(params!.slug as string); if (!post) { return { notFound: true }; } return { props: { post }, revalidate: 60 // ISR: Revalidate every 60 seconds }; }; // pages/api/revalidate.ts export default async function handler(req: any, res: any) { if (req.query.secret !== process.env.REVALIDATE_SECRET) { return res.status(401).json({ message: 'Invalid token' }); } try { await res.revalidate(`/blog/${req.body.slug}`); return res.json({ revalidated: true }); } catch (err) { return res.status(500).send('Error revalidating'); } }
Best Practices
- Content Modeling - Design flexible content models
- API Optimization - Use GraphQL for precise data fetching
- Caching - Implement multi-layer caching
- Image Optimization - Use CDN and image transformations
- Preview Mode - Enable content preview for editors
- Webhooks - Use webhooks for real-time updates
- ISR - Use Incremental Static Regeneration
- Localization - Support multi-language content
- Versioning - Track content versions
- Security - Secure API keys and webhooks
Quick Start
Contentful Integration
const contentful = require('contentful') const client = contentful.createClient({ space: process.env.CONTENTFUL_SPACE_ID, accessToken: process.env.CONTENTFUL_ACCESS_TOKEN }) // Fetch entries const entries = await client.getEntries({ content_type: 'blogPost' })
Strapi Integration
// Fetch from Strapi API const response = await fetch('http://localhost:1337/api/posts', { headers: { 'Authorization': `Bearer ${process.env.STRAPI_API_TOKEN}` } }) const posts = await response.json()
Production Checklist
- Content Model: Design flexible content models
- API Keys: Secure API keys and tokens
- Caching: Cache content appropriately
- Webhooks: Set up webhooks for content updates
- Preview: Preview mode for draft content
- Localization: Multi-language content support
- Versioning: Content versioning if needed
- Media: Media asset management
- Performance: Optimize API calls
- Error Handling: Handle API failures
- Testing: Test content fetching
- Documentation: Document content structure
Anti-patterns
❌ Don't: Fetch on Every Render
// ❌ Bad - Fetches every render function BlogPost({ id }) { const [post, setPost] = useState(null) useEffect(() => { fetchPost(id).then(setPost) // Fetches every time }) }
// ✅ Good - Cache and memoize const postCache = new Map() function BlogPost({ id }) { const [post, setPost] = useState(postCache.get(id)) useEffect(() => { if (!post) { fetchPost(id).then(p => { postCache.set(id, p) setPost(p) }) } }, [id]) }
❌ Don't: Expose API Keys
// ❌ Bad - API key in client code const client = contentful.createClient({ space: 'public-space-id', accessToken: 'secret-token' // Exposed! })
// ✅ Good - Use backend proxy // Frontend fetch('/api/contentful/posts') // Backend app.get('/api/contentful/posts', async (req, res) => { const client = contentful.createClient({ space: process.env.CONTENTFUL_SPACE_ID, accessToken: process.env.CONTENTFUL_ACCESS_TOKEN }) const posts = await client.getEntries() res.json(posts) })
Integration Points
- API Design (
) - API patterns01-foundations/api-design/ - Caching (
) - Content caching04-database/redis-caching/ - Contentful Integration (
) - Specific platform33-content-management/contentful-integration/