Skip to content

Magnolia CMS to Caisy Migration: A Technical Guide

A technical guide to migrating from Magnolia CMS to Caisy, covering JCR extraction, blueprint mapping, asset migration via tus.io, and rich text AST conversion.

Wahab Wahab · · 21 min read
Magnolia CMS to Caisy Migration: A Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Migrating from Magnolia CMS to Caisy means moving from a Java-based, JCR-backed enterprise CMS with deep page-template coupling and hierarchical content trees to a lightweight, GraphQL-first headless CMS built around flat blueprints, documents, and components.

The core challenge: Magnolia stores everything in a hierarchical Java Content Repository (JCR) as nested nodes — pages containing areas containing components. Caisy has no concept of pages, areas, or template-driven rendering. Every content entry in Caisy is a document or component created from a blueprint, and the content model must be redesigned before a single record moves.

There is no native export-import pathway between these systems. Migrating requires building a custom ETL pipeline that flattens Magnolia's node trees, translates WYSIWYG HTML into Caisy's rich text Abstract Syntax Tree (AST), and re-maps internal UUID references across a completely new schema.

Much like teams migrating away from other enterprise Java platforms, teams make this move to shed Java/Tomcat infrastructure overhead, gain a modern GraphQL API, and give frontend teams full stack choice. The trade-off is real: you lose Magnolia's in-context editing, visual page builder, server-side rendering, and multi-workspace JCR architecture. What you gain is a clean API surface, reusable content models, and zero backend ops.

This guide covers extraction from Magnolia's REST and JCR export tools, content model translation into Caisy blueprints, programmatic import via Caisy's Internal API and TypeScript SDK, asset migration via tus.io, rich text AST conversion, and the edge cases that break naive migrations.

Scope: This guide targets Magnolia CMS 6.x (specifically 6.2+). Magnolia 5.x differs in Delivery API availability and DataTransporter signatures. Magnolia Cloud deployments may restrict direct JCR access — verify with your cloud plan before attempting Groovy-based exports.

Warning

Key constraint: Magnolia's JCR tree structure — pages → areas → components — does not map 1:1 to Caisy. You must flatten and restructure your content model before import. Caisy's External API supports mutations only when explicitly enabled per blueprint, and all documents created via mutation land in draft state.

Magnolia vs. Caisy: Architectural Differences That Shape the Migration

Before writing extraction scripts, understand the fundamental data model gap.

Magnolia CMS is a Java Web application running on a Jakarta Servlet Container (usually Tomcat), commonly deployed as a Docker container. Magnolia stores data in a Java Content Repository (JCR) — a hierarchical object database particularly well-suited for content. The data is structured as a tree of nodes. Each node has one or more types; the type defines what properties the node has, the number and type of its child nodes, and its behavior.

Content lives in workspaces: website for pages, dam for digital assets, and custom workspaces for each content type you define. Magnolia's primary node types include mgnl:page, mgnl:area, mgnl:component, and mgnl:asset. A typical page node contains nested area nodes, which contain component nodes — this hierarchy is the page layout.

Caisy is a developer-friendly, API-first headless CMS with a GraphQL API, intuitive content modeling, and a focus on performance for modern web development workflows. Content structure is defined by blueprints — custom templates or models for different content types that define what data each entry stores and manages. By using blueprints, you create flexible, reusable content models.

There is no page hierarchy, no area concept, and no template binding in Caisy. The frontend consumes content via GraphQL and decides how to render it.

Concept Magnolia CMS Caisy
Data store JCR (Apache Jackrabbit) Managed cloud database
Content unit Node (page, area, component) Document / Component
Schema definition Content type YAML + JCR node types Blueprint
API REST Delivery API v2 + GraphQL + Nodes API GraphQL (External + Internal)
Asset storage dam JCR workspace (binary in nodes) Managed CDN, tus.io upload
Rendering Server-side (FreeMarker/JSP) or headless Headless only
Hierarchy Deep tree (page → area → component) Flat (documents with references)
i18n Locale-specific properties on same node Per-locale document variants
Publishing Author/public instance replication Draft → Published states
Rich text storage Raw HTML string (CKEditor) ProseMirror-compatible JSON AST

