Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Progressive Web App Content Management With Contentful

Episode 65 written by Jeff Delaney
full courses and content on fireship.io


Full source code repo.

In this lesson, we will integrate Contentful with Angular 5. As an added bonus, I will show you how to use Firebase Cloud Functions and Cloud Messaging to broadcast push notifications when a new piece of content is published.

Let’s take a look at how content management systems (CMS) have evolved over the last couple decades. In the 1990’s we had static websites. In the 2000’s we had WordPress. Today we have programmable content with Contentful.

Create a content space on contentful

Big thank you to Frantic for suggesting this topic - check out their work for world-class web development inspiration.

Contentful Birds Eye View

Contentful allows you to manage content across multiple platforms. It breaks down content into programmable chunks. For example, a blog post could be represented as an object with a title, featured image, author, body, and tags array.

The benefit of breaking down content like this is that you can use it across multiple platforms. Your product might include a website, mobile app, and desktop app - but content is managed from a single source. This makes it easy for your team members to update content and build validation rules that will be consistent across your infrastructure.

Contentful is a powerful tool, and I recommend taking some time to get comfortable with its API. Watching their 30-min introduction demo is a great place to start.

Step 1: Create a Space

To get started, you need to create a space. A space is just a container to keep your concerns organized in a unified package. For example, you would likely want a separate space for blog posts and e-commerce products.

Create a content space on contentful

Step 2: Create a Content Type

For this tutorial, I am going to create a Lesson model in Contentful that mirrors the content you are reading right now. A content type is an interface or blueprint for any digital content you can envision - blog post, advertisement, author profile, tweet, photo, product, etc.

You build out your content type by adding various fields to it. In this case, our Lesson will need to have a title, episode number, timestamp, body, and a tag list.

A major benefit of building content this way is that you can standardize and validate each field.

Create a content type and add some fields to it

Step 3: Create Some Unique Content

Now that you have your content blueprint laid out you can start creating real content. Each field will have a form input based on the corresponding field type. Long text is written in markdown format, which needs to be converted to HTML when we get into Angular.

Create custom content in the contentful web app

Pretty simple - we now have a consistent model for lesson content and can broadcast it to multiple apps.

App Setup

I am starting from a brand new Angular app. Its only dependencies are the Contentful JavaScript SDK and marked (marked is used to convert markdown to HTML).

ng new lessonApp
npm install contentful
npm install marked

API Keys

Next, we need to add the API token and space ID to the Make sure to use the Content Delivery API token - not the content management token. I am keeping track of the credentials in my environment.ts file.

export const environment = {
production: false,

contentful: {
spaceId: 'YOUR_SPACE',
token: 'YOUR_TOKEN'
}
};

Displaying Content with the Contentful NPM Client

Now that we have a basic Angular app and some Contentful content, we need to connect the two together.

contentful.service.ts

The Contentful service will initialize the Contentful client and provide methods to request data from the API.

ng g service contentful --module app

The getContent method makes the HTTP call the the lesson entry, which returns a Promise. To make it play well with Angular, I convert it to an Observable then map it down to its fields, which is an object with the actual data we want to display.

I also added a method called logContent that you can use to see the full JavaScript object in the browser console.

Don’t worry about the markdownToHtml method for now as it will be used in the next section.

import { Injectable } from '@angular/core';
import * as contentful from 'contentful';
import { environment } from '../environments/environment';
import { Observable } from 'rxjs/Observable';

import * as marked from 'marked';

@Injectable()
export class ContentfulService {

private client = contentful.createClient({
space: environment.contentful.spaceId,
accessToken: environment.contentful.token
})

constructor() { }

// console logs a response for debugging
logContent(contentId) {
this.client.getEntry(contentId)
.then(entry => console.log(entry) )
}

// retrieves content mapped to its data fields
getContent(contentId) {
const promise = this.client.getEntry(contentId)
return Observable.fromPromise(promise).map(entry => entry.fields)
}

// convert markdown string to
markdownToHtml(md: string) {
return marked(md)
}

}

