npm install && npm run build. In this post, I’ll work through adding a new build step and using a custom static site generator. To keep things interesting, I’ll use an F# script to generate the site.
Azure Static Web Apps generates and commits the GitHub Actions configuration for your project when you create your project. After that, it’s just a standard GitHub Actions configuration, and you are able to do anything you want with it. Since it’s already been committed to source control, you can freely try out any additions you want. So long as you don’t git push -f and erase the original, you’ll always be able to safely return to a working configuration.
Azure Static Web Apps is intended to build your web app and deploy it, but you can also host a truly static site. Azure Static Web Apps uses a tool called Oryx to infer the kind of app you are deploying and run the appropriate commands to build the application. If it cannot determine what kind of application you have pushed, it will assume it is already built (or just a plain, static site). This is great news! That means you don’t have to try to bypass anything if you want to run another tool.
To demonstrate how this works, I’ll start with a failed build. It’s the heart of TDD, after all. While setting up my new Azure Static Web App, I specified / as my application root. My static files are actually in /src. Here’s what Oryx determined:

Taking a look at the generated GitHub Actions configuration, you can see that there’s a section explicitly marked off as “safe to edit” with comments beginning with ###### marking the boundaries.
| name: Azure Static Web Apps CI/CD | |
| on: | |
| push: | |
| branches: | |
| – master | |
| pull_request: | |
| types: [opened, synchronize, reopened, closed] | |
| branches: | |
| – master | |
| 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@v1 | |
| – name: Build And Deploy | |
| id: builddeploy | |
| uses: Azure/[email protected] | |
| with: | |
| azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_CLIFF_06D430810 }} | |
| 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 you app requirements. ###### | |
| # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig | |
| app_location: "/" # App source code path | |
| api_location: "api" # Api source code path – optional | |
| app_artifact_location: "" # 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/[email protected] | |
| with: | |
| azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_CLIFF_06D430810 }} | |
| action: "close" |
If I change app_location: "/" to app_location: "src", my build and deploy should succeed.

The site itself is nothing exciting. It’s a simple event registration form. It has an event drop-down, but it’s currently empty.

