By Lee Martin, on Feb 5, 2019

Developing an Apple Music search into Contentful using UI extensions

I’m currently helping a client setup a year long marketing campaign and we decided to use Contentful as our infrastructure to manage data and content. For the uninitiated, Contentful is a content management system with a very intuitive online interface and library of APIs. You setup your data models like any database and then use the content created to craft any number of apps and services. I honestly didn’t even know headless CMS existed a few months ago and now find myself adding it to almost every proposal. Giving my clients easy access to their own data feels more professional and saves me time.

On this particular project, we have a Content Model of Song which contains the fields of Name (song name) and a list of associated Artists. In addition to displaying our songs, we realized it would be helpful to our users of our web app if we linked to the song on Apple Music and Spotify. A quick idea would be to simply add another text field for each so that we could manually provide the relevant url. This would require my client to open each streaming app, search for the song, copy the link, and return to Contentful to input it. I thought it would be wise to try and build the search into the Contentful UI to save my client a few steps and expand the data we would be storing on each track.

So you could imagine my surprise when I started reading about Contentful’s UI extensions. Amazingly, Contentful provides an SDK for rolling your own customized interface components and it works all through an iframe. My vision for our component was simple. The client should be able to type in the name of a song and receive a list of results back from the Apple Music API. If the client finds what they are looking for, they simply need to click the result and it will update the field accordingly. The field itself would be JSON so we can store a nice object of song data rather than just the url.

Here’s a video of what we’ll be building. Ready to build? The first thing we’ll want to do is build an Apple Music search which I’ll be using Vue.js and CodePen.IO to construct.

Apple Music search

In order to develop with the Apple Music API, you’ll first need a valid developer token. I’ve written a bit about that process here. Once you have a token, you can then use the MusicKit JS framework to begin calling the Apple Music API. Here’s the simplest Vue app ever that does exactly this. First create a single element in your HTML with the id of extension. Then, include both the Vue script and MusicKit JS script. Then type up your simple Vue app.

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const app = new Vue({
  el: '#extension',
  template: `
    <section>
      <input v-model="query" @keyup.enter="search" />
    </section>
  `,
  data() {
    return {
      music: null,
      query: ""
    }
  },
  methods: {
    search() {
      this.music.api.search(this.query, {
        limit: 5, 
        types: 'songs'
      })
      .then(data => {
        console.log(data)       
      })
      .catch(console.error)
    }
  },
  mounted() {
    document.addEventListener('musickitloaded', () => {
      MusicKit.configure({
        developerToken: 'YOUR_DEVELOPER_TOKEN'
      })
      
      this.music = MusicKit.getInstance()
    })
  }
})

Let’s break this down. As soon as your Vue app is mounted, the MusicKit framework configures itself with your developer token. The view itself is simply a single input which accepts a song query. When you press enter on your keyboard, the search method is called. This method then uses the MusicKit instance you configured earlier to query the Apple Music API for the first 5 songs which match the criteria. At the moment these results are simply logged in the console but you can easily evolve this to list them out.

The next step is to add your search as an extension and begin to wire it together with Contentful.

Adding an extension

You can add an extension to your Contentful space by going to Settings > Extensions and clicking “Add extension.” First, give your extension a good name. We’ll call ours “Apple Music Search.” Then, choose the appropriate field type for your use case. We’ll select Object since we plan on using this to build a JSON object. Finally, point to where your extension is hosted or simply drop your code into Contentful’s hosted editor if it will be less than 200kb. Since I built our extension on Codepen, I added a link to the Codepen debug url. Click “Save” and you’ll have successfully added a Extension.

Adding the extension

You must now add the field to your model that will utilize this Extension. For us, that involves adding a new JSON object field called Apple to our Song model. While configuring this new field, select the tab “Appearance” and choose your newly created Extension as the display choice. Once everything is saved, you should see your new search input show up when creating a new Song. Nice work. Type something in, click enter, and check your console for search results. It’s now time to bring in Contentful’s UI Extension SDK.

Integrating UI extension SDK

Let’s add the sdk library script into your application alongside Vue and MusicKit. You may also want to include the Contentful stylesheet if you’re trying to mimic their aesthetic. We’ll then begin evolving our Vue app. First, let’s initialize the SDK within our mounted event alongside the MusicKit configuration and associate it with our data object for easy access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data() {
  return {
    extension: null,
    ...
  }
},
...
mounted() {
  window.contentfulExtension.init(extension => {
    this.extension = extension
    
    this.extension.window.startAutoResizer()
  })
  ...
}

