August 26, 2020

Building your blog on JA(M)Stack

JAMStack is a new approach to develop and deliver your website to the world, by decoupling the ancient monolithic web server in a frontend and backend part. With services like Netlify, Github Pages, Contentful or Netlify CMS there are free (but restricted) services that allow you to host a blog.

As of today (2022/05/31) the blog uses Gatsby instead of Angular for its frontend presentation. Stay tuned for a future blogpost about the migration/setup with Gatsby.

Most modern Content Management Systems like Typo3, WordPress, or Drupal are monolithic by default, which means their frontend and backend are strongly bound together. They usually run on one single server and are thereby not scalable, rigid, and not very fast. JAMStack stands for client-side JavaScript, reusable APIs, and prebuilt Markup. There are countless explanations on what JAMStack is. Therefore I'm not going into further detail. A good in-depth explanation can be found here.

Our architecture

For building our blog, we slightly alter this definition. Instead of using prebuilt Markup (which is usually generated by a static site generator) we use dynamic calls to our backend and create the Markup on the fly. Just like in this blogpost, we will build the frontend with angular and Netlify. The backend is a headless CMS called Contentful. Contentful is a service that lets you create a content model and entries. These entries can then be retrieved via an API.

Blog Sequence Diagram

Setting up contentful

Our backend setup is pretty straight forward. You first have to create an account, and from there the introduction takes you to your first steps as a content creator. For our basic setup we only need one Content Model. We call this Content Model Blogpost and create the diffent fields as following:

contentful blogpost fields All the fields are required (when creating a blogpost) except the tags field.

  • Title: The title of the blogpost, shown in the overview, detail and as browser title
  • Slug: The speaking part of the URL route, for search engine optimiziation
  • Hero Image (I forgot to rename this field😅): Main image of the entry
  • Description: Short text for this entry, introduction shown on overview
  • Body: Main text for this blogpost
  • Publish Date: Release Time/Date of the entry
  • Tags: Different tags, as string, for future use

Description and body are both set as Markdown-Text, which allows to insert links, different types of headers, lists, images and much more. The last step we have to do to make our backend ready for frontend use is to generate Preview-API keys. Go to "Contentful -> Settings -> API Keys" and generate a new key in "Content delivery / preview tokens". You're now all set to implement your first frontend application for this contentful backend.

Web frontend setup

A detailed introduction on how to use contentful with angular can be found here.

Environment and node packages

In our angular frontend we start by setting up the environment for our service. Modify your environment.ts file likewise:

export const environment = {
  production: false,
  contentful: {
    space: 'D09YN7bBzsn',
    accessToken: 'IX4PTu2rEWE7Ck708pmPjTuFHVhi4607yQEIImN0grZHgxSP8qai',
    contentTypeIds: {
      blogPost: 'blogPost'
    }
  }
};

There is a helper contentful plugin, to load into your angular application.

$ npm install contentful --save

To show our content on the application we also need a Markdown to html converter.

$ npm install ngx-md --save

Retrieve and display data

We create a new service that will load our blogposts into the angular application. Create a local contentful client object to establish a connection to the server:

private cdaClient = createClient({
  space: environment.contentful.space,
  accessToken: environment.contentful.accessToken
});

Use that client to retrieve all entries/blogposts, map each image and order them by creation date descending:

getBlogPosts(query?: object): Promise<Entry<any>[]> {
  return this.cdaClient.getEntries(Object.assign({
    content_type: environment.contentful.contentTypeIds.blogPost,
    order: '-sys.createdAt'
  }, query))
  .then(res => {
    return res.items.map(entry => {
      const imageLink = (entry.fields as any).heroImage;
      if (imageLink) {
        (entry.fields as any).image = res.includes.Asset.find(image => image.sys.id === imageLink.sys.id);
      }
      return entry;
    });
  });
}

We show these entries in the overview:

<ng-container *ngIf="blogPosts.length; else emptyState">
  <article *ngFor="let blogPost of blogPosts">
    <a [routerLink]="['/blog', blogPost.fields.slug]">
      <img [src]="blogPost.fields.image.fields.file.url" />
    </a>
    <h3>{{ blogPost.sys.createdAt | date:'medium' }}</h3>
    <h2>
      <a [routerLink]="['/blog', blogPost.fields.slug]">
        {{ blogPost.fields.title }}
      </a>
    </h2>
    <div class="description" Markdown>{{ blogPost.fields.description }}</div>
  </article>
</ng-container>

The Markdown attribute on the description field is an angular directive by ngx-md. The routerLink attribute is created with the slug of the blogpost. This means we have so called "speaking" URLs for better search engine optimization.

For our detail view we create a function to retrieve a blogpost by its slug:

getBlogPostBySlug(slug: string, query?: object): Promise<Entry<any>> {
  return this.cdaClient.getEntries(Object.assign({
    content_type: environment.contentful.contentTypeIds.blogPost
  }, Object.assign({'fields.slug': slug}, query)))
  .then(res => {
    return res.items[0];
  });
}

We show this blogpost with markdown and image on top:

<article>
  <header>
    <h3>{{ blogEntry.sys.createdAt | date:'medium' }}</h3>
    <h1>
      {{ blogEntry.fields.title }}
    </h1>
    <div class="description" Markdown>
      {{ blogEntry.fields.description }}
    </div>
  </header>
  <img [src]="blogEntry.fields.heroImage.fields.file.url" />
  <div class="body" Markdown>{{ blogEntry.fields.body }}</div>
</article>

Our router setup is as simple as it gets:

const routes: Routes = [
  { path: '', redirectTo: '/blog', pathMatch: 'full' },
  {
    path: 'blog',
    loadChildren: () => 
    import('./blog-overview/blog-overview.module').then(m => m.BlogOverviewModule)
  },
  {
    path: 'blog/:slug' ,
    loadChildren: () => 
    import('./blog-entry/blog-entry.module').then(m => m.BlogEntryModule)
  }
];

The full code of the frontend is hosted on github.

Next steps

The blog should be fully functional now, but there are a lot of things to add or to expand. These are some ideas that will be done in the future and might be documented as a blogpost:

  • Ability to leave a comment on any blogpost (serverless function support)
  • State managment for angular, cache blogposts
  • Responsive layout (image support etc)
  • Move from angular to gatsby or jekyll for better SEO support
  • Pagination on the overview
Made with ♥️ and  Benjamin Mathieu, 2020