Optimizely to Storyblok Migration CLI

Surjit

Surjit Bharath

Founder of Hidden Foundry

Storyblok Architect ~ Optimizely Most Valued Professional (OMVP) ~ CMS and Commerce certified

Contact Me

🧭 cms2storyblok — Optimizely ➜ Storyblok Migration CLI

NuGet Badge .NET Badge License Badge

A cross-platform CLI that helps you migrate Optimizely content and schemas into Storyblok — automatically and safely.

When moving a customer from one CMS to another, developers often find themselves repeating the same tedious setup work—cataloging content types, recreating fields, aligning validators, and wiring up relationships. These manual steps are time-consuming, error-prone, and inconsistent across environments.

To reduce the amount of repetitive developer effort required, I’ve created a cross-platform CLI that helps teams migrate from Optimizely to Storyblok. We’re rolling this out in phases: Phase 1 focuses on migrating content types (component schemas), while future phases will extend to content (entries) migration and beyond.


UPDATED: 14/10/2025

Export Pages and Blocks from Optimizely and push them into Storyblok.

 

What it does today (Phase 2)

Schema

  • Pulls content type definitions from Optimizely (Content Definitions API).

  • Saves them as a standardized JSON snapshot

  • Pushes those schemas into Storyblok as components/fields.

  • Safe to re‑run: only creates/updates what’s different.

Content

  • Pulls pages and blocks from Optimizely.

  • Saves them as standardized NDJSON files for consistency and easy diffing.

  • Pushes those entries into Storyblok, recreating structure and components.

  • Relationship fields and page/block linking are in progress.

  • Media asset handling (images, documents, etc.) is also in progress.



✨ Overview

  • Packaged as a .NET Global Tool for Windows, macOS, and Linux
  • Designed for phased migration:
    • Phase 1: Schema migration (content types)
    • Phase 2: Page & block content migration
    • Phase 3: Relationships and media assets (in progress)
  • Uses a neutral DiscoveredSchema model so other CMS sources can be added later
  • Built with System.CommandLine and Serilog

💡 Tip: Run cms2storyblok --help or cms2storyblok <command> --help for current flags.



⚙️ Requirements

  • .NET SDK 9.0 (check via dotnet --info)
  • Access to both APIs:
    • Optimizely Content Definitions API (base URL + token)
    • Storyblok Management API (space + token)


📦 Install

Stable

dotnet tool install -g cms2storyblok

Pre-release (beta builds)

dotnet tool install -g cms2storyblok --prerelease

On macOS/Linux ensure the .NET tools path is on your PATH:

export PATH="$HOME/.dotnet/tools:$PATH"

Update

dotnet tool update -g cms2storyblok --prerelease

Uninstall

dotnet tool uninstall -g cms2storyblok


🚀 Quick Start

1️⃣ Export schemas from Optimizely

macOS/Linux (zsh/bash)

export OPTI_BASE_URL="https://<your-opti-host>/api"
export OPTI_TOKEN="<your-opti-api-token>"

cms2storyblok optimizely-schema \
  --base-url "$OPTI_BASE_URL" \
  --token "$OPTI_TOKEN"

Windows (PowerShell)

$env:OPTI_BASE_URL = "https://<your-opti-host>/api"
$env:OPTI_TOKEN    = "<your-opti-api-token>"

cms2storyblok optimizely-schema `
  --base-url $env:OPTI_BASE_URL `
  --token    $env:OPTI_TOKEN

This writes timestamped files like:

./schemas/OptimizelySchema_yyyyMMdd_HHmmss.json


2️⃣ Push schemas to Storyblok

macOS/Linux

export STORYBLOK_MANAGEMENT_TOKEN="<your-storyblok-token>"
export STORYBLOK_SPACE_ID="<your-space-id>"

cms2storyblok schema:push \
  --input ./schemas/OptimizelySchema_20250911_114501.json \
  --space "$STORYBLOK_SPACE_ID" \
  --token "$STORYBLOK_MANAGEMENT_TOKEN"

Windows

$env:STORYBLOK_MANAGEMENT_TOKEN = "<your-storyblok-token>"
$env:STORYBLOK_SPACE_ID         = "<your-space-id>"

