CMS config

admin/cms.yaml is optional. It holds the settings that are about how Emet behaves rather than how your site is built: which editor opens by default, what the link dialog offers, which SEO rules count, the AI providers, whether entries need review and where the local Hugo server is. Delete the file and everything falls back to defaults, with one exception: AI features are off entirely until this file has an ai: block.

Emet reads exactly five things from this file: editor, seo, ai, review and preview.hugo_server. Anything else, including preview.styles or preview.wrapper, is ignored here; those belong in site.yaml.

A complete example#

editor:
  default_mode: visual
  links:
    show_new_tab: true
    show_nofollow: true

preview:
  hugo_server:
    url: http://localhost:1313
    enabled: true

seo:
  enabled: true
  rules:
    titleHasPowerWords:
      enabled: false
    keywordDensity:
      weight: 8

ai:
  enabled: true
  defaultProvider: gemini
  providers:
    - type: gemini
      model: gemini-3.5-flash
    - type: openai
      model: gpt-5.5
    - type: anthropic
      model: claude-sonnet-4-6
  features:
    inline: true
    imageGeneration: true
    translation: true
    seoOptimize: true
    pageBuilder: true
  shortcuts:
    inlineAction: Mod-J
    openChat: Mod-Shift-A
  brandVoice: |
    Plain British English. Short sentences. No exclamation marks.
  actions:
    - id: tldr
      label: Add a TL;DR
      prompt: Write a two-sentence summary and put it at the top under a "TL;DR" heading.
      scope: body

review:
  enabled: true
  admins: [clara]

Editor#

Key What it does Default
default_mode visual or markdown. Which editor an entry opens in. visual
allow_raw_html Whether the visual editor may write raw HTML into Markdown. Unset means detect it from the Hugo config. detected
links.show_new_tab Adds an Open in new tab checkbox to the link dialog. false
links.show_nofollow Adds a nofollow checkbox to the link dialog. false
links.default_new_tab Pre-ticks new tab for new links. false
links.default_nofollow Pre-ticks nofollow for new links. false

default_mode only applies until someone switches modes. After that their choice is remembered in their browser and the config value is ignored for them, which is what you’d want: a Markdown person shouldn’t have to flip the toggle every time.

By default the link dialog is just a URL field and optional link text. The two show_* keys add the checkboxes; the two default_* keys set what they start as. Ticking either one sets target="_blank" or rel="nofollow" on the link, which Markdown has no syntax for, so expect HTML in your source for those links. The checkboxes only appear when the site allows raw HTML (see below); otherwise they are hidden whatever these keys say.

Raw HTML#

Markdown has no syntax for underline, subscript, superscript, highlighting, text alignment or a header-less table, and no way to say a link should open in a new tab or carry nofollow. The visual editor can only save those as raw HTML. The catch is that Hugo drops raw HTML from the page unless the site sets markup.goldmark.renderer.unsafe = true, and a build run with --panicOnWarning fails on it outright. So before offering any of those features, Emet works out whether your site would actually render them:

  1. If editor.allow_raw_html is set in cms.yaml, that wins.
  2. Otherwise Emet reads markup.goldmark.renderer.unsafe from your Hugo config: the root hugo.toml (or config.yaml and friends), with config/_default/ and the environment named by preview.hugo_environment in site.yaml merged over it, as Hugo does. The environment defaults to production.
  3. If neither says yes, raw HTML is off, which is Hugo’s own default.

With raw HTML off, those toolbar controls and their keyboard shortcuts disappear, pasted rich text loses its alignment and underline styling, and the visual editor never writes an HTML tag. Files that already contain HTML aren’t stripped, but the status bar says “Contains raw HTML that Hugo will omit” in both editor modes, so you can see what the published site is losing.

The answer is worked out once per session and checked again when you pull remote changes or save the Hugo config in Emet’s own config editor. Until it’s known, the visual editor shows an empty pane for a moment.

Set allow_raw_html: true only if your Hugo config lives somewhere Emet can’t read it, or false to lock the editor down on a site that does have unsafe on:

editor:
  allow_raw_html: false

Hugo server preview#

preview:
  hugo_server:
    url: http://localhost:1313
    enabled: true

This only matters when you’re running the dev server against a local site or using the desktop app, both of which can show the real Hugo output instead of the CSS preview. url is where the preview looks for hugo server (the desktop app starts its own and uses that instead); enabled: false hides the Hugo preview toggle so the pane is CSS-only. Both keys are optional. See Hugo server preview.