Next we’ll create an additional Vue component just for the song results. This will allow us to reuse the layout for both a search result and search selection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Vue.component('song', {
  props: ['song', 'set'],
  template: `
    <div class="song" @click="setSong">
      <div class="artwork">
        <img :src="song.artwork" height="40" width="40" />
      </div>
      <div class="title">
        <h1>{{ song.name }}</h1>
        <h2>{{ song.artist }}</h1>
      </div>
      <div class="actions" v-if="set">
        <button @click="removeSong">
          <cf-icon name="delete"><svg width="14" height="14" viewBox="-1 -1 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M4.846 7l-3.77-3.77L0 2.155 2.154 0 3.23 1.077 7 4.847l3.77-3.77L11.845 0 14 2.154 12.923 3.23 9.153 7l3.77 3.77L14 11.845 11.846 14l-1.077-1.077L7 9.153l-3.77 3.77L2.155 14 0 11.846l1.077-1.077z"></path></g></svg></cf-icon>
        </button>
      </div>
    </div>
  `,
  methods() {
    setSong() {
      if (!this.set) {
        this.$emit('set-song')
      }
    },
    removeSong() {
      this.$emit('remove-song')
    }
  }
}  

Each song component will display the artwork, artist, song title, and artist name. It can be used as a search result which when clicked would update the currently chosen song. It can also be used as a selected result and includes a Contentful provided delete icon which would remove the song when clicked. Both the set and remove functions will be setup on the parent app.

Next, let’s modify our main app component’s template to use this new song component. First, add a blank array of songs to the data(). Then evolve your Apple Music search to map the result data into the final format we’ll store on Contentful and visualize on our song component. In addition to artwork, artist, name, and url, we’ll store the ISRC and preview url because who know how our app’s might evolve.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
search() {
  this.music.api.search(this.query, {
    limit: 5, 
    types: 'songs'
  })
  .then(data => {
    this.songs = data.songs.data.map(song => {
      return {
        id: song.id,
        artist: song.attributes.artistName,
        name: song.attributes.name,
        artwork: song.attributes.artwork.url,
        url: song.attributes.url,
        isrc: song.attributes.isrc,
        preview: song.attributes.previews[0].url    
  })
  .catch(console.error)
}

Let’s also declare two more methods that we’ll actually update the value of our field on Contentful. One for setting and another for removing the value.

javascript
1
2
3
4
5
6
7
8
methods: {
  set(song) {
    this.extension.field.setValue(song)
  },
  remove() {
    this.extension.field.removeValue()
  }
}

We’re close now. Let’s update our app’s template code to make sure the extension SDK is loaded before showing our app. We’ll also check for an existing value. If we find one, we will simply show a set song component with the delete button. If we do not find a value, we will show the search field and also a list of results if they are present.

html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
template: `
  <section>
    <template v-if="extension">
      <template v-if="extension.field.getValue()">
        <song :song="extension.field.getValue()" set="true" v-on:remove-song="remove"></song>
      </template>
      <template v-else>
        <input v-model="query" placeholder="Apple Music Search" @keyup.enter="search" />
        
        <div id="songs">
          <song v-for="song in songs" :song="song" v-on:set-song="set(song)"></song>
        </div>
      </template>
    </template>
  </section>
`~~~

Now let’s add a little bit of CSS so things look nice in the Contentful interface.

## Styling the UI extension

As I mentioned earlier, you should add Contentful’s base stylesheet as a starting point for your design. If you’re a React user, you may also want to take a look at their shared component library. That looks super promising! For our sake, we’ll just keep it simple and clean up the layout a bit. Here’s what I used:

~~~css
#songs{
  margin-top: 0.5em;
}
#songs .song{
  align-items: center;
  border-bottom: 1px solid #E5EBEC;
  cursor: pointer;
  display: flex;
  height: 60px;
  padding: 0 0.5em;
}
#songs .song:hover{
  background: #f7f9fa;
}
#songs .song .artwork{
  background: #E5EBEC;
  height: 40px;
  margin-right: 0.5em;
  width: 40px;
}
#songs .song .title{
  flex: 1;
}
#songs .song .title h1{
  font-weight: bold;
}
#songs .song .actions button{
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
}
#songs .song .actions button:hover{
  color: #536171;
}
svg{
  color: #8091a5;
  transition: color 200ms ease-in-out;
}

Now do some testing and see how your custom extension has come together. If all goes well, you should see something like this:

Editor experience

Thanks for reading. I hope this technology and the possibilities of it excites you as much as me. Props to Contentful for building a developer friendly platform that inspires this level of customization. As always, leave a comment if you have a question or feedback.

Lee Martin

Developing and designing campaigns and products for 15 years in music. Previously Silva Artist Management, SoundCloud, and Songkick. Currently splitting time between client, contract, consulting, and personal projects.

Community Contributor
Become a contributer