| 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';
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');
Pattern 3: Pre-built arrays
import { timeline, stats } from '../content/siteContent';
Field Naming Conventions
Site Content Table
Field: Unique identifier used in getContent(section, 'field')
Section: Groups content (details, footer, expect, history, stats)
Content: The actual text value
Color: 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()
<h1>Welcome to CJS2026</h1>
<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-content to pull fresh data
- Verify
src/content/siteContent.js was updated (check timestamp)
- Confirm component uses correct
getContent('section', 'field')
Generate script fails
- Check
AIRTABLE_API_KEY is 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.
import { sessions as allSessions } from '../content/scheduleData'
export const scheduleData = [...]
export const sessions = [...]
export const scheduleData = sessions
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:
const slug = data.slug || data.name?.toLowerCase().replace(/\s+/g, '-');
const localLogoPath = data.logoUrl && slug ? `/sponsor-logos/${slug}.png` : null;
return {
logoUrl: data.logoUrl || null,
localLogoPath: localLogoPath,
};
Components use: src={sponsor.localLogoPath || sponsor.logoUrl}
Guidelines
- Never edit
src/content/*.js files 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-all before 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