On the Magnolia side, the Pages app operates on the website workspace and mgnl:page nodes. Magnolia's GraphQL module can expose registered content types, but it cannot retrieve content from the Pages or Stories apps — for full page retrieval, use the REST Delivery endpoint. On the Caisy side, API queries are generated from each blueprint's API ID, which means your target schema naming is not cosmetic; it becomes part of your frontend contract.

Step 1: Audit and Map Your Magnolia Content Model

Before extracting anything, catalog every content type, workspace, and template in use.

What to inventory

  • Workspaces: website, dam, and every custom workspace created by content type definitions. The Content Types module allows defining Magnolia content types in a single YAML file of a light module. Each content type typically creates its own workspace.
  • Node types in use: mgnl:page, mgnl:area, mgnl:component, mgnl:asset, and any custom node types.
  • Properties per content type: The model describes properties such as name, description, asset, and date. In the JCR implementation, property types include String, Boolean, Decimal, Double, Long, Date, asset, richText, another content type, or a submodel.
  • Cross-workspace references: Pages referencing DAM assets, content types referencing other content types.
  • Templates: Which templates are bound to which page types. These have no Caisy equivalent but inform how you structure blueprints.
  • Localization: How many locales; whether content is locale-specific at the property level or relies on fallback behavior.
  • URL rules and redirects: In Magnolia, a page's name and position in the hierarchy determine its URL. That path history belongs in your migration dataset.
  • Scheduled publication: If Magnolia has scheduled content publication in use, identify those items explicitly — Caisy supports scheduling, but there is no automated transfer of scheduled states.

Mapping Magnolia concepts to Caisy blueprints

The translation requires deliberate design decisions:

  • Magnolia page type → Caisy document blueprint (e.g., "Page", "Article", "Landing Page"). Strip template binding; the blueprint holds content fields only.
  • Magnolia component → Caisy component blueprint. Reusable pieces connected to documents. In Caisy, component blueprints can be embedded within documents but are not independently queryable via all... queries in the same way document blueprints are.
  • Magnolia area → No direct equivalent. If an area groups multiple components, model this as a connection field (list of component references) on the parent document blueprint. Magnolia supports single, list, and noComponent area types — do not flatten all three into one generic array. Model single areas as singleton fields, list areas as repeatable component connection fields, and noComponent areas (pure structural zones like htmlHeader) as separate linked documents or omit them if they are purely layout artifacts.
  • Magnolia submodel → Caisy component or nested field group within a blueprint.
  • DAM asset → Caisy asset (system blueprint). A single blueprint already exists for Caisy assets as a system blueprint. You can extend this blueprint but cannot delete it or its required fields. This blueprint is mandatory for upload and asset handling to work.

If your Magnolia site relies on inherited areas or shared headers/footers from parent pages, treat that as a redesign item. Magnolia can automatically render components inherited from parent pages when inheritance is enabled on an area. Caisy gives you reusable components and linked documents, but it does not recreate Magnolia inheritance; your frontend and content model have to make that behavior explicit.

Tip

Two design rules that prevent downstream breakage: First, freeze blueprint API names before importing production data. Caisy uses the API name to generate GraphQL operations, and renaming a blueprint or field after import can break frontend queries even when document IDs stay stable. Second, only mark a blueprint as unique when it is truly singleton content (e.g., global navigation or footer). Pagination, filtering, and sorting are available only on all... queries for non-unique blueprints, and unique documents do not allow mutations via the External API.

Step 2: Extract Content from Magnolia

Magnolia offers three primary extraction paths. Choose based on your technical access and content volume.

The Delivery API v2 provides more flexibility than v1: you can define multiple endpoint configurations, deliver localized content, and resolve references to nodes of other workspaces including assets and asset renditions.

Configure a delivery endpoint in a Magnolia light module:

# light-modules/migration-export/restEndpoints/delivery/all-pages.yaml
$type: jcrDeliveryEndpoint_v2
workspace: website
depth: 10
nodeTypes:
  - mgnl:page
childNodeTypes:
  - mgnl:area
  - mgnl:component
references:
  - name: assetReference
    propertyName: image
    referenceResolver:
      class: info.magnolia.rest.reference.dam.AssetReferenceResolverDefinition

Then query it:

curl -u superuser:superuser \
  "http://localhost:8080/magnoliaAuthor/.rest/delivery/all-pages?limit=50&offset=0&lang=all"

The lang=all parameter returns all locale variants in a single request. This returns fully resolved JSON including nested components and DAM asset metadata. For each custom content type workspace, create a separate endpoint.

Limitations:

  • The Delivery API is read-only (HTTP GET only).
  • Pagination is via offset and limit parameters; Magnolia defaults to 50 results per request. Large sites need pagination loops with checkpoint logging.
  • Binary asset data is not returned inline — you get metadata and asset links and must download binaries separately.
  • Endpoint paths are project-specific (derived from your endpoint configuration file names). Script against your actual configured endpoint names.

Your extraction script must recursively traverse the returned JSON, identifying every object with a @nodeType of mgnl:component and logging its @id (the Magnolia JCR UUID). You will need these UUIDs to resolve internal links during the two-pass import described in Step 6.

Option B: JCR XML/YAML export (best for full workspace dumps, Magnolia 6.x)

Magnolia provides functions to import data into a JCR workspace or export data from a JCR workspace to a file. The Exporter can write JCR content to XML or YAML.

Use the JCR Tools app or Groovy scripting console to export entire workspaces. The following example uses DataTransporter as available in Magnolia 6.x — method signatures differ in 5.x:

// Groovy script — verified against Magnolia 6.2 DataTransporter API
// Execute in the Magnolia Groovy Console (Tools > Groovy Console)
import info.magnolia.importexport.DataTransporter
import javax.jcr.Session
 
Session session = ctx.getJCRSession('website')
def outputStream = new FileOutputStream('/tmp/website-export.xml')
 
// Parameters: outputStream, session, basePath, workspace,
//             keepHistory, keepNodeIds, format
DataTransporter.executeExport(
  outputStream,
  session,
  '/',          // root path
  true,         // keep history
  true,         // keep node IDs (critical for UUID mapping)
  DataTransporter.XML
)
outputStream.close()

Version note: The DataTransporter.executeExport signature changed between Magnolia 5.x and 6.x. In 5.x, the method accepts a different parameter order. Consult the Javadoc for your specific Magnolia version before executing. Run this in a non-production author instance first.

Magnolia supports JCR System View XML format. YAML exports are smaller and human-readable, but binary data cannot be exported to YAML — handle media as a separate download stream using the DAM approach in Step 4.

Set keepNodeIds: true (the boolean parameter) to preserve JCR UUIDs in the export. These UUIDs are your cross-reference keys throughout the migration.

Option C: CSV export via Content Exporter module

The Content Exporter module provides CSV import and export functionality. Useful for simple content types with flat property structures, but it loses hierarchical relationships between pages, areas, and components. Not suitable as a substitute for page-level migration.

When to use the Nodes API

If your Magnolia instance has custom JCR properties not exposed via the Delivery endpoint, fall back to the Nodes API (/.rest/nodes/v1/website/). This returns raw JCR data including jcr:mixinTypes and mgnl:template properties. Heavier to parse, but guarantees no data loss from custom properties.

Step 3: Build Your Caisy Blueprint Schema

With your audit complete and content extracted, create blueprints in Caisy that represent your flattened content model.

Blueprint creation: UI vs. code

You can create blueprints manually in Caisy's UI or programmatically using the Internal API. The Internal API provides operations for managing blueprints within your projects — creating, updating, deleting, fetching, and duplicating entire project structures.

For migrations, the code-driven approach using the TypeScript SDK is strongly preferred:

import { initSdk } from "@caisy/sdk";
 
const sdk = initSdk({
  token: "<YOUR_PERSONAL_ACCESS_TOKEN>",
  endpoint: "https://cloud.caisy.io/caisy/graphql"
});
 
const result = await sdk.GetManyBlueprints({
  input: { projectId: "<PROJECT_ID>" }
});

Use the Internal API (not the External API) for migration operations. The External API is for production content reads by frontend applications; the Internal API is what powers the Caisy UI itself and is the correct surface for bulk data operations.

Field type mapping

Magnolia JCR property type Caisy field type
String (single-line) Text (single-line)
String (multi-line) Text (multi-line)
richText Rich Text (ProseMirror-compatible JSON AST)
Boolean Boolean
Long Number (integer)
Double / Decimal Number (float)
Date Date / DateTime
asset (JCR UUID reference) Connection → Asset
Content type reference Connection → Document or Component
Submodel Component blueprint or field group

Caisy separates the editor-facing title from the API name. Blueprint, group, and field IDs remain stable across projects when using blueprint sync — useful for maintaining consistent dev/staging/prod models and for rerunnable imports that don't require remapping references after each test cycle.

Step 4: Migrate Assets to Caisy

Assets must be migrated before content documents, because content documents will reference the Caisy asset IDs.

Extracting assets from Magnolia

Magnolia's digital asset management system stores data in the dam JCR workspace. Asset metadata lives in the JCR; binaries may be stored inline in the repository or externally (e.g., Amazon S3 or Azure Blob, depending on your Magnolia configuration). Check your magnolia.properties or storage configuration before assuming where binaries live.

For bulk extraction, use the Delivery API with a DAM endpoint to get asset metadata and download binaries from Magnolia's URL pattern:

http://your-magnolia-host/dam/jcr:{uuid}/{filename}

Or export the dam workspace via JCR XML export (binary data will be Base64-encoded in the XML).

Preserve three layers of asset data:

  1. The original binary file (do not migrate Magnolia-generated renditions — only the original, high-resolution source)
  2. Asset-level metadata: title, alt text, caption, MIME type, original filename
  3. Per-placement overrides: Magnolia content apps can store per-usage alt text or captions separately from the asset-level defaults. Flattening these together causes accessibility and SEO regression.

The default alt text in Magnolia comes from the asset title unless a content-app field overrides it for a specific usage. Explicitly audit whether your editors have been using per-placement overrides before assuming asset-level alt text is sufficient.

Uploading to Caisy via tus.io

For asset uploads, Caisy implements the open-source tus.io resumable upload protocol. You can use any tus client library.

import * as tus from 'tus-js-client';
 
async function uploadAsset(
  fileBlob: Blob,
  originalFilename: string,
  mimeType: string,
  token: string,
  projectId: string,
  magnoliaUuid: string,
  idMappingTable: Map<string, string>
): Promise<void> {
  return new Promise((resolve, reject) => {
    const upload = new tus.Upload(fileBlob, {
      headers: {
        'x-caisy-token': token,
        'x-caisy-project-id': projectId,
      },
      endpoint: 'https://cloud.caisy.io/upload/',
      metadata: {
        filename: originalFilename,
        filetype: mimeType,
      },
      retryDelays: [0, 1000, 3000, 5000],  // built-in retry on network failure
      onSuccess: () => {
        const caisyAssetId = upload.url?.split('/').pop() ?? '';
        idMappingTable.set(magnoliaUuid, caisyAssetId);
        resolve();
      },
      onError: (error) => reject(error),
    });
    upload.start();
  });
}

Record every mapping: {magnoliaJcrUuid} → {caisyAssetDocumentId}. You will need this for every asset reference in content documents.

Warning

