Noon Docs · Getting Started · Concepts · Guides · Reference

An agent sets a site's colours, fonts and spacing through the manage_theme tool, and an operator can add one block of global CSS through site settings. Both apply on the next request. There is no rebuild step.

Turn the theme plugin on

The theme plugin must be listed in the server's NOON_PLUGINS configuration before manage_theme or the theme_document collection exist. If the plugin is off, the site renders exactly as if no theme were ever set.

The manage_theme tool

manage_theme takes an action: set, clear, register-google-font, or widen-font.

set replaces the whole token document

A set call stores exactly the tokens it sends. It does not merge with what was there before. If the stored document has ten keys and a call sends three, the document afterwards has three keys, not thirteen. To remove one token while keeping the rest, send every token you want to keep plus the ones you are changing.

expectedRevision is required on every set once a document already exists, and refused when none exists yet. Read the current revision from the theme_document collection with query before writing.

Discover the token vocabulary for free

Send {"action":"set"} with no tokens field, or tokens: {}. Noon answers with a violation that lists every valid key and its grammar. Use this to learn the vocabulary rather than guessing.

The token vocabulary

Every key is optional. A value that fails its grammar is refused with a plain-language explanation and, where there is exactly one obvious fix, a suggested correction.

KeyValue
backgroundLowercase hex colour, #rgb or #rrggbb. No named colours, no rgb(...), no alpha.
surfaceSame grammar as background.
textSame grammar as background.
mutedSame grammar as background.
accentSame grammar as background.
accent-contrastSame grammar as background.
borderSame grammar as background.
body-fontsystem-sans, system-serif, system-mono, or a font key returned by register-google-font.
heading-fontSame choices as body-font.
base-font-sizeA length with a unit, from 12px to 32px or from 0.75rem to 2rem. A bare 0 is refused.
scale-ratioA unitless decimal from 1 to 2, e.g. 1.25. Never an expression.
spacing-unit0, or a length from 0px to 64px or from 0rem to 4rem.
radius0, or a length from 0px to 64px or from 0rem to 4rem.
measureA length with a unit, from 320px to 1600px, from 20rem to 100rem, or from 20ch to 120ch. A bare 0 is refused.

A hex colour must be lowercase. An uppercase colour like #FFF is refused with a suggested lowercase correction. No token value may contain a parenthesis, quote, comma, colon, semicolon, angle bracket, or whitespace, because these tokens become CSS custom properties directly and none of that punctuation is ever needed in a legal value.

Set colours, fonts and spacing

{
  "action": "set",
  "tokens": {
    "background": "#ffffff",
    "text": "#111111",
    "accent": "#2b6cb0",
    "accent-contrast": "#ffffff",
    "body-font": "system-sans",
    "heading-font": "system-serif",
    "scale-ratio": "1.25",
    "spacing-unit": "1rem",
    "radius": "0.5rem"
  }
}

Turn the theme off, reversibly

{ "action": "clear", "expectedRevision": 3 }

clear deactivates the theme. The site renders unstyled again, exactly as before any theme was set. The stored tokens are not deleted: a later set is not needed to bring the same theme back, though the current tool surface only exposes set, so restoring the same look means sending the same tokens again. clear on an already-inactive or never-created document still succeeds; it has simply already reached the state you asked for.

Add a brand font

register-google-font self-hosts a font family from Google Fonts, but only when its licence is one of three that permit redistribution: OFL-1.1, Apache-2.0, or UFL-1.0. Noon checks the licence itself and refuses every other family.

{
  "action": "register-google-font",
  "family": "Inter",
  "weights": [400, 700]
}

family is required. weights is optional and defaults to [400]; up to nine weights may be requested at once, each a three-digit number from 100 to 900. key is optional and defaults to a key derived from the family name; fallback is optional and defaults to system-sans.

The result names the registered key or keys and gives the exact next call to make:

{
  "registered": ["inter-400", "inter-700"],
  "family": "Inter",
  "license": "OFL-1.1",
  "outcome": "registered",
  "hint": "activate via action:'set' tokens:{\"body-font\":\"inter-400\"} (or \"heading-font\")"
}

A registered key is picked up by name from that point on, in body-font or heading-font, alongside the three system stacks.

If every requested weight downloads as identical bytes, Noon recognises the family as a variable font and registers one key covering the whole weight range instead of one key per weight. A single-weight request cannot be detected as variable this way; use widen-font afterwards to extend it once you know the family is variable:

{ "action": "widen-font", "key": "inter-400", "weights": [400, 700] }

widen-font only ever widens a font already registered under a single weight into a range that contains that weight, and only after confirming the newly downloaded bytes are the same font. It never registers a new font and never changes the family or fallback of the row it widens.

How tokens reach the page

A set theme becomes CSS custom properties in the page head on the very next request. No build or deploy step is involved. A theme with no tokens set, a cleared theme, and the plugin being switched off all render identically: the page looks exactly as it would with no theme at all.

Site-wide custom CSS

Site settings (manage_site) carry one more field for styling: customCss, a single string of raw CSS applied to every page. It is capped at 16 KiB and may not contain the literal sequence </style in any case, because that would close the element it is emitted inside.

customCss is emitted after the theme's token block in the page head, so a rule in customCss that sets the same custom property as a theme token wins. It also outranks every stylesheet Noon ships itself, regardless of source order, because those live inside a named CSS layer and customCss does not.

Because manage_site replaces the whole settings document and any field you omit resets to its default, read the current settings with query first and send every field you want to keep, not only customCss:

{
  "settings": {
    "siteName": "Acme Journal",
    "customCss": ":root{--noon-accent:#ff6600;}"
  },
  "expectedRevision": 5
}