I spent a long time looking for the perfect framework for technical blogging. I wanted something that would not only let me write text, but also show the results of scripts, visualize data, and do it all beautifully. At the same time, it was important for everything to be static, so it could be hosted for free, without servers or extra hassle.
And finally, I got lucky — I came across Observable Framework. This thing does all that and even more. In this post, I’ll explain what it is, how to run it together with GitHub and Azure Static Web Apps, and how to connect PowerShell to the whole setup.
In a Nutshell: Observable Framework
Observable Framework is a static site generator by the ObservableHQ team that combines the power of interactive notebooks with the convenience of modern frontend development. Unlike typical blogging platforms, it allows you to:
- run JavaScript right in your posts,
- use D3, Vega, and Observable Inputs for visualizations,
- load and process data using PowerShell, Python, or virtually any other language — as long as you return a result.
You write your notes in Markdown, embed interactivity — and voilà, you’ve got a modern static site ready to be deployed on GitHub Pages or Azure Static Web Apps. All interactivity works in the browser, no backend needed. Simple, fast, elegant.
Setting Up Observable Framework
Getting started takes just a few clicks — start here. The template repo includes a devcontainer
and all necessary config. Just create your own repo from the template and you’re ready to start writing posts.
A new post is just a new file, for example /src/my-new-post/index.md
.
But plain Markdown is boring. We want interactive tables, charts, visualizations. And that means data. Here’s where the magic starts.
Observable Framework lets you reference executable scripts instead of plain data files. For example, this:
const allData = await FileAttachment("allData.csv").csv()
There’s no allData.csv
, but there is an allData.csv.ps1
. The framework will run the PowerShell script, get its output, and treat it as if it were a normal CSV file.
To make this work with PowerShell, a couple of tweaks are needed.
1. Add PowerShell to devcontainer
Update your devcontainer.json
to include PowerShell:
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 4
},
// skipping stuff
"features": {
"ghcr.io/devcontainers/features/powershell:1": {
"version": "latest"
}
}
// skipping stuff
}
2. Register the .ps1
interpreter in observablehq.config.js
export default {
// skipping stuff
interpreters: {
".ps1": ["/usr/bin/pwsh"]
},
// skipping more stuff
};
This tells the framework to use pwsh
to run PowerShell scripts.
More on Data Loaders — here
Example PowerShell Script
Create /src/my-new-post/data.csv.ps1
:
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/vega/vega/refs/heads/main/docs/data/movies.json' |
ConvertFrom-Json |
Select-Object -Property Title, 'Worldwide Gross', 'US Gross', 'IMDB Rating' |
ConvertTo-Csv
The script grabs a JSON file, filters and converts it to CSV, and writes to stdout — that’s key!
Then, in your post, just add:
Inputs.table(FileAttachment("data.csv").csv())
And boom — you’ve got an interactive data table.
Live examples from this blog:
- Azure permissions and groups with PowerShell + JS
- Same, but with Vega
- Sankey graph of Azure Firewall traffic
Azure Static Web Apps
To publish, we use Azure Static Web Apps — Microsoft’s free static hosting service. It integrates with GitHub effortlessly: connect your repo, specify the build folder (dist
) — and you’re done. If you give Azure access to your GitHub account, it will automatically create a GitHub Actions workflow and necessary secrets.
But here’s the gotcha: Azure uses Oryx to build your site. Oryx runs in a container without PowerShell, so your .ps1
scripts won’t work.
The solution? Build the site before Oryx runs. Let Oryx simply upload the results.
Here’s a working GitHub Actions workflow (modified from the default Azure setup):
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@v3
with:
submodules: true
lfs: false
# ADDING THIS STEP!!!
- name: Install dependencies and build
run: |
npm install
npm run build
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
# SKIPPING SECRETS :)
action: "upload"
# SETTING CORRECT app_location and output_location
app_location: "dist"
api_location: ""
output_location: "dist"
# DISABLING build, as it was built on the previous step!
skip_app_build: true
skip_api_build: true
# SKIPPING OTHER STUFF ....
Now your site builds with PowerShell and everything — and Azure just hosts it. Stable, fast, free.
If you’re looking for a blog platform where you can run scripts, visualize data, and host it for free — Observable Framework + Azure Static Web Apps is the way to go.