payload-block-extractor

SkillSearch

Use when a developer adds a new page block to this CMS and needs to wire up semantic search indexing for it. Triggers on: "add block to search", "index new block", "wire up extractor", or any mention of a new block needing search support.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the payload-block-extractor skill

What this skill tells your AI

The instructions your AI receives, as published by focusreactive/payload-plugins in .agents/skills/payload-block-extractor/SKILL.md and read by ahel’s review.

Use this skill when a new block has been created at apps/cms/src/blocks/<BlockName>/config.ts and needs its text extraction wired into semantic search.

Each block owns its extractor at apps/cms/src/blocks/<BlockName>/extractText.ts. The orchestrator that calls all extractors is apps/cms/src/collections/Page/extractPageText.ts. The two shared utilities are extractLexicalText and joinText from @/lib/utils/text.


Step 1 — Read the block config

Read apps/cms/src/blocks/<BlockName>/config.ts. Identify all fields and their types.


Step 2 — Map fields to extractor expressions

Field typeExpression
text / textareablock.fieldName
richTextextractLexicalText(block.fieldName)
array with sub-fields(block.items ?? []).flatMap(item => [<recurse sub-fields>])
relationship / upload / other// TODO: manual extraction needed for block.fieldName

For nested array fields, recurse into sub-fields using the same mapping rules.


Step 3 — Create apps/cms/src/blocks/<BlockName>/extractText.ts

Write the extractor function in the block's own folder:

import type { <BlockName>Block } from '@/payload-types'
import { extractLexicalText, joinText } from '@/lib/utils/text'

export function extract<BlockName>Text(block: <BlockName>Block): string {
  return joinText([
    // mapped field expressions
  ])
}

Example — for a block named Promo with a text field, a richText body, and an items array each having a label text field:

// apps/cms/src/blocks/Promo/extractText.ts
import type { PromoBlock } from "@/payload-types";
import { extractLexicalText, joinText } from "@/lib/utils/text";

export function extractPromoText(block: PromoBlock): string {
  return joinText([block.text, extractLexicalText(block.body), ...(block.items ?? []).flatMap((item) => [item.label])]);
}

Step 4 — Register the extractor in extractPageText.ts

Open apps/cms/src/collections/Page/extractPageText.ts.

  1. Add an import for the new extractor function (keep imports sorted alphabetically by block name):
import { extract<BlockName>Text } from '@/blocks/<BlockName>/extractText'
  1. Add a case to the switch inside extractPageBlockText:
case 'blockSlug':
  return extract<BlockName>Text(block)

The blockSlug is the slug property defined in the block's config.ts.


Step 5 — Lint and type-check

cd apps/cms && bun run check-types && bun run lint

Fix any errors before committing.


Step 6 — Commit

git add apps/cms/src/blocks/<BlockName>/extractText.ts
git add apps/cms/src/collections/Page/extractPageText.ts
git commit -m "feat(search): add text extractor for <BlockName> block"

Relationship fields

When a field is a relationship or upload, emit a // TODO rather than guessing:

// TODO: manual extraction needed for block.heroImage (relationship/upload)

Leave the TODO in place and note it in the PR description so a human can decide whether to populate it (e.g. by looking up the related document's title).

Signals

GitHub stars
21
Last commit
Sep 2026
Advanced
Item type
skill
Key
payload-block-extractor
Source
github.com/focusreactive/payload-plugins