Creating the Cosmic JS Contentful Importer


TL;DR

Install the Contentful Importer Extension
View the codebase on GitHub

Creating a Cosmic JS extension was an easy and enjoyable experience. I made an extension to import data from Contentful to Cosmic in a matter of hours. My extension takes the JSON file the Contentful CLI gives you and transforms it to Cosmic data objects. It even transfers media from Contentful to Cosmic.

For an overview of how to use the extension check out the other article I wrote, Importing Data from Contentful to Cosmic JS.

When creating a Cosmic extension you have two options, making a page to be served in an iframe or a Jamstack app. After some consideration I choose a Jamstack app. It is essentially a React component and some data services. If I made a server-side extension I’d need to worry about hosting, server maintenance, etc. In my case the only data API needed is Cosmic’s. JavaScript’s File API made parsing Contentful’s JSON right in the browser very easy.

Because Cosmic provides the information you need for accessing the API as query parameters my page is very simple. It is just some text, a file input, and a button. Progress and output are piped to the React component from my services.


To get started I created a react workspace:

$ npx create-react-app cosmic-contentful-importer


Next, Cosmic offers a template ( https://github.com/cosmicjs/extension-starter/blob/master/README.md ) that includes an HTML file and a JSON file you need for uploading your extension to your app. Either download or clone the repo and add the files to the root of your react workspace.

Off the bat you can delete these two files:

index.html
extension-starter.zip


Next, change the extension.json file to specify the app's title, icon, logo and repository URL:

{
  "title": "Contentful Importer",
  "font_awesome_class": "fa-download",
  "image_url": "https://cdn.cosmicjs.com/2d015110-41f7-11ea-93cf-dfe709ea319d-cosmic-contentful.png",
  "repo_url": "https://github.com/cosmicjs/contentful-importer"
}


Now that we're done with initial configuration we can get to coding. 


We'll add 3 services in the "src" folder: 

contentful.service.js // For contentful
cosmic.service.js // For cosmic 
importer.service.js // For combining cosmic + contentful


Now we'll head to index.js to create a react component to consume the services. 

First we need a way to get the slug and keys from the URL so define this function: 

const getParam = param => {
  var urlParams = new URLSearchParams(window.location.search);
  return urlParams.get(param);
};


Now when we define the constructor for our component we can set the state based on the params:

constructor() {
    super();

    this.state = {
      file: null,
      slug: getParam("bucket_slug"),
      read_key: getParam("read_key"),
      write_key: getParam("write_key"),
      errorMessage: false,
      progress: false,
      loading: false,
      messages: []
    };
}


For the sake of brevity and readability we'll skip creating the render method but you can find It here (). The most important elements on the page are the file input and run button which run setFile and parseFile respectively.

When the file Input changes, it updates the state with the file:

setFile(e) {
    const file = e.target.files[0];

    this.setState({ ...this.state, file });
}


When the button is clicked the file is retrieved from state and passed to the importer service: 

  parseFile() {
    try {
      this.setState({
        messages: [],
        loading: true
      });

      const { file } = this.state;

      if (!file) {
        throw new Error("No file provided");
      }

      this.importerService.loadContentfulContent(
        file,
        m => this.progressCallback(m),
        e => this.errorCallback(e),
        () => this.completeCallback(),
        m => this.messageCallback(m)
      );
    } catch (e) {
      this.errorCallback(e);
    }
  }


In the importer service the loadContentfulContent method uses the FileReader API to parse the JSON file then passes off the contents to another method:

loadContentfulContent(file, onProgress, onError, onComplete, onMessage) {
    const reader = new FileReader();

    reader.readAsText(file, 'UTF-8');

    reader.onload = e => {
      try {
        const content = e.target.result;

        const json = JSON.parse(content);

        this._parseContent(json, onProgress, onError, onComplete, onMessage);
      } catch(e) {
        onError(e);
      }
    }
}


The parseContent method will consume the Contentful and Cosmic services we created above to first format the contentful data then send it over to cosmic. Along the way it will pass progress messages to the callback methods provided to display progress in the UI:

  async _parseContent(content, onProgress, onError, onComplete, onMessage) {
    try{
      // Checks object properties to ensure compatibility 
      this._validateContent(content);
      
      onProgress('Content valid. Parsing...');

      // Parses contentful object types to cosmic object types 
      const fields = this.contentful.toCosmicObjectTypes(content.contentTypes);

      onProgress('Successfully parsed content types');
      
      // Creates object types in cosmic
      const cosmicObjectTypes = await this.cosmic.addObjectTypes(fields.contentTypes);

      onProgress('Successfully created content types');

      // Parses contentful media objects to cosmic media objects
      const media = await this.contentful.toCosmicMedia(content.assets, content.locales);

      onProgress('Successfully parsed media');

      onProgress('Uploading media to Cosmic...');

      const cosmicMedia = await this.cosmic.addMediaObjects(media);

      // Check for failed uploads, usually due to file sizes and alert user
      cosmicMedia.forEach(media => {
        if (media.failed) {
          onMessage(`Failed to upload image: ${media.file.metadata.title} - ${media.file.metadata.originalUrl}`);
        }
      })

      onProgress('Successfully created media');
      
      // Parse posts and other objects to cosmic objects
      const parsedObjects = this.contentful.toCosmicObjects(
        content.entries,
        content.locales,
        fields.displayFieldMap,
        fields.metafieldDescriptors,
        media
      );

      onProgress('Successfully parsed entries');

      // Create posts and other objects in cosmic
      const cosmicObjects = await this.cosmic.addObjects(parsedObjects);

      onProgress('Successfully created objects');

      onComplete();
    } catch(e) {
      onError(e);
      console.log(e);
    }
}


There isn't a 1:1 conversion of Contentful objects to Cosmic objects so we need some logic in contentful.service.js to determine the correct cosmic type for an object:

function getMetafieldType(type) {
  if (
    type === "Symbol" ||
    type === "Boolean" ||
    type === "Object" ||
    type === "Location"
  ) {
    return "text";
  } else if (type === "RichText") {
    return "html-textarea";
  } else if (type === "Text") {
    return "markdown";
  } else if (type === "Number" || type === "Integer" || type === "Decimal") {
    return "number";
  } else if (type === "Date") {
    return "date";
  } else if (type === "Asset") {
    return "file";
  } else if (type === "Link") {
    return "object";
  } else if (type === "Array") {
    return "objects";
  }
}


There is no concept of "Symbol", "Boolean", "Object" or "Location" (a lat/lng JSON object) in Cosmic so they will be stored as stringified versions. Both markdown and text can be stored in "Text" so we'll set those to markdown to be safe. Types of Link and Array are stored as objects. The remaining field types have a close mapping as seen above.


Finally, to bundle the react app into a Cosmic app, we'll write a small build command and add it to "scripts" in package.json:

"deploy": "npm run build && cp extension.json build/extension.json && zip -r build.zip build",


I hope you enjoyed this tutorial about how to add an Extension to your Cosmic JS Bucket. If you have any questions. Connect with the Cosmic JS community on Slack and on Twitter.

Was this article helpful?

Thank you! Your input helps us improve our articles and resources. If you have any other questions or comments, please contact support.

You may also like


In this tutorial, Iā€™m going to give you an overview of Mux Extension, talk about the benefits of adding Mux to your Cosmic Bucket and show you how to install it to your Bucket in a few clicks.
Single page app that allows you to showcase you digital products and accepts credit card payment online. Very similar to the marketing sales funnel, but done in simple single page sales funnel.
In this tutorial, we're going to build a digital shrine to the great musical artists of our time and to listening to music by album - the way great artists meant it to be listened to! In the process, you'll learn a little bit about Cosmic JS, React, CSS Grid, Flexbox, Material UI and Spotify's aweso
Exporting Cosmic JS-powered data to CSV is easy with this new extension šŸš€
Cosmic JS Extensions make it possible to extend the functionality of Cosmic JS for both the developer and editor. A recent example built by the Cosmic JS Community is the Google Analytics Extension, which allows team members to easily see analytics insights directly in their Cosmic JS Bucket.
Build a static web page for your company or personal portfolio using the Gatsby static site generator and the Cosmic JS Content Management Platform.