WegWorks Logo
WegWorksSimplifying Tomorrow

How to Migrate from AEM to Payload CMS: Complete Guide

Published: December 5, 2025 · 15 min read · AEM Migration, Headless CMS

Adobe Experience Manager (AEM) has long been an enterprise-grade content management solution, but its complexity, licensing costs, and infrastructure requirements are driving many organizations to explore modern alternatives. Payload CMS has emerged as a compelling headless CMS option that offers flexibility, developer experience, and cost efficiency that AEM struggles to match.

This guide walks you through the complete process of migrating from AEM to Payload CMS, covering content modeling, data migration, API integration, and deployment strategies.

Why Organizations Are Moving Away from AEM

  • Cost: AEM licensing can exceed $200,000+ annually for enterprise deployments
  • Complexity: Steep learning curve with Java/OSGi architecture
  • Infrastructure: Heavy resource requirements for on-premise or Cloud Service
  • Flexibility: Monolithic architecture limits modern frontend frameworks

Understanding Payload CMS

Payload CMS is an open-source, headless content management system built with TypeScript and Node.js. It provides a powerful admin panel, flexible content modeling, and a robust API—all while giving you complete control over your data and infrastructure.

Key Advantages of Payload Over AEM

Feature AEM Payload CMS
Licensing Cost $200K+/year Free (MIT License)
Technology Stack Java, OSGi, HTL TypeScript, Node.js, React
API-First Partial (Content Services) Native REST & GraphQL
Deployment Options Cloud Service or On-Premise Any Node.js host, Docker, Serverless
Database JCR (Apache Oak) MongoDB or PostgreSQL
Admin Customization Complex (Coral UI, Granite) Simple React components
Learning Curve Steep (months) Moderate (weeks)

Migration Planning

A successful AEM to Payload migration requires careful planning across several dimensions:

1 Content Audit

Inventory all content types, components, and assets in your AEM instance. Identify:

  • Page templates and their components
  • Content Fragments and Experience Fragments
  • DAM assets (images, videos, documents)
  • Translations and language copies
  • Custom workflows and business logic

2 Content Model Mapping

Map AEM content structures to Payload collections:

  • AEM Page Components → Payload Collections with blocks
  • Content Fragments → Payload Collections
  • DAM Assets → Payload Media library
  • Tags → Payload relationship fields

3 Integration Assessment

Evaluate external integrations that need to be migrated:

  • Adobe Target personalization
  • Adobe Analytics tracking
  • Third-party DAM connections
  • Custom Sling servlets and APIs

Setting Up Payload CMS

Start by creating a new Payload project:

# Create a new Payload project
npx create-payload-app@latest my-payload-cms

# Navigate to the project
cd my-payload-cms

# Install dependencies
npm install

Configure Your Database

Payload supports MongoDB and PostgreSQL. For most AEM migrations, MongoDB provides a flexible document structure similar to JCR:

// payload.config.ts
import { buildConfig } from 'payload/config';
import { mongooseAdapter } from '@payloadcms/db-mongodb';

export default buildConfig({
  serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
  admin: {
    user: 'users',
  },
  db: mongooseAdapter({
    url: process.env.MONGODB_URI,
  }),
  collections: [
    // Your collections will go here
  ],
});

Content Modeling in Payload

Recreating AEM Page Structures

AEM pages with components can be modeled as Payload collections with blocks:

// collections/Pages.ts
import { CollectionConfig } from 'payload/types';
import { HeroBlock } from '../blocks/Hero';
import { ContentBlock } from '../blocks/Content';
import { CTABlock } from '../blocks/CTA';

export const Pages: CollectionConfig = {
  slug: 'pages',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'slug', 'status', 'updatedAt'],
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      unique: true,
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'status',
      type: 'select',
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Published', value: 'published' },
      ],
      defaultValue: 'draft',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'layout',
      type: 'blocks',
      blocks: [HeroBlock, ContentBlock, CTABlock],
    },
    {
      name: 'meta',
      type: 'group',
      fields: [
        { name: 'title', type: 'text' },
        { name: 'description', type: 'textarea' },
        { name: 'image', type: 'upload', relationTo: 'media' },
      ],
    },
  ],
};

Migrating Content Fragments

AEM Content Fragments map directly to Payload collections:

// collections/Articles.ts (Content Fragment equivalent)
export const Articles: CollectionConfig = {
  slug: 'articles',
  admin: {
    useAsTitle: 'title',
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'author',
      type: 'relationship',
      relationTo: 'authors',
    },
    {
      name: 'category',
      type: 'relationship',
      relationTo: 'categories',
      hasMany: true,
    },
    {
      name: 'content',
      type: 'richText',
    },
    {
      name: 'publishDate',
      type: 'date',
      admin: {
        date: {
          pickerAppearance: 'dayAndTime',
        },
      },
    },
  ],
};

Exporting Content from AEM

Use AEM's Content Services or QueryBuilder to export content as JSON:

# Query all pages under /content/mysite
curl -u admin:admin \
  "http://localhost:4502/bin/querybuilder.json?\
path=/content/mysite&\
type=cq:Page&\
p.limit=-1" \
  -o aem-pages.json

# Export Content Fragments
curl -u admin:admin \
  "http://localhost:4502/bin/querybuilder.json?\
path=/content/dam/mysite/content-fragments&\
type=dam:Asset&\
p.limit=-1" \
  -o aem-content-fragments.json

Export Assets from DAM

# Create a package of DAM assets
# In AEM Package Manager, create package with filter:
# /content/dam/mysite

# Or use Asset Share Commons for bulk export
curl -u admin:admin \
  "http://localhost:4502/content/dam/mysite.assetdownload.zip" \
  -o dam-assets.zip

Data Transformation Script

