Migrating to Prismic

Learn how to migrate your existing data to Prismic using our APIs.

Tools

Prismic comes with a set of tools to assist content migration.

APIs

  • The Migration API allows for creating and updating documents in a Prismic repository.
  • The Asset API allows for creating and updating assets in a Prismic repository.

Those APIs are consumed through @prismicio/client, one of the packages assisting Prismic migration.

Packages

  • @prismicio/client is the core Prismic package for creating web apps with Prismic and JavaScript.
  • @prismicio/migrate is a library of helpers to help you migrate content to Prismic.

Process

We recommend the following content migration process. It ensures all necessary pieces are in place at the right time.

1. Create a Prismic repository

Create a Prismic repository if you don’t have one yet. A Prismic repository stores a website’s content and assets.

Once created, configure the repository’s main locale and any additional locales needed for your project.

2. Set up a project and install Slice Machine

Follow the instructions displayed after creating the repository. These instructions guides you through setting up a project and installing Slice Machine, Prismic developer tool.

3. Define content models

Custom type and slice models describe the shape of your content. Writers will fill in fields defined by your content models, and the Document API will serve them to your website as JSON. With Prismic, content modeling happens within Slice Machine locally and models get stored in your project’s code.

This step is necessary for the Migration API to understand the shape of each document. It also comes with the benefit of generating TypeScript types for your content models, allowing @prismicio/client to provide more accurate type hints.

Once content models are defined, make sure to push them from Slice Machine to Prismic.

4. Write a migration script

You will need to write a script to migrate content from your current website to Prismic. This is where the bulk of the migration process happens.

Because content might come from a variety of CMSs and in various forms, we cannot provide a universal migration script that would work for everyone, hence the need to write your own.

A typical migration script goes as follows:

  • Fetch data from your current data sources.
  • Convert that content to match your Prismic models.
  • Create documents and assets in Prismic with @prismicio/client.

The following section gives you more guidance on writing migration scripts. You can also browse a complete example of a migration script on GitHub.

Migration Script

Script setup

Install @prismicio/client, @prismicio/migrate, and dotenv.

npm install @prismicio/client @prismicio/migrate dotenv

Create a permanent write token and add it to a .env file.

.env
PRISMIC_WRITE_TOKEN=YOUR_TOKEN

Create a migrate.mjs script at the root of your project with the following content:

migrate.mjs
import "dotenv/config";
import * as prismic from "@prismicio/client";
import { htmlAsRichText } from "@prismicio/migrate";

import { repositoryName } from "./slicemachine.config.json";

// Prismic setup
const writeClient = prismic.createWriteClient(
  repositoryName,
  {
    writeToken: process.env.PRISMIC_WRITE_TOKEN,
  },
);

const migration = prismic.createMigration();

// Custom migration code will go here...

// Execute the prepared migration at the very end of the script
await writeClient.migrate(migration, {
  reporter: (event) => console.log(event),
});

The following examples show how to work with the migration instance, before calling the writeClient.migrate() method. How you write your script will depend on the shape of your existing content and your Prismic content models.

Creating documents

Create a new document in the migration with migration.createDocument(). The document’s type, uid, lang, and data properties should be provided. A tags property can optionally be provided. The document’s title should be provided as the second argument.

const document = migration.createDocument(
  {
    type: "page",
    // For some document types, `uid` can be optional,
    // TypeScript will let you know when it's the case.
    uid: "home",
    lang: "en-us",
    tags: ["team-marketing", "team-branding"],
    // Learn more in the "Providing document data" section.
    data: {
      /* ... */
    },
  },
  "Homepage",
);

Creating alternate language documents

When working on a multi-language project, you likely have documents that are translations of one another (e.g. the “À propos” page is the French translation of the English “About Us” page). When a document is a translation of a master language document, you should link them together. This allows editors to quickly switch from one translation to another and to keep track of translation progress.

To register a link between a master language document and an alternate language document, use the masterLanguageDocument option.

const enAboutUs = migration.createDocument(
  {
    type: "page",
    uid: "about-us",
    lang: "en-us",
    data: {
      /* ... */
    },
  },
  "About Us",
);

