Deploy Statiq to Azure Static Web App

How to deploy a Statiq-based static site as an Azure Static Web App

Published on Tuesday, 18 May 2021

Deploy Statiq to Azure Static Web App

Azure Static Web Apps just went GA a few days ago. I took the opportunity to deploy my blog as a "SWA" (Static Web App). In this post, I am writing about my experience with the service and how you can deploy a Statiq-based site.

What is "Azure Static Web App"?

It is a new Azure service which lets you deploy a static app (only static resources like images, HTML, JavaScript, CSS etc.). These resources will then automatically be served from "points of Presence" (PoP) around the world to make sure every user gets the site delivered in the fastest possible way. SWA uses Azure's CDN for this.

Serverless Batteries Included

However, with your Static Web App, you can also deploy an API in the form of Azure Functions. You may either use functions deployed with your app, or reference an existing Function App.

Security

To make your site secure, SWA offers authentication with Azure AD, GitHub and Twitter, so you can cordon-off parts (or all) of your site to authenticated users in roles you define.

For this to work, SWA also supports free TLS certificates for your custom domains - even for your Apex Domains!

Great Developer Experience

With GitHub and Azure DevOps integration, you can easily build a CI/CD process with staging environments for your pull requests.

This is complemented with a Visual Studio Code extension and a CLI tool for local tests.

Pricing

The free edition should cover most of your needs. Only if you have one of the following requirements, a monthly fee and bandwidth charges (>100GB) are due:

  • >2 Domains
  • Authentication
  • Bring your own Azure Function (separate plan, not the one included with SWA)
  • Higher storage requirements

Check out the pricing page for more details.

What is "Statiq"?

Statiq is a .NET Static Generator Framework written in C#.

Essentially, you configure pieces of code which are added to Pipelines. These pipelines process input (like Markdown, images, JSON) to an output.

Statiq offers Statiq.Framework, as the base and Statiq.Web as the "happy path" on top to generate static sites.

I am using Static.Web for my Blog. Since I usually do not have to change any code when publishing articles, I do not need to recompile all the time, but just run dotnet run to re-generate my content.

Check out my previous blog post on how I use Statiq.

Setting up a GitHub Actions Workflow

If you use SWA with GitHub, SWA creates a GitHub Actions workflow in your repo. This sets up two things:

  1. Build and deploy for pushes into the main branch
  2. Builds for opening, syncing, re-opening and closing of Pull Requests to the main branch

Here is the YAML that was generated for me (StructedSiteAssembler is my subdirectory for the blog application)

name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/StructedSiteAssembler" # App source code path
          api_location: "api" # Api source code path - optional
          output_location: "output" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          action: "close"

This specific config is needed, because it contains all the magic to deploy to SWA, which is not exposed to the public. This bothers me a bit, because it actually locks down the platform quite a bit and makes it harder to debug. But I never mind that, as GitHub Actions provides the flexibility needed, as you will see later.

The docker image used here has another thing included: Oryx. It detects which development stack you are using and generates & runs a build script for you. The output of which is then deployed to SWA.

The actual "SWA magic" is in here:

      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/StructedSiteAssembler" # App source code path
          api_location: "api" # Api source code path - optional
          output_location: "output" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

.NET is detected and built correctly, but Statiq requires to run the app afterwards to actually create the output. This is where the "out of the box experience" did not work for me.

The Solution

While Oryx correctly determines a lot of popular stacks, Statiq is not very widely adopted (yet!), so we have to do a little workaround. Thankfully, the build within the GitHub Action Azure/static-web-apps-deploy@v1 can be skipped. Thus, it is only used to upload the artifact and deploy to the Azure Static Web App.

To do this, we just need to add a few lines which

  • Set up .NET
  • Restore packages
  • Build the app
  • Run the app to generate output

between - uses: actions/checkout@v2 and - name: Build And Deploy:

            - uses: actions/setup-dotnet@v1
              with:
                  dotnet-version: 5.0.203
            - run: dotnet restore
            - run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
            - run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
            - name: Build And Deploy

However, as we still need the Build And Deploy step to deploy to SWA - but not the actual "Build" step with Oryx - we have the option to disable the build. Looking at the docs, we can learn that you can use skip_app_build: true to skip the build. But it also requires us to set the app_location to the actual output directory. The value of output_location is ignored, if you set skip_app_build: true.

Given my folder structure:

.
├───.github
│   └───workflows
└───StructedSiteAssembler
    ├───bin
    ├───input
    │   ├───images
    │   ├───posts
    │   └───scss
    ├───output
    │   ├───images
    │   ├───img
    │   ├───js
    │   ├───posts
    │   ├───scss
    │   ├───tags
    │   └───vendor
    ├───theme
    │   └───input
    │       ├───img
    │       ├───js
    │       ├───scss
    │       └───vendor
    └───wwwroot

my config would looks like this:

                  app_location: "/StructedSiteAssembler/output" # App source code path
                  api_location: "" # Api source code path - optional
                  output_location: "" # Built app content directory - optional
                  skip_app_build: true

An here is the full Action setup for my blog:

name: Azure Static Web Apps CI/CD

on:
    push:
        branches:
            - main
    pull_request:
        types: [opened, synchronize, reopened, closed]
        branches:
            - main

jobs:
    build_and_deploy_job:
        if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
        runs-on: ubuntu-latest
        name: Build and Deploy Job
        steps:
            - uses: actions/checkout@v2
              with:
                  submodules: true
            - uses: actions/setup-dotnet@v1
              with:
                  dotnet-version: 5.0.203
            - run: dotnet restore
            - run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
            - run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
            - name: Build And Deploy
              id: builddeploy
              uses: Azure/static-web-apps-deploy@v1
              with:
                  azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
                  repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
                  action: "upload"
                  ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
                  # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
                  app_location: "/StructedSiteAssembler/output" # App source code path
                  api_location: "" # Api source code path - optional
                  output_location: "" # Built app content directory - optional
                  skip_app_build: true
                  ###### End of Repository/Build Configurations ######

    close_pull_request_job:
        if: github.event_name == 'pull_request' && github.event.action == 'closed'
        runs-on: ubuntu-latest
        name: Close Pull Request Job
        steps:
            - name: Close Pull Request
              id: closepullrequest
              uses: Azure/static-web-apps-deploy@v1
              with:
                  azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
                  action: "close"

Conclusion

Azure Static Web Apps is a great service to host a site, or even a browser-based game! With it's very fast response times all over the World thanks to Azure's CDN, the tight integration with Azure Functions and built-in authentications makes it a great, flexible and very low cost choice for many use-cases.

I am certain we will be seeing tighter integration with more frontend frameworks and improved monitoring in the coming months.

Further Reading