Create a migration script to transform AEM JSON to Payload format:

// migrate.ts
import fs from 'fs';
import payload from 'payload';

interface AEMPage {
  'jcr:path': string;
  'jcr:title': string;
  'jcr:content': {
    'jcr:title': string;
    'cq:template': string;
    'sling:resourceType': string;
    [key: string]: any;
  };
}

async function migratePages() {
  // Initialize Payload
  await payload.init({
    secret: process.env.PAYLOAD_SECRET,
    local: true,
  });

  // Read AEM export
  const aemData = JSON.parse(
    fs.readFileSync('./aem-pages.json', 'utf-8')
  );

  for (const aemPage of aemData.hits as AEMPage[]) {
    const content = aemPage['jcr:content'];
    
    // Transform AEM page to Payload format
    const payloadPage = {
      title: content['jcr:title'] || aemPage['jcr:title'],
      slug: extractSlug(aemPage['jcr:path']),
      status: 'draft',
      layout: transformComponents(content),
      meta: {
        title: content['jcr:title'],
        description: content['jcr:description'],
      },
    };

    try {
      await payload.create({
        collection: 'pages',
        data: payloadPage,
      });
      console.log(`Migrated: ${payloadPage.slug}`);
    } catch (error) {
      console.error(`Failed: ${payloadPage.slug}`, error);
    }
  }

  process.exit(0);
}

function extractSlug(jcrPath: string): string {
  return jcrPath
    .replace('/content/mysite/', '')
    .replace(/\/jcr:content.*/, '');
}

function transformComponents(content: any): any[] {
  const layout: any[] = [];
  
  // Map AEM components to Payload blocks
  Object.entries(content).forEach(([key, value]) => {
    if (key.startsWith('par') && typeof value === 'object') {
      // Parse parsys children
      Object.entries(value as object).forEach(([compKey, compValue]) => {
        const block = mapComponentToBlock(compValue as any);
        if (block) layout.push(block);
      });
    }
  });

  return layout;
}

function mapComponentToBlock(component: any): any | null {
  const resourceType = component['sling:resourceType'];
  
  switch (resourceType) {
    case 'mysite/components/hero':
      return {
        blockType: 'hero',
        heading: component.heading,
        subheading: component.subheading,
        backgroundImage: component.fileReference,
      };
    case 'mysite/components/text':
      return {
        blockType: 'content',
        richText: component.text,
      };
    // Add more component mappings
    default:
      console.warn(`Unmapped component: ${resourceType}`);
      return null;
  }
}

migratePages();

Asset Migration

Migrate DAM assets to Payload's media library:

// migrate-assets.ts
import fs from 'fs';
import path from 'path';
import payload from 'payload';

async function migrateAssets() {
  await payload.init({
    secret: process.env.PAYLOAD_SECRET,
    local: true,
  });

  const assetsDir = './extracted-dam-assets';
  const files = getAllFiles(assetsDir);

  for (const filePath of files) {
    const fileName = path.basename(filePath);
    const fileBuffer = fs.readFileSync(filePath);

    try {
      await payload.create({
        collection: 'media',
        data: {
          alt: extractAltFromPath(filePath),
        },
        file: {
          data: fileBuffer,
          name: fileName,
          mimetype: getMimeType(fileName),
          size: fileBuffer.length,
        },
      });
      console.log(`Uploaded: ${fileName}`);
    } catch (error) {
      console.error(`Failed: ${fileName}`, error);
    }
  }
}

function getAllFiles(dir: string): string[] {
  const files: string[] = [];
  const items = fs.readdirSync(dir, { withFileTypes: true });
  
  for (const item of items) {
    const fullPath = path.join(dir, item.name);
    if (item.isDirectory()) {
      files.push(...getAllFiles(fullPath));
    } else if (isAssetFile(item.name)) {
      files.push(fullPath);
    }
  }
  
  return files;
}

migrateAssets();

Frontend Integration

Once your content is in Payload, integrate with your frontend framework:

// Next.js example: fetching pages from Payload
import { getPayloadClient } from '../payload/payloadClient';

export async function getStaticProps({ params }) {
  const payload = await getPayloadClient();
  
  const pages = await payload.find({
    collection: 'pages',
    where: {
      slug: { equals: params.slug },
      status: { equals: 'published' },
    },
  });

  if (!pages.docs.length) {
    return { notFound: true };
  }

  return {
    props: {
      page: pages.docs[0],
    },
    revalidate: 60,
  };
}

export async function getStaticPaths() {
  const payload = await getPayloadClient();
  
  const pages = await payload.find({
    collection: 'pages',
    where: { status: { equals: 'published' } },
    limit: 1000,
  });

  return {
    paths: pages.docs.map((page) => ({
      params: { slug: page.slug },
    })),
    fallback: 'blocking',
  };
}

Deployment Options

Payload offers flexible deployment options compared to AEM:

Platform Best For Monthly Cost
Payload Cloud Managed hosting, zero DevOps $35-500+
Vercel + MongoDB Atlas Serverless, auto-scaling $20-200
Docker + VPS Full control, cost-effective $10-100
AWS/GCP/Azure Enterprise scale, compliance $50-500+

Migration Considerations

  • Personalization: If using Adobe Target, evaluate alternatives like LaunchDarkly or Optimizely
  • Analytics: Adobe Analytics can still work with Payload via client-side integration
  • Workflows: AEM workflows need to be rebuilt using Payload hooks or external workflow engines
  • Training: Plan for team training on TypeScript/Node.js stack

Post-Migration Checklist

Need Help with Your AEM to Payload Migration?

WegWorks has deep expertise in both AEM and modern headless CMS platforms. We can help you plan and execute a seamless migration that minimizes disruption while maximizing the benefits of your new platform. Contact us to discuss your migration project.