const frAboutUs = migration.createDocument(
  {
    type: "page",
    uid: "a-propos",
    lang: "fr-fr",
    data: {
      /* ... */
    },
  },
  "À propos",
  // Learn more in the "Defining content relationships" section
  { masterLanguageDocument: enAboutUs },
);

Creating a document from Prismic

You can migrate content from one Prismic repository to another using migration.createDocumentFromPrismic(). This method takes care of updating the document’s content relationships and creating its assets.

const otherClient = prismic.createClient(
  "another-repository-name",
);

// Fetches a document from another repository
const documentFromPrismic = await otherClient.getByUID(
  "page",
  "home",
);

const document = migration.createDocumentFromPrismic(
  documentFromPrismic,
  "Homepage",
);

Updating documents

You can update existing documents using migration.updateDocument(). The document’s uid, tags, and data properties can be updated, as well as the title displayed in Prismic.

const documentToUpdate = await writeClient.getByUID(
  "page",
  "home",
);

// Add a tag
documentToUpdate.tags.push("team-marketing");

// Keep the same document title
const document = migration.updateDocument(documentToUpdate);
// Or update the document's title
const document = migration.updateDocument(
  documentToUpdate,
  "Updated title",
);

Creating assets

You can create assets using migration.createAsset(). The asset’s file and filename should be provided. Metadata about the asset can be provided, including notes, credits, alt, and tags.

const asset = migration.createAsset(
  "https://example.com/foo.png",
  "foo.png",
  // All metadata are optional
  {
    notes: "lorem",
    credits: "ipsum",
    alt: "dolor",
    tags: ["sit", "amet"],
  },
);

The file parameter can be a URL string, a URL object, a File instance, or any value accepted by File in your environment (e.g. a Base 64 string, Blob, etc.). Any of the following will work:

// URL-like
migration.createAsset(
  "https://example.com/foo.png",
  "foo.png",
);
migration.createAsset(
  new URL("https://example.com/bar.png"),
  "bar.png",
);

// File-like
migration.createAsset(
  new File(["example-data"]),
  "baz.png",
);
migration.createAsset(
  fs.readFileSync("quux.png"),
  "quux.png",
);

Assets with the same source file are deduplicated. Metadata for each deduplicated asset will be merged. This becomes useful when defining asset fields.

const fooPng = migration.createAsset(
  "https://example.com/foo.png",
  "foo.png",
);

// Later in your code, but `foo.png` will only be created once.
const anotherFooPng = migration.createAsset(
  "https://example.com/foo.png",
  "foo.png",
);

Providing document data

When migrating documents, we need to create (or update) them with Prismic data. This is done through the data property of documents created with migration.createDocument() (or updated with migration.updateDocument()).

The data object gets fully typed thanks to TypeScript types generated by Slice Machine when custom types and slices are defined. From here, most Prismic fields can be defined as-is with data from your previous data sources.

