Contentful CMS
Headless CMS integration for SOG Knives with Space ID zg6h9gisshv3 managing homepage, category, product, and stories content
Overview
SOG Knives uses Contentful as a headless CMS to manage editorial content alongside BigCommerce's e-commerce data. This allows the marketing team to update homepage heroes, category landing pages, product highlights, and stories without developer intervention.
Platform: Contentful (Cloud)
Space ID: zg6h9gisshv3
Environment: master
API: Content Delivery API (CDA)
Integration: JavaScript API client in theme
Contentful Space Structure
Content Model
Contentful Space (zg6h9gisshv3)
├── Homepage Hero
├── Featured Products
├── Category Hero
├── Category Content Blocks
├── Product Enhancement
├── Story Post
├── Media Asset
└── Navigation Menu
Content Types
Homepage Hero
Content Type ID: homepageHero
Manages the main hero section on the homepage.
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Title | title | Short Text | Required, max 80 chars |
| Subtitle | subtitle | Short Text | Max 150 chars |
| Background Image | backgroundImage | Media | Required, image |
| CTA Text | ctaText | Short Text | Required, max 30 chars |
| CTA URL | ctaUrl | Short Text | Required, valid URL |
| Text Position | textPosition | Dropdown | left, center, right |
| Overlay Opacity | overlayOpacity | Number | 0-100 |
| Active | active | Boolean | Required |
Example Entry
{
"sys": {
"id": "hero-spring-2026",
"contentType": {
"sys": {
"id": "homepageHero"
}
}
},
"fields": {
"title": "New Spring Collection",
"subtitle": "Tactical knives engineered for precision and reliability",
"backgroundImage": {
"sys": { "id": "asset-abc123" }
},
"ctaText": "Shop Now",
"ctaUrl": "/tactical-knives",
"textPosition": "left",
"overlayOpacity": 40,
"active": true
}
}
Featured Products
Content Type ID: featuredProducts
Curated product collections for homepage display.
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Section Title | sectionTitle | Short Text | Required, max 60 chars |
| Product IDs | productIds | Short Text (multi) | BigCommerce product IDs |
| Display Type | displayType | Dropdown | grid, carousel, featured |
| Background Color | backgroundColor | Short Text | Hex color code |
| Order | order | Number | Display order |
Integration with BigCommerce
Featured Products references BigCommerce product IDs, then theme fetches full product data:
// Fetch featured products
const featuredEntry = await contentfulClient.getEntry('featured-spring-2026');
const productIds = featuredEntry.fields.productIds; // ['123', '456', '789']
// Fetch product data from BigCommerce
const products = await Promise.all(
productIds.map(id => fetch(`/api/storefront/products/${id}`).then(r => r.json()))
);
Category Hero
Content Type ID: categoryHero
Enhanced hero sections for category landing pages.
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Category ID | categoryId | Number | Required, BigCommerce category ID |
| Hero Title | heroTitle | Short Text | Max 80 chars |
| Hero Subtitle | heroSubtitle | Long Text | Max 250 chars |
| Hero Image | heroImage | Media | Image asset |
| Show Filter Bar | showFilterBar | Boolean | Default true |
Category Content Blocks
Content Type ID: categoryContentBlock
Rich content sections for category pages (e.g., buying guides, feature callouts).
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Category ID | categoryId | Number | Required |
| Block Title | blockTitle | Short Text | Max 60 chars |
| Block Content | blockContent | Rich Text | Markdown supported |
| Block Position | blockPosition | Dropdown | top, middle, bottom |
| Block Type | blockType | Dropdown | text, video, gallery, comparison |
| Media Assets | mediaAssets | Media (multi) | Optional |
| Order | order | Number | Display order |
Rich Text Rendering
Contentful rich text is rendered using the @contentful/rich-text-html-renderer library:
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
const richTextDocument = entry.fields.blockContent;
const html = documentToHtmlString(richTextDocument);
Product Enhancement
Content Type ID: productEnhancement
Additional content for product detail pages (features, specifications, videos).
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Product ID | productId | Number | Required, BigCommerce product ID |
| Feature Callouts | featureCallouts | Short Text (multi) | Max 6 items |
| Specification Table | specificationTable | Rich Text | Markdown table |
| Video URL | videoUrl | Short Text | YouTube or Vimeo URL |
| Download Links | downloadLinks | JSON | Manual PDFs, spec sheets |
| Related Content | relatedContent | Reference (multi) | Links to Story Posts |
Story Post
Content Type ID: storyPost
Blog-style content for the Stories section.
Fields
| Field Name | Field ID | Type | Validation |
|---|---|---|---|
| Title | title | Short Text | Required, max 100 chars |
| Slug | slug | Short Text | Required, unique, URL-safe |
| Author | author | Short Text | Max 50 chars |
| Publish Date | publishDate | Date & Time | Required |
| Featured Image | featuredImage | Media | Image asset |
| Excerpt | excerpt | Long Text | Max 300 chars |
| Body Content | bodyContent | Rich Text | Full article |
| Category | category | Dropdown | News, Tips, Reviews, Events |
| Tags | tags | Short Text (multi) | SEO tags |
| Related Products | relatedProducts | Short Text (multi) | BigCommerce product IDs |
API Integration
Contentful Client Setup
// assets/js/contentful.js
import * as contentful from 'contentful';
// Initialize Contentful client
const contentfulClient = contentful.createClient({
space: 'zg6h9gisshv3',
accessToken: window.contentfulDeliveryToken, // Injected by theme
environment: 'master',
host: 'cdn.contentful.com' // Content Delivery API
});
export default contentfulClient;
Fetching Content
Homepage Hero
export async function getHomepageHero() {
try {
const entries = await contentfulClient.getEntries({
content_type: 'homepageHero',
'fields.active': true,
limit: 1,
order: '-sys.createdAt'
});
if (entries.items.length === 0) {
return null;
}
return entries.items[0];
} catch (error) {
console.error('Error fetching homepage hero:', error);
return null;
}
}
Category Content
export async function getCategoryContent(categoryId) {
try {
const [hero, blocks] = await Promise.all([
// Fetch category hero
contentfulClient.getEntries({
content_type: 'categoryHero',
'fields.categoryId': categoryId,
limit: 1
}),
// Fetch category content blocks
contentfulClient.getEntries({
content_type: 'categoryContentBlock',
'fields.categoryId': categoryId,
order: 'fields.order'
})
]);
return {
hero: hero.items[0] || null,
blocks: blocks.items || []
};
} catch (error) {
console.error('Error fetching category content:', error);
return { hero: null, blocks: [] };
}
}
Product Enhancements
export async function getProductEnhancement(productId) {
try {
const entries = await contentfulClient.getEntries({
content_type: 'productEnhancement',
'fields.productId': productId,
limit: 1,
include: 2 // Include related content references
});
return entries.items[0] || null;
} catch (error) {
console.error('Error fetching product enhancement:', error);
return null;
}
}
Stories
export async function getStories(category = null, limit = 10, skip = 0) {
const query = {
content_type: 'storyPost',
order: '-fields.publishDate',
limit,
skip
};
if (category) {
query['fields.category'] = category;
}
try {
const entries = await contentfulClient.getEntries(query);
return {
stories: entries.items,
total: entries.total,
skip: entries.skip,
limit: entries.limit
};
} catch (error) {
console.error('Error fetching stories:', error);
return { stories: [], total: 0 };
}
}
Asset Handling
Contentful assets (images, videos, PDFs) are transformed with URL parameters:
function getOptimizedImageUrl(asset, options = {}) {
const baseUrl = asset.fields.file.url;
const params = new URLSearchParams({
w: options.width || 1200,
h: options.height || '',
fit: options.fit || 'fill',
f: options.format || 'webp',
q: options.quality || 80
});
return `${baseUrl}?${params.toString()}`;
}
// Usage
const heroImage = entry.fields.backgroundImage;
const imageUrl = getOptimizedImageUrl(heroImage, {
width: 1920,
format: 'webp',
quality: 85
});
Caching Strategy
Server-Side Caching
Contentful responses are cached in BigCommerce theme to reduce API calls:
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const cache = new Map();
export async function getCachedContent(cacheKey, fetchFunction) {
const now = Date.now();
const cached = cache.get(cacheKey);
if (cached && (now - cached.timestamp) < CACHE_TTL) {
return cached.data;
}
const data = await fetchFunction();
cache.set(cacheKey, {
data,
timestamp: now
});
return data;
}
// Usage
const hero = await getCachedContent('homepage-hero', getHomepageHero);
Client-Side Caching
LocalStorage caches content on the client:
const LOCALSTORAGE_PREFIX = 'contentful_';
const CLIENT_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
export function getCachedFromLocalStorage(key) {
try {
const item = localStorage.getItem(`${LOCALSTORAGE_PREFIX}${key}`);
if (!item) return null;
const { data, timestamp } = JSON.parse(item);
const now = Date.now();
if ((now - timestamp) < CLIENT_CACHE_TTL) {
return data;
}
localStorage.removeItem(`${LOCALSTORAGE_PREFIX}${key}`);
return null;
} catch (error) {
return null;
}
}
export function setCachedToLocalStorage(key, data) {
try {
const item = {
data,
timestamp: Date.now()
};
localStorage.setItem(`${LOCALSTORAGE_PREFIX}${key}`, JSON.stringify(item));
} catch (error) {
console.error('LocalStorage error:', error);
}
}
Cache Invalidation
Webhooks from Contentful trigger cache invalidation:
// Backend webhook handler (Node.js example)
app.post('/webhooks/contentful', async (req, res) => {
const event = req.body;
// Verify webhook signature
const isValid = verifyContentfulWebhook(req);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Clear relevant cache based on content type
const contentType = event.sys.contentType.sys.id;
clearCacheForContentType(contentType);
res.status(200).send('OK');
});
function clearCacheForContentType(contentType) {
// Clear server-side cache
const keysToRemove = [];
for (const [key] of cache.entries()) {
if (key.startsWith(contentType)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => cache.delete(key));
// Notify clients to clear their LocalStorage (via WebSocket or server-sent events)
notifyClientsToRefresh(contentType);
}
Template Integration
Homepage Hero
Category Content Blocks
Product Enhancement
Content Management Workflow
Content Author Workflow
- Login to Contentful: https://app.contentful.com/spaces/zg6h9gisshv3
- Navigate to Content: Click "Content" in sidebar
- Create/Edit Entry: Click "Add entry" or select existing entry
- Fill in Fields: Enter title, text, upload images
- Preview: Use preview mode to see live changes (requires preview API setup)
- Publish: Click "Publish" to make content live
- Automatic Update: Site refreshes content within 5 minutes (cache TTL)
Content Versioning
Contentful maintains version history for all entries:
- View History: Click "Versions" tab in entry editor
- Compare Versions: Select two versions to see diff
- Restore Version: Click "Restore" to revert to previous version
- Audit Trail: See who made changes and when
Content Scheduling
Schedule content to publish at specific times:
// Scheduled publish (handled by Contentful)
const scheduledEntry = await contentfulClient.createEntryWithId(
'homepageHero',
'hero-summer-2026',
{
fields: {
title: { 'en-US': 'Summer Collection 2026' },
// ... other fields
active: { 'en-US': false }
}
}
);
// Schedule publish for June 1, 2026 at 12:00 UTC
await scheduledEntry.publishScheduled('2026-06-01T12:00:00Z');
Environment Management
Environments
| Environment | Purpose | API Host |
|---|---|---|
master | Production content | cdn.contentful.com |
staging | QA and testing | preview.contentful.com |
development | Local development | preview.contentful.com |
Environment-Specific Tokens
const CONTENTFUL_CONFIG = {
production: {
space: 'zg6h9gisshv3',
accessToken: process.env.CONTENTFUL_PRODUCTION_TOKEN,
environment: 'master',
host: 'cdn.contentful.com'
},
staging: {
space: 'zg6h9gisshv3',
accessToken: process.env.CONTENTFUL_STAGING_TOKEN,
environment: 'staging',
host: 'preview.contentful.com'
},
development: {
space: 'zg6h9gisshv3',
accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
environment: 'master',
host: 'preview.contentful.com'
}
};
const config = CONTENTFUL_CONFIG[process.env.NODE_ENV || 'development'];
const client = contentful.createClient(config);
Analytics & Monitoring
Content Performance Tracking
Track which Contentful content drives engagement:
// Track homepage hero clicks
document.getElementById('hero-cta').addEventListener('click', () => {
gtag('event', 'contentful_hero_click', {
hero_id: hero.sys.id,
hero_title: hero.fields.title,
cta_url: hero.fields.ctaUrl
});
});
// Track category content block views
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
gtag('event', 'contentful_block_view', {
block_id: entry.target.dataset.blockId,
block_type: entry.target.dataset.blockType,
category_id: entry.target.dataset.categoryId
});
}
});
});
Error Monitoring
Log Contentful API errors to monitoring service:
async function fetchWithErrorHandling(fetchFunction, context) {
try {
return await fetchFunction();
} catch (error) {
// Log to error monitoring (e.g., Sentry)
Sentry.captureException(error, {
tags: {
service: 'contentful',
context: context
},
extra: {
spaceId: 'zg6h9gisshv3'
}
});
// Return fallback content
return null;
}
}
Security Considerations
API Token Management
- Delivery Token: Read-only, safe for client-side use
- Preview Token: Used for unpublished content previews
- Management Token: Server-side only, never exposed to client
Rate Limiting
Contentful API has rate limits:
- Delivery API: 78 requests per second
- Preview API: 14 requests per second
- Management API: 10 requests per second
Implement exponential backoff for rate limit errors:
async function fetchWithRetry(fetchFunction, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetchFunction();
} catch (error) {
if (error.sys?.id === 'RateLimitExceeded' && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
Content Validation
Validate Contentful data before rendering:
function validateHomepageHero(entry) {
const required = ['title', 'subtitle', 'backgroundImage', 'ctaText', 'ctaUrl'];
const missing = required.filter(field => !entry.fields[field]);
if (missing.length > 0) {
console.error(`Missing required fields: ${missing.join(', ')}`);
return false;
}
return true;
}