sanity-plugin-seofields 1.7.0 → 1.9.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 +440 -29
- package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.cts +78 -0
- package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.ts +78 -0
- package/dist/SeoHealthTool-2COI27KI.cjs +8 -0
- package/dist/SeoHealthTool-2COI27KI.cjs.map +1 -0
- package/dist/SeoHealthTool-CWI3KB2V.js +8 -0
- package/dist/SeoHealthTool-CWI3KB2V.js.map +1 -0
- package/dist/{SeoPreview-WYH7NYNM.cjs → SeoPreview-LMZAWWOE.cjs} +35 -38
- package/dist/SeoPreview-LMZAWWOE.cjs.map +1 -0
- package/dist/{SeoPreview-KEGGQSPJ.js → SeoPreview-UNQBKTQO.js} +14 -11
- package/dist/SeoPreview-UNQBKTQO.js.map +1 -0
- package/dist/chunk-3OK3S2F7.cjs +673 -0
- package/dist/chunk-3OK3S2F7.cjs.map +1 -0
- package/dist/{chunk-GWPOJSGW.cjs → chunk-5XTQRILL.cjs} +1022 -307
- package/dist/chunk-5XTQRILL.cjs.map +1 -0
- package/dist/{chunk-2NMEKWO5.js → chunk-7N4MLTMR.js} +8 -15
- package/dist/chunk-7N4MLTMR.js.map +1 -0
- package/dist/{chunk-S367Y35J.cjs → chunk-A57XNQC7.cjs} +9 -20
- package/dist/chunk-A57XNQC7.cjs.map +1 -0
- package/dist/chunk-BWLJDK5J.js +624 -0
- package/dist/chunk-BWLJDK5J.js.map +1 -0
- package/dist/chunk-FUUWBQE2.cjs +195 -0
- package/dist/chunk-FUUWBQE2.cjs.map +1 -0
- package/dist/{chunk-ZBHLMQTS.cjs → chunk-HHO2AKAP.js} +44 -17
- package/dist/chunk-HHO2AKAP.js.map +1 -0
- package/dist/chunk-KIEO6QG3.js +195 -0
- package/dist/chunk-KIEO6QG3.js.map +1 -0
- package/dist/chunk-R2U7JF7U.cjs +624 -0
- package/dist/chunk-R2U7JF7U.cjs.map +1 -0
- package/dist/{chunk-HRKRV2X7.js → chunk-XZKQWV2H.js} +974 -148
- package/dist/chunk-XZKQWV2H.js.map +1 -0
- package/dist/{chunk-HDZZQCH7.js → chunk-Y46ACXM4.cjs} +45 -5
- package/dist/chunk-Y46ACXM4.cjs.map +1 -0
- package/dist/chunk-Z5AOUU4H.js +673 -0
- package/dist/chunk-Z5AOUU4H.js.map +1 -0
- package/dist/cli.js +27 -27
- package/dist/{component-DQ4KuQQD.d.ts → component-CjT1hvxh.d.ts} +3 -27
- package/dist/{component-D1sBm6ts.d.cts → component-WYh0z8as.d.cts} +3 -27
- package/dist/define-cli.cjs +2 -4
- package/dist/define-cli.cjs.map +1 -1
- package/dist/define-cli.js +4 -4
- package/dist/define-cli.js.map +1 -1
- package/dist/head.cjs +14 -0
- package/dist/head.cjs.map +1 -0
- package/dist/head.d.cts +254 -0
- package/dist/head.d.ts +254 -0
- package/dist/head.js +14 -0
- package/dist/head.js.map +1 -0
- package/dist/index.cjs +1797 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -598
- package/dist/index.d.ts +11 -598
- package/dist/index.js +1777 -197
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +210 -447
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +8 -200
- package/dist/next.d.ts +8 -200
- package/dist/next.js +200 -124
- package/dist/next.js.map +1 -1
- package/dist/plugin-DQdThyxG.d.ts +449 -0
- package/dist/plugin-DoGeZP79.d.cts +449 -0
- package/dist/schema/next.cjs +173 -319
- package/dist/schema/next.cjs.map +1 -1
- package/dist/schema/next.d.cts +4 -4
- package/dist/schema/next.d.ts +4 -4
- package/dist/schema/next.js +172 -8
- package/dist/schema/next.js.map +1 -1
- package/dist/schema.cjs +390 -489
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +26 -16
- package/dist/schema.d.ts +26 -16
- package/dist/schema.js +236 -13
- package/dist/schema.js.map +1 -1
- package/dist/server.cjs +149 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +102 -0
- package/dist/server.d.ts +102 -0
- package/dist/server.js +149 -0
- package/dist/server.js.map +1 -0
- package/dist/{types-CeBULNFF.d.ts → types-BAdN1RMu.d.ts} +2 -2
- package/dist/{types-_wBLg6RV.d.cts → types-C5i3KMgs.d.cts} +2 -2
- package/dist/{types-9_wbCIi9.d.ts → types-DP-DiW6f.d.cts} +29 -2
- package/dist/{types-Bb49oRBV.d.cts → types-DRLUGLtX.d.ts} +29 -2
- package/dist/{types-B91ena4g.d.cts → types-DxlPXihz.d.cts} +8 -1
- package/dist/{types-B91ena4g.d.ts → types-DxlPXihz.d.ts} +8 -1
- package/package.json +25 -3
- package/dist/SeoHealthDashboard-IP5BJ322.cjs +0 -10
- package/dist/SeoHealthDashboard-IP5BJ322.cjs.map +0 -1
- package/dist/SeoHealthDashboard-PTUEVYZL.js +0 -4
- package/dist/SeoHealthDashboard-PTUEVYZL.js.map +0 -1
- package/dist/SeoHealthTool-CZZRMWQK.cjs +0 -14
- package/dist/SeoHealthTool-CZZRMWQK.cjs.map +0 -1
- package/dist/SeoHealthTool-IULT4G25.js +0 -12
- package/dist/SeoHealthTool-IULT4G25.js.map +0 -1
- package/dist/SeoPreview-KEGGQSPJ.js.map +0 -1
- package/dist/SeoPreview-WYH7NYNM.cjs.map +0 -1
- package/dist/chunk-2NMEKWO5.js.map +0 -1
- package/dist/chunk-5YRH47VQ.cjs +0 -2167
- package/dist/chunk-5YRH47VQ.cjs.map +0 -1
- package/dist/chunk-ARELX3GK.cjs +0 -689
- package/dist/chunk-ARELX3GK.cjs.map +0 -1
- package/dist/chunk-AW7OKBQQ.js +0 -2161
- package/dist/chunk-AW7OKBQQ.js.map +0 -1
- package/dist/chunk-FONJYVF2.js +0 -610
- package/dist/chunk-FONJYVF2.js.map +0 -1
- package/dist/chunk-GWPOJSGW.cjs.map +0 -1
- package/dist/chunk-HDZZQCH7.js.map +0 -1
- package/dist/chunk-HRKRV2X7.js.map +0 -1
- package/dist/chunk-S367Y35J.cjs.map +0 -1
- package/dist/chunk-ZBHLMQTS.cjs.map +0 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__spreadProps,
|
|
3
|
+
__spreadValues
|
|
4
|
+
} from "./chunk-7N4MLTMR.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/readability.ts
|
|
7
|
+
function countSyllables(word) {
|
|
8
|
+
var _a;
|
|
9
|
+
const w = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
10
|
+
if (w.length <= 3) return 1;
|
|
11
|
+
const groups = (_a = w.match(/[aeiouy]+/g)) != null ? _a : [];
|
|
12
|
+
let count = groups.length;
|
|
13
|
+
if (w.endsWith("e") && w.length > 1 && !/[aeiouy]/.test(w[w.length - 2])) {
|
|
14
|
+
count = Math.max(1, count - 1);
|
|
15
|
+
}
|
|
16
|
+
return Math.max(1, count);
|
|
17
|
+
}
|
|
18
|
+
function countSentences(text) {
|
|
19
|
+
const matches = text.match(/[.!?]+/g);
|
|
20
|
+
return matches ? matches.length : 1;
|
|
21
|
+
}
|
|
22
|
+
function analyzeReadability(text) {
|
|
23
|
+
const wordList = text.trim().split(/\s+/).filter(Boolean);
|
|
24
|
+
const words = wordList.length;
|
|
25
|
+
if (words < 10) return null;
|
|
26
|
+
const sentences = countSentences(text);
|
|
27
|
+
const syllables = wordList.reduce((acc, word) => acc + countSyllables(word), 0);
|
|
28
|
+
const rawScore = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
|
|
29
|
+
const score = Math.round(rawScore * 10) / 10;
|
|
30
|
+
const rawGrade = 0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59;
|
|
31
|
+
const gradeLevel = Math.round(rawGrade * 10) / 10;
|
|
32
|
+
const avgWordsPerSentence = Math.round(words / sentences * 10) / 10;
|
|
33
|
+
let label;
|
|
34
|
+
let color;
|
|
35
|
+
if (score >= 70) {
|
|
36
|
+
label = "Easy to read";
|
|
37
|
+
color = "green";
|
|
38
|
+
} else if (score >= 50) {
|
|
39
|
+
label = "Fairly easy";
|
|
40
|
+
color = "green";
|
|
41
|
+
} else if (score >= 30) {
|
|
42
|
+
label = "Moderately difficult";
|
|
43
|
+
color = "orange";
|
|
44
|
+
} else {
|
|
45
|
+
label = "Difficult to read";
|
|
46
|
+
color = "red";
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
score,
|
|
50
|
+
gradeLevel,
|
|
51
|
+
avgWordsPerSentence,
|
|
52
|
+
words,
|
|
53
|
+
sentences,
|
|
54
|
+
label,
|
|
55
|
+
color
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/utils/seoPrompts.ts
|
|
60
|
+
var FREE_INDUSTRIES = [
|
|
61
|
+
"blog",
|
|
62
|
+
"restaurant",
|
|
63
|
+
"travel",
|
|
64
|
+
"ecommerce",
|
|
65
|
+
"education",
|
|
66
|
+
"fitness",
|
|
67
|
+
"hospitality",
|
|
68
|
+
"nonprofit"
|
|
69
|
+
];
|
|
70
|
+
var PUBLIC_ANGLE_COUNT = 4;
|
|
71
|
+
var GENERIC_CTX = "Write for a general web audience.";
|
|
72
|
+
function buildKeywordLine(keyword, keywords, keywordRequirement, fallback) {
|
|
73
|
+
if (keywords.length > 0) {
|
|
74
|
+
return `- At least one of these keywords MUST appear verbatim: ${keywords.join(", ")}`;
|
|
75
|
+
}
|
|
76
|
+
if (keyword) {
|
|
77
|
+
return `- The keyword "${keyword}" MUST appear ${keywordRequirement}`;
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
var PUBLIC_INDUSTRY_CONTEXTS = {
|
|
82
|
+
blog: "Write for a blog or editorial publication. Use engaging, informative language. Appeal to curious readers seeking knowledge.",
|
|
83
|
+
ecommerce: "Write for an e-commerce store. Use action-oriented language that drives purchases. Highlight value, deals, and product benefits.",
|
|
84
|
+
education: "Write for an educational platform or course provider. Use encouraging, growth-focused language. Emphasize skills, outcomes, and career impact.",
|
|
85
|
+
restaurant: "Write for a restaurant, cafe, or food business. Use sensory, appetizing language. Evoke taste, ambiance, and memorable dining experience.",
|
|
86
|
+
travel: "Write for a travel agency or tourism brand. Use evocative, inspiring language. Paint vivid pictures of destinations and unforgettable experiences.",
|
|
87
|
+
fitness: "Write for a gym, studio, or personal training brand. Use motivating, energetic language. Emphasize results, transformation, and community.",
|
|
88
|
+
hospitality: "Write for a hotel, resort, or short-term rental brand. Use welcoming, sensory language. Evoke comfort, amenities, and a memorable stay.",
|
|
89
|
+
nonprofit: "Write for a nonprofit or charitable organization. Use sincere, mission-driven language. Emphasize impact, community, and the cause over sales language."
|
|
90
|
+
};
|
|
91
|
+
var TITLE_ANGLES = [
|
|
92
|
+
"Lead with a strong action verb",
|
|
93
|
+
"Lead with the key benefit to the reader",
|
|
94
|
+
"Use a how-to format (How to \u2026)",
|
|
95
|
+
"Use a question format that the reader wants answered"
|
|
96
|
+
];
|
|
97
|
+
var titleFns = TITLE_ANGLES.map((angle) => ({ content, keyword, keywords, ctx }) => {
|
|
98
|
+
const kwLine = buildKeywordLine(
|
|
99
|
+
keyword,
|
|
100
|
+
keywords,
|
|
101
|
+
"verbatim in the title",
|
|
102
|
+
"- Make it compelling and clear without a specific keyword"
|
|
103
|
+
);
|
|
104
|
+
return `You are an SEO expert. ${ctx}
|
|
105
|
+
Generate one SEO-optimized meta title using this angle: ${angle}.
|
|
106
|
+
Requirements:
|
|
107
|
+
- Strictly 50\u201360 characters including spaces and punctuation
|
|
108
|
+
${kwLine}
|
|
109
|
+
- Apply the angle \u2014 do not deviate
|
|
110
|
+
- Return ONLY the title text. No quotes, no explanation.
|
|
111
|
+
|
|
112
|
+
Page content:
|
|
113
|
+
${content}`;
|
|
114
|
+
});
|
|
115
|
+
var DESC_ANGLES = [
|
|
116
|
+
"Open with a direct answer to the reader's likely question, then add a clear call to action",
|
|
117
|
+
"Open with the pain point the reader has, then offer the solution this page provides",
|
|
118
|
+
"Open with a bold claim, then briefly back it up",
|
|
119
|
+
"Open with who this is for, then state what they will get"
|
|
120
|
+
];
|
|
121
|
+
var descFns = DESC_ANGLES.map((angle) => ({ content, keyword, keywords, ctx }) => {
|
|
122
|
+
const kwLine = buildKeywordLine(
|
|
123
|
+
keyword,
|
|
124
|
+
keywords,
|
|
125
|
+
"naturally in the description",
|
|
126
|
+
"- Include a clear call to action"
|
|
127
|
+
);
|
|
128
|
+
return `You are an SEO expert. ${ctx}
|
|
129
|
+
Generate one SEO-optimized meta description using this angle: ${angle}.
|
|
130
|
+
Requirements:
|
|
131
|
+
- Strictly 120\u2013160 characters including spaces and punctuation
|
|
132
|
+
${kwLine}
|
|
133
|
+
- Short sentences (under 15 words each), plain everyday words, no jargon \u2014 aim for high readability
|
|
134
|
+
- Apply the angle \u2014 do not deviate
|
|
135
|
+
- Return ONLY the description text. No quotes, no explanation.
|
|
136
|
+
|
|
137
|
+
Page content:
|
|
138
|
+
${content}`;
|
|
139
|
+
});
|
|
140
|
+
var OG_TITLE_ANGLES = [
|
|
141
|
+
"Curiosity hook \u2014 make the reader need to click to find out",
|
|
142
|
+
"FOMO angle \u2014 what they will miss if they do not read this",
|
|
143
|
+
"Social proof angle \u2014 imply consensus, popularity, or trust",
|
|
144
|
+
"Question angle \u2014 pose the exact question the audience is thinking"
|
|
145
|
+
];
|
|
146
|
+
var ogTitleFns = OG_TITLE_ANGLES.map(
|
|
147
|
+
(angle) => ({ content, keyword, ctx }) => `You are a social media and SEO expert. ${ctx}
|
|
148
|
+
Generate one Open Graph title for social sharing using this angle: ${angle}.
|
|
149
|
+
Requirements:
|
|
150
|
+
- Strictly 40\u201360 characters
|
|
151
|
+
- Optimized for social sharing engagement, not just search
|
|
152
|
+
- Include the core topic clearly${keyword ? ` (topic: "${keyword}")` : ""}
|
|
153
|
+
- Apply the angle above \u2014 do not deviate from it
|
|
154
|
+
- Return ONLY the OG title text. No quotes, no explanation.
|
|
155
|
+
|
|
156
|
+
Page content:
|
|
157
|
+
${content}`
|
|
158
|
+
);
|
|
159
|
+
var OG_DESC_ANGLES = [
|
|
160
|
+
"Focus on what the reader gets \u2014 concrete takeaways or outcomes",
|
|
161
|
+
"Transformation promise \u2014 describe the before/after state",
|
|
162
|
+
"Bold claim backed by a brief piece of evidence or context",
|
|
163
|
+
"Pain relief framing \u2014 identify the problem and promise relief"
|
|
164
|
+
];
|
|
165
|
+
var ogDescFns = OG_DESC_ANGLES.map(
|
|
166
|
+
(angle) => ({ content, keyword, ctx }) => `You are a social media and SEO expert. ${ctx}
|
|
167
|
+
Generate one Open Graph description for social sharing using this angle: ${angle}.
|
|
168
|
+
Requirements:
|
|
169
|
+
- Strictly 90\u2013120 characters
|
|
170
|
+
- Compelling enough to drive clicks from social feeds
|
|
171
|
+
- Include the core topic naturally${keyword ? ` (topic: "${keyword}")` : ""}
|
|
172
|
+
- Apply the angle above \u2014 do not deviate from it
|
|
173
|
+
- Return ONLY the OG description text. No quotes, no explanation.
|
|
174
|
+
|
|
175
|
+
Page content:
|
|
176
|
+
${content}`
|
|
177
|
+
);
|
|
178
|
+
var TW_TITLE_ANGLES = [
|
|
179
|
+
"Punchy curiosity hook \u2014 make the reader need to click",
|
|
180
|
+
"Direct FOMO \u2014 what they miss by scrolling past",
|
|
181
|
+
"Conversational question the audience is silently asking",
|
|
182
|
+
"Bold, scroll-stopping claim in plain language"
|
|
183
|
+
];
|
|
184
|
+
var twTitleFns = TW_TITLE_ANGLES.map(
|
|
185
|
+
(angle) => ({ content, keyword, ctx }) => `You are a social media copywriter. ${ctx}
|
|
186
|
+
Generate one X (Twitter) card title using this angle: ${angle}.
|
|
187
|
+
Requirements:
|
|
188
|
+
- Strictly 30\u201370 characters
|
|
189
|
+
- Punchy and direct \u2014 Twitter audiences scroll fast
|
|
190
|
+
- Include the core topic naturally${keyword ? ` (topic: "${keyword}")` : ""}
|
|
191
|
+
- Apply the angle above \u2014 do not deviate from it
|
|
192
|
+
- Return ONLY the title text. No quotes, no explanation.
|
|
193
|
+
|
|
194
|
+
Page content:
|
|
195
|
+
${content}`
|
|
196
|
+
);
|
|
197
|
+
var TW_DESC_ANGLES = [
|
|
198
|
+
"Conversational direct answer \u2014 what the page gives you",
|
|
199
|
+
"Pain-point opener followed by relief in one sentence",
|
|
200
|
+
"Bold hook followed by the core benefit",
|
|
201
|
+
"Short-form transformation promise"
|
|
202
|
+
];
|
|
203
|
+
var twDescFns = TW_DESC_ANGLES.map(
|
|
204
|
+
(angle) => ({ content, keyword, ctx }) => `You are a social media copywriter. ${ctx}
|
|
205
|
+
Generate one X (Twitter) card description using this angle: ${angle}.
|
|
206
|
+
Requirements:
|
|
207
|
+
- Strictly 50\u2013200 characters
|
|
208
|
+
- Conversational and punchy \u2014 casual Twitter tone
|
|
209
|
+
- Include the core topic naturally${keyword ? ` (topic: "${keyword}")` : ""}
|
|
210
|
+
- Apply the angle above \u2014 do not deviate from it
|
|
211
|
+
- Return ONLY the description text. No quotes, no explanation.
|
|
212
|
+
|
|
213
|
+
Page content:
|
|
214
|
+
${content}`
|
|
215
|
+
);
|
|
216
|
+
var FK_ANGLES = [
|
|
217
|
+
"Identify the single keyword phrase a user would most likely search to find this page",
|
|
218
|
+
"Identify the primary search intent keyword \u2014 what the reader types when looking for this content",
|
|
219
|
+
"Extract the most commercially or informationally valuable keyword from this content",
|
|
220
|
+
"Identify the exact phrase that best captures the core topic of this page"
|
|
221
|
+
];
|
|
222
|
+
var fkFns = FK_ANGLES.map((angle) => ({ content, meta, ctx }) => {
|
|
223
|
+
const hasTitle = Boolean(meta == null ? void 0 : meta.title);
|
|
224
|
+
const hasDescription = Boolean(meta == null ? void 0 : meta.description);
|
|
225
|
+
const hasSlug = Boolean(meta == null ? void 0 : meta.slug);
|
|
226
|
+
const metaBlock = hasTitle || hasDescription || hasSlug ? `
|
|
227
|
+
Existing page fields:
|
|
228
|
+
${hasTitle ? `Meta title: "${meta.title}"` : ""}
|
|
229
|
+
${hasDescription ? `Meta description: "${meta.description}"` : ""}
|
|
230
|
+
${hasSlug ? `URL slug: "${meta.slug}"` : ""}
|
|
231
|
+
|
|
232
|
+
CRITICAL: The keyword you return MUST appear verbatim (exact match, case-insensitive) inside the meta title${hasDescription ? " and meta description" : ""} above.
|
|
233
|
+
Do NOT invent a new keyword \u2014 extract one that already exists in those fields.
|
|
234
|
+
${hasSlug ? "Prefer a phrase that also appears in the URL slug." : ""}
|
|
235
|
+
` : "";
|
|
236
|
+
return `You are an SEO expert. ${ctx}
|
|
237
|
+
Task: ${angle}.
|
|
238
|
+
${metaBlock}
|
|
239
|
+
Requirements:
|
|
240
|
+
- Return ONLY the keyword or keyword phrase (1\u20134 words typically)
|
|
241
|
+
- It MUST appear verbatim in the meta title${hasDescription ? " and description" : ""} provided above
|
|
242
|
+
- No explanation, no quotes, no punctuation, no extra words
|
|
243
|
+
- Single line response
|
|
244
|
+
|
|
245
|
+
Page content:
|
|
246
|
+
${content}`;
|
|
247
|
+
});
|
|
248
|
+
var KW_ANGLES = [
|
|
249
|
+
"Generate 8 keywords mixing high-volume short-tail and specific long-tail phrases",
|
|
250
|
+
"Generate 8 keywords that cover both the main topic and closely related subtopics",
|
|
251
|
+
"Generate 8 keywords targeting both informational and commercial search intent",
|
|
252
|
+
"Generate 8 keywords including question-based phrases people would search"
|
|
253
|
+
];
|
|
254
|
+
var kwFns = KW_ANGLES.map(
|
|
255
|
+
(angle) => ({ content, keyword, ctx }) => `You are an SEO expert. ${ctx}
|
|
256
|
+
${angle}${keyword ? ` Include "${keyword}" as one of the keywords.` : ""}
|
|
257
|
+
Requirements:
|
|
258
|
+
- Return exactly 8 keywords as a comma-separated list
|
|
259
|
+
- No numbering, no explanation, no quotes around individual keywords
|
|
260
|
+
- Mix short-tail (1\u20132 words) and long-tail (3\u20135 words) phrases
|
|
261
|
+
- All keywords must be directly relevant to the page content
|
|
262
|
+
|
|
263
|
+
Page content:
|
|
264
|
+
${content}`
|
|
265
|
+
);
|
|
266
|
+
var BASE_ANGLES = {
|
|
267
|
+
title: titleFns,
|
|
268
|
+
description: descFns,
|
|
269
|
+
focusKeyword: fkFns,
|
|
270
|
+
keywords: kwFns,
|
|
271
|
+
ogTitle: ogTitleFns,
|
|
272
|
+
ogDescription: ogDescFns,
|
|
273
|
+
twitterTitle: twTitleFns,
|
|
274
|
+
twitterDescription: twDescFns
|
|
275
|
+
};
|
|
276
|
+
function buildFieldPrompts(ctx, count) {
|
|
277
|
+
const out = {};
|
|
278
|
+
for (const field of Object.keys(BASE_ANGLES)) {
|
|
279
|
+
out[field] = BASE_ANGLES[field].slice(0, count).map((fn) => (p) => fn(__spreadProps(__spreadValues({}, p), { ctx })));
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
var INDUSTRY_PROMPTS = Object.fromEntries(
|
|
284
|
+
Object.keys(PUBLIC_INDUSTRY_CONTEXTS).map((industry) => [
|
|
285
|
+
industry,
|
|
286
|
+
buildFieldPrompts(PUBLIC_INDUSTRY_CONTEXTS[industry], PUBLIC_ANGLE_COUNT)
|
|
287
|
+
])
|
|
288
|
+
);
|
|
289
|
+
var DEFAULT_PROMPTS = buildFieldPrompts(GENERIC_CTX, PUBLIC_ANGLE_COUNT);
|
|
290
|
+
async function resolvePromptPool(field, industry, licenseKey, projectId) {
|
|
291
|
+
const isFreeIndustry = Boolean(
|
|
292
|
+
industry && FREE_INDUSTRIES.includes(industry)
|
|
293
|
+
);
|
|
294
|
+
const publicPrompts = isFreeIndustry ? INDUSTRY_PROMPTS[industry][field] : DEFAULT_PROMPTS[field];
|
|
295
|
+
if (!licenseKey) return publicPrompts;
|
|
296
|
+
try {
|
|
297
|
+
const mod = await import("seofields-pro");
|
|
298
|
+
if (typeof mod.resolveLicensedPromptPool !== "function") return publicPrompts;
|
|
299
|
+
return mod.resolveLicensedPromptPool({
|
|
300
|
+
licenseKey,
|
|
301
|
+
projectId,
|
|
302
|
+
field,
|
|
303
|
+
industry,
|
|
304
|
+
publicPrompts
|
|
305
|
+
});
|
|
306
|
+
} catch (e) {
|
|
307
|
+
return publicPrompts;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function pickPrompt(field, params, industry, licenseKey, projectId) {
|
|
311
|
+
const pool = await resolvePromptPool(field, industry, licenseKey, projectId);
|
|
312
|
+
const fn = pool[Math.floor(Math.random() * pool.length)];
|
|
313
|
+
return fn(params);
|
|
314
|
+
}
|
|
315
|
+
var TEST_MODE_OUTPUTS = {
|
|
316
|
+
title: [
|
|
317
|
+
"Boost Your SEO Rankings With These Proven Tips",
|
|
318
|
+
"How to Write Meta Titles That Drive More Clicks",
|
|
319
|
+
"10 Strategies to Improve Your Search Visibility",
|
|
320
|
+
"Why Your Meta Title Is Killing Your Click-Through Rate"
|
|
321
|
+
],
|
|
322
|
+
description: [
|
|
323
|
+
"Learn how to write SEO-optimized meta titles that rank higher and earn more clicks. Discover the exact techniques used by top SEO professionals.",
|
|
324
|
+
"Struggling with low organic traffic? Our complete guide covers everything you need to fix your meta tags and climb the search rankings fast.",
|
|
325
|
+
"Meta titles under 60 characters get the best click-through rates. Find out how to craft compelling titles that rank and convert visitors.",
|
|
326
|
+
"This guide is for content creators and marketers who want to improve search rankings without technical expertise. Start seeing results today."
|
|
327
|
+
],
|
|
328
|
+
focusKeyword: [
|
|
329
|
+
"seo meta title",
|
|
330
|
+
"on-page seo",
|
|
331
|
+
"meta tag optimization",
|
|
332
|
+
"organic traffic growth"
|
|
333
|
+
],
|
|
334
|
+
keywords: [
|
|
335
|
+
"seo tips, meta title optimization, on-page seo, search rankings, click-through rate, seo guide, organic traffic, meta description, keyword placement, seo best practices",
|
|
336
|
+
"meta tags, title tag optimization, serp optimization, seo ranking factors, page title seo, seo for beginners, improve search rankings, seo checklist, meta title length, seo tools",
|
|
337
|
+
"on-page optimization, meta tag guide, seo title tips, increase organic traffic, search visibility, seo writing, title tag examples, meta description length, content seo, seo strategy",
|
|
338
|
+
"seo optimization guide, meta title examples, serp click-through rate, google ranking tips, seo content writing, page title best practices, keyword in title, seo traffic, rank higher google, search engine results"
|
|
339
|
+
],
|
|
340
|
+
ogTitle: [
|
|
341
|
+
"The SEO Secret That Tripled Our Organic Traffic",
|
|
342
|
+
"Are You Making These 5 Common Meta Title Mistakes?",
|
|
343
|
+
"This One Change Can Double Your Search Click-Through Rate",
|
|
344
|
+
"Why 90% of Meta Titles Fail to Drive Clicks"
|
|
345
|
+
],
|
|
346
|
+
ogDescription: [
|
|
347
|
+
"Get the exact framework top SEO teams use to write meta titles that rank higher and earn more clicks from search results.",
|
|
348
|
+
"Before: low CTR and invisible search rankings. After: consistent page-one results. Here is how to make that shift with your meta tags.",
|
|
349
|
+
"Research shows properly optimized meta titles get 30% more clicks. We show you exactly how to achieve that in under 10 minutes.",
|
|
350
|
+
"If your page is not getting clicks from search results, your meta title is likely the problem. Here is how to diagnose and fix it."
|
|
351
|
+
],
|
|
352
|
+
twitterTitle: [
|
|
353
|
+
"The Meta Title Trick That Tripled Our Traffic",
|
|
354
|
+
"You're Probably Writing Meta Titles Wrong",
|
|
355
|
+
"One Change That Doubles Your Click-Through Rate",
|
|
356
|
+
"Why Most Meta Titles Fail (And How to Fix Yours)"
|
|
357
|
+
],
|
|
358
|
+
twitterDescription: [
|
|
359
|
+
"Most pages leave clicks on the table because of weak meta titles. Here's the fix \u2014 in under 10 minutes.",
|
|
360
|
+
"Low search CTR? Your meta title is probably the culprit. Here's how to diagnose and fix it today.",
|
|
361
|
+
"Optimized meta titles get 30% more clicks. This guide shows you exactly how to write one that works.",
|
|
362
|
+
"Stop writing generic meta titles. This thread-worthy guide gives you templates that rank and convert."
|
|
363
|
+
]
|
|
364
|
+
};
|
|
365
|
+
function pickTestOutput(field) {
|
|
366
|
+
const arr = TEST_MODE_OUTPUTS[field];
|
|
367
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/utils/seoContentGen.ts
|
|
371
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
372
|
+
var PROVIDER_DEFAULTS = {
|
|
373
|
+
openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4o-mini", requiresKey: true },
|
|
374
|
+
anthropic: {
|
|
375
|
+
baseUrl: "https://api.anthropic.com",
|
|
376
|
+
model: "claude-haiku-4-5-20251001",
|
|
377
|
+
requiresKey: true
|
|
378
|
+
},
|
|
379
|
+
groq: {
|
|
380
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
381
|
+
model: "llama-3.3-70b-versatile",
|
|
382
|
+
requiresKey: true
|
|
383
|
+
},
|
|
384
|
+
gemini: {
|
|
385
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
386
|
+
model: "gemini-2.0-flash",
|
|
387
|
+
requiresKey: true
|
|
388
|
+
},
|
|
389
|
+
ollama: { baseUrl: "http://localhost:11434/v1", model: "llama3.2", requiresKey: false }
|
|
390
|
+
};
|
|
391
|
+
var FIELD_CONSTRAINTS = {
|
|
392
|
+
title: { min: 50, max: 60 },
|
|
393
|
+
description: { min: 120, max: 160 },
|
|
394
|
+
ogTitle: { min: 40, max: 60 },
|
|
395
|
+
ogDescription: { min: 90, max: 120 },
|
|
396
|
+
twitterTitle: { min: 30, max: 70 },
|
|
397
|
+
twitterDescription: { min: 50, max: 200 }
|
|
398
|
+
};
|
|
399
|
+
function parseApiError(status, body) {
|
|
400
|
+
var _a;
|
|
401
|
+
const msg = ((_a = body == null ? void 0 : body.error) == null ? void 0 : _a.message) || (body == null ? void 0 : body.message) || "";
|
|
402
|
+
if (status === 401)
|
|
403
|
+
return `Authentication failed (401). Check your API key.${msg ? ` ${msg}` : ""}`;
|
|
404
|
+
if (status === 429)
|
|
405
|
+
return `Rate limit reached (429). Wait a moment and try again.${msg ? ` ${msg}` : ""}`;
|
|
406
|
+
if (status >= 500)
|
|
407
|
+
return `Provider server error (${status}). Try again shortly.${msg ? ` ${msg}` : ""}`;
|
|
408
|
+
return msg || `Request failed with status ${status}.`;
|
|
409
|
+
}
|
|
410
|
+
async function callOpenAICompat(baseUrl, opts, prompt, signal) {
|
|
411
|
+
var _a, _b, _c;
|
|
412
|
+
const headers = { "Content-Type": "application/json" };
|
|
413
|
+
if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
414
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers,
|
|
417
|
+
signal,
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
model: opts.model,
|
|
420
|
+
messages: [{ role: "user", content: prompt }],
|
|
421
|
+
/* eslint-disable camelcase */
|
|
422
|
+
max_tokens: 300,
|
|
423
|
+
/* eslint-enable camelcase */
|
|
424
|
+
temperature: opts.temperature
|
|
425
|
+
})
|
|
426
|
+
});
|
|
427
|
+
const data = await res.json();
|
|
428
|
+
if (!res.ok) throw new Error(parseApiError(res.status, data));
|
|
429
|
+
return (((_c = (_b = (_a = data.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) == null ? void 0 : _c.content) || "").trim();
|
|
430
|
+
}
|
|
431
|
+
async function callAnthropic(opts, apiKey, prompt, signal) {
|
|
432
|
+
var _a, _b;
|
|
433
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: {
|
|
436
|
+
"Content-Type": "application/json",
|
|
437
|
+
"x-api-key": apiKey,
|
|
438
|
+
"anthropic-version": "2023-06-01",
|
|
439
|
+
"anthropic-dangerous-direct-browser-access": "true"
|
|
440
|
+
},
|
|
441
|
+
signal,
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
model: opts.model,
|
|
444
|
+
/* eslint-disable camelcase */
|
|
445
|
+
max_tokens: 300,
|
|
446
|
+
/* eslint-enable camelcase */
|
|
447
|
+
temperature: opts.temperature,
|
|
448
|
+
messages: [{ role: "user", content: prompt }]
|
|
449
|
+
})
|
|
450
|
+
});
|
|
451
|
+
const data = await res.json();
|
|
452
|
+
if (!res.ok) throw new Error(parseApiError(res.status, data));
|
|
453
|
+
return (((_b = (_a = data.content) == null ? void 0 : _a[0]) == null ? void 0 : _b.text) || "").trim();
|
|
454
|
+
}
|
|
455
|
+
async function callProxy({
|
|
456
|
+
field,
|
|
457
|
+
content,
|
|
458
|
+
focusKeyword,
|
|
459
|
+
keywords,
|
|
460
|
+
meta,
|
|
461
|
+
endpoint,
|
|
462
|
+
industry,
|
|
463
|
+
licenseKey,
|
|
464
|
+
signal
|
|
465
|
+
}) {
|
|
466
|
+
const res = await fetch(endpoint, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: { "Content-Type": "application/json" },
|
|
469
|
+
signal,
|
|
470
|
+
body: JSON.stringify({ field, content, focusKeyword, keywords, meta, industry, licenseKey })
|
|
471
|
+
});
|
|
472
|
+
const data = await res.json();
|
|
473
|
+
if (!res.ok) throw new Error(parseApiError(res.status, data));
|
|
474
|
+
const result = data.result;
|
|
475
|
+
if (!result) throw new Error('Proxy response missing "result" field.');
|
|
476
|
+
return result.trim();
|
|
477
|
+
}
|
|
478
|
+
function stripQuotes(text) {
|
|
479
|
+
return text.replace(/^["'""]|["'""]$/g, "").trim();
|
|
480
|
+
}
|
|
481
|
+
async function refineLength(field, raw, constraint, callApi) {
|
|
482
|
+
const len = raw.length;
|
|
483
|
+
const isTooShort = len < constraint.min;
|
|
484
|
+
const action = isTooShort ? "Expand" : "Shorten";
|
|
485
|
+
const issue = isTooShort ? `too short at ${len} characters (minimum is ${constraint.min})` : `too long at ${len} characters (maximum is ${constraint.max})`;
|
|
486
|
+
const prompt = `This SEO ${field} is ${issue}. Target range: ${constraint.min}\u2013${constraint.max} characters.
|
|
487
|
+
${action} it to fit within that range. Preserve the meaning and any keywords present.
|
|
488
|
+
Count every character carefully before outputting. The final text MUST be ${constraint.min}\u2013${constraint.max} chars.
|
|
489
|
+
Current text (${len} chars): "${raw}"
|
|
490
|
+
Return ONLY the rewritten text. No quotes, no labels, no character count.`;
|
|
491
|
+
return callApi(prompt);
|
|
492
|
+
}
|
|
493
|
+
async function refineKeyword(field, raw, keywords, constraint, callApi) {
|
|
494
|
+
const kwList = keywords.join(", ");
|
|
495
|
+
const rangeNote = constraint ? ` The final text MUST be ${constraint.min}\u2013${constraint.max} characters.` : "";
|
|
496
|
+
const prompt = `This SEO ${field} is missing a required keyword.
|
|
497
|
+
Rewrite it to include at least one of these keywords naturally: ${kwList}.${rangeNote}
|
|
498
|
+
Do not force the keyword awkwardly \u2014 weave it in as if it was always there.
|
|
499
|
+
Current text: "${raw}"
|
|
500
|
+
Return ONLY the rewritten text. No quotes, no labels, no explanation.`;
|
|
501
|
+
return callApi(prompt);
|
|
502
|
+
}
|
|
503
|
+
async function refineFocusKeyword(raw, meta, callApi) {
|
|
504
|
+
const missing = [];
|
|
505
|
+
const kw = raw.toLowerCase().trim();
|
|
506
|
+
if (meta.title && !meta.title.toLowerCase().includes(kw))
|
|
507
|
+
missing.push(`meta title: "${meta.title}"`);
|
|
508
|
+
if (meta.description && !meta.description.toLowerCase().includes(kw))
|
|
509
|
+
missing.push(`meta description: "${meta.description}"`);
|
|
510
|
+
const prompt = `The focus keyword "${raw}" does not appear verbatim in the following existing page fields:
|
|
511
|
+
${missing.join("\n")}
|
|
512
|
+
|
|
513
|
+
Extract a keyword or phrase (1\u20134 words) that appears VERBATIM in those fields.
|
|
514
|
+
Do not invent a new keyword \u2014 only return a phrase already present in the text above.
|
|
515
|
+
${meta.slug ? `URL slug: "${meta.slug}" \u2014 prefer a phrase that also matches part of this.` : ""}
|
|
516
|
+
|
|
517
|
+
Return ONLY the keyword. No quotes, no explanation, no punctuation. Single line.`;
|
|
518
|
+
return callApi(prompt);
|
|
519
|
+
}
|
|
520
|
+
async function refineReadability(raw, constraint, callApi) {
|
|
521
|
+
const prompt = `This SEO meta description uses language that is too complex.
|
|
522
|
+
Rewrite it so it reads simply and clearly:
|
|
523
|
+
- Use short sentences (under 15 words each)
|
|
524
|
+
- Use common everyday words \u2014 no jargon or passive voice
|
|
525
|
+
- Keep the same core meaning and any keywords present
|
|
526
|
+
- The final text MUST be ${constraint.min}\u2013${constraint.max} characters
|
|
527
|
+
Current text: "${raw}"
|
|
528
|
+
Return ONLY the rewritten text. No quotes, no labels, no explanation.`;
|
|
529
|
+
return callApi(prompt);
|
|
530
|
+
}
|
|
531
|
+
function checkLength(field, text) {
|
|
532
|
+
const constraint = FIELD_CONSTRAINTS[field];
|
|
533
|
+
if (!constraint) return true;
|
|
534
|
+
return text.length >= constraint.min && text.length <= constraint.max;
|
|
535
|
+
}
|
|
536
|
+
function checkKeywords(field, text, keywords) {
|
|
537
|
+
if ((field === "title" || field === "description") && keywords.length > 0) {
|
|
538
|
+
const lower = text.toLowerCase();
|
|
539
|
+
return keywords.some((kw) => lower.includes(kw.toLowerCase().trim()));
|
|
540
|
+
}
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
function checkReadability(field, text) {
|
|
544
|
+
if (field === "description") {
|
|
545
|
+
const readability = analyzeReadability(text);
|
|
546
|
+
return !readability || readability.score >= 50;
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
function checkFocusKeywordInMeta(kw, meta) {
|
|
551
|
+
const lower = kw.toLowerCase().trim();
|
|
552
|
+
if (meta.title && !meta.title.toLowerCase().includes(lower)) return false;
|
|
553
|
+
if (meta.description && !meta.description.toLowerCase().includes(lower)) return false;
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
function validateOutput(field, text, keywords, meta) {
|
|
557
|
+
if (!checkLength(field, text)) return null;
|
|
558
|
+
if (!checkKeywords(field, text, keywords)) return null;
|
|
559
|
+
if (!checkReadability(field, text)) return null;
|
|
560
|
+
if (field === "focusKeyword" && meta && (meta.title || meta.description)) {
|
|
561
|
+
if (!checkFocusKeywordInMeta(text, meta)) return null;
|
|
562
|
+
}
|
|
563
|
+
return text;
|
|
564
|
+
}
|
|
565
|
+
function warnIfKeyExposed(config) {
|
|
566
|
+
if (config.apiKey && typeof window !== "undefined") {
|
|
567
|
+
console.warn(
|
|
568
|
+
`[sanity-plugin-seofields] "apiKey" is bundled into the Studio's client-side JS and is readable by anyone with Studio access. Use "endpoint" with a server-side proxy instead \u2014 see "sanity-plugin-seofields/server" for ready-made handlers.`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function buildCallApi(config, temperature, abortSignal) {
|
|
573
|
+
var _a, _b, _c;
|
|
574
|
+
const provider = (_a = config.provider) != null ? _a : "openai";
|
|
575
|
+
const providerDef = PROVIDER_DEFAULTS[provider];
|
|
576
|
+
if (!providerDef) throw new Error(`Unknown provider "${provider}".`);
|
|
577
|
+
if (providerDef.requiresKey && !config.apiKey) {
|
|
578
|
+
throw new Error(`No API key configured for "${provider}". Add apiKey to your ai plugin config.`);
|
|
579
|
+
}
|
|
580
|
+
const baseUrl = (_b = config.baseUrl) != null ? _b : providerDef.baseUrl;
|
|
581
|
+
const model = (_c = config.model) != null ? _c : providerDef.model;
|
|
582
|
+
const opts = { apiKey: config.apiKey, model, temperature };
|
|
583
|
+
const useAnthropic = provider === "anthropic" && !config.baseUrl;
|
|
584
|
+
return (prompt) => useAnthropic ? callAnthropic(opts, config.apiKey, prompt, abortSignal) : callOpenAICompat(baseUrl, opts, prompt, abortSignal);
|
|
585
|
+
}
|
|
586
|
+
async function refineRawText(raw, ctx) {
|
|
587
|
+
const { field, keywords, meta, constraint, callApi, abortSignal } = ctx;
|
|
588
|
+
let text = raw;
|
|
589
|
+
if (constraint && !checkLength(field, text)) {
|
|
590
|
+
try {
|
|
591
|
+
abortSignal.throwIfAborted();
|
|
592
|
+
text = stripQuotes(await refineLength(field, text, constraint, callApi));
|
|
593
|
+
} catch (e) {
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!checkKeywords(field, text, keywords)) {
|
|
597
|
+
try {
|
|
598
|
+
abortSignal.throwIfAborted();
|
|
599
|
+
text = stripQuotes(await refineKeyword(field, text, keywords, constraint, callApi));
|
|
600
|
+
if (constraint && !checkLength(field, text)) {
|
|
601
|
+
abortSignal.throwIfAborted();
|
|
602
|
+
text = stripQuotes(await refineLength(field, text, constraint, callApi));
|
|
603
|
+
}
|
|
604
|
+
} catch (e) {
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (!checkReadability(field, text) && constraint) {
|
|
608
|
+
try {
|
|
609
|
+
abortSignal.throwIfAborted();
|
|
610
|
+
text = stripQuotes(await refineReadability(text, constraint, callApi));
|
|
611
|
+
if (!checkLength(field, text)) {
|
|
612
|
+
abortSignal.throwIfAborted();
|
|
613
|
+
text = stripQuotes(await refineLength(field, text, constraint, callApi));
|
|
614
|
+
}
|
|
615
|
+
} catch (e) {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (field === "focusKeyword" && meta && (meta.title || meta.description) && !checkFocusKeywordInMeta(text, meta)) {
|
|
619
|
+
try {
|
|
620
|
+
abortSignal.throwIfAborted();
|
|
621
|
+
text = stripQuotes(await refineFocusKeyword(text, meta, callApi));
|
|
622
|
+
} catch (e) {
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return text;
|
|
626
|
+
}
|
|
627
|
+
async function generateSeoText(field, content, focusKeyword, keywords, { config, signal, meta }) {
|
|
628
|
+
var _a, _b;
|
|
629
|
+
const abortSignal = signal != null ? signal : AbortSignal.timeout(15e3);
|
|
630
|
+
if (config.endpoint) {
|
|
631
|
+
return callProxy({
|
|
632
|
+
field,
|
|
633
|
+
content,
|
|
634
|
+
focusKeyword,
|
|
635
|
+
keywords,
|
|
636
|
+
meta,
|
|
637
|
+
endpoint: config.endpoint,
|
|
638
|
+
industry: config.industry,
|
|
639
|
+
licenseKey: config._licenseKey,
|
|
640
|
+
signal: abortSignal
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
warnIfKeyExposed(config);
|
|
644
|
+
const callApi = buildCallApi(config, (_a = config.temperature) != null ? _a : 0.7, abortSignal);
|
|
645
|
+
const maxRetries = (_b = config.maxRetries) != null ? _b : DEFAULT_MAX_RETRIES;
|
|
646
|
+
const isLicensed = Boolean(config._licenseKey);
|
|
647
|
+
const constraint = FIELD_CONSTRAINTS[field];
|
|
648
|
+
let firstRaw = "";
|
|
649
|
+
let lastRaw = "";
|
|
650
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
651
|
+
abortSignal.throwIfAborted();
|
|
652
|
+
const prompt = await pickPrompt(
|
|
653
|
+
field,
|
|
654
|
+
{ content, keyword: focusKeyword, keywords, meta },
|
|
655
|
+
config.industry,
|
|
656
|
+
isLicensed ? config._licenseKey : void 0,
|
|
657
|
+
config._projectId
|
|
658
|
+
);
|
|
659
|
+
const raw = stripQuotes(await callApi(prompt));
|
|
660
|
+
if (attempt === 0) firstRaw = raw;
|
|
661
|
+
lastRaw = await refineRawText(raw, { field, keywords, meta, constraint, callApi, abortSignal });
|
|
662
|
+
const valid = validateOutput(field, lastRaw, keywords, meta);
|
|
663
|
+
if (valid !== null) return valid;
|
|
664
|
+
}
|
|
665
|
+
return config.keepFirstOnValidationFail === true ? firstRaw : lastRaw;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export {
|
|
669
|
+
analyzeReadability,
|
|
670
|
+
pickTestOutput,
|
|
671
|
+
generateSeoText
|
|
672
|
+
};
|
|
673
|
+
//# sourceMappingURL=chunk-Z5AOUU4H.js.map
|