OptimoCMSDocs
Guides

CI/CD pipeline

Automatiseer content updates en publicatie met GitHub Actions en de OptimoCMS SDK. Van staging naar productie in één push.

CI/CD pipeline met GitHub Actions

Automatiseer je OptimoCMS workflow met GitHub Actions. Push content-wijzigingen naar een repository en laat de pipeline automatisch je site bijwerken en publiceren.

In deze tutorial:

  • GitHub Actions workflow instellen
  • Content updaten via de SDK in CI
  • Staging (sandbox) → productie flow
  • Automatisch publiceren bij merge naar main

Vereisten

  • GitHub repository met je content/configuratie
  • OptimoCMS API key (opgeslagen als GitHub Secret)
  • Optioneel: een sandbox-site voor staging

Stap 1 — GitHub Secrets instellen

Ga naar je repository → SettingsSecrets and variablesActions en voeg toe:

SecretWaarde
OPTIMOCMS_API_KEYJe live API key
OPTIMOCMS_SITE_IDJe productie site ID
OPTIMOCMS_SANDBOX_KEY(optioneel) Sandbox API key voor staging
OPTIMOCMS_SANDBOX_SITE_ID(optioneel) Sandbox site ID

Stap 2 — Content als code

Structureer je content in de repository:

content/
├── pages/
│   ├── home.json
│   ├── about.json
│   └── menu.json
├── design-tokens.json
└── navigation.json

Voorbeeld content/pages/home.json:

{
  "title": "Home",
  "slug": "home",
  "blocks": [
    {
      "type": "Hero",
      "props": {
        "title": "Welkom bij ons restaurant",
        "subtitle": "De beste Italiaanse keuken sinds 1985",
        "ctaText": "Reserveer nu",
        "ctaLink": "/reserveren"
      }
    },
    {
      "type": "FeaturedMenu",
      "props": {
        "title": "Chef's aanbeveling",
        "items": ["bruschetta", "risotto", "tiramisu"]
      }
    }
  ]
}

Stap 3 — Deploy script

Maak een scripts/deploy.ts die content naar OptimoCMS pushed:

import { OptimoCMS } from '@optimocms/sdk';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';

const client = new OptimoCMS({
  apiKey: process.env.OPTIMOCMS_API_KEY!,
});

const siteId = process.env.OPTIMOCMS_SITE_ID!;

interface PageContent {
  title: string;
  slug: string;
  blocks: Array<{ type: string; props: Record<string, unknown> }>;
  seo?: { title?: string; description?: string };
}

async function deployPages(): Promise<void> {
  const pagesDir = join(process.cwd(), 'content', 'pages');
  const files = readdirSync(pagesDir).filter(f => f.endsWith('.json'));

  console.log(`Deploying ${files.length} pagina's...`);

  for (const file of files) {
    const content: PageContent = JSON.parse(
      readFileSync(join(pagesDir, file), 'utf-8')
    );

    // Check of pagina al bestaat
    const existing = await client.pages.list(siteId, { status: 'published' });
    const existingPage = existing.data.find(p => p.slug === content.slug);

    if (existingPage) {
      await client.pages.update(siteId, existingPage.id, {
        title: content.title,
        blocks: content.blocks,
        seo: content.seo,
      });
      console.log(`  ✓ Updated: ${content.title} (${content.slug})`);
    } else {
      await client.pages.create(siteId, {
        title: content.title,
        slug: content.slug,
        status: 'published',
        blocks: content.blocks,
        seo: content.seo,
      });
      console.log(`  ✓ Created: ${content.title} (${content.slug})`);
    }
  }
}

async function deployDesignTokens(): Promise<void> {
  const tokensFile = join(process.cwd(), 'content', 'design-tokens.json');

  try {
    const tokens = JSON.parse(readFileSync(tokensFile, 'utf-8'));
    await client.designTokens.update(siteId, tokens);
    console.log('  ✓ Design tokens bijgewerkt');
  } catch {
    console.log('  ⏭ Geen design-tokens.json gevonden, overgeslagen');
  }
}

async function deployNavigation(): Promise<void> {
  const navFile = join(process.cwd(), 'content', 'navigation.json');

  try {
    const nav = JSON.parse(readFileSync(navFile, 'utf-8'));
    await client.navigation.update(siteId, nav);
    console.log('  ✓ Navigatie bijgewerkt');
  } catch {
    console.log('  ⏭ Geen navigation.json gevonden, overgeslagen');
  }
}

async function main(): Promise<void> {
  console.log('🚀 OptimoCMS deploy gestart\n');

  await deployPages();
  await deployDesignTokens();
  await deployNavigation();

  // Publiceer de site
  const result = await client.sites.publish(siteId);
  console.log(`\n✓ Site gepubliceerd (${result.status})`);
}

main().catch(error => {
  console.error('Deploy mislukt:', error.message);
  process.exit(1);
});

Stap 4 — GitHub Actions workflow

Maak .github/workflows/deploy-content.yml:

name: Deploy Content to OptimoCMS

on:
  push:
    branches: [main]
    paths:
      - 'content/**'
  workflow_dispatch:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.event_name == 'workflow_dispatch' || github.ref != 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Deploy to Sandbox
        env:
          OPTIMOCMS_API_KEY: ${{ secrets.OPTIMOCMS_SANDBOX_KEY }}
          OPTIMOCMS_SITE_ID: ${{ secrets.OPTIMOCMS_SANDBOX_SITE_ID }}
        run: npx tsx scripts/deploy.ts

  deploy-production:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Deploy to Production
        env:
          OPTIMOCMS_API_KEY: ${{ secrets.OPTIMOCMS_API_KEY }}
          OPTIMOCMS_SITE_ID: ${{ secrets.OPTIMOCMS_SITE_ID }}
        run: npx tsx scripts/deploy.ts

      - name: Notify success
        if: success()
        run: echo "✓ Content deployed to production"

      - name: Notify failure
        if: failure()
        run: echo "✗ Deploy failed — check logs"

Stap 5 — Staging → Productie flow

Gebruik een PR-gebaseerde workflow met sandbox preview:

name: Preview on PR

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

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Deploy to Sandbox
        env:
          OPTIMOCMS_API_KEY: ${{ secrets.OPTIMOCMS_SANDBOX_KEY }}
          OPTIMOCMS_SITE_ID: ${{ secrets.OPTIMOCMS_SANDBOX_SITE_ID }}
        run: npx tsx scripts/deploy.ts

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## 🔍 Content Preview\n\nDe wijzigingen zijn gedeployd naar de sandbox:\n\n**Preview:** https://${process.env.SANDBOX_SUBDOMAIN}.sandbox.optimocms.com\n\nMerge deze PR om naar productie te deployen.`
            });

Workflow overzicht

Feature branch         PR                    Main branch
     │                  │                        │
     │  push content/   │                        │
     ├─────────────────►│  deploy to sandbox     │
     │                  ├───────────────────►     │
     │                  │  preview link in PR     │
     │                  │                        │
     │                  │  merge PR              │
     │                  ├───────────────────────►│
     │                  │                        │  deploy to production
     │                  │                        ├───────────────────►

Optioneel: Content validatie

Voeg een validatie-stap toe die controleert of JSON-bestanden geldig zijn:

// scripts/validate.ts
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';

const pagesDir = join(process.cwd(), 'content', 'pages');
const files = readdirSync(pagesDir).filter(f => f.endsWith('.json'));
let errors = 0;

for (const file of files) {
  try {
    const content = JSON.parse(readFileSync(join(pagesDir, file), 'utf-8'));

    if (!content.title) throw new Error('Missing "title"');
    if (!content.slug) throw new Error('Missing "slug"');
    if (!Array.isArray(content.blocks)) throw new Error('"blocks" must be an array');

    console.log(`✓ ${file}`);
  } catch (err) {
    console.error(`✗ ${file}: ${err instanceof Error ? err.message : err}`);
    errors++;
  }
}

if (errors > 0) {
  console.error(`\n${errors} bestand(en) met fouten`);
  process.exit(1);
}

console.log(`\n✓ Alle ${files.length} bestanden valide`);

Voeg toe aan je workflow vóór de deploy-stap:

- name: Validate content
  run: npx tsx scripts/validate.ts

Volgende stappen

On this page