Journal · · Brian Moenga

Building a publishing platform, Part 1: One repo, two apps

Nx / Angular SSR / Strapi 5

I spent much of the past year building a digital publication for geospatial content in Africa: articles, industry news, magazine issues, events and a partner directory. It has two moving parts: a public-facing Angular 21 SSR frontend, and a Strapi 5 headless CMS where editors do their work. This first note covers the decision I made before writing any feature code: putting both apps in a single Nx monorepo.

Why a monorepo at all?

A CMS-driven site lives and dies on the contract between the frontend and the content API. Keep them in separate repos and that contract exists only in your head. Rename a field in Strapi and the frontend finds out at runtime, in production, usually on a Sunday. In one repo, the contract becomes code:

platform/
├── apps/
│   ├── web/                # Angular 21 SSR frontend
│   ├── web-e2e/            # Playwright e2e tests
│   └── cms/                # Strapi 5 headless CMS
├── libs/
│   └── shared/interfaces/  # The contract lives here
└── tools/scripts/          # Seed & utility scripts

libs/shared/interfaces holds the TypeScript interfaces for every content type: Article, Author, Event, Issue and friends. Both apps import from it. Change an interface and the compiler tells you every place on either side that needs to catch up. That single library has caught more bugs than any test I have written for this project.

What Nx actually buys you

You could wire this up with plain npm workspaces, and for a two-app repo that is a fair choice. Nx earns its keep in three specific ways:

Task caching. Nx hashes the inputs of every task. If nothing in apps/cms changed, nx build cms returns in milliseconds from cache. CI on a content-type-only change never rebuilds the frontend.

The dependency graph. dependsOn: ["^build"] means building web automatically builds shared/interfaces first. And nx affected lets CI test and build only what a commit actually touched.

One toolchain. A single ESLint config, one TypeScript version and one set of generators across an Angular app and a Node-based CMS. Boring, in the best way.

The awkward part: Strapi in Nx

Strapi is not a first-class Nx citizen. There is no official plugin. My approach: treat apps/cms as a mostly self-contained Strapi project and give it a thin project.json whose targets just call Strapi's own CLI (strapi develop, strapi build). Nx handles orchestration and caching; Strapi never knows it lives in a monorepo. Resist the urge to make Strapi's build "properly Nx-native." You will fight webpack configs for a weekend and gain nothing.

SSR because search engines read the web

A publishing platform without server-side rendering is invisible. Angular Universal renders every article on the server, so crawlers and link previews get real HTML with real OpenGraph tags. Strapi data is fetched during server render, and Angular's transfer state means the browser does not refetch what the server already resolved.

Takeaways

Put the API contract in a shared library from day one. It is the whole point of the monorepo. Let Nx cache and orchestrate, but let Strapi be Strapi. And if you are building anything content-driven, SSR is not an optimisation, it is table stakes.

Next in the series: Part 2 covers zero-downtime deploys with Traefik, Docker and GitHub Actions: how the whole stack ships to a single VPS with automatic TLS.

Contact

Socials
Stingz
Nairobi, Kenya
-1.2921°, 36.8219° · EAT

Colophon

Stingz.io is built by the studio using hand-coded HTML & CSS on Nuxt, with GSAP and Locomotive Scroll for motion and Plus Jakarta Sans for type. Follow the Journal for updates.