Launched:self-serve migrations intoSuperhuman Docs (Coda)
Try it now
01Agent-first
Runs where you already work
Plug it into Claude, ChatGPT or Cursor. Describe the move in plain English; the agent runs it.
02Engineer-led
Our production engine, unlocked
The pipeline our engineers use on managed enterprise migrations — the same code, now something you can drive yourself.
03Pricing
Try 10 pages free, then $1 a page
Credit-based, pay-as-you-go. No scoping call, no quote — sample it on your own docs before you spend anything.
04Sources
NotionSlabConfluenceSoonGoogle DocsSoon
Skip to content

Kentico to Strapi Migration: A Technical Guide

A technical guide to migrating from Kentico Xperience 13 to Strapi v5: SQL extraction, content model mapping, rich text conversion, and edge cases.

Raajshekhar Rajan Raajshekhar Rajan · · 21 min read
Kentico to Strapi 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

Kentico to Strapi Migration: A Technical Guide

Migrating from Kentico Xperience 13 to Strapi means moving from a monolithic .NET CMS with a relational SQL Server backend, page-tree hierarchy, and Page Builder widget system — to a headless, API-first Node.js CMS where every content type auto-generates REST and GraphQL endpoints and the frontend is entirely decoupled.

The core challenge: Kentico stores content across three or more interconnected SQL tables per page type, with widget configurations serialized as JSON blobs inside a DocumentPageBuilderWidgets column. Strapi stores each collection type in its own database table with a flat schema.json definition. There is no automated tool that bridges these two data models. Every migration requires custom extraction, schema mapping, transformation, and API-driven import.

With Kentico Xperience 13 reaching end of support on December 31, 2026 — and already in security-only support since January 2026 — teams evaluating a move to a headless CMS have a hard deadline. This guide covers the full technical path: how to extract from Kentico, how to design the Strapi content model, how to handle the import, and what breaks along the way.

Warning

KX13 deadline: Kentico Xperience 13 support ends December 31, 2026. After that date, Kentico provides no hotfixes, security patches, or technical support. The platform continues to run but as fully unsupported software. We recommend starting the scoping process at least 3–4 months before any internal deadline. See the official Kentico end-of-support announcement for the authoritative source.

How Kentico and Strapi Store Content Differently

Understanding the data model mismatch is the prerequisite for everything that follows.

Kentico Xperience 13 is a monolithic CMS where routing, presentation, and data are tightly coupled. The content hierarchy is managed via the CMS_Tree table — every piece of content exists at a specific node path (e.g., /Company/About-Us). This tree dictates URL structure, navigation menus, and template inheritance. Each page has a corresponding row in CMS_Document (language-specific properties, workflow state, Page Builder widget configuration) and — if the page type has custom fields — a row in a coupled data table (e.g., custom_Article, CONTENT_News). Media files live either in the filesystem, Azure Blob Storage, or as page attachments stored in CMS_Attachment.

Strapi v5 is headless and completely flat. It is a Node.js application backed by PostgreSQL, MySQL, or SQLite. Content types are defined by schema.json files that live in the project directory at ./src/api/[api-name]/content-types/[content-type-name]/schema.json. Each content type auto-generates a database table and full CRUD REST endpoints. Strapi uses Collection Types (multiple entries, like blog posts) and Single Types (one entry, like a homepage). Reusable field groups are modeled as Components, and flexible content blocks use Dynamic Zones — Strapi's closest analog to Kentico's Page Builder widget zones. Strapi has no concept of a page tree or inherited routing; routing is handled entirely by your frontend application.

When migrating, you must flatten Kentico's hierarchy. A Kentico node at /Blog/2025/Tech/Migration-Guide becomes a single Strapi Article entry. The URL path is preserved by mapping Kentico's NodeAliasPath into a custom slug or urlPath field in the Strapi Collection Type, allowing your Next.js or Nuxt router to resolve the legacy URLs correctly.