const document = migration.createDocument({
  type: 'page',
  uid: 'home',
  lang: 'en-us',
  data: {
    myBoolean: true,
    myNumber: 3,
    myKeyText: 'Lorem ipsum',
    mySelect: 'primary',
    myColor: '#663399',
    myDate: '2024-05-23',
    myTimestamp: '2024-05-23T12:34:56+123'
    myLink: { link_type: 'Web', url: 'https://example.com' },
    myEmbed: { embed_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
    myGeopoint: { latitude: 48.8673362731933 longitude: 2.37049508094787 },
    myGroup: [{ /* First item */ }, { /* Second item... */ }],
    mySliceZone: [{ /* First slice */ }, { /* Second slice... */ }],
  },
}, 'Homepage')

To define rich text fields, asset-related fields, and content relationships (links between two documents), refer to the following sections.

Defining rich text fields

Basic usage

HTML content can be serialized to Prismic rich text with htmlAsRichText().

const title = htmlAsRichText("<h1>Hello World</h1>").result;
const body = htmlAsRichText(someBlogPostHTML).result;

const document = migration.createDocument(
  {
    type: "post",
    uid: "hello-world",
    lang: "en-us",
    data: { title, body },
  },
  "Hello World",
);

htmlAsRichText() supports an optional config argument providing great flexibility to how HTML code gets converted. Learn more about it in its technical reference.

Images

Text images should be registered in your migration too as assets. This is done through the serializer config.

const blogPostBody = htmlAsRichText(someBlogPostHTML, {
  serializer: {
    img: ({ node }) => {
      const src = node.properties.src;
      const filename = src.split("/").pop();
      const alt = node.properties.alt;

      return {
        type: "image",
        id: migration.createAsset(src, filename, { alt }),
      };
    },
  },
}).result;

Content relationships

Text links to other Prismic documents should be declared as content relationships. This is also done through the serializer config.

const blogPostBody = htmlAsRichText(someBlogPostHTML, {
  serializer: {
    a: ({ node }) => {
      const href = node.properties.href;

      // Matches URLs like `/blog/hello-world`
      if (href.startsWith("/blog/")) {
        // e.g. `hello-world`
        const uid = href.split("/").pop();

        return {
          type: "hyperlink",
          // Creates a content relationship to
          // the blog post with a matching `uid`
          data: () => migration.getByUID("blog", uid),
        };
      }

      // Serializes other links as external links
      return "hyperlink";
    },
  },
}).result;

Images

Image fields can be defined with their migration asset directly.

const document = migration.createDocument(
  {
    type: "post",
    uid: "hello-world",
    lang: "en-us",
    data: {
      thumbnail: migration.createAsset(
        "https://example.com/foo.png",
        "foo.png",
      ),
    },
  },
  "Hello World",
);

If an image field was modeled with multiple thumbnails, it can be defined as an object with an id property linking to the main image, and other properties defining additional thumbnails.

const fooPng = migration.createAsset(
  "https://example.com/foo.png",
  "foo.png",
);

const document = migration.createDocument(
  {
    type: "post",
    uid: "hello-world",
    lang: "en-us",
    data: {
      metaImage: {
        id: fooPng,
        squared: fooPng,
        twitter: fooPng,
        openGraph: fooPng,
      },
    },
  },
  "Hello World",
);

Links to media fields can be defined as an object with an id property linking to the linked asset.

const document = migration.createDocument(
  {
    type: "post",
    uid: "hello-world",
    lang: "en-us",
    data: {
      attachment: {
        link_type: "Media",
        id: migration.createAsset(
          "https://example.com/foo.pdf",
          "foo.pdf",
        ),
      },
    },
  },
  "Hello World",
);

Defining content relationships

Content relationships can be defined with migration documents. Migration documents are returned when creating (or updating) documents with migration.createDocument() (or migration.updateDocument()).

const fooDocument = migration.createDocument(
  {
    type: "page",
    uid: "foo",
    lang: "en-us",
    data: {
      /* ... */
    },
  },
  "Foo",
);

const barDocument = migration.createDocument(
  {
    type: "page",
    uid: "bar",
    lang: "en-us",
    data: {
      related: fooDocument,
    },
  },
  "Bar",
);

Content relationships can also be defined with an already existing document.

const existingBarDocument = await writeClient.getByUID(
  "page",
  "bar",
);

const fooDocument = migration.createDocument(
  {
    type: "page",
    uid: "foo",
    lang: "en-us",
    data: {
      related: existingBarDocument,
    },
  },
  "Foo",
);

When facing circular content relationships (i.e. foo links to bar and bar to foo), we can define lazy content relationships with migration.getByUID() and migration.getSingle().

const settingsDocument = migration.createDocument(
  {
    type: "settings", // `settings` is a singleton document with no `uid`
    lang: "en-us",
    data: {
      related: () => migration.getByUID("page", "bar"),
    },
  },
  "Settings",
);

const barDocument = migration.createDocument(
  {
    type: "page",
    uid: "bar",
    lang: "en-us",
    data: {
      related: () => migration.getSingle("settings"),
    },
  },
  "Bar",
);

Lazy content relationships can be asynchronous. The following example queries for an already published document.

const fooDocument = migration.createDocument(
  {
    type: "page",
    uid: "foo",
    lang: "en-us",
    data: {
      related: async () => {
        const existingBarDocument = await client.getByUID("page", "bar");

        return existingBarDocument;
      },
    },
  },
  "Foo",
);

Script execution

When ready, your script can be executed with the following command:

# For `migration.mjs`
node migrate.mjs

# For `migration.ts`
npx tsx migration.ts