| name | cms-content-pipeline |
| description | Master the Firestore CMS integration - content generation scripts, field mapping, and safe content modification patterns |
CMS Content Pipeline
When to Activate
Use this skill when the agent needs to:
- Add new CMS-controlled content fields
- Understand why content isn't appearing correctly
- Debug generate script failures
- Modify how Firestore data maps to components
- Understand the Airtable → Firestore migration
The Content Pipeline
Content flows through a static generation pipeline:
┌─────────────────┐ ┌──────────────────────────┐ ┌─────────────────┐
│ Firestore CMS │ ──► │ generate-from-firestore │ ──► │ src/content/*.js│
│ (Admin Panel) │ │ (CI/CD build-time) │ │ (Static JS) │
└─────────────────┘ └──────────────────────────┘ └─────────────────┘
│ │
│ OR (legacy) ▼
│ ┌─────────────────┐
┌─────────────────┐ ┌──────────────────┐ │ React Component │
│ Airtable Table │ ──► │ generate-*.cjs │ ───────────│ (Direct import) │
│ (Deprecated) │ │ (Local dev) │ └─────────────────┘
└─────────────────┘ └──────────────────┘
Critical insight: Content is NOT fetched at runtime. It's generated at build time and imported as static JavaScript modules.
CI/CD flow: GitHub Actions runs generate-from-firestore.cjs which pulls from Firestore collections and generates static JS files.
Four Content Tables
| Table | Script | Output | Primary Consumer |
|---|---|---|---|
| Site Content | generate-content.cjs |
siteContent.js |
Home.jsx |
| Schedule | generate-schedule.cjs |
scheduleData.js |
Schedule.jsx |
| Organizations | generate-organizations.cjs |
organizationsData.js |
Sponsors.jsx |
| Updates | generate-updates.cjs |
updatesData.js |
Updates.jsx, UpdateDetail.jsx |
Content Access Patterns
Pattern 1: getContent() for text values
import { getContent } from '../content/siteContent';
// getContent(section, field, fallback)
const headline = getContent('details', 'headline', 'Default Text');
Pattern 2: getContentMeta() for styled content
import { getContentMeta, getColorClass } from '../content/siteContent';
const meta = getContentMeta('details', 'year');
const colorClass = getColorClass(meta?.color, 'text');
// Returns: 'text-brand-teal', 'text-brand-cardinal', etc.
Pattern 3: Pre-built arrays
import { timeline, stats } from '../content/siteContent';
// timeline: Array of 10 summit years with location, theme, link
// stats: Array of 4 stats (summits, cities, attendees, mission)
Field Naming Conventions
Site Content Table
Field: Unique identifier used ingetContent(section, 'field')Section: Groups content (details, footer, expect, history, stats)Content: The actual text valueColor: Brand color name (teal, cardinal, ink, green-dark)Order: Sort order within section
Schedule Table (flexible field names)
The script handles variations:
title: fields['Session title'] || fields.title || fields.Title
day: fields.day || fields.Day
start_time: fields.start_time || fields['Start time']
Adding New CMS Content
Step 1: Identify if content should be CMS-controlled
Ask: "Will editors need to change this text without deploying code?"
Step 2: Update component to use getContent()
// Before (hardcoded)
<h1>Welcome to CJS2026</h1>
// After (CMS-controlled)
<h1>{getContent('hero', 'welcome_text', 'Welcome to CJS2026')}</h1>
Step 3: Document in CLAUDE.md
Add to "Pending Airtable updates" table:
| welcome_text | hero | "Welcome to CJS2026" | (editor sets) | Pending |
Step 4: Editor adds field in Airtable
Field must match exactly: section + field name.
Troubleshooting
Content not appearing
- Check Airtable has field with correct Section and Field name
- Run
npm run generate-contentto pull fresh data - Verify
src/content/siteContent.jswas updated (check timestamp) - Confirm component uses correct
getContent('section', 'field')
Generate script fails
- Check
AIRTABLE_API_KEYis set (env or .env file) - Verify network connectivity to Airtable
- Check Airtable table hasn't been renamed/deleted
Sponsors not showing
- Verify "Sponsor" checkbox is checked in Organizations table
- Check "Sponsor tier" field has valid value
- Run
npm run generate-organizations - For logo issues: Firebase credentials needed for upload
Integration Points
- cjs-architecture - For understanding where content flows
- firebase-patterns - For CloudFunction-based content (getSiteContent)
CI/CD Export Naming Convention
Critical: The generate-from-firestore.cjs script must export variables that match what components import.
The Export Mismatch Bug (2026-01-04)
Problem: CI builds failed because components imported sessions but the script exported scheduleData.
// Component imports:
import { sessions as allSessions } from '../content/scheduleData'
// Script exported (wrong):
export const scheduleData = [...]
// Script should export (correct):
export const sessions = [...]
export const scheduleData = sessions // Alias for compatibility
Rule: When modifying generate-from-firestore.cjs, always check what names components are importing.
Local Logo Path Pattern
Sponsor logos need localLogoPath for static file serving:
// In generate-from-firestore.cjs
const slug = data.slug || data.name?.toLowerCase().replace(/\s+/g, '-');
const localLogoPath = data.logoUrl && slug ? `/sponsor-logos/${slug}.png` : null;
return {
// ...other fields
logoUrl: data.logoUrl || null,
localLogoPath: localLogoPath, // For static file fallback
};
Components use: src={sponsor.localLogoPath || sponsor.logoUrl}
Guidelines
- Never edit
src/content/*.jsfiles directly - they're auto-generated - Always provide meaningful fallback values in
getContent() - Document new CMS fields in CLAUDE.md before implementation
- Use
npm run generate-allbefore deployment to ensure fresh content - Match export names to component import names in generation scripts
- Include localLogoPath for sponsor logos to enable static fallback
- Test CI builds after modifying generation scripts