Configuration Options
All options are passed inside the healthDashboard key of seofields(). The only required option is licenseKey — everything else has sensible defaults.
Quick Reference
| Option | Type | Default | Description |
|---|---|---|---|
| licenseKeyrequired | string | — | Required to unlock the dashboard. |
| apiVersion | string | '2023-01-01' | Sanity Content Lake API version used for the dashboard query. |
| tool.title | string | 'SEO Health' | Label shown on the Studio navigation tab. |
| tool.name | string | 'seo-health-dashboard' | Internal tool slug used by Sanity Studio. |
| content.icon | string | — | Emoji shown before the heading inside the dashboard page. |
| content.title | string | — | Custom heading inside the dashboard page. |
| content.description | string | — | Subtitle shown beneath the heading in the dashboard. |
| content.loadingLicense | string | 'Verifying license…' | Text shown while the license key is being verified. |
| content.loadingDocuments | string | 'Loading documents…' | Text shown while the document list is being fetched. |
| content.noDocuments | string | 'No documents found' | Text shown when the query returns zero results (or search/filter matches nothing). |
| showTypeColumn | boolean | true | Show or hide the document type column in the results table. |
| showDocumentId | boolean | true | Show or hide the Sanity document _id under each title. |
| display.typeColumndeprecated | boolean | true | Show or hide the document type column in the results table. |
| display.documentIddeprecated | boolean | true | Show or hide the Sanity document _id under each title. |
| query.types | string[] | — | Limit the dashboard to specific Sanity document type names. |
| query.requireSeo | boolean | true | Only include documents that have a non-null seo field. |
| query.groq | string | — | Fully custom GROQ query — overrides query.types and query.requireSeo. |
| typeDisplayLabels | Record<string, string> | — | Map raw _type values to human-readable labels. |
| typeLabelsdeprecated | Record<string, string> | — | Map raw _type values to human-readable labels. |
| typeColumnMode | 'badge' | 'text' | 'badge' | Render the Type column as a dynamically-coloured badge or plain text. |
| titleField | string | Record<string, string> | 'title' | Document field to use as the display title — supports per-type mapping. |
| previewMode | boolean | false | Load realistic dummy data instead of querying your dataset — great for demos and screenshots. |
| structureTool | string | — | Name of the Structure tool to use when opening documents from the dashboard. |
| export | boolean | { enabled?: boolean; formats?: Array<'csv' | 'json'> } | true | Enable export buttons so editors can download the currently filtered document list as CSV or JSON. |
| compactStats | boolean | false | Replace the 6-card stats grid with a single row of inline stat pills to save vertical space. |
| getDocumentBadge | (doc) => { label, bgColor?, textColor?, fontSize? } | undefined | — | Callback that returns a custom badge shown next to the document title. |
| docBadgedeprecated | (doc) => { label, bgColor?, textColor?, fontSize? } | undefined | — | Callback that returns a custom badge shown next to the document title. |
Query precedence & defaults
The dashboard builds its document list using one of two strategies, evaluated in this order:
- 1
query.groq— highest priority. When this is set, the dashboard runs your GROQ string verbatim and ignoresquery.typesandquery.requireSeoentirely. Your query must return documents with at least:_id,_type,title,seo,_updatedAt. - 2
query.types— restricts the built-in query to only the listed document types. Combine withquery.requireSeoto also toggle whether documents without anseofield are included. - 3Default (nothing set) — the dashboard queries
*[seo != null], returning every document across all types that has a non-nullseofield.
query.groq and query.types, query.groq wins and query.types is silently ignored.Detailed Reference
licenseKeystringREQUIREDWithout a valid licenseKey the SEO Health tab will not render any data. Keys follow the SEOF-XXXX-XXXX-XXXX format and are tied to your Sanity project ID.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
},
})apiVersionstringdefault: '2023-01-01'The default is stable and works for all current Sanity projects. Only change this if you need a specific API version for compatibility reasons.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
apiVersion: '2024-01-01',
},
})tool.titlestringdefault: 'SEO Health'The text that appears as the tool's tab name in the Sanity Studio sidebar. The heading inside the dashboard page is controlled separately via content.title.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
tool: { title: 'SEO Audit' },
},
})tool.namestringdefault: 'seo-health-dashboard'The internal identifier for the tool. Only change this if you register multiple instances or have a naming conflict with another Studio tool.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
tool: { name: 'my-seo-tool' },
},
})content.iconstringdefault: —Rendered next to the title inside the dashboard page. Any single emoji works.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { icon: '🔍' },
},
})content.titlestringdefault: —Replaces the default page heading inside the dashboard. The Studio nav tab label is controlled separately via tool.title.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { title: 'Content SEO Audit' },
},
})content.descriptionstringdefault: —Adds a short description below the dashboard heading. Useful for giving editors context about what the dashboard shows.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { description: 'Track SEO quality across all published blog posts.' },
},
})content.loadingLicensestringdefault: 'Verifying license…'Replaces the default spinner message displayed while the dashboard checks your license key against the server. Useful for matching your team's language or branding.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { loadingLicense: 'Checking your plan…' },
},
})content.loadingDocumentsstringdefault: 'Loading documents…'Replaces the default spinner message while the GROQ query is in flight. Helpful when fetching a large content set and you want to set editor expectations.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { loadingDocuments: 'Fetching your content, hang tight…' },
},
})content.noDocumentsstringdefault: 'No documents found'Displayed in place of the table when no documents match the current query, search, or filter. You can tailor this message to match your content model — e.g. tell editors which document types are expected to have SEO fields.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
content: { noDocuments: 'No pages with SEO fields yet — try publishing one first.' },
},
})showTypeColumnbooleandefault: trueDefaults to true. Set to false to hide the _type column — useful when all your documents are the same type.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
showTypeColumn: false,
},
})showDocumentIdbooleandefault: trueDefaults to true. Exposes the raw document ID in the table. Set to false for a cleaner editor UI.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
showDocumentId: false,
},
})display.typeColumnbooleandefault: trueDEPRECATED→ showTypeColumnRenamed in v1.3.2 to the top-level showTypeColumn. The old nested form still works for backwards compatibility but will be removed in a future major release.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
showTypeColumn: false, // ← replacement key
// display: { typeColumn: false }, // ← old form, still works
},
})display.documentIdbooleandefault: trueDEPRECATED→ showDocumentIdRenamed in v1.3.2 to the top-level showDocumentId. The old nested form still works for backwards compatibility but will be removed in a future major release.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
showDocumentId: false, // ← replacement key
// display: { documentId: false }, // ← old form, still works
},
})query.typesstring[]default: —Restricts the built-in query to only the listed document types. By default (nothing set) all types with a non-null seo field are included. Ignored entirely when query.groq is also set — see the precedence callout above.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
query: { types: ['post', 'page', 'product'] },
},
})query.requireSeobooleandefault: trueWhen true (the default), documents where seo is null or missing are excluded from the results. Set to false to include all documents of the queried types even if they have no seo field. This option is only meaningful when query.types is set — it has no effect when query.groq is used, since your custom query controls filtering entirely.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
query: { requireSeo: false },
},
})query.groqstringdefault: —Highest priority — when set, query.types and query.requireSeo are both ignored entirely. The dashboard runs your GROQ string verbatim. Use this when you need full control over filtering (e.g. exclude drafts, filter by a nested field, or join across document types).
| Key | Type | Purpose |
|---|---|---|
| _id | string | Sanity document ID — used to build the Studio edit link. |
| _type | string | Document type — drives the Type column and type-filter dropdown. |
| title | string | Display name shown in the dashboard table. Use titleField to map a different field name per type. ⚠ Must be a plain string. If your document stores the title as Portable Text (block content) or any other non-string type, use pt::text() in your GROQ projection to convert it first — e.g. "title": |
| seo | object | SEO field object — the dashboard reads all sub-fields to compute the 0–100 score. |
| _updatedAt | string (ISO 8601) | Last-modified timestamp — shown in the Last Updated column. |
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
query: {
groq: `*[_type == "post" && defined(title) && !(_id in path("drafts.**"))] {
_id, _type, title, seo, _updatedAt
}`,
},
},
})typeDisplayLabelsRecord<string, string>default: —Used in both the Type column and the Type filter dropdown. Any _type without an entry falls back to the raw _type string. Useful when your document type names are technical (e.g. productDrug) and you want editors to see friendly names (e.g. Products).
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
typeDisplayLabels: {
productDrug: 'Products',
singleCondition: 'Condition',
landingPage: 'Landing Page',
},
},
})typeLabelsRecord<string, string>default: —DEPRECATED→ typeDisplayLabelsRenamed in v1.3.2 to typeDisplayLabels. The old key still works for backwards compatibility but will be removed in a future major release.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
typeDisplayLabels: { // ← replacement key
productDrug: 'Products',
},
// typeLabels: { ... }, // ← old key, still works
},
})typeColumnMode'badge' | 'text'default: 'badge'badge (default) renders each type as a coloured pill. Badge colours are assigned dynamically: a deterministic hash of the _type string picks a colour from a 16-colour palette, so every type — including custom ones — automatically gets a distinct, consistent colour with no configuration needed. text renders it as plain inline text — useful for dense layouts or when badge colours feel distracting.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
typeColumnMode: 'text',
},
})titleFieldstring | Record<string, string>default: 'title'By default the dashboard reads the title field on every document. If some of your document types store the display name in a different field (e.g. name, label, heading), set titleField to that field name. Pass a string to use the same field across all types, or a Record<string, string> to specify a different field per type. Unmapped types always fall back to title.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
// Same field for all types
titleField: 'name',
// — or — per-type mapping
titleField: {
post: 'title',
product: 'name',
category: 'label',
},
},
})previewModebooleandefault: falseWhen true, the dashboard bypasses license validation and skips all Sanity queries. Instead it renders a set of sample documents covering the full score range (excellent → missing). An amber 'Preview Mode' badge is shown in the dashboard header so editors know the data is not real. Useful for documentation screenshots, showcasing the dashboard to stakeholders, or testing the UI before your content model is in place.
seofields({
healthDashboard: {
previewMode: true,
},
})structureToolstringdefault: —When you have multiple structure tools registered in your Sanity Studio, the generic intent resolver always picks the first one. Set structureTool to the name of the tool that contains your monitored documents (e.g. 'common') and clicking a document title will navigate directly to /{basePath}/{structureTool}/intent/edit/id=…;type=…/ instead.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
structureTool: 'common', // the `name` of your structure tool
},
})exportboolean | { enabled?: boolean; formats?: Array<'csv' | 'json'> }default: trueWhen true (the default), both a 'Export CSV' and 'Export JSON' button appear in the controls bar. The export always reflects the current search query, status filter, type filter, and sort order — exactly the rows visible on screen. Set to false to hide all export controls. Pass an object to enable only specific formats.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
// Disable export entirely
export: false,
// — or — enable only CSV
export: { formats: ['csv'] },
// — or — enable only JSON
export: { formats: ['json'] },
},
})compactStatsbooleandefault: falseWhen false (default), the dashboard renders the familiar 6-card grid showing Total, Avg Score, Excellent, Good, Fair, and Poor/Missing counts. When true, that grid is hidden and a compact pill row is shown inside the header instead — for example: '📋 12 · 🟢 Excellent: 4 · 🟡 Good: 3 · 🟠 Fair: 2 · 🔴 Poor/Missing: 3 · 📊 Avg: 65% · 🗂️ Types: 2'. Useful in dense dashboards or when vertical space is at a premium.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
compactStats: true,
},
})getDocumentBadge(doc) => { label, bgColor?, textColor?, fontSize? } | undefineddefault: —The function receives the full document object (including any custom fields your schema defines) and should return a badge descriptor or undefined to render nothing. bgColor and textColor accept any valid CSS colour value. Tip: use this to surface business-level metadata at a glance — for example, whether a page targets NHS or Private patients.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
getDocumentBadge: (doc) => {
if (doc.services === 'NHS')
return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
if (doc.services === 'Private')
return { label: 'Private', bgColor: '#fef3c7', textColor: '#92400e' }
},
},
})docBadge(doc) => { label, bgColor?, textColor?, fontSize? } | undefineddefault: —DEPRECATED→ getDocumentBadgeRenamed in v1.3.2 to getDocumentBadge. The old key still works for backwards compatibility but will be removed in a future major release.
seofields({
healthDashboard: {
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
getDocumentBadge: (doc) => { // ← replacement key
if (doc.services === 'NHS')
return { label: 'NHS', bgColor: '#e0f2fe', textColor: '#0369a1' }
},
// docBadge: (doc) => { ... }, // ← old key, still works
},
})Full Example
All options used together — mix and match what you need:
display.typeColumn→showTypeColumndisplay.documentId→showDocumentIdtypeLabels→typeDisplayLabelsdocBadge→getDocumentBadge
Old keys still work for backwards compatibility but are deprecated — the dashboard shows an amber banner listing every deprecated key in use, grouped by the release that introduced the rename, each with a direct link to the relevant changelog section. See the v1.3.2 changelog for full migration details.
- Export CSV / JSON — download the currently filtered list via the
exportoption. Both formats reflect the active search query, filters, and sort order. - Pagination & page-size selector — results are now paged (25 / 50 / 100 / 200 rows). The chosen page size persists in localStorage.
- Saved filters — the last-used status filter and type filter are stored in localStorage and restored on next load.
- Compact stat pills — set
compactStats: trueto replace the 6-card stats grid with a single row of inline pills.
- Refresh button — a "Refresh" button in the dashboard header re-fetches documents without a full-page loading flash. The icon spins while the update runs and the button disables automatically during in-progress loads.
- Non-string title warning — if a document's
titleis a Portable Text array (or any non-string value), an amber ⚠ badge appears in the Title column instead of a broken link. The badge prompts you to add"title": pt::text(title)to yourquery.groqprojection.
import { defineConfig } from 'sanity'
import seofields from 'sanity-plugin-seofields'
export default defineConfig({
// ... your project config
plugins: [
seofields({
healthDashboard: {
// Required
licenseKey: 'SEOF-XXXX-XXXX-XXXX',
// Studio nav tab
tool: {
title: 'SEO Audit', // tab label in Studio sidebar
name: 'seo-audit', // internal tool slug
},
// Dashboard page content & loading text
content: {
icon: '🔍',
title: 'SEO Audit',
description: 'Track SEO quality across published content.',
loadingLicense: 'Checking your plan…',
loadingDocuments: 'Fetching content, hang tight…',
noDocuments: 'No pages with SEO fields yet.',
},
// Table columns
showTypeColumn: true, // replaces display.typeColumn
showDocumentId: false, // replaces display.documentId
// Querying
query: {
types: ['post', 'page'],
requireSeo: true,
// groq: '*[_type == "post"] { _id, _type, title, seo, _updatedAt }',
},
// Type column labels & render mode
typeDisplayLabels: { // replaces typeLabels
productDrug: 'Products',
singleCondition: 'Condition',
},
typeColumnMode: 'badge', // 'badge' | 'text'
// Use a different field as the display title per type
titleField: {
post: 'title',
product: 'name',
category: 'label',
},
// Custom badge next to document title
getDocumentBadge: (doc) => { // replaces docBadge
if (doc.status === 'draft')
return { label: 'Draft', bgColor: '#f3f4f6', textColor: '#6b7280' }
if (doc.status === 'published')
return { label: 'Published', bgColor: '#dcfce7', textColor: '#15803d' }
},
// Preview / demo mode — shows dummy data, no licence required
previewMode: false,
// Route document links to a specific structure tool (multi-tool setups)
structureTool: 'desk', // omit if you only have one structure tool
apiVersion: '2023-01-01',
// Export — allow editors to download filtered data as CSV or JSON
export: { formats: ['csv', 'json'] }, // or: export: false to disable
// Compact stats — inline pills instead of 6-card grid (saves vertical space)
compactStats: false,
},
}),
],
})Was this page helpful?