10x-media plugins
Admin Wiki

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:

MyDescription.tsx
'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:

MyDescription.tsx
'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:

MemberTypeNotes
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 / canUpdatebooleanThe reader's evaluated permissions on the wiki collection
canWritebooleancanCreate and the write-affordance mode allows it here
editMode / setEditModeboolean / (on) => voidThe per-browser edit-mode switch
blockLabelsRecord<string, string>Singular label per block slug, for chips
customLabelsRecord<string, string>Declared label per custom target key, in the reader's admin language
blockChipsbooleanWhether the "Covers" chips include blocks
localestring | nullThe content locale guides resolve in for this reader
pagesSlugstringThe configured wiki pages slug
refresh()() => voidRefetch the targets map
wikiViewEnabledbooleanWhether 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: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: 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:

ComponentPurpose
WikiGuideCardOne guide as a card. href to navigate, onClick to open a drawer, compact for narrow columns
WikiAllGuidesButtonThe overflow affordance into a drawer holding several guides
GuideDrawerThe reading drawer. Takes entries, an optional initial guide, and a modal slug
GuideArticleGuide content rendered on its own, without a drawer around it
TargetChipsThe surfaces a guide covers, as pills, with a limit before collapsing to +N. Honors chips.blocks
WikiWriteGuideThe write-in-place affordance, in inline, button, or menuItem shape. Takes the target key it should prefill
WikiCustomHelpThe full help surface for a declared custom target, by bare key
WikiEditModeToggleThe edit-mode pill. Renders nothing when there is nothing to toggle
WikiTargetFieldsThe grouped field-target list, as the Targets tab renders it
WikiFieldPickerDrawerOne kind's picker drawer. Takes a kind, the covered slugs, the current value, and an onConfirm
WikiFieldPickTargetThe 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:

NameWhereOpt out by
adminWikiGuidesCollection and global sidebars. Exported as WIKI_GUIDES_FIELDDeclaring a field with that name, triggers.edit / triggers.global, or excluding the entity
adminWikiBlockHelpEvery covered block's fields. Exported as WIKI_BLOCK_HELP_FIELDDeclaring 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.

On this page