Concept Kentico XP 13 Strapi v5
Content structure Page tree hierarchy (CMS_Tree + CMS_Document + coupled table) Flat collection types with relational references
Custom fields Coupled data tables per page type attributes in schema.json per collection type
Flexible layouts Page Builder widgets (JSON in DocumentPageBuilderWidgets) Dynamic Zones with component entries
Rich text HTML with internal link macros and /getmedia/ URLs Rich text (HTML/Markdown) or Blocks editor (JSON)
Media storage Filesystem, Azure Blob, or CMS_Attachment table Upload plugin (local, S3, Cloudinary)
API access REST service at /rest (disabled by default) Auto-generated REST + GraphQL (enabled per role)
i18n Language versions as separate CMS_Document rows per node Built-in i18n with locale variants per document

How to Extract Data from Kentico XP 13

Direct SQL extraction is the most reliable path for cross-platform migrations out of Kentico. The native export tools were not designed for data portability — the Kentico Site Export Wizard generates a proprietary ZIP package containing XML files and binary assets, designed exclusively for Kentico-to-Kentico transfers. It cannot be parsed into Strapi.

For a detailed walkthrough of every extraction method, see our Kentico export guide.

For most enterprise migrations, direct SQL extraction is the most reliable method. It bypasses API pagination limits and lets you join the complex web of Kentico tables into flat, workable views.

Kentico splits content across several core tables:

  • CMS_Tree — tree position, node aliases, shared metadata, and routing data
  • CMS_Document — language-specific properties, workflow states, Page Builder widget configurations
  • CONTENT_* / custom_* — the actual custom fields you defined for a specific page type

A standard extraction query joins these tables to produce a complete record:

SELECT
  t.NodeAliasPath,
  t.NodeGUID,
  d.DocumentName,
  d.DocumentCulture,
  d.DocumentPublishFrom,
  d.DocumentPageBuilderWidgets,
  c.*  -- coupled data columns
FROM CMS_Tree t
JOIN CMS_Document d ON t.NodeID = d.DocumentNodeID
LEFT JOIN custom_Article c ON d.DocumentForeignKeyValue = c.ArticleID
WHERE d.DocumentCulture = 'en-US'
  AND t.NodeClassID = (SELECT ClassID FROM CMS_Class WHERE ClassName = 'custom.Article')
ORDER BY t.NodeAliasPath;

Key considerations:

  • Join pattern varies per page type. Each page type with custom fields has its own coupled table. The join column is DocumentForeignKeyValue matched against the coupled table's primary key.
  • Widget data needs JSON parsing. The DocumentPageBuilderWidgets column contains a JSON structure with sections → zones → widgets → variants → properties. You must parse this to extract content from widget instances.
  • Attachments are separate. Page attachments are in CMS_Attachment (binary data and metadata). Media library files are on the filesystem under ~/[SiteName]/media/ or in Azure Blob Storage.
  • Versioning adds complexity. If you need historical versions, query CMS_VersionHistory — but for most migrations, you only need the latest published version.
  • Validate your schema first. After designing Strapi schema.json files, run strapi develop locally to verify schema validity before executing any imports. Schema errors surface immediately at startup rather than mid-migration.

This approach gives you exact control over the dataset. Export these views as JSON or CSV files, which act as the source of truth for the transformation layer.

Kentico REST API (limited)

Kentico's built-in REST service is available at /rest but is disabled by default and must be explicitly enabled in Settings → Integration → REST. It supports retrieving pages and objects in XML or JSON format.

The critical limitation: the offset pagination parameter only works for object data, not for pages. For page data, you can use the childrenof path pattern to retrieve children of a specific node, but there is no cursor-based pagination. The API also lacks field selection and filtering depth. If your Kentico instance has over 5,000 pages, the REST API will likely time out or require aggressive request throttling.

For cross-platform migrations involving more than a few hundred pages, SQL extraction is overwhelmingly the better choice.

What about Kentico's official migration tool?

Kentico provides an official migration tool designed specifically for KX13 → Xperience by Kentico (XbyK) transfers. It migrates page types, pages, media libraries, contacts, forms, and categories — but only into another Kentico instance. It does not produce portable output usable by Strapi or any non-Kentico target. For more on what it covers and what it misses, see What Kentico's Migration Tool Doesn't Move.