Watch for:

  • Caisy rate limits vary by pricing tier. Contact Caisy support or check your plan documentation for the specific request-per-minute limits that apply to your project before running bulk uploads. Implement exponential backoff regardless.
  • If an image is in an unsupported format, Caisy's API will ignore the resize operation and return the file in its original state. Plan explicitly for nonstandard formats (e.g., TIFF, WebP with alpha, SVG).
  • Do not migrate Magnolia's generated image renditions — only migrate the original, high-resolution source file. Caisy handles image optimization dynamically via its CDN.
  • Large media libraries (10,000+ assets) must be batched with checkpoint logging (persist the idMappingTable to disk after each batch) so you can resume on failure without re-uploading completed assets.

Rollback strategy for assets

If asset uploads produce corrupted or incorrect data, the safest rollback is to delete the Caisy project's asset documents in bulk via the Internal API and re-run from the last checkpoint. Because assets are uploaded before content documents, a full asset rollback does not require re-migrating content — provided content documents have not yet been created. This is another reason to complete and verify the asset migration fully before beginning document import.

Step 5: Import Content Documents via Caisy's API

With blueprints created and assets uploaded, import content documents. You have two paths.

Path A: External API mutations (simpler, limited)

If mutations are enabled on a blueprint, Caisy generates three GraphQL mutations in the External API for that blueprint: create, update, and delete.

Enable mutations on each blueprint in Caisy's UI, then run GraphQL mutations:

mutation CreateArticle($input: ArticleInput!) {
  createArticle(input: $input) {
    id
    title
  }
}

Limitation: All documents created via mutation land in draft state. Publish separately via the Internal API or manually. External mutations are appropriate for small, ongoing write flows — not for bulk migration of hundreds or thousands of records.

The Internal API is what powers every action inside the Caisy UI. With it, you can programmatically control every element in Caisy. Use PutManyDocuments for bulk loads:

import { initSdk } from "@caisy/sdk";
 
const sdk = initSdk({
  token: "<TOKEN>",
  endpoint: "https://cloud.caisy.io/caisy/graphql"
});
 
async function importBatch(
  rows: MagnoliaContent[],
  pageBlueprintId: string,
  projectId: string,
  idMappingTable: Map<string, string>
): Promise<void> {
  try {
    const result = await sdk.PutManyDocuments({
      input: {
        projectId,
        documentInputs: rows.map((row) => ({
          documentId: row.id,
          title: row.title,
          blueprintId: pageBlueprintId,
          fields: mapFields(row, idMappingTable),
        })),
      },
    });
 
    // PutManyDocuments returns partial success — check per-document status
    const failures = result.PutManyDocuments?.documents?.filter(
      (d) => d?.status !== 'SUCCESS'
    );
    if (failures?.length) {
      console.error(`Partial failure: ${failures.length} documents failed`);
      failures.forEach((f) => console.error(f?.documentId, f?.error));
      // Write failed IDs to a retry queue file
    }
  } catch (err) {
    console.error('Batch failed entirely:', err);
    // Log the batch for retry; do not lose the row data
  }
}

Critical: PutManyDocuments supports partial success — it may succeed for some documents in a batch and fail for others. Always inspect the per-document status in the response and write failures to a retry queue. Never assume a successful HTTP response means all documents were written.

The full import pattern:

  1. Parse extracted Magnolia JSON/XML into a normalized format
  2. For each content item, look up the target Caisy blueprint ID
  3. Map each Magnolia property to the corresponding Caisy field ID
  4. Replace Magnolia JCR UUIDs (for assets and cross-references) with Caisy document IDs using your mapping table
  5. Create the document via SDK, handle partial failures
  6. Log success/failure with the original Magnolia node path for traceability

Rollback strategy for documents

If document import produces structurally corrupt content (wrong blueprint assignments, missing field mappings), the cleanest rollback is to delete the affected documents via the Internal API using the document IDs you logged during import. If the entire run must be rolled back, use a project-level delete or restore from a Caisy project snapshot if your plan supports it. This is why per-document logging with the original Magnolia path is not optional — it is your rollback manifest.

Rich text content is where most migration bugs hide. This is the single most common failure point in headless CMS migrations.

The HTML-to-AST problem

