@focus-reactive/payload-plugin-seo 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -124
- package/dist/components/SeoButton/SeoButtonInner.d.ts +2 -4
- package/dist/components/SeoButton/SeoButtonInner.d.ts.map +1 -1
- package/dist/components/SeoButton/SeoButtonInner.js +10 -7
- package/dist/components/SeoButton/SeoButtonInner.js.map +1 -1
- package/dist/components/SeoDrawer/build-analysis-input.d.ts +1 -8
- package/dist/components/SeoDrawer/build-analysis-input.d.ts.map +1 -1
- package/dist/components/SeoDrawer/build-analysis-input.js +22 -34
- package/dist/components/SeoDrawer/build-analysis-input.js.map +1 -1
- package/dist/components/SeoDrawer/buildInput.js +1 -1
- package/dist/components/SeoDrawer/buildInput.js.map +1 -1
- package/dist/components/SeoDrawer/useLiveDocument.d.ts +2 -5
- package/dist/components/SeoDrawer/useLiveDocument.d.ts.map +1 -1
- package/dist/components/SeoDrawer/useLiveDocument.js +14 -62
- package/dist/components/SeoDrawer/useLiveDocument.js.map +1 -1
- package/dist/content/index.d.ts +2 -2
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +2 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/resolve/resolve-docs.d.ts +3 -0
- package/dist/content/resolve/resolve-docs.d.ts.map +1 -0
- package/dist/content/resolve/resolve-docs.js +48 -0
- package/dist/content/resolve/resolve-docs.js.map +1 -0
- package/dist/content/schema/helpers.d.ts +1 -0
- package/dist/content/schema/helpers.d.ts.map +1 -1
- package/dist/content/schema/helpers.js +4 -0
- package/dist/content/schema/helpers.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +18 -3
- package/dist/plugin.js.map +1 -1
- package/dist/types/config.d.ts +35 -25
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/config/overrideAdmin.d.ts.map +1 -1
- package/dist/utils/config/overrideAdmin.js +2 -10
- package/dist/utils/config/overrideAdmin.js.map +1 -1
- package/package.json +1 -1
- package/dist/content/extract/context.d.ts +0 -11
- package/dist/content/extract/context.d.ts.map +0 -1
- package/dist/content/extract/context.js +0 -1
- package/dist/content/extract/context.js.map +0 -1
- package/dist/content/extract/extract.d.ts +0 -18
- package/dist/content/extract/extract.d.ts.map +0 -1
- package/dist/content/extract/extract.js +0 -211
- package/dist/content/extract/extract.js.map +0 -1
- package/dist/content/extract/selection.d.ts +0 -3
- package/dist/content/extract/selection.d.ts.map +0 -1
- package/dist/content/extract/selection.js +0 -32
- package/dist/content/extract/selection.js.map +0 -1
- package/dist/content/lexical/transform.d.ts +0 -5
- package/dist/content/lexical/transform.d.ts.map +0 -1
- package/dist/content/lexical/transform.js +0 -56
- package/dist/content/lexical/transform.js.map +0 -1
- package/dist/content/resolve/collect-refs.d.ts +0 -8
- package/dist/content/resolve/collect-refs.d.ts.map +0 -1
- package/dist/content/resolve/collect-refs.js +0 -79
- package/dist/content/resolve/collect-refs.js.map +0 -1
- package/dist/content/resolve/hydrate.d.ts +0 -7
- package/dist/content/resolve/hydrate.d.ts.map +0 -1
- package/dist/content/resolve/hydrate.js +0 -123
- package/dist/content/resolve/hydrate.js.map +0 -1
- package/dist/content/resolve/resolver.d.ts +0 -7
- package/dist/content/resolve/resolver.d.ts.map +0 -1
- package/dist/content/resolve/resolver.js +0 -65
- package/dist/content/resolve/resolver.js.map +0 -1
- package/dist/content/resolve/types.d.ts +0 -12
- package/dist/content/resolve/types.d.ts.map +0 -1
- package/dist/content/resolve/types.js +0 -7
- package/dist/content/resolve/types.js.map +0 -1
- package/dist/content/walk/walkFields.d.ts +0 -17
- package/dist/content/walk/walkFields.d.ts.map +0 -1
- package/dist/content/walk/walkFields.js +0 -88
- package/dist/content/walk/walkFields.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Live SEO analysis for [Payload CMS](https://payloadcms.com/) v3 + Next.js, powered by [Yoast](https://github.com/Yoast/wordpress-seo). Adds a real-time SEO drawer to the document editor — keyphrase optimization, on-page checks, readability, inclusive language, content vitals, and a Google SERP preview — without adding a single field to your database.
|
|
4
4
|
|
|
5
|
-
The plugin injects a button into the editor toolbar of each configured collection. Clicking it opens a drawer that reads the current (unsaved) form values,
|
|
5
|
+
The plugin injects a button into the editor toolbar of each configured collection. Clicking it opens a drawer that reads the current (unsaved) form values, derives the title, meta description, and slug from dot-path config, runs **your** registered content extractor to build the body content, and runs the Yoast analysis engine **entirely in the browser**. Nothing is persisted — there are zero new collections, globals, or fields.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -19,8 +19,11 @@ The plugin adds NO database fields, collections, or globals. It injects a button
|
|
|
19
19
|
document editor toolbar (admin.components.edit.beforeDocumentControls) of each configured
|
|
20
20
|
collection. The button opens a drawer that:
|
|
21
21
|
- Reads the live (unsaved) form values for the document
|
|
22
|
-
-
|
|
23
|
-
-
|
|
22
|
+
- Derives title, meta description, and slug from dot-path config
|
|
23
|
+
- Runs YOUR registered content extractor to build the body content. Content extraction is
|
|
24
|
+
app-owned — there is no built-in walker. The extractor receives the raw form values plus a
|
|
25
|
+
toolkit ({ resolveDocs, helpers }); it fetches any referenced/upload docs it needs via
|
|
26
|
+
resolveDocs and returns a ContentNode[] the plugin serializes to HTML.
|
|
24
27
|
- Runs the Yoast engine (yoastseo + @yoast/search-metadata-previews) in the browser
|
|
25
28
|
- Shows tabs: Keyphrase, On-page SEO, Readability, Inclusive, Content vitals, SERP preview
|
|
26
29
|
|
|
@@ -43,8 +46,9 @@ seoPlugin({
|
|
|
43
46
|
seoTitle: 'seoTitle', // dot-path; falls back to useAsTitle / 'title'
|
|
44
47
|
metaDescription: 'metaDescription',
|
|
45
48
|
slug: 'slug', // default: 'slug'
|
|
46
|
-
content: 'sections', // dot-path to the main content field (blocks/richText/textarea)
|
|
47
49
|
},
|
|
50
|
+
// REQUIRED: lookup key for a content extractor you register (see Step 4).
|
|
51
|
+
extractContentPath: '@/seo/extractPageContent#default',
|
|
48
52
|
},
|
|
49
53
|
],
|
|
50
54
|
site: { name: 'My Site', baseUrl: 'https://example.com', faviconUrl: '/favicon.ico' },
|
|
@@ -65,18 +69,39 @@ const nextConfig = {
|
|
|
65
69
|
transpilePackages: ['@yoast/search-metadata-previews', '@yoast/components'],
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
## Step 4 — Write and register a content extractor (REQUIRED)
|
|
73
|
+
|
|
74
|
+
Content extraction is entirely yours — there is no built-in walker, and extractContentPath is
|
|
75
|
+
required. Write an extractor and register it under the same key, from an admin-mounted client
|
|
76
|
+
module:
|
|
77
|
+
|
|
78
|
+
// src/seo/extractPageContent.ts
|
|
79
|
+
import type { ContentExtractor } from '@focus-reactive/payload-plugin-seo/content'
|
|
80
|
+
const extractPageContent: ContentExtractor = async (values, ctx, { resolveDocs, helpers }) => {
|
|
81
|
+
// 1. collect ids from the RAW values (relationship/upload fields are ids)
|
|
82
|
+
// 2. const docs = await resolveDocs([{ collection: 'media', ids, select: ['url','alt'] }])
|
|
83
|
+
// 3. build the IR with helpers
|
|
84
|
+
return helpers.compact([helpers.heading(1, values.title as string) /* … */])
|
|
85
|
+
}
|
|
86
|
+
export default extractPageContent
|
|
87
|
+
|
|
88
|
+
// src/providers/SeoExtractorRegistrar.tsx ("use client")
|
|
89
|
+
import { registerContentExtractors } from '@focus-reactive/payload-plugin-seo/content'
|
|
90
|
+
import extractPageContent from '@/seo/extractPageContent'
|
|
91
|
+
registerContentExtractors({ '@/seo/extractPageContent#default': extractPageContent })
|
|
92
|
+
// export a component that renders {children} and mount it via admin.components.providers
|
|
93
|
+
|
|
68
94
|
## Important notes
|
|
69
95
|
|
|
70
96
|
- The plugin reads UNSAVED form values, so analysis updates live as you type (debounced ~1s).
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
and returns `ContentNode[]` — a structured Intermediate Representation the plugin serializes to HTML internally.
|
|
97
|
+
- Content extraction is done by YOUR registered extractor; extractContentPath is required and
|
|
98
|
+
there is no built-in fallback. The extractor receives raw form values, a ctx
|
|
99
|
+
({ locale, apiRoute }), and a toolkit ({ resolveDocs, helpers }), and returns ContentNode[].
|
|
100
|
+
- relationship/upload fields arrive as ids; use toolkit.resolveDocs(queries) to fetch only the
|
|
101
|
+
docs/fields you need (one parallel request per collection), then read them with store.get().
|
|
77
102
|
- Non-English analysis requires the locale code in `supportedLocales`; the matching Yoast
|
|
78
103
|
language pack is dynamically imported on demand.
|
|
79
|
-
- No GA4, no API keys, no server calls except
|
|
104
|
+
- No GA4, no API keys, no server calls except the resolveDocs reads against your own Payload API.
|
|
80
105
|
```
|
|
81
106
|
|
|
82
107
|
---
|
|
@@ -92,9 +117,10 @@ Document editor (configured collection)
|
|
|
92
117
|
▼
|
|
93
118
|
SEO Drawer (client-only)
|
|
94
119
|
│
|
|
95
|
-
├─ read live form values (title, description, slug,
|
|
96
|
-
├─
|
|
97
|
-
|
|
120
|
+
├─ read live form values (title, description, slug, keyphrase)
|
|
121
|
+
├─ run your registered extractor(values, ctx, toolkit)
|
|
122
|
+
│ └─ toolkit.resolveDocs(): parallel, projected /api/{collection} fetches
|
|
123
|
+
├─ extractor returns ContentNode[] → plugin serializes to HTML
|
|
98
124
|
▼
|
|
99
125
|
Yoast engine (in browser): Paper + EnglishResearcher + SeoAssessor
|
|
100
126
|
│
|
|
@@ -137,8 +163,9 @@ export default buildConfig({
|
|
|
137
163
|
seoTitle: "seoTitle",
|
|
138
164
|
metaDescription: "metaDescription",
|
|
139
165
|
slug: "slug",
|
|
140
|
-
content: "sections",
|
|
141
166
|
},
|
|
167
|
+
// Required: register a matching extractor (see "Content Extraction").
|
|
168
|
+
extractContentPath: "@/seo/extractPageContent#default",
|
|
142
169
|
},
|
|
143
170
|
],
|
|
144
171
|
site: {
|
|
@@ -176,6 +203,10 @@ const nextConfig = {
|
|
|
176
203
|
export default nextConfig;
|
|
177
204
|
```
|
|
178
205
|
|
|
206
|
+
### Step 4 — Write and register a content extractor
|
|
207
|
+
|
|
208
|
+
Required — see [Content Extraction](#content-extraction).
|
|
209
|
+
|
|
179
210
|
---
|
|
180
211
|
|
|
181
212
|
## Configuration Reference
|
|
@@ -203,30 +234,27 @@ interface SeoPluginConfig {
|
|
|
203
234
|
interface SeoCollectionConfig {
|
|
204
235
|
/** Collection slug to attach the drawer to. */
|
|
205
236
|
slug: string;
|
|
206
|
-
/** Dot-paths telling the plugin which fields hold the
|
|
237
|
+
/** Dot-paths telling the plugin which fields hold the title / meta description / slug. */
|
|
207
238
|
fields?: SeoFieldPaths;
|
|
208
239
|
/**
|
|
209
|
-
* Lookup key for a registered ContentExtractor
|
|
210
|
-
* Set
|
|
211
|
-
* Convention:
|
|
212
|
-
* The extractor runs in the
|
|
213
|
-
*
|
|
214
|
-
*
|
|
240
|
+
* REQUIRED. Lookup key for a registered ContentExtractor — the only content path;
|
|
241
|
+
* there is no built-in walker. Set it to the same string you pass as the key in
|
|
242
|
+
* registerContentExtractors(). Convention: the module path of the extractor file,
|
|
243
|
+
* e.g. "@/collections/Page/extractPageContent#default". The extractor runs in the
|
|
244
|
+
* browser on the raw form values and returns ContentNode[]. See "Content Extraction".
|
|
245
|
+
*
|
|
246
|
+
* A collection whose extractContentPath is missing/empty is dropped at plugin init
|
|
247
|
+
* (with a warning); if no collection has a valid extractContentPath the plugin no-ops.
|
|
248
|
+
* If the key is set but not registered at runtime, content analysis for that collection
|
|
249
|
+
* is empty (a one-time console error is logged) — there is no built-in fallback.
|
|
215
250
|
*/
|
|
216
|
-
extractContentPath
|
|
251
|
+
extractContentPath: string;
|
|
217
252
|
}
|
|
218
253
|
```
|
|
219
254
|
|
|
220
255
|
### SeoFieldPaths
|
|
221
256
|
|
|
222
257
|
```ts
|
|
223
|
-
interface ContentSelection {
|
|
224
|
-
/** Dot-paths to walk, in order. Omitted or empty = whole document root. */
|
|
225
|
-
include?: string[];
|
|
226
|
-
/** Dot-paths to skip (merged with auto-excluded seoTitle/metaDescription/slug). */
|
|
227
|
-
exclude?: string[];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
258
|
interface SeoFieldPaths {
|
|
231
259
|
/** Dot-path to the SEO title. Falls back to the collection's useAsTitle / `title`. */
|
|
232
260
|
seoTitle?: string;
|
|
@@ -235,27 +263,10 @@ interface SeoFieldPaths {
|
|
|
235
263
|
metaDescription?: string;
|
|
236
264
|
/** Dot-path to the slug. Default: 'slug' */
|
|
237
265
|
slug?: string;
|
|
238
|
-
/**
|
|
239
|
-
* Built-in content selection. A string is a single field path (back-compat).
|
|
240
|
-
* An object selects include/exclude paths over the whole document.
|
|
241
|
-
* Ignored when extractContentPath is set and registered.
|
|
242
|
-
*/
|
|
243
|
-
content?: string | ContentSelection;
|
|
244
266
|
}
|
|
245
267
|
```
|
|
246
268
|
|
|
247
|
-
Dot-paths support nesting, e.g. `"meta.description"` or `"content.body"`.
|
|
248
|
-
|
|
249
|
-
**`content` selection semantics:**
|
|
250
|
-
|
|
251
|
-
| Value | Behavior |
|
|
252
|
-
| ------------------------------------ | ---------------------------------------------------------- |
|
|
253
|
-
| `"blocks"` (string) | Walk that one field path. Back-compat — unchanged from v1. |
|
|
254
|
-
| `{ include: ["blocks", "excerpt"] }` | Walk each listed path in order, concatenated. |
|
|
255
|
-
| `{ exclude: ["meta"] }` (no include) | Walk the whole document root, skipping excluded subtrees. |
|
|
256
|
-
| `{}` / `{ include: [] }` | Walk the whole document root. |
|
|
257
|
-
|
|
258
|
-
**Automatic metadata exclusion:** the configured `seoTitle`, `metaDescription`, and `slug` paths are always excluded from body content so their text is not double-counted in the Yoast analysis.
|
|
269
|
+
Dot-paths support nesting, e.g. `"meta.description"` or `"content.body"`. Body content is **not** configured here — it is produced by your registered extractor (see below).
|
|
259
270
|
|
|
260
271
|
### SeoSiteConfig
|
|
261
272
|
|
|
@@ -272,6 +283,8 @@ interface SeoSiteConfig {
|
|
|
272
283
|
|
|
273
284
|
## Content Extraction
|
|
274
285
|
|
|
286
|
+
Content extraction is **app-owned**: you register one `ContentExtractor` per collection. The plugin makes no assumptions about your document schema, relationships, link types, or URL construction — it hands your extractor the raw values plus a small, generic toolkit, and serializes whatever `ContentNode[]` you return.
|
|
287
|
+
|
|
275
288
|
### The `ContentNode` Intermediate Representation
|
|
276
289
|
|
|
277
290
|
The plugin represents page content as a flat array of typed nodes before serializing to HTML. This is the `ContentNode` union exported from `@focus-reactive/payload-plugin-seo/content`:
|
|
@@ -286,11 +299,11 @@ type ContentNode =
|
|
|
286
299
|
| { type: "html"; html: string }; // lexical-converted or raw HTML escape hatch
|
|
287
300
|
```
|
|
288
301
|
|
|
289
|
-
Serialization to HTML (for the Yoast engine) happens entirely inside the plugin.
|
|
302
|
+
Serialization to HTML (for the Yoast engine) happens entirely inside the plugin. Extractors produce `ContentNode[]`; they never construct HTML strings directly.
|
|
290
303
|
|
|
291
304
|
### Builder helpers
|
|
292
305
|
|
|
293
|
-
The `/content` subpath exports pure builder functions. Each helper returns `null` for empty or missing input, so you can
|
|
306
|
+
The `/content` subpath exports pure builder functions. Each helper returns `null` for empty or missing input, and `compact` drops the nulls — so you can build sparse arrays and clean them in one pass. The same helpers are also handed to your extractor as `toolkit.helpers`, so you can use either the imports or the injected object.
|
|
294
307
|
|
|
295
308
|
```ts
|
|
296
309
|
import {
|
|
@@ -299,8 +312,9 @@ import {
|
|
|
299
312
|
link, // link(href?: string | null, text?: string | null): ContentNode | null
|
|
300
313
|
image, // image(src?: string | null, alt?: string | null): ContentNode | null
|
|
301
314
|
video, // video(src?: string | null, poster?: string | null): ContentNode | null
|
|
302
|
-
richText, // richText(lexicalValue: unknown): ContentNode | null (
|
|
315
|
+
richText, // richText(lexicalValue: unknown): ContentNode | null (lexical → HTML; null when empty)
|
|
303
316
|
html, // html(raw?: string | null): ContentNode | null
|
|
317
|
+
compact, // compact(nodes: (ContentNode | null | undefined)[]): ContentNode[]
|
|
304
318
|
} from "@focus-reactive/payload-plugin-seo/content";
|
|
305
319
|
import type {
|
|
306
320
|
ContentNode,
|
|
@@ -308,77 +322,73 @@ import type {
|
|
|
308
322
|
} from "@focus-reactive/payload-plugin-seo/content";
|
|
309
323
|
```
|
|
310
324
|
|
|
311
|
-
###
|
|
312
|
-
|
|
313
|
-
When `extractContentPath` is not set (or points to an unregistered key), the plugin's built-in extractor runs:
|
|
314
|
-
|
|
315
|
-
1. **Selects** the subtree(s) specified by `fields.content` — a single field path, an `include` list, or the whole document root.
|
|
316
|
-
2. **Collects upload / relationship references** by walking the form schema (arrays, blocks, groups, tabs, rows, collapsibles, and lexical richText, including inline media nodes).
|
|
317
|
-
3. **Resolves media** by calling your Payload REST API per collection:
|
|
318
|
-
`GET /api/{collection}?depth=0&locale={locale}&where[id][in][]=…` — fetching each doc's `url`, `mimeType`, and `alt`. Results are cached in-memory and invalidated when the drawer re-opens or content changes.
|
|
319
|
-
4. **Hydrates** the value tree (upload IDs → full docs) and walks it to emit `ContentNode[]`:
|
|
320
|
-
- Lexical richText → `{ type: "html", html: "…" }` via `@payloadcms/richtext-lexical/html`
|
|
321
|
-
- `{ url, mimeType: "image/*", alt? }` → `{ type: "image", … }`
|
|
322
|
-
- `{ url, label | text | title }` → `{ type: "link", … }`
|
|
323
|
-
- Strings → `{ type: "paragraph", … }`
|
|
324
|
-
- Structural keys (`id`, `blockType`, `blockName`, `_template`, `order`) are skipped
|
|
325
|
-
5. **Serializes** the Intermediate Representation to an HTML string that is fed to the Yoast engine.
|
|
326
|
-
|
|
327
|
-
Image checks (alt text, keyphrase in alt, image count) work against the real, resolved media — not raw relationship IDs.
|
|
328
|
-
|
|
329
|
-
### Custom extractor (`ContentExtractor`)
|
|
325
|
+
### The extractor contract
|
|
330
326
|
|
|
331
327
|
```ts
|
|
332
328
|
type ContentExtractor = (
|
|
333
|
-
values: Record<string, unknown>,
|
|
329
|
+
values: Record<string, unknown>, // RAW form values; relationship/upload fields are ids
|
|
330
|
+
ctx: ExtractContext, // { locale?: string; apiRoute?: string }
|
|
331
|
+
toolkit: ExtractToolkit, // { resolveDocs, helpers }
|
|
334
332
|
) => ContentNode[] | Promise<ContentNode[]>;
|
|
335
|
-
```
|
|
336
333
|
|
|
337
|
-
|
|
334
|
+
interface ExtractToolkit {
|
|
335
|
+
resolveDocs: (queries: DocQuery[]) => Promise<DocStore>;
|
|
336
|
+
helpers: ContentHelpers; // heading, paragraph, link, image, video, richText, html, compact
|
|
337
|
+
}
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
interface DocQuery {
|
|
340
|
+
collection: string;
|
|
341
|
+
ids: (string | number)[];
|
|
342
|
+
select?: string[]; // field projection → ?select[field]=true
|
|
343
|
+
depth?: number; // relationship population → ?depth=N (default 0)
|
|
344
|
+
}
|
|
342
345
|
|
|
343
|
-
|
|
346
|
+
interface DocStore {
|
|
347
|
+
get(collection: string, id: string | number): Record<string, unknown> | undefined;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Your extractor:
|
|
352
|
+
|
|
353
|
+
- Receives the **raw**, unsaved form values. Relationship and upload fields are **ids** (or id arrays / `{ relationTo, value }`), **not** populated objects — the plugin does no hydration.
|
|
354
|
+
- Owns ref collection and any link/URL building. The plugin makes no assumptions about your link types (internal references, custom URLs, etc.) — you decide what to fetch and how to turn it into a node.
|
|
355
|
+
- Uses `toolkit.resolveDocs(queries)` to fetch referenced/upload documents. You pass one query per collection with the `ids` you collected and an optional `select` projection (fetch only the fields you need) and `depth`. **All queries run in parallel.** Read results with `store.get(collection, id)`.
|
|
356
|
+
- Returns `ContentNode[]` (built with the helpers); the plugin serializes it.
|
|
344
357
|
|
|
345
358
|
```ts
|
|
346
|
-
// src/
|
|
347
|
-
import {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
link,
|
|
352
|
-
richText,
|
|
359
|
+
// src/collections/Page/extractPageContent.ts
|
|
360
|
+
import { heading, image, paragraph, richText } from "@focus-reactive/payload-plugin-seo/content";
|
|
361
|
+
import type {
|
|
362
|
+
ContentExtractor,
|
|
363
|
+
DocStore,
|
|
353
364
|
} from "@focus-reactive/payload-plugin-seo/content";
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
365
|
+
|
|
366
|
+
const extractPageContent: ContentExtractor = async (values, _ctx, { resolveDocs, helpers }) => {
|
|
367
|
+
const blocks = (values as { blocks?: Record<string, unknown>[] }).blocks ?? [];
|
|
368
|
+
|
|
369
|
+
// 1. Collect the ids you care about from the RAW values (you know your schema).
|
|
370
|
+
const mediaIds = blocks.flatMap((b) => (typeof b.image === "number" ? [b.image] : []));
|
|
371
|
+
|
|
372
|
+
// 2. Fetch them — one parallel request per collection, projected to only the fields you need.
|
|
373
|
+
const docs: DocStore = await resolveDocs([
|
|
374
|
+
{ collection: "media", ids: mediaIds, select: ["url", "alt", "mimeType"] },
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
// 3. Build the Intermediate Representation.
|
|
378
|
+
return helpers.compact(
|
|
379
|
+
blocks.flatMap((b) => {
|
|
380
|
+
const media = typeof b.image === "number" ? docs.get("media", b.image) : undefined;
|
|
381
|
+
return [
|
|
382
|
+
heading(2, b.title as string),
|
|
383
|
+
paragraph(b.subtitle as string),
|
|
384
|
+
image((media as { url?: string })?.url, (media as { alt?: string })?.alt),
|
|
385
|
+
richText(b.content),
|
|
386
|
+
];
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export default extractPageContent;
|
|
382
392
|
```
|
|
383
393
|
|
|
384
394
|
### The registry: why it exists and how to use it
|
|
@@ -401,7 +411,6 @@ seoPlugin({
|
|
|
401
411
|
seoTitle: "meta.title",
|
|
402
412
|
metaDescription: "meta.description",
|
|
403
413
|
slug: "slug",
|
|
404
|
-
content: "blocks",
|
|
405
414
|
},
|
|
406
415
|
extractContentPath: "@/collections/Page/extractPageContent#default",
|
|
407
416
|
},
|
|
@@ -447,11 +456,11 @@ export default buildConfig({
|
|
|
447
456
|
});
|
|
448
457
|
```
|
|
449
458
|
|
|
450
|
-
If the configured `extractContentPath` is set but the function is not registered (e.g. the provider is missing), the plugin logs a one-time console
|
|
459
|
+
If the configured `extractContentPath` is set but the function is not registered (e.g. the provider is missing), the plugin logs a one-time console error and content analysis for that collection is empty — there is no built-in fallback.
|
|
451
460
|
|
|
452
|
-
### Limitation:
|
|
461
|
+
### Limitation: links and uploads embedded inside richText
|
|
453
462
|
|
|
454
|
-
|
|
463
|
+
`helpers.richText(value)` serializes the lexical tree to HTML **as-is**. Internal-link nodes and upload nodes embedded inside richText *body content* are **not** resolved by the plugin — their `href`s / `src`s are left as the lexical tree provides them. This keeps the plugin fully schema-agnostic. If you need those resolved, walk the lexical tree yourself inside your extractor (its structure is standard Payload lexical), collect the referenced ids, fetch them with `resolveDocs`, and rewrite the nodes before building the IR.
|
|
455
464
|
|
|
456
465
|
---
|
|
457
466
|
|
|
@@ -478,23 +487,29 @@ The drawer presents six tabs, all derived from a single in-browser Yoast analysi
|
|
|
478
487
|
|
|
479
488
|
```ts
|
|
480
489
|
seoPlugin({
|
|
481
|
-
collections: [
|
|
490
|
+
collections: [
|
|
491
|
+
{
|
|
492
|
+
slug: "pages",
|
|
493
|
+
fields: { slug: "slug" },
|
|
494
|
+
extractContentPath: "@/seo/extractPageContent#default",
|
|
495
|
+
},
|
|
496
|
+
],
|
|
482
497
|
supportedLocales: ["en", "de", "fr", "es"],
|
|
483
498
|
});
|
|
484
499
|
```
|
|
485
500
|
|
|
486
|
-
The active locale is taken from the admin and normalized to Yoast's `xx_XX` form (e.g. `en` → `en_EN`).
|
|
501
|
+
The active locale is taken from the admin and normalized to Yoast's `xx_XX` form (e.g. `en` → `en_EN`). The locale is passed to your extractor as `ctx.locale` and to `resolveDocs` (so projected fetches are locale-correct). A locale not listed in `supportedLocales` falls back to English processing.
|
|
487
502
|
|
|
488
503
|
---
|
|
489
504
|
|
|
490
505
|
## Exports Reference
|
|
491
506
|
|
|
492
|
-
| Import path | Exports
|
|
493
|
-
| --------------------------------------------------------- |
|
|
494
|
-
| `@focus-reactive/payload-plugin-seo` | `seoPlugin`, and types `SeoPluginConfig`, `SeoCollectionConfig`, `SeoFieldPaths`, `SeoSiteConfig`, `ContentExtractor
|
|
495
|
-
| `@focus-reactive/payload-plugin-seo/content` | Builder helpers `heading`, `paragraph`, `link`, `image`, `video`, `richText`, `html`; `registerContentExtractors`, `resolveContentExtractor`; types `ContentNode`, `HeadingLevel`, `ContentExtractor`, `
|
|
496
|
-
| `@focus-reactive/payload-plugin-seo/admin.css` | Compiled admin styles for the drawer & button
|
|
497
|
-
| `@focus-reactive/payload-plugin-seo/components/SeoButton` | `SeoButton` — the toolbar button component (wired automatically by the plugin via the importMap; you normally never import this directly)
|
|
507
|
+
| Import path | Exports |
|
|
508
|
+
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
509
|
+
| `@focus-reactive/payload-plugin-seo` | `seoPlugin`, and types `SeoPluginConfig`, `SeoCollectionConfig`, `SeoFieldPaths`, `SeoSiteConfig`, `ContentExtractor` |
|
|
510
|
+
| `@focus-reactive/payload-plugin-seo/content` | Builder helpers `heading`, `paragraph`, `link`, `image`, `video`, `richText`, `html`, `compact`; `registerContentExtractors`, `resolveContentExtractor`; types `ContentNode`, `HeadingLevel`, `ContentExtractor`, `ExtractContext`, `ExtractToolkit`, `DocQuery`, `DocStore`, `ContentHelpers` |
|
|
511
|
+
| `@focus-reactive/payload-plugin-seo/admin.css` | Compiled admin styles for the drawer & button |
|
|
512
|
+
| `@focus-reactive/payload-plugin-seo/components/SeoButton` | `SeoButton` — the toolbar button component (wired automatically by the plugin via the importMap; you normally never import this directly) |
|
|
498
513
|
|
|
499
514
|
---
|
|
500
515
|
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
export interface SeoButtonProps {
|
|
2
2
|
collectionSlug: string;
|
|
3
3
|
fields: Record<string, string>;
|
|
4
|
-
extractContentPath: string
|
|
4
|
+
extractContentPath: string;
|
|
5
5
|
site: {
|
|
6
6
|
name: string;
|
|
7
7
|
baseUrl: string;
|
|
8
8
|
faviconUrl: string;
|
|
9
9
|
};
|
|
10
10
|
supportedLocales: string[];
|
|
11
|
-
resolveDepth: number;
|
|
12
|
-
slugPaths: Record<string, string>;
|
|
13
11
|
}
|
|
14
|
-
export declare function SeoButtonInner({ collectionSlug, fields, site, supportedLocales, extractContentPath,
|
|
12
|
+
export declare function SeoButtonInner({ collectionSlug, fields, site, supportedLocales, extractContentPath, }: SeoButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
15
13
|
//# sourceMappingURL=SeoButtonInner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SeoButtonInner.d.ts","sourceRoot":"","sources":["../../../src/components/SeoButton/SeoButtonInner.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"SeoButtonInner.d.ts","sourceRoot":"","sources":["../../../src/components/SeoButton/SeoButtonInner.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAID,wBAAgB,cAAc,CAAC,EAC7B,cAAc,EACd,MAAM,EACN,IAAI,EACJ,gBAAgB,EAChB,kBAAkB,GACnB,EAAE,cAAc,2CAqDhB"}
|
|
@@ -9,17 +9,21 @@ import { isKeyphrasePending } from "../SeoDrawer/keyphrasePending";
|
|
|
9
9
|
import { useAnalysis } from "../SeoDrawer/useAnalysis";
|
|
10
10
|
import { useLiveDocument } from "../SeoDrawer/useLiveDocument";
|
|
11
11
|
const DRAWER_SLUG = "seo-analytics-drawer";
|
|
12
|
-
function SeoButtonInner({
|
|
12
|
+
function SeoButtonInner({
|
|
13
|
+
collectionSlug,
|
|
14
|
+
fields,
|
|
15
|
+
site,
|
|
16
|
+
supportedLocales,
|
|
17
|
+
extractContentPath
|
|
18
|
+
}) {
|
|
13
19
|
const { openModal } = useModal();
|
|
14
20
|
const [keyphrase, setKeyphrase] = useState("");
|
|
15
|
-
const { signature, getInput
|
|
21
|
+
const { signature, getInput } = useLiveDocument({
|
|
16
22
|
collectionSlug,
|
|
17
23
|
fields,
|
|
18
24
|
site: { name: site.name, baseUrl: site.baseUrl },
|
|
19
25
|
keyphrase,
|
|
20
|
-
extractContentPath
|
|
21
|
-
resolveDepth,
|
|
22
|
-
slugPaths
|
|
26
|
+
extractContentPath
|
|
23
27
|
});
|
|
24
28
|
const { result, analyzing, analyzedKeyphrase, analyzeNow } = useAnalysis({
|
|
25
29
|
getInput,
|
|
@@ -29,10 +33,9 @@ function SeoButtonInner({ collectionSlug, fields, site, supportedLocales, extrac
|
|
|
29
33
|
const keyphrasePending = isKeyphrasePending(keyphrase, analyzedKeyphrase);
|
|
30
34
|
const overall = result?.overall ?? null;
|
|
31
35
|
const open = useCallback(() => {
|
|
32
|
-
invalidateMedia();
|
|
33
36
|
analyzeNow();
|
|
34
37
|
openModal(DRAWER_SLUG);
|
|
35
|
-
}, [analyzeNow,
|
|
38
|
+
}, [analyzeNow, openModal]);
|
|
36
39
|
return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex", children: [
|
|
37
40
|
/* @__PURE__ */ jsx(
|
|
38
41
|
Button,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/SeoButton/SeoButtonInner.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, useModal } from \"@payloadcms/ui\";\nimport { Gauge } from \"lucide-react\";\nimport { useCallback, useState } from \"react\";\nimport { ScoreBadge } from \"./ScoreBadge\";\nimport { SeoDrawer } from \"../SeoDrawer\";\nimport { isKeyphrasePending } from \"../SeoDrawer/keyphrasePending\";\nimport { useAnalysis } from \"../SeoDrawer/useAnalysis\";\nimport { useLiveDocument } from \"../SeoDrawer/useLiveDocument\";\n\nexport interface SeoButtonProps {\n collectionSlug: string;\n fields: Record<string, string>;\n extractContentPath: string
|
|
1
|
+
{"version":3,"sources":["../../../src/components/SeoButton/SeoButtonInner.tsx"],"sourcesContent":["\"use client\";\n\nimport { Button, useModal } from \"@payloadcms/ui\";\nimport { Gauge } from \"lucide-react\";\nimport { useCallback, useState } from \"react\";\nimport { ScoreBadge } from \"./ScoreBadge\";\nimport { SeoDrawer } from \"../SeoDrawer\";\nimport { isKeyphrasePending } from \"../SeoDrawer/keyphrasePending\";\nimport { useAnalysis } from \"../SeoDrawer/useAnalysis\";\nimport { useLiveDocument } from \"../SeoDrawer/useLiveDocument\";\n\nexport interface SeoButtonProps {\n collectionSlug: string;\n fields: Record<string, string>;\n extractContentPath: string;\n site: { name: string; baseUrl: string; faviconUrl: string };\n supportedLocales: string[];\n}\n\nconst DRAWER_SLUG = \"seo-analytics-drawer\";\n\nexport function SeoButtonInner({\n collectionSlug,\n fields,\n site,\n supportedLocales,\n extractContentPath,\n}: SeoButtonProps) {\n const { openModal } = useModal();\n const [keyphrase, setKeyphrase] = useState(\"\");\n\n const { signature, getInput } = useLiveDocument({\n collectionSlug,\n fields,\n site: { name: site.name, baseUrl: site.baseUrl },\n keyphrase,\n extractContentPath,\n });\n const { result, analyzing, analyzedKeyphrase, analyzeNow } = useAnalysis({\n getInput,\n signature,\n supportedLocales,\n });\n\n const keyphrasePending = isKeyphrasePending(keyphrase, analyzedKeyphrase);\n const overall = result?.overall ?? null;\n\n const open = useCallback(() => {\n analyzeNow();\n openModal(DRAWER_SLUG);\n }, [analyzeNow, openModal]);\n\n return (\n <span className=\"relative inline-flex\">\n <Button\n aria-label=\"SEO Analytics\"\n buttonStyle=\"none\"\n className=\"seo-doc-btn m-0 w-[calc(var(--base)*1.6)] h-[calc(var(--base)*1.6)] inline-flex items-center justify-center border border-[var(--theme-elevation-100)] rounded-rs bg-transparent text-neutral-800 transition-[border-color,background-color] duration-100 hover:border-neutral-300 hover:bg-neutral-100\"\n extraButtonProps={{ title: undefined }}\n icon={<Gauge />}\n iconStyle=\"without-border\"\n margin={false}\n onClick={open}\n size=\"small\"\n tooltip=\"SEO Analytics\"\n />\n {overall && <ScoreBadge score={overall.seoScore} status={overall.status} />}\n\n <SeoDrawer\n analyzeNow={analyzeNow}\n analyzing={analyzing}\n drawerSlug={DRAWER_SLUG}\n keyphrase={keyphrase}\n keyphrasePending={keyphrasePending}\n result={result}\n setKeyphrase={setKeyphrase}\n site={site}\n />\n </span>\n );\n}\n"],"mappings":";AAqDI,SAMU,KANV;AAnDJ,SAAS,QAAQ,gBAAgB;AACjC,SAAS,aAAa;AACtB,SAAS,aAAa,gBAAgB;AACtC,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,0BAA0B;AACnC,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAUhC,MAAM,cAAc;AAEb,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,EAAE,UAAU,IAAI,SAAS;AAC/B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE;AAE7C,QAAM,EAAE,WAAW,SAAS,IAAI,gBAAgB;AAAA,IAC9C;AAAA,IACA;AAAA,IACA,MAAM,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ;AAAA,IAC/C;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,EAAE,QAAQ,WAAW,mBAAmB,WAAW,IAAI,YAAY;AAAA,IACvE;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,mBAAmB,mBAAmB,WAAW,iBAAiB;AACxE,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,OAAO,YAAY,MAAM;AAC7B,eAAW;AACX,cAAU,WAAW;AAAA,EACvB,GAAG,CAAC,YAAY,SAAS,CAAC;AAE1B,SACE,qBAAC,UAAK,WAAU,wBACd;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAW;AAAA,QACX,aAAY;AAAA,QACZ,WAAU;AAAA,QACV,kBAAkB,EAAE,OAAO,OAAU;AAAA,QACrC,MAAM,oBAAC,SAAM;AAAA,QACb,WAAU;AAAA,QACV,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAK;AAAA,QACL,SAAQ;AAAA;AAAA,IACV;AAAA,IACC,WAAW,oBAAC,cAAW,OAAO,QAAQ,UAAU,QAAQ,QAAQ,QAAQ;AAAA,IAEzE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { ClientField } from "payload";
|
|
2
|
-
import type { ExtractContext } from "../../content/extract/context";
|
|
3
|
-
import type { DocResolver } from "../../content/resolve/resolver";
|
|
4
1
|
import type { AnalysisInput } from "../../engine/types/analysis";
|
|
5
2
|
import type { ContentExtractor, SeoFieldPaths } from "../../types/config";
|
|
6
3
|
export interface BuildAnalysisInputArgs {
|
|
@@ -16,11 +13,7 @@ export interface BuildAnalysisInputArgs {
|
|
|
16
13
|
name: string;
|
|
17
14
|
baseUrl: string;
|
|
18
15
|
};
|
|
19
|
-
|
|
20
|
-
ctx: ExtractContext;
|
|
21
|
-
resolver: DocResolver;
|
|
22
|
-
resolveDepth: number;
|
|
23
|
-
override?: ContentExtractor;
|
|
16
|
+
extractor?: ContentExtractor;
|
|
24
17
|
}
|
|
25
18
|
export declare function buildAnalysisInput(args: BuildAnalysisInputArgs): Promise<AnalysisInput>;
|
|
26
19
|
//# sourceMappingURL=build-analysis-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-analysis-input.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/build-analysis-input.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build-analysis-input.d.ts","sourceRoot":"","sources":["../../../src/components/SeoDrawer/build-analysis-input.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EACV,gBAAgB,EAIhB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAK5B,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAC,CAY7F"}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { serialize } from "../../content/schema/serialize";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
compact,
|
|
4
|
+
heading,
|
|
5
|
+
html,
|
|
6
|
+
image,
|
|
7
|
+
link,
|
|
8
|
+
paragraph,
|
|
9
|
+
richText,
|
|
10
|
+
video
|
|
11
|
+
} from "../../content/schema/helpers";
|
|
12
|
+
import { createResolveDocs } from "../../content/resolve/resolve-docs";
|
|
6
13
|
import { buildInput } from "./buildInput";
|
|
7
|
-
|
|
8
|
-
if (content == null)
|
|
9
|
-
return { include: [], exclude: [] };
|
|
10
|
-
if (typeof content === "string")
|
|
11
|
-
return { include: [content], exclude: [] };
|
|
12
|
-
const sel = content;
|
|
13
|
-
return { include: sel.include ?? [], exclude: sel.exclude ?? [] };
|
|
14
|
-
}
|
|
15
|
-
function metadataPaths(fields) {
|
|
16
|
-
return [fields.seoTitle, fields.metaDescription, fields.slug ?? "slug"].filter((p) => p !== void 0);
|
|
17
|
-
}
|
|
14
|
+
const helpers = { heading, paragraph, link, image, video, html, richText, compact };
|
|
18
15
|
async function buildAnalysisInput(args) {
|
|
19
16
|
const ir = await extractIntermediateRepresentation(args);
|
|
20
17
|
const contentHtml = serialize(ir);
|
|
@@ -28,26 +25,17 @@ async function buildAnalysisInput(args) {
|
|
|
28
25
|
});
|
|
29
26
|
}
|
|
30
27
|
async function extractIntermediateRepresentation(args) {
|
|
31
|
-
|
|
32
|
-
const meta = metadataPaths(args.fields);
|
|
33
|
-
if (args.fields.content == null)
|
|
28
|
+
if (!args.extractor)
|
|
34
29
|
return [];
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
values: args.values,
|
|
45
|
-
fields: args.hostFields,
|
|
46
|
-
ctx,
|
|
47
|
-
selection,
|
|
48
|
-
metadataPaths: meta,
|
|
49
|
-
depth: args.resolveDepth
|
|
50
|
-
});
|
|
30
|
+
const ctx = {
|
|
31
|
+
locale: args.payloadLocale,
|
|
32
|
+
apiRoute: args.apiRoute
|
|
33
|
+
};
|
|
34
|
+
const toolkit = {
|
|
35
|
+
resolveDocs: createResolveDocs(args.apiRoute, args.payloadLocale),
|
|
36
|
+
helpers
|
|
37
|
+
};
|
|
38
|
+
return await args.extractor(args.values, ctx, toolkit);
|
|
51
39
|
}
|
|
52
40
|
export {
|
|
53
41
|
buildAnalysisInput
|