sanity-plugin-seofields 1.0.1

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.
@@ -0,0 +1,386 @@
1
+ // utils/seoUtils.ts
2
+ export const stopWords = ['the', 'a', 'an', 'and', 'or', 'but']
3
+
4
+ export const hasMatchingKeyword = (title: string, keywordList: string[]): boolean => {
5
+ if (!title || keywordList.length === 0) return false
6
+ const lowerTitle = title.toLowerCase()
7
+ return keywordList.some((keyword) => keyword && lowerTitle.includes(keyword.toLowerCase()))
8
+ }
9
+
10
+ export const hasKeywordOveruse = (
11
+ title: string,
12
+ keywordList: string[],
13
+ maxOccurrences = 3,
14
+ ): boolean => {
15
+ if (!title || keywordList.length === 0) return false
16
+ const lowerTitle = title.toLowerCase()
17
+ return keywordList.some((keyword) => {
18
+ if (!keyword) return false
19
+ const matches = lowerTitle.match(new RegExp(keyword.toLowerCase(), 'g'))
20
+ return matches ? matches.length > maxOccurrences : false
21
+ })
22
+ }
23
+
24
+ export const startsWithStopWord = (title: string): boolean => {
25
+ if (!title) return false
26
+ const firstWord = title.trim().split(' ')[0].toLowerCase()
27
+ return stopWords.includes(firstWord)
28
+ }
29
+
30
+ export const primaryKeywordAtStart = (title: string, keywords: string[]): boolean => {
31
+ if (!title || keywords.length === 0) return true
32
+ return title.toLowerCase().startsWith(keywords[0].toLowerCase())
33
+ }
34
+
35
+ export const truncate = (text: string, maxLength: number) =>
36
+ text.length > maxLength ? text.slice(0, maxLength) + '…' : text
37
+
38
+ export const hasExcessivePunctuation = (title: string): boolean => /[!@#$%^&*]{2,}/.test(title)
39
+
40
+ export const getMetaTitleValidationMessages = (title: string, keywords: string[]) => {
41
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
42
+
43
+ const minChar = 50
44
+ const maxChar = 60
45
+ const charCount = title?.length || 0
46
+
47
+ // Empty check
48
+ if (!title?.trim()) {
49
+ feedback.push({text: 'Meta Title is empty. Add content to improve SEO.', color: 'red'})
50
+ return feedback
51
+ }
52
+
53
+ // Length check
54
+ if (charCount < minChar)
55
+ feedback.push({
56
+ text: `Title is ${charCount} characters — below recommended ${minChar}.`,
57
+ color: 'orange',
58
+ })
59
+ else if (charCount > maxChar)
60
+ feedback.push({
61
+ text: `Title is ${charCount} characters — exceeds recommended ${maxChar}.`,
62
+ color: 'red',
63
+ })
64
+ else feedback.push({text: `Title length (${charCount}) looks good for SEO.`, color: 'green'})
65
+
66
+ // Keyword checks
67
+ if (keywords.length > 0) {
68
+ const hasKeyword = hasMatchingKeyword(title, keywords)
69
+ feedback.push({
70
+ text: hasKeyword
71
+ ? 'Keyword found in title — good job!'
72
+ : 'Keywords defined but missing in title.',
73
+ color: hasKeyword ? 'green' : 'red',
74
+ })
75
+
76
+ if (hasKeywordOveruse(title, keywords)) {
77
+ feedback.push({
78
+ text: 'Keyword appears too many times — avoid keyword stuffing.',
79
+ color: 'orange',
80
+ })
81
+ }
82
+ } else {
83
+ feedback.push({
84
+ text: 'No keywords defined. Consider adding relevant keywords.',
85
+ color: 'orange',
86
+ })
87
+ }
88
+
89
+ // Stop word check
90
+ if (startsWithStopWord(title))
91
+ feedback.push({text: 'Title starts with a stop word — consider rephrasing.', color: 'orange'})
92
+
93
+ // Punctuation check
94
+ if (hasExcessivePunctuation(title))
95
+ feedback.push({text: 'Title contains excessive punctuation — simplify it.', color: 'orange'})
96
+
97
+ return feedback
98
+ }
99
+
100
+ export const getMetaDescriptionValidationMessages = (description: string, keywords: string[]) => {
101
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
102
+
103
+ const minChar = 150
104
+ const maxChar = 160
105
+ const charCount = description?.length || 0
106
+
107
+ if (!description?.trim()) {
108
+ feedback.push({text: 'Meta description is empty. Add content to improve SEO.', color: 'red'})
109
+ return feedback
110
+ }
111
+
112
+ // Length check
113
+ if (charCount < minChar)
114
+ feedback.push({
115
+ text: `Description is ${charCount} chars — below recommended ${minChar}.`,
116
+ color: 'orange',
117
+ })
118
+ else if (charCount > maxChar)
119
+ feedback.push({
120
+ text: `Description is ${charCount} chars — exceeds recommended ${maxChar}.`,
121
+ color: 'red',
122
+ })
123
+ else
124
+ feedback.push({text: `Description length (${charCount}) looks good for SEO.`, color: 'green'})
125
+
126
+ // Keyword checks
127
+ if (keywords.length > 0) {
128
+ const hasKeyword = hasMatchingKeyword(description, keywords)
129
+ feedback.push({
130
+ text: hasKeyword
131
+ ? 'Keyword found in description — good job!'
132
+ : 'Keywords defined but missing in description.',
133
+ color: hasKeyword ? 'green' : 'red',
134
+ })
135
+
136
+ if (hasKeywordOveruse(description, keywords)) {
137
+ feedback.push({
138
+ text: 'Keyword appears too many times — avoid keyword stuffing.',
139
+ color: 'orange',
140
+ })
141
+ }
142
+ } else {
143
+ feedback.push({
144
+ text: 'No keywords defined. Consider adding relevant keywords.',
145
+ color: 'orange',
146
+ })
147
+ }
148
+
149
+ // Stop word / filler check
150
+ if (startsWithStopWord(description))
151
+ feedback.push({
152
+ text: 'Description starts with a stop word — consider rephrasing.',
153
+ color: 'orange',
154
+ })
155
+
156
+ // Punctuation / special characters
157
+ if (hasExcessivePunctuation(description))
158
+ feedback.push({
159
+ text: 'Description contains excessive punctuation — simplify it.',
160
+ color: 'orange',
161
+ })
162
+
163
+ return feedback
164
+ }
165
+
166
+ export const getOgTitleValidation = (title: string, keywords: string[] = []) => {
167
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
168
+ const min = 40
169
+ const max = 60
170
+ const count = title?.length || 0
171
+
172
+ // Empty check
173
+ if (!title?.trim()) {
174
+ feedback.push({text: 'OG Title is empty. Add content for better social preview.', color: 'red'})
175
+ return feedback
176
+ }
177
+
178
+ // Length check
179
+ if (count < min)
180
+ feedback.push({
181
+ text: `OG Title is ${count} chars — shorter than recommended ${min}.`,
182
+ color: 'orange',
183
+ })
184
+ else if (count > max)
185
+ feedback.push({text: `OG Title is ${count} chars — exceeds recommended ${max}.`, color: 'red'})
186
+ else feedback.push({text: `OG Title length (${count}) looks good.`, color: 'green'})
187
+
188
+ // Keyword checks
189
+ if (keywords.length > 0) {
190
+ const hasKeyword = hasMatchingKeyword(title, keywords)
191
+ feedback.push({
192
+ text: hasKeyword
193
+ ? 'Keyword found in OG title — good job!'
194
+ : 'Keywords defined but missing in OG title.',
195
+ color: hasKeyword ? 'green' : 'red',
196
+ })
197
+
198
+ if (hasKeywordOveruse(title, keywords)) {
199
+ feedback.push({
200
+ text: 'Keyword appears too many times in OG title — avoid keyword stuffing.',
201
+ color: 'orange',
202
+ })
203
+ }
204
+ } else {
205
+ feedback.push({
206
+ text: 'No keywords defined. Consider adding relevant keywords.',
207
+ color: 'orange',
208
+ })
209
+ }
210
+
211
+ // Additional OG-specific checks
212
+ if (startsWithStopWord(title))
213
+ feedback.push({
214
+ text: 'OG Title starts with a stop word — consider rephrasing.',
215
+ color: 'orange',
216
+ })
217
+
218
+ if (hasExcessivePunctuation(title))
219
+ feedback.push({text: 'OG Title contains excessive punctuation — simplify it.', color: 'orange'})
220
+
221
+ return feedback
222
+ }
223
+
224
+ export const getOgDescriptionValidation = (desc: string, keywords: string[] = []) => {
225
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
226
+ const min = 90
227
+ const max = 120
228
+ const count = desc?.length || 0
229
+
230
+ // Empty check
231
+ if (!desc?.trim()) {
232
+ feedback.push({
233
+ text: 'OG Description is empty. Add content for better social preview.',
234
+ color: 'red',
235
+ })
236
+ return feedback
237
+ }
238
+
239
+ // Length check
240
+ if (count < min)
241
+ feedback.push({
242
+ text: `OG Description is ${count} chars — shorter than recommended ${min}.`,
243
+ color: 'orange',
244
+ })
245
+ else if (count > max)
246
+ feedback.push({
247
+ text: `OG Description is ${count} chars — exceeds recommended ${max}.`,
248
+ color: 'red',
249
+ })
250
+ else feedback.push({text: `OG Description length (${count}) looks good.`, color: 'green'})
251
+
252
+ // Keyword checks
253
+ if (keywords.length > 0) {
254
+ const hasKeyword = hasMatchingKeyword(desc, keywords)
255
+ feedback.push({
256
+ text: hasKeyword
257
+ ? 'Keyword found in OG description — good job!'
258
+ : 'Keywords defined but missing in OG description.',
259
+ color: hasKeyword ? 'green' : 'red',
260
+ })
261
+
262
+ if (hasKeywordOveruse(desc, keywords)) {
263
+ feedback.push({
264
+ text: 'Keyword appears too many times in OG description — avoid keyword stuffing.',
265
+ color: 'orange',
266
+ })
267
+ }
268
+ } else {
269
+ feedback.push({
270
+ text: 'No keywords defined. Consider adding relevant keywords.',
271
+ color: 'orange',
272
+ })
273
+ }
274
+
275
+ // Additional OG-specific checks
276
+ if (startsWithStopWord(desc))
277
+ feedback.push({
278
+ text: 'OG Description starts with a stop word — consider rephrasing.',
279
+ color: 'orange',
280
+ })
281
+
282
+ if (hasExcessivePunctuation(desc))
283
+ feedback.push({
284
+ text: 'OG Description contains excessive punctuation — simplify it.',
285
+ color: 'orange',
286
+ })
287
+
288
+ return feedback
289
+ }
290
+
291
+ export const getTwitterTitleValidation = (title: string, keywords: string[] = []) => {
292
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
293
+ const min = 30
294
+ const max = 70
295
+ const count = title?.length || 0
296
+
297
+ if (!title?.trim()) {
298
+ feedback.push({text: 'Twitter Title is empty. Add content for better SEO.', color: 'red'})
299
+ return feedback
300
+ }
301
+
302
+ // Length check
303
+ if (count < min)
304
+ feedback.push({
305
+ text: `Twitter Title is ${count} chars — shorter than recommended ${min}.`,
306
+ color: 'orange',
307
+ })
308
+ else if (count > max)
309
+ feedback.push({
310
+ text: `Twitter Title is ${count} chars — exceeds recommended ${max}.`,
311
+ color: 'red',
312
+ })
313
+ else feedback.push({text: `Twitter Title length (${count}) looks good.`, color: 'green'})
314
+
315
+ // Keyword checks
316
+ if (keywords.length > 0) {
317
+ const hasKeyword = hasMatchingKeyword(title, keywords)
318
+ feedback.push({
319
+ text: hasKeyword
320
+ ? 'Keyword found in Twitter title — good job!'
321
+ : 'Keywords defined but missing in Twitter title.',
322
+ color: hasKeyword ? 'green' : 'red',
323
+ })
324
+ } else {
325
+ feedback.push({
326
+ text: 'No keywords defined. Consider adding relevant keywords.',
327
+ color: 'orange',
328
+ })
329
+ }
330
+
331
+ // Punctuation check
332
+ if (/[!@#$%^&*]{2,}/.test(title))
333
+ feedback.push({text: 'Twitter Title has excessive punctuation — simplify it.', color: 'orange'})
334
+
335
+ return feedback
336
+ }
337
+
338
+ export const getTwitterDescriptionValidation = (desc: string, keywords: string[] = []) => {
339
+ const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
340
+ const min = 50
341
+ const max = 200
342
+ const count = desc?.length || 0
343
+
344
+ if (!desc?.trim()) {
345
+ feedback.push({text: 'Twitter Description is empty. Add content for better SEO.', color: 'red'})
346
+ return feedback
347
+ }
348
+
349
+ // Length check
350
+ if (count < min)
351
+ feedback.push({
352
+ text: `Twitter Description is ${count} chars — shorter than recommended ${min}.`,
353
+ color: 'orange',
354
+ })
355
+ else if (count > max)
356
+ feedback.push({
357
+ text: `Twitter Description is ${count} chars — exceeds recommended ${max}.`,
358
+ color: 'red',
359
+ })
360
+ else feedback.push({text: `Twitter Description length (${count}) looks good.`, color: 'green'})
361
+
362
+ // Keyword checks
363
+ if (keywords.length > 0) {
364
+ const hasKeyword = hasMatchingKeyword(desc, keywords)
365
+ feedback.push({
366
+ text: hasKeyword
367
+ ? 'Keyword found in Twitter description — good job!'
368
+ : 'Keywords defined but missing in Twitter description.',
369
+ color: hasKeyword ? 'green' : 'red',
370
+ })
371
+ } else {
372
+ feedback.push({
373
+ text: 'No keywords defined. Consider adding relevant keywords.',
374
+ color: 'orange',
375
+ })
376
+ }
377
+
378
+ // Punctuation check
379
+ if (/[!@#$%^&*]{2,}/.test(desc))
380
+ feedback.push({
381
+ text: 'Twitter Description has excessive punctuation — simplify it.',
382
+ color: 'orange',
383
+ })
384
+
385
+ return feedback
386
+ }
@@ -0,0 +1,11 @@
1
+ const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
+ const {name, version, sanityExchangeUrl} = require('./package.json')
3
+
4
+ export default showIncompatiblePluginDialog({
5
+ name: name,
6
+ versions: {
7
+ v3: version,
8
+ v2: undefined,
9
+ },
10
+ sanityExchangeUrl,
11
+ })