CMS Integration Guide
Learn how to integrate QwickApps React Framework with popular Content Management Systems (CMS) for dynamic, content-driven applications.
Overview
QwickApps React Framework's data binding system works with any headless CMS or API endpoint. This guide covers integration patterns for popular CMS platforms and custom implementations.
Supported CMS Platforms
WordPress (Headless)
Full integration with WordPress REST API and custom post types.
Strapi
Open-source headless CMS with excellent API support.
Contentful
Popular hosted headless CMS with powerful content modeling.
Sanity
Real-time collaborative headless CMS with flexible schema.
Ghost
Modern publishing platform with built-in REST and Admin APIs.
Custom APIs
Integration patterns for custom REST and GraphQL APIs.
WordPress Integration
Setup WordPress Headless
- Install Required Plugins:
# Essential plugins for headless WordPress
- WP REST API
- Advanced Custom Fields (ACF) Pro
- JWT Authentication for WP-API
- WP GraphQL (optional)
- Configure REST API:
// functions.php - Enable CORS for headless setup
function add_cors_http_header(){
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
}
add_action('init','add_cors_http_header');
- Create Custom Post Types:
// Register custom post type for components
function register_component_content() {
register_post_type('qwickapps_content', [
'public' => true,
'show_in_rest' => true,
'rest_base' => 'content',
'supports' => ['title', 'editor', 'custom-fields']
]);
}
add_action('init', 'register_component_content');
WordPress Data Provider
import axios from 'axios';
import { DataProvider } from '@qwickapps/schema';
export class WordPressDataProvider implements DataProvider {
constructor(
private baseUrl: string,
private authToken?: string
) {}
async getData(dataSource: string): Promise<any[]> {
const [postType, slug] = dataSource.split('.');
try {
const response = await axios.get(
`${this.baseUrl}/wp-json/wp/v2/${postType}`,
{
params: { slug },
headers: this.authToken ? {
'Authorization': `Bearer ${this.authToken}`
} : {}
}
);
return response.data.map(post => ({
html: post.content.rendered,
title: post.title.rendered,
excerpt: post.excerpt.rendered,
meta: post.meta || {}
}));
} catch (error) {
console.error('WordPress API error:', error);
return [];
}
}
}
WordPress Usage Example
import { WordPressDataProvider } from './providers/WordPressDataProvider';
import { DataProvider, QwickApp, Article, Button } from '@qwickapps/react-framework';
const wpProvider = new WordPressDataProvider(
'https://your-wp-site.com',
'your-jwt-token'
);
function WordPressApp() {
return (
<QwickApp appId="wp-app">
<DataProvider dataSource={{ dataProvider: wpProvider }}>
{/* Load content from WordPress */}
<Article dataSource="posts.homepage-hero" />
<Button dataSource="buttons.cta-primary" />
</DataProvider>
</QwickApp>
);
}
Strapi Integration
Strapi Setup
- Create Content Types in Strapi Admin:
// Content Type: Component Content
{
"kind": "collectionType",
"collectionName": "component_contents",
"attributes": {
"identifier": { "type": "string", "required": true },
"html": { "type": "richtext" },
"metadata": { "type": "json" }
}
}
- Configure Permissions:
- Enable public access for find and findOne operations
- Set up API tokens for authenticated requests
Strapi Data Provider
import { DataProvider } from '@qwickapps/schema';
export class StrapiDataProvider implements DataProvider {
constructor(
private baseUrl: string,
private apiToken?: string
) {}
async getData(dataSource: string): Promise<any[]> {
const [collection, identifier] = dataSource.split('.');
try {
const response = await fetch(
`${this.baseUrl}/api/${collection}?filters[identifier][$eq]=${identifier}`,
{
headers: this.apiToken ? {
'Authorization': `Bearer ${this.apiToken}`
} : {}
}
);
const result = await response.json();
return result.data.map(item => ({
html: item.attributes.html,
placeholder: item.attributes.placeholder,
...item.attributes.metadata
}));
} catch (error) {
console.error('Strapi API error:', error);
return [];
}
}
}
Contentful Integration
Contentful Setup
- Create Content Model:
// Content Model: Component Content
{
"name": "Component Content",
"fields": [
{ "id": "identifier", "type": "Symbol", "required": true },
{ "id": "content", "type": "RichText" },
{ "id": "placeholder", "type": "Symbol" },
{ "id": "metadata", "type": "Object" }
]
}
Contentful Data Provider
import { createClient } from 'contentful';
import { DataProvider } from '@qwickapps/schema';
export class ContentfulDataProvider implements DataProvider {
private client;
constructor(config: {
space: string;
accessToken: string;
environment?: string;
}) {
this.client = createClient({
space: config.space,
accessToken: config.accessToken,
environment: config.environment || 'master'
});
}
async getData(dataSource: string): Promise<any[]> {
try {
const entries = await this.client.getEntries({
content_type: 'componentContent',
'fields.identifier': dataSource
});
return entries.items.map(item => ({
html: this.richTextToHtml(item.fields.content),
placeholder: item.fields.placeholder,
...item.fields.metadata
}));
} catch (error) {
console.error('Contentful API error:', error);
return [];
}
}
private richTextToHtml(richText: any): string {
// Convert Contentful rich text to HTML
// Use @contentful/rich-text-html-renderer
return richText?.content?.map(node =>
node.nodeType === 'paragraph' ?
`<p>${node.content[0]?.value || ''}</p>` : ''
).join('') || '';
}
}
Sanity Integration
Sanity Schema
// schemas/componentContent.js
export default {
name: 'componentContent',
title: 'Component Content',
type: 'document',
fields: [
{
name: 'identifier',
title: 'Data Source Identifier',
type: 'string',
validation: Rule => Rule.required()
},
{
name: 'content',
title: 'HTML Content',
type: 'array',
of: [{ type: 'block' }]
},
{
name: 'placeholder',
title: 'Placeholder Text',
type: 'string'
}
]
}
Sanity Data Provider
import sanityClient from '@sanity/client';
import { DataProvider } from '@qwickapps/schema';
export class SanityDataProvider implements DataProvider {
private client;
constructor(config: {
projectId: string;
dataset: string;
apiVersion?: string;
token?: string;
}) {
this.client = sanityClient({
projectId: config.projectId,
dataset: config.dataset,
apiVersion: config.apiVersion || '2023-01-01',
token: config.token,
useCdn: !config.token
});
}
async getData(dataSource: string): Promise<any[]> {
try {
const query = `*[_type == "componentContent" && identifier == $dataSource]`;
const results = await this.client.fetch(query, { dataSource });
return results.map(item => ({
html: this.blocksToHtml(item.content),
placeholder: item.placeholder
}));
} catch (error) {
console.error('Sanity API error:', error);
return [];
}
}
private blocksToHtml(blocks: any[]): string {
// Convert Sanity portable text to HTML
return blocks?.map(block => {
if (block.style === 'h1') return `<h1>${block.children[0]?.text}</h1>`;
if (block.style === 'h2') return `<h2>${block.children[0]?.text}</h2>`;
return `<p>${block.children?.map(child => child.text).join('') || ''}</p>`;
}).join('') || '';
}
}
GraphQL Integration
Generic GraphQL Provider
import { DataProvider } from '@qwickapps/schema';
export class GraphQLDataProvider implements DataProvider {
constructor(
private endpoint: string,
private headers: Record<string, string> = {}
) {}
async getData(dataSource: string): Promise<any[]> {
const query = `
query GetComponentContent($identifier: String!) {
componentContents(where: { identifier: $identifier }) {
html
placeholder
metadata
}
}
`;
try {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.headers
},
body: JSON.stringify({
query,
variables: { identifier: dataSource }
})
});
const result = await response.json();
return result.data?.componentContents || [];
} catch (error) {
console.error('GraphQL API error:', error);
return [];
}
}
}
Advanced Patterns
Multi-Source Data Provider
Combine multiple CMS sources:
export class MultiSourceDataProvider implements DataProvider {
constructor(private providers: Map<string, DataProvider>) {}
async getData(dataSource: string): Promise<any[]> {
const [source, ...path] = dataSource.split('.');
const provider = this.providers.get(source);
if (!provider) {
throw new Error(`No provider found for source: ${source}`);
}
return provider.getData(path.join('.'));
}
}
// Usage
const multiProvider = new MultiSourceDataProvider(new Map([
['wp', new WordPressDataProvider('https://wp.example.com')],
['strapi', new StrapiDataProvider('https://strapi.example.com')]
]));
// Load from WordPress
<Article dataSource="wp.posts.homepage" />
// Load from Strapi
<Button dataSource="strapi.buttons.cta" />
Caching with CMS Integration
import { CachedDataProvider } from '@qwickapps/schema';
const cachedCMSProvider = new CachedDataProvider(
new StrapiDataProvider('https://api.example.com'),
{
maxSize: 200,
defaultTTL: 300000, // 5 minutes
customTTL: {
'pages.*': 600000, // Pages cache for 10 minutes
'buttons.*': 60000, // Buttons cache for 1 minute
'content.*': 1800000 // Content cache for 30 minutes
}
}
);
Real-time Updates
WebSocket integration for live content updates:
export class RealtimeDataProvider implements DataProvider {
private cache = new Map();
private ws: WebSocket;
constructor(private baseProvider: DataProvider, wsUrl: string) {
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = this.handleRealtimeUpdate.bind(this);
}
async getData(dataSource: string): Promise<any[]> {
// Check cache first
if (this.cache.has(dataSource)) {
return this.cache.get(dataSource);
}
// Fallback to base provider
const data = await this.baseProvider.getData(dataSource);
this.cache.set(dataSource, data);
return data;
}
private handleRealtimeUpdate(event: MessageEvent) {
const update = JSON.parse(event.data);
if (update.dataSource && update.data) {
this.cache.set(update.dataSource, update.data);
// Trigger re-render of components using this data
this.notifySubscribers(update.dataSource);
}
}
}
Security Best Practices
API Token Management
// Use environment variables for sensitive tokens
const cmsProvider = new StrapiDataProvider(
process.env.REACT_APP_STRAPI_URL!,
process.env.REACT_APP_STRAPI_TOKEN
);
// Implement token refresh for long-lived applications
class SecureDataProvider implements DataProvider {
private async getValidToken(): Promise<string> {
// Implement token refresh logic
return this.refreshTokenIfNeeded();
}
}
Content Sanitization
import DOMPurify from 'dompurify';
export class SecureCMSProvider implements DataProvider {
async getData(dataSource: string): Promise<any[]> {
const data = await this.baseCMSProvider.getData(dataSource);
return data.map(item => ({
...item,
html: item.html ? DOMPurify.sanitize(item.html) : item.html
}));
}
}
Testing CMS Integration
Mock CMS Provider
export class MockCMSProvider implements DataProvider {
constructor(private mockData: Record<string, any[]>) {}
async getData(dataSource: string): Promise<any[]> {
return this.mockData[dataSource] || [];
}
}
// Test setup
const mockProvider = new MockCMSProvider({
'buttons.cta': [{
html: '<button>Test CTA</button>',
placeholder: 'Loading CTA...'
}]
});
Integration Tests
import { render, screen } from '@testing-library/react';
import { DataProvider, Button } from '@qwickapps/react-framework';
test('loads content from CMS', async () => {
const mockProvider = new MockCMSProvider({
'buttons.test': [{ label: 'Test Button' }]
});
render(
<DataProvider dataSource={{ dataProvider: mockProvider }}>
<Button dataSource="buttons.test" />
</DataProvider>
);
expect(await screen.findByText('Test Button')).toBeInTheDocument();
});
Performance Considerations
Batch Requests
export class BatchingDataProvider implements DataProvider {
private pendingRequests = new Map();
private batchTimeout: NodeJS.Timeout | null = null;
async getData(dataSource: string): Promise<any[]> {
return new Promise((resolve, reject) => {
this.pendingRequests.set(dataSource, { resolve, reject });
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this.executeBatch();
}, 10); // Batch requests within 10ms
}
});
}
private async executeBatch() {
const dataSources = Array.from(this.pendingRequests.keys());
const requests = Array.from(this.pendingRequests.values());
try {
const results = await this.batchRequest(dataSources);
requests.forEach((request, index) => {
request.resolve(results[index]);
});
} catch (error) {
requests.forEach(request => request.reject(error));
}
this.pendingRequests.clear();
this.batchTimeout = null;
}
}
Troubleshooting
Common Issues
CORS Errors
// Enable CORS in your CMS
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
Authentication Failures
// Check token validity
const isValidToken = await verifyToken(apiToken);
if (!isValidToken) {
throw new Error('Invalid or expired API token');
}
Slow API Responses
// Implement request timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch(apiUrl, { signal: controller.signal });
What's Next?
- Data Binding Guide - Learn Framework integration patterns
- Performance Guide - Optimize CMS data loading
- Schema System - Validate CMS data with schemas
- Component Development - Build CMS-aware components