Magnolia typically uses CKEditor, storing rich text as raw HTML strings in JCR String properties. Caisy requires a ProseMirror-compatible JSON AST for its rich text fields — specifically the same schema used by Tiptap v2 (which is built on ProseMirror). If you pass an HTML string into a Caisy rich text field, the GraphQL mutation will throw a validation error.

Your ETL pipeline must include an HTML-to-AST parser that outputs valid ProseMirror/Tiptap v2 JSON. The recommended approach is to use the @tiptap/html package or rehype with a ProseMirror serializer, not a hand-rolled parser.

Here is what the transformation looks like:

Magnolia HTML input:

<h2>Migration Guide</h2>
<p>Contact us for <a href="resolve/uuid/12345">help</a>.</p>

Required Caisy AST output (ProseMirror/Tiptap v2 schema):

{
  "type": "doc",
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 2 },
      "content": [{ "type": "text", "text": "Migration Guide" }]
    },
    {
      "type": "paragraph",
      "content": [
        { "type": "text", "text": "Contact us for " },
        {
          "type": "text",
          "marks": [{ "type": "link", "attrs": { "href": "/help", "target": null } }],
          "text": "help"
        },
        { "type": "text", "text": "." }
      ]
    }
  ]
}

The top-level "type": "doc" wrapper is required. Every node must conform to ProseMirror node schema: type, optional attrs, optional content (array), optional marks (array of { type, attrs }). Validate your AST output against Caisy's rich text schema before running bulk imports — a single malformed node type will fail the entire document mutation.

Supported node types in Caisy's rich text include: doc, paragraph, heading (attrs: level 1–6), bulletList, orderedList, listItem, blockquote, codeBlock, hardBreak, horizontalRule, image. Supported mark types include: bold, italic, underline, strike, code, link (attrs: href, target).

Image references in rich text

Parse all <img src="..." /> tags inside rich text. Replace Magnolia DAM URLs with Caisy Asset CDN URLs using your asset ID mapping table. Inline images within rich text need their src attributes updated after asset migration completes and before document import runs.

Internal linking is one of the hardest challenges in this migration. In Magnolia, an internal link in a rich text field looks like this: <a href="resolve/uuid/8f2a-4b9c-...">About Us</a>.

During your initial import, the target document ("About Us") might not exist yet in Caisy — you don't have its Caisy Document ID. If you try to link to it, the reference breaks.

The two-pass migration method:

Pass 1 — Ingestion: Import all documents into Caisy. Whenever you encounter a Magnolia JCR UUID in a rich text field or reference field, leave it as a typed placeholder: [[MAGNOLIA_UUID:8f2a-4b9c-...]]. During this pass, build and persist a master mapping table of MagnoliaJcrUuid → CaisyDocumentId to disk (not just in memory) so it survives process restarts.

Pass 2 — Resolution: Once all content is in Caisy, run a second script that:

  1. Queries every document via the Internal API
  2. Searches AST nodes and string fields for [[MAGNOLIA_UUID:...]] placeholders
  3. Looks up the corresponding Caisy ID in the mapping table
  4. Replaces the placeholder with the correct Caisy document reference
  5. Updates the document via the SDK

This guarantees zero broken internal links. The two-pass approach also decouples import order from reference resolution — you can import in any order without tracking dependency graphs.

Embedded components in rich text

Magnolia can embed components within rich text via FreeMarker template directives (e.g., [@cms.component content=... /]). These directives have no equivalent in Caisy rich text and must be extracted into separate component documents with explicit reference fields on the parent document. Identify all embedded component patterns in your rich text corpus before building the parser.

Localization Strategy

Magnolia stores localized content as locale-suffixed properties on the same JCR node (e.g., title, title_de, title_fr). Use the lang=all parameter on the Delivery API to retrieve all locale variants in a single request.

