Back to blog
Blog

Creating the Cosmic Contentful Importer

Archived
Community
Kevin Diem's avatar

Kevin Diem

February 03, 2020

Note: This article has been archived as it may contain features or techniques not supported with the latest version of Cosmic.

cover image

The climb is worth it. Photo by Jerry Zhang on Unsplash.

TL;DR

Install the Contentful Importer Extension
View the codebase on GitHub

Creating a Cosmic 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.

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 Bucket. If you have any questions. Connect with the Cosmic community on Slack and on Twitter.