@focus-reactive/payload-plugin-seo

中文文档
1.12.0发布于 2026/11/12MITTypeScriptES Module构建来源已验证近 30 天下载 2818

现场SIO分析 有效载荷CMS v3 + Next.js, 由 烟。在文档编辑器上添加一个实时的 SEO 抽屉 — 键入优化、页面检查、可读性、包容性朗

中文使用文档
文档目录34 节
  1. AI 整合提示
  2. 如何运作
  3. 安装
  4. 快速启动
  5. 步骤1——注册插件
  6. 步骤2 — 导入管理员样式
  7. 步骤 3 — 转接 Yaast UI 包
  8. 步骤4——编写和登记内容提取器
  9. 配置参考
  10. 插件选项
  11. 徐运会
  12. 徐场派斯
  13. 徐锡克
  14. 内容提取
  15. 这个 ContentNode 中级代表
  16. 构建器助手
  17. 提取器合同
  18. 登记册:为何存在以及如何使用登记册
  19. 限制: 嵌入到 richText 中的链接和上传
  20. 生成的 SEO 字段
  21. 使用量
  22. seoTextField 选项
  23. 生成端点和 OpenAI 配置
  24. 生成提取器
  25. 分析绘图器
  26. 密钥句标签
  27. 密钥词索引
  28. 终点和接入
  29. "已经使用"的通知
  30. 内部链接标签
  31. 本地化
  32. 行政语言
  33. 参考文献
  34. 许可证

@ focus- reactive/ payload-plugin-seo (重心反应/负载)

现场SIO分析 有效载荷CMS v3 + Next.js, 由 烟。在文档编辑器上添加一个实时的 SEO 抽屉——键词优化,上页检查,可读性,包容性语言,内容关键值,以及Google SERP预览——而不在文档中添加一个单字段.

插件向每个已配置的收藏的编辑工具栏中插入一个按钮。 点击它打开一个抽屉,该抽屉可以读取当前(未保存)的窗体值、取出标题、元描述和从dot-path config中取出 slock, 运行  注册内容提取器以构建主体内容,并运行 Yoast 分析引擎 完全在浏览器中. 默认情况下,没有任何东西继续存在——没有新的收藏、全球收藏或字段。 选入 键词索引 添加一个隐藏收藏 .


AI 整合提示

复制并粘贴此提示到您的 AI 助手( Cursor, Claude等) 中, 将插件整合到已有的 Biveload + Next.js 项目中 .

I want to add live SEO analysis to my Payload CMS v3 + Next.js project using @focus-reactive/payload-plugin-seo.

## How it works

By default the plugin adds NO database fields, collections, or globals. It injects a button into the
document editor toolbar (admin.components.edit.beforeDocumentControls) of each configured
collection. The button opens a drawer that:
- Reads the live (unsaved) form values for the document
- Derives title, meta description, and slug from dot-path config
- Runs YOUR registered content extractor to build the body content. Content extraction is
  app-owned — there is no built-in walker. The extractor receives the raw form values plus a
  toolkit ({ resolveDocs, helpers }); it fetches any referenced/upload docs it needs via
  resolveDocs and returns a ContentNode[] the plugin serializes to HTML.
- Runs the Yoast engine (yoastseo + @yoast/search-metadata-previews) in the browser
- Shows tabs: Keyphrase, On-page SEO, Readability, Inclusive, Content vitals, SERP preview

It works with or without a focus keyphrase; keyphrase-specific checks unlock once you enter one.

## Installation

pnpm add @focus-reactive/payload-plugin-seo

## Step 1 — Register the plugin in payload.config.ts

import { seoPlugin } from '@focus-reactive/payload-plugin-seo'

// Inside buildConfig({ plugins: [...] })
seoPlugin({
  collections: [
    {
      slug: 'pages',
      fields: {
        seoTitle: 'seoTitle',           // dot-path; falls back to useAsTitle / 'title'
        metaDescription: 'metaDescription',
        slug: 'slug',                   // default: 'slug'
      },
      // REQUIRED: lookup key for a content extractor you register (see Step 4).
      extractContentPath: '@/seo/extractPageContent#default',
    },
  ],
  site: { name: 'My Site', baseUrl: 'https://example.com', faviconUrl: '/favicon.ico' },
  supportedLocales: ['en'],            // language packs to load; default ['en']
})

## Step 2 — Import the admin styles

In your Payload admin CSS (e.g. app/(payload)/custom.scss):

@import "@focus-reactive/payload-plugin-seo/admin.css";

## Step 3 — Allow Next.js to transpile the Yoast UI packages

In next.config.mjs:

const nextConfig = {
  transpilePackages: ['@yoast/search-metadata-previews', '@yoast/components'],
}

## Step 4 — Write and register a content extractor (REQUIRED)

Content extraction is entirely yours — there is no built-in walker, and extractContentPath is
required. Write an extractor and register it under the same key, from an admin-mounted client
module:

// src/seo/extractPageContent.ts
import type { ContentExtractor } from '@focus-reactive/payload-plugin-seo/content'
const extractPageContent: ContentExtractor = async (values, ctx, { resolveDocs, helpers }) => {
  // 1. collect ids from the RAW values (relationship/upload fields are ids)
  // 2. const docs = await resolveDocs([{ collection: 'media', ids, select: ['url','alt'] }])
  // 3. build the IR with helpers
  return helpers.compact([helpers.heading(1, values.title as string) /* … */])
}
export default extractPageContent