In the same repository, I have a services.csv file that we could load with a JavaScript CSV parsing library like d3 or PapaParse. However, where is the fun in doing this with JavaScript? We’re building a static site, after all, and as that’s far more bleeding edge than the latest front-end JavaScript library, we’re going to populate the drop-down options with some static site generating magic!
Below is the F# script file I added to the /src directory to “build” my static form. It retrieves the data from the CSV and inserts it into the HTML page, writing the modified HTML and other files in the /src folder to the /out folder. I won’t go into the details in this post. If you are familiar with either F# or JavaScript, you should find this fairly readable.
| #r "nuget:AngleSharp" | |
| #r "nuget:Deedle" | |
| open System | |
| open System.IO | |
| open AngleSharp | |
| open AngleSharp.Html.Parser | |
| open Deedle | |
| let src = DirectoryInfo(__SOURCE_DIRECTORY__) | |
| let root = src.Parent.FullName | |
| let dataDir = Path.Combine(root, "data") | |
| let srcDir = src.FullName | |
| let outDir = Path.Combine(root, "out") | |
| // Load and parse CSV | |
| let csvPath = Path.Combine(dataDir, "services.csv") | |
| let df = Frame.ReadCsv(csvPath) | |
| //df.Print() | |
| let now = DateTime.Today | |
| let lastSunday = now.AddDays(-(float now.DayOfWeek)) | |
| let nextSunday = lastSunday.AddDays(7.) | |
| let services = | |
| df.Rows |> Series.filterValues (fun row -> | |
| let date = row.GetAs<DateTime>("Date") | |
| lastSunday < date && date <= nextSunday) | |
| //services.Print() | |
| let svcs = | |
| services |> Series.mapValues (fun row -> | |
| let date = row.GetAs<DateTime>("Date") | |
| let time = row.GetAs<string>("Time").Split(':') | |
| let dt = date.AddHours(float time.[0]).AddMinutes(float time.[1]) | |
| let title = row.GetAs<string>("Title") | |
| let lang = row.GetAs<string>("Language") | |
| sprintf "%s %s (%s)" (dt.ToString("d")) title lang) | |
| //svcs.Print() | |
| // Load and parse HTML | |
| let htmlPath = Path.Combine(srcDir, "index.html") | |
| let context = BrowsingContext.New(Configuration.Default) | |
| let parser = context.GetService<IHtmlParser>() | |
| let doc = using (File.OpenRead htmlPath) parser.ParseDocument | |
| // Set the correct form action | |
| let form : Html.Dom.IHtmlFormElement = downcast doc.GetElementsByTagName("form").[0] | |
| form.Action <- "https://some-function-api.azurewebsites.net/" | |
| // Add the options for the upcoming week. | |
| let frag = doc.CreateDocumentFragment() | |
| for key in svcs.Keys do | |
| let opt : Html.Dom.IHtmlOptionElement = downcast doc.CreateElement("option") | |
| opt.Value <- svcs.[key] | |
| opt.TextContent <- svcs.[key] | |
| frag.AppendChild(opt) |> ignore | |
| let select = doc.GetElementsByTagName("select").[0] | |
| select.AppendChild(frag) |> ignore | |
| // Clean the out directory | |
| if (Directory.Exists outDir) then Directory.Delete(outDir, recursive=true) | |
| Directory.CreateDirectory outDir | |
| // Write the result to the outDir | |
| let outPath = Path.Combine(outDir, "index.html") | |
| // Prettified | |
| using (File.CreateText outPath) (fun writer -> writer.Write(doc.Prettify())) | |
| // Minified | |
| using (File.CreateText outPath) (fun writer -> writer.Write(doc.Minify())) | |
| // Copy style and script | |
| File.Copy(Path.Combine(srcDir, "style.css"), Path.Combine(outDir, "style.css")) | |
| // Write the timestamp | |
| let lastWriteTime = DateTimeOffset(File.GetLastWriteTime(outPath)) | |
| using (File.CreateText(Path.Combine(outDir, "timestamp"))) (fun writer -> writer.Write(lastWriteTime.ToUnixTimeMilliseconds())) |
Once you add a build script and output folder, you’ll need to modify the GitHub Actions configuration again. The easy bit is specifying the output folder. You just need to change the app_location parameter again from app_location: "src" to app_location: "out", or whatever you name your output folder. You may wonder whey app_location instead of app_artifact_location, and the answer is that without a build, Oryx will deploy from the app_location. If it detects and runs a build, it will then look in the app_artifact_location.
The other easy bit is adding a build step. That’s right. That’s all that’s required to use a custom static site generator. If you’ve used GitHub Actions at all, this should be fairly trivial. I added two steps to run the F# script, between the -uses: actions/checkout@v1 and the - name: Build And Deploy section added by Azure Static Web Apps:
- uses: actions/checkout@v1
- name: Setup .NET Core 3.1
id: setupdotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.300
- name: Generate
id: generate
run: dotnet fsi --langversion:preview ./src/build.fsx
- name: Build And Deploy
The first piece sets up .NET Core 3.1, and the second runs the F# script with dotnet fsi --langversion:preview. The --langversion:preview flag enables the use of the NuGet references in the script above, which is really convenient.

The form now provides a list of events in the drop-down for the user to select.

