Dan Matthew

Program note · Writing · 6 Sep 2026

How this blog works: a plain collection, a small plugin, and a strict CSP

You are reading this on NimbusCMS. The post is not special, it is a row in a content collection, and almost everything that makes it feel like a blog lives in one small plugin on purpose.

#nimbuscms #php #plugins #security

You are reading this on danmat.dev, which runs on NimbusCMS. The post itself is not special. It is a row in a content collection, and almost everything that makes it feel like a blog lives in one small plugin, on purpose.

A blog on Nimbus is a plain collection called blog. A post is an entry: a title, a slug, a summary, a markdown body, a cover image, some tags, and an optional canonical URL. Core already serves that collection at /blog and /blog/{slug}, paginates it, and puts it in the sitemap. The theme renders it. So what is actually left for a plugin to do?

Exactly the parts a plain collection cannot do

  • The per post head. This is the real reason a blog wants a plugin. On a post the plugin emits the canonical link, the Open Graph article tags, a Twitter card, and a JSON-LD Article block, all built from the post's own fields. That is head metadata, which has to work under any theme, so it belongs in a plugin contribution, not in theme markup.
  • The canonical rule a writer actually cares about. I cross post some of these to Dev.to. If a post first appeared elsewhere, its canonical_url points there so the other copy stays canonical. If it is native here, the field is empty and danmat.dev is the original. The plugin honours that field so search engines credit the right home.
  • A feed and tag pages. The RSS feed is raw XML, so it is a small plugin route. Tag archives are themed, so they are a plugin page section the theme styles.

The split ends up clean. Core owns the content and the routing, the theme owns how it looks, and the plugin owns the head and the feed and the tags. Nothing about the concept "blog" leaked into the CMS. A different site could install the same plugin over its own collection and get the same behaviour in its own theme.

The strict rule the whole site lives under

Nimbus ships a deliberately strict content security policy. Scripts and styles are nonce only, so an injected inline script cannot run, and there is no external anything, no third party fonts or scripts or images. That is good for safety and slightly inconvenient the first time you want a diagram in a post.

The theme's markdown renderer escapes all HTML, so a raw SVG dropped in a body would come out as visible text. The fix is a guard: an inline diagram is allowed only if it is a bare svg element with no script, no event handlers, and no foreign content, and then it is echoed as is. Anything else is escaped. The author gets diagrams, the reader gets no new script surface.

The one hole I opened, on purpose

The project cards on this site embed my own live apps in an iframe, which that same strict policy forbids by default. Rather than weaken the policy for everyone, I added a per site allowlist: a site owner names the exact origins it may embed, and the exact origins allowed to embed it, in a shell gated config file, validated so a typo can never widen the policy. It is empty by default, which is the locked down behaviour every site starts with. danmat.dev lists its own apps and nothing else.

The rough edges

The cross post canonical works today by having the theme step aside on blog pages so the plugin owns the head. A cleaner version would let any content entry declare its own canonical in core, and that is a change I have sketched but not shipped. The blog plugin also renders nothing itself by design, so a site that installs it still has to give its theme blog templates. And the feed is titled from a config value rather than the live site title, which is a small thing I would rather read from the settings store.

The nicest test of a platform is whether its own showcase needs special treatment. This blog did not. It is a collection, a theme, and a plugin that does only the parts a collection cannot, running under the same strict policy as everything else, with one deliberate and validated hole so I can show my work inside a card.