What about Strapi's import/export plugin?

Strapi ships an official @strapi/plugin-import-export-entries plugin that can import CSV or JSON files directly into Strapi collection types. It is useful for low-volume migrations (under ~10,000 records with simple schemas) where you can produce clean CSV output from your Kentico SQL extraction.

Its limitations for Kentico migrations specifically: it does not handle Dynamic Zone fields, does not resolve media GUIDs, and cannot manage the import ordering required for relational integrity. For complex Kentico content models with Page Builder widgets, rich text rewrites, and multilingual content, the REST API or direct database seeding approach is more appropriate.

How to Design the Strapi Content Model for Kentico Data

Map each Kentico page type to a Strapi Collection Type. For each coupled data table in Kentico, create a corresponding schema.json in Strapi.

A Kentico page type like custom.Article with fields ArticleTitle (text), ArticleSummary (long text), ArticleBody (rich text), and ArticleDate (datetime) maps to a Strapi schema like this:

{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": {
    "draftAndPublish": true
  },
  "attributes": {
    "title": { "type": "string", "required": true },
    "summary": { "type": "text" },
    "body": { "type": "richtext" },
    "publishDate": { "type": "datetime" },
    "slug": { "type": "uid", "targetField": "title" },
    "kenticoNodeGuid": { "type": "string" },
    "featuredImage": {
      "type": "media",
      "multiple": false,
      "allowedTypes": ["images"]
    }
  }
}

Mapping rules that matter

  • Keep a kenticoNodeGuid field on every migrated collection type. This gives you a join key for validation, delta syncs, and debugging. Mark it as non-visible in the admin UI after migration.
  • Kentico page types without custom fields (container pages) often don't need a Strapi equivalent — they were structural, not content-bearing.
  • Kentico categories are stored in CMS_Category with a tree structure. In Strapi, model these as a separate Category collection type with a self-referencing relation for parent/child hierarchy, then link via a relation field.
  • Page Builder widgets → Strapi Dynamic Zones. If a Kentico page uses Page Builder with a RichText widget, an Image widget, and a custom CTA widget, model these as Strapi Components (e.g., shared.rich-text, shared.image-block, shared.cta-block) and reference them through a Dynamic Zone field on the parent collection type.

Translating Page Builder widgets to Dynamic Zones

Kentico Xperience 13 relies heavily on the MVC Page Builder. Content editors drag and drop widgets onto a page layout. Kentico stores this configuration as a serialized JSON blob in the DocumentPageBuilderWidgets column of CMS_Document. The structure nests as editableAreas → sections → zones → widgets → variants → properties.

Strapi handles modular content using Dynamic Zones and Components. A Dynamic Zone is an array of different components that can be mixed and matched.

To migrate this data, you write a transformation script that:

  1. Parses the Kentico widget JSON
  2. Maps each Kentico widget identifier (e.g., Kentico.FormWidget) to a corresponding Strapi Component name (e.g., shared.form)
  3. Extracts property values from each widget instance
  4. Formats the properties into a Strapi-compatible component array

Custom widgets with complex property types — page selectors, path selectors, media selectors that store GUIDs — require lookup resolution during transformation. If you skip this, you get raw GUIDs in Strapi fields that point to nothing. If a Kentico widget has no equivalent Strapi component, that data will be lost unless you expand your Strapi schema.

Handling rich text content

Kentico rich text fields contain HTML with Kentico-specific constructs:

  • Internal page links use ~/ relative paths or Kentico macros
  • Media references point to /getmedia/{GUID}/filename or /getattachment/{GUID}/filename
  • Widget inline content may include resolved widget output
  • Macro expressions ({% ... %}) that resolve at render time — current user properties, dynamic URLs, conditional content — have no Strapi equivalent and must be resolved to static values or stripped

Before importing into Strapi, run a transformation pass that:

  1. Replaces /getmedia/{GUID}/ URLs with the new Strapi media URLs (mapped via a GUID-to-Strapi-URL lookup table built during media import)
  2. Rewrites internal page links to the new URL scheme
  3. Strips Kentico-specific macros and resolves them to static values
  4. Validates HTML structure — malformed markup can cause rendering issues in frontends