Azure Static Web Apps does not yet support every static site generator and likely never will. However, the platform can certainly be used by any of them, as long as you are willing to make a few tweaks to the GitHub Actions configuration. Trust me when I say that this is still preferable to the alternative of setting up all the pieces separately. (I will likely do a follow-up post on that topic, as well.)
I hope you enjoyed this little jaunt into custom static site generation with Azure Static Web Apps. Feel free to try it for yourself. You can find the source for this post here. Leave a comment if you do try this or find any other interesting tricks to using Azure Static Web Apps. Happy generating!
]]>TL;DR Azure Static Web Apps currently only supports Azure Functions v3 with Node.js v12 and HttpTrigger bindings for APIs. See the docs.
First things first, you’ll need the Azure Functions Core Tools. You can install the latest version 3 with npm:
npm install -g azure-functions-core-tools@3 --unsafe-perm true
From the root directory of your static app project, you’ll want to create a new directory and initialize it for Azure Functions:
mkdir api && cd api
func init ...
When I then tried to create my function, however, I received a nice little error message like the one reported here. The issue is reportedly fixed, though I was still finding it happening a few days ago. This comment provides a workaround. I’ll assume those of you following along either have a newer, working version or applied the suggested fix and proceed.
I’ve been learning Python recently, so I thought it would be fun to have a Python API to run alongside my static app and thereby continue learning two things at once. If you, unlike me, have already read through the ever-expanding documentation, you’ll have stumbled upon the docs for APIs and realized where this post is heading.
I added a function using with the following command:
func init --worker-runtime python
func new -l python -t HttpTrigger -n hello
cd ..
git checkout -b python # create a new branch off master
git add .
git commit -m "Add Python api"
Easy peasy.
I then went to GitHub, opened a PR from my python branch against master, and opened GitHub Actions to watch the magic happen.

The build succeeded, and aside from the weird bit about Enumerating repo to find any files with extension 'csproj'..., everything seemed to go off without any problem.
When I navigated to the Azure Portal, however, there were no functions listed. Looking back at the above you’ll notice a few additional lines that I missed on my first glance:
Error: could not detect the language from repo.
...
Oryx was unable to determine the build steps. Continuing assuming the assets in this folder are already build. ...
Something called Oryx is apparently responsible for the builds and apparently didn’t like that I used Python. I’ll cover more on Oryx in a later blog post.
While waiting on the above to run, I also started another branch using node, as I wanted to get a sense for how this all worked. Lucky for me, b/c if you have by now read the doc linked above, you’ll have learned that only Node will work and only on Azure Functions v3 with only HttpTriggers. I lucked out on two of the three. Actually this is worth calling out, so I’ll quote the docs:
Azure Static Web Apps provides serverless API endpoints via Azure Functions. By leveraging Azure Functions, APIs dynamically scale based on demand, and include the following features:
– Integrated security with direct access to user authentication and role-based authorization data.
https://docs.microsoft.com/en-us/azure/static-web-apps/apis
– Seamless routing that makes the api route available to the web app securely without requiring custom CORS rules.
– Azure Functions v3 compatible with Node.js 12.
– HTTP triggers and output bindings.
Here’s how I created a simple Node Azure Function:
func init --worker-runtime node --language typescript
func new -l typescript -t HttpTrigger -n hello
cd ..
git checkout -b node # create a new branch off master
git add .
git commit -m "Add TypeScript api"
I was inspecting the Python build and trying to figure out what happened while the following build ran. Having assumed this completed, I was surprised to learn that the build failed.

I’ve highlighted the issue. The build didn’t fail, only the deployment. However, that was sufficient to cause the build to be marked as a failed build. It turns out, at least during the preview, you can have only 1 pre-production environment per app. Again, read the documentation. They’ve done a great job making sure to address these types of things if you will, unlike me, just read them.
I removed the python environment from the Azure Portal and triggered a rebuild in GitHub Actions.


Azure Static Web Apps sends a link to the pre-production environment in a comment to your pull request (PR)! This makes it really easy to then navigate to your site and start testing things out. (Note the -2 at the end of the sub-domain, which is the number for the pre-production environment.)


Back in the Azure Portal, you can see the functions and environments deployed for your Azure Static Web App.
To see the available functions, select Functions from the side navigation.

This defaults to the production environment. It will also reset to the Production environment anytime you click the Refresh button.

If you choose a different environment, you’ll see the functions available for that environment.

If you select Environments from the side navigation, you’ll see your production and any pre-production, or Staging, environments you’ve deployed.

