Creating the Frontend of a Headless CMS in 10 Steps

We continue our headless CMS journey by creating a blog frontend

In a previous post, we introduced what a headless CMS is and learned how to develop a headless CMS API backend using Flask and Python. Now that we have this backend, we can use any tech to create the frontend, right? So, in this post, we’ll discover how to create a frontend using Angular and Express. The frontend will be consuming all the APIs we created earlier.

What Is Angular and Why Are We Using It?

Angular is an open-source typescript-based frontend technology developed by Google. You can say it is an upgraded version of AngularJS since it is also developed by the same team of developers. We’re using Angular here because it’s one of the most popular JS frameworks for frontend development, and it’s also easy to learn if you know the basics of JavaScript.

Prerequisites:

  • HTML
  • CSS
  • Angular
  • NPM
  • NodeJS
  • Imgur Account with API access

Step –1: Setup

If you don’t have npm and NodeJS, you can download it from here. It’s a simple installation.

Before we begin coding, we have already created the skeleton of the folders and files the project will need. You can install these folders, files, and dependencies by cloning it from Git and running a simple command.

So open your terminal and type:

git clone https://github.com/vyomsrivastava/cms-starter.git

And then:

cd cms-starter

With all the dependencies installed, everything is already configured in the file package.json. To demonstrate, here is the content of the JSON file:

{
  "name": "headless-cms-frontend",
  "version": "1.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~8.0.1",
    "@angular/cdk": "~8.2.3",
    "@angular/common": "~8.0.1",
    "@angular/compiler": "~8.0.1",
    "@angular/core": "~8.0.1",
    "@angular/forms": "~8.0.1",
    "@angular/material": "^8.2.3",
    "@angular/platform-browser": "~8.0.1",
    "@angular/platform-browser-dynamic": "~8.0.1",
    "@angular/router": "~8.0.1",
    "rxjs": "~6.4.0",
    "tslib": "^1.9.0",
    "zone.js": "~0.9.1"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.800.0",
    "@angular/cli": "~8.0.4",
    "@angular/compiler-cli": "~8.0.1",
    "@angular/language-service": "~8.0.1",
    "@types/node": "~8.9.4",
    "@types/jasmine": "~3.3.8",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "^5.0.0",
    "jasmine-core": "~3.4.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.1.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.0",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.15.0",
    "typescript": "~3.4.3"
  }
}

Explanation:

Here we’re listing out our packages that are required by the Angular frontend. We have dependencies like animation, forms, router, and others for a loading animation, creating forms, calling APIs, etc.

Now run the below command to install them:

npm install

Once everything is set up, you’re ready to dive into the code.

Step –2: Admin UI Setup:

In the above step, we have already cloned and installed the dependencies. Now, we’ll have to build a UI to enable an admin to create, edit, publish, and delete blog posts.

On your code editor, open the file admin/admin.component.ts and paste the below code:

import { AuthService } from './../auth.service';
import { DialogBodyComponent } from './../dialog-body/dialog-body.component';
import { MatDialog } from '@angular/material/dialog';
import { Component, OnInit } from '@angular/core';

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

  constructor(private dialog:MatDialog,private auth_service:AuthService) { }

  ngOnInit() {
  }

  open_dialog(message:string){
    const dialogRef = this.dialog.open(DialogBodyComponent,{
      data:{
        message
      },
      width:'550px',
      height:'200px'
    });
    dialogRef.afterClosed().subscribe((confirm:boolean)=>{
      if(confirm){
        this.sign_out();
      }
    })
  }

  sign_out(){
    this.auth_service.logout();
  }

}

Explanation:

Here we have created two functions, open_dialogue and sign_out. The open_dialogue will return a dialogue box when the Signout button is clicked. This is an alert box the admin will see when clicking on Sign Out.

Next, we have to work on the UI from where the admin can publish blog posts. We have to install one more package for a WSYWIG Editor.

On your terminal, paste the below command:

npm install @syncfusion/ej2-angular-richtexteditor --save

This will install a rich text editor that provides many functionalities for formatting the text, such as inserting images between the paragraphs and adding code snippets. This package will convert them into HTML tags.

Now we also have to import this editor in our code to implement it on the UI as well. So open app.module.ts in the editor and paste the below code:

import { RichTextEditorAllModule } from '@syncfusion/ej2-angular-richtexteditor';

Once the above is done, we also have to include the editor in the index.html file. So open the index.html file and paste the below code:

<link href="https://cdn.syncfusion.com/ej2/material.css" rel="stylesheet" />

