Building & Hosting a semi-static website with Strapi, Gatsby.js and Azure

by Dominik Deschner
Building & Hosting a semi-static website with Strapi, Gatsby.js and Azure

Introduction

The world wide web is filled with plenty of applications and delightful content ranging from scientific contributions over dicussion boards to company websites. A high fraction of this content is static or updated very irregulary. Also many websites are powered by content management systems that allow easy setup and management. By design those systems not only delivery static html to the consumer but generate the served content for every request over and over again by fetching dynamically content from databases or other content sources. Which results in wasting valuable ressources like computational power and energy. To transform into a more sustainable usage of ressources this is a huge challenge to solve.

The big picture

Meanwhile there are different approaches to efficiently serve semi-static content, like the headless content management system Strapi, Gatsby.js and Microsoft Azure. In this article we will setup & deploy a blog website. The following picture shows the architectural approach.

Semi-static content delivery
Semi-static content delivery

The whole content of our blog prebuilt into static html and served via the Azure Static Webapp service, which acts as our CDN (content delivery network). We use Strapi to easily create and edit content which is then consumed by a Gatsby.js which transforms the content to html, preprocesses pictures and adds interactivity. Github Actions act as our glue, since we utillize it to build & deploy updates of our page to azure. This is triggered when

  1. Changes to the source code occure
  2. The content of our page is changed in the Strapi cms

So every change to the content or the structure/design of our page issues a new build cycle that then is deployed as static content to the Azure Static Web App. 

Strapi setup

First we bootstrap a new strapi project by following the getting started guide. Then we also need to install an azure blob storage provider:

npm install strapi-provider-upload-azure-storage

Then we add the following to the config/plugin.ts file. So we use the local file system when developing locally and use our Azure Blob Storage container when running in production mode.

export default ({env}: any) => {
    const isProd = env('NODE_ENV', 'development') === 'production';
    if (isProd) {
        console.log("Using Azure Storage");
        return {
            upload: {
              config: {
                provider: "strapi-provider-upload-azure-storage",
                providerOptions: {
                  account: env("STORAGE_ACCOUNT"),
                  accountKey: env("STORAGE_ACCOUNT_KEY"),//either account key or sas token is enough to make authentication 
                  sasToken: env("STORAGE_ACCOUNT_SAS_TOKEN"),
                  serviceBaseURL: env("STORAGE_URL"), // optional
                  containerName: env("STORAGE_CONTAINER_NAME"),
                  defaultPath: "assets",
                  cdnBaseURL: env("STORAGE_CDN_URL"), // optional
                },
              },
            },
          }
    }

    return {};
};

Now you can issue to start Strapi. With default settings the UI is located at http://localhost:1337.

npm run build && npm run develop

There we are guided through the initial setup of creating an administrator user. After logging in we need to create an API token which we use to authenticate Gatsby.js against. This process is described in the following screenshot. Please note it is necessary to use the token type "Full access". Save the token in a secure location we will use it later.

Create API Token in Strapi for use with Gatsby
Create API Token in Strapi for use with Gatsby

Then we need to create the data types we need to manage for our web page. Strapi distinguishes between single types, which means that there can be only one instance of the type. Typically we use single types to define the content of a single page like the imprint or an about page. And then there are collection types, which can have zero or more entries. For our example we will use a collection type to store our articles. The configuration process is illustrated in the following pictures.

Initial configuration of article collection
Initial configuration of article collection
Add two fields to our article
Add two fields to our article

Gatsby.js setup

First we create a new gatsby project by following the quickstart guide. Then install the strapi source plugin and follow the instructions in order to set everything up. Hint: Here you need to use the API token we created previously. so we can consume data from our backend. Make sure to include the "article" collection type in the configuration so gatsby pulls the data into its' storage. 

Then we can start our gatsby application by running

npm run develop

You can observe the build log for the following output to know that everything has worked as expected. Furthermore we should see the datatype allStrapiArticle in the gatsby graphql explorer.

info Starting to fetch data from Strapi - /api/about with populate=%2A
info Starting to fetch data from Strapi - /api/imprint with populate=%2A                                                              
info Starting to fetch data from Strapi - /api/page-setting with populate=%2A      

Azure Setup

In order to leverage the cloud for our web page we will create a new ressource group in the Azure Portal where the following services are created:

  1. App Service Plan
  2. App Service #1 (Strapi)
  3. App Service #2 (strapi-webhook-actions-proxy)
  4. PostgreSQL
  5. Static Web App

Since Strapi does not support async database initialization there is no easy way to use Azure Managed Identity so we are forced to authenticate the database via username/password. 

