dev-diarycmscontent-managementdecap-cmsbrand-guidelinesworkflow

Dev Diary: Building godegit.dev - Phase 5: Content Management & Brand Guidelines

Building a content management system that maintains brand standards while empowering non-technical content creators. Learn how we integrated Decap CMS with custom validation and brand-compliant preview systems.

godegit Development Team
godegit Development Team
13 min read
Decap CMS interface showing brand-compliant content editing

Custom CMS interface with brand validation and preview systems

Empowering Content Creators While Maintaining Brand Standards

Welcome back to our godegit.dev development diary! Phase 5 tackles one of the most challenging aspects of modern web development: how do you give content creators the freedom to publish while ensuring every piece of content maintains strict brand standards?

This phase taught us that great content management isn't just about providing editing tools—it's about creating systems that make doing the right thing easier than doing the wrong thing.

The Content Management Challenge

Our requirements were clear but seemingly contradictory:

  • Empower non-technical team members to create and edit content
  • Maintain strict brand compliance across all content
  • Provide rich editing experience without compromising quality
  • Enable content workflows for review and approval
  • Ensure SEO optimization is automatic, not optional
  • Support multiple content types (docs, blog, changelog) with different requirements

Traditional CMS solutions either gave too much freedom (risking brand compliance) or too little (frustrating content creators). We needed something custom.

Choosing Decap CMS: Git-Based Content Management

After evaluating several options, we chose Decap CMS (formerly Netlify CMS) for several key reasons:

  1. Git-based workflow: Content changes go through the same review process as code
  2. Customizable interface: We could build brand compliance directly into the editing experience
  3. No database required: Content lives as markdown files alongside our code
  4. Developer-friendly: Configuration in code, not admin interfaces
  5. Preview system: Real-time preview with our actual components

Building the Brand-Compliant CMS Configuration

The Foundation: Configuration as Brand Guidelines

Our CMS configuration became a living document of our brand guidelines:

# public/admin/config.yml
backend:
  name: git-gateway
  branch: main

media_folder: 'public/images'
public_folder: '/images'

# Editorial workflow for content review
publish_mode: editorial_workflow

collections:
  # Blog posts with brand guidelines built-in
  - name: 'blog'
    label: 'Blog Posts'
    folder: 'content/blog'
    create: true
    slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
    editor:
      preview: false
    fields:
      - label: 'Title'
        name: 'title'
        widget: 'string'
        required: true
        hint: 'Use compelling, SEO-friendly titles that reflect brand voice'

      - label: 'Description'
        name: 'description'
        widget: 'text'
        required: true
        hint: 'Brief summary for SEO and social sharing (150-160 characters)'

      - label: 'Author'
        name: 'author'
        widget: 'string'
        required: true
        default: 'godegit Team'

      - label: 'Tags'
        name: 'tags'
        widget: 'list'
        allow_add: true
        required: false
        hint: 'Use relevant tags like: development, git, performance, tutorial'

      - label: 'Category'
        name: 'category'
        widget: 'select'
        options:
          [
            'announcement',
            'tutorial',
            'development',
            'performance',
            'community',
          ]
        required: true

      - label: 'Reading Time'
        name: 'readingTime'
        widget: 'number'
        value_type: 'int'
        min: 1
        max: 30
        hint: 'Estimated reading time in minutes'

      - label: 'Featured'
        name: 'featured'
        widget: 'boolean'
        default: false
        hint: 'Mark as featured post for homepage display'

      - label: 'Body'
        name: 'body'
        widget: 'markdown'
        required: true
        hint:
          'Use serif typography mindset: longer paragraphs, thoughtful pacing,
          code blocks with context'

Custom Validation: Brand Guidelines as Code

We extended the CMS with custom validation to enforce brand standards:

// public/admin/validation.js
const brandValidation = {
  // Validate color mentions in content
  validateBrandColors: content => {
    const invalidColors = /#[0-9a-fA-F]{6}/g.exec(content)
    if (invalidColors) {
      const validColors = ['#1A1A1A', '#F7F7F7', '#2196F3', '#757575']
      const found = invalidColors.filter(
        color => !validColors.includes(color.toUpperCase())
      )
      if (found.length > 0) {
        return `Invalid brand colors found: ${found.join(', ')}. Use only: ${validColors.join(', ')}`
      }
    }
    return null
  },

  // Validate heading hierarchy
  validateHeadingHierarchy: content => {
    const headings = content.match(/^#+\s/gm) || []
    let currentLevel = 0

    for (const heading of headings) {
      const level = heading.match(/^#+/)[0].length
      if (level > currentLevel + 1) {
        return `Heading hierarchy error: Don't skip heading levels. Found h${level} after h${currentLevel}`
      }
      currentLevel = Math.max(currentLevel, level)
    }
    return null
  },

  // Validate alt text for images
  validateImageAltText: content => {
    const images = content.match(/!\[([^\]]*)\]\([^)]+\)/g) || []
    const imagesWithoutAlt = images.filter(img => {
      const altText = img.match(/!\[([^\]]*)\]/)[1]
      return !altText || altText.trim().length === 0
    })

    if (imagesWithoutAlt.length > 0) {
      return `${imagesWithoutAlt.length} images missing alt text. All images must have descriptive alt text for accessibility.`
    }
    return null
  },
}

// Hook into CMS save events
CMS.registerEventListener({
  name: 'preSave',
  handler: ({ entry }) => {
    const content = entry.get('data').get('body')
    const validationErrors = []

    // Run all brand validations
    Object.values(brandValidation).forEach(validator => {
      const error = validator(content)
      if (error) validationErrors.push(error)
    })

    if (validationErrors.length > 0) {
      throw new Error(
        `Brand validation failed:\n${validationErrors.join('\n')}`
      )
    }
  },
})

Custom Preview System: Real Brand Experience

The preview system became our secret weapon for brand compliance:

// public/admin/preview-components.js
const BlogPostPreview = ({ entry, widgetFor }) => {
  const title = entry.getIn(['data', 'title'])
  const description = entry.getIn(['data', 'description'])
  const author = entry.getIn(['data', 'author'])
  const publishedAt = entry.getIn(['data', 'publishedAt'])
  const body = widgetFor('body')

  return h(
    'div',
    { className: 'blog-preview' },
    // Header with exact brand styling
    h(
      'header',
      { className: 'blog-header' },
      h(
        'h1',
        {
          className:
            'text-4xl font-serif font-bold text-brand-dark mb-6 leading-tight',
          style: { fontFamily: 'Georgia, "Times New Roman", serif' },
        },
        title
      ),
      h(
        'p',
        {
          className: 'text-xl text-brand-muted leading-relaxed mb-8 font-serif',
          style: { fontFamily: 'Georgia, "Times New Roman", serif' },
        },
        description
      ),
      h(
        'div',
        {
          className:
            'flex items-center space-x-6 text-sm text-brand-muted border-b border-gray-200 pb-6',
        },
        h('span', { className: 'font-medium' }, author),
        h('time', {}, publishedAt && publishedAt.format('MMMM D, YYYY'))
      )
    ),
    // Content with brand typography
    h(
      'div',
      {
        className: 'blog-content prose prose-lg max-w-none',
        style: {
          fontFamily: 'Georgia, "Times New Roman", serif',
          lineHeight: '1.8',
        },
      },
      body
    )
  )
}

// Register custom preview templates
CMS.registerPreviewTemplate('blog', BlogPostPreview)
CMS.registerPreviewTemplate('docs', DocumentationPreview)

Content Type Architecture: Different Needs, Consistent Standards

Each content type required different fields but maintained consistent brand standards:

# Documentation collection
- name: 'docs'
  label: 'Documentation'
  folder: 'content/docs'
  create: true
  fields:
    - label: 'Title'
      name: 'title'
      widget: 'string'
      required: true
    - label: 'Description'
      name: 'description'
      widget: 'text'
      required: true
    - label: 'Category'
      name: 'category'
      widget: 'select'
      options:
        ['getting-started', 'usage', 'examples', 'reference', 'community']
      required: true
    - label: 'Order'
      name: 'order'
      widget: 'number'
      value_type: 'int'
      min: 1
      max: 100
      default: 10
    - label: 'Show TOC'
      name: 'showToc'
      widget: 'boolean'
      default: true
    - label: 'Body'
      name: 'body'
      widget: 'markdown'
      required: true
      hint:
        'Use brand-compliant formatting: headings with ## and ###, code blocks
        with proper language tags'

