Building a headless CMS on Hugo
Why we split authoring from delivery, and how HugoPress wires a rich editor to a static site through Git.
A classic monolithic CMS bundles authoring, storage and rendering into one running process. That is convenient — until you start caring about performance, security and scaling. The headless approach decouples those three responsibilities, and that decoupling is exactly what HugoPress is built on.
Three layers, three responsibilities
- The editor — authors write in a block editor (Editor.js). The output is clean structured JSON, not WYSIWYG HTML soup.
- The domain layer — the CMS turns blocks into finished HTML via the
BlockRenderingPipeline. The theme itself knows nothing about Editor.js. - Delivery — the rendered content is written as
content/<lang>/...and committed to Git. Hugo builds a static site from it.
The theme does not know Editor.js. All renderers live on the CMS side, which keeps the template dumb, portable and easy to test.
Why Git is the transport layer
Git gives us three things almost for free:
- History — every publish is a commit, so we get audit and rollback.
- Review — preview runs on the same repository as production.
- Idempotency — if the content has not changed, there is nothing to commit, and the publish is skipped.
public function publish(Article $article): void
{
$html = $this->pipeline->render($article->blocks());
$this->repository->writeContent($article->locale(), $article->slug(), $html);
$this->git->commitIfChanged(sprintf('publish: %s', $article->slug()));
}
What you get
A static site has no runtime to attack, no database queries on the request path, and it can be served from a CDN as close to the reader as possible. Meanwhile the author works in a comfortable editor and never thinks about the build.
In the next article we look at why a site like this is so fast.