SEO#

seo:
  enabled: true
  rules:
    titleHasPowerWords:
      enabled: false
    keywordDensity:
      weight: 8

enabled: false hides the SEO tab altogether. Under rules, each of the 26 rules can be switched off or given a different weight; the score is the weighted share of rules that pass, so weight is relative, not a fixed number of points. The rule IDs are listed in SEO rules; the old snake_case names (keyword_in_title and friends) from early versions still work.

Note that the thresholds each rule checks against, like keyword density or title length, are fixed. A params key under a rule is accepted but nothing reads it.

AI#

AI is off until cms.yaml has an ai: block. The shortest one that works is:

ai:
  enabled: true

That gives you three providers (Gemini, OpenAI and Anthropic, with Gemini as the default), every feature switched on and no keys. Each person then pastes their own API key into the AI settings, where it stays in their browser. For anything more than that you’ll want the full block.

Providers#

Key What it does
type gemini, openai, anthropic or custom.
model The model to use by default. People can pick a different one from the list the provider reports.
apiKey A site-wide key shared by everyone. See below.
endpoint Base URL for a custom provider. Ignored on the other three, so a repository config can’t quietly redirect your OpenAI key somewhere else.

defaultProvider picks which of them is selected to begin with; it falls back to the first in the list.

custom is any OpenAI-compatible API, which covers Ollama’s /v1 endpoint and most self-hosted gateways. A key is optional for it.

For Gemini, a Vertex AI Express key (they start with AQ.) is detected automatically and requests go to the Vertex endpoint instead of AI Studio. The trade-off is that Vertex Express doesn’t offer a model listing, so you get a fixed list of older Gemini models rather than whatever’s current.

Site-wide keys#

Put an apiKey on a provider and everyone using the CMS gets AI without setting anything up. The AI settings show a Site key badge for that provider, and anyone can still enter their own key to override it.

Warning
A site key is committed to your repository in plain text. Anyone who can read the repository can read the key and spend your money with it. Only do this on a private repository, and prefer a key with a spending limit. The setup wizard warns you if it notices the repository is public, but it can’t notice a repository that becomes public later.

Features#

Each of these is on by default; set it to false to remove that feature.

Key What it turns off
inline The AI button in the editor toolbar and the selection menu.
imageGeneration Image generation, including Edit with AI on existing images.
translation The translate action. This also needs more than one language configured.
seoOptimize The SEO title, description and keyword generators in the chat panel and toolbar menu.
pageBuilder The page builder.

Shortcuts#

shortcuts.inlineAction (default Mod-J) opens the inline action menu and shortcuts.openChat (default Mod-Shift-A) opens the chat panel. These use ProseMirror’s key names, where Mod is Ctrl on Windows and Linux and Cmd on a Mac.

Brand voice#

brandVoice is free text that’s added to the system prompt of every AI request: rewrites, chat, translation, the page builder. It’s the place for “we don’t use exclamation marks” and “the product is called Emet, not EmetCMS”. Keep it short; it goes out with every request.

Custom actions#

actions adds your own entries to the toolbar’s AI menu, after the built-in ones.

Key What it does
id A unique name.
label The menu text.
prompt The instruction sent with the text.
scope selection (the default) acts on the selected text, or the whole body if nothing’s selected. body always acts on the whole body.

See AI for what the features do and AI providers for the models each provider offers.

Review#

Per-entry review, off by default. Switch it on and every entry gets a Review tab in the editor and a review badge in the entry list, the sidebar gets a Review board, and the publish dialog won’t Go Live until every changed entry has been approved. See Reviewing entries for how it works.

review:
  enabled: true
  field: review                      # frontmatter key that holds the state
  require_approval_to_publish: true  # gate Go Live on approvals
  admins:                            # who may override the gate
    - octocat
Key What it does Default
enabled Turns the workflow on. With it off nothing changes in the UI or in your files. false
field The frontmatter key the state lives under. Change it if your theme already uses review. review
require_approval_to_publish Disable Go Live in the publish dialog while any changed entry is not approved, or was edited after approval. true
admins GitHub logins allowed to override the gate and publish anyway, giving a reason that’s recorded in each entry. Setting this replaces the default rather than adding to it. repository admins

The state lives in the entry’s own frontmatter, so it travels with the file and needs no database. Review-only saves (marking ready, approving, commenting) are committed as chore(review): … with your skip-CI marker, so they don’t trigger a build.