cState is an open-source status page built on the Hugo static site generator. There is no database and no application server: components are listed in a configuration file, every incident is a Markdown file in a Git repository, and Hugo turns them into plain HTML, an RSS feed and a JSON file. Because the output is static, you can host it somewhere completely independent of the infrastructure it reports on. In this tutorial you will create a cState site on Ubuntu 24.04, define your components, publish and resolve an incident, and deploy it to GitHub Pages with GitHub Actions or to your own Nginx server.
Prerequisites
To follow this tutorial you need:
- A machine running Ubuntu 24.04 LTS to build the site on: your workstation, or a server such as a CubePath VPS, with a non-root
sudouser. - Git installed (
sudo apt install git). - A GitHub account if you want to host the page on GitHub Pages, or a separate server with Nginx if you want to host it yourself.
- Optionally, a subdomain such as
status.your_domainfor the page. Point it somewhere other than your main infrastructure, so the status page stays up during an outage.
Step 1 - Installing Hugo extended
cState needs the extended edition of Hugo. Theme compatibility matters here: cState 6.0 builds with Hugo 0.160.0, while Hugo 0.166.0 fails with a template "small" not found error. This guide pins 0.160.0, and you will use the same version in CI so local and published builds match.
Download the Debian package from the Hugo releases page and install it:
cd /tmp
curl -fsSLO https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_extended_0.160.0_linux-amd64.deb
sudo apt install ./hugo_extended_0.160.0_linux-amd64.deb
On ARM machines, use the linux-arm64 package instead. Check the version:
hugo version
hugo v0.160.0-...+extended linux/amd64 BuildDate=... VendorInfo=gohugoio
The +extended part must be present.
Step 2 - Creating the site from the example repository
cState's recommended starting point is its example site, which already contains a working configuration and includes the theme as a Git submodule in themes/cstate. Clone it with --recursive so the submodule is downloaded too:
cd ~
git clone --recursive -b master https://github.com/cstate/example.git status-page
cd status-page
Check that the theme is there:
ls themes/cstate
archetypes CODE_OF_CONDUCT.md CONTRIBUTING.md docker Dockerfile exampleSite i18n images layouts LICENSE.md README.md static theme.toml
If themes/cstate is empty, run git submodule update --init --recursive.
The example ships with sample incidents and a prebuilt public/ directory. Remove both, keep build output out of Git, and rename the branch to main:
git rm -q content/issues/*.md
git rm -rq public
git rm -q --cached .hugo_build.lock
echo "public/" >> .gitignore
echo ".hugo_build.lock" >> .gitignore
git branch -M main
The site now builds from your own content only.
Step 3 - Configuring the page and its components
All settings live in config.yml. Open it:
nano config.yml
At the top, set the page title and its final URL:
title: Example Status
baseURL: https://status.your_domain/
If you will use GitHub Pages without a custom domain, you can leave baseURL as it is; the deployment workflow in Step 6 overrides it with the correct address.
Further down, replace the categories and systems lists under params with your own. Categories group components under a heading, and closed: true collapses a category by default. Each system is one component on the page:
params:
categories:
- name: Customer-facing
description: Services our customers use directly.
- name: Infrastructure
description: Internal systems the services depend on.
closed: true
systems:
- name: Website
category: Customer-facing
link: https://your_domain/
- name: API
description: Public REST API.
category: Customer-facing
- name: Database
category: Infrastructure
- name: Email delivery
category: Infrastructure
Remember the exact system names: incidents refer to them. In the same params block, adjust a few more values:
description: the sentence shown under the overall status.brand,ok,disrupted,down,notice: colors of the header and of each state, as hex values.logoanduseLogo: put your logo instatic/logo.pngand keepuseLogo: true, or set it tofalseto show the title as text.googleAnalytics: delete this line unless you actually use Google Analytics.
To show the interface in another language, set both languageCode and defaultContentLanguage at the top of the file to one of the languages in themes/cstate/i18n, for example es for Spanish.
Step 4 - Previewing the site locally
Start Hugo's development server. It rebuilds the site every time you save a file:
hugo server
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
Open http://localhost:1313 in your browser. If you are building on a remote server, open an SSH tunnel from your workstation first with ssh -L 1313:localhost:1313 your_user@your_server_ip. You may see a deprecation warning about languageCode; it does not affect the build.
cState also publishes a machine-readable summary at /index.json. In a second terminal, check the overall status:
curl -s http://localhost:1313/index.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["summaryStatus"])'
ok
Step 5 - Reporting and resolving an incident
Each incident is a Markdown file in content/issues/. The file name becomes the URL, so start it with the date. Create one:
nano content/issues/2026-09-25-api-latency.md
---
title: Elevated API latency
date: 2026-09-25 14:20:00
resolved: false
severity: disrupted
affected:
- API
section: issue
---
*Investigating* - We are investigating slower than normal API responses. {{< track "2026-09-25 14:20:00" >}}
The front matter fields are:
| Field | Meaning |
|---|---|
title | Incident title shown on the page (required) |
date | When the problem started, without time zone (required) |
resolved | false while the incident affects the status, true afterwards (required) |
resolvedWhen | When the impact ended; add it when you resolve the incident |
severity | notice, disrupted or down (required) |
affected | List of system names exactly as written in config.yml (required) |
section | Always issue (required) |
The track shortcode prints a timestamp next to each update. Save the file and look at the preview: the API component turns orange and the header shows the incident. The JSON summary changes as well:
curl -s http://localhost:1313/index.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["summaryStatus"])'
disrupted
When the incident is over, add the newest update at the top of the body, set resolved: true and add resolvedWhen:
---
title: Elevated API latency
date: 2026-09-25 14:20:00
resolved: true
resolvedWhen: 2026-09-25 15:05:00
severity: disrupted
affected:
- API
section: issue
---
*Resolved* - A saturated database connection pool caused the slowdown. The pool size was increased and response times are back to normal. {{< track "2026-09-25 15:05:00" >}}
*Investigating* - We are investigating slower than normal API responses. {{< track "2026-09-25 14:20:00" >}}
The component returns to green and the incident moves to the history section.
For announcements that should not change any component's status, such as planned maintenance, use informational: true and pin the notice to the top of the page with pin: true:
---
title: Planned database maintenance on October 3
date: 2026-09-26 09:00:00
informational: true
pin: true
section: issue
---
On October 3 between 02:00 and 02:30 UTC we will upgrade the database cluster. The API may return errors for up to five minutes during this window.
Stop the preview with CTRL+C and commit your changes:
git add -A
git commit -m "Configure status page and add first incident"
Step 6 - Deploying to GitHub Pages
Create an empty repository on GitHub, for example your_user/status-page, and point your local repository at it:
git remote set-url origin [email protected]:your_user/status-page.git
Add a workflow that installs the same Hugo version, builds the site and publishes it:
mkdir -p .github/workflows
nano .github/workflows/pages.yml
name: Deploy status page
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-24.04
env:
HUGO_VERSION: 0.160.0
steps:
- name: Install Hugo
run: |
curl -fsSLo "${RUNNER_TEMP}/hugo.deb" \
"https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
sudo dpkg -i "${RUNNER_TEMP}/hugo.deb"
- name: Check out the repository and the cState submodule
uses: actions/checkout@v5
with:
submodules: recursive
fetch-depth: 0
- name: Configure Pages
id: pages
uses: actions/configure-pages@v5
- name: Build
run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Upload the site
uses: actions/upload-pages-artifact@v4
with:
path: ./public
deploy:
needs: build
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
fetch-depth: 0 downloads the full Git history, which cState uses to show when each page was last modified.
In the repository on GitHub, open Settings > Pages and set Source to GitHub Actions. Then commit the workflow and push:
git add .github/workflows/pages.yml
git commit -m "Deploy with GitHub Actions"
git push -u origin main
Follow the run under the repository's Actions tab. When it finishes, the page is live at https://your_user.github.io/status-page/. Verify it from the command line:
curl -s https://your_user.github.io/status-page/index.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["summaryStatus"])'
To use status.your_domain, enter it under Settings > Pages > Custom domain, create a CNAME DNS record pointing status to your_user.github.io, and enable Enforce HTTPS once the certificate is issued. The next deployment picks up the new address automatically.
From now on, reporting an incident is a commit: add or edit a file in content/issues/, push, and the page updates in about a minute. You can even do it from GitHub's web editor when your own machines are unreachable.
Step 7 - Hosting on your own Nginx server (alternative)
If you prefer to host the page yourself, use a server that does not share infrastructure with the services it reports on. On that server, install Nginx and Certbot, open the firewall and create the web root:
sudo apt update
sudo apt install nginx certbot python3-certbot-nginx rsync
sudo ufw allow 'Nginx Full'
sudo mkdir -p /var/www/status
sudo chown your_user:your_user /var/www/status
Create a server block:
sudo nano /etc/nginx/sites-available/status
server {
listen 80;
listen [::]:80;
server_name status.your_domain;
root /var/www/status;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Enable it and request a certificate:
sudo ln -s /etc/nginx/sites-available/status /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d status.your_domain
On your build machine, build the site with its final URL and copy it over. --delete removes files that no longer exist, such as renamed incidents:
hugo --minify --baseURL https://status.your_domain/
rsync -az --delete public/ your_user@status_server_ip:/var/www/status/
Check the result:
curl -s https://status.your_domain/index.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["summaryStatus"])'
Troubleshooting
hugo fails with template "small" not found or other template errors. Your Hugo version is newer than what the theme supports. Install the pinned version from Step 1 and use the same HUGO_VERSION in the workflow.
failed to load Git data: not a git repository. enableGitInfo: true in config.yml requires the site to be a Git repository. Build from inside the cloned repository, or set enableGitInfo: false.
themes/cstate is empty or the build cannot find the theme. The submodule was not downloaded. Run git submodule update --init --recursive locally, and make sure the workflow uses submodules: recursive.
An incident does not change a component's status. The names under affected must match the name of a system in config.yml exactly, including capitalization, resolved must be false, and section must be issue.
The published page has broken styles or links. baseURL does not match the address the page is served from. On GitHub Pages, let the workflow set it; when self-hosting, pass --baseURL with a trailing slash.
Conclusion
You built a cState status page with Hugo, defined your components, reported and resolved an incident with plain Markdown files, and deployed it with GitHub Actions or to your own Nginx server. As next steps, write down in your incident process who is allowed to push to the status repository, subscribe your team to the page's RSS feed at /index.xml, and read /index.json from your chat or monitoring tools to show the current status elsewhere.