Github Actions

Last but not least we need to orchestrate the deployment pipline. I use two distinct pipeline definitions one for deploying code changes to the repository and then a pipeline that is triggered by Strapi when the backend data is changed.

Please note: I am using a mono repo where Strapi and gatsby source is stored. Therefor I use dorny/paths-filter to only deploy relevant changes to the according services. 

Repository CI/CD:

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Deploy web app to Azure Static Web Apps

env:
  APP_LOCATION: '/' # location of your client code
  OUTPUT_LOCATION: './frontend/public' # location of client code build output
  GATSBY_STRAPI_API_URL: ${{ vars.GATSBY_STRAPI_API_URL }}
  STRAPI_TOKEN: ${{ secrets.STRAPI_TOKEN }}

on:
  push:
    branches:
      - main

permissions:
  issues: write
  contents: read

jobs:
  build_and_deploy_frontend:
    runs-on: ubuntu-latest
    name: Build and Deploy
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            fe:
              - 'frontend/**'
              - 'frontend/*'
      - name: Set up Node.js version
        if: steps.changes.outputs.fe == 'true'
        uses: actions/setup-node@v1
        with:
          node-version: '18.x'
      - name: npm install, build, and test
        if: steps.changes.outputs.fe == 'true'
        working-directory: ./frontend
        run: |
          npm install
          npm run build --if-present

      - name: Deploy Frontend
        if: steps.changes.outputs.fe == 'true'
        uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: ${{ env.APP_LOCATION }}
          output_location: ${{ env.OUTPUT_LOCATION }}

  build_and_deploy_cms:
    env:
      NODE_ENV: production
      AZURE_CMS_WEBAPP_NAME: strapi-app
      AZURE_WEBAPP_PACKAGE_PATH: './cms'

    runs-on: ubuntu-latest
    name: Build and Deploy CMS
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true
      - uses: dorny/paths-filter@v2
        id: changescms
        with:
          filters: |
            cms:
              - 'cms/**'
              - 'cms/*'
      - name: Set up Node.js version
        if: steps.changescms.outputs.cms == 'true'
        uses: actions/setup-node@v1
        with:
          node-version: '18.x'
      - name: npm install, build, and test
        if: steps.changescms.outputs.cms == 'true'
        working-directory: ./cms
        run: |
          npm ci
          npm run build --prod

      - name: Deploy CMS
        if: steps.changescms.outputs.cms == 'true'
        uses: azure/webapps-deploy@v2
        with:
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          package: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'
          app-name: ${{ vars.AZURE_CMS_WEBAPP_NAME }} # Replace with your app name

Content CD:

name: Deploy Gatsby to Azure Static Web Apps

env:
  APP_LOCATION: '/' # location of your client code
  OUTPUT_LOCATION: './frontend/public' # location of client code build output
  GATSBY_STRAPI_API_URL: ${{ vars.GATSBY_STRAPI_API_URL }}
  STRAPI_TOKEN: ${{ secrets.STRAPI_TOKEN }}

on:
  repository_dispatch:
    types: [strapi_updated]
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build_and_deploy_frontend:
    runs-on: ubuntu-latest
    name: Build and Deploy
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true

      - name: Cache gatsby cccontent
        uses: actions/cache@v3
        with:
          key: gatsby-${{ hashFiles('./frontend/package-lock.json') }}
          path: |
            ~/.frontend/node_modules
            ~/frontend/.cache
            ~/frontend/public

      - name: Set up Node.js version
        uses: actions/setup-node@v1
        with:
          node-version: '18.x'
      - name: npm install, build, and test
        working-directory: ./frontend
        run: |
          npm install
          npm run build --if-present
      - name: Upload artifacts
        uses: actions/upload-artifact@v2
        with:
          name: static-site
          path: ./frontend/public/**

      - name: Deploy Frontend
        uses: Azure/static-web-apps-deploy@1a947af9992250f3bc2e68ad0754c0b0c11566c9
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: ${{ env.APP_LOCATION }}
          output_location: ${{ env.OUTPUT_LOCATION }}

Strapis webhook format is not compatible with github actions. In order to make it work we can use & deploy strapi-webhook-actions-proxy which marshalls the payload for us. This is also deployed. Then we can create a webhook in our strapi installation that targets the freshly deployed webhook-actions-proxy. 

Conclusion

We just build our own static site application that brings similar convenience like a classic content management system but with plenty advantages like blazing fast page loads, SEO and a smaller and more substainable fingerprint. So contributing our part to save the planet does not only feel good but also provides us with better user experience. Doesn't that sound great?

Posted on 10.08.2023 07:30, 16042 views