To make the text editor reusable, we can make it a component so that we can use it anywhere we want to. So open admin/add-blog/add-blog.component.html in the code editor and paste the below code:

<div class="add-blog-input-content">
  <ejs-richtexteditor [(value)]="content" height="auto" placeholder="Add Paragraph..."></ejs-richtexteditor>
</div>

Step –3: Imgur API Setup:

We will be using Imgur to upload our images. Go to Imgur.com, create your account, and get your API credentials. Make sure you’re selecting OAuth2 without a callback URL.

Now, copy your CLIENT_ID from Imgur, open api-calls/feature-image.service.ts and paste the below code:

import { HttpClient,HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class FeatureImageService {
  private imgur_url:string = 'https://api.imgur.com/3/image';
  private client_id:string = 'YOUR_CLIENT_ID';
  constructor(private http:HttpClient) { }

  upload_image(image_file:File){
    let formData = new FormData();
    formData.append('image',image_file, image_file.name);


    let headers = new HttpHeaders({
      "authorization": 'Client-ID '+this.client_id
    });

    return this.http.post(this.imgur_url , formData, {headers:headers});
  }
}

Paste your CLIENT_ID in the above code.

Explanation:

We have created a function here to accept the image file and upload it on the Imgur server using the API.

Step –4: Add Blog Components

Now once the Imgur APIs are done, we can create different components and open admin/add-blog/add-blog.component.ts in the code editor and paste the below code:

import { AlertDialogBodyComponent } from './../../alert-dialog-body/alert-dialog-body.component';
import { BlogService } from './../../api-calls/blog.service';
import { FeatureImageService } from './../../api-calls/feature-image.service';
import { TagComponent } from './../../material-components/tag/tag.component';
import { Component, OnInit,ViewChild } from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import { DialogBodyComponent } from 'src/app/dialog-body/dialog-body.component';

@Component({
  selector: 'app-add-blog',
  templateUrl: './add-blog.component.html',
  styleUrls: ['./add-blog.component.css']
})
export class AddBlogComponent implements OnInit {
  private selectedFile:File;
  private preview_image:any;
  private tags: [];
  private title:string;
  private content:string;
  private blog_id:string;
  private show_spinner: boolean = false;
  @ViewChild(TagComponent, {static:false}) childReference:any;
  constructor(private image_service: FeatureImageService, private blog_service:BlogService, private dialog:MatDialog) { }

  ngOnInit() {
  }

  ngAfterViewInit(){
    this.tags = this.childReference.tags;
  }

  

  processFile(imageInput:any){
    this.selectedFile = imageInput.files[0];
    this.previewImageLoad();
  }

  previewImageLoad(){
    let reader = new FileReader();
    reader.onloadend = e =>{
      this.preview_image = reader.result;
    }
    reader.readAsDataURL(this.selectedFile);
  }

  open_dialog(message:string){
    const dialogRef = this.dialog.open(DialogBodyComponent, {
      width: '550px',
      height: '200px',
      data: {
        message
      }
      
    });
    dialogRef.afterClosed().subscribe((confirm:boolean)=>{
      if(confirm){
        this.submit_blog();
      }
    })
    
  }

  open_alert_dialog(message:string){
    let dialogRef = this.dialog.open(AlertDialogBodyComponent,{
      width:'550px',
      height: '200px',
      data:{
        message
      }
    });
  }

  async submit_blog(){
      this.show_spinner = true;
      const image_data = await this.image_service.upload_image(this.selectedFile).toPromise();
      let blog = {
        title: this.title,
        content: this.content,
        feature_image:image_data["data"].link,
        tags:[]
      }

      this.tags.map((element)=>{
        blog.tags.push(element["name"])
      });

      this.blog_service.add_blog(blog).subscribe((response:any)=>{
        this.blog_id = response.id;
        this.show_spinner = false;
        this.open_alert_dialog(`Blog has been created with the id: ${this.blog_id}`);
        this.title = "";
        this.content = "";
        this.preview_image = "";
        this.tags = []; 
      });

    }
}

Explanation:

Here we have created different functions to perform various operations. Now let’s understand each of them:

open_dialogue: This function will return a dialogue box for user confirmation. This is a very flexible function because we can pass custom messages in this function.

open_alert_dialog: This function will return an alert box for user confirmation. This is also a flexible function and takes custom messages.

processFile: This function takes the images and sets the previewImage so that the images which are being uploaded can be displayed at the same time.

Step –5: Calling the Python APIs

Now that we understood all the above code, it’s time to call our Python APIs. So, open api-calls/blog.service.ts and paste the below code:

export class BlogService {
  private add_blog_url:string = 'https://localhost:5000/add_blog';
  constructor(private http:HttpClient) { }

  add_blog(blog_props:Object){
    return this.http.post(this.add_blog_url,blog_props);
  }
}

Explanation:

Here we’re calling our Python API, and the endpoint is add_blog, which is used to create a new blog post.

Step –6: Display All Published Posts in the Admin UI

Now we have to create a page that will display all the published blog posts and will let you to delete and edit the article. So now open admin/all-blogs/all-blogs.component.ts and paste the below code:

import { AlertDialogBodyComponent } from './../../alert-dialog-body/alert-dialog-body.component';
import { DialogBodyComponent } from 'src/app/dialog-body/dialog-body.component';
import { BlogService } from './../../api-calls/blog.service';
import { Component, OnInit, OnChanges, ChangeDetectorRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';

interface Blog{
  title:string,
  content:string,
  feature_image:string,
  tags: []
}

@Component({
  selector: 'app-all-blogs',
  templateUrl: './all-blogs.component.html',
  styleUrls: ['./all-blogs.component.css']
})



export class AllBlogsComponent implements OnInit {
  private blogs: Array<Blog> = [];
  private deleted_blog_id:string;
  private show_spinner:boolean = false;
  constructor(private blog_service:BlogService, private dialog:MatDialog) {}

  ngOnInit() {
    this.load_all_blogs();
  }
  
  load_all_blogs(){
    this.blog_service.get_all_blogs().subscribe((response:any)=>{
      response.all_blogs.forEach((element:any) => {
        this.blogs.push(element);
      });
    })
  }

  open_dialog(message:string, blog_id:string): void {
    let dialogRef = this.dialog.open(DialogBodyComponent,{
      data: {
        message
      },
      width: '550px',
      height:'200px'
    })

    dialogRef.afterClosed().subscribe((confirm:boolean)=>{
      if(confirm){
        this.delete_single_blog(blog_id);
      }
    });
  }

  open_alert_dialog(message:string){
    let dialogRef = this.dialog.open(AlertDialogBodyComponent,{
      width:'550px',
      height: '200px',
      data:{
        message
      }
    });
  }



  delete_single_blog(blog_id:string){
    this.show_spinner = true;
    this.blog_service.delete_blog(blog_id).subscribe((response)=>{
      if(response){
        this.show_spinner = false;
        this.open_alert_dialog("The blog was successfully deleted");
      }
    })
  }

}

Explanation:

Here, we’re importing the Alert dialog component and the Dialog component that will be rendered on this page. load_all_blogs() will call get_all_blogs() to fetch all the blog posts. If you look in the function delete_single_blog() we’re passing a string into function open_alert_dialog(). Once the blog is deleted, we’re passing a success message. It’ll also stop the spinner; we’re doing so by setting show_spinner = false.

Now open file blog.service and paste the below code:

private get_all_blogs_url:string = 'https://localhost:5000/blogs';
private delete_blog_url:string = 'https://localhost:5000/delete_blog/';

get_all_blogs(){
    return this.http.get(this.get_all_blogs_url);
}

delete_blog(id:string){
    return this.http.delete(this.delete_blog_url + id);
}

Explanation:

Now we have two functions, get_all_blogs() and delete_blog(), in the BlogService to make API calls by setting the required information.

Step –7: Update the Existing Blogs:

So far we have added most of the functionalities in the blog, like adding a blog post, and deleting a blog post. Now we have to add components for updating the existing blog posts. Open admin/update-blog/update-blog.component.ts and paste the below lines of code:

import { FeatureImageService } from './../../api-calls/feature-image.service';
import { AlertDialogBodyComponent } from './../../alert-dialog-body/alert-dialog-body.component';
import { DialogBodyComponent } from 'src/app/dialog-body/dialog-body.component';
import { MatDialog } from '@angular/material/dialog';
import { BlogService } from './../../api-calls/blog.service';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';


interface Blog{
  title:string,
  content:string,
  tags:string[],
  feature_image:any
}

@Component({
  selector: 'app-update-blog',
  templateUrl: './update-blog.component.html',
  styleUrls: ['./update-blog.component.css']
})



export class UpdateBlogComponent implements OnInit {
  private blog_id: string;
  private selectedFile:any;
  private show_spinner:boolean = false;
  private blog_props: Blog = {
    title: "",
    content: "",
    tags: [],
    feature_image: ""
  }

  constructor(private active_route:ActivatedRoute, 
              private dialog:MatDialog, 
              private blog_service:BlogService,
              private image_service:FeatureImageService
              ) { } 

  ngOnInit() {
      this.active_route.params.subscribe((response)=>{
      this.blog_id = response.id;
      this.get_blog_info();
      });
      
  }

  processFile(imageInput:any){
    this.selectedFile = imageInput.files[0];
    this.previewImageLoad();
  }

  previewImageLoad(){
    let reader = new FileReader();
    reader.onloadend = e =>{
      this.blog_props.feature_image = reader.result;
    }
    reader.readAsDataURL(this.selectedFile);
  }

  open_dialog(message:string){
    const dialogRef = this.dialog.open(DialogBodyComponent, {
      width: '550px',
      height: '200px',
      data: {
        message
      }
      
    });
    dialogRef.afterClosed().subscribe((confirm:boolean)=>{
      if(confirm){
        this.submit_blog();
      }
    })
    
  }

  open_alert_dialog(message:string){
    let dialogRef = this.dialog.open(AlertDialogBodyComponent,{
      width:'550px',
      height: '200px',
      data:{
        message
      }
    });
  }

  get_blog_info(){
    this.blog_service.get_single_blog(this.blog_id).subscribe((response:any)=>{
      this.blog_props.title = response.single_blog.title;
      this.blog_props.content = response.single_blog.content;
      this.blog_props.feature_image = response.single_blog.feature_image;
      response.single_blog.tags.forEach((element:any) => {
        this.blog_props.tags.push(element);
      });
    });
  }

  async submit_blog(){
    this.show_spinner = true;
    let image_link:any;
    if(this.selectedFile){
      const image_data = await this.image_service.upload_image(this.selectedFile).toPromise();
      image_link = image_data["data"].link;
    }
    else{
      image_link = this.blog_props.feature_image; 
    }
    
    let blog = {
      title: this.blog_props.title,
      content: this.blog_props.content,
      feature_image:image_link,
    }

    this.blog_service.update_blog(blog,this.blog_id).subscribe((response:any)=>{
      this.blog_id = response.blog_id;
      this.show_spinner = false;
      this.open_alert_dialog(`Blog with the id: ${this.blog_id} has been updated`);
    });

  }

}

To define a service, open blog.service again and paste the below line of code:

private get_single_blog_url:string = 'https://localhost:5000/blog/';
private update_blog_url:string = 'https://localhost:5000/update_blog/';

get_single_blog(blog_id:string){
  return this.http.get(this.get_single_blog_url + blog_id);
}

update_blog(blog_props: Object, blog_id:string){
  return this.http.put(this.update_blog_url + blog_id, blog_props);
}

Explanation:

If you walked through the previous article where we created the API for this headless CMS, we have defined an API endpoint that will return the blog details by accepting the id of the blog post. We’ll hit that endpoint here.

Step –8: Creating Home Page:

We’ll first create components of homepage in homepage/homepage.component.ts. So, open homepage/homepage.component.ts and add the below lines of code:

import { BlogService } from './../api-calls/blog.service';
import { Component, OnInit } from '@angular/core';

interface Blog{
  title:string,
  feature_image:string,
  created_at:string,
  content:string
}

@Component({
  selector: 'app-homepage',
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
  private all_blogs: Blog[] = [];
  constructor(private blog_service:BlogService) { }

  ngOnInit() {
    this.load_all_blogs();
  }

  load_all_blogs(){
    this.blog_service.get_all_blogs().subscribe((response:any)=>{
      response.all_blogs.forEach((element:any) => {
        this.all_blogs.push(element);
      });
    })
  }

}

Explanation:

We’re importing a blog.service here so that we can make API calls. On initialization, it’ll call the function load_all_blogs(), which will call get_all_blogs() from the blog.serviceand fetches the blog posts and then runs a forEach loop which pushes all the blog posts on the page.

We have one more page, blog-details, for which we have to add one more component in blog-details.component.ts.

import { ActivatedRoute } from '@angular/router';
import { BlogService } from './../api-calls/blog.service';
import { Component, OnInit } from '@angular/core';

interface Blog{
  title:string,
  content:string,
  feature_image:string,
  tags: string[],
  created_at: string
}

@Component({
  selector: 'app-blog-details',
  templateUrl: './blog-details.component.html',
  styleUrls: ['./blog-details.component.css']
})
export class BlogDetailsComponent implements OnInit {
  private blog_id:string;
  private blog_props : Blog = {
    title: "",
    content:"",
    feature_image: "",
    tags: [],
    created_at:""
  };
  constructor(private blog_service:BlogService, private active_route:ActivatedRoute) { }

  ngOnInit() {
    this.active_route.params.subscribe((response)=>{
      this.blog_id = response.id;
      this.get_blog_details();
    })
    
  }

  get_blog_details(){
    this.blog_service.get_single_blog(this.blog_id).subscribe((response:any)=>{
      this.blog_props.title = response.single_blog.title;
      this.blog_props.content = response.single_blog.content;
      this.blog_props.feature_image = response.single_blog.feature_image;
      this.blog_props.created_at = response.single_blog.created_at;
      response.single_blog.tags.foreach((element:any)=>{
        this.blog_props.tags.push(element);
      });
    });
  }

}

Explanation:

Here again, we’re importing blog.service to make API calls. On initializing the page, we’re calling get_blog_details(), which will render blog details on the page.

Step –9: Login Page Setup

We’re all done with the frontend setup. Now we just have to work on the login page. So, first we’ll add the login component. Open login/login.component.ts and paste the below code:

import { Router } from '@angular/router';
import { AuthService } from './../auth.service';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  private email:string;
  private password:string;
  private hide:true;
  constructor(private auth_service:AuthService,private router:Router) { }

  ngOnInit() {
  }
  
  submit_form(){
    let credentials = {
      email:this.email,
      password:this.password
    }
    this.auth_service.login(credentials).subscribe((response:any)=>{
      if(response.token){
        localStorage.setItem('auth_token', response.token);
        this.router.navigate(['/admin']);
      }
      
    })
  }

}

Explanation:

Here we have created a function submit_form(), which will call the login API endpoint and submit the login page’s login credentials. It’ll return the auth token, which we’ll save in the local storage.

Now open the auth.service page and paste the below code:

import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {JwtHelperService} from '@auth0/angular-jwt';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private login_url:string = 'https://localhost:5000/login';
  constructor(private http:HttpClient, private jwtHelper:JwtHelperService,private router:Router) { }

  login(credentials:Object){
    return this.http.post(this.login_url, credentials);
  }

  is_logged_in(){
    const token = localStorage.getItem('auth_token');
    if(!token) return false

    const is_expired = this.jwtHelper.isTokenExpired(token);

    return !is_expired;
  }

  logout(){
    localStorage.removeItem('auth_token');
    this.router.navigate(['/login']);
  }
}

Explanation

Here we’re doing many things.login() is used for logging in, and is_logged_in() is used to check if the admin is logged in or not by checking the local storage value of auth_token.

We also have to install the JWT handler, so run the below command on the terminal:

npm install @auth0/angular-jwt

Now inside the imports array of app.module.ts paste the below code:

JwtModule.forRoot({
   config: {
      tokenGetter: function tokenGetter() {
          return localStorage.getItem('auth_token');},
      whitelistedDomains: ['localhost:5000'],
      blacklistedRoutes: ['https://localhost:5000/login']
   }
})

Step –10: Safeguarding the Login Path:

Now we have to make the login page only accessible when the user is not logged in. Open auth-guard.service.ts and paste the below line of codes:

import { AuthService } from './auth.service';
import { Router, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';


@Injectable({
  providedIn: 'root'
})
export class AuthGuardService implements CanActivate{

  constructor(private auth_service:AuthService, private router:Router) { }

  canActivate(next:ActivatedRouteSnapshot,state:RouterStateSnapshot):boolean{
    if(this.auth_service.is_logged_in()) return true
    
    this.router.navigate(['/login']);
    return false;
  }
}

Once the above code is inserted, paste the below code in app-routing.module.ts:

{path:'admin', component:AdminComponent,
    canActivate:[AuthGuardService],
    children:[
      {path:'all-blogs', component:AllBlogsComponent},
      {path:'add-blog', component:AddBlogComponent},
      {path:'update-blog/:id',component:UpdateBlogComponent}
    ]
}

Explanation:

We’re checking here if the admin is logged in using the function is_logged_in(). If the user is logged in, it will allow access to inner admin pages or go to the login page.

Finally, everything is done. We’re assuming that the flask server is already running on a different terminal — we just have to run the Angular. So run ng serve on the terminal and… TADA!!! You can access the Angular pages. Make sure to add some blog posts, or else it’ll only display a blank page on the root URL. You can also clone the ready-made code from here: https://github.com/vyomsrivastava/cms-frontend.

Final Words

In this post, we have created a complete CMS system using Flask and Angular. Of course, this is just a starting point and is very flexible for many other functionalities. To expand this basic setup, you can add many other functionalities, such as a user management system to permit different roles like Author, Contributor, Subscriber, or Admin. Of course, you can also improve the user interface.

We hope this helps get you started building a frontend for your headless CMS API backend. If you’re facing any code issues, please comment down below, and we’ll try to help you out.