Community Articles Creating the Cosmic JS Contentful Importer

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 article I’m going demonstrate how to publish new Objects containing survey responses in Cosmic JS as they are completed in Typeform. Click here to connect Typeform to Cosmic JS using Zapier.
Cosmic JS makes it easy to deploy your websites and applications to the web. You can deploy an app from any git repository to the Cosmic App Server from any Bucket. Cosmic JS uses Dokku (Docker + Heroku-like deployment) to deploy your app from any GitHub, BitBucket or any other public repo.
Build a documentation app in a afternoon leveraging the powered of Gatsby and Cosmic JS.
In this tutorial I’ll demonstrate how to build an Authentication app using Angular 6 and Cosmic JS. Starting from scratch, learn how to build, configure and deploy this Angular6 Auth app to the Cosmic JS App Server for testing purposes.
A website boilerplate satisfies some common website requirements including dynamic pages, blog articles, author management, SEO ability, contact form and website search.
Learn how I built the Twitter Search Extension to search and save Tweets to your Bucket.

Ready to Get Started?

No payment info required.
Start Now   Contact Sales