// src/providers/SeoExtractorRegistrar.tsx   ("use client")
import { registerContentExtractors } from '@focus-reactive/payload-plugin-seo/content'
import extractPageContent from '@/seo/extractPageContent'
registerContentExtractors({ '@/seo/extractPageContent#default': extractPageContent })
// export a component that renders {children} and mount it via admin.components.providers

## Important notes

- The plugin reads UNSAVED form values, so analysis updates live as you type (debounced ~1s).
- Content extraction is done by YOUR registered extractor; extractContentPath is required and
  there is no built-in fallback. The extractor receives raw form values, a ctx
  ({ locale, apiRoute }), and a toolkit ({ resolveDocs, helpers }), and returns ContentNode[].
- relationship/upload fields arrive as ids; use toolkit.resolveDocs(queries) to fetch only the
  docs/fields you need (one parallel request per collection), then read them with store.get().
- Non-English analysis requires the locale code in `supportedLocales`; the matching Yoast
  language pack is dynamically imported on demand.
- No GA4, no API keys. Without keyphraseIndex there are no server calls except the resolveDocs reads
  against your own Payload API.
- Optional: `keyphraseIndex: true` stores keyphrases in a hidden `seo-keyphrases` collection,
  warns when a keyphrase is already used elsewhere, and adds an Internal links tab. It adds a
  collection, so SQL adapters need a migration (payload migrate:create). Add a per-collection
  `resolveUrl: ({ doc, locale, req }) => string | null` to show public URLs in link suggestions.

如何运作

Document editor (configured collection)
        │
        ▼
[ SeoButton ]  ← injected into admin.components.edit.beforeDocumentControls
        │  click
        ▼
   SEO Drawer (client-only)
        │
        ├─ read live form values (title, description, slug, keyphrase)
        ├─ run your registered extractor(values, ctx, toolkit)
        │     └─ toolkit.resolveDocs(): parallel, projected /api/{collection} fetches
        ├─ extractor returns ContentNode[] → plugin serializes to HTML
        ▼
   Yoast engine (in browser): Paper + EnglishResearcher + SeoAssessor
        │
        ▼
   Tabs: Keyphrase · On-page SEO · Readability · Inclusive · Content vitals · SERP preview

随着形式值的变化,分析运行在~1秒分解上. 没有写入文档数据 ——抽屉是编辑器当前状态之上的只读覆接. 键词保存在浏览器或服务器上 keyphraseIndex。 。 。 .


安装

pnpm add @focus-reactive/payload-plugin-seo

同伴依赖性 : payload ^3.0.0, (中文(简体) ), @payloadcms/next ^3.0.0, (中文(简体) ), @payloadcms/ui ^3.0.0, (中文(简体) ), @payloadcms/richtext-lexical ^3.0.0, (中文(简体) ), lucide-react ^0.469.0。 。 。 . next ^14 || ^15 财务报告和审定财务报表 react编号 :react-dom ^18 || ^19 是可选的同龄人.

Yoast引擎(yoastseo, (中文(简体) ), @yoast/search-metadata-previews)以船舶作为直接依赖——你并不自己安装,但你确实需要转接两个UI包(参见快起步第3步).


快速启动

步骤1——注册插件

// payload.config.ts
import { buildConfig } from "payload";
import { seoPlugin } from "@focus-reactive/payload-plugin-seo";

export default buildConfig({
  plugins: [
    seoPlugin({
      collections: [
        {
          slug: "pages",
          fields: {
            seoTitle: "seoTitle",
            metaDescription: "metaDescription",
            slug: "slug",
          },
          // Required: register a matching extractor (see "Content Extraction").
          extractContentPath: "@/seo/extractPageContent#default",
        },
      ],
      site: {
        name: "My Site",
        baseUrl: process.env.NEXT_PUBLIC_SERVER_URL ?? "http://localhost:3000",
      },
      supportedLocales: ["en", "de", "fr", "es"],
    }),
  ],
});

此输入 道德操守办公室 按钮进入每个已配置收藏的文档工具栏。 按钮上的一个彩色点反映了当前的总体状态(好/警告/差).

步骤2 — 导入管理员样式

抽屉的部件从内部导入编译的CSS,但包件也装在 ./admin.css 因此,您可以在您的管理样式表中明确包含它 :

/* app/(payload)/custom.scss */
@import "@focus-reactive/payload-plugin-seo/admin.css";

步骤 3 — 转接 Yaast UI 包

@yoast/search-metadata-previews 财务报告和审定财务报表 @yoast/components 内部CSS舰 node_modules,其中Next.js(和涡轮包装)不会处理,除非它们被列入 transpilePackages数字 :

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["@yoast/search-metadata-previews", "@yoast/components"],
};

export default nextConfig;

步骤4——编写和登记内容提取器

所需——见 内容提取。 。 。 .


配置参考

插件选项

interface SeoPluginConfig {
  /** Skip injection entirely. Default: false */
  disabled?: boolean;
  /** Collections to attach the SEO drawer to. At least one is required. */
  collections: SeoCollectionConfig[];
  /** Site identity used in the SERP preview and permalink. */
  site?: SeoSiteConfig;
  /** Locale codes whose Yoast language packs may be loaded. Default: ['en'] */
  supportedLocales?: string[];
  /** Override or add UI strings per admin language (see "Admin language"). */
  translations?: Translations;
  /** Store keyphrases on the server and enable the "already used on" notice and the
   *  Internal links tab. Adds the `seo-keyphrases` collection. Default: false */
  keyphraseIndex?: boolean;
}

徐运会

interface SeoCollectionConfig {
  /** Collection slug to attach the drawer to. */
  slug: string;
  /** Dot-paths telling the plugin which fields hold the title / meta description / slug. */
  fields?: SeoFieldPaths;
  /**
   * REQUIRED. Lookup key for a registered ContentExtractor — the only content path;
   * there is no built-in walker. Set it to the same string you pass as the key in
   * registerContentExtractors(). Convention: the module path of the extractor file,
   * e.g. "@/collections/Page/extractPageContent#default". The extractor runs in the
   * browser on the raw form values and returns ContentNode[]. See "Content Extraction".
   *
   * A collection whose extractContentPath is missing/empty is dropped at plugin init
   * (with a warning); if no collection has a valid extractContentPath the plugin no-ops.
   * If the key is set but not registered at runtime, content analysis for that collection
   * is empty (a one-time console error is logged) — there is no built-in fallback.
   */
  extractContentPath: string;
  /**
   * Server-only. Public URL of a document, used by link suggestions when
   * `keyphraseIndex` is on. Return null when the document has no public URL.
   */
  resolveUrl?: (args: { doc: Record<string, unknown>; locale: string; req: PayloadRequest }) =>
    string | null | Promise<string | null>;
}

徐场派斯

interface SeoFieldPaths {
  /** Dot-path to the SEO title. Falls back to the collection's useAsTitle / `title`. */
  seoTitle?: string;
  /** Dot-path to the meta description. Absent → meta-description checks are disabled
   *  and the SERP snippet shows no description. */
  metaDescription?: string;
  /** Dot-path to the slug. Default: 'slug' */
  slug?: string;
}

点-道支持筑巢等. "meta.description" 或者说 "content.body"。正文内容为 没有 在此配置——由您注册的提取器生产(见下文).

徐锡克

interface SeoSiteConfig {
  /** Site name shown in the SERP preview. */
  name?: string;
  /** Base URL used to build the permalink in the SERP preview. */
  baseUrl?: string;
  /** Favicon shown in the SERP preview. */
  faviconUrl?: string;
}

内容提取

内容提取为 应用程序: 您注册一个 ContentExtractor 每个收藏。 该插件不假设您的文档计划、 关系、 链接类型或 URL 构造 —— 它将原始值加一个小的通用工具包交给您的提取器, 并按序列排序 ContentNode[] 你回来.

这个 ContentNode 中级代表

插件在序列化到 HTML 之前代表页面内容为平整的打出节点数组. 这是 ContentNode 联合导出自 @focus-reactive/payload-plugin-seo/content数字 :

type ContentNode =
  | { type: "heading"; level: 1 | 2 | 3 | 4 | 5 | 6; text: string }
  | { type: "paragraph"; text: string }
  | { type: "link"; href: string; text: string }
  | { type: "image"; src: string; alt?: string }
  | { type: "video"; src: string; poster?: string }
  | { type: "html"; html: string }; // lexical-converted or raw HTML escape hatch

串行到HTML(用于Yoast引擎)完全发生在插件内部. 提取器生产 ContentNode[];它们从未直接构造 HTML 字符串.

构建器助手

这个 /content 子路径导出纯建构函数 。 每个助手返回 null 用于空或缺失输入,以及 compact 将空出——这样你就可以建立稀有的阵列,并在一个通道中清理出. 同样的助手也交给了您的提取器 toolkit.helpers,因此可以使用导入或注入对象.

import {
  heading, // heading(level: 1|2|3|4|5|6, text?: string | null): ContentNode | null
  paragraph, // paragraph(text?: string | null): ContentNode | null
  link, // link(href?: string | null, text?: string | null): ContentNode | null
  image, // image(src?: string | null, alt?: string | null): ContentNode | null
  video, // video(src?: string | null, poster?: string | null): ContentNode | null
  richText, // richText(lexicalValue: unknown): ContentNode | null  (lexical → HTML; null when empty)
  html, // html(raw?: string | null): ContentNode | null
  compact, // compact(nodes: (ContentNode | null | undefined)[]): ContentNode[]
} from "@focus-reactive/payload-plugin-seo/content";
import type {
  ContentNode,
  HeadingLevel,
} from "@focus-reactive/payload-plugin-seo/content";

提取器合同

type ContentExtractor = (
  values: Record<string, unknown>, // RAW form values; relationship/upload fields are ids
  ctx: ExtractContext, // { locale?: string; apiRoute?: string }
  toolkit: ExtractToolkit, // { resolveDocs, helpers }
) => ContentNode[] | Promise<ContentNode[]>;

interface ExtractToolkit {
  resolveDocs: (queries: DocQuery[]) => Promise<DocStore>;
  helpers: ContentHelpers; // heading, paragraph, link, image, video, richText, html, compact
}

interface DocQuery {
  collection: string;
  ids: (string | number)[];
  select?: string[]; // field projection → ?select[field]=true
  depth?: number; // relationship population → ?depth=N (default 0)
}

interface DocStore {
  get(collection: string, id: string | number): Record<string, unknown> | undefined;
}

你的提取器:

  • 接收 原始内容,未保存的形式值。 关系和上传字段 编号 (或 id 数组 / { relationTo, value }, (中文(简体) ), 没有 被覆盖的对象——插件不进行水合.
  • 存档副本和任何链接/URL大楼。 该插件对您的链接类型( 内部引用, 自定义 URL等) 不作假设 —— 您决定要获取什么以及如何将其变成节点 .
  • 用途 toolkit.resolveDocs(queries) 获取引用/上载文档。 您每集通过一个查询 ids 您所收集的和可选的 select 投影(只比较需要的字段)和 depth。 。 。 . 所有查询都是平行进行的. 读取结果 store.get(collection, id)。 。 。 .
  • 回返 ContentNode[] (由帮助者所建);插件序列化.
// src/collections/Page/extractPageContent.ts
import { heading, image, paragraph, richText } from "@focus-reactive/payload-plugin-seo/content";
import type {
  ContentExtractor,
  DocStore,
} from "@focus-reactive/payload-plugin-seo/content";

const extractPageContent: ContentExtractor = async (values, _ctx, { resolveDocs, helpers }) => {
  const blocks = (values as { blocks?: Record<string, unknown>[] }).blocks ?? [];

  // 1. Collect the ids you care about from the RAW values (you know your schema).
  const mediaIds = blocks.flatMap((b) => (typeof b.image === "number" ? [b.image] : []));

  // 2. Fetch them — one parallel request per collection, projected to only the fields you need.
  const docs: DocStore = await resolveDocs([
    { collection: "media", ids: mediaIds, select: ["url", "alt", "mimeType"] },
  ]);

  // 3. Build the Intermediate Representation.
  return helpers.compact(
    blocks.flatMap((b) => {
      const media = typeof b.image === "number" ? docs.get("media", b.image) : undefined;
      return [
        heading(2, b.title as string),
        paragraph(b.subtitle as string),
        image((media as { url?: string })?.url, (media as { alt?: string })?.alt),
        richText(b.content),
      ];
    }),
  );
};

export default extractPageContent;

登记册:为何存在以及如何使用登记册

3.84 有效载荷没有客户端导入地图—— admin.dependencies 仅解决服务器侧函数的路径字符串,一个已解决的函数不能作为道具跨越服务器 客户端边界. 因为Yoast分析在浏览器中运行, extractContentPath 无法在客户端上用有效载荷机械解决.

插件以一个 globalThis- 支持注册. extractContentPath 在配置中是 查找密钥; 耗用应用程序在已挂载的客户端模块中在同一密钥下登记实际功能. 登记处使用 globalThis (不是空模块级) Map),所以键和函数在相隔的捆绑块中解决同一实例.

步骤1——设定 extractContentPath 在插件配置( 查找键) 中 :

// payload.config.ts (or your plugins file)
import { seoPlugin } from "@focus-reactive/payload-plugin-seo";

seoPlugin({
  collections: [
    {
      slug: "page",
      fields: {
        seoTitle: "meta.title",
        metaDescription: "meta.description",
        slug: "slug",
      },
      extractContentPath: "@/collections/Page/extractPageContent#default",
    },
  ],
});

步骤2——建立客户登记部门:

// src/providers/SeoExtractorRegistrar.tsx
"use client";

import { registerContentExtractors } from "@focus-reactive/payload-plugin-seo/content";
import type { ReactNode } from "react";

import extractPageContent from "@/collections/Page/extractPageContent";

// registerContentExtractors runs once when this module loads in the admin bundle.
// The key must exactly match the extractContentPath string in your plugin config.
registerContentExtractors({
  "@/collections/Page/extractPageContent#default": extractPageContent,
});

export function SeoExtractorRegistrar({ children }: { children: ReactNode }) {
  return <>{children}</>;
}

export default SeoExtractorRegistrar;

步骤3——登记员上山担任管理员:

// payload.config.ts
export default buildConfig({
  admin: {
    components: {
      providers: ["/providers/SeoExtractorRegistrar"],
    },
  },
  // ...
});

如果配置 extractContentPath 设置,但该函数没有注册(例如,提供者缺失),插件记录了一次性控制台错误,该集合的内容分析是空的——没有内置倒计时.

helpers.richText(value) 将词典树序列化为 HTML 作为。内部链接节点和上传嵌入于丰富文本的节点 内容 为: 没有 由插件解决 href编号 srcs是左侧的 依词典树提供。 这使插件完全具有预设性。 如果需要解开的,请在提取器内自行走出词典树(其结构是标准的有效载荷词典),收集引用的ID,并用 resolveDocs,并在构建IR前重写节点.


生成的 SEO 字段

seoTextField 是一个可选的功能, 直接在内部添加 AI 辅助生成 a 有效载荷文本字段。 从这里导入 @focus-reactive/payload-plugin-seo/fields 财务报告和审定财务报表 放在收藏品的任何地方 fields 数组——它返回一个标准的有效载荷 text 字段,其中显示长度表、状态药丸和 (可选)a 生成 按钮.

使用量

// payload.config.ts (or a separate collection file)
import { seoPlugin } from "@focus-reactive/payload-plugin-seo";
import { seoTextField } from "@focus-reactive/payload-plugin-seo/fields";

const MyCollection = {
  slug: "pages",
  fields: [
    {
      name: "meta",
      type: "group",
      fields: [
        seoTextField({
          name: "title",
          kind: "title",
          label: "Meta Title",
          localized: true,
          showButton: true,         // Mode 2: on-demand Generate button
          generateOnPublish: true,  // Mode 1: auto-fill on publish when empty
        }),
        seoTextField({
          name: "description",
          kind: "description",
          label: "Meta Description",
          localized: true,
          showButton: true,
          generateOnPublish: true,
        }),
      ],
    },
  ],
};

两种模式可以同时活跃在同一领域.

seoTextField 选项

interface SeoTextFieldOptions {
  /** Field name — passed straight to the underlying Payload text field. */
  name: string;
  /**
   * "title" or "description".
   * Drives the generation prompt, the default target range, the measurement unit, and the
   * rendered input/field type:
   *   title       → single-line `text` field; pixel-width measurement (Yoast heuristic)
   *   description → multi-line `textarea` field; character-count measurement
   */
  kind: "title" | "description";
  /** Human-readable label shown in the admin UI. */
  label?: string;
  /** Mark the field as required. */
  required?: boolean;
  /** Enable per-locale storage. */
  localized?: boolean;
  /** Standard Payload field admin overrides. */
  admin?: Record<string, unknown>;
  /**
   * Mode 2 — render a black-shade Generate button in the field's label row.
   * Clicking it calls the generation endpoint using the current (unsaved) page content.
   * Works on new/unsaved documents.
   * Default: false.
   */
  showButton?: boolean;
  /**
   * Mode 1 — on publish, auto-fill the field from page content ONLY when it is empty.
   * A manual value is never overwritten.
   * Shows a small "generated on publish" tooltip icon next to the label.
   * Default: false.
   */
  generateOnPublish?: boolean;
  /**
   * Override the target length window.
   * Defaults: title → 400–600 px; description → 120–156 chars.
   */
  range?: { min?: number; max?: number };
}

字段与本地有效载荷文本字段相同,下面增加两个 输入:长度表(标题的像素,描述的字符)和状态 药丸显示 过短, (中文(简体) ), 不错,或 太长了 对已配置的范围.

生成端点和 OpenAI 配置

至少当一个 seoTextField 已经 showButton 或者说 generateOnPublish 启用,则 插件注册服务器端点 POST {apiRoute}/seo/generate。这个终点 读取 OPENAI_API_KEY 从服务器环境调用 OpenAI 生成 字段值.

通过插件配置生成 generation 选项 :

seoPlugin({
  collections: [/* ... */],
  generation: {
    /** OpenAI model. Default: "gpt-4o-mini". */
    model: "gpt-4o-mini",
    /**
     * API key. Optional — falls back to process.env.OPENAI_API_KEY automatically.
     * Useful when the key lives in a non-standard env variable or is resolved at
     * config time.
     */
    apiKey: process.env.OPENAI_API_KEY,
    /**
     * Maximum characters of page content sent to the model.
     * Default: 6000.
     */
    maxContentChars: 6000,
    /** Override the system prompt used for title generation. */
    titlePrompt: "Write an SEO meta title…",
    /** Override the system prompt used for description generation. */
    descriptionPrompt: "Write an SEO meta description…",
  },
})

如果没有 API 密钥解析( 没有) generation.apiKey 也没有 OPENAI_API_KEY 已设定, 生成为 优雅的残疾: 生成按钮隐藏在 UI 中 被推倒的钩子是禁用-没有出错.

生成是 locate- aware: 正在运行的文档 locate 被传送到端点 并包含在提示.

生成提取器

模式 2 (按钮) 重复使用 extractContentPath 已配置的提取器 分析抽屉。 不需要额外的注册——按钮调用相同 客户端提取器.

模式 1 (在打印中) 运行服务器侧并需要一个 serverExtractContent 函数 在收藏的插件配置上。 也一样 ContentExtractor 合约,但 接收本地API支持的 resolveDocs 并保存文档 :

// src/collections/Page/extractPageContent.server.ts
import type { ContentExtractor } from "@focus-reactive/payload-plugin-seo/content";

export const serverExtractPageContent: ContentExtractor = async (
  values,
  _ctx,
  { resolveDocs, helpers },
) => {
  // values = the document being saved (same shape as the client-side extractor)
  // resolveDocs = Local API-backed; works the same way as the client version
  return helpers.compact([
    helpers.heading(1, values.title as string),
    // … rest of your extraction logic
  ]);
};
// payload.config.ts
import { serverExtractPageContent } from "@/collections/Page/extractPageContent.server";

seoPlugin({
  collections: [
    {
      slug: "page",
      fields: { seoTitle: "meta.title", metaDescription: "meta.description", slug: "slug" },
      extractContentPath: "@/collections/Page/extractPageContent#default", // client (button + drawer)
      serverExtractContent: serverExtractPageContent,                       // server (on-publish)
    },
  ],
  generation: { /* ... */ },
});

因为提取器只读出ID 并调用注射器 resolveDocs,同样的 函数正体通常可以同时为客户端和服务器服务而无需修改——只是 导出两次(或从共享文件再导出).

说明: 插件自动注册 SeoClientConfigProvider 管理员 内部。 你不需要加进去 admin.components.providers 手动.


分析绘图器

抽屉内有6个标签,均取自单一的内浏览器Yoast分析口(a) Paper 分析者: SeoAssessor 与语言相适应 Researcher:

标签它检查什么
密钥句工作空间: a 焦点密钥句 ——标题用法, swit, meta 描述, 第1段,密度,图像代词,同义词——加到 四个相关的密钥词 (共5个),各有同义词和独立的"瑜伽"检查. 只有焦点密钥句的结果为总体分数提供素材;相關密钥句被分出并并列以作参考而永不影响. 输入解锁这些检查的密钥口.
页面上标题宽度,元描述存在/长度,内出链接,标题结构.
可读性句子/段落长度,过渡词,被动语音,接连句.
包容性旗帜可能排斥或不包容的语言.
内容要点文字计数,句子/段落计数,图像和视频计数,读取时间,突出词.
搜索结果预览Live Google SERP 预览( 桌面+ 移动) 有密钥句突出显示, 以 @yoast/search-metadata-previews。 。 。 .
内部链接其它文件,其密钥短语见此文本,但尚未链接。 仅与 keyphraseIndex。 。 。 .

无密钥句 : 抽屉仍然运行 页面、 可读性、 兼容性、 内容要点、 以及 SRP 标签。 只有关键词特定评估才键入焦点关键词和分析运行.

密钥句标签

这个 密钥句 选项卡为主- 细节工作空间: 一条铁路列出所有已配置的密钥句( 焦点密钥句, 然后是任何相关的密钥句) , 并选择一个条目显示其细节面板 — 密钥句文本、 其同义词和它自己的 Yoast 结果 .

  • 焦点密钥句 - 第一个条目。 它运行全 SeoAssessor (上面列出的同一张支票),它的结果是唯一能为抽屉的总分徽章提供素材的.
  • 相关的密钥句 ——最多再有4个条目(共5个,一个没有配置选项的固定封顶). 每个都经营Yoast的 RelatedKeywordAssessor,为次要密钥句所构建的更小的检查集(标题、元描述、第一段、图像备选案文、密钥句密度和突出字覆盖)。 相关结果仅供参考——从未影响总分.
  • 同义词 ——每个密钥词(焦点或相關)可以有自己的同义词列表;同义词被折入该密钥词自己的分析通关中,对于一个单一的"Yoast焦点"密钥词,它们的方式是一样的.
  • 突出显示的 SERP ——搜索-结果预览只粗体显示焦点密钥词及其同义词;相关的密钥词在那里不突出显示.

持久性: 默认密钥句是 未存储于数据库 ——没有新的领域,藏品,也没有全球. 完整列表( 文本 + 每个条目的同义词) 保存在浏览器中 localStorage,每个收藏+文档+地语区按键,并按下保存上的空条目。 切换文档或地产加载(或开始)单独的列表;清除浏览器存储重置. 与 keyphraseIndex 列表保存在服务器上.


密钥词索引

设定 keyphraseIndex: true 在服务器上存储密钥句,所以每个编辑器都看到相同的集 :

seoPlugin({
  collections: [/* … */],
  keyphraseIndex: true,
});
  • 密钥句和同义词被保存到每个文档和隐藏的地方 seo-keyphrases 收缴(docCollection, (中文(简体) ), docId, (中文(简体) ), locale, (中文(简体) ), keyphrases, (中文(简体) ), intents,唯一打开 docCollection + 键 docId + 键 locale) (中文(简体) ). 每个文档最多有5个密钥词,每个同义词有10个,每个条目有100个字符. 保存时会将更长的输入切入到这些限制.
  • 随着索引的上传,密钥词不再被保存在浏览器存储中. 如果服务器从未为文档和地语存储过密钥句,那么在索引启用前保存在浏览器中的密钥句会在抽屉首次打开时被上传. 上传从不覆盖这期间所创建的另一分页或编辑器的行,浏览器副本只有在服务器接受后才被删除. 当它已经有密钥句时,抽屉会切换到它们.
  • 服务器未接受的编辑保存在浏览器中,并与下个保存一起再次发送,或者当抽屉在重新装入后下一次打开时. 键口选项卡显示一个通知, 直到保存成功 。 保存一个文档时,一次发送一个.
  • 能够读取文档但未更新的编辑器会看到其密钥句只读.
  • 如果从服务器上装入密钥句失败,抽屉会回到该文档的浏览器存储上,而没有"已经使用的"通知和内部链接标签.
  • 删除每个密钥句都会保留一行空,所以一个旧的浏览器副本永远不会在它上传.
  • 在页面被隐藏或关闭时发送一个待编辑,因此通航不丢失.
  • 删除文档时删除行.
  • 选项添加了收藏 。 在 SQL 适配器上运行( Postgres, SQLite) payload migrate:create 并应用迁移.
  • 关闭选项不会删除所存储行 。 抽屉回到了浏览器存储器,对于键入句被移入服务器的文档,浏览器开始为空.

终点和接入

插件在您的 API 路由下注册了四个终点 . locale 当本地化被启用或被忽略时,则需要本地化.

  • GET /seo/keyphrases?collection=&docId=&locale= 返回时 { keyphrases, indexed, canUpdate }时 indexed 表示服务器是否有文档的行 canUpdate 用户是否可以更改.
  • PUT /seo/keyphrases 与身体 { collection, docId, locale, keyphrases: [{ text, synonyms }], ifAbsent?: boolean } 替换密钥句并返回 { keyphrases }与 ifAbsent: true 它只创建了缺失行和答案 409 当一个密钥存在时,与存储的密钥句.
  • GET /seo/keyphrase-usage?collection=&docId=&locale=&phrase= 返回时 { docs: [{ collection, id, title }] },其他使用相同键词的文档.
  • POST /seo/link-suggestions 与身体 { collection, docId, locale, text, linkedHrefs?: string[], linkedDocs?: [{ collection, docId }] } 返回时 { suggestions }。 。 。 .

这个 seo-keyphrases 收藏本身是关闭的:每一个操作在其访问控制中都被拒绝,它被隐藏在管理员处,而不是在GraphQL上被曝光. 只有这些端点读取或写出.

  • 每个请求都需要登录用户(否则 401)来自管理员用户收藏 admin.user (否则) 403) (中文(简体) ).
  • 用户必须能够读取请求所涉及的文档, 否则 404。 。 。 .
  • PUT /seo/keyphrases 还需要更新对该文件的访问,否则 403它被评价为有效载荷的方式 GET /api/<collection>/access/<id> 结束点做,所以收藏 access.update 获取文档 id 与文档相匹配。 检查时出错为无权限 .
  • 每个返回的文档被加载到用户的读取权限,因此编辑者从没有看到标题或URL,无法自己打开.

"已经使用"的通知

键词标签显示 "这个键词已经用在:" ,并链接到其他文档的编辑视图中,这些文档在同一地方有相同的密钥。 拉丁字母上的文字顺序、大小写和口音被忽略(CMS Payload =: payload cms, (中文(简体) ), café =: cafe) (中文(简体) ). 同义词不作比较.

抽屉里有个 内部链接 选项卡. 寻找机会 列出最多10个其他文档,其关键词或同义词出现在本文档的文本中,且尚未从中链接,其中包含匹配的短语,匹配次数,与匹配的文本突出显示的片段以及复制的目标链接.

每个收藏 resolveUrl 因此建议显示已链接到 URL 的公共 URL 和目标被跳过 。 没有它,建议只链接到管理员编辑视图.

import type { ResolveUrl } from "@focus-reactive/payload-plugin-seo";

const resolvePostUrl: ResolveUrl = ({ doc, locale }) =>
  typeof doc.slug === "string" ? `/${locale}/blog/${doc.slug}` : null;

seoPlugin({
  keyphraseIndex: true,
  collections: [
    {
      slug: "posts",
      extractContentPath: "@/collections/Posts/extractPostContent#default",
      resolveUrl: resolvePostUrl,
    },
  ],
});

resolveUrl 运行在服务器上的目标文档( 深度 0) 。 相对和绝对的URL都有效. 所抛出的错误会被记录并当作 null。 。 。 .

匹配规则 :

  • 全词只: web 不匹配 websites。 。 。 .
  • 拉丁字母上的大小写和口音被忽略: café 匹配 cafe。 。 。 . ñ西里尔字母 й 和日语的kana语,例如: が 保持鲜明的字母.
  • 连字符计为空格 : e-commerce 匹配 e commerce。 。 。 .
  • 脚本中文字在文字之间没有空格(中文,日文,泰文)被拆分为: Intl.Segmenter 运行时间提供它.
  • 只有在同一地方有密钥句的文档才能被推荐. 在只允许使用草稿的汇编中,建议只出版文件.
  • 当文本已链接到时, 目标会被跳过, 要么作为丰富的文本内部链接, 要么通过 href 他们的道路等于 resolveUrl 结果.
  • 丰富的文本内部链接来自 Lexical LinkFeature 形状( e)linkType: "internal", (中文(简体) ), doc: { relationTo, value })在表单数据中的任何位置. 其他链接字段,如自定义链接组或关系字段,仅通过 href 他们让内容.
  • 一个 href 计算时,当它是相对的,或者当它的宿主等于 serverURL,该插件的 site.baseUrl要求或绝对要求 resolveUrl 结果. http 财务报告和审定财务报表 https 和领先者 www. 作为同一地点处理。 与其他站点的链接被忽略,即使它们的路径匹配.
  • 路径进行字面比较: /en/alpha 财务报告和审定财务报表 /alpha 不一样 如果您的站点同时为默认的地址服务, 并同时设置前缀, 请返回 resolveUrl 您使用的内容链接窗体.
  • 每个请求一次读取地块500的索引行(docCollection, (中文(简体) ), docId, (中文(简体) ), keyphrases 并把它们匹配到记忆中。 匹配目标随后被装入不断增长的批次中,直到找到10个建议为止,每次请求最多300个目标.
  • 限制: 发送用于匹配的文本被封为 100,000个字符, 最多500个 hrefs每个最多有2 048个字符,最多500个丰富的文本内部链接被比较.

本地化

supportedLocales 列出抽屉中可以装入 Yoast 语言包的地盘代码。 English是用英语构建的;文件第一次被编辑时,其他语言是按要求动态导入的:

seoPlugin({
  collections: [
    {
      slug: "pages",
      fields: { slug: "slug" },
      extractContentPath: "@/seo/extractPageContent#default",
    },
  ],
  supportedLocales: ["en", "de", "fr", "es"],
});

活动区域从管理员那里取出 并规范到Yoast xx_XX 形式(例如:. en → en_EN) (中文(简体) ). 语言被传递到您的提取器 ctx.locale 转至 resolveDocs (因此,预测的获取是正确的地方). 未列出在 supportedLocales 回到英语处理.

行政语言

抽屉和生成的字段会说管理员的语言(Payload's i18n,用户的语言开关),独立于上方的内容locale. 带有插件的英语和捷克船;任何其他管理语言都以字符串回落为英语字符串.

有效载荷只提供您配置的管理员语言, 所以将捷克语添加到 i18n.supportedLanguages 要获得捷克UI:

import { cs } from "@payloadcms/translations/languages/cs";
import { en } from "@payloadcms/translations/languages/en";

export default buildConfig({
  i18n: { supportedLanguages: { en, cs } },
  // ...
});

字符串注册为 有效载荷自定义翻译 seo 命名空间,因此它们可以被覆盖,或者添加一个完整的语言 translations键是其中的 src/translations/messages/en.ts数字 :

seoPlugin({
  // ...
  translations: {
    de: {
      title: "SEO-Analyse",
      "tab.keyphrase": "Fokus-Keyphrase",
      "viz.show.link_one": "{{count}} Link anzeigen",
      "viz.show.link_other": "{{count}} Links anzeigen",
    },
  },
});

{{name}} 占位符被插入。 多键是单独的后缀 _zero, (中文(简体) ), _one, (中文(简体) ), _two, (中文(简体) ), _few, (中文(简体) ), _many 或者说 _other选择 Intl.PluralRules 行政语言; _other 需要,其余的只有在语言有该形式的情况下(捷克语使用 _one, (中文(简体) ), _few 财务报告和审定财务报表 _other) (中文(简体) ). 密钥会对照英文集进行打字检查( Translations 类型为导出)。 这个 seo: 前缀和先前版本的分号标签键("seo:tab:keyphrase")仍然被接受。 此选项的字符串优先于 i18n.translations.<language>.seo 在您的有效载荷配置中.


参考文献

导入路径出口
@focus-reactive/payload-plugin-seoseoPlugin, (中文(简体) ), seoTextField; 类型 SeoPluginConfig, (中文(简体) ), SeoCollectionConfig, (中文(简体) ), SeoFieldPaths, (中文(简体) ), SeoSiteConfig, (中文(简体) ), ContentExtractor, (中文(简体) ), SeoGenerationConfig, (中文(简体) ), ResolveUrl, (中文(简体) ), ResolveUrlArgs, (中文(简体) ), Translations, (中文(简体) ), MessageKey
@focus-reactive/payload-plugin-seo/content构建器助手 heading, (中文(简体) ), paragraph, (中文(简体) ), link, (中文(简体) ), image, (中文(简体) ), video, (中文(简体) ), richText, (中文(简体) ), html, (中文(简体) ), compact· ; registerContentExtractors, (中文(简体) ), resolveContentExtractor; 类型 ContentNode, (中文(简体) ), HeadingLevel, (中文(简体) ), ContentExtractor, (中文(简体) ), ExtractContext, (中文(简体) ), ExtractToolkit, (中文(简体) ), DocQuery, (中文(简体) ), DocStore, (中文(简体) ), ContentHelpers
@focus-reactive/payload-plugin-seo/fieldsseoTextField; 类型 SeoTextFieldOptions
@focus-reactive/payload-plugin-seo/admin.css抽屉按钮的编译管理样式( B)
@focus-reactive/payload-plugin-seo/components/SeoButtonSeoButton ——工具栏按钮组件(由插件通过导入Map自动接通;通常从不直接导入)
@focus-reactive/payload-plugin-seo/components/SeoFieldSeoField 增强文本字段组件(长米+状态丸+生成按钮);自动接线通过 seoTextField;通常从不直接导入
@focus-reactive/payload-plugin-seo/providers/SeoClientConfigProviderSeoClientConfigProvider ——将客户端插件配置(生成设置,收藏地图)带入管理员捆绑;由插件自动注册——不添加到 admin.components.providers 手动

许可证

MIT 焦点反应

版本与发布记录

@focus-reactive/payload-plugin-seo 共发布 18 个版本,首次发布于 2026/06/16;过去 12 个月发布了 18 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.12.0latest2026/11/123 天前
1.11.02026/11/123 天前
1.10.22026/10/2323 天前
1.10.12026/07/054 个月前
1.10.02026/07/044 个月前
1.9.02026/07/024 个月前

关于 @focus-reactive/payload-plugin-seo 的常见问题

@focus-reactive/payload-plugin-seo 是什么?

现场SIO分析 有效载荷CMS v3 + Next.js, 由 烟。在文档编辑器上添加一个实时的 SEO 抽屉 — 键入优化、页面检查、可读性、包容性朗

如何安装 @focus-reactive/payload-plugin-seo?

在项目目录运行 npm install @focus-reactive/payload-plugin-seo,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:payload(^3.0.0)、lucide-react(^0.469.0)、@payloadcms/ui(^3.0.0)、@payloadcms/next(^3.0.0)、@payloadcms/richtext-lexical(^3.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@focus-reactive/payload-plugin-seo 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@focus-reactive/payload-plugin-seo 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@focus-reactive/payload-plugin-seo 可以商用吗?

@focus-reactive/payload-plugin-seo 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@focus-reactive/payload-plugin-seo 的下载量有多少?

根据 npm 官方下载统计,@focus-reactive/payload-plugin-seo 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,818 次,日均约 94 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@focus-reactive/payload-plugin-seo 还在维护吗?

最新版本 1.12.0 发布于 2026/11/12(3 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/06/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。