This repository is for the main static site of https://saltproject.io, built with Hugo using pydata-hugo-theme, a generic Hugo port of the PyData Sphinx Theme, consumed as a Hugo Module. The site is built and deployed to GitHub Pages via GitHub Actions.
- Install Hugo
- The exact version pinned for this project is tracked in the root
.hugo-versionfile. The devcontainer and CI both install that exact version automatically (see.devcontainer/Dockerfileand.github/actions/setup-hugo), so it only needs to be updated in one place. - Must be the
extendedversion, and at least the minimum declared in the theme module'shugo.toml(module.hugoVersion.min).
- The exact version pinned for this project is tracked in the root
- Git
- Python 3.14+ (only needed to run
scripts/validate-tags.pylocally) - Go — version pinned in the root
.go-versionfile. Only needed if you're updating the vendored theme module (see The theme module); not required for a normal build/serve.
No Go toolchain and no Node/npm install are required to build or serve this site: the theme module and its build-time JS dependencies (FontAwesome/Bootstrap) are vendored in _vendor/ and checked into this repository (see The theme module below), so hugo resolves everything from disk.
# Serves for viewing changes locally
# Dynamically loads updates when changes happen in repo
hugo servePreview http://localhost:1313/ in your browser.
The Hugo server won't display content that is marked as draft: true in the Markdown front matter. It also won't display content that is set to a future date. One workaround is to remove draft: true and set the content date for the current date.
You could also run this command to tell Hugo to build a local preview of content that is set with a future date:
hugo server --buildFutureBe aware that the command hugo server --buildDrafts currently errors out in Salt Project and, at the time of writing, this hasn't been fixed. The only workaround is to remove draft: true from the front matter.
To contribute, create a fork of this repository. See Using the Fork-and-Branch Git Workflow for help.
Every pull request runs through the PR checks GitHub Actions workflow (.github/workflows/pr-checks.yml), which:
- Builds the site with
hugo --minifyto catch any build breakage. - Runs
scripts/validate-tags.pyto make sure every blog post only uses tags from the approved taxonomy (see Blog post tags below).
Both checks must pass before a pull request can be merged.
The site deploys via the Deploy Hugo site to Pages GitHub Actions workflow (.github/workflows/gh-pages.yml). It builds on every push, but only deploys to GitHub Pages when the push is a tag matching v**.
The easiest and preferred way to cut a release is to create a GitHub Release, which automatically creates and pushes the underlying tag for you:
-
Merge your changes into
mainfirst — the tag should point at the commit you want live. -
On GitHub, go to the repo's Releases page and click Draft a new release.
-
Under Choose a tag, type a new tag name following the existing convention:
v<major>.<minor>.<patch>(e.g. the current latest tag isv2.0.38, so the next one would bev2.0.39). Select Create new tag: <tag> on publish. -
Set Target to
main. -
Give the release a title and describe the changes being shipped (this doubles as your release notes).
-
Click Publish release. This pushes the new tag, which triggers the
Deploy Hugo site to Pagesworkflow and publishes the changes live. -
Watch the workflow run under the Actions tab to confirm the build and deploy succeed.
NOTE: If the workflow fails, check the run's logs to see what's going wrong. If you can't resolve it, contact the SRE team for support.
Alternatively, you can push a tag directly with
git tag v2.0.39 && git push origin v2.0.39, but creating a GitHub Release is preferred since it keeps a human-readable changelog tied to each deployed tag.
To create a new blog post:
-
Pull down the latest changes from
mainand check out a new branch. -
Open one of the template blog posts in your browser. These files are located in the
content/blogfolder. Open the example file based on the type of blog post you want to write:If you want to write this type of post... Use this example file... Release announcement example-release.mdOpen Hour notes example-open-hour.mdGeneric blog entry (including security advisories) example.md -
Each example file contains instructions in commented out code for how to fill in the necessary Markdown front matter for that type of blog post. Follow those instructions and edit the content.
The page title is rendered automatically from the front matter
titlefield — don't add a duplicate# Titleheading at the top of the body. -
Build a local preview to check that the changes render properly.
-
Stage and commit your changes, then open a pull request against
main. -
Once the
PR checksworkflow passes and the PR is approved, merge it in. -
Push the current
mainbranch to the live site by cutting a new release. (See Push the currentmainbranch to the live site.)
Security advisories are no longer a separate content type — they're just blog posts tagged security, published in content/blog alongside everything else, as part of a single unified blog feed. Follow the same process as Create a new blog post and use the security tag.
The legacy /security-announcements/ RSS feed URL is preserved for old subscribers: scripts/duplicate-security-feed.sh copies the built tags/security RSS feed to that legacy path. This runs automatically as part of the deploy workflow after the Hugo build — see .github/workflows/gh-pages.yml.
Every blog post's tags must come from the approved taxonomy defined in scripts/tags.toml. If you need to add or remove an approved tag, edit that file — no code changes required.
You can check your local posts against the taxonomy at any time with:
python3 scripts/validate-tags.pyThis same check runs automatically on every pull request via the PR checks workflow.
Hugo is a static site generator, which means it compiles the raw content (Markdown files) and uses the layout and style code to generate HTML files that can be stored on a web server.
This site uses pydata-hugo-theme — a generic Hugo port of the PyData Sphinx Theme — imported as a Hugo Module via [module.imports] in hugo.toml. See The theme module for how that module is resolved in this repo.
The content folder contains the raw Markdown source files that contain the content for the site. It's organized into a few sections:
content/blog— the unified blog feed: release announcements, community updates, Open Hour notes, and security advisories, all tagged appropriately (see Blog post tags).content/community— standalone community pages (event calendar, working groups, RSS feeds).content/security-announcements— kept only for the legacy/security-announcements/RSS feed URL (see Create a new security announcement); it no longer holds its own post content.
Hugo treats _index.md pages inside a folder as a "list" page, which is useful for listing the content in that folder on a landing page.
The layouts folder contains the raw HTML files that explain how Hugo should render content when it's transformed (compiled) into HTML, using Hugo's templating language (Go templates).
Most of the site's layout logic lives in the theme module (_vendor/github.com/saltstack/pydata-hugo-theme/layouts). The top-level layouts folder holds project-specific overrides and additions that go beyond (or intentionally diverge from) the theme's defaults — for example:
layouts/page.html— overrides the theme's default page template to automatically render the page title as an<h1>, so individual pages don't need a duplicate Markdown heading.layouts/community/event-calendar.htmlandlayouts/community/working-groups.html— custom layouts for those two community pages, selected via thelayoutfront matter field on their respective content files.
When Hugo looks for a template, it checks the project's layouts folder first and falls back to the theme module's layouts folder if there's no project-level override. This means the theme can be updated/upstreamed while site-specific customizations stay isolated in the top-level layouts folder — a fix that belongs in the theme itself (e.g. a genuine theme bug or a generically useful option) should be made in the pydata-hugo-theme module's own repository, not by editing the vendored copy under _vendor/ directly (those edits are overwritten the next time the module is re-vendored).
Both the theme and the project's layouts folder can contain a _partials folder with reusable "snippets" of HTML/Go template code that get called into other layout files dynamically, for example:
{{ partial "header-article.html" . }}
The most important partials are head.html, header.html, and footer.html, which render the global HTML for those elements across the site.
The static folder contains CSS, JavaScript, and other static assets that are copied as-is into the built site. static/images holds site images (for example, static/images/blog for blog post images).
When you reference these files in content or templates, you leave off the static part of the path, since Hugo serves everything under static from the site root.
_vendor/github.com/saltstack/pydata-hugo-theme/layouts/baseof.html defines the baseline HTML structure for all pages on the site. It calls the head.html, header.html, and footer.html partials, and defines the main block that individual page layouts fill in.
The Markdown content includes some front matter (metadata) at the top of each file that passes important information to Hugo when it is rendering the layout. For example, a blog post's front matter looks like:
---
draft: true
title: "Title"
summary: "Summary"
date: "yyyy-mm-dd"
author: Testy McTester - DELETE IF ANONYMOUS
authorbio: "Author bio here. Delete if no bio."
url: "blog/title-shortened"
image: images/blog/
image_alt:
tags:
- release
- community
---
That front matter metadata sends important information that Hugo uses when building the page — swapping in the title, the summary that appears on the blog index page, the author, and so on.
Depending on the type of content, it may need different front matter. See the example files in content/blog (example.md, example-release.md, example-open-hour.md) to understand what front matter is needed for each type of post.
The scripts folder holds standalone helper scripts used in local development and CI:
scripts/validate-tags.py— validates blog post tags against the approved taxonomy inscripts/tags.toml.scripts/duplicate-security-feed.sh— copies the builttags/securityRSS feed to the legacy/security-announcements/URL after a Hugo build.
.github/workflows/pr-checks.yml— runs on every pull request; builds the site with Hugo and validates blog post tags. Only ever requestscontents: read— see the security note at the top ofgh-pages.ymlfor why it must stay that way..github/workflows/gh-pages.yml— builds the site on every push, and deploys it to GitHub Pages when the push is a tag matchingv**(see Push the currentmainbranch to the live site). Deploy-time (pages/id-tokenwrite) permissions are scoped to just thedeployjob; it never triggers onpull_request..github/workflows/check-theme-updates.yml— runs weekly (and on-demand viaworkflow_dispatch) to check for a newerpydata-hugo-thememodule release; opens a PR ifscripts/update-vendored-theme.shfinds changes to vendor. Never triggers onpull_request, since it needscontents: write/pull-requests: writeto open that PR..github/actions/setup-hugo— local composite action shared by the workflows above to install the Hugo CLI. Defaults to the version pinned in the root.hugo-versionfile — the same file the devcontainer'sDockerfilereads — so the required Hugo version only needs to be maintained in that one place.
pydata-hugo-theme is a standalone, generic Hugo Module — it carries no Salt Project branding or content, so it can be reused by any Hugo site. This repo consumes it via [[module.imports]] in hugo.toml and a require/replace pair in the root go.mod.
The module (including its FontAwesome/Bootstrap build-time JS, mounted via [[module.mounts]] in the module's own hugo.toml) is checked into this repo under _vendor/github.com/saltstack/pydata-hugo-theme/ via hugo mod vendor. This means:
-
No Go toolchain, npm, or network access is needed to build or serve this site —
hugoreads everything it needs straight out of_vendor/. -
_vendor/is generated — never hand-edit files under it. To pick up a theme change, run:scripts/update-vendored-theme.sh # update to the latest module version scripts/update-vendored-theme.sh v0.2.0 # or pin a specific version
This bumps the module (
hugo mod get), re-vendors it (hugo mod vendor), and re-copiesLICENSE/README.md— the two non-Hugo fileshugo mod vendordoesn't carry over — into_vendor/github.com/saltstack/pydata-hugo-theme/. Review the resulting diff, then commit it.A scheduled GitHub Actions workflow (
.github/workflows/check-theme-updates.yml) runs this same script weekly and opens a PR if anything changed, so updates don't rely on someone remembering to check. It can also be run on demand from the Actions tab (workflow_dispatch). Either way, updates are never auto-merged — re-vendoring the theme stays a deliberate, human-reviewed action. -
Their license text isn't duplicated anywhere else in this repo — see
_vendor/github.com/saltstack/pydata-hugo-theme/LICENSE.
pydata-hugo-theme is a Hugo port of the PyData Sphinx Theme (docs), originally built for Sphinx documentation sites. Credit to the PyData Sphinx Theme authors and contributors for the design and functionality this port is based on. See the module's own README.md (_vendor/github.com/saltstack/pydata-hugo-theme/README.md) for the full attribution notice.
This repository as a whole is not released under an open source or Creative Commons license — all rights reserved except as noted below.
The one exception is the vendored pydata-hugo-theme module: it is licensed under the BSD 3-Clause License, matching the license used by the upstream PyData Sphinx Theme. See _vendor/github.com/saltstack/pydata-hugo-theme/LICENSE for the full text.