Customization
Adopting the wiki in custom fields and components, and the integration limits worth knowing.
The plugin's surfaces are injected automatically. Everything they are built from is also exported, so a project with custom admin components can put guides wherever it wants.
Fields with a custom Description
The config walk skips any field that already declares its own admin.components.Description, so such a field has no help surface until you add one.
Render WikiFieldHelp inside your own description component:
'use client'
import { WikiFieldHelp } from '@10x-media/admin-wiki/client'
export const MyDescription = ({ schemaPath }: { schemaPath: string }) => (
<>
<span>My own description text.</span>
<WikiFieldHelp schemaPath={schemaPath} />
</>
)The schemaPath is the owner-qualified, index-free path from Targeting: collection:posts.hero.title, or block:heroBanner.heading when the field lives inside a block.
For a component that hardcodes its own rendering and wants the data rather than the UI, useWikiFieldHelp is the one-line adoption path:
const { canCreate, canWrite, entries, hasGuides, picker } = useWikiFieldHelp('collection:posts.title')Prefer canWrite over canCreate when deciding whether to show a write affordance: it already accounts for the configured writeAffordances mode, where canCreate is the raw permission alone.
Picker mode
The field picker renders your real fields, so your custom component appears there too. picker is non-null while a field is rendered inside the picker's drawer, and null everywhere else.
The injected description handles this already. A component rendering its own surface branches the same way:
'use client'
import { WikiFieldHelp, WikiFieldPickTarget, useWikiFieldPicker } from '@10x-media/admin-wiki/client'
export const MyDescription = ({ schemaPath }: { schemaPath: string }) => {
const picker = useWikiFieldPicker()
return (
<>
<span>My own description text.</span>
{picker ? <WikiFieldPickTarget schemaPath={schemaPath} /> : <WikiFieldHelp schemaPath={schemaPath} />}
</>
)
}WikiFieldPickTarget renders nothing outside a picker, so the branch is optional. A component that ignores picker mode keeps working: its fields carry no plate in the drawer, and their paths stay reachable through the path input under the Fields list.
Fields with a function description
Nothing to do: admin.description as a function is supported, and so is a description keyed by locale. The function cannot travel in client props, so such a field gets a server Description that evaluates it with the request's i18n and hands the result down, exactly as Payload's own renderer would. Fields with a static description skip that hop, so the extra server component is paid for only where it is needed.
Your own admin views
A screen the config does not describe, a view registered through admin.components.views and anything inside it, is documented with a custom target: declare the key once in the plugin options, then render WikiCustomHelp with the bare key.
'use client'
import { WikiCustomHelp } from '@10x-media/admin-wiki/client'
<WikiCustomHelp target="dashboard" />
<WikiCustomHelp target="dashboard.attention" showWriteAffordance={false} />That is the whole integration for a view that wants the plugin's own trigger: the hover card, the guide drawer, and the write affordance, exactly as a field gets them. For a screen that wants its own button or its own placement, see Building your own surface.
Any target key
WikiTargetHelp is the generic surface. It takes a full target key and renders nothing unless something targets it:
'use client'
import { customTargetKey, WikiTargetHelp } from '@10x-media/admin-wiki/client'
<WikiTargetHelp targetKey="block:heroBanner" />
<WikiTargetHelp targetKey="collection:posts" showWriteAffordance={false} />
<WikiTargetHelp targetKey={customTargetKey('dashboard')} />The four config-derived prefixes are stable and documented, so a literal reads fine for those. A custom key is the one to always build through customTargetKey, never to type with the namespace on it.
The context
useWikiTargets() exposes everything the provider holds, for components that need more than one surface's worth:
| Member | Type | Notes |
|---|---|---|
entriesFor(key) | WikiTargetEntry[] | Guides for a target key. Synchronous; never fetches |
loadGuide(id) | Promise<WikiGuideDoc | null> | Full content, lazily, cached per guide and locale |
canCreate / canUpdate | boolean | The reader's evaluated permissions on the wiki collection |
canWrite | boolean | canCreate and the write-affordance mode allows it here |
editMode / setEditMode | boolean / (on) => void | The per-browser edit-mode switch |
blockLabels | Record<string, string> | Singular label per block slug, for chips |
customLabels | Record<string, string> | Declared label per custom target key, in the reader's admin language |
blockChips | boolean | Whether the "Covers" chips include blocks |
locale | string | null | The content locale guides resolve in for this reader |
pagesSlug | string | The configured wiki pages slug |
refresh() | () => void | Refetch the targets map |
wikiViewEnabled | boolean | Whether the /wiki routes are registered |
Entries arrive sorted featured-first, then by title.
Building your own surface
Every surface listens on a target key, and the key builders are exported alongside the components, so a key is never spelled by hand:
collectionTargetKey('posts') // collection:posts
globalTargetKey('settings') // global:settings
blockTargetKey('heroBanner') // block:heroBanner
fieldTargetKey('collection:posts.title') // field:collection:posts.title
customTargetKey('dashboard') // custom:dashboardThe four config-derived prefixes are stable and documented, so a literal reads fine for those. A custom key is the one to always build: the custom: namespace is the plugin's to spell, not yours, and a hand-written prefix that drifts attaches the surface to nothing while still looking correct. WikiCustomHelp is exactly that call with the builder already inside it.
The pieces the built-in surfaces are made of are all exported from @10x-media/admin-wiki/client:
| Component | Purpose |
|---|---|
WikiGuideCard | One guide as a card. href to navigate, onClick to open a drawer, compact for narrow columns |
WikiAllGuidesButton | The overflow affordance into a drawer holding several guides |
GuideDrawer | The reading drawer. Takes entries, an optional initial guide, and a modal slug |
GuideArticle | Guide content rendered on its own, without a drawer around it |
TargetChips | The surfaces a guide covers, as pills, with a limit before collapsing to +N. Honors chips.blocks |
WikiWriteGuide | The write-in-place affordance, in inline, button, or menuItem shape. Takes the target key it should prefill |
WikiCustomHelp | The full help surface for a declared custom target, by bare key |
WikiEditModeToggle | The edit-mode pill. Renders nothing when there is nothing to toggle |
WikiTargetFields | The grouped field-target list, as the Targets tab renders it |
WikiFieldPickerDrawer | One kind's picker drawer. Takes a kind, the covered slugs, the current value, and an onConfirm |
WikiFieldPickTarget | The select plate. Renders nothing outside a picker |
A minimal custom surface:
'use client'
import { useDrawerSlug, useModal } from '@payloadcms/ui'
import { GuideDrawer, useWikiTargets, WikiAllGuidesButton } from '@10x-media/admin-wiki/client'
export const MyGuides = () => {
const { entriesFor } = useWikiTargets()
const { openModal } = useModal()
const slug = useDrawerSlug('my-guides')
const entries = entriesFor('collection:posts')
if (entries.length === 0) return null
return (
<>
<WikiAllGuidesButton count={entries.length} onClick={() => openModal(slug)} />
<GuideDrawer entries={entries} slug={slug} />
</>
)
}The same shape on a custom target, with your own trigger and your own empty state. Only the key changes:
'use client'
import { useDrawerSlug, useModal } from '@payloadcms/ui'
import {
customTargetKey,
GuideDrawer,
useWikiTargets,
WikiWriteGuide,
} from '@10x-media/admin-wiki/client'
export const DashboardGuides = () => {
const { entriesFor } = useWikiTargets()
const { openModal } = useModal()
const slug = useDrawerSlug('dashboard-guides')
const targetKey = customTargetKey('dashboard')
const entries = entriesFor(targetKey)
if (entries.length === 0) {
return <WikiWriteGuide targetKey={targetKey} variant="button" />
}
return (
<>
<button onClick={() => openModal(slug)} type="button">
Guides ({entries.length})
</button>
<GuideDrawer entries={entries} slug={slug} />
</>
)
}entriesFor is a synchronous lookup into the targets map the provider already holds, so a surface built this way costs no request of its own. WikiWriteGuide takes the same key and prefills the target list it belongs to, so a custom key opens the create drawer with the Custom targets list already filled.
Reserved field names
Two field names are injected into host schemas, both namespaced so they will not collide with a real field:
| Name | Where | Opt out by |
|---|---|---|
adminWikiGuides | Collection and global sidebars. Exported as WIKI_GUIDES_FIELD | Declaring a field with that name, triggers.edit / triggers.global, or excluding the entity |
adminWikiBlockHelp | Every covered block's fields. Exported as WIKI_BLOCK_HELP_FIELD | Declaring a field with that name, or excluding the block |
An entity that already declares one keeps its own. Both are UI fields, so neither reaches the database or your generated types.
Adding your own fields to the wiki's collections goes the other way: see Collection overrides.
Registering components
Components you hand the plugin (a video player, a block renderer, a wiki view slot) are import-map paths. The plugin registers them under config.admin.dependencies so payload generate:importmap finds them without you referencing them anywhere else.
A path missing from the import map is reported, not swallowed: a block renderer renders a visible placeholder, a video player falls back to the default HTML5 one, both with a warning in the server log. A slot component renders nothing and logs the missing key.
The registry
getWikiRegistry(config) returns the plugin's resolved options plus config-derived data (blockLabels, validTargetKeys), read from config.custom. Exported from both @10x-media/admin-wiki and @10x-media/admin-wiki/rsc, for server components and endpoints that need to know how the plugin was configured. Returns undefined when the plugin did not run.