Caisy handles localization at the document level — each locale is a variant of the same document identified by the same document ID. During migration:

  1. Extract all locale variants from Magnolia using lang=all
  2. Verify which properties actually have locale-specific values — Magnolia's fallback locale behavior (returning the default language when a translation is absent) can mask missing translations in exports
  3. Create the base document in Caisy's default locale
  4. Add locale variants via the Internal API for each additional language that has actual translated content

Ensure your extraction script groups localized properties together by JCR UUID before pushing to Caisy. Otherwise you risk creating duplicate, disconnected documents instead of a single document with multiple locale variants.

For QA validation during migration, Caisy's preview mode is readable via the External API using the x-caisy-preview: true request header. Use this to compare staged imports against Magnolia source content before publishing.

What You Will Lose

Be explicit with stakeholders about what does not survive this migration:

  • In-context page editing: Magnolia's visual page editor where editors drag components into areas. Caisy is a structured content editor — no visual page builder. If your marketing team requires a visual, block-based editor, you may need to evaluate platforms like Tilda instead.
  • Server-side rendering: Magnolia's FreeMarker/JSP template engine disappears. Your frontend handles all rendering.
  • Workflow and approval chains: Magnolia's publication workflows, including four-eye approval and task-driven promotion, do not transfer. Caisy has roles, draft/published states, content scheduling, and version history, but does not have a documented equivalent of Magnolia's task-driven approval queue. If your release governance depends on approval workflows, treat this as an explicit redesign item with editor training requirements, or consider whether an enterprise DXP like Sitecore is a better fit.
  • JCR query capabilities: Magnolia supports JCR-SQL2 queries across workspaces. Caisy's GraphQL API is powerful but semantically different — all... queries support filtering and sorting by field, but workspace-spanning joins have no equivalent.
  • Access control granularity: Magnolia's role-based permissions at the node and workspace level are more granular than Caisy's project-level user roles. If you rely on per-section or per-workspace access control, map this explicitly before migration.
  • Multi-site from a single instance: Magnolia's multisite module manages multiple domains from one installation. In Caisy, this maps to multi-project or multi-tenant setups with separate API endpoints per project.
  • JCR version history: Caisy provides version history for tracking content changes and reverting to previous states, but Magnolia's historical JCR version tree will not migrate — only the current published state of each node transfers.
  • Scheduled publication states: Magnolia nodes with future publish dates must be explicitly identified and re-scheduled in Caisy after import.
Danger

If production releases currently depend on Magnolia tasks, scheduled approval, or author/public promotion queues, do not promise editorial parity until you have tested the replacement process in Caisy end to end with your actual editors.

Estimated Effort

These estimates assume one engineer with TypeScript proficiency and access to both Magnolia's author instance and Caisy's Internal API. They exclude the frontend rebuild, which is typically the largest effort in any headless migration. Rich text transformation edge cases and QA of localized content are included in the ranges.

Site profile Estimated effort
<50 pages, 1 content type, <200 assets, single locale 2–3 days
50–500 pages, 3–5 content types, multi-language, <1,000 assets 1–2 weeks
500+ pages, 10+ content types, complex component trees, 5,000+ assets, multi-site 3–5 weeks

Assumptions underlying these estimates: No custom JCR node types beyond standard Magnolia types; rich text fields contain standard HTML (no custom CKEditor plugins producing nonstandard markup); Magnolia Delivery API is accessible; one engineer handles both extraction and import. Each of these assumptions failing adds 1–3 days per item. These are working estimates from migration projects, not guarantees — use them for initial scoping only.

Migration Checklist

  • Identify Magnolia version (5.x vs. 6.x vs. Cloud) and verify API access level
  • Inventory all workspaces, content types, templates, locales, URL rules, and redirects
  • Identify any scheduled publication states on Magnolia content
  • Export content via Delivery API v2 with lang=all or JCR XML export (keepNodeIds: true)
  • Download all DAM assets with UUID-to-filename mapping; document per-placement alt text overrides
  • Design Caisy blueprint schema — flatten Magnolia's hierarchy, map all three area types
  • Freeze blueprint API names before importing production data
  • Create blueprints in Caisy (UI or Internal API)
  • Upload assets to Caisy via tus.io; persist {magnoliaUuid → caisyAssetId} mapping table to disk
  • Verify asset uploads: spot-check 10% for correct MIME type, filename, and metadata
  • Build HTML-to-ProseMirror AST parser; validate output schema before bulk run
  • Build and run content import scripts (TypeScript SDK, PutManyDocuments) with per-document failure logging
  • Pass 1: Import all documents with [[MAGNOLIA_UUID]] placeholders for unresolved references
  • Pass 2: Resolve all placeholders using master UUID mapping table; update documents via SDK
  • Import locale variants for multi-language content; verify no duplicate documents created
  • Re-schedule any content that had future publication dates in Magnolia
  • Validate: spot-check 10% of content across all content types, verify asset links, test rich text rendering
  • Set up 301 redirects from old Magnolia URL paths to new frontend routes
  • Run delta sync for content changed during UAT or editor training
  • Publish all documents via bulk operation through Internal API
  • Cut traffic only after 404 logs, redirect logs, and preview checks are clean for 24 hours

Common Failure Modes

Failure Cause Prevention
Rich text mutation rejected Invalid AST node type or missing "type": "doc" wrapper Validate AST schema against ProseMirror spec before bulk import
Broken internal links Pass 1 placeholders not resolved in Pass 2 Never skip Pass 2; verify placeholder count matches resolution count
Duplicate locale documents UUID grouping failed before Caisy import Group by JCR UUID in extraction, not by path
Missing asset binaries Binaries stored externally (S3/Azure) not downloaded Audit magnolia.properties for external blob store config
Partial batch failures silently dropped PutManyDocuments partial success not checked Always inspect per-document status in response; write failures to retry queue
Blueprint API name changed post-import Blueprint renamed after frontend queries built Freeze API names in Step 1; use Caisy blueprint sync for consistent IDs
Asset renditions migrated instead of originals Magnolia rendition URLs used in extraction Confirm DAM download URLs point to source files, not rendition endpoints

Frequently Asked Questions

Can I export Magnolia content directly into Caisy?
No. Magnolia exports content as JCR XML, YAML, or JSON via its REST API. This data must be programmatically transformed to match Caisy's blueprint schema, rich text AST format, and asset reference structure before import.
How do Magnolia content types map to Caisy blueprints?
Magnolia page types become Caisy document blueprints. Magnolia components become Caisy component blueprints. Magnolia areas (which group components on a page) have no direct equivalent — model them as connection fields (lists of component references) on the parent document blueprint.
How do I migrate assets from Magnolia's DAM to Caisy?
Export assets from Magnolia's dam JCR workspace via XML export or HTTP download. Upload each file to Caisy using the tus.io protocol with x-caisy-token and x-caisy-project-id headers. Record the Magnolia UUID to Caisy asset ID mapping for relinking references in content documents. Only migrate original source files — not Magnolia's generated renditions.
What do I lose when migrating from Magnolia to Caisy?
You lose in-context page editing, server-side rendering (FreeMarker/JSP), publication workflows with four-eye approval, JCR-SQL2 queries, granular node-level access control, and multi-site management from a single instance. Magnolia's version history does not migrate. Caisy is headless-only, so all rendering moves to your frontend.
Should I use Caisy's External API or Internal API for migration?
Use the Internal API via the TypeScript SDK. Caisy's docs explicitly call out automatic migrations and imports/exports as a use case for the Internal API. The External API mutations are limited — they require per-blueprint enablement, and all created documents land in draft state. PutManyDocuments via the Internal API is the more performant path for bulk loads.

More from our Blog

Sitefinity to Tilda Migration: A Technical Guide
Migration Guide/General

Sitefinity to Tilda Migration: A Technical Guide

A technical guide to migrating from Sitefinity CMS to Tilda. Covers OData API extraction, Feeds CSV import, manual page rebuild, and the hard constraints of Tilda's read-only API.

Nachi Nachi · · 22 min read