Website content migration is a critical process that involves transferring content from one platform to another, whether due to a CMS migration, a website redesign, or a platform switch. A poorly executed migration can result in data loss and broken links. This guide aims to help technical teams plan and execute a smooth migration, ensuring minimal disruption and preserving content integrity.
#What is website content migration?
Website content migration refers to the process of moving digital content, such as interlinked data, rich text, images, and videos, from one content management system (CMS) or platform to another. The need to migrate can be driven by several factors, like reducing costs, switching to a better platform, better performance, improved security, enhanced user experience, a rebranding initiative, or just moving to a better tech stack.
There are several types of content migration, including:
CMS-to-CMS: Moving content from one CMS to another of the same type for example - Strapi to Hygraph (both are headless CMS), while preserving structure and functionality.
Platform-to-Platform: Transitioning content between different digital platforms, such as moving from a traditional CMS to a headless CMS.
Redesign: Updating the website’s design while ensuring all existing content is effectively transferred and optimized for the new structure.
For example, a business shifting from a monolithic traditional CMS to a headless CMS like Hygraph can gain greater flexibility and scalability while improving content delivery across multiple clients.
#How to plan a website content migration
Website content migration is a complex process and would be highly driven around the purpose of migrating our data. The reason can be anything from using a less expensive solution, moving from a traditional CMS to a headless one, or just choosing another CMS for better features, better user and developer experience, and other long-term prospects. In this section, we will see step-by-step how to migrate a CMS with the help of an in-depth example where we move data from Strapi to Hygraph.
As a developer, before starting to migrate the data, it is important to understand the main concepts of both the systems like what is the nomenclature - Strapi has Content Types, while Hygraph calls the same thing Models. Carefully going through the developer documentation of both systems helps us understand how the content data is structured, how the relationships are managed, and how the assets are managed by both systems. We should check the available APIs, even play with APIs of both systems using a dummy project, seeing the request-response structures. Doing all of this helps a lot in the planning phase and decreases the chances of blunders happening later. Here is the developer documentation for Strapi, and here is the documentation of Hygraph for your reference.
Strapi Project
We will walk through an end-to-end example to see how we can plan website content migration from one CMS to another. For this example, we will create a backend service that will migrate our CMS content from Strapi to Hygraph. In case you just want to follow along you can fork this repository from Strapi and connect it to Strapi Cloud or if you want you can host this project on your server as well. We expect you to have enough developer experience to set up this project on your own.
Once this starter template from Strapi is set up you will be able to see the Content Models structured as shown below, let us understand the entities, their fields, and the relationship between them.
Category - Stores category data to which an article can belong. A Category can have many Articles, An Article can belong to only one Article.
Authors - Stores the profile of an Author, and has fields like name, email, avatar, and linked articles.
An Author can have multiple articles.
Articles - Stores the article data like title, description, cover image, relationship with author, and category.
An Article can belong to one category. An Article can be written by one Author.
Below is some sample data already loaded in by the Strapi template.
Hygraph Project
Next, we will need to set up similar models inside Hygraph. If you do not have a Hygraph project you can set it up here for absolutely free and get started and read more about setting up models here. Let us create three models in the Hygraph dashboard for Category, Author, and Article as shown below. These are pretty similar to the Strapi structure.
We don't have any data here yet, we will build a migration service to help us with that.
Migration Service
Now, to migrate our data from Strapi to Hygraph, we will build a Node.js backend service. It would move all the assets, categories, authors, and article data and also preserve the relationships between the records. Let us start by creating a very basic REST API.
Setting Up Migration Service
main.ts
import express from 'express';import migrateRoutes from './routes/migrate.routes';const host = process.env.HOST ?? 'localhost';const port = process.env.PORT ? Number(process.env.PORT) : 3000;const app = express();app.use('/migrate', migrateRoutes);app.get('/', (req, res) => {res.send({ message: 'Hello API' });});app.listen(port, host, () => {console.log(` [ ready ] http://${host}:${port}`);});
routes/migrate.routes.ts
import express from 'express';import { MigrateController } from '../controllers/migrate.controller';const router = express.Router();const migrateController = new MigrateController();router.get('/', (req, res) => migrateController.migrate(req, res));export default router;
controllers/migrate.controller.ts
import { Request, Response } from 'express';import { MigrateService } from '../services/migrate.service';export class MigrateController {private migrateService: MigrateService;constructor() {this.migrateService = new MigrateService();}async migrate(req: Request, res: Response): Promise<void> {try {const response = await this.migrateService.migrateCMSData();res.send({ data: response });} catch (error) {console.error('Error: ', error);res.status(500).send({ error: 'Migration failed' });}}}
services/migrate.service.ts
export class MigrateService {constructor() {}async migrateCMSData() {try {return { message: 'Hello World' };} catch (err) {console.error('Something went wrong while migrating CMS.', err);throw err;}}}
Okay, so this basic boilerplate code will set up a REST API with a route /migrate
which is tied to the migrateCMSData
function in migrate.service.ts
and responds with a simple Hello World for now.
For the migration, we can see from our Strapi content that we will need a way to export the following from Strapi data
- Assets
- Category Records
- Author Records
- Linked to Assets for the cover image
- Article Records
- Linked to Category Records
- Linked to Assets for the cover image
- Linked to Author Records
We can keep our logic simple - Migrate Assets & Categories first as they are not dependent on anything, and manage an IdMapping
object for each content type/model migrated so we can use them for defining relationships in the dependent records like Author and Articles. Something like -
{"strapiId": "hygraphId"}
We will build two new services namely StrapiService
and HygraphService
. The StrapiService
will help us pull the data from Strapi, HygraphService
will be responsible for pushing the data to Hygraph, and MigrateService
will use both of them and orchestrate the code flow. This will keep our code clean and structured.
Migrating Assets
Let us start by migrating assets, generally, most CMSes have separate endpoints for managing Assets.
Strapi supports both REST and GraphQL APIs. We will use Strapi's REST API to get the asset data.
Hygraph
First, make a function in StrapiService to fetch all assets. We can use axios
to handle the API call.
strapi.service.ts
import axios from 'axios';const STRAPI_URL = process.env.STRAPI_URL;const STRAPI_API_KEY = process.env.STRAPI_API_KEY;export class StrapiService {constructor() {}async fetchAllAssets() {try {const response = await axios(`${STRAPI_URL}/upload/files`, {headers: {Authorization: `Bearer ${STRAPI_API_KEY}`}});return response.data;} catch (error) {console.error('Error fetching media from Strapi:', error);return [];}}}
Hygraph provides a GraphQL API, we can use any lightweight graphql client like graphql-request
Initialize the client in the constructor. Ideally, you should implement a singleton pattern to create clients for external connections, we have avoided it here to reduce the scope of this example. Create an uploadAssets
function that will accept assets upload them to Hygraph, it will also maintain a Strapi ID → Hygraph ID mapping object and return it.
hygraph.service.ts
import { GraphQLClient } from 'graphql-request';import { uploadAssetMutation } from '../graphql';import { HygraphAsset } from '../types';const HYGRAPH_API_URL=process.env.HYGRAPH_API_URL;const HYGRAPH_API_TOKEN=process.env.HYGRAPH_API_TOKEN;export class HygraphService {private readonly client;constructor() {this.client = new GraphQLClient(HYGRAPH_API_URL, {headers: {Authorization: `Bearer ${HYGRAPH_API_TOKEN}`,},});}async uploadAssets(assets: HygraphAsset[]) {try {const assetIdMapping: Record<string, string> = {};for (const asset of assets) {const { externalId, uploadUrl, fileName } = asset;const inputVariable = {input: {uploadUrl,fileName}}const response = await this.client.request(uploadAssetMutation, inputVariable);const hygraphAssetId = response?.createAsset?.id;if (!hygraphAssetId) {console.error('Something went wrong while adding asset', { asset, response });continue;}assetIdMapping[externalId] = hygraphAssetId;}return assetIdMapping;} catch (error) {console.error('Error uploading asset to Hygraph:', error);return null;}}}
types/index.ts
export interface HygraphAsset {externalId: string;uploadUrl: string;fileName: string;}
graphql/mutation.ts
import { gql } from 'graphql-request';export const uploadAssetMutation = gql`mutation UploadAsset($input: AssetCreateInput!) {createAsset(data: $input) {idurl}}`;
Finally, we can the use classes created above in our migration service and migrate the assets.
migrate.service.ts
...async migrateCMSData() {try {const assetIdMapping = await this.migrateAssets();return { assetIdMapping };} catch (err) {console.error('Something went wrong while migrating CMS.', err);throw new Error('Migration failed');}}async migrateAssets() {const assets = await this.strapiService.fetchAllAssets();console.log(`Fetched ${assets.length} assets from Strapi.`);const transformedAssets = assets.map((asset) => {const { documentId, url, name } = assetreturn {externalId: documentId,uploadUrl: url,fileName: name,}});return await this.hygraphService.uploadAssets(transformedAssets);}...
That's it, hit the migrate endpoint from Postman and see the migration in action.
Migrating Category, Author, and Articles
Similarly to assets, we can migrate Categories, Authors, and Articles preserving the relationships using the ID Mappings. We expect you to read and understand JavaScript / TypeScript code. Below is the final code for all three services.
strapi.service.ts
import axios from 'axios';const STRAPI_URL = process.env.STRAPI_URL;const STRAPI_API_KEY = process.env.STRAPI_URL;export class StrapiService {async fetchAllAssets() {try {const response = await axios(`${STRAPI_URL}/upload/files`, {headers: {Authorization: `Bearer ${STRAPI_API_KEY}`}});return response.data;} catch (error) {console.error('Error fetching media from Strapi:', error);return [];}}private async fetchData(endpoint: string, populateAllRelations = false) {const results: unknown[] = [];try {let url = `${STRAPI_URL}/${endpoint}`;if (populateAllRelations) {url += `?populate=*`}const response = await axios.get(url, {headers: {Authorization: `Bearer ${STRAPI_API_KEY}`}});const data = response.data.data;results.push(...data);} catch (error) {console.error(`Failed to fetch ${endpoint}:`, error.message);}return results;}async getCategories() {return this.fetchData('categories');}async getAuthors() {return this.fetchData('authors', true);}async getArticles() {return this.fetchData('articles', true);}}
graphql/index.ts
import { gql } from 'graphql-request';export const uploadAssetMutation = gql`mutation UploadAsset($input: AssetCreateInput!) {createAsset(data: $input) {idurl}}`;export const createCategoryMutation = gql`mutation createCategory($input: CategoryCreateInput!) {createCategory(data: $input){idname}}`;export const createAuthorMutation = gql`mutation createAuthor($input: AuthorCreateInput!) {createAuthor(data: $input) {idname}}`;export const createArticleMutation = gql`mutation createArticle($input: ArticleCreateInput!) {createArticle(data: $input) {idtitle}}`;
types/index.ts
export interface HygraphAuthor {externalId: string;name: string;email: string;cover?: string;}export interface HygraphCategory {externalId: string;name: string;slug: string;description?: string;}export interface HygraphArticle {externalId: string;title: string;description: string;slug: string;coverId?: string;authorId?: string;categoryId?: string;}export interface HygraphAsset {externalId: string;uploadUrl: string;fileName: string;}
hygraph.service.ts
import { GraphQLClient } from 'graphql-request';import { createArticleMutation, createAuthorMutation, createCategoryMutation, uploadAssetMutation } from '../graphql';import { HygraphArticle, HygraphAsset, HygraphAuthor, HygraphCategory } from '../types';const HYGRAPH_API_URL=process.env.HYGRAPH_API_URL;const HYGRAPH_API_TOKEN=process.env.HYGRAPH_API_TOKEN;export class HygraphService {private readonly client;constructor() {this.client = new GraphQLClient(HYGRAPH_API_URL, {headers: {Authorization: `Bearer ${HYGRAPH_API_TOKEN}`,},});}async uploadAssets(assets: HygraphAsset[]) {try {const assetIdMapping: Record<string, string> = {};for (const asset of assets) {const { externalId, uploadUrl, fileName } = asset;const inputVariable = {input: {uploadUrl,fileName}}const response = await this.client.request(uploadAssetMutation, inputVariable);const hygraphAssetId = response?.createAsset?.id;if (!hygraphAssetId) {console.error('Something went wrong while adding asset', { asset, response });continue;}assetIdMapping[externalId] = hygraphAssetId;}return assetIdMapping;} catch (error) {console.error('Error uploading asset to Hygraph:', error);return null;}}async createCategories(categories: HygraphCategory[]) {const categoryIdMapping: Record<string, string> = {};for (const category of categories) {try {const { externalId, name, slug, description } = category;const inputVariable = {input: {name,slug,description}};const response = await this.client.request(createCategoryMutation, inputVariable);const hygraphCategoryId = response?.createCategory?.id;if (!hygraphCategoryId) {console.error('Something went wrong while creating category', { category, response });continue;}categoryIdMapping[externalId] = hygraphCategoryId;console.log(`Created category: ${category.name} (Hygraph ID: ${hygraphCategoryId})`);} catch (error) {console.error(`Error creating category "${category.name}":`, error);}}return categoryIdMapping;}async createAuthors(authors: HygraphAuthor[]) {const authorIdMapping: Record<string, string> = {};for (const author of authors) {try {const { externalId, name, email, cover } = author;const inputVariable = {input: {name,email,cover: {connect: {id: cover,}}}}const response = await this.client.request(createAuthorMutation, inputVariable);const hygraphAuthorId = response?.createAuthor?.id;if (!hygraphAuthorId) {console.error('Something went wrong while creating author', { author, response });continue;}authorIdMapping[externalId] = hygraphAuthorId;console.log(`Created Author: ${name} (Hygraph ID: ${hygraphAuthorId})`);} catch (error) {console.error(`Error creating category "${author.name}":`, error);}}return authorIdMapping;}async createArticles(articles: HygraphArticle[]) {const articleIdMapping: Record<string, string> = {};for (const article of articles) {try {const { externalId, title, description, slug, coverId, authorId, categoryId } = article;const inputVariable = {input: {title,description,slug,cover: {connect: {id: coverId,},},author: {connect: {id: authorId,}},category: {connect: {id: categoryId,}},}};const response = await this.client.request(createArticleMutation, inputVariable);const hygraphArticleId = response?.createArticle?.id;if (!hygraphArticleId) {console.error('Error creating article', { article, response });continue;}articleIdMapping[externalId] = hygraphArticleId;console.log(`Created Article: ${title} (Hygraph ID: ${hygraphArticleId})`);} catch (error) {console.error(`Error creating article "${article.title}":`, error);}}return articleIdMapping;}}
Final Results
As you can see we have successfully migrated everything from Strapi to Hygraph. We moved all the assets, and content records, and preserved the relationships between different models.
Let's talk a little about scaling, our example uses IdMapping objects and hence it stores all the mapping data in memory, but if your CMS has millions of records and hundreds of interdependent entities, you might consider having a different approach as we cannot have infinite memory on the server. In such cases, solutions like streaming, queues, or processing all entries and dependencies one by one like - syncing one article, syncing its author if it exists or creating one, syncing its category if it exists or creating one, and then moving to the next article might be a more efficient approach.
#Best practices for successful content migration
Testing and Validation Once migration is complete, as you can see everything in Hygraph is in Draft status, you can verify the content and relationship manually or by automation. Once you are satisfied with the migrated content you can publish the content and use the new CMS in your frontend preferably behind a feature flag and rollout to users incrementally to ensure the production environment is stable with time. Optionally, you can also use staging environments to identify and resolve issues before going live.
Communication and Collaboration Effective communication between developers, content managers, and stakeholders is essential for a smooth migration. Establish a clear migration roadmap with defined roles and responsibilities. Regular updates and feedback loops help address challenges promptly and maintain alignment throughout the process.
Backups and Data Integrity We should create backups of all website content, databases, and media assets before initiating the migration. This serves as a fail-safe in case of data loss or corruption.
#Why you should make the move to Hygraph
Hygraph is a powerful headless CMS battle-tested in production by many companies. Hygraph has all the key features that are expected from a modern CMS today and offers a modern solution for businesses seeking scalable and flexible content management. We recommend checking out our successful case studies here for insights into how our customers are using the platform.
Some of the key features of Hygraph are as follows
Headless Architecture: Separates content management from content presentation, allowing developers to build highly customized front-end experiences.
GraphQL-Powered APIs: Hygraph leverages GraphQL APIs to enable efficient data fetching, reducing over-fetching and under-fetching issues common with REST APIs.
Scalability and Performance: Designed to handle high traffic and extensive content repositories, ensuring fast and reliable performance.
Multi-Tenancy Support: Organizations can manage multiple projects under one account, streamlining operations.
Content Federation: Aggregates content from multiple sources and APIs, making it easier to unify data from different platforms.
Roles and Permissions: Enables granular role-permission settings, enhancing security and content governance.
Webhooks and Integrations: Easily connects with third-party tools and services, including eCommerce platforms, analytics tools, and marketing automation software.
If you are still using a traditional CMS to manage your content, you should definitely consider checking out our guide to choosing the best headless CMS. It explains why you should move to a headless CMS and provides an overview of available options.
#Conclusion
In this guide, we learned that content migration from one CMS to another can be a complex but highly rewarding process in the long term if done for the right reasons. By following a well-structured migration plan, including brainstorming approaches, reading documentation in-depth, thorough testing, secure backups, and effective team collaboration, organizations can ensure a seamless transition. We also saw an in-depth example of how to migrate a Strapi CMS to Hygraph by building an intermediate Migration Service. If you're considering a content migration, investing in a CMS like Hygraph can significantly enhance content management and delivery, ensuring long-term success.
Blog Author