Content management
pacod-cms
One stack, many editorial websites — articles, advertising and statistics, with AI translation built in.
pacod-cms is an open-source content management system for editorial websites. One installation hosts many sites, each with its own database, files and cache; editors work in a multilingual studio, and readers get fast, host-routed public sites.
- Apache-2.0
- Version 0.0.1
- Preview
Features
-
Editorial core
Articles, categories, authors, tags and standing pages, with a shared image library across the website.
-
Multi-site by design
Each website has its own MongoDB database, files and cache, and answers at the hosts it claims.
-
Advertising
Advertising bookings with an HTML5 banner builder, plus a link-in-bio page for social profiles.
-
Statistics
Reader, advertising and crawler statistics, so editors see what is read and what is crawled.
-
Multilingual
Content in many languages with a configurable original language; the studio itself in English, Bulgarian, German and Russian.
-
AI translation
Translate an article or a whole website as a job; each answer is refused unless it keeps the article's markup exactly.
-
Organisations and roles
Organisations, websites and invitations, with roles per organisation and per website held in pacod-id.
-
Configurable PDFs
Article PDFs as a background job, in the website's print layout or a two-column scientific format, with the abstract and keywords taken from the article or written by AI.
-
Journal issues
Articles gathered into an issue, one PDF per language with a cover, an editorial, contents and numbered pages, published in the website's archive.
Where it fits
One stack, many websites
pacod-cms runs many websites, for many organisations, from one stack. Each website has its own MongoDB database, its own folder of files and its own cache entries, and answers at the hosts it claims. Anyone who signs up can create an organisation, become its admin, create websites in it and invite people to work on them; a platform admin sets the limits.
The design assumes a website's owner may be hostile to everyone else on the installation. Websites run script their owners wrote — HTML5 banners, tag managers, article bodies — and the platform has to keep that script, and that data, where it belongs.
How the parts fit
| Part | What it does |
|---|---|
pacod-cms-core |
A library: documents, services, the tenancy, the control plane, seed content, the design model, and a Java port of pacod-id's rules engine. |
pacod-cms-site |
The public site for every website at once: pages, read-only JSON under /web/v1/**, media, traffic counters. |
pacod-cms-studio |
The studio: the portal for organisations, websites and members, the editing screens, background jobs, AI. |
| Caddy | The front door, routing by host: the studio's host to the studio, pacod-id's host to pacod-id, every other host to the site. |
| Gotenberg | Renders the PDFs of articles and journal issues from the site's print views. |
The two applications never call each other. Both use MongoDB directly, so an edit made in the studio reaches the public site through the database; the site's caches expire after 60 seconds instead of being evicted, so an edit is live within a minute.
From a host to a page
Every unit of work belongs to exactly one website, bound for its duration.
- Caddy hands the request to the site. In production it requests a website's certificate at the first visit, but only after the site confirms the host belongs to an active website.
- The site looks the host up in the control plane before anything else runs. A host no active website claims gets a plain 404, never a default website.
- With the website bound, every MongoDB operation goes to that website's database. With none bound, the database layer throws rather than guess.
- The page type's layout — regions of ordered sections — is resolved against the website's template set, and each section's data is looked up first. Templates never query; a section whose data fails is left out and logged, and the page still renders.
In the studio the website comes from the path (/sites/<id>/mgmt/…); background work binds the
website it is for.
Where data lives
| Where | What |
|---|---|
pacod_cms |
The control plane: organisations, websites, domains, platform settings, the job queue. |
site_<id> |
One database per website: articles, pages, images, banners, menus, settings, layouts, theme, statistics. |
sites/<id>/ |
The website's files: article images, banner creatives, PDFs. A draft issue's PDFs sit in a private folder only the studio serves. |
| pacod-id | People, sessions and role bindings. The CMS stores no accounts. |
A website's identity — name, logos, contact details, third-party ids — is data, not markup. A third-party id renders only when it is set and has its vendor's shape, so a fresh website loads no tracking at all. The website's original content language is the last fallback for a missing translation and the legally governing text.
Design without touching code
Under Design in the studio, each website picks a template set — editorial, a magazine with a
column beside the content, or minimal, one quiet column — and arranges a layout per page type:
home, article, category, latest, standing page and search. Sections include lists of articles
(latest, a category or the most read), rich text, a call to action, a search box and the social
links. A theme sets colours with night-mode values, fonts and widths.
Switching sets first shows what would be lost, and nothing is removed: sections the new set cannot show are kept and come back on a switch back. Layouts and the theme stay drafts until someone allowed to publish them does. The preview shows drafts from the website's own address, at desktop, tablet or phone width, through a signed token valid for 30 minutes, and counts nothing.
A new template set is code an operator installs, not an upload: a Thymeleaf template can do anything the site can.
PDFs and journal issues
Long work runs in the studio as a job, one organisation's at a time, with a live log and a cancel button. PDF export is one: Gotenberg renders each article into one PDF per language it is written in, and the article links its PDF once it exists.
- Two formats. Standard is the website's print view. Scientific is a paper layout: a full-width head with the title, authors, abstract and keywords, then the text in two columns.
- Two ways to make them. Code only takes the abstract and keywords from the article's description and tags. With AI has pacod-gw write an abstract, keywords and a short running title for each article and language. The article's text is never changed.
- PDF settings hold each website's defaults: format and way, paper (A4, Letter or A5) and margins, the journal's name per language and its ISSN, a running head and page numbers. Any run can choose another format and way.
A journal issue gathers articles into one PDF per language. It has a volume, a number and a
month, a title and an editorial per language, and an optional cover image; its articles are picked
one by one or filled from its month, then put in order. Generating it renders a cover, the
editorial, a table of contents with page numbers and then the articles, with a bookmark per article
and the pages numbered throughout. A draft issue's PDFs are downloaded from the studio only.
Publishing it puts the issue and its PDFs on the website at /issues and in its sitemap.
Security model
Who may do what
pacod-id owns accounts and roles. Tokens carry no roles: the studio follows pacod-id's feed of role
bindings and decides every permission itself against the CMS's manifest (authz/cms.yaml). The
roles are platform admin, provisioner, organisation admin, website admin, designer, editor, author
and analyst, held at the platform, an organisation or a website. An author writes; an editor also
publishes.
- Every studio handler declares the permission it needs; one that declares none is refused. No role is implicit.
- When pacod-id's feed has not answered for a minute, the studio answers 503 rather than decide with roles that may have been taken away. The public websites are unaffected.
Websites kept apart
- The studio sits on a different registrable domain from the websites, or refuses to start. Otherwise a website's script could set cookies for the studio and defeat its CSRF protection.
- The studio never serves a website's HTML or script. Draft and HTML5 banner previews are framed from the website's own origin.
- Gotenberg is locked down: it can reach only the site's internal address and runs no JavaScript.
- Only published articles reach a reader, on every route, list, feed, sitemap and PDF. A draft journal issue is never public.
AI that keeps its hands off your keys
Every AI call goes through pacod-gw; the studio never holds a provider's key. AI is off for new organisations by default: a platform admin switches it on per organisation, and a website's admin can switch it off for that website.
- Article translation runs as a job. A translated body must keep its source's markup exactly —
only
alt,title,aria-labelandplaceholdermay change — or it is refused. - Website translation covers standing pages, section names, menus, the link page and, if ticked, tags, without overwriting an edit made while it ran; at most 2,000 calls a run.
- Writing help rewrites a selection six ways and suggests a summary and tags.
- Alt text per language is suggested by a vision model from the picture itself.
- Design assist drafts a theme or a layout from a few words, written only to the draft.
- PDF front matter: for PDFs made with AI, an abstract, keywords and a running title per article and language, checked as plain text. They are reused until the article is edited.
The gateway sees opaque ids only, never names or e-mail addresses.
Three environments, one Makefile
| dev | stage | prod | |
|---|---|---|---|
| Front door | HTTP, plus HTTPS from Caddy's local CA | HTTPS (Let's Encrypt) | HTTPS (Let's Encrypt) |
| pacod-id's mail | caught by mailpit | caught by mailpit | your relay |
| AI | mock gateway | mock gateway | your pacod-gw, or off |
| Demo websites | yes | yes | none |
Stage is production's configuration with mail caught and AI mocked. make check refuses to start
while a secret is missing or the configuration is unsafe. Back up .env, the MongoDB databases, the
websites' files and pacod-id's PostgreSQL. The studio's /mgmt-health is liveness only: a studio
that cannot reach pacod-id stays healthy while its screens answer 503, and says so in its log.
Every environment runs on pacod-net, the Docker network all pacod projects share. The CMS's own
services — the site, the studio, MongoDB and Gotenberg — are in one compose file; its own copies of
the shared parts — Caddy, pacod-id with its PostgreSQL, the development gateway and the mail
catcher — are in a second, which the Makefile loads alongside. Only one stack running the shared
parts fits on a machine. pacod-demo loads only the first file and runs the
CMS with the other projects behind one Caddy, pacod-id, PostgreSQL and gateway; it is online at
demo.pacod.io.
Limits and further reading
Known limitations in v1:
- A website's identity (
siteSettings) has no studio screen yet; it is edited in the database. - The demo content is in English only.
- The members list does not show platform-wide roles.
The roadmap lists what v1 defers, among it a write API, a gateway key and AI budget per organisation, a separate worker, S3 storage and content importers.
In the repository:
- Architecture — how a request finds its website
- Security — what keeps websites and the studio apart
- Template sets — writing a set of your own
- Running and Deployment — environments, TLS, hosts, settings
- Roadmap — what v1 includes and what is deferred
Screenshots
Tech stack
- Written in
- Java
- Tech stack
- Java 25
- Spring Boot 4.1
- Thymeleaf
- MongoDB
- Maven
- Caddy
- Gotenberg
- Docker
Quick start
make initmake upopen http://studio.localhost:8000/make site-create SLUG=acme NAME="Acme News" HOSTS=acme.localhost DEMO=1Works with
Licence
Apache-2.0
The project's own code is Apache-2.0. The studio bundles the TinyMCE editor (GPL-2.0-or-later), so the studio as built is redistributed under GPL-3.0 terms as a whole. The public site does not contain it.