A common failure mode: migrating the content first, then realizing hundreds of embedded links are broken because the media GUID mapping was not built yet. Always import media first and build the lookup table before transforming rich text.

Strapi v5 rich text field types: Strapi v5 offers both a standard richtext field (accepts HTML or Markdown) and a blocks field type that uses a proprietary JSON structure. If you choose the Blocks editor for your Strapi schema, you cannot pass raw HTML — you must parse the Kentico HTML through a library like rehype to convert it into Strapi's nested block array. The specific plugins needed are rehype-parse to parse the HTML, rehype-stringify for output, and a custom visitor that maps HTML elements to Strapi block node types. For example:

{
  "type": "paragraph",
  "children": [
    { "type": "text", "text": "Hello " },
    { "type": "text", "text": "World", "bold": true }
  ]
}

Kentico HTML constructs that require custom handling in the rehype visitor: inline styles (strip or convert to Strapi-supported marks), tables (Strapi Blocks supports tables natively in v5 — map <thead>/<tbody>/<tr>/<td> to the Strapi table block schema), and embedded macros (resolve or strip before parsing). The standard richtext field type avoids this complexity by accepting HTML directly, but gives editors less structured control in the Strapi admin.

Handling Media and File Attachments

Kentico has two distinct file storage mechanisms: page attachments (stored in CMS_Attachment, often as binary blobs in the database) and media library files (stored on the filesystem under ~/[SiteName]/media/ or in Azure Blob Storage). Both need to be extracted, but through different paths. Page attachments require a SQL query to extract the binary; media library files require filesystem or blob storage access.

Strapi requires all media to be uploaded to its Media Library via the POST /api/upload endpoint with multipart/form-data before it can be linked to a content entry. Both Kentico file types end up in Strapi's unified Upload plugin.

The media migration workflow follows a strict sequence:

  1. Extract the physical files from Kentico — either via SQL binary export from CMS_Attachment or filesystem/blob storage copy for media library files.
  2. POST each file to Strapi's /api/upload endpoint.
  3. Capture the returned Strapi file id and URL from the response.
  4. Build a Kentico GUID → Strapi file ID/URL mapping table. This table is essential for rewriting rich text links and populating media relation fields on content entries.

If you upload content entries first without the media IDs, images will be missing and rich text links will be broken. Order of operations is critical.

Info

