Build a PWA With Ionic, Angular, and the WordPress REST API

To keep my dev skills polished, I always like to have a little weekend side-hustle. This weekend, I decided to start building a Progressive Web App (PWA) for desktop and mobile that delivers WordPress content using Ionic, Angular, and the WordPress API. This is my “developer log”, which you can use as a tutorial for learning more about these technologies or for just understanding the code base, which I’m sharing on GitHub. You can fork the code and customize it for yourself, or share in the development and post pull requests with your improvements.


  • Before proceeding, make sure the latest version of Node.js and npm are installed. I write with the assumption that you are familiar with these two systems.
  • You will also need an editor or an IDE (integrated development environment). A free IDE that is  fantastic is VS Code from Microsoft.
  • You’ll need Git if you want to work with my code base. I recommend using Git even if you don’t intend to share code as this will allow you to snapshot stable check-points, keep a journal of commits, and roll back when you screw things up. It’s just good practice.
  • Essential references are provided here as they will undoubtably be helpful along the journey:
  • Optionally, Docker, if you want to setup your local WordPress development environment in the way I describe in this post.
  • Evolving source code for this project can be found on GitHub

Install the Ionic CLI

The Ionic CLI is a command-line utility that we’ll use for creating and developing our Ionic / Angular app. This needs to be installed globally by using the following command in a terminal or console. For Window’s it’s recommended to open an Admin command prompt. For Mac/Linux, run the command with sudo.

$ npm install -g ionic

If you already have the Ionic CLI installed and you want to update it to the latest version, enter the following command:

$ npm install -g ionic

Start an app

Now we can use the Ionic CLI to create a blank app or a starter-app using a  pre-made app template. We’re going to use the sidemenu template, but others are available such as blank, and tabs. Change to the directory where you want to create your project and execute the following command. If you’re following along, you can name the app whatever you want. Naming things is hard, so I’m just going to go with “ionic-ng-wp-client”.

$ ionic start ionic-ng-wp-client sidemenu --type=angular

This will start a command-line wizard sequence:

  • Install the free Ionic Applow SDK and connect your app? (Y/n) Answer n, no. This can also be added later if desired.

Run the App

Now we can run the Ionic app to see what the sidemenu template has given us as a starting point. Change into the newly created project’s directory and run the app…

$ cd ./ionic-ng-wp-client $ ionic serve

The ionic serve command will spawn the app in your web browser and it should look something like this:

Isn’t that awesome? With just a couple of simple commands, we’ve got a running app and we’re ready to go!

Now, let’s take a quick peek at the project structure that was created for us.

The src/ directory has items such as the index.html file, configuration files for tests, an assets/ folder for images, and the main app/ directory for the app’s code. The src/app/ directory contains the root app component and module as well as additional directories that contain features such as pages, components, services, etc.

The project has automatically been initialized as a Git repository (notice the .gitignore file) and an Initial commit has been performed on the local repo. From here on out, you’ll see a badge similar to the following, which indicates a point at which I’ve committed code.

Initial commit

Setup a WordPress development environment

In order to ensure the app works with any general WordPress instance, I want to setup a completely fresh local WordPress instance for development.

If you already have a local development environment with WordPress installed, or if you just want to develop against your production instance (not recommended), you can skip this section. If you’ve done WordPress development in the past, you might have used something like MAMP. My preference is to use Docker containers, which makes it easier (I think) to have isolated environments for specific purposes like this.

For this setup, I am generally following the procedure outlined by A. Tate Barber in his post on Medium called Local WordPress Development with Docker: 3 Easy Steps. If you don’t have Docker installed, of course, you’ll need to do that first.

Setup docker-compose

In the project’s root directory, create a file called docker-compose.yml with the following contents.

version: "2" services: my-wpdb: image: mariadb ports: - "8081:3306" environment: MYSQL_ROOT_PASSWORD: ChangeMeIfYouWant my-wp: image: wordpress volumes: - ./wp:/var/www/html ports: - "8080:80" links: - my-wpdb:mysql environment: WORDPRESS_DB_PASSWORD: ChangeMeIfYouWant

Note: Docker compose files are YAML files that require indentation. Getting the indentation wrong can lead to errors when running the `docker-compose` command.

This file will create the LAMP stack we need to run WordPress using two containers – one for the database and one for the web server. We are using official Docker images and one is the official WordPress image. It’s also has a shell script that automatically connects to the database container, creates a database, clones WordPress files, and sets up the wp-config based on environment variables that we pass to it (see the official image page for all the options).

With this docker-compose file in place, we can create and run the environment with a single command. Simply execute the following command from within the project directory.

$ docker-compose up -d

After you run this command and it creates the containers, you can execute the following command to verify that they are running:

$ docker ps -a

You should now have a wp/ directory in your project root, which has all the files for WordPress mapped into the container’s /var/www/html directory. It should now look something like this:

With the containers running, you can now navigate to http://localhost:8080 to complete the WordPress setup.

If you want to see the output of the containers (to monitor for errors), you can follow their logs with:

$ docker-compose logs -f

If you want to stop and restart the containers, run:

$ docker-compose stop $ docker-compose up -d

Complete the WordPress setup in the browser. Since this is just a local development environment, I would recommend using a username and password that are simple to remember. wordpress / wordpress, for example.

We’ve just made a major step in the process, so it’s a good time for a Git commit. First, however, we should add the wp/ directory to the .gitignore file and create a that explains to other developers how to run the local docker environment. It’s also a good time to add any other helpful information to the README file, which will be the landing page for the project in GitHub. And then, commit…

Add docker-compose.yml and

Populate WordPress with dummy content

To build and test the app, we need to have a variety of content in our WordPress instance. For this, we’ll use a handy plugin called FakerPress, which will create dummy content, featured images, random meta information and the like. To install it, navigate to your WordPress Dashboard > Plugins, select Add New, search for “FakerPress” and click Install Now.

Activate the plugin and then navigate to FakerPress > Posts in the WordPress menu. Set a number of posts to generate and the date range for them. I’m choosing 42 posts for now, with a date range of 2010-09-07 to 2018-09-07. For everything else, I’m keeping the defaults for now.

Also, in the Meta Field Rules section, you should enter actual range values for the possible width and height sizes for the images. The plugin doesn’t appear to use default values, I learned, so you’ll end up with broken featured images in your posts if you don’t enter hard values as shown below…

Click Generate and then give the plugin time to do its thing and voila! We now have a bunch of dummy content to work with…

Enable the WordPress REST API

Right now, if you navigate to http://localhost:8080/wp-json/wp/v2/posts, you will get 404 Not Found. This has to do with the permalinks setting for your site. The /wp-json/wp/v2 endpoint is available when your site is set up to use the custom permalink setting.

In the WordPress dashboard, navigate to Settings > Permalinks and choose the Custom Structure with /%postname%/ in the field after the host and port. Save changes and try the URL again.

This time, you should see a lot of JSON in your browser, which confirms that the REST endpoint is now accessible.

[{"id":1,"date":"2018-09-08T18:33:28","date_gmt":"2018-09-08T18:33:28","guid":{"rendered":"http:\/\/localhost:8080\/?p=1"},"modified":"2018-09-08T18:33:28","modified_gmt":"2018-09-08T18:33:28","slug":"hello-world","status":"publish","type":"post","link":"http:\/\/localhost:8080\/hello-world\/","title":{"rendered":"Hello world!"},"content":{"rendered":"<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"_links":{"self":[{"href":"http:\/\/localhost:8080\/wp-json\/wp\/v2\/posts\/1"}],"collection":[{"href":"http:\/\/localhost:8080\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/localhost:8080\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/localhost:8080\/wp-json\/wp\/v2\/users\/1"}],"replies": ...etc., etc,...

Store the API URL

Next, we we’ll configure the REST API endpoint URL in source code using environment constants. This will allow us to use a different endpoint URL for different environments and also have just one place for users to change the configuration to suit their own needs.

Add the API endpointURL to src/app/environments/ and to src/app/environments/environment.ts as shown below.


export const environment = { production: true, endpointURL: 'http://localhost:8080/wp-json/' };


export const environment = { production: false, endpointURL: 'http://localhost:8080/wp-json/' };

Later, this allow us to get the URL from our environment by using:

import {environment} from '../../environments/environment'; const ENDPOINT_URL = environment.endpointURL;

Commit the change…

Store the API URL

Stub out the DataService

At this point, we’re ready to get cooking! To begin with, we’ll need a service that can intermediate between the REST endpoint and our components in the app. We can use the Ionic CLI to generate the necessary stub files and, since this will be a shared component, we’ll put it in a shared/ directory. We do this simply by running the following command.

$ ionic generate

This presents a little menu as shown below.

Select the service option and hit enter. Then, for the Name/path of service:, type shared/Data and hit ENTER.

This generates a TypeScript file for the service and a test specification.

  • src/
    • app/
      • shared/
        • data.service.spec.ts
        • data.service.ts

Take a look at data.service.ts. Here’s what the Ionic CLI generated for us.

import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class DataService { constructor() { } }

In keeping with good Angular style practice, the CLI appended Service, so the name is DataService.

We can add the import and a constant for the ENDPOINT_URL, which we’ll need later when we flesh out the service.

import { Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; const ENDPOINT_URL = environment.endpointURL; @Injectable({ providedIn: 'root' }) export class DataService { constructor() { } }

Install RxJS dependencies

To fetch data from the endpoint, we’ll be using the Angular HttpClient. It makes use of Observables instead of Promises, so we’ll need to install RxJS dependencies. Run the following command:

npm install rxjs && npm install rxjs-compat

We also need to add the HttpClientModule to the imports in src/app/app.module.ts so that app.module.ts should then look like this:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; import { SplashScreen } from '@ionic-native/splash-screen/ngx'; import { StatusBar } from '@ionic-native/status-bar/ngx'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [AppComponent], entryComponents: [], imports: [ BrowserModule, HttpClientModule, IonicModule.forRoot(), AppRoutingModule ], providers: [ StatusBar, SplashScreen, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy } ], bootstrap: [AppComponent] }) export class AppModule {}

Next, we’ll add methods to fetch post data from the API endpoint. Update src/app/shared/data.service.ts with the following code…

import { Injectable } from '@angular/core'; import { environment } from '../../environments/environment'; import { HttpClient } from '@angular/common/http'; import 'rxjs/add/operator/map'; import {of} from 'rxjs/observable/of'; const ENDPOINT_URL = environment.endpointURL; @Injectable({ providedIn: 'root' }) export class DataService { items: any[]; constructor(private http: HttpClient) { } /** * Gets a page of posts or all posts formerly fetched */ getPosts(): any { if (this.items) { // The of operator accepts a number of items as parameters, and returns an Observable that emits each of // these parameters, in order, as its emitted sequence. In this case, we will only be returning this.items // to any subscriber. return of(this.items); } else { // http.get() creates an observable. // map() creates and returns its own new observable from the observable that http.get() created, // which we can then subscribe to. Therefore, we can subscribe to the result of this method. // // The Map operator applies a function of your choosing to each item emitted by the source Observable, and // returns an Observable that emits the results of these function applications. return this.http.get(ENDPOINT_URL + 'wp/v2/posts?_embed').map(this.processPostData, this); } } // A place for post-processing, before making the fetched data available to view. processPostData(data: any[]) { // Do post-processing code here (if useful) this.items = data; return this.items; } }

Now we we’ll add methods for the Home view to communicate with the DataService. Update src/app/home/ with the following code…

import {Component, OnInit} from '@angular/core'; import {DataService} from '../shared/data.service'; @Component({ selector: 'app-home', templateUrl: '', styleUrls: [''], }) export class HomePage implements OnInit { items: any[]; constructor(public dataService: DataService) { } ngOnInit() { console.log('>> ngOnInit'); this.dataService.getPosts().subscribe((data: any[]) => { this.items = data; console.log('ngOnInit() > items: %o', this.items); }); } }

Now if you run ionic serve and inspect the console, you should see that post items are being returned from WordPress and logged to the console…

Sweet! Now we’ll put those items into the view using ion-list and ion-item. Update src/app/home/ with the following code…

<ion-header> <ion-toolbar> <ion-buttons slot="start"> <ion-menu-button></ion-menu-button> </ion-buttons> <ion-title> Home </ion-title> </ion-toolbar> </ion-header> <ion-content padding> <ion-list> <ion-item *ngFor="let item of items"> <span [innerHTML]="">{{item.title.rendered}}</span> <div class="item-note" slot="end"> {{}} </div> </ion-item> </ion-list> </ion-content>

Now, when you run the app with ionic serve, you should see something similar to the following.

The app runs fine, but if we run ng test at this point, we’ll get some errors, because we also need to make the HttpClientModule available to the test specs for the HomePage and DataService.

In src/app/home/, import the HTTPClientModule and then add the HttpClientModule into an imports stanza in the TestBed configuration, similar to how it’s done in app.module.ts. When you’re done, the file should look like this:

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { HomePage } from './'; describe('HomePage', () => { let component: HomePage; let fixture: ComponentFixture<HomePage>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ HomePage ], imports: [ HttpClientModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(HomePage); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });

We need to do the same for the data.service.spec.ts file. When you’re done, the file should look like this:

import { TestBed } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { DataService } from './data.service'; describe('DataService', () => { beforeEach(() => TestBed.configureTestingModule({ imports: [ HttpClientModule ], })); it('should be created', () => { const service: DataService = TestBed.get(DataService); expect(service).toBeTruthy(); }); });

Now, if you run ng test, all tests should pass. Great! It’s a good time to commit this stable check-point. I’m doing so with the following commit message:

Stub out the DataService

Wrapping up

For this weekend side-hustle, I provided initial steps for building a Progressive Web App (PWA) for desktop and mobile that delivers WordPress content using Ionic, Angular, and the WordPress API. So far, all we’ve done is set the stage, but I’m posting the process in parts so that it might be more immediately helpful to anybody who needs it. I hope it has been helpful to you.

Remember that the evolving source code can always be cloned from GitHub at

I hope you’ll join me on this project, contribute comments, or your own improvements. I intend to take the project further and will continue my “developer log” here, so stay tuned!

Next: Part 2 >>