A plugin adds tables, collections, and sometimes MCP tools or routes to a Noon install. Each plugin below is a separate feature you turn on only if you need it.
Turning a plugin on
Set the NOON_PLUGINS environment variable to a comma separated list of plugin ids, for example:
NOON_PLUGINS=word_count,seo,theme
If the variable is unset, no plugins run. The order of the ids is the order the plugins' hooks run in.
Every Noon process (the core API, the MCP server, and the web frontend) reads this variable at startup and must see the same value. If they disagree, content saved through one process can be invisible or broken in another.
After you change NOON_PLUGINS, restart every process. A plugin you just enabled also needs its database tables created first:
pnpm db:migrate
Run the migration before the restart, or the process refuses to start and tells you which plugin's tables are missing.
The plugin ids are: geo, seo, shop, theme, vote, word_count.
geo
The reader gets country-based content variants and currency labels, without an external geolocation service.
A geo rule matches a visitor's country to a content variant and a currency. One rule can be the wildcard default (matchCountry: "**") that applies when no country-specific rule matches. A country can have at most one active rule at a time.
| Adds | Detail |
|---|---|
| Collection | geo_rules |
| Tool | manage_geo_rule (action upsert or delete; reads go through query) |
| Routes | none |
| Hooks | none (geo resolution happens at request time, not at save time) |
Configuration: none required. Country matching reads a header your reverse proxy sets; see the deployment guide for the header name and the proxy trust setting.
seo
The reader gets JSON-LD structured data, a sitemap, and an RSS-style feed, compiled automatically every time content is saved.
Nothing about this plugin is agent-invoked. Saving a page or post compiles its structured-data document inside the same transaction as the save, so the document can never go stale or be skipped. There are no tools: an agent that wants to read a compiled document queries the seo_jsonld collection.
| Adds | Detail |
|---|---|
| Collection | seo_jsonld |
| Tools | none |
| Routes | GET /sitemap.xml, GET /feed.xml (served by the web frontend; return 404 if the plugin is disabled) |
| Hooks | compiles on after_post_save, after_layout_compile, cleans up on after_draft_discard, after_unpublish, and after_site_settings_write |
Configuration: requires NOON_SITE_ORIGIN, an absolute http:// or https:// URL with no trailing slash. Every canonical URL, sitemap entry, and feed link is built from it. The core process refuses to start with the seo plugin enabled and this variable missing or invalid.
shop
The reader gets a product catalog and Stripe-hosted checkout links, with orders tracked automatically from Stripe webhook events.
A product still saves without Stripe configured. It just records checkoutStatus: 'unconfigured' and checkoutUrl: null until Stripe credentials are set, at which point new saves compile a real checkout link. Order rows are created and updated only by the Stripe webhook, never by a tool.
| Adds | Detail |
|---|---|
| Collections | products, orders |
| Tools | create_product, update_product (soft-delete a product with active: false) |
| Routes | POST /api/v1/webhooks/stripe (served by the core API, authenticated by the Stripe signature, not a bearer token) |
| Hooks | none |
Configuration: NOON_STRIPE_SECRET_KEY and NOON_STRIPE_WEBHOOK_SECRET, both optional. Leave them unset to run a shop with no live checkout. Set NOON_STRIPE_SECRET_KEY to compile real checkout links. Set NOON_STRIPE_WEBHOOK_SECRET (from the Stripe webhook endpoint, not the API keys page) so the webhook route accepts events; without it, the webhook route refuses every request rather than accepting an unverified one.
theme
The reader gets a small set of site-wide design tokens (colors, fonts, spacing) and self-hosted Google Fonts, with no build step to apply a change.
The active theme document is a single row. Setting it replaces the whole document: a set call with three token keys leaves a three-key document, even if the previous one had more. There is no merge.
| Adds | Detail |
|---|---|
| Collection | theme_document |
| Tool | manage_theme (action set or clear, plus a register-google-font action for self-hosting a font) |
| Routes | none |
| Hooks | none (theme resolution happens at request time) |
Configuration: none required to enable it. If a themed deployment omits theme from NOON_PLUGINS, the site renders unthemed even if a theme document exists in the database, with no warning.
vote
The reader gets polls with a deadline, single or multiple choice, and results that appear once the poll closes.
Individual ballots are deliberately not queryable through the query tool. A ballot row carries a hashed voter identity, and exposing it on a general query surface would let anyone with read access reconstruct who voted for what. Tallies are read through the tool's results action instead, which returns counts only.
| Adds | Detail |
|---|---|
| Collections | vote_polls, vote_options (ballots are not a collection) |
| Tool | manage_poll (actions: create, update, close, add-option, remove-option, results) |
| Routes | POST /api/vote (served by the web frontend, public) |
| Hooks | none |
Configuration: requires NOON_VOTE_HASH_KEY, 32 random bytes used to turn a visitor's address into a per-poll voter handle. Without it, the vote route refuses every request rather than hashing with an empty key. The vote route also needs the real client address forwarded by your reverse proxy (X-Real-IP); without it, voting is refused for the same fail-closed reason.
word_count
The reader gets an automatic word count and reading time on every post and page, and a guard that blocks publishing a post with an empty body.
The count is recomputed on every save, including a rollback, and refreshed again if a draft is discarded on content that is still published. There is nothing to configure and nothing to invoke: the stats are read through the word_count_stats collection.
| Adds | Detail |
|---|---|
| Collection | word_count_stats |
| Tools | none |
| Routes | none |
| Hooks | transforms the draft to set frontmatter.readingTime on save, upserts stats on save, vetoes publishing a post with an empty body, cleans up on draft discard |
Configuration: none.