Skip to main content

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 NameField IDTypeValidation
TitletitleShort TextRequired, max 80 chars
SubtitlesubtitleShort TextMax 150 chars
Background ImagebackgroundImageMediaRequired, image
CTA TextctaTextShort TextRequired, max 30 chars
CTA URLctaUrlShort TextRequired, valid URL
Text PositiontextPositionDropdownleft, center, right
Overlay OpacityoverlayOpacityNumber0-100
ActiveactiveBooleanRequired

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
}
}

Content Type ID: featuredProducts

Curated product collections for homepage display.

Fields

Field NameField IDTypeValidation
Section TitlesectionTitleShort TextRequired, max 60 chars
Product IDsproductIdsShort Text (multi)BigCommerce product IDs
Display TypedisplayTypeDropdowngrid, carousel, featured
Background ColorbackgroundColorShort TextHex color code
OrderorderNumberDisplay 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 NameField IDTypeValidation
Category IDcategoryIdNumberRequired, BigCommerce category ID
Hero TitleheroTitleShort TextMax 80 chars
Hero SubtitleheroSubtitleLong TextMax 250 chars
Hero ImageheroImageMediaImage asset
Show Filter BarshowFilterBarBooleanDefault true

Category Content Blocks

Content Type ID: categoryContentBlock

Rich content sections for category pages (e.g., buying guides, feature callouts).

Fields

Field NameField IDTypeValidation
Category IDcategoryIdNumberRequired
Block TitleblockTitleShort TextMax 60 chars
Block ContentblockContentRich TextMarkdown supported
Block PositionblockPositionDropdowntop, middle, bottom
Block TypeblockTypeDropdowntext, video, gallery, comparison
Media AssetsmediaAssetsMedia (multi)Optional
OrderorderNumberDisplay 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 NameField IDTypeValidation
Product IDproductIdNumberRequired, BigCommerce product ID
Feature CalloutsfeatureCalloutsShort Text (multi)Max 6 items
Specification TablespecificationTableRich TextMarkdown table
Video URLvideoUrlShort TextYouTube or Vimeo URL
Download LinksdownloadLinksJSONManual PDFs, spec sheets
Related ContentrelatedContentReference (multi)Links to Story Posts

Story Post

Content Type ID: storyPost

Blog-style content for the Stories section.

Fields

Field NameField IDTypeValidation
TitletitleShort TextRequired, max 100 chars
SlugslugShort TextRequired, unique, URL-safe
AuthorauthorShort TextMax 50 chars
Publish DatepublishDateDate & TimeRequired
Featured ImagefeaturedImageMediaImage asset
ExcerptexcerptLong TextMax 300 chars
Body ContentbodyContentRich TextFull article
CategorycategoryDropdownNews, Tips, Reviews, Events
TagstagsShort Text (multi)SEO tags
Related ProductsrelatedProductsShort 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

{{!-- templates/pages/home.html --}}
{{#partial "hero"}}
<div class="homepage-hero" data-contentful-hero>
<div class="homepage-hero__background">
<img src="" alt="" id="hero-background" loading="eager">
</div>
<div class="homepage-hero__content">
<h1 class="homepage-hero__title" id="hero-title"></h1>
<p class="homepage-hero__subtitle" id="hero-subtitle"></p>
<a href="#" class="btn btn-primary" id="hero-cta"></a>
</div>
</div>

<script>
(async () => {
const { getHomepageHero, getOptimizedImageUrl } = await import('/assets/js/contentful.js');
const hero = await getHomepageHero();

if (hero) {
document.getElementById('hero-background').src =
getOptimizedImageUrl(hero.fields.backgroundImage, { width: 1920 });
document.getElementById('hero-title').textContent = hero.fields.title;
document.getElementById('hero-subtitle').textContent = hero.fields.subtitle;
document.getElementById('hero-cta').href = hero.fields.ctaUrl;
document.getElementById('hero-cta').textContent = hero.fields.ctaText;
}
})();
</script>
{{/partial}}

Category Content Blocks

{{!-- templates/pages/category.html --}}
{{#partial "page"}}
<div class="category-page">
{{!-- Contentful hero section --}}
<div id="category-hero" data-category-id="{{category.id}}"></div>

{{!-- Product grid --}}
<div class="product-listing">
{{> components/category/product-grid}}
</div>

{{!-- Contentful content blocks --}}
<div id="category-content-blocks"></div>
</div>

<script>
(async () => {
const { getCategoryContent } = await import('/assets/js/contentful.js');
const categoryId = parseInt(document.getElementById('category-hero').dataset.categoryId);
const content = await getCategoryContent(categoryId);

// Render hero
if (content.hero) {
renderCategoryHero(content.hero);
}

// Render content blocks
if (content.blocks.length > 0) {
renderContentBlocks(content.blocks);
}
})();
</script>
{{/partial}}

Product Enhancement

{{!-- templates/pages/product.html --}}
{{#partial "page"}}
<div class="product-view">
{{> components/products/product-view}}

{{!-- Contentful enhancements --}}
<div id="product-enhancement" data-product-id="{{product.id}}"></div>
</div>

<script>
(async () => {
const { getProductEnhancement } = await import('/assets/js/contentful.js');
const productId = parseInt(document.getElementById('product-enhancement').dataset.productId);
const enhancement = await getProductEnhancement(productId);

if (enhancement) {
renderProductEnhancement(enhancement);
}
})();
</script>
{{/partial}}

Content Management Workflow

Content Author Workflow

  1. Login to Contentful: https://app.contentful.com/spaces/zg6h9gisshv3
  2. Navigate to Content: Click "Content" in sidebar
  3. Create/Edit Entry: Click "Add entry" or select existing entry
  4. Fill in Fields: Enter title, text, upload images
  5. Preview: Use preview mode to see live changes (requires preview API setup)
  6. Publish: Click "Publish" to make content live
  7. 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

EnvironmentPurposeAPI Host
masterProduction contentcdn.contentful.com
stagingQA and testingpreview.contentful.com
developmentLocal developmentpreview.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;
}