Building with asp.net core, aurelia and contentful
In a previous blogpost i confessed my newfound love for Contentful. A headless CMS that exposes content through simple restful json APIs. I got so worked up about it that I decided that I needed to build something with it just to try it out. As I’m an avid fan of the asp.net community standup the choice for backend technology was easy. Frontend was another story though. I initially decided to write the app using angular, but then switched mid-building. I’ve followed the development of the Aurelia framework closely, but never really had a chance to use it in production. As it’s almost ready for a release candidate I decided to switch to it and give it a try as well.
But first things first. As I mentioned in the previous post Contentful exposes their content through a couple of different APIs, the one we will be focusing on in this instance is the Content Delivery API. This is the API responsible for delivering us our content from Contentful. It’s a fast read-only API that delivers content through a global CDN to minimize latency. You access it through simple GET requests and the response is delivered as JSON for content and as files for other content types (images, video etc.).
To be allowed access to the content you need to provide an access token with your request, either as a query string parameter or in the request header. This is a simple and flexible approach, but immediately begs the question; How do we secure our content if we have different levels of authorization for our application? For starters you should probably never expose your authentication token directly to your end user unless you’re absolutely certain that all content you have should be publicly accessible. I decided to wrap all calls to the contentful API in a controller like this:
public class ContentController : Controller
{
private readonly string _accesstoken = "ACCESSTOKENHERE";
private readonly string _spaceId = "SPACEID";
public async Task<IActionResult> StartPage()
{
HttpClient client = new HttpClient();
var response = await client.GetStringAsync("https://cdn.contentful.com/spaces/{_spaceId}/entries/6wUc55hRL26se2iEAS2O42?access_token=${_accesstoken}");
return Content(response, "application/json");
}
}
Nothing too funky going on here, we put our access token in a private variable and provide it with the request we make to the contentful API. We then return the string exactly as provided using the Content method that allows us to specify the content type of the returned response. This lets us hide the access token in our server side code that the client consuming our site content will never have to be aware of.
Lets dissect the actual call to the contentful api some more though. First we have the url to the
contenful cdn: https://cdn.contenful.com/
then we have this part, /spaces/
, what’s that all about?
Contentful puts all content that belongs together in a single space, it’s your logical container for all of your content for the project. I suppose there could be instances where you would spread your content over several spaces, but the most common apporach must be: one project, one space. You will, however, share the space between different applications, so our webapplication and our android app, for example, would certainly share the same space as they would want to consume the same content. A company that has an external web and an intranet would logically have their content in two spaces, one for the content of the web and one for the intranet.
Allright, so we provide our space id to let contentful know from which space we want to fetch content,
then comes the /entries/
part where we specify which entry (or entries as we shall se later on) we want to fetch.
In this case I want to fetch the startpage that happens to have the id of 6wUc55hRL26se2iEAS2O42. Then we specify our access
token (I’ve put it in the query string for simplicity, but you could as mentioned above hide this detail away in a header instead) and
fire off the request. So we’re essentially telling contentful: Get me the entry with id 6wUc55hRL26se2iEAS2O42 from the space with
id SPACEID and here’s my access token so you know I’m actually allowed to get this content.
Contentful then responds with a bunch of json, like this:
{
"sys": {
"space": {
"sys": {
"type": "Link",
"linkType": "Space",
"id": "SPACEID"
}
},
"id": "6wUc55hRL26se2iEAS2O42",
"type": "Entry",
"createdAt": "2016-04-16T12:47:06.483Z",
"updatedAt": "2016-04-22T18:40:45.159Z",
"revision": 3,
"contentType": {
"sys": {
"type": "Link",
"linkType": "ContentType",
"id": "startPage"
}
},
"locale": "sv-SE"
},
"fields": {
"title": "This is the startpage",
"body": "This is the body text."
}
}
Lets quickly go through the anatomy of this response. First we have the "sys"
part which contains system defined metadata of the
entry. In this case which space it belongs to, the id and locale of the entry, when it was created and updated and so on. The
space in turn has it’s own system defined metadata, such as the space id etc. Then there’s the "fields"
part that contains the
fields we’ve created for this contentype. This is the actual content that the application needs to present.
If we now needed to secure certain content we could put up a controller action that requires authorization. Like this:
[Authorize]
public async Task<IActionResult> SecureArticles()
{
HttpClient client = new HttpClient();
var response = await client.GetStringAsync($"https://cdn.contentful.com/spaces/{_spaceId}/entries/?access_token={_accesstoken}&content_type=secureArticlePage&include=1");
return Content(response, "application/json");
}
This is for the secure articles of the application. Here we do not provide an id for the entry, instead we specify that we want
all the entries with the contenttype of secureArticlePage. We also provide an additional query string parameter of include=1
this means that we expect contentful to return not only the entries but also all the related assets of each entry. The number indicates
how many number of levels of related assets to resolve, up to a maximum of 10. The default is 1, which means we could actually
omit it in this case, but I left it there for clarity. This could be a potential security concern for us though, as we might
accidentally fetch related assets that needs authorization with an unauthorized request. If for example our startpage had links
to one of our secure article pages it might get returned in the json response. To make sure that no related assets is
returned we need to specifically set the include query string parameter to 0. This should be done for all requests where you
might potentially have sensitive assets linked from an unsecured entry.
When completed our content controller would contain the following actions for this simple app:
public async Task<IActionResult> StartPage()
public async Task<IActionResult> Articles()
public async Task<IActionResult> Article(string id)
public async Task<IActionResult> SubArticle(string id)
[Authorize]
public async Task<IActionResult> SecureArticle(string id)
[Authorize]
public async Task<IActionResult> SecureArticles()
I decided to split my content in a couple of contenttypes: startpage, article, subpage and securearticle. There are of course
other ways to do this, you could for example have one contenttype and differentiate them with fields such as "secure"
, "subpage"
etc.
I felt that for the sake of simplicity and this very basic app, it would be prudent to have them as different content types,
even though they are very similar structurally.
With our backend setup it’s time to focus on getting some content out and present it to the user. As mentioned above I decided to go with Aurelia as a front end framework as I really love the simple conventionbased approach they take when it comes to modeling our views.
My index.cshtml for my homecontroller looks something like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] &mdash; Cool site</title>
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/lib/font-awesome/css/font-awesome.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css"
asp-fallback-href="~/lib/font-awesome/css/font-awesome.min.css" asp-fallback-test-class="fa"
asp-fallback-test-property="font-family" asp-fallback-test-value="FontAwesome" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body aurelia-app>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
System.import("aurelia-bootstrapper");
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
System.import("aurelia-bootstrapper");
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
</body>
</html>
As you can see there’s basically nothing in there. Just a few tag helpers for scripts in different environments. Then there’s the hook
into aurelia, the attribute aurelia-app
on the body tag. This tells aurelia where we want our application to be loaded. By convention
it will look for an app.js
and an app.html
file in the root of your application. The app.html
will contain our navigation for
the application and a place to show our content, you could think of it in terms of a _layout.cshtml or a masterpage. Lets start by
having it display all of our articles in a navigation layout. Here’s how our app.js
and app.html
could look in a first simple version:
<template>
<div class="container body-content">
<div class="row">
<div class="col-sm-3 hidden-xs">
<ul class="nav nav-pills nav-stacked">
<li role="presentation">
<a href="#/">Home</a>
</li>
<li role="presentation" repeat.for="article of articles">
<a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
</li>
</ul>
</div>
<div class="col-sm-9">
<div if.bind="router.isNavigating" class="text-center">
<span class="fa fa-spinner fa-spin fa-4x"></span>
</div>
<router-view></router-view>
</div>
</div>
</div>
</div>
</template>
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'
@inject(HttpClient)
export class App {
articles = [];
constructor(http) {
http.configure(config => {
config
.useStandardConfiguration()
.withBaseUrl('/');
});
this.http = http;
}
configureRouter(config, router) {
config.title = 'This is the app title';
config.map([
{ route: ['', 'startpage'], name: 'startpage', moduleId: './Views/startpage', nav:true, title: 'Startpage'},
{ route: 'articles/:slug', name:'articles', moduleId: './Views/article', nav:false, title: 'Article'}
]);
this.router = router;
}
activate() {
return this.http.fetch('content/articles')
.then(response => response.json())
.then(data => {
this.articles = data.items;
})
}
Aurelia supports ECMAscript 6 (and even 7) and that makes creating a view model class extremely straightforward. Simply export a
class with the same name as your view and aurelia will by convention understand that they belong together. The App class first sets up
the aurelia http fetch client (Aurelia recently introduced the aurelia-fetch-client as an alternative to the old aurelia-http-client ,
if you’re new to the fetch api I recommend this excellent blog post by David Walsh) with a baseurl. Then we have a method called
configureRouter. This is a hook into the aurelia routing system to tell Aurelia what routes we will be having in our application and
how we will be navigating to them. In our case we’ll start with two simple routes, one for our startpage and one for our articles.
If we look closely at our app.html we can see that we have an element called <router-view>
this is where the view of our current route
view will be injected.
Finally we have a method called activate, this method is another hook into aurelia that will be automatically called by the routing system before presenting a view. This means that this is the perfect place to load things like remote resources that need to be available before presenting the view. In our case we call our back end controller action to fetch all of the articles from Contentful.
In our view we then iterate over all the articles and present them in a navigation. This markup
<li role="presentation" repeat.for="article of articles">
<a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
</li>
basically tells aurelia to repeat over the articles array from our App class and create a link for each one using the title of the article as the link text. The route-href attribute is an aurelia custom attribute that automatically creates a link to a specific route with any custom parameter values we want. We'll look at it a bit more detailed below.
Next we need to create models and views for our startpage and our articles. That is just as easy as creating our App, we just create a view/viewmodel pair. Here’s the view for the startpage:
<template>
<require from="/components/markdown"></require>
<div>
<h2>${title}</h2>
<div innerHTML.bind="body | markdown"></div>
</div>
</template>
Very simple at this stage. Just present the title using aurelias databinding syntax. Then bind the innerHTML of a div to the body property. There’s one noteworthy thing though; Contentful sends the data of rich text fields as markdown. This means that we need to convert it back into HTML if we are to present it in our application. To do that I’ve created a very simple aurelia value-converter using showdown.js that looks like this.
import showdown from 'showdown';
export class MarkdownValueConverter {
constructor() {
this.converter = new showdown.Converter();
}
toView(value) {
return this.converter.makeHtml(value);
}
}
I then import the converter into our view using <require from="/components/markdown"></require>
and when binding a value in aurelia
you can always pass it through a value converter using the "value | converter"
syntax. Which is what we do here
<div innerHTML.bind="body | markdown"></div>
This means that we pass the value of our body markdown from contentful through the showdown makeHtml-method to turn it back into html before presenting it to the user.
The startpage.js file for the viewmodel looks like this:
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'
@inject(HttpClient)
export class startPage {
title = '';
body = '';
constructor(http) {
this.http = http;
}
activate() {
return this.http.fetch('content/startpage')
.then(response => response.json())
.then(data => {
this.title = data.fields.title;
this.body = data.fields.body;
})
}
}
No surprises here, we simply fetch the startpage from contentful in our activate method and set the title and body properties accordingly.
We should next implement a simple view for our article and I’m actually planning on using the same view for subpages and securearticlepages as well. Here’s the view:
<template>
<require from="/components/markdown"></require>
<require from="/components/filesize"></require>
<require from="/components/filetype"></require>
<div>
<h2>${title}</h2>
<div innerHTML.bind="body | markdown"></div>
</div>
<table if.bind="documents.length" class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>File</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr repeat.for="doc of documents">
<th scope="row" class="text-center"><span class="fa fa-lg ${doc.fields.file.contentType | filetype}"></span></th>
<td><a href.bind="doc.fields.file.url" target="_blank">${doc.fields.title}</a></td>
<td>${doc.fields.file.details.size | filesize}</td>
</tr>
</tbody>
</table>
</template>
Pretty much the same with the addition of two new value converters for file size and file type. And a table for displaying any assets that may be linked to the article. Here are the value converters:
export class FilesizeValueConverter {
toView(value){
//Check if it's a number
if(!Number.isSafeInteger(value)){
return value;
}
var byteUnits = ['kB', 'MB', 'GB', 'TB'];
var i = -1;
do{
value = value / 1024;
i++;
}
while(value > 1024)
return Math.max(value, 0.01).toFixed(2) + ' ' + byteUnits[i];
}
}
export class FiletypeValueConverter {
toView(value){
switch(value.toLowerCase()){
case 'application/pdf':
return 'fa-file-pdf-o';
//omitted for brevity
default:
return 'fa-file-o'
}
return 'fa-file-o';
}
}
And here’s the view model for articles:
import {inject} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'
@inject(articleActivator)
export class article {
title = '';
body = '';
subPages = [];
documents = [];
constructor(activator) {
this.activator = activator;
}
activate(params) {
return this.activator.activateArticle('content/article/' + params.slug, this)
}
}
There are mainly two interesting bits here, the first is the activate method that takes a parameter this time and extracts a property called slug from it and appends it to our url for calling our back end. If you remember the routes we specified in our app.js file, this is where we actually use the parameters specified there. This is what the route for articles looked like:
{
route: 'articles/:slug',
name:'articles',
moduleId: './Views/article',
nav:false,
title: 'Article'
}
And the mark-up for creating a route for an article looked like this:
<a route-href="route: articles; params.bind: {slug: article.fields.slug}">
${article.fields.title}
</a>
As you can see we specify the :slug as part of our route when configuring it. We then populate the slug part in our markup from the slug-property of our article. The slug property is of course defined in Contentful, there’s an actual property type for string properties that is specifically for this purpose. Just one little detail that makes me love contentful just a little bit more.
![slug]({{ site.baseurl }}/assets/images/slug.png)
Not only that but as I create my entry and set the title contentful automatically generates the slug property based on that title. Very convenient!
This slug is then used to fetch the correct entry from contentful, we simply filter the entries based on slug and returns the one that match. Lets have a look at how our back-end controller action achieves that.
public async Task<IActionResult> Article(string id)
{
HttpClient client = new HttpClient();
var response = await client.GetStringAsync($"https://cdn.contentful.com/spaces/{_spaceId}/entries/?access_token={_accesstoken}&content_type=articlePage&fields.slug={id}");
return Content(response, "application/json");
}
The magic here is in the &fields.slug={id}
this tells Contentful to filter our entries based on the supplied values of fields.
We could filter on multiple fields and even on multiple values if we would like.
But lets go back to our article.js file, there was another interesting bit in there; We actually imported an articleActivator
class into our article.js. This is done to be able to reuse it across our different types of articles. If we have a look at the
view model for subpages we’ll see how we can leverage that.
import {inject} from 'aurelia-framework';
import {useView} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'
@useView('views/article.html')
@inject(articleActivator)
export class subPage {
title = '';
body = '';
subPages = [];
documents = [];
constructor(activator) {
this.activator = activator;
}
activate(params) {
return this.activator.activateArticle('content/subarticle/' + params.sub, this)
}
}
As you can see it is basically identical with the article.js file, the only difference is that the subPage class uses the @useView
decorator to tell aurelia where to look for its view. By convention it would look for a view with the same name and location as
the view model, but here we specifically tell it to use the article.html view we saw earlier. We also see that it uses the same
articleActivator class as the article.js file. Lets have a look at that activator.
import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'
@inject(HttpClient)
export class articleActivator {
constructor(http) {
this.http = http;
}
activateArticle(url, model) {
return this.http.fetch(url)
.then(response => response.json())
.then(data => {
model.title = data.items[0].fields.title;
model.body = data.items[0].fields.body;
if(data.includes && data.includes.Entry && data.items[0].fields.subPages) {
var subIds = data.items[0].fields.subPages.map(x => x.sys.id);
model.subPages = data.includes.Entry.filter(x =>
subIds.indexOf(x.sys.id) >= 0
);
}
else {
model.subPages = [];
}
if(data.includes && data.includes.Asset && data.items[0].fields.documents) {
var docIds = data.items[0].fields.documents.map(x => x.sys.id);
model.documents = data.includes.Asset.filter(x =>
docIds.indexOf(x.sys.id) >= 0
);
} else {
model.documents = [];
}
})
}
}
What we’ve done here is just moved the part that we would normally put in our activate method into its own separate class so we can reuse it across different classes. This kind of composition in conjunction with the aurelia dependency injection can become quite powerful when used correctly. The activator itself is pretty straightforward, it fetches json data from a url and sets the title, body, documents and subpages properties.
We now have one view model left to implement. The secure article. I think you’ll find it familiar.
import {inject} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'
import {useView} from 'aurelia-framework';
@useView('views/article.html')
@inject(articleActivator)
export class securearticle {
title = '';
body = '';
documents = [];
subPages = [];
constructor(activator) {
this.activator = activator;
}
activate(params) {
return this.activator.activateArticle('content/securearticle/' + params.slug, this)
}
}
Identical to the subpage. Since we’ve moved everything vital into our articleactivator these classes become little more than simple model classes, just as they should be.
The last piece of the puzzle is to add routes for subpages and secure articles to our app.js file. The complete routes configuration would look something like this.
config.map([
{ route: ['', 'startpage'], name: 'startpage', moduleId: './Views/startpage', nav:true, title: 'Startpage'},
{ route: 'articles/:slug', name:'articles', moduleId: './Views/article', nav:false, title: 'Article'},
{ route: 'articles/:slug/:sub', name:'subArticles', moduleId: './Views/subpage', nav:false, title: 'Article'},
{ route: 'securearticles/:slug', name:'secureArticles', moduleId: './Views/securearticle', nav:false, title: 'Article'}
]);
We will also have to add the navigation to be able to reach those routes to our app.html file.
<ul class="nav nav-pills nav-stacked">
<li role="presentation">
<a href="#/">Home</a>
</li>
<li role="presentation" repeat.for="article of articles">
<a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
<ul if.bind="article.fields.subPages && router.currentInstruction.fragment.indexOf('/articles/' + article.fields.slug) == 0">
<li repeat.for="sub of article.fields.subPages">
<a route-href="route: subArticles; params.bind: {slug: article.fields.slug, sub: getSubEntry(sub.sys.id).fields.slug}">${getSubEntry(sub.sys.id).fields.title}</a>
</li>
</ul>
</li>
<li role="presentation" repeat.for="secureArticle of secureArticles" class="${router}">
<a route-href="route: secureArticles; params.bind: {slug: secureArticle.fields.slug}">${secureArticle.fields.title} <span class="fa fa-lock"></span></a>
</li>
</ul>
We then need to add a property for secureArticles to our app.js file secureArticles = [];
and make sure to populate it in our
activate method just as we did with the articles.
Bear in mind that the secure articles will only be accessible by an authenticated and authorized user as the security for them is implemented in the back end.
Whew! Alot of code, but what’s the conclusion?
This mini app took me a couple of hours to put together. Leveraging the contentful back-end with asp.net core was trivial. The challenge came more with structuring my front end. There are lots of pros with the headless CMS approach, but it does put a whole lot of responsibility on the developer. As always with great power comes great responsibility.
The freedom and creativity this model allows is simply unparalleled and I am honestly considering migrating my blog straight over to Contentful. It wouldn’t cost me anything (as the pricing model is highly generous for small applications) and would give me an easy to reach and simple way to update the blog. However I do love Open Live Writer so I might stick to it for a bit longer…