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:
- If
editor.allow_raw_htmlis set incms.yaml, that wins. - Otherwise Emet reads
markup.goldmark.renderer.unsafefrom your Hugo config: the roothugo.toml(orconfig.yamland friends), withconfig/_default/and the environment named bypreview.hugo_environmentinsite.yamlmerged over it, as Hugo does. The environment defaults toproduction. - 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: 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#
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.