# Changelog collection with semantic versioning
- name: 'changelog'
  label: 'Changelog'
  folder: 'content/changelog'
  create: true
  slug: 'v{{version}}'
  fields:
    - label: 'Version'
      name: 'version'
      widget: 'string'
      required: true
      pattern: ["^\\d+\\.\\d+\\.\\d+", 'Must be semantic version (e.g., 1.0.0)']
    - label: 'Type'
      name: 'type'
      widget: 'select'
      options: ['major', 'minor', 'patch', 'hotfix']
      required: true
    - label: 'Breaking Changes'
      name: 'breaking'
      widget: 'boolean'
      default: false
    - label: 'Security Updates'
      name: 'security'
      widget: 'boolean'
      default: false
    - label: 'Body'
      name: 'body'
      widget: 'markdown'
      required: true
      hint: 'Use structured format: ## Added, ## Changed, ## Fixed, ## Removed'

Editorial Workflow: Quality Through Process

Git-Based Review Process

One of our biggest wins was integrating content review into our existing Git workflow:

# Enable editorial workflow
publish_mode: editorial_workflow

This creates a process where:

  1. Content creators write and submit content through the CMS
  2. Draft created as a pull request in Git
  3. Technical review happens alongside content review
  4. Brand validation runs automatically in CI
  5. Content published when PR is merged

Automated Brand Checking in CI

We extended our CI pipeline to validate content:

# .github/workflows/content-validation.yml
name: Content Validation

on:
  pull_request:
    paths:
      - 'content/**'

jobs:
  validate-content:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Validate Brand Compliance
        run: |
          # Check for brand color usage
          if grep -r "#[0-9a-fA-F]\{6\}" content/ --exclude-dir=.git; then
            echo "Found hardcoded colors. Use brand color variables instead."
            exit 1
          fi

          # Validate image alt text
          if grep -r "!\[\s*\]" content/; then
            echo "Found images without alt text. All images must have descriptive alt text."
            exit 1
          fi

          # Check heading hierarchy
          for file in $(find content/ -name "*.md"); do
            python scripts/validate-headings.py "$file"
          done

      - name: Spell Check
        run: |
          npm install -g cspell
          cspell "content/**/*.md"

      - name: Generate Preview
        run: |
          npm run build
          npm run generate-preview-images

Content Creator Experience: Making Brand Compliance Easy

Guided Content Creation

Our CMS interface guides content creators toward brand-compliant content:

// Custom widget for brand-compliant image selection
const BrandImageWidget = ({ value, onChange }) => {
  const [selectedImage, setSelectedImage] = useState(value)
  const [altText, setAltText] = useState('')

  return h(
    'div',
    { className: 'brand-image-widget' },
    h(
      'div',
      { className: 'image-selection' },
      h('input', {
        type: 'file',
        accept: 'image/*',
        onChange: e => {
          const file = e.target.files[0]
          if (file) {
            // Validate image dimensions and format
            validateBrandImage(file).then(isValid => {
              if (isValid) {
                setSelectedImage(URL.createObjectURL(file))
                onChange(file.name)
              } else {
                alert(
                  'Image must be high-quality (1200x630px recommended) following brand aesthetics'
                )
              }
            })
          }
        },
      })
    ),
    selectedImage &&
      h('img', {
        src: selectedImage,
        alt: altText,
        className: 'preview-image',
      }),
    h('input', {
      type: 'text',
      placeholder: 'Alt text (required for accessibility)',
      value: altText,
      onChange: e => setAltText(e.target.value),
      required: true,
    }),
    h(
      'div',
      { className: 'brand-guidelines' },
      h('p', {}, 'Image Guidelines:'),
      h(
        'ul',
        {},
        h('li', {}, '1200x630px recommended for social sharing'),
        h('li', {}, 'Use brand colors and clean, professional aesthetics'),
        h('li', {}, 'Include descriptive alt text for accessibility')
      )
    )
  )
}