app.component.ts

The app component will inject the service, then request the content as an Observable.

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ContentfulService } from './contentful.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.sass']
})
export class AppComponent implements OnInit {

lesson$: Observable<any>;

constructor(private contentful: ContentfulService) { }

ngOnInit() {
// this.contentful.logContent('some_id') // to console log the content for debugging
this.lesson$ = this.contentful.getContent('some_id')
}

}

In the HTML, we can unwrap the Observable with the async pipe and display its fields accordingly.

<div *ngIf="lesson$ | async as lesson">
<h1>{{ lesson.title }}</h1>
<time>Published on {{ lesson.createdAt | date: 'fullDate' }}</time>
<hr>

<h3>Tags</h3>
<span *ngFor="let tag of lesson.tags">
{{ tag }}
</span>


<!-- markdown string -->
{{ lesson.body }}

</div>

Markdown to HTML

One issue that we’re currently facing is that Contentful returns the body text as a string in markdown format. Angular does not know how to read markdown, so we need a way to convert it to HTML.

Let’s create a pipe that can convert markdown to HTML in Angular.

npm install marked --save
ng g pipe mdToHtml

md-to-html.pipe.ts

The pipe will call the method markdownToHtml method from the service, which tells marked to convert the string into HTML.

import { Pipe, PipeTransform } from '@angular/core';
import { ContentfulService } from './contentful.service';

@Pipe({
name: 'mdToHtml'
})
export class MdToHtmlPipe implements PipeTransform {

constructor(private contentful: ContentfulService ) {}

transform(value: string): any {
return this.contentful.markdownToHtml(value);
}

}

Then update the app component to use the pipe’s outputted HTML.

<div [innerHTML]="lesson.body | mdToHtml">

Only bind to the innerHTML property if you fully trust the source of the HTML. If users generate this content, you will need to sanitize their input to avoid XSS vulnerabilities.

Triggering Push Messages via Webhooks

Contentful also sends webhooks when your content changes. You can use webhooks to invoke Firebase Cloud Functions with HTTP triggers. This section will provide a Firebase Cloud Function for sending out push notifications for a topic, but you will need to follow the Push Notification lesson for an end-to-end working example.

Build the Cloud Function

First, initialize functions in your project.

firebase init functions

This function assumes that you have subscribed users to receive messages based on a certain topic. In this example, they are subscribed to the lesson topic. You can read more about FCM topic messaging in the official docs.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();


exports.topicNotifications = functions.https.onRequest((req, res) => {

// Contentful will send the entry data as the request body
const lesson = req.body.fields
const topic = 'lessons'

// Message details for end user
const payload = {
notification: {
title: 'New Lesson Posted',
body: `AngularFirebase.com posted a new lesson called ${lesson.title}`,
icon: 'https://goo.gl/Fz9nrQ'
}
}

// Send messages via FCM
return admin.messaging().sendToTopic(topic, payload)
.then(_ => {
// successful response
res.status(200).send('Lesson subscribers have been notified')
})
.catch(err => {
res.status(400).send('Messages failed to send')
});
});

Deploy the function, then copy the URL endpoint that it returns to you.

firebase deploy --only functions

Update Webhooks in Contentful

Now we need to tell Contentful to send webhooks to our Cloud Function. Go into space setting -> webhooks and click Add Webhook. Copy and paste the deployed cloud function endpoint in the URL field. Also, select Only selected events and check the box that intersects Entry and Publish.

Update webhooks in contentful for Firebase Cloud Functions

Now create a new post and you should see the webhook activity logged in Contentful and in Firebase. If you have Firebase Cloud Messaging enabled, your users should receive notifications on their subscribed devices.

The End

You now have a cross-platform content management system for your Angular PWA. Programmable content with Contentful is an excellent choice for developers who hate the tedious task of re-deploying multiple apps each time some static content changes.