Skip to main content

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

  1. Install Required Plugins:
# Essential plugins for headless WordPress
- WP REST API
- Advanced Custom Fields (ACF) Pro
- JWT Authentication for WP-API
- WP GraphQL (optional)
  1. 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');
  1. 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

  1. 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" }
}
}
  1. 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

  1. 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?