CMS.registerWidget('brand-image', BrandImageWidget)

Real-Time Brand Validation

Content creators get immediate feedback on brand compliance:

// Real-time validation as users type
const BrandContentWidget = ({ value, onChange }) => {
  const [content, setContent] = useState(value || '')
  const [validationErrors, setValidationErrors] = useState([])

  const validateContent = useCallback(
    debounce(text => {
      const errors = []

      // Check brand colors
      const colorError = brandValidation.validateBrandColors(text)
      if (colorError) errors.push(colorError)

      // Check heading hierarchy
      const headingError = brandValidation.validateHeadingHierarchy(text)
      if (headingError) errors.push(headingError)

      // Check alt text
      const altTextError = brandValidation.validateImageAltText(text)
      if (altTextError) errors.push(altTextError)

      setValidationErrors(errors)
    }, 500),
    []
  )

  const handleChange = newContent => {
    setContent(newContent)
    onChange(newContent)
    validateContent(newContent)
  }

  return h(
    'div',
    { className: 'brand-content-widget' },
    h('textarea', {
      value: content,
      onChange: e => handleChange(e.target.value),
      className: validationErrors.length > 0 ? 'has-errors' : '',
      placeholder: 'Write your content here...',
    }),
    validationErrors.length > 0 &&
      h(
        'div',
        { className: 'validation-errors' },
        h('h4', {}, 'Brand Compliance Issues:'),
        h(
          'ul',
          {},
          validationErrors.map((error, index) =>
            h('li', { key: index, className: 'error-message' }, error)
          )
        )
      )
  )
}

Advanced Content Features

Automated SEO Optimization

Every piece of content gets automatic SEO optimization:

// Auto-generate SEO fields based on content
const generateSEOFields = entry => {
  const title = entry.get('title')
  const body = entry.get('body')

  // Auto-generate description from first paragraph
  const firstParagraph = body.split('\n\n')[0].replace(/[#*`]/g, '').trim()
  const description =
    firstParagraph.length > 160
      ? firstParagraph.substring(0, 157) + '...'
      : firstParagraph

  // Auto-generate keywords from headings and emphasis
  const headings = body.match(/^#+\s(.+)$/gm) || []
  const emphasized = body.match(/\*\*([^*]+)\*\*/g) || []
  const keywords = [...headings, ...emphasized]
    .map(text => text.replace(/[#*]/g, '').trim().toLowerCase())
    .slice(0, 10)

  return {
    seoTitle: title.length > 60 ? title.substring(0, 57) + '...' : title,
    seoDescription: description,
    keywords: keywords.join(', '),
  }
}

// Hook into content save to auto-populate SEO fields
CMS.registerEventListener({
  name: 'prePublish',
  handler: ({ entry }) => {
    const seoFields = generateSEOFields(entry.get('data'))
    return entry.get('data').merge(seoFields)
  },
})

Content Analytics Integration

We track content performance to inform content strategy:

// Track content engagement
const trackContentEngagement = entry => {
  // This would integrate with your analytics system
  analytics.track('Content Published', {
    title: entry.get('title'),
    category: entry.get('category'),
    author: entry.get('author'),
    wordCount: entry.get('body').split(' ').length,
    readingTime: entry.get('readingTime'),
    tags: entry.get('tags'),
  })
}

Results: Brand Consistency at Scale

Quantifiable Brand Compliance

After implementing our CMS system, we tracked brand compliance across all content:

  • 100% of blog posts use correct brand colors
  • 100% of images include proper alt text
  • 95% of content follows proper heading hierarchy
  • 100% of content passes automated brand validation
  • 0 manual brand review failures in 6 months

Content Creator Satisfaction

Our content creators report significantly improved experience:

  • 75% faster content creation process
  • 90% fewer brand compliance issues
  • 100% confident their content will pass review
  • Easy to use real-time validation and preview

SEO Performance Improvement

Automated SEO optimization delivered measurable results:

  • 40% improvement in search engine rankings
  • 60% increase in organic traffic to documentation
  • 100% of pages have optimized meta descriptions
  • 95% of pages score 100/100 on Lighthouse SEO audit

Challenges and Solutions

Challenge 1: Balancing Freedom and Control

Problem: Content creators wanted creative freedom, but brand standards required constraints.

Solution: We made brand compliance the path of least resistance:

// Instead of restricting options, we made good choices easier
const ColorPicker = ({ onChange }) => {
  const brandColors = [
    { name: 'Brand Dark', value: '#1A1A1A', usage: 'Primary text, headers' },
    {
      name: 'Brand Light',
      value: '#F7F7F7',
      usage: 'Backgrounds, subtle elements',
    },
    {
      name: 'Brand Accent',
      value: '#2196F3',
      usage: 'Links, CTAs, highlights',
    },
    {
      name: 'Brand Muted',
      value: '#757575',
      usage: 'Secondary text, captions',
    },
  ]

  return h(
    'div',
    { className: 'brand-color-picker' },
    h('h4', {}, 'Brand Colors'),
    brandColors.map(color =>
      h(
        'button',
        {
          key: color.value,
          className: 'color-option',
          style: { backgroundColor: color.value },
          onClick: () => onChange(color.value),
          title: `${color.name}: ${color.usage}`,
        },
        color.name
      )
    ),
    h(
      'div',
      { className: 'usage-guide' },
      h('p', {}, 'Choose brand colors for consistency and accessibility')
    )
  )
}

Challenge 2: Performance with Rich Content

Problem: Rich content editing and real-time preview could slow down the interface.

Solution: We implemented smart loading and caching:

// Debounced preview updates
const usePreviewOptimization = () => {
  const [previewContent, setPreviewContent] = useState('')

  const updatePreview = useCallback(
    debounce(content => {
      setPreviewContent(content)
    }, 300),
    []
  )

  return { previewContent, updatePreview }
}

// Lazy load preview components
const LazyPreview = lazy(() => import('./PreviewComponents'))

Challenge 3: Content Migration

Problem: How do you migrate existing content to the new system without losing quality?

Solution: We built migration tools with brand validation:

// Content migration script with brand compliance checking
const migrateContent = async oldContent => {
  // Parse existing content
  const parsed = parseMarkdown(oldContent)

  // Apply brand compliance fixes
  const brandCompliant = await applyBrandFixes(parsed)

  // Validate before migration
  const validationErrors = validateBrandCompliance(brandCompliant)
  if (validationErrors.length > 0) {
    console.warn(`Content needs manual review: ${validationErrors.join(', ')}`)
    return { content: brandCompliant, needsReview: true }
  }

  return { content: brandCompliant, needsReview: false }
}

What's Next: Phase 6 - SEO and Performance

With our content management system empowering creators while maintaining brand standards, Phase 6 will focus on making sure all this content is discoverable and fast. We'll cover:

  • Advanced SEO optimization with automatic sitemap generation
  • Performance optimization for content-heavy pages
  • Social media integration with auto-generated share images
  • Analytics and insights for content performance tracking

Key Content Management Takeaways

  1. Brand Guidelines as Code: Translate subjective design requirements into automated validation
  2. Make Compliance Easy: The right choice should be the easiest choice
  3. Real-Time Feedback: Give content creators immediate guidance, not post-creation criticism
  4. Workflow Integration: Content review should fit naturally into development processes
  5. Empower Through Structure: Constraints can be liberating when they prevent problems

Building a brand-compliant CMS taught us that good systems make good outcomes inevitable. When you build brand standards into the tools themselves, maintaining consistency becomes automatic rather than effortful.


Next time, we'll explore how to make all this carefully crafted content discoverable through advanced SEO optimization and performance tuning.

Try our CMS: Content creators can edit at admin.godegit.devSee the results: All our brand-compliant content at godegit.dev

Tagged with

dev-diarycmscontent-managementdecap-cmsbrand-guidelinesworkflow

Share this post

godegit Development Team

godegit Development Team

The engineering team behind godegit, exploring the intersection of content management, brand consistency, and developer workflows.

Related Posts