Strapi upload limits: Strapi Cloud plans enforce upload size limits — non-image files are capped at 200 MB. For self-hosted Strapi v5, the upload size limit is configured in config/middlewares.ts via the koa-body settings (the v5 config path differs from v4's config/plugins.js approach). The default body parser limit for JSON payloads is 1 MB; both are configurable. See the Strapi v5 middleware configuration docs for the correct v5 syntax.

How to Import Content into Strapi via API

Strapi's REST API automatically generates POST endpoints for every collection type. To create an entry in the articles collection:

async function createArticle(articleData, apiToken, strapiBaseUrl) {
  const maxRetries = 3;
  let attempt = 0;
 
  while (attempt < maxRetries) {
    try {
      const response = await fetch(`${strapiBaseUrl}/api/articles`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiToken}`
        },
        body: JSON.stringify({
          data: {
            title: articleData.title,
            summary: articleData.summary,
            body: articleData.body,
            publishDate: articleData.publishDate,
            slug: articleData.slug,
            kenticoNodeGuid: articleData.kenticoNodeGuid,
            featuredImage: articleData.strapiMediaId
          }
        })
      });
 
      if (!response.ok) {
        const errorBody = await response.json();
        // Log validation errors without retrying — a 400 will not succeed on retry
        if (response.status === 400) {
          console.error(`Validation error for ${articleData.slug}:`, JSON.stringify(errorBody, null, 2));
          return { success: false, status: 400, error: errorBody };
        }
        // Retry on 429, 502, 503, 504
        throw new Error(`HTTP ${response.status}: ${JSON.stringify(errorBody)}`);
      }
 
      const result = await response.json();
      console.log(`Created: ${articleData.slug} → documentId: ${result.data.documentId}`);
      return { success: true, documentId: result.data.documentId };
 
    } catch (err) {
      attempt++;
      if (attempt >= maxRetries) {
        console.error(`Failed after ${maxRetries} attempts: ${articleData.slug}`, err.message);
        return { success: false, error: err.message };
      }
      // Exponential backoff: 1s, 2s, 4s
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
    }
  }
}

Key implementation notes:

  • Separate retry logic by status code. 400 (validation error) will not succeed on retry — log it and skip. 429, 502, 503, and 504 are transient and worth retrying with backoff.
  • Log the documentId from every successful response. You will need it for subsequent locale variant POSTs and for the publish step.
  • Track failures in a separate log file keyed by kenticoNodeGuid. This enables targeted reruns without re-processing successfully migrated records.

Import order matters

Strapi enforces referential integrity on relations. Import in this order:

  1. Media files — upload all images, documents, and assets first. Build the GUID-to-Strapi-ID mapping table.
  2. Independent collection types — categories, authors, tags, or any content type that doesn't depend on other migrated content.
  3. Dependent collection types — articles, pages, and other content that references the above via relations.
  4. Dynamic Zone content — entries with Dynamic Zone fields that reference components. The component data is embedded inline in the POST body, not created separately.
  5. Publish step — in Strapi v5 with Draft & Publish enabled, entries are created in draft by default. Publish them via a separate API call or by including status: 'published' in the document service.

Strapi lifecycle hooks during import

If your Strapi project defines lifecycle hooks (e.g., beforeCreate, afterCreate in ./src/api/[name]/content-types/[name]/lifecycles.js), they fire on every REST API POST during migration. Hooks that trigger external services (email, webhooks, search index updates, image processing) will execute for every migrated record. This causes two problems: significantly slower throughput and unintended side effects (e.g., sending notification emails for 50,000 migrated articles).

Before running a full migration, disable or guard lifecycle hooks:

// In your lifecycle file, add an environment check
module.exports = {
  afterCreate(event) {
    if (process.env.MIGRATION_MODE === 'true') return; // skip during migration
    // ... normal hook logic
  }
};

Set MIGRATION_MODE=true in your environment during the import run, then remove it afterward. This pattern avoids modifying hook logic permanently.

The Document API and localization

Strapi v5 introduced the Document API, which changed how entries are created, localized, and published. In v5, entries share a unified documentId across all locale variants. This aligns well with Kentico's localization model, where a single CMS_Tree node can have multiple CMS_Document rows for different cultures (e.g., en-US and fr-FR).

The migration script must group Kentico rows by NodeID, treat one culture as the default locale, and create locale variants for the others. When POSTing to the Strapi v5 API, you define the locale in the payload:

{
  "data": {
    "title": "Migrating from Kentico",
    "slug": "migrating-from-kentico",
    "content": [],
    "coverImage": 142,
    "locale": "en"
  }
}

To add the French translation, make a subsequent POST to /api/articles referencing the same documentId but with "locale": "fr". Getting the NodeID grouping wrong results in duplicate top-level entries instead of proper locale variants.

Rate limits and batching

Strapi is a Node.js application. Flooding the Document API with thousands of concurrent POST requests will exhaust the database connection pool or block the Node event loop, resulting in 502 Bad Gateway errors.

All ingestion scripts must implement concurrency limits. Use p-limit (v3.x for CommonJS compatibility, v4+ for ESM) with concurrency set to 10–20 parallel requests as a starting baseline. Going higher risks Strapi's single-threaded event loop backing up, especially if lifecycle hooks run on each create.

import pLimit from 'p-limit';
const limit = pLimit(15); // adjust based on server RAM and DB tier
 
const results = await Promise.all(
  articles.map(article => limit(() => createArticle(article, apiToken, strapiBaseUrl)))
);

Practical throughput at concurrency=15 on a mid-tier VPS (4 vCPU, 8 GB RAM, PostgreSQL on same host): approximately 200–400 records/hour for simple flat content types, dropping to 80–150 records/hour for entries with Dynamic Zone components and media relations. These figures assume no lifecycle hooks and no external service calls. Use these as planning baselines, not guarantees — your schema complexity and server tier are the dominant variables.

High-volume migrations: bypassing the API

For datasets exceeding ~100,000 records, the REST API becomes the bottleneck. Strapi does not natively expose a bulk insert endpoint. Two escape hatches exist:

  1. Direct database seeding via Knex. Strapi v5 uses Knex internally. You can write seeder scripts that insert directly into the Strapi-managed tables, bypassing the API layer entirely. This requires understanding Strapi's internal table structure (collection tables, component join tables, relation tables, locale tables) and correctly populating all of them. Lifecycle hooks do not fire with this approach. This is the fastest path for very large datasets but requires careful schema inspection to get join table entries right.

  2. Bootstrap function (register or bootstrap in src/index.js). You can run migration logic inside Strapi's bootstrap lifecycle using the Strapi entity service, which has lower overhead than HTTP but still fires hooks. Useful for mid-volume migrations where you want Strapi's validation but not HTTP overhead.

Neither approach is officially documented for bulk migration use — they require reading Strapi's internal source to map the table structure correctly.

Validating imported content

If you are validating migrated content by reading it back through Strapi's API, the default pagination limit is 100 items per page. Set pagination [pageSize]=1000 (or whatever your max is configured to via api.maxLimit in config/api.ts) to avoid false positives where a count query shows fewer records than expected. Cross-reference total record counts between your Kentico SQL query (COUNT(*) per page type) and the Strapi API response's meta.pagination.total field.

What Breaks: Edge Cases and Failure Modes

Page Builder widget content that doesn't map cleanly

Kentico's Page Builder stores widget configurations as nested JSON with property values specific to each widget type. Custom widgets with complex property types — page selectors, path selectors, media selectors that store GUIDs — require lookup resolution during transformation. If you skip this step, you end up with raw GUIDs in Strapi fields that reference nothing.

Attachment vs. media library files

Kentico has two distinct file storage mechanisms, and they require different extraction methods. Page attachments require a SQL query against CMS_Attachment to extract the binary data. Media library files require filesystem or blob storage access. Mixing them up or forgetting one category is a common source of missing images post-migration.

Kentico rich text editors produce links like /getmedia/{GUID}/image.jpg or page links using NodeAliasPath. These must be rewritten before import. The most common failure mode: migrating content first, then discovering hundreds of embedded links are broken because the media GUID mapping was never built.

Kentico's multi-table join for multilingual content

In Kentico, each language version of a page is a separate row in CMS_Document sharing the same CMS_Tree node. In Strapi v5, each document has locale variants under a single documentId. The migration script must group Kentico rows by NodeID, treat one culture as the default locale, and create locale variants for the others. Getting this grouping wrong results in duplicate entries instead of proper translations.

Kentico macros in content fields

Kentico content fields can contain macro expressions ({% ... %}) that resolve at render time — things like current user properties, dynamic URLs, or conditional content. These have no Strapi equivalent. Your transformation script must either resolve them to static values (if possible from the data you have) or strip them with appropriate fallback content.

Forms module data

Kentico's BizForm module stores submitted form data in dedicated tables (BizForm_[formname]). The guide's scope covers content migration, not form submission records. If you need submitted records, they must be exported separately via SQL against those tables and imported into whatever form data store your new stack uses — Strapi has no built-in form submission system.

What You Lose in This Migration

Be explicit with stakeholders about what does not transfer:

  • Page Builder layouts and sections. The visual arrangement of widgets on a page is a presentation concern stored as JSON. Strapi has no Page Builder — the frontend handles layout. You keep the content from widgets, but lose the layout configuration.
  • Workflow and versioning history. Kentico's document workflow states, approval chains, and version history don't migrate. Strapi v5 has Draft & Publish and basic version tracking, but prior Kentico versions are not carried over.
  • Form submission data. Kentico's BizForm data sits in dedicated BizForm_[formname] tables. If you need submitted form records, export them separately via SQL — Strapi has no built-in form submission system.
  • Personalization and A/B test variants. Kentico's widget variants for personalization and A/B testing are stored alongside widget data. These are platform-specific and have no Strapi equivalent.
  • User accounts and permissions. Kentico's user roles, page-level ACLs, and membership data don't map to Strapi's role-based permission model. User migration requires a separate effort.

How Long Does a Kentico to Strapi Data Migration Take?

Separate the project into two distinct clocks: your internal rebuild clock and the data migration clock.

Your internal rebuild clock measures the time it takes your engineering team to configure the Strapi schemas, build the frontend (Next.js, Nuxt, etc.), replicate the Kentico Page Builder logic in frontend components, and set up hosting infrastructure. This effort typically takes several months.

The data migration clock measures the actual extraction, transformation, and ingestion of your content. At the throughput rates described above (200–400 records/hour for simple content, 80–150 records/hour for complex Dynamic Zone content at concurrency=15), practical estimates by dataset size:

Dataset size Estimated migration time
Under 5,000 records, simple schema Under 24 hours
5,000–50,000 records, moderate complexity 2–5 business days
50,000–200,000 records, multilingual + Dynamic Zones 7–13 business days
Over 200,000 records Direct DB seeding recommended; timeline varies

These estimates assume: media import completed before content import, lifecycle hooks disabled during migration, and a dedicated mid-tier VPS or equivalent cloud instance. Bottlenecks that extend timelines: rich text link rewriting at scale, widget property GUID resolution requiring many individual lookups, and multilingual grouping logic with many locale variants.

Kentico itself publishes no universal estimate for platform migration timelines — it states that it is "not possible to provide a universal estimate for timeline or costs."

Info

Two clocks to keep apart: The data migration timeline does not describe the full replatforming effort, which includes designing the Strapi content model, building the frontend, setting up hosting, and configuring editorial workflows. That is a separate project measured in months. ClonePartner handles the data migration; we don't rebuild the application.

Kentico to Strapi Migration Checklist

A practical sequence for teams executing this migration:

  1. Audit Kentico content types. Query CMS_Class to inventory all page types, custom tables, and custom modules with content. Note which have coupled data tables and which are container-only.
  2. Map page types to Strapi collection types. Define schema.json files for each content type that carries data. Identify which Kentico fields map to which Strapi attribute types.
  3. Validate Strapi schemas locally. Run strapi develop against each schema.json before any import. Fix schema errors at this stage — they surface immediately at startup.
  4. Model Page Builder widgets as Strapi Components. Parse sample DocumentPageBuilderWidgets JSON to identify all widget types in use. Create a Strapi Component for each. Build the widget-identifier-to-component-name mapping table.
  5. Disable or guard lifecycle hooks. Set MIGRATION_MODE=true (or equivalent) in your Strapi environment before import runs to prevent hooks from firing on migrated records.
  6. Extract media. Export all media library files from the filesystem/blob storage and all page attachments from CMS_Attachment. Upload to Strapi and build the GUID → Strapi URL mapping table.
  7. Extract and transform content. Run SQL queries against the joined Kentico tables. Transform rich text (rewrite /getmedia/ links, strip macros, parse to Blocks format if needed). Resolve widget property GUIDs. Structure output as Strapi-compatible JSON payloads.
  8. Run sample migration. Import a representative subset (e.g., 50–100 records across all content types) into a staging Strapi instance. Validate field mapping, relations, media references, rich text rendering, and locale variants.
  9. Iterate until clean. Fix mapping issues, re-run. Repeat as needed.
  10. Full migration. Import the complete dataset with concurrency controls and retry logic. Track failures by kenticoNodeGuid for targeted reruns.
  11. Validate. Cross-check record counts (COUNT(*) in Kentico vs. meta.pagination.total in Strapi API), spot-check field values, verify referential integrity on relations.
  12. Delta sync. If content editors continued working in Kentico during migration, run a delta sync to capture changes made after the initial extraction.
  13. Cutover. Point the frontend to Strapi, verify end-to-end, decommission Kentico access.

Why Teams Move from Kentico to Strapi

The most common technical drivers across migration engagements:

  • KX13 end-of-support pressure. Teams that don't want to upgrade to Xperience by Kentico (which itself requires a full replatforming effort — page types, content model, and routing all change) are evaluating headless alternatives like Strapi or Contentful. XbyK is not a simple upgrade path; it is a new product.
  • Licensing cost reduction. Kentico's DXP licensing is priced for enterprise budgets. Strapi Community Edition is MIT-licensed and free to self-host. The cost differential is significant for teams running Kentico solely as a content repository without using its marketing automation or personalization features. Teams that rely heavily on those enterprise features typically migrate to Sitecore or Optimizely, but for headless content delivery, Strapi is a fraction of the cost.
  • Frontend decoupling. Teams building with React, Next.js, or Nuxt want a headless CMS that treats the API as a first-class citizen — not a bolt-on REST service disabled by default. While some .NET shops migrate to Umbraco to keep their C# backend, teams moving to a modern JavaScript stack prefer Strapi's native Node.js architecture.
  • Developer experience. Strapi's content type schema is a JSON file in version control. Content types auto-generate API endpoints. No .NET compilation cycle, no Visual Studio dependency, schema changes are tracked in Git diffs.

Getting the Data Moved

A Kentico-to-Strapi migration combines SQL extraction from a complex relational schema, JSON parsing of Page Builder widget data, content transformation with link rewriting, lifecycle hook management, and API-driven import with proper sequencing and retry logic. The extraction side alone touches at minimum three SQL tables per page type.

ClonePartner has completed 1,500+ data migrations. We have extracted from Kentico's multi-table schema before, and we know where the edge cases hide — widget property GUIDs that resolve to nothing, attachment binaries that need streaming out of SQL, rich text links that break silently, lifecycle hooks that fire 50,000 times during import. Our process: connect accounts, build a migration checklist, run unlimited sample migrations until the data is right, then execute the full migration with delta sync. One fixed price per project starting at $500 — no hourly billing, no per-record metering.

Frequently Asked Questions

Can I use Kentico's export tools to migrate to Strapi?
No. The Kentico Site Export Wizard generates a proprietary ZIP package designed exclusively for Kentico-to-Kentico transfers. Kentico's official migration tool only targets Xperience by Kentico (XbyK). For a Strapi migration, you must use direct SQL extraction or the Kentico REST API to get migration-ready data.
How do Kentico page types map to Strapi content types?
Each Kentico page type with custom fields maps to a Strapi Collection Type defined by a schema.json file. Kentico's coupled data table columns become Strapi attributes. Page Builder widgets map to Strapi Components referenced through Dynamic Zones. Container page types without custom fields typically don't need a Strapi equivalent.
What happens to Kentico Page Builder widget content in Strapi?
Widget content values are preserved, but layout configuration is lost. The DocumentPageBuilderWidgets JSON must be parsed to extract property values from each widget instance. These values are imported as Component entries within Strapi Dynamic Zones. The visual layout (sections, zones, ordering) becomes a frontend concern in Strapi.
How long does a Kentico to Strapi data migration take?
Most data migrations are completed within 1–5 business days, depending on data volume, complexity, and API rate limits. Small datasets under 10,000 records can finish in under 24 hours. This covers only the data migration — not the broader replatforming effort of building a new frontend and configuring Strapi, which is a separate project measured in months.
Does Kentico Xperience 13 end of support affect my migration timeline?
Yes. KX13 enters full end of support on December 31, 2026. After that date, Kentico provides no security patches, hotfixes, or technical support. If you're planning a move to Strapi, start scoping at least 3–4 months before your internal deadline to allow time for content model design, sample migrations, and validation.

More from our Blog

Kentico to Optimizely Migration: A Technical Guide
Migration Guide/Kentico/Optimizely

Kentico to Optimizely Migration: A Technical Guide

Technical guide to migrating content from Kentico (KX13) to Optimizely CMS. Covers SQL extraction, content model mapping, widget-to-block transformation, and Content Management API import.

Abdul Aleem Abdul Aleem · · 17 min read