The fact that the UI supports multiple gives me hope that the platform will eventually support more than one pre-production environment, meaning that this will be a great platform for supporting feature branch previews, something I’ve been doing with a bunch of scripts in the past.
When you merge your PR to master, you will notice that two GitHub Actions are triggered, one against master, and one against your PR branch.

The master branch build will deploy to your production environment. What’s even more helpful and convenient is that the other build cleans up the pre-production environment!

I love this! Cleaning up unused environments was always a tedious process for my multi-branch support I set up in the past. I really appreciate that Azure Static Web Apps provides this infrastructure.
In the Portal, you’ll notice our Production environment now has the function deployed.

We can also hit the API against the production endpoint.

And of course, our Sapper app continues to cheer us on!

While you can certainly find a much terser walkthrough in the Azure Static Web Apps documentation, I thought this meandering walkthrough worthwhile for showing what may also be possible and what we may expect to see in upcoming releases. I have several planned follow-up posts to show some current work-arounds, as well as some interesting use cases, so stay tuned.
You may also be interested to see what the rest of the community is creating. You can find a collection of samples on GitHub.
]]>I’ll dig into that fantastic combo in later posts. For the present, I’ll stick to something simple: publishing a Sapper generated static website.
Creating a Sapper app is very easy, and you can learn all you need to know through their excellent documentation. For our purposes, you’ll need to have node, npm, and npx installed. Then just follow the instructions from the Sapper site to create a new repo.
# for Rollup
npx degit "sveltejs/sapper-template#rollup" my-app
# for webpack
npx degit "sveltejs/sapper-template#webpack" my-app
cd my-app
npm install
npm run dev & open http://localhost:3000
You should see the following website open in your preferred browser:

The Sapper template already has an export command configured in the package.json. You can generate a static site with npm run export, then launch the static site with npx sirv __sapper__/export.
Azure Static Web Apps will run npm run build on your static site each time you push a commit. Let’s make the necessary changes to the package.json to get Azure Static Web Apps to generate what we want:
"scripts": {
...,
"build": "sapper build --legacy && sapper export --legacy",
...
}
If you run npm run build && npx serve __sapper__/export now, you should see your static site running from __sapper__/export, and it should look the same as above.
NOTE: The --legacy flag configures Sapper to build with polyfill support for older browsers. You can drop this if you only need to support evergreen browsers.
The rest of the steps are reproduced from the Azure Static Web Apps wiki article for Next.js, which I’ve copied below for your convenience.
Azure Static Web Apps will deploy your app from a GitHub repository and it will keep doing so for every pushed commit. Set up a repository:
# Initialize git git init # Add all files git add . # Commit changes git commit -m "initial commit"git remote add origin https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>git push --set-upstream origin masterThe following steps show how to create a new static site app and deploy it to a production environment.
a-z, A-Z, 0-9, and -. This value is used as the URL prefix for your static app in the format of https://<APP_NAME>.....Azure Static Web App needs access to the repository where your Sapper app lives so it can automatically deploy commits:
There are few things that Azure Static Web App can assume–things like automatically installing npm modules and running npm run build. There are also few you have to be explicit about, like what folder will the static app be copied to after build so the static site can be served from there.
Congratulations! You now have a Sapper static website deployed to Azure Static Web Apps using GitHub Actions. If you git pull origin master, you’ll see a new .github/workflows folder with a file named something like azure-static-web-apps-....yml. Changing this file will change your deployment configuration to Azure Static Web Apps, so handle with care.
Once again, this merely scratches the surface of what Azure Static Web Apps was built to handle. In upcoming posts, I’ll explore a bit more of the support for Azure Functions and plan to walk through updating the Community for F# site (finally) with pages for videos, dojos, etc. In the meantime, you may want to have a look at the Azure Static Web Apps docs for more information.
If you tried this or have moved on to combining this with Azure Functions, what did you think? How does your experience compare with other JAMstack solutions?
]]>