cms2storyblok schema:push `
  --input .\schemas\OptimizelySchema_20250911_114501.json `
  --space $env:STORYBLOK_SPACE_ID `
  --token $env:STORYBLOK_MANAGEMENT_TOKEN


3️⃣ Export content (pages + blocks) from Optimizely

macOS/Linux

export OPTI_BASE_URL="https://www.hiddenfoundry.com/"
cms2storyblok optimizely-get-content \
  --base-url "$OPTI_BASE_URL"

Windows

$env:OPTI_BASE_URL = "https://www.hiddenfoundry.com/"
cms2storyblok optimizely-get-content `
  --base-url $env:OPTI_BASE_URL

This command exports pages and blocks from Optimizely and saves them as NDJSON:

./export/content_pages.ndjson
./export/content_blocks.ndjson

You can edit these files before pushing them to Storyblok.



4️⃣ Push pages into Storyblok

macOS/Linux

export STORYBLOK_SPACE_ID="<your-space-id>"
export STORYBLOK_MANAGEMENT_TOKEN="<your-storyblok-token>"

cms2storyblok storyblok-push-pages-content \
  --ndjson ./export/content_pages.ndjson \
  --space-id "$STORYBLOK_SPACE_ID" \
  --token "$STORYBLOK_MANAGEMENT_TOKEN"

Windows

$env:STORYBLOK_SPACE_ID = "<your-space-id>"
$env:STORYBLOK_MANAGEMENT_TOKEN = "<your-storyblok-token>"

cms2storyblok storyblok-push-pages-content `
  --ndjson "C:\temp\export\content_pages.ndjson" `
  --space-id $env:STORYBLOK_SPACE_ID `
  --token $env:STORYBLOK_MANAGEMENT_TOKEN

5️⃣ Push blocks into Storyblok

macOS/Linux

export STORYBLOK_SPACE_ID="<your-space-id>"
export STORYBLOK_MANAGEMENT_TOKEN="<your-storyblok-token>"

cms2storyblok storyblok-push-blocks-content \
  --ndjson ./export/content_blocks.ndjson \
  --space-id "$STORYBLOK_SPACE_ID" \
  --token "$STORYBLOK_MANAGEMENT_TOKEN"

Windows

$env:STORYBLOK_SPACE_ID = "<your-space-id>"
$env:STORYBLOK_MANAGEMENT_TOKEN = "<your-storyblok-token>"

cms2storyblok storyblok-push-blocks-content `
  --ndjson "C:\temp\export\content_blocks.ndjson" `
  --space-id $env:STORYBLOK_SPACE_ID `
  --token $env:STORYBLOK_MANAGEMENT_TOKEN

💡 Tip: Add --dry-run to preview any command without writing to Storyblok.



💡 Example Workflow (All Phases)

# 1. Export schemas from Optimizely
cms2storyblok optimizely-schema --base-url "https://<your-opti-host>/api"

# 2. Push schemas into Storyblok
cms2storyblok schema:push --input ./schemas/OptimizelySchema_*.json \
  --space <your-space-id> --token <your-storyblok-token>

# 3. Export content (pages + blocks) from Optimizely
cms2storyblok optimizely-get-content --base-url "https://www.hiddenfoundry.com/"

# 4. Push pages into Storyblok
cms2storyblok storyblok-push-pages-content \
  --ndjson ./export/content_pages.ndjson \
  --space-id <your-space-id> --token <your-storyblok-token>

# 5. Push blocks into Storyblok
cms2storyblok storyblok-push-blocks-content \
  --ndjson ./export/content_blocks.ndjson \
  --space-id <your-space-id> --token <your-storyblok-token>


⚠️ Notes & Best Practices

  • Run commands in order. Schemas must exist before content is pushed.
  • Use --dry-run first to safely test changes.
  • Version exports. Keep dated folders (e.g. /schemas_2025-10-14/) for diffs and rollback.
  • Review NDJSON before pushing if you want to adjust mappings.
  • Backup Storyblok spaces before large imports.
  • Relationships & media assets are in progress — future updates will handle nested blocks and referenced files automatically.


🪵 Logging & Exit Codes

  • Non-zero exit codes indicate network/auth/validation failures.
  • CI pipelines can rely on exit codes for success/failure logic.


🤝 Contributing

Issues and PRs welcome. Please include:

  • OS + dotnet --info
  • Command used
  • Redacted logs/errors

📧 [email protected]



🪪 License

MIT

;