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 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 four things from this file: editor, seo, ai 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: bodyEditor#
| 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#
Hugo’s Markdown renderer drops raw HTML unless the site config sets markup.goldmark.renderer.unsafe = true, and a build with --panicOnWarning fails on it outright. Several visual-editor features (underline, subscript, superscript, highlight, text alignment, links that open in a new tab or carry nofollow) have no Markdown syntax and can only be saved as HTML, so Emet checks the policy before offering them:
editor.allow_raw_htmlincms.yaml, if set, wins.- Otherwise Emet reads
markup.goldmark.renderer.unsafefrom the Hugo config (hugo.toml,config.yamland friends, orconfig/_defaultoverlaid with the environment frompreview.hugo_environment, which defaults toproduction). - If neither says yes, raw HTML is off, which is Hugo’s own default.
When raw HTML is off, those controls and their keyboard shortcuts are gone from the visual editor, pasted rich text loses its alignment and underline styling, and the serializer never writes an HTML tag. Nothing is stripped from files that already contain HTML, but the status bar shows a “Contains raw HTML that Hugo will omit” notice in both editor modes so you can see what the published site is losing. Set allow_raw_html: true only for sites whose Hugo config lives somewhere Emet cannot read it, or false to lock the editor down on a site that does have unsafe on.
editor:
allow_raw_html: falseHugo server preview#
preview:
hugo_server:
url: http://localhost:1313
enabled: trueThis 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: 8enabled: 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: trueThat 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.
WarningA 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#
Off by default. Switch it on and every entry gets a Review tab in the editor, a review state badge in the entry list, a Review board view in the sidebar, and a check in the publish dialog that stops Go Live until every changed entry has been approved. See Reviewing entries for the workflow.
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 publish anyway, with a reason that is recorded in each entry. When unset, repository admins can. | repository admins |
The state is stored in the entry’s own frontmatter, so it travels with the file, survives branch resets and needs no database. Hugo ignores the key unless a template reads it. Review-only saves (marking ready, approving, commenting) are committed as chore(review): … with [skip ci], so they don’t trigger a build.