@bringyouup/payload-plugin-ai-seo 0.1.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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/index.cjs +382 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +380 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +69 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Artyom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @bringyouup/payload-plugin-ai-seo
|
|
2
|
+
|
|
3
|
+
AI-powered SEO meta generation plugin for [Payload CMS](https://payloadcms.com/).
|
|
4
|
+
|
|
5
|
+
Automatically generates SEO-optimized `title` and `description` meta tags using OpenAI when creating or updating documents.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🤖 AI-powered meta tag generation using OpenAI
|
|
10
|
+
- 🎯 Flexible field mapping for any collection structure
|
|
11
|
+
- 🌍 Localization support (generates SEO for current locale)
|
|
12
|
+
- ⚡ Automatic generation on document create/update
|
|
13
|
+
- 🔧 Highly configurable per collection
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @bringyouup/payload-plugin-ai-seo
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @bringyouup/payload-plugin-ai-seo
|
|
21
|
+
# or
|
|
22
|
+
yarn add @bringyouup/payload-plugin-ai-seo
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// payload.config.ts
|
|
29
|
+
import { buildConfig } from "payload";
|
|
30
|
+
import { aiSeoPlugin } from "@bringyouup/payload-plugin-ai-seo";
|
|
31
|
+
|
|
32
|
+
export default buildConfig({
|
|
33
|
+
// ... your config
|
|
34
|
+
plugins: [
|
|
35
|
+
aiSeoPlugin({
|
|
36
|
+
enabled: true,
|
|
37
|
+
apiKey: process.env.OPENAI_API_KEY ?? "",
|
|
38
|
+
collections: ["pages", "posts"],
|
|
39
|
+
seoFields: {
|
|
40
|
+
title: "meta.title",
|
|
41
|
+
description: "meta.description",
|
|
42
|
+
},
|
|
43
|
+
contentFields: ["title", "content"],
|
|
44
|
+
model: "gpt-4o-mini",
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
### Plugin Options
|
|
53
|
+
|
|
54
|
+
| Option | Type | Required | Description |
|
|
55
|
+
| --------------- | ------------------------------------------ | -------- | ------------------------------------- |
|
|
56
|
+
| `enabled` | `boolean` | Yes | Enable/disable the plugin |
|
|
57
|
+
| `apiKey` | `string` | Yes | OpenAI API key |
|
|
58
|
+
| `collections` | `string \| string[] \| CollectionConfig[]` | Yes | Collections to apply SEO generation |
|
|
59
|
+
| `seoFields` | `SeoFields` | No | Default field paths for SEO output |
|
|
60
|
+
| `contentFields` | `string[]` | No | Default fields to analyze for SEO |
|
|
61
|
+
| `model` | `OpenAIChatModelId` | No | OpenAI model (default: `gpt-4o-mini`) |
|
|
62
|
+
|
|
63
|
+
### Collection-Specific Configuration
|
|
64
|
+
|
|
65
|
+
You can configure each collection individually:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
aiSeoPlugin({
|
|
69
|
+
enabled: true,
|
|
70
|
+
apiKey: process.env.OPENAI_API_KEY ?? "",
|
|
71
|
+
collections: [
|
|
72
|
+
{
|
|
73
|
+
collection: "pages",
|
|
74
|
+
seoFields: {
|
|
75
|
+
title: "meta.title",
|
|
76
|
+
description: "meta.description",
|
|
77
|
+
},
|
|
78
|
+
contentFields: ["title", "hero", "blocks"],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
collection: "posts",
|
|
82
|
+
seoFields: {
|
|
83
|
+
title: "seo.metaTitle",
|
|
84
|
+
description: "seo.metaDesc",
|
|
85
|
+
},
|
|
86
|
+
contentFields: ["title", "excerpt", "content"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### SeoFields Interface
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface SeoFields {
|
|
96
|
+
title: string; // Path to title field (e.g., 'meta.title')
|
|
97
|
+
description: string; // Path to description field (e.g., 'meta.description')
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## How It Works
|
|
102
|
+
|
|
103
|
+
1. When a document is created or updated, the plugin extracts content from specified `contentFields`
|
|
104
|
+
2. Content is sent to OpenAI for analysis
|
|
105
|
+
3. AI generates SEO-optimized title (≤60 chars) and description (≤160 chars)
|
|
106
|
+
4. Generated values are written to the specified `seoFields` paths
|
|
107
|
+
5. Only empty fields are filled - existing SEO values are preserved
|
|
108
|
+
|
|
109
|
+
## Requirements
|
|
110
|
+
|
|
111
|
+
- Payload CMS 3.0+
|
|
112
|
+
- Node.js 18+
|
|
113
|
+
- OpenAI API key
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ai = require('ai');
|
|
4
|
+
var openai = require('@ai-sdk/openai');
|
|
5
|
+
|
|
6
|
+
// src/generateSeoWithAi.ts
|
|
7
|
+
var seoSchema = ai.jsonSchema({
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
title: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "SEO meta title, under 60 characters"
|
|
13
|
+
},
|
|
14
|
+
description: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "SEO meta description, under 160 characters"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: ["title", "description"],
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
});
|
|
22
|
+
async function generateSeoWithAi(input) {
|
|
23
|
+
const { pageTitle, pageContent, apiKey, model = "gpt-4o-mini" } = input;
|
|
24
|
+
console.log(`[aiSeo] Starting AI generation with model: ${model}`);
|
|
25
|
+
if (!apiKey?.trim()) {
|
|
26
|
+
console.error("[aiSeo] API key is empty, cannot generate SEO");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const openai$1 = openai.createOpenAI({ apiKey });
|
|
31
|
+
let prompt = `Generate SEO meta title and meta description for a page. Return only the two strings, no explanations. Meta title must be under 60 characters, meta description under 160 characters.
|
|
32
|
+
|
|
33
|
+
Page title: ${pageTitle}`;
|
|
34
|
+
if (pageContent && pageContent.trim()) {
|
|
35
|
+
const contentToSend = pageContent.slice(0, 3e3);
|
|
36
|
+
console.log(
|
|
37
|
+
`[aiSeo] Sending ${contentToSend.length} chars of content to AI (trimmed from ${pageContent.length})`
|
|
38
|
+
);
|
|
39
|
+
prompt += `
|
|
40
|
+
|
|
41
|
+
Page content:
|
|
42
|
+
${contentToSend}`;
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`[aiSeo] No page content available, using only title`);
|
|
45
|
+
}
|
|
46
|
+
console.log(`[aiSeo] Full prompt length: ${prompt.length} characters`);
|
|
47
|
+
console.log(`[aiSeo] Prompt preview:`, prompt.slice(0, 200) + "...");
|
|
48
|
+
console.log(`[aiSeo] Calling OpenAI API...`);
|
|
49
|
+
const { object } = await ai.generateObject({
|
|
50
|
+
model: openai$1(model),
|
|
51
|
+
prompt,
|
|
52
|
+
schema: seoSchema
|
|
53
|
+
});
|
|
54
|
+
console.log(`[aiSeo] Raw AI response:`, object);
|
|
55
|
+
const result = {
|
|
56
|
+
title: (object.title ?? "").slice(0, 60),
|
|
57
|
+
description: (object.description ?? "").slice(0, 160)
|
|
58
|
+
};
|
|
59
|
+
console.log(`[aiSeo] Final result:`, result);
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("[aiSeo] generateSeoWithAi failed:", err);
|
|
63
|
+
if (err instanceof Error) {
|
|
64
|
+
console.error("[aiSeo] Error details:", err.message);
|
|
65
|
+
console.error("[aiSeo] Stack trace:", err.stack);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/setValueByPath.ts
|
|
72
|
+
function setValueByPath(obj, path, value) {
|
|
73
|
+
const parts = path.split(".");
|
|
74
|
+
let current = obj;
|
|
75
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
76
|
+
const key = parts[i];
|
|
77
|
+
const next = current[key];
|
|
78
|
+
if (next != null && typeof next === "object" && !Array.isArray(next)) {
|
|
79
|
+
current = next;
|
|
80
|
+
} else {
|
|
81
|
+
const nextObj = {};
|
|
82
|
+
current[key] = nextObj;
|
|
83
|
+
current = nextObj;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
current[parts[parts.length - 1]] = value;
|
|
87
|
+
}
|
|
88
|
+
function getValueByPath(obj, path) {
|
|
89
|
+
const parts = path.split(".");
|
|
90
|
+
let current = obj;
|
|
91
|
+
for (const key of parts) {
|
|
92
|
+
if (current == null || typeof current !== "object" || Array.isArray(current)) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
current = current[key];
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
function isEmpty(value) {
|
|
100
|
+
if (value == null) return true;
|
|
101
|
+
if (typeof value === "string") return value.trim() === "";
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/beforeChangeAiSeo.ts
|
|
106
|
+
var DEFAULT_CONTENT_FIELDS = ["title"];
|
|
107
|
+
var DEFAULT_SEO_FIELDS = {
|
|
108
|
+
title: "meta.title",
|
|
109
|
+
description: "meta.description"
|
|
110
|
+
};
|
|
111
|
+
function getLocaleFromRequest(req) {
|
|
112
|
+
if (req.locale) {
|
|
113
|
+
return req.locale;
|
|
114
|
+
}
|
|
115
|
+
const searchLocale = req.searchParams?.get("locale");
|
|
116
|
+
if (searchLocale) {
|
|
117
|
+
return searchLocale;
|
|
118
|
+
}
|
|
119
|
+
const defaultLocale = req.payload.config.localization?.defaultLocale;
|
|
120
|
+
if (defaultLocale) {
|
|
121
|
+
return defaultLocale;
|
|
122
|
+
}
|
|
123
|
+
return "en";
|
|
124
|
+
}
|
|
125
|
+
function getLocalizedValue(value, locale, defaultLocale) {
|
|
126
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
const obj = value;
|
|
130
|
+
const keys = Object.keys(obj);
|
|
131
|
+
const hasLocaleKeys = keys.length > 0 && keys.every((key) => /^[a-z]{2}(-[A-Z]{2})?$/.test(key));
|
|
132
|
+
if (!hasLocaleKeys) {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
console.log(`[aiSeo] Detected localized field with locales:`, keys);
|
|
136
|
+
if (locale in obj) {
|
|
137
|
+
console.log(`[aiSeo] Using locale: ${locale}`);
|
|
138
|
+
return obj[locale];
|
|
139
|
+
}
|
|
140
|
+
if (defaultLocale in obj) {
|
|
141
|
+
console.log(`[aiSeo] Falling back to default locale: ${defaultLocale}`);
|
|
142
|
+
return obj[defaultLocale];
|
|
143
|
+
}
|
|
144
|
+
const firstKey = keys[0];
|
|
145
|
+
console.log(`[aiSeo] Using first available locale: ${firstKey}`);
|
|
146
|
+
return obj[firstKey];
|
|
147
|
+
}
|
|
148
|
+
function resolvePageTitle(data) {
|
|
149
|
+
const title = data.title;
|
|
150
|
+
if (typeof title === "string" && title.trim()) return title.trim();
|
|
151
|
+
if (title != null && typeof title === "object" && !Array.isArray(title)) {
|
|
152
|
+
const obj = title;
|
|
153
|
+
const first = Object.values(obj).find((v) => typeof v === "string" && v.trim());
|
|
154
|
+
return typeof first === "string" ? first.trim() : "Page";
|
|
155
|
+
}
|
|
156
|
+
return "Page";
|
|
157
|
+
}
|
|
158
|
+
function extractLexicalText(node) {
|
|
159
|
+
if (!node || typeof node !== "object") {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
const obj = node;
|
|
163
|
+
const parts = [];
|
|
164
|
+
if (obj.type === "text" && typeof obj.text === "string") {
|
|
165
|
+
return obj.text;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(obj.children)) {
|
|
168
|
+
for (const child of obj.children) {
|
|
169
|
+
const text = extractLexicalText(child);
|
|
170
|
+
if (text) {
|
|
171
|
+
parts.push(text);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join(" ");
|
|
176
|
+
}
|
|
177
|
+
function extractTextContent(value, locale, defaultLocale, depth = 0) {
|
|
178
|
+
if (depth > 10) {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
if (value == null) {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
188
|
+
return String(value);
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
return value.map((item) => extractTextContent(item, locale, defaultLocale, depth + 1)).filter(Boolean).join("\n");
|
|
192
|
+
}
|
|
193
|
+
if (typeof value === "object") {
|
|
194
|
+
const obj = value;
|
|
195
|
+
if ("root" in obj && obj.root != null) {
|
|
196
|
+
return extractLexicalText(obj.root);
|
|
197
|
+
}
|
|
198
|
+
if ("children" in obj && Array.isArray(obj.children)) {
|
|
199
|
+
return extractLexicalText(obj);
|
|
200
|
+
}
|
|
201
|
+
if ("blockType" in obj || "type" in obj) {
|
|
202
|
+
const textFields = ["text", "content", "heading", "label", "title"];
|
|
203
|
+
for (const field of textFields) {
|
|
204
|
+
if (field in obj && obj[field]) {
|
|
205
|
+
const text = extractTextContent(obj[field], locale, defaultLocale, depth + 1);
|
|
206
|
+
if (text) {
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const localizedValue = getLocalizedValue(obj, locale, defaultLocale);
|
|
213
|
+
if (localizedValue !== obj) {
|
|
214
|
+
return extractTextContent(localizedValue, locale, defaultLocale, depth + 1);
|
|
215
|
+
}
|
|
216
|
+
const texts = Object.entries(obj).filter(([key]) => !key.startsWith("_") && key !== "id").map(([, val]) => extractTextContent(val, locale, defaultLocale, depth + 1)).filter(Boolean);
|
|
217
|
+
return texts.join(" ");
|
|
218
|
+
}
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
function buildPageContent(data, contentFields, locale, defaultLocale, collectionSlug) {
|
|
222
|
+
console.log(`[aiSeo] Building content for collection: ${collectionSlug}`);
|
|
223
|
+
console.log(`[aiSeo] Using locale: ${locale}, default: ${defaultLocale}`);
|
|
224
|
+
console.log(`[aiSeo] Content fields to extract:`, contentFields);
|
|
225
|
+
const contentParts = [];
|
|
226
|
+
for (const path of contentFields) {
|
|
227
|
+
const value = getValueByPath(data, path);
|
|
228
|
+
console.log(`[aiSeo] Field "${path}":`, {
|
|
229
|
+
exists: value != null,
|
|
230
|
+
type: Array.isArray(value) ? "array" : typeof value,
|
|
231
|
+
arrayLength: Array.isArray(value) ? value.length : void 0,
|
|
232
|
+
hasRoot: value != null && typeof value === "object" && "root" in value,
|
|
233
|
+
hasChildren: value != null && typeof value === "object" && "children" in value
|
|
234
|
+
});
|
|
235
|
+
if (value != null) {
|
|
236
|
+
const text = extractTextContent(value, locale, defaultLocale).trim();
|
|
237
|
+
if (text) {
|
|
238
|
+
const preview = text.length > 100 ? text.slice(0, 100) + "..." : text;
|
|
239
|
+
console.log(`[aiSeo] Extracted ${text.length} chars from "${path}":`, preview);
|
|
240
|
+
contentParts.push(text);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(`[aiSeo] No text extracted from "${path}"`);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`[aiSeo] Field "${path}" is null/undefined`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const fullContent = contentParts.join("\n\n");
|
|
249
|
+
console.log(`[aiSeo] Total content length: ${fullContent.length} characters`);
|
|
250
|
+
return fullContent;
|
|
251
|
+
}
|
|
252
|
+
function createBeforeChangeAiSeo(options, collectionSlug) {
|
|
253
|
+
const {
|
|
254
|
+
apiKey,
|
|
255
|
+
seoFields,
|
|
256
|
+
contentFields = DEFAULT_CONTENT_FIELDS,
|
|
257
|
+
model
|
|
258
|
+
} = options;
|
|
259
|
+
const targetFields = seoFields ?? DEFAULT_SEO_FIELDS;
|
|
260
|
+
return async function beforeChangeAiSeo({ data, operation, collection, req }) {
|
|
261
|
+
console.log(
|
|
262
|
+
`[aiSeo] Hook triggered for collection: ${collection?.slug ?? collectionSlug}, operation: ${operation}`
|
|
263
|
+
);
|
|
264
|
+
if (operation !== "create") {
|
|
265
|
+
console.log(`[aiSeo] Skipping - operation is "${operation}", not "create"`);
|
|
266
|
+
return data;
|
|
267
|
+
}
|
|
268
|
+
const rawData = data;
|
|
269
|
+
const titleEmpty = isEmpty(getValueByPath(rawData, targetFields.title));
|
|
270
|
+
const descriptionEmpty = isEmpty(getValueByPath(rawData, targetFields.description));
|
|
271
|
+
console.log(`[aiSeo] SEO fields:`, targetFields);
|
|
272
|
+
console.log(`[aiSeo] Title field "${targetFields.title}" is empty:`, titleEmpty);
|
|
273
|
+
console.log(`[aiSeo] Description field "${targetFields.description}" is empty:`, descriptionEmpty);
|
|
274
|
+
const needsFill = titleEmpty || descriptionEmpty;
|
|
275
|
+
if (!needsFill) {
|
|
276
|
+
console.log(`[aiSeo] All SEO fields already filled, skipping AI generation`);
|
|
277
|
+
return data;
|
|
278
|
+
}
|
|
279
|
+
const locale = getLocaleFromRequest(req);
|
|
280
|
+
const defaultLocale = req.payload.config.localization?.defaultLocale ?? "en";
|
|
281
|
+
console.log(`[aiSeo] Request locale: ${locale}, default: ${defaultLocale}`);
|
|
282
|
+
const pageTitle = resolvePageTitle(rawData);
|
|
283
|
+
console.log(`[aiSeo] Page title:`, pageTitle);
|
|
284
|
+
const pageContent = buildPageContent(
|
|
285
|
+
rawData,
|
|
286
|
+
contentFields,
|
|
287
|
+
locale,
|
|
288
|
+
defaultLocale,
|
|
289
|
+
collection?.slug ?? collectionSlug
|
|
290
|
+
);
|
|
291
|
+
const result = await generateSeoWithAi({ pageTitle, pageContent, apiKey, model });
|
|
292
|
+
if (!result) {
|
|
293
|
+
console.error(`[aiSeo] AI generation failed, skipping`);
|
|
294
|
+
return data;
|
|
295
|
+
}
|
|
296
|
+
console.log(`[aiSeo] AI generated:`, result);
|
|
297
|
+
if (titleEmpty) {
|
|
298
|
+
console.log(`[aiSeo] Setting "${targetFields.title}" to:`, result.title);
|
|
299
|
+
setValueByPath(rawData, targetFields.title, result.title);
|
|
300
|
+
}
|
|
301
|
+
if (descriptionEmpty) {
|
|
302
|
+
console.log(`[aiSeo] Setting "${targetFields.description}" to:`, result.description);
|
|
303
|
+
setValueByPath(rawData, targetFields.description, result.description);
|
|
304
|
+
}
|
|
305
|
+
console.log(`[aiSeo] Successfully filled SEO fields`);
|
|
306
|
+
return data;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/plugin.ts
|
|
311
|
+
function normalizeCollections(collections) {
|
|
312
|
+
if (typeof collections === "string") {
|
|
313
|
+
return [{ collection: collections }];
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(collections) && collections.every((c) => typeof c === "string")) {
|
|
316
|
+
return collections.map((collection) => ({
|
|
317
|
+
collection
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
if (Array.isArray(collections)) {
|
|
321
|
+
return collections;
|
|
322
|
+
}
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
function aiSeoPlugin(options) {
|
|
326
|
+
const {
|
|
327
|
+
apiKey,
|
|
328
|
+
collections,
|
|
329
|
+
seoFields: defaultSeoFields,
|
|
330
|
+
contentFields: defaultContentFields
|
|
331
|
+
} = options;
|
|
332
|
+
if (!options.enabled) {
|
|
333
|
+
console.log("[aiSeo] Plugin disabled");
|
|
334
|
+
return (config) => config;
|
|
335
|
+
}
|
|
336
|
+
if (!apiKey?.trim()) {
|
|
337
|
+
if (process.env.NODE_ENV !== "production") {
|
|
338
|
+
console.warn(
|
|
339
|
+
"[aiSeo] Plugin skipped: apiKey is empty. Set OPENAI_API_KEY to enable AI SEO fill."
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return (config) => config;
|
|
343
|
+
}
|
|
344
|
+
const collectionConfigs = normalizeCollections(collections);
|
|
345
|
+
const hooksByCollection = new Map(
|
|
346
|
+
collectionConfigs.map((collectionConfig) => {
|
|
347
|
+
const hook = createBeforeChangeAiSeo(
|
|
348
|
+
{
|
|
349
|
+
...options,
|
|
350
|
+
seoFields: collectionConfig.seoFields ?? defaultSeoFields,
|
|
351
|
+
contentFields: collectionConfig.contentFields ?? defaultContentFields
|
|
352
|
+
},
|
|
353
|
+
collectionConfig.collection
|
|
354
|
+
);
|
|
355
|
+
return [collectionConfig.collection, hook];
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
return (config) => {
|
|
359
|
+
const updatedCollections = config.collections?.map((col) => {
|
|
360
|
+
const hook = hooksByCollection.get(col.slug);
|
|
361
|
+
if (!hook) {
|
|
362
|
+
return col;
|
|
363
|
+
}
|
|
364
|
+
const existingBeforeChange = col.hooks?.beforeChange ?? [];
|
|
365
|
+
return {
|
|
366
|
+
...col,
|
|
367
|
+
hooks: {
|
|
368
|
+
...col.hooks,
|
|
369
|
+
beforeChange: [...existingBeforeChange, hook]
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
...config,
|
|
375
|
+
collections: updatedCollections ?? config.collections
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
exports.aiSeoPlugin = aiSeoPlugin;
|
|
381
|
+
//# sourceMappingURL=index.cjs.map
|
|
382
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generateSeoWithAi.ts","../src/setValueByPath.ts","../src/beforeChangeAiSeo.ts","../src/plugin.ts"],"names":["jsonSchema","openai","createOpenAI","generateObject"],"mappings":"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","file":"index.cjs","sourcesContent":["import { generateObject, jsonSchema } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type { GenerateSeoInput, GenerateSeoResult } from './types'\n\nconst seoSchema = jsonSchema<GenerateSeoResult>({\n type: 'object',\n properties: {\n title: {\n type: 'string',\n description: 'SEO meta title, under 60 characters',\n },\n description: {\n type: 'string',\n description: 'SEO meta description, under 160 characters',\n },\n },\n required: ['title', 'description'],\n additionalProperties: false,\n})\n\nexport async function generateSeoWithAi(\n input: GenerateSeoInput,\n): Promise<GenerateSeoResult | null> {\n const { pageTitle, pageContent, apiKey, model = 'gpt-4o-mini' } = input\n\n console.log(`[aiSeo] Starting AI generation with model: ${model}`)\n\n if (!apiKey?.trim()) {\n console.error('[aiSeo] API key is empty, cannot generate SEO')\n return null\n }\n\n try {\n const openai = createOpenAI({ apiKey })\n\n // Build prompt based on available content\n let prompt = `Generate SEO meta title and meta description for a page. Return only the two strings, no explanations. Meta title must be under 60 characters, meta description under 160 characters.\n\nPage title: ${pageTitle}`\n\n if (pageContent && pageContent.trim()) {\n const contentToSend = pageContent.slice(0, 3000) // Limit content to avoid token limits\n console.log(\n `[aiSeo] Sending ${contentToSend.length} chars of content to AI (trimmed from ${pageContent.length})`,\n )\n prompt += `\n\nPage content:\n${contentToSend}`\n } else {\n console.log(`[aiSeo] No page content available, using only title`)\n }\n\n console.log(`[aiSeo] Full prompt length: ${prompt.length} characters`)\n console.log(`[aiSeo] Prompt preview:`, prompt.slice(0, 200) + '...')\n\n console.log(`[aiSeo] Calling OpenAI API...`)\n const { object } = await generateObject({\n model: openai(model),\n prompt,\n schema: seoSchema,\n })\n\n console.log(`[aiSeo] Raw AI response:`, object)\n\n const result = {\n title: (object.title ?? '').slice(0, 60),\n description: (object.description ?? '').slice(0, 160),\n }\n\n console.log(`[aiSeo] Final result:`, result)\n\n return result\n } catch (err) {\n console.error('[aiSeo] generateSeoWithAi failed:', err)\n if (err instanceof Error) {\n console.error('[aiSeo] Error details:', err.message)\n console.error('[aiSeo] Stack trace:', err.stack)\n }\n return null\n }\n}\n","/**\n * Sets a nested value in an object by dot path (e.g. 'meta.title').\n * Creates intermediate objects as needed.\n */\nexport function setValueByPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.')\n let current: Record<string, unknown> = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i]\n const next = current[key]\n if (next != null && typeof next === 'object' && !Array.isArray(next)) {\n current = next as Record<string, unknown>\n } else {\n const nextObj: Record<string, unknown> = {}\n current[key] = nextObj\n current = nextObj\n }\n }\n current[parts[parts.length - 1]] = value\n}\n\n/**\n * Gets a nested value by dot path. Returns undefined if path is missing.\n */\nexport function getValueByPath(obj: Record<string, unknown>, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n for (const key of parts) {\n if (current == null || typeof current !== 'object' || Array.isArray(current)) {\n return undefined\n }\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n\n/**\n * Returns true if the value is empty (undefined, null, or blank string).\n */\nexport function isEmpty(value: unknown): boolean {\n if (value == null) return true\n if (typeof value === 'string') return value.trim() === ''\n return false\n}\n","import type { CollectionBeforeChangeHook, PayloadRequest } from 'payload'\nimport type { AiSeoPluginOptions, SeoFields } from './types'\nimport { generateSeoWithAi } from './generateSeoWithAi'\nimport { getValueByPath, setValueByPath, isEmpty } from './setValueByPath'\nimport { BaseLocalizationConfig } from 'payload'\n\nconst DEFAULT_CONTENT_FIELDS = ['title']\nconst DEFAULT_SEO_FIELDS: SeoFields = {\n title: 'meta.title',\n description: 'meta.description',\n}\n\n/**\n * Extract locale from request.\n * Priority: req.locale > searchParams > payload config default > 'en'\n */\nfunction getLocaleFromRequest(req: PayloadRequest): string {\n // 1. Try req.locale (set by Payload for localized requests)\n if (req.locale) {\n return req.locale\n }\n\n // 2. Try searchParams (from admin UI)\n const searchLocale = req.searchParams?.get('locale')\n if (searchLocale) {\n return searchLocale\n }\n\n // 3. Try default from Payload config\n const defaultLocale = (req.payload.config.localization as BaseLocalizationConfig)?.defaultLocale\n if (defaultLocale) {\n return defaultLocale\n }\n\n // 4. Fallback to 'en'\n return 'en'\n}\n\n/**\n * Extract value from a potentially localized field.\n * If the value is an object with locale keys, returns the value for the specified locale (or default).\n */\nfunction getLocalizedValue(value: unknown, locale: string, defaultLocale: string): unknown {\n if (value == null || typeof value !== 'object' || Array.isArray(value)) {\n return value\n }\n\n const obj = value as Record<string, unknown>\n\n // Check if this looks like a localized object by checking if it has string keys that match locale pattern\n // Typically locales are 2-letter codes like 'en', 'es', 'fr', etc.\n const keys = Object.keys(obj)\n const hasLocaleKeys = keys.length > 0 && keys.every((key) => /^[a-z]{2}(-[A-Z]{2})?$/.test(key))\n\n if (!hasLocaleKeys) {\n return value\n }\n\n console.log(`[aiSeo] Detected localized field with locales:`, keys)\n\n // Try to get value for the requested locale\n if (locale in obj) {\n console.log(`[aiSeo] Using locale: ${locale}`)\n return obj[locale]\n }\n\n // Fallback to default locale\n if (defaultLocale in obj) {\n console.log(`[aiSeo] Falling back to default locale: ${defaultLocale}`)\n return obj[defaultLocale]\n }\n\n // Return first available value\n const firstKey = keys[0]\n console.log(`[aiSeo] Using first available locale: ${firstKey}`)\n return obj[firstKey]\n}\n\nfunction resolvePageTitle(data: Record<string, unknown>): string {\n const title = data.title\n if (typeof title === 'string' && title.trim()) return title.trim()\n if (title != null && typeof title === 'object' && !Array.isArray(title)) {\n const obj = title as Record<string, unknown>\n const first = Object.values(obj).find((v) => typeof v === 'string' && (v as string).trim())\n return typeof first === 'string' ? first.trim() : 'Page'\n }\n return 'Page'\n}\n\n/**\n * Extract text content from Lexical node recursively.\n */\nfunction extractLexicalText(node: unknown): string {\n if (!node || typeof node !== 'object') {\n return ''\n }\n\n const obj = node as Record<string, unknown>\n const parts: string[] = []\n\n // Handle text nodes\n if (obj.type === 'text' && typeof obj.text === 'string') {\n return obj.text\n }\n\n // Handle nodes with children\n if (Array.isArray(obj.children)) {\n for (const child of obj.children) {\n const text = extractLexicalText(child)\n if (text) {\n parts.push(text)\n }\n }\n }\n\n return parts.join(' ')\n}\n\n/**\n * Extract text content from a value (handles strings, objects, arrays, blocks, richText).\n */\nfunction extractTextContent(\n value: unknown,\n locale: string,\n defaultLocale: string,\n depth = 0,\n): string {\n // Prevent infinite recursion\n if (depth > 10) {\n return ''\n }\n\n if (value == null) {\n return ''\n }\n\n if (typeof value === 'string') {\n return value\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value)\n }\n\n if (Array.isArray(value)) {\n return value\n .map((item) => extractTextContent(item, locale, defaultLocale, depth + 1))\n .filter(Boolean)\n .join('\\n')\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n\n // Handle Lexical editor format (Payload CMS richText)\n if ('root' in obj && obj.root != null) {\n return extractLexicalText(obj.root)\n }\n\n // Handle direct children array (alternative format)\n if ('children' in obj && Array.isArray(obj.children)) {\n return extractLexicalText(obj)\n }\n\n // Handle block types with specific text fields\n if ('blockType' in obj || 'type' in obj) {\n const textFields = ['text', 'content', 'heading', 'label', 'title']\n for (const field of textFields) {\n if (field in obj && obj[field]) {\n const text = extractTextContent(obj[field], locale, defaultLocale, depth + 1)\n if (text) {\n return text\n }\n }\n }\n }\n\n // Check if this might be a localized field (has locale keys like 'en', 'es')\n const localizedValue = getLocalizedValue(obj, locale, defaultLocale)\n if (localizedValue !== obj) {\n // This was a localized field, extract from the resolved value\n return extractTextContent(localizedValue, locale, defaultLocale, depth + 1)\n }\n\n // Extract text from all values in the object\n const texts = Object.entries(obj)\n .filter(([key]) => !key.startsWith('_') && key !== 'id') // Skip meta fields\n .map(([, val]) => extractTextContent(val, locale, defaultLocale, depth + 1))\n .filter(Boolean)\n\n return texts.join(' ')\n }\n\n return ''\n}\n\n/**\n * Build page content from specified field paths.\n */\nfunction buildPageContent(\n data: Record<string, unknown>,\n contentFields: string[],\n locale: string,\n defaultLocale: string,\n collectionSlug?: string,\n): string {\n console.log(`[aiSeo] Building content for collection: ${collectionSlug}`)\n console.log(`[aiSeo] Using locale: ${locale}, default: ${defaultLocale}`)\n console.log(`[aiSeo] Content fields to extract:`, contentFields)\n\n const contentParts: string[] = []\n\n for (const path of contentFields) {\n const value = getValueByPath(data, path)\n console.log(`[aiSeo] Field \"${path}\":`, {\n exists: value != null,\n type: Array.isArray(value) ? 'array' : typeof value,\n arrayLength: Array.isArray(value) ? value.length : undefined,\n hasRoot: value != null && typeof value === 'object' && 'root' in value,\n hasChildren: value != null && typeof value === 'object' && 'children' in value,\n })\n\n if (value != null) {\n const text = extractTextContent(value, locale, defaultLocale).trim()\n if (text) {\n const preview = text.length > 100 ? text.slice(0, 100) + '...' : text\n console.log(`[aiSeo] Extracted ${text.length} chars from \"${path}\":`, preview)\n contentParts.push(text)\n } else {\n console.log(`[aiSeo] No text extracted from \"${path}\"`)\n }\n } else {\n console.log(`[aiSeo] Field \"${path}\" is null/undefined`)\n }\n }\n\n const fullContent = contentParts.join('\\n\\n')\n console.log(`[aiSeo] Total content length: ${fullContent.length} characters`)\n\n return fullContent\n}\n\nexport function createBeforeChangeAiSeo(\n options: AiSeoPluginOptions,\n collectionSlug?: string,\n): CollectionBeforeChangeHook {\n const {\n apiKey,\n seoFields,\n contentFields = DEFAULT_CONTENT_FIELDS,\n model,\n } = options\n\n // Use seoFields if provided, otherwise use default\n const targetFields = seoFields ?? DEFAULT_SEO_FIELDS\n\n return async function beforeChangeAiSeo({ data, operation, collection, req }) {\n console.log(\n `[aiSeo] Hook triggered for collection: ${collection?.slug ?? collectionSlug}, operation: ${operation}`,\n )\n\n if (operation !== 'create') {\n console.log(`[aiSeo] Skipping - operation is \"${operation}\", not \"create\"`)\n return data\n }\n\n const rawData = data as Record<string, unknown>\n\n // Check if SEO fields need filling\n const titleEmpty = isEmpty(getValueByPath(rawData, targetFields.title))\n const descriptionEmpty = isEmpty(getValueByPath(rawData, targetFields.description))\n\n console.log(`[aiSeo] SEO fields:`, targetFields)\n console.log(`[aiSeo] Title field \"${targetFields.title}\" is empty:`, titleEmpty)\n console.log(`[aiSeo] Description field \"${targetFields.description}\" is empty:`, descriptionEmpty)\n\n const needsFill = titleEmpty || descriptionEmpty\n if (!needsFill) {\n console.log(`[aiSeo] All SEO fields already filled, skipping AI generation`)\n return data\n }\n\n // Get locale from request\n const locale = getLocaleFromRequest(req)\n const defaultLocale =\n (req.payload.config.localization as BaseLocalizationConfig)?.defaultLocale ?? 'en'\n\n console.log(`[aiSeo] Request locale: ${locale}, default: ${defaultLocale}`)\n\n const pageTitle = resolvePageTitle(rawData)\n console.log(`[aiSeo] Page title:`, pageTitle)\n\n const pageContent = buildPageContent(\n rawData,\n contentFields,\n locale,\n defaultLocale,\n collection?.slug ?? collectionSlug,\n )\n const result = await generateSeoWithAi({ pageTitle, pageContent, apiKey, model })\n\n if (!result) {\n console.error(`[aiSeo] AI generation failed, skipping`)\n return data\n }\n\n console.log(`[aiSeo] AI generated:`, result)\n\n // Set title if empty\n if (titleEmpty) {\n console.log(`[aiSeo] Setting \"${targetFields.title}\" to:`, result.title)\n setValueByPath(rawData, targetFields.title, result.title)\n }\n\n // Set description if empty\n if (descriptionEmpty) {\n console.log(`[aiSeo] Setting \"${targetFields.description}\" to:`, result.description)\n setValueByPath(rawData, targetFields.description, result.description)\n }\n\n console.log(`[aiSeo] Successfully filled SEO fields`)\n return data\n }\n}\n","import type { Config, Plugin } from 'payload'\nimport type { AiSeoPluginOptions, CollectionConfig } from './types'\nimport { createBeforeChangeAiSeo } from './beforeChangeAiSeo'\n\n/** Normalize collections config to a standard array format. */\nfunction normalizeCollections(\n collections: string | string[] | CollectionConfig[],\n): CollectionConfig[] {\n // Single string: 'page' → [{ collection: 'page' }]\n if (typeof collections === 'string') {\n return [{ collection: collections }]\n }\n\n // Array of strings: ['page', 'posts'] → [{ collection: 'page' }, ...]\n if (Array.isArray(collections) && collections.every((c) => typeof c === 'string')) {\n return (collections as string[]).map((collection) => ({\n collection,\n }))\n }\n\n // Array of configs: already in correct format\n if (Array.isArray(collections)) {\n return collections as CollectionConfig[]\n }\n\n return []\n}\n\nexport function aiSeoPlugin(options: AiSeoPluginOptions): Plugin {\n const {\n apiKey,\n collections,\n seoFields: defaultSeoFields,\n contentFields: defaultContentFields,\n } = options\n\n if (!options.enabled) {\n console.log('[aiSeo] Plugin disabled')\n return (config: Config) => config\n }\n\n if (!apiKey?.trim()) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[aiSeo] Plugin skipped: apiKey is empty. Set OPENAI_API_KEY to enable AI SEO fill.',\n )\n }\n return (config: Config) => config\n }\n\n const collectionConfigs = normalizeCollections(collections)\n\n // Create a map of collection slug → hook\n const hooksByCollection = new Map(\n collectionConfigs.map((collectionConfig) => {\n const hook = createBeforeChangeAiSeo(\n {\n ...options,\n seoFields: collectionConfig.seoFields ?? defaultSeoFields,\n contentFields: collectionConfig.contentFields ?? defaultContentFields,\n },\n collectionConfig.collection,\n )\n return [collectionConfig.collection, hook]\n }),\n )\n\n return (config: Config): Config => {\n const updatedCollections = config.collections?.map((col) => {\n const hook = hooksByCollection.get(col.slug)\n if (!hook) {\n return col\n }\n\n const existingBeforeChange = col.hooks?.beforeChange ?? []\n return {\n ...col,\n hooks: {\n ...col.hooks,\n beforeChange: [...existingBeforeChange, hook],\n },\n }\n })\n\n return {\n ...config,\n collections: updatedCollections ?? config.collections,\n }\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from 'payload';
|
|
2
|
+
import { AiSeoPluginOptions } from './types.cjs';
|
|
3
|
+
export { CollectionConfig, GenerateSeoInput, GenerateSeoResult, SeoFields } from './types.cjs';
|
|
4
|
+
export { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
5
|
+
|
|
6
|
+
declare function aiSeoPlugin(options: AiSeoPluginOptions): Plugin;
|
|
7
|
+
|
|
8
|
+
export { AiSeoPluginOptions, aiSeoPlugin };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Plugin } from 'payload';
|
|
2
|
+
import { AiSeoPluginOptions } from './types.js';
|
|
3
|
+
export { CollectionConfig, GenerateSeoInput, GenerateSeoResult, SeoFields } from './types.js';
|
|
4
|
+
export { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
5
|
+
|
|
6
|
+
declare function aiSeoPlugin(options: AiSeoPluginOptions): Plugin;
|
|
7
|
+
|
|
8
|
+
export { AiSeoPluginOptions, aiSeoPlugin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { jsonSchema, generateObject } from 'ai';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
|
|
4
|
+
// src/generateSeoWithAi.ts
|
|
5
|
+
var seoSchema = jsonSchema({
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
title: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "SEO meta title, under 60 characters"
|
|
11
|
+
},
|
|
12
|
+
description: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "SEO meta description, under 160 characters"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
required: ["title", "description"],
|
|
18
|
+
additionalProperties: false
|
|
19
|
+
});
|
|
20
|
+
async function generateSeoWithAi(input) {
|
|
21
|
+
const { pageTitle, pageContent, apiKey, model = "gpt-4o-mini" } = input;
|
|
22
|
+
console.log(`[aiSeo] Starting AI generation with model: ${model}`);
|
|
23
|
+
if (!apiKey?.trim()) {
|
|
24
|
+
console.error("[aiSeo] API key is empty, cannot generate SEO");
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const openai = createOpenAI({ apiKey });
|
|
29
|
+
let prompt = `Generate SEO meta title and meta description for a page. Return only the two strings, no explanations. Meta title must be under 60 characters, meta description under 160 characters.
|
|
30
|
+
|
|
31
|
+
Page title: ${pageTitle}`;
|
|
32
|
+
if (pageContent && pageContent.trim()) {
|
|
33
|
+
const contentToSend = pageContent.slice(0, 3e3);
|
|
34
|
+
console.log(
|
|
35
|
+
`[aiSeo] Sending ${contentToSend.length} chars of content to AI (trimmed from ${pageContent.length})`
|
|
36
|
+
);
|
|
37
|
+
prompt += `
|
|
38
|
+
|
|
39
|
+
Page content:
|
|
40
|
+
${contentToSend}`;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`[aiSeo] No page content available, using only title`);
|
|
43
|
+
}
|
|
44
|
+
console.log(`[aiSeo] Full prompt length: ${prompt.length} characters`);
|
|
45
|
+
console.log(`[aiSeo] Prompt preview:`, prompt.slice(0, 200) + "...");
|
|
46
|
+
console.log(`[aiSeo] Calling OpenAI API...`);
|
|
47
|
+
const { object } = await generateObject({
|
|
48
|
+
model: openai(model),
|
|
49
|
+
prompt,
|
|
50
|
+
schema: seoSchema
|
|
51
|
+
});
|
|
52
|
+
console.log(`[aiSeo] Raw AI response:`, object);
|
|
53
|
+
const result = {
|
|
54
|
+
title: (object.title ?? "").slice(0, 60),
|
|
55
|
+
description: (object.description ?? "").slice(0, 160)
|
|
56
|
+
};
|
|
57
|
+
console.log(`[aiSeo] Final result:`, result);
|
|
58
|
+
return result;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error("[aiSeo] generateSeoWithAi failed:", err);
|
|
61
|
+
if (err instanceof Error) {
|
|
62
|
+
console.error("[aiSeo] Error details:", err.message);
|
|
63
|
+
console.error("[aiSeo] Stack trace:", err.stack);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/setValueByPath.ts
|
|
70
|
+
function setValueByPath(obj, path, value) {
|
|
71
|
+
const parts = path.split(".");
|
|
72
|
+
let current = obj;
|
|
73
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
74
|
+
const key = parts[i];
|
|
75
|
+
const next = current[key];
|
|
76
|
+
if (next != null && typeof next === "object" && !Array.isArray(next)) {
|
|
77
|
+
current = next;
|
|
78
|
+
} else {
|
|
79
|
+
const nextObj = {};
|
|
80
|
+
current[key] = nextObj;
|
|
81
|
+
current = nextObj;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
current[parts[parts.length - 1]] = value;
|
|
85
|
+
}
|
|
86
|
+
function getValueByPath(obj, path) {
|
|
87
|
+
const parts = path.split(".");
|
|
88
|
+
let current = obj;
|
|
89
|
+
for (const key of parts) {
|
|
90
|
+
if (current == null || typeof current !== "object" || Array.isArray(current)) {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
current = current[key];
|
|
94
|
+
}
|
|
95
|
+
return current;
|
|
96
|
+
}
|
|
97
|
+
function isEmpty(value) {
|
|
98
|
+
if (value == null) return true;
|
|
99
|
+
if (typeof value === "string") return value.trim() === "";
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/beforeChangeAiSeo.ts
|
|
104
|
+
var DEFAULT_CONTENT_FIELDS = ["title"];
|
|
105
|
+
var DEFAULT_SEO_FIELDS = {
|
|
106
|
+
title: "meta.title",
|
|
107
|
+
description: "meta.description"
|
|
108
|
+
};
|
|
109
|
+
function getLocaleFromRequest(req) {
|
|
110
|
+
if (req.locale) {
|
|
111
|
+
return req.locale;
|
|
112
|
+
}
|
|
113
|
+
const searchLocale = req.searchParams?.get("locale");
|
|
114
|
+
if (searchLocale) {
|
|
115
|
+
return searchLocale;
|
|
116
|
+
}
|
|
117
|
+
const defaultLocale = req.payload.config.localization?.defaultLocale;
|
|
118
|
+
if (defaultLocale) {
|
|
119
|
+
return defaultLocale;
|
|
120
|
+
}
|
|
121
|
+
return "en";
|
|
122
|
+
}
|
|
123
|
+
function getLocalizedValue(value, locale, defaultLocale) {
|
|
124
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
const obj = value;
|
|
128
|
+
const keys = Object.keys(obj);
|
|
129
|
+
const hasLocaleKeys = keys.length > 0 && keys.every((key) => /^[a-z]{2}(-[A-Z]{2})?$/.test(key));
|
|
130
|
+
if (!hasLocaleKeys) {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
console.log(`[aiSeo] Detected localized field with locales:`, keys);
|
|
134
|
+
if (locale in obj) {
|
|
135
|
+
console.log(`[aiSeo] Using locale: ${locale}`);
|
|
136
|
+
return obj[locale];
|
|
137
|
+
}
|
|
138
|
+
if (defaultLocale in obj) {
|
|
139
|
+
console.log(`[aiSeo] Falling back to default locale: ${defaultLocale}`);
|
|
140
|
+
return obj[defaultLocale];
|
|
141
|
+
}
|
|
142
|
+
const firstKey = keys[0];
|
|
143
|
+
console.log(`[aiSeo] Using first available locale: ${firstKey}`);
|
|
144
|
+
return obj[firstKey];
|
|
145
|
+
}
|
|
146
|
+
function resolvePageTitle(data) {
|
|
147
|
+
const title = data.title;
|
|
148
|
+
if (typeof title === "string" && title.trim()) return title.trim();
|
|
149
|
+
if (title != null && typeof title === "object" && !Array.isArray(title)) {
|
|
150
|
+
const obj = title;
|
|
151
|
+
const first = Object.values(obj).find((v) => typeof v === "string" && v.trim());
|
|
152
|
+
return typeof first === "string" ? first.trim() : "Page";
|
|
153
|
+
}
|
|
154
|
+
return "Page";
|
|
155
|
+
}
|
|
156
|
+
function extractLexicalText(node) {
|
|
157
|
+
if (!node || typeof node !== "object") {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
const obj = node;
|
|
161
|
+
const parts = [];
|
|
162
|
+
if (obj.type === "text" && typeof obj.text === "string") {
|
|
163
|
+
return obj.text;
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(obj.children)) {
|
|
166
|
+
for (const child of obj.children) {
|
|
167
|
+
const text = extractLexicalText(child);
|
|
168
|
+
if (text) {
|
|
169
|
+
parts.push(text);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return parts.join(" ");
|
|
174
|
+
}
|
|
175
|
+
function extractTextContent(value, locale, defaultLocale, depth = 0) {
|
|
176
|
+
if (depth > 10) {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
if (value == null) {
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
if (typeof value === "string") {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
186
|
+
return String(value);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return value.map((item) => extractTextContent(item, locale, defaultLocale, depth + 1)).filter(Boolean).join("\n");
|
|
190
|
+
}
|
|
191
|
+
if (typeof value === "object") {
|
|
192
|
+
const obj = value;
|
|
193
|
+
if ("root" in obj && obj.root != null) {
|
|
194
|
+
return extractLexicalText(obj.root);
|
|
195
|
+
}
|
|
196
|
+
if ("children" in obj && Array.isArray(obj.children)) {
|
|
197
|
+
return extractLexicalText(obj);
|
|
198
|
+
}
|
|
199
|
+
if ("blockType" in obj || "type" in obj) {
|
|
200
|
+
const textFields = ["text", "content", "heading", "label", "title"];
|
|
201
|
+
for (const field of textFields) {
|
|
202
|
+
if (field in obj && obj[field]) {
|
|
203
|
+
const text = extractTextContent(obj[field], locale, defaultLocale, depth + 1);
|
|
204
|
+
if (text) {
|
|
205
|
+
return text;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const localizedValue = getLocalizedValue(obj, locale, defaultLocale);
|
|
211
|
+
if (localizedValue !== obj) {
|
|
212
|
+
return extractTextContent(localizedValue, locale, defaultLocale, depth + 1);
|
|
213
|
+
}
|
|
214
|
+
const texts = Object.entries(obj).filter(([key]) => !key.startsWith("_") && key !== "id").map(([, val]) => extractTextContent(val, locale, defaultLocale, depth + 1)).filter(Boolean);
|
|
215
|
+
return texts.join(" ");
|
|
216
|
+
}
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
function buildPageContent(data, contentFields, locale, defaultLocale, collectionSlug) {
|
|
220
|
+
console.log(`[aiSeo] Building content for collection: ${collectionSlug}`);
|
|
221
|
+
console.log(`[aiSeo] Using locale: ${locale}, default: ${defaultLocale}`);
|
|
222
|
+
console.log(`[aiSeo] Content fields to extract:`, contentFields);
|
|
223
|
+
const contentParts = [];
|
|
224
|
+
for (const path of contentFields) {
|
|
225
|
+
const value = getValueByPath(data, path);
|
|
226
|
+
console.log(`[aiSeo] Field "${path}":`, {
|
|
227
|
+
exists: value != null,
|
|
228
|
+
type: Array.isArray(value) ? "array" : typeof value,
|
|
229
|
+
arrayLength: Array.isArray(value) ? value.length : void 0,
|
|
230
|
+
hasRoot: value != null && typeof value === "object" && "root" in value,
|
|
231
|
+
hasChildren: value != null && typeof value === "object" && "children" in value
|
|
232
|
+
});
|
|
233
|
+
if (value != null) {
|
|
234
|
+
const text = extractTextContent(value, locale, defaultLocale).trim();
|
|
235
|
+
if (text) {
|
|
236
|
+
const preview = text.length > 100 ? text.slice(0, 100) + "..." : text;
|
|
237
|
+
console.log(`[aiSeo] Extracted ${text.length} chars from "${path}":`, preview);
|
|
238
|
+
contentParts.push(text);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(`[aiSeo] No text extracted from "${path}"`);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`[aiSeo] Field "${path}" is null/undefined`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const fullContent = contentParts.join("\n\n");
|
|
247
|
+
console.log(`[aiSeo] Total content length: ${fullContent.length} characters`);
|
|
248
|
+
return fullContent;
|
|
249
|
+
}
|
|
250
|
+
function createBeforeChangeAiSeo(options, collectionSlug) {
|
|
251
|
+
const {
|
|
252
|
+
apiKey,
|
|
253
|
+
seoFields,
|
|
254
|
+
contentFields = DEFAULT_CONTENT_FIELDS,
|
|
255
|
+
model
|
|
256
|
+
} = options;
|
|
257
|
+
const targetFields = seoFields ?? DEFAULT_SEO_FIELDS;
|
|
258
|
+
return async function beforeChangeAiSeo({ data, operation, collection, req }) {
|
|
259
|
+
console.log(
|
|
260
|
+
`[aiSeo] Hook triggered for collection: ${collection?.slug ?? collectionSlug}, operation: ${operation}`
|
|
261
|
+
);
|
|
262
|
+
if (operation !== "create") {
|
|
263
|
+
console.log(`[aiSeo] Skipping - operation is "${operation}", not "create"`);
|
|
264
|
+
return data;
|
|
265
|
+
}
|
|
266
|
+
const rawData = data;
|
|
267
|
+
const titleEmpty = isEmpty(getValueByPath(rawData, targetFields.title));
|
|
268
|
+
const descriptionEmpty = isEmpty(getValueByPath(rawData, targetFields.description));
|
|
269
|
+
console.log(`[aiSeo] SEO fields:`, targetFields);
|
|
270
|
+
console.log(`[aiSeo] Title field "${targetFields.title}" is empty:`, titleEmpty);
|
|
271
|
+
console.log(`[aiSeo] Description field "${targetFields.description}" is empty:`, descriptionEmpty);
|
|
272
|
+
const needsFill = titleEmpty || descriptionEmpty;
|
|
273
|
+
if (!needsFill) {
|
|
274
|
+
console.log(`[aiSeo] All SEO fields already filled, skipping AI generation`);
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
const locale = getLocaleFromRequest(req);
|
|
278
|
+
const defaultLocale = req.payload.config.localization?.defaultLocale ?? "en";
|
|
279
|
+
console.log(`[aiSeo] Request locale: ${locale}, default: ${defaultLocale}`);
|
|
280
|
+
const pageTitle = resolvePageTitle(rawData);
|
|
281
|
+
console.log(`[aiSeo] Page title:`, pageTitle);
|
|
282
|
+
const pageContent = buildPageContent(
|
|
283
|
+
rawData,
|
|
284
|
+
contentFields,
|
|
285
|
+
locale,
|
|
286
|
+
defaultLocale,
|
|
287
|
+
collection?.slug ?? collectionSlug
|
|
288
|
+
);
|
|
289
|
+
const result = await generateSeoWithAi({ pageTitle, pageContent, apiKey, model });
|
|
290
|
+
if (!result) {
|
|
291
|
+
console.error(`[aiSeo] AI generation failed, skipping`);
|
|
292
|
+
return data;
|
|
293
|
+
}
|
|
294
|
+
console.log(`[aiSeo] AI generated:`, result);
|
|
295
|
+
if (titleEmpty) {
|
|
296
|
+
console.log(`[aiSeo] Setting "${targetFields.title}" to:`, result.title);
|
|
297
|
+
setValueByPath(rawData, targetFields.title, result.title);
|
|
298
|
+
}
|
|
299
|
+
if (descriptionEmpty) {
|
|
300
|
+
console.log(`[aiSeo] Setting "${targetFields.description}" to:`, result.description);
|
|
301
|
+
setValueByPath(rawData, targetFields.description, result.description);
|
|
302
|
+
}
|
|
303
|
+
console.log(`[aiSeo] Successfully filled SEO fields`);
|
|
304
|
+
return data;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/plugin.ts
|
|
309
|
+
function normalizeCollections(collections) {
|
|
310
|
+
if (typeof collections === "string") {
|
|
311
|
+
return [{ collection: collections }];
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(collections) && collections.every((c) => typeof c === "string")) {
|
|
314
|
+
return collections.map((collection) => ({
|
|
315
|
+
collection
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(collections)) {
|
|
319
|
+
return collections;
|
|
320
|
+
}
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
function aiSeoPlugin(options) {
|
|
324
|
+
const {
|
|
325
|
+
apiKey,
|
|
326
|
+
collections,
|
|
327
|
+
seoFields: defaultSeoFields,
|
|
328
|
+
contentFields: defaultContentFields
|
|
329
|
+
} = options;
|
|
330
|
+
if (!options.enabled) {
|
|
331
|
+
console.log("[aiSeo] Plugin disabled");
|
|
332
|
+
return (config) => config;
|
|
333
|
+
}
|
|
334
|
+
if (!apiKey?.trim()) {
|
|
335
|
+
if (process.env.NODE_ENV !== "production") {
|
|
336
|
+
console.warn(
|
|
337
|
+
"[aiSeo] Plugin skipped: apiKey is empty. Set OPENAI_API_KEY to enable AI SEO fill."
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return (config) => config;
|
|
341
|
+
}
|
|
342
|
+
const collectionConfigs = normalizeCollections(collections);
|
|
343
|
+
const hooksByCollection = new Map(
|
|
344
|
+
collectionConfigs.map((collectionConfig) => {
|
|
345
|
+
const hook = createBeforeChangeAiSeo(
|
|
346
|
+
{
|
|
347
|
+
...options,
|
|
348
|
+
seoFields: collectionConfig.seoFields ?? defaultSeoFields,
|
|
349
|
+
contentFields: collectionConfig.contentFields ?? defaultContentFields
|
|
350
|
+
},
|
|
351
|
+
collectionConfig.collection
|
|
352
|
+
);
|
|
353
|
+
return [collectionConfig.collection, hook];
|
|
354
|
+
})
|
|
355
|
+
);
|
|
356
|
+
return (config) => {
|
|
357
|
+
const updatedCollections = config.collections?.map((col) => {
|
|
358
|
+
const hook = hooksByCollection.get(col.slug);
|
|
359
|
+
if (!hook) {
|
|
360
|
+
return col;
|
|
361
|
+
}
|
|
362
|
+
const existingBeforeChange = col.hooks?.beforeChange ?? [];
|
|
363
|
+
return {
|
|
364
|
+
...col,
|
|
365
|
+
hooks: {
|
|
366
|
+
...col.hooks,
|
|
367
|
+
beforeChange: [...existingBeforeChange, hook]
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
...config,
|
|
373
|
+
collections: updatedCollections ?? config.collections
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export { aiSeoPlugin };
|
|
379
|
+
//# sourceMappingURL=index.js.map
|
|
380
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generateSeoWithAi.ts","../src/setValueByPath.ts","../src/beforeChangeAiSeo.ts","../src/plugin.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { generateObject, jsonSchema } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type { GenerateSeoInput, GenerateSeoResult } from './types'\n\nconst seoSchema = jsonSchema<GenerateSeoResult>({\n type: 'object',\n properties: {\n title: {\n type: 'string',\n description: 'SEO meta title, under 60 characters',\n },\n description: {\n type: 'string',\n description: 'SEO meta description, under 160 characters',\n },\n },\n required: ['title', 'description'],\n additionalProperties: false,\n})\n\nexport async function generateSeoWithAi(\n input: GenerateSeoInput,\n): Promise<GenerateSeoResult | null> {\n const { pageTitle, pageContent, apiKey, model = 'gpt-4o-mini' } = input\n\n console.log(`[aiSeo] Starting AI generation with model: ${model}`)\n\n if (!apiKey?.trim()) {\n console.error('[aiSeo] API key is empty, cannot generate SEO')\n return null\n }\n\n try {\n const openai = createOpenAI({ apiKey })\n\n // Build prompt based on available content\n let prompt = `Generate SEO meta title and meta description for a page. Return only the two strings, no explanations. Meta title must be under 60 characters, meta description under 160 characters.\n\nPage title: ${pageTitle}`\n\n if (pageContent && pageContent.trim()) {\n const contentToSend = pageContent.slice(0, 3000) // Limit content to avoid token limits\n console.log(\n `[aiSeo] Sending ${contentToSend.length} chars of content to AI (trimmed from ${pageContent.length})`,\n )\n prompt += `\n\nPage content:\n${contentToSend}`\n } else {\n console.log(`[aiSeo] No page content available, using only title`)\n }\n\n console.log(`[aiSeo] Full prompt length: ${prompt.length} characters`)\n console.log(`[aiSeo] Prompt preview:`, prompt.slice(0, 200) + '...')\n\n console.log(`[aiSeo] Calling OpenAI API...`)\n const { object } = await generateObject({\n model: openai(model),\n prompt,\n schema: seoSchema,\n })\n\n console.log(`[aiSeo] Raw AI response:`, object)\n\n const result = {\n title: (object.title ?? '').slice(0, 60),\n description: (object.description ?? '').slice(0, 160),\n }\n\n console.log(`[aiSeo] Final result:`, result)\n\n return result\n } catch (err) {\n console.error('[aiSeo] generateSeoWithAi failed:', err)\n if (err instanceof Error) {\n console.error('[aiSeo] Error details:', err.message)\n console.error('[aiSeo] Stack trace:', err.stack)\n }\n return null\n }\n}\n","/**\n * Sets a nested value in an object by dot path (e.g. 'meta.title').\n * Creates intermediate objects as needed.\n */\nexport function setValueByPath(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.')\n let current: Record<string, unknown> = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i]\n const next = current[key]\n if (next != null && typeof next === 'object' && !Array.isArray(next)) {\n current = next as Record<string, unknown>\n } else {\n const nextObj: Record<string, unknown> = {}\n current[key] = nextObj\n current = nextObj\n }\n }\n current[parts[parts.length - 1]] = value\n}\n\n/**\n * Gets a nested value by dot path. Returns undefined if path is missing.\n */\nexport function getValueByPath(obj: Record<string, unknown>, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n for (const key of parts) {\n if (current == null || typeof current !== 'object' || Array.isArray(current)) {\n return undefined\n }\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n\n/**\n * Returns true if the value is empty (undefined, null, or blank string).\n */\nexport function isEmpty(value: unknown): boolean {\n if (value == null) return true\n if (typeof value === 'string') return value.trim() === ''\n return false\n}\n","import type { CollectionBeforeChangeHook, PayloadRequest } from 'payload'\nimport type { AiSeoPluginOptions, SeoFields } from './types'\nimport { generateSeoWithAi } from './generateSeoWithAi'\nimport { getValueByPath, setValueByPath, isEmpty } from './setValueByPath'\nimport { BaseLocalizationConfig } from 'payload'\n\nconst DEFAULT_CONTENT_FIELDS = ['title']\nconst DEFAULT_SEO_FIELDS: SeoFields = {\n title: 'meta.title',\n description: 'meta.description',\n}\n\n/**\n * Extract locale from request.\n * Priority: req.locale > searchParams > payload config default > 'en'\n */\nfunction getLocaleFromRequest(req: PayloadRequest): string {\n // 1. Try req.locale (set by Payload for localized requests)\n if (req.locale) {\n return req.locale\n }\n\n // 2. Try searchParams (from admin UI)\n const searchLocale = req.searchParams?.get('locale')\n if (searchLocale) {\n return searchLocale\n }\n\n // 3. Try default from Payload config\n const defaultLocale = (req.payload.config.localization as BaseLocalizationConfig)?.defaultLocale\n if (defaultLocale) {\n return defaultLocale\n }\n\n // 4. Fallback to 'en'\n return 'en'\n}\n\n/**\n * Extract value from a potentially localized field.\n * If the value is an object with locale keys, returns the value for the specified locale (or default).\n */\nfunction getLocalizedValue(value: unknown, locale: string, defaultLocale: string): unknown {\n if (value == null || typeof value !== 'object' || Array.isArray(value)) {\n return value\n }\n\n const obj = value as Record<string, unknown>\n\n // Check if this looks like a localized object by checking if it has string keys that match locale pattern\n // Typically locales are 2-letter codes like 'en', 'es', 'fr', etc.\n const keys = Object.keys(obj)\n const hasLocaleKeys = keys.length > 0 && keys.every((key) => /^[a-z]{2}(-[A-Z]{2})?$/.test(key))\n\n if (!hasLocaleKeys) {\n return value\n }\n\n console.log(`[aiSeo] Detected localized field with locales:`, keys)\n\n // Try to get value for the requested locale\n if (locale in obj) {\n console.log(`[aiSeo] Using locale: ${locale}`)\n return obj[locale]\n }\n\n // Fallback to default locale\n if (defaultLocale in obj) {\n console.log(`[aiSeo] Falling back to default locale: ${defaultLocale}`)\n return obj[defaultLocale]\n }\n\n // Return first available value\n const firstKey = keys[0]\n console.log(`[aiSeo] Using first available locale: ${firstKey}`)\n return obj[firstKey]\n}\n\nfunction resolvePageTitle(data: Record<string, unknown>): string {\n const title = data.title\n if (typeof title === 'string' && title.trim()) return title.trim()\n if (title != null && typeof title === 'object' && !Array.isArray(title)) {\n const obj = title as Record<string, unknown>\n const first = Object.values(obj).find((v) => typeof v === 'string' && (v as string).trim())\n return typeof first === 'string' ? first.trim() : 'Page'\n }\n return 'Page'\n}\n\n/**\n * Extract text content from Lexical node recursively.\n */\nfunction extractLexicalText(node: unknown): string {\n if (!node || typeof node !== 'object') {\n return ''\n }\n\n const obj = node as Record<string, unknown>\n const parts: string[] = []\n\n // Handle text nodes\n if (obj.type === 'text' && typeof obj.text === 'string') {\n return obj.text\n }\n\n // Handle nodes with children\n if (Array.isArray(obj.children)) {\n for (const child of obj.children) {\n const text = extractLexicalText(child)\n if (text) {\n parts.push(text)\n }\n }\n }\n\n return parts.join(' ')\n}\n\n/**\n * Extract text content from a value (handles strings, objects, arrays, blocks, richText).\n */\nfunction extractTextContent(\n value: unknown,\n locale: string,\n defaultLocale: string,\n depth = 0,\n): string {\n // Prevent infinite recursion\n if (depth > 10) {\n return ''\n }\n\n if (value == null) {\n return ''\n }\n\n if (typeof value === 'string') {\n return value\n }\n\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value)\n }\n\n if (Array.isArray(value)) {\n return value\n .map((item) => extractTextContent(item, locale, defaultLocale, depth + 1))\n .filter(Boolean)\n .join('\\n')\n }\n\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n\n // Handle Lexical editor format (Payload CMS richText)\n if ('root' in obj && obj.root != null) {\n return extractLexicalText(obj.root)\n }\n\n // Handle direct children array (alternative format)\n if ('children' in obj && Array.isArray(obj.children)) {\n return extractLexicalText(obj)\n }\n\n // Handle block types with specific text fields\n if ('blockType' in obj || 'type' in obj) {\n const textFields = ['text', 'content', 'heading', 'label', 'title']\n for (const field of textFields) {\n if (field in obj && obj[field]) {\n const text = extractTextContent(obj[field], locale, defaultLocale, depth + 1)\n if (text) {\n return text\n }\n }\n }\n }\n\n // Check if this might be a localized field (has locale keys like 'en', 'es')\n const localizedValue = getLocalizedValue(obj, locale, defaultLocale)\n if (localizedValue !== obj) {\n // This was a localized field, extract from the resolved value\n return extractTextContent(localizedValue, locale, defaultLocale, depth + 1)\n }\n\n // Extract text from all values in the object\n const texts = Object.entries(obj)\n .filter(([key]) => !key.startsWith('_') && key !== 'id') // Skip meta fields\n .map(([, val]) => extractTextContent(val, locale, defaultLocale, depth + 1))\n .filter(Boolean)\n\n return texts.join(' ')\n }\n\n return ''\n}\n\n/**\n * Build page content from specified field paths.\n */\nfunction buildPageContent(\n data: Record<string, unknown>,\n contentFields: string[],\n locale: string,\n defaultLocale: string,\n collectionSlug?: string,\n): string {\n console.log(`[aiSeo] Building content for collection: ${collectionSlug}`)\n console.log(`[aiSeo] Using locale: ${locale}, default: ${defaultLocale}`)\n console.log(`[aiSeo] Content fields to extract:`, contentFields)\n\n const contentParts: string[] = []\n\n for (const path of contentFields) {\n const value = getValueByPath(data, path)\n console.log(`[aiSeo] Field \"${path}\":`, {\n exists: value != null,\n type: Array.isArray(value) ? 'array' : typeof value,\n arrayLength: Array.isArray(value) ? value.length : undefined,\n hasRoot: value != null && typeof value === 'object' && 'root' in value,\n hasChildren: value != null && typeof value === 'object' && 'children' in value,\n })\n\n if (value != null) {\n const text = extractTextContent(value, locale, defaultLocale).trim()\n if (text) {\n const preview = text.length > 100 ? text.slice(0, 100) + '...' : text\n console.log(`[aiSeo] Extracted ${text.length} chars from \"${path}\":`, preview)\n contentParts.push(text)\n } else {\n console.log(`[aiSeo] No text extracted from \"${path}\"`)\n }\n } else {\n console.log(`[aiSeo] Field \"${path}\" is null/undefined`)\n }\n }\n\n const fullContent = contentParts.join('\\n\\n')\n console.log(`[aiSeo] Total content length: ${fullContent.length} characters`)\n\n return fullContent\n}\n\nexport function createBeforeChangeAiSeo(\n options: AiSeoPluginOptions,\n collectionSlug?: string,\n): CollectionBeforeChangeHook {\n const {\n apiKey,\n seoFields,\n contentFields = DEFAULT_CONTENT_FIELDS,\n model,\n } = options\n\n // Use seoFields if provided, otherwise use default\n const targetFields = seoFields ?? DEFAULT_SEO_FIELDS\n\n return async function beforeChangeAiSeo({ data, operation, collection, req }) {\n console.log(\n `[aiSeo] Hook triggered for collection: ${collection?.slug ?? collectionSlug}, operation: ${operation}`,\n )\n\n if (operation !== 'create') {\n console.log(`[aiSeo] Skipping - operation is \"${operation}\", not \"create\"`)\n return data\n }\n\n const rawData = data as Record<string, unknown>\n\n // Check if SEO fields need filling\n const titleEmpty = isEmpty(getValueByPath(rawData, targetFields.title))\n const descriptionEmpty = isEmpty(getValueByPath(rawData, targetFields.description))\n\n console.log(`[aiSeo] SEO fields:`, targetFields)\n console.log(`[aiSeo] Title field \"${targetFields.title}\" is empty:`, titleEmpty)\n console.log(`[aiSeo] Description field \"${targetFields.description}\" is empty:`, descriptionEmpty)\n\n const needsFill = titleEmpty || descriptionEmpty\n if (!needsFill) {\n console.log(`[aiSeo] All SEO fields already filled, skipping AI generation`)\n return data\n }\n\n // Get locale from request\n const locale = getLocaleFromRequest(req)\n const defaultLocale =\n (req.payload.config.localization as BaseLocalizationConfig)?.defaultLocale ?? 'en'\n\n console.log(`[aiSeo] Request locale: ${locale}, default: ${defaultLocale}`)\n\n const pageTitle = resolvePageTitle(rawData)\n console.log(`[aiSeo] Page title:`, pageTitle)\n\n const pageContent = buildPageContent(\n rawData,\n contentFields,\n locale,\n defaultLocale,\n collection?.slug ?? collectionSlug,\n )\n const result = await generateSeoWithAi({ pageTitle, pageContent, apiKey, model })\n\n if (!result) {\n console.error(`[aiSeo] AI generation failed, skipping`)\n return data\n }\n\n console.log(`[aiSeo] AI generated:`, result)\n\n // Set title if empty\n if (titleEmpty) {\n console.log(`[aiSeo] Setting \"${targetFields.title}\" to:`, result.title)\n setValueByPath(rawData, targetFields.title, result.title)\n }\n\n // Set description if empty\n if (descriptionEmpty) {\n console.log(`[aiSeo] Setting \"${targetFields.description}\" to:`, result.description)\n setValueByPath(rawData, targetFields.description, result.description)\n }\n\n console.log(`[aiSeo] Successfully filled SEO fields`)\n return data\n }\n}\n","import type { Config, Plugin } from 'payload'\nimport type { AiSeoPluginOptions, CollectionConfig } from './types'\nimport { createBeforeChangeAiSeo } from './beforeChangeAiSeo'\n\n/** Normalize collections config to a standard array format. */\nfunction normalizeCollections(\n collections: string | string[] | CollectionConfig[],\n): CollectionConfig[] {\n // Single string: 'page' → [{ collection: 'page' }]\n if (typeof collections === 'string') {\n return [{ collection: collections }]\n }\n\n // Array of strings: ['page', 'posts'] → [{ collection: 'page' }, ...]\n if (Array.isArray(collections) && collections.every((c) => typeof c === 'string')) {\n return (collections as string[]).map((collection) => ({\n collection,\n }))\n }\n\n // Array of configs: already in correct format\n if (Array.isArray(collections)) {\n return collections as CollectionConfig[]\n }\n\n return []\n}\n\nexport function aiSeoPlugin(options: AiSeoPluginOptions): Plugin {\n const {\n apiKey,\n collections,\n seoFields: defaultSeoFields,\n contentFields: defaultContentFields,\n } = options\n\n if (!options.enabled) {\n console.log('[aiSeo] Plugin disabled')\n return (config: Config) => config\n }\n\n if (!apiKey?.trim()) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[aiSeo] Plugin skipped: apiKey is empty. Set OPENAI_API_KEY to enable AI SEO fill.',\n )\n }\n return (config: Config) => config\n }\n\n const collectionConfigs = normalizeCollections(collections)\n\n // Create a map of collection slug → hook\n const hooksByCollection = new Map(\n collectionConfigs.map((collectionConfig) => {\n const hook = createBeforeChangeAiSeo(\n {\n ...options,\n seoFields: collectionConfig.seoFields ?? defaultSeoFields,\n contentFields: collectionConfig.contentFields ?? defaultContentFields,\n },\n collectionConfig.collection,\n )\n return [collectionConfig.collection, hook]\n }),\n )\n\n return (config: Config): Config => {\n const updatedCollections = config.collections?.map((col) => {\n const hook = hooksByCollection.get(col.slug)\n if (!hook) {\n return col\n }\n\n const existingBeforeChange = col.hooks?.beforeChange ?? []\n return {\n ...col,\n hooks: {\n ...col.hooks,\n beforeChange: [...existingBeforeChange, hook],\n },\n }\n })\n\n return {\n ...config,\n collections: updatedCollections ?? config.collections,\n }\n }\n}\n"]}
|
package/dist/types.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.cjs"}
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
2
|
+
export { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
3
|
+
|
|
4
|
+
/** SEO fields mapping - specifies where to write AI-generated SEO data. */
|
|
5
|
+
interface SeoFields {
|
|
6
|
+
/** Path where to write the generated title (e.g. 'meta.title', 'seo.metaTitle', 'test1'). */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Path where to write the generated description (e.g. 'meta.description', 'seo.metaDesc', 'test2'). */
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
/** Configuration for a single collection. */
|
|
12
|
+
interface CollectionConfig {
|
|
13
|
+
/** Collection slug (e.g. 'page'). */
|
|
14
|
+
collection: string;
|
|
15
|
+
/**
|
|
16
|
+
* SEO fields mapping.
|
|
17
|
+
* Explicitly specifies where to write title and description.
|
|
18
|
+
* Example: { title: 'meta.title', description: 'meta.description' }
|
|
19
|
+
* Or with custom field names: { title: 'test1', description: 'test2' }
|
|
20
|
+
*/
|
|
21
|
+
seoFields?: SeoFields;
|
|
22
|
+
/**
|
|
23
|
+
* Field paths to extract content from for AI analysis (e.g. ['title', 'content', 'excerpt']).
|
|
24
|
+
* If not specified, only 'title' field will be used.
|
|
25
|
+
* The AI will analyze all these fields to generate better SEO meta tags.
|
|
26
|
+
*/
|
|
27
|
+
contentFields?: string[];
|
|
28
|
+
}
|
|
29
|
+
interface AiSeoPluginOptions {
|
|
30
|
+
/** Whether to enable the plugin. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** API key for the AI provider (e.g. OpenAI). Prefer passing from process.env. */
|
|
33
|
+
apiKey: string;
|
|
34
|
+
/**
|
|
35
|
+
* Collection(s) to attach the hook to. Can be:
|
|
36
|
+
* - A single collection slug: 'page'
|
|
37
|
+
* - An array of collection slugs: ['page', 'posts']
|
|
38
|
+
* - An array of collection configs with custom fields:
|
|
39
|
+
* [{ collection: 'page', seoFields: { title: 'meta.title', description: 'meta.description' } }]
|
|
40
|
+
*/
|
|
41
|
+
collections: string | string[] | CollectionConfig[];
|
|
42
|
+
/**
|
|
43
|
+
* Default SEO fields mapping.
|
|
44
|
+
* Used when collections don't specify their own seoFields.
|
|
45
|
+
* Example: { title: 'meta.title', description: 'meta.description' }
|
|
46
|
+
*/
|
|
47
|
+
seoFields?: SeoFields;
|
|
48
|
+
/**
|
|
49
|
+
* Default field paths to extract content from for AI analysis (e.g. ['title', 'content', 'excerpt']).
|
|
50
|
+
* Used when collections are specified without individual contentFields config.
|
|
51
|
+
* If not specified, only 'title' field will be used.
|
|
52
|
+
*/
|
|
53
|
+
contentFields?: string[];
|
|
54
|
+
/** OpenAI chat model for generateObject (e.g. 'gpt-4o-mini'). */
|
|
55
|
+
model?: OpenAIChatModelId;
|
|
56
|
+
}
|
|
57
|
+
interface GenerateSeoInput {
|
|
58
|
+
pageTitle: string;
|
|
59
|
+
/** Full page content for AI analysis. Can include body text, excerpts, etc. */
|
|
60
|
+
pageContent?: string;
|
|
61
|
+
apiKey: string;
|
|
62
|
+
model?: OpenAIChatModelId;
|
|
63
|
+
}
|
|
64
|
+
interface GenerateSeoResult {
|
|
65
|
+
title: string;
|
|
66
|
+
description: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type { AiSeoPluginOptions, CollectionConfig, GenerateSeoInput, GenerateSeoResult, SeoFields };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
2
|
+
export { OpenAIChatModelId } from '@ai-sdk/openai/internal';
|
|
3
|
+
|
|
4
|
+
/** SEO fields mapping - specifies where to write AI-generated SEO data. */
|
|
5
|
+
interface SeoFields {
|
|
6
|
+
/** Path where to write the generated title (e.g. 'meta.title', 'seo.metaTitle', 'test1'). */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Path where to write the generated description (e.g. 'meta.description', 'seo.metaDesc', 'test2'). */
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
/** Configuration for a single collection. */
|
|
12
|
+
interface CollectionConfig {
|
|
13
|
+
/** Collection slug (e.g. 'page'). */
|
|
14
|
+
collection: string;
|
|
15
|
+
/**
|
|
16
|
+
* SEO fields mapping.
|
|
17
|
+
* Explicitly specifies where to write title and description.
|
|
18
|
+
* Example: { title: 'meta.title', description: 'meta.description' }
|
|
19
|
+
* Or with custom field names: { title: 'test1', description: 'test2' }
|
|
20
|
+
*/
|
|
21
|
+
seoFields?: SeoFields;
|
|
22
|
+
/**
|
|
23
|
+
* Field paths to extract content from for AI analysis (e.g. ['title', 'content', 'excerpt']).
|
|
24
|
+
* If not specified, only 'title' field will be used.
|
|
25
|
+
* The AI will analyze all these fields to generate better SEO meta tags.
|
|
26
|
+
*/
|
|
27
|
+
contentFields?: string[];
|
|
28
|
+
}
|
|
29
|
+
interface AiSeoPluginOptions {
|
|
30
|
+
/** Whether to enable the plugin. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** API key for the AI provider (e.g. OpenAI). Prefer passing from process.env. */
|
|
33
|
+
apiKey: string;
|
|
34
|
+
/**
|
|
35
|
+
* Collection(s) to attach the hook to. Can be:
|
|
36
|
+
* - A single collection slug: 'page'
|
|
37
|
+
* - An array of collection slugs: ['page', 'posts']
|
|
38
|
+
* - An array of collection configs with custom fields:
|
|
39
|
+
* [{ collection: 'page', seoFields: { title: 'meta.title', description: 'meta.description' } }]
|
|
40
|
+
*/
|
|
41
|
+
collections: string | string[] | CollectionConfig[];
|
|
42
|
+
/**
|
|
43
|
+
* Default SEO fields mapping.
|
|
44
|
+
* Used when collections don't specify their own seoFields.
|
|
45
|
+
* Example: { title: 'meta.title', description: 'meta.description' }
|
|
46
|
+
*/
|
|
47
|
+
seoFields?: SeoFields;
|
|
48
|
+
/**
|
|
49
|
+
* Default field paths to extract content from for AI analysis (e.g. ['title', 'content', 'excerpt']).
|
|
50
|
+
* Used when collections are specified without individual contentFields config.
|
|
51
|
+
* If not specified, only 'title' field will be used.
|
|
52
|
+
*/
|
|
53
|
+
contentFields?: string[];
|
|
54
|
+
/** OpenAI chat model for generateObject (e.g. 'gpt-4o-mini'). */
|
|
55
|
+
model?: OpenAIChatModelId;
|
|
56
|
+
}
|
|
57
|
+
interface GenerateSeoInput {
|
|
58
|
+
pageTitle: string;
|
|
59
|
+
/** Full page content for AI analysis. Can include body text, excerpts, etc. */
|
|
60
|
+
pageContent?: string;
|
|
61
|
+
apiKey: string;
|
|
62
|
+
model?: OpenAIChatModelId;
|
|
63
|
+
}
|
|
64
|
+
interface GenerateSeoResult {
|
|
65
|
+
title: string;
|
|
66
|
+
description: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type { AiSeoPluginOptions, CollectionConfig, GenerateSeoInput, GenerateSeoResult, SeoFields };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bringyouup/payload-plugin-ai-seo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-powered SEO meta generation plugin for Payload CMS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/bringyouup/payload-plugin-ai-seo.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/bringyouup/payload-plugin-ai-seo/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/bringyouup/payload-plugin-ai-seo#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"payload",
|
|
17
|
+
"payload-plugin",
|
|
18
|
+
"payloadcms",
|
|
19
|
+
"seo",
|
|
20
|
+
"ai",
|
|
21
|
+
"openai",
|
|
22
|
+
"meta",
|
|
23
|
+
"automation"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"require": "./dist/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./types": {
|
|
35
|
+
"types": "./dist/types.d.ts",
|
|
36
|
+
"import": "./dist/types.js",
|
|
37
|
+
"require": "./dist/types.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"clean": "rm -rf dist",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"pack:local": "npm run clean && npm run build && npm pack",
|
|
54
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"payload": "3.73.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@ai-sdk/openai": "~3.0.25",
|
|
61
|
+
"ai": "~6.0.68"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^22.0.0",
|
|
65
|
+
"payload": "3.73.0",
|
|
66
|
+
"tsup": "^8.0.0",
|
|
67
|
+
"typescript": "^5.7.0",
|
|
68
|
+
"vitest": "^3.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|