sanity-plugin-seofields 1.2.4 → 1.2.6
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/dist/index.cjs +2604 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +422 -0
- package/dist/index.d.ts +339 -492
- package/dist/index.js +1284 -2013
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +182 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +241 -0
- package/dist/next.d.ts +202 -295
- package/dist/next.js +110 -70
- package/dist/next.js.map +1 -1
- package/dist/types-B91ena4g.d.cts +89 -0
- package/dist/types-B91ena4g.d.ts +89 -0
- package/package.json +37 -18
- package/dist/index.d.mts +0 -575
- package/dist/index.mjs +0 -3292
- package/dist/index.mjs.map +0 -1
- package/dist/next.d.mts +0 -334
- package/dist/next.mjs +0 -102
- package/dist/next.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/components/SeoHealthDashboard.tsx +0 -1568
- package/src/components/SeoHealthPane.tsx +0 -81
- package/src/components/SeoHealthTool.tsx +0 -11
- package/src/components/SeoPreview.tsx +0 -178
- package/src/components/meta/MetaDescription.tsx +0 -39
- package/src/components/meta/MetaTitle.tsx +0 -44
- package/src/components/openGraph/OgDescription.tsx +0 -46
- package/src/components/openGraph/OgTitle.tsx +0 -45
- package/src/components/twitter/twitterDescription.tsx +0 -45
- package/src/components/twitter/twitterTitle.tsx +0 -45
- package/src/helpers/SeoMetaTags.tsx +0 -154
- package/src/helpers/seoMeta.ts +0 -283
- package/src/index.ts +0 -26
- package/src/next.ts +0 -12
- package/src/plugin.ts +0 -344
- package/src/schemas/index.ts +0 -121
- package/src/schemas/types/index.ts +0 -20
- package/src/schemas/types/metaAttribute/index.ts +0 -60
- package/src/schemas/types/metaTag/index.ts +0 -17
- package/src/schemas/types/openGraph/index.ts +0 -114
- package/src/schemas/types/robots/index.ts +0 -26
- package/src/schemas/types/twitter/index.ts +0 -108
- package/src/types.ts +0 -108
- package/src/utils/fieldsUtils.ts +0 -160
- package/src/utils/seoUtils.ts +0 -423
- package/src/utils/utils.ts +0 -9
- package/v2-incompatible.js +0 -11
package/src/utils/fieldsUtils.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import {AllFieldKeys, SeoFieldsPluginConfig, ValidHiddenFieldKeys} from '../plugin'
|
|
2
|
-
|
|
3
|
-
const DEFAULT_FIELD_INFO: Record<AllFieldKeys, {title: string; description: string}> = {
|
|
4
|
-
title: {
|
|
5
|
-
title: 'Meta Title',
|
|
6
|
-
description:
|
|
7
|
-
'The meta title is displayed in search engine results as the clickable headline for a given result. It should be concise and accurately reflect the content of the page.',
|
|
8
|
-
},
|
|
9
|
-
description: {
|
|
10
|
-
title: 'Meta Description',
|
|
11
|
-
description:
|
|
12
|
-
'Provide a concise summary of the page content. This description may be used by search engines in search results.',
|
|
13
|
-
},
|
|
14
|
-
metaImage: {
|
|
15
|
-
title: 'Meta Image',
|
|
16
|
-
description:
|
|
17
|
-
'Upload an image that represents the content of the page. This image may be used in social media previews and search engine results.',
|
|
18
|
-
},
|
|
19
|
-
keywords: {
|
|
20
|
-
title: 'Keywords',
|
|
21
|
-
description:
|
|
22
|
-
'Add relevant keywords for this page. These keywords will be used for SEO purposes.',
|
|
23
|
-
},
|
|
24
|
-
canonicalUrl: {
|
|
25
|
-
title: 'Canonical URL',
|
|
26
|
-
description:
|
|
27
|
-
'Specify the canonical URL for this page. This helps prevent duplicate content issues by indicating the preferred version of a page.',
|
|
28
|
-
},
|
|
29
|
-
robots: {
|
|
30
|
-
title: 'Robots Settings',
|
|
31
|
-
description: 'Configure how search engine crawlers should index and follow links on this page.',
|
|
32
|
-
},
|
|
33
|
-
metaAttributes: {
|
|
34
|
-
title: 'Additional Meta Attributes',
|
|
35
|
-
description:
|
|
36
|
-
'Add custom meta attributes to the head of the document for additional SEO and social media integration.',
|
|
37
|
-
},
|
|
38
|
-
openGraphTitle: {
|
|
39
|
-
title: 'Open Graph Title',
|
|
40
|
-
description: '',
|
|
41
|
-
},
|
|
42
|
-
openGraphUrl: {
|
|
43
|
-
title: 'Open Graph URL',
|
|
44
|
-
description:
|
|
45
|
-
'The canonical URL of the page. This should be the full URL including protocol (https://).',
|
|
46
|
-
},
|
|
47
|
-
openGraphDescription: {
|
|
48
|
-
title: 'Open Graph Description',
|
|
49
|
-
description: '',
|
|
50
|
-
},
|
|
51
|
-
openGraphSiteName: {
|
|
52
|
-
title: 'Open Graph Site Name',
|
|
53
|
-
description: 'The name of your website. This is often the site title.',
|
|
54
|
-
},
|
|
55
|
-
openGraphType: {
|
|
56
|
-
title: 'Open Graph Type',
|
|
57
|
-
description: 'Select the type of content for Open Graph.',
|
|
58
|
-
},
|
|
59
|
-
openGraphImageType: {
|
|
60
|
-
title: 'Image Type',
|
|
61
|
-
description: 'Choose whether to upload an image or provide an image URL for Open Graph.',
|
|
62
|
-
},
|
|
63
|
-
openGraphImage: {
|
|
64
|
-
title: 'Open Graph Image',
|
|
65
|
-
description:
|
|
66
|
-
'Recommended size: 1200x630px (minimum 600x315px). Max file size: 5MB. Aspect ratio 1.91:1.',
|
|
67
|
-
},
|
|
68
|
-
openGraphImageUrl: {
|
|
69
|
-
title: 'Open Graph Image URL',
|
|
70
|
-
description:
|
|
71
|
-
'Enter the full URL of the image. Ensure the image is accessible and meets the recommended size of 1200x630px (minimum 600x315px).',
|
|
72
|
-
},
|
|
73
|
-
twitterCard: {
|
|
74
|
-
title: 'X Card Type',
|
|
75
|
-
description: '',
|
|
76
|
-
},
|
|
77
|
-
twitterSite: {
|
|
78
|
-
title: 'Site X Handle',
|
|
79
|
-
description: 'The X (formerly Twitter) handle of the website (e.g., @example)',
|
|
80
|
-
},
|
|
81
|
-
twitterCreator: {
|
|
82
|
-
title: 'X Creator Handle',
|
|
83
|
-
description: 'The X (formerly Twitter) handle of the content creator (e.g., @creator)',
|
|
84
|
-
},
|
|
85
|
-
twitterTitle: {
|
|
86
|
-
title: 'X Title',
|
|
87
|
-
description: 'The title of the content as it should appear on X (formerly Twitter).',
|
|
88
|
-
},
|
|
89
|
-
twitterDescription: {
|
|
90
|
-
title: 'X Description',
|
|
91
|
-
description: 'A brief description of the content for X (formerly Twitter).',
|
|
92
|
-
},
|
|
93
|
-
twitterImageType: {
|
|
94
|
-
title: 'X Image Type',
|
|
95
|
-
description:
|
|
96
|
-
'Choose whether to upload an image or provide an image URL for X (formerly Twitter).',
|
|
97
|
-
},
|
|
98
|
-
twitterImage: {
|
|
99
|
-
title: 'X Image',
|
|
100
|
-
description:
|
|
101
|
-
'An image URL which should be at least 120x120px for "summary" card and 280x150px for "summary_large_image" card.',
|
|
102
|
-
},
|
|
103
|
-
twitterImageUrl: {
|
|
104
|
-
title: 'X Image URL',
|
|
105
|
-
description:
|
|
106
|
-
'Enter the full URL of the image for X (formerly Twitter). Ensure the image is accessible and meets the recommended size.',
|
|
107
|
-
},
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const getFieldInfo = (
|
|
111
|
-
fieldName: string,
|
|
112
|
-
fieldOverrides: SeoFieldsPluginConfig['fieldOverrides'],
|
|
113
|
-
): {title: string; description: string} => {
|
|
114
|
-
const fieldInfo =
|
|
115
|
-
(fieldOverrides && fieldOverrides[fieldName as keyof typeof fieldOverrides]) ||
|
|
116
|
-
DEFAULT_FIELD_INFO[fieldName as keyof typeof DEFAULT_FIELD_INFO]
|
|
117
|
-
return fieldInfo
|
|
118
|
-
? {title: fieldInfo.title || '', description: fieldInfo.description || ''}
|
|
119
|
-
: {title: '', description: ''}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if a field should be hidden based on the current document type
|
|
124
|
-
*/
|
|
125
|
-
export const isFieldHidden = (
|
|
126
|
-
fieldName: ValidHiddenFieldKeys,
|
|
127
|
-
config: SeoFieldsPluginConfig,
|
|
128
|
-
documentType?: string,
|
|
129
|
-
): boolean => {
|
|
130
|
-
// Check if field is in default hidden fields
|
|
131
|
-
if (config.defaultHiddenFields?.includes(fieldName)) {
|
|
132
|
-
return true
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if field is hidden for specific post type
|
|
136
|
-
if (documentType && config.fieldVisibility?.[documentType]?.hiddenFields?.includes(fieldName)) {
|
|
137
|
-
return true
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return false
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get the hidden function for any field
|
|
145
|
-
*/
|
|
146
|
-
export const getFieldHiddenFunction = (
|
|
147
|
-
fieldName: ValidHiddenFieldKeys,
|
|
148
|
-
config: SeoFieldsPluginConfig,
|
|
149
|
-
) => {
|
|
150
|
-
return ({
|
|
151
|
-
document,
|
|
152
|
-
}: {
|
|
153
|
-
document?: {
|
|
154
|
-
_type?: string
|
|
155
|
-
}
|
|
156
|
-
}): boolean => {
|
|
157
|
-
const documentType = document?._type
|
|
158
|
-
return isFieldHidden(fieldName, config, documentType)
|
|
159
|
-
}
|
|
160
|
-
}
|
package/src/utils/seoUtils.ts
DELETED
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import {FeedbackType} from '../types'
|
|
2
|
-
|
|
3
|
-
export const stopWords = ['the', 'a', 'an', 'and', 'or', 'but']
|
|
4
|
-
|
|
5
|
-
export const hasMatchingKeyword = (title: string, keywordList: string[]): boolean => {
|
|
6
|
-
if (!title || keywordList.length === 0) return false
|
|
7
|
-
const lowerTitle = title.toLowerCase()
|
|
8
|
-
return keywordList.some((keyword) => keyword && lowerTitle.includes(keyword.toLowerCase()))
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const hasKeywordOveruse = (
|
|
12
|
-
title: string,
|
|
13
|
-
keywordList: string[],
|
|
14
|
-
maxOccurrences = 3,
|
|
15
|
-
): boolean => {
|
|
16
|
-
if (!title || keywordList.length === 0) return false
|
|
17
|
-
const lowerTitle = title.toLowerCase()
|
|
18
|
-
return keywordList.some((keyword) => {
|
|
19
|
-
if (!keyword) return false
|
|
20
|
-
const matches = lowerTitle.match(new RegExp(keyword.toLowerCase(), 'g'))
|
|
21
|
-
return matches ? matches.length > maxOccurrences : false
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const startsWithStopWord = (title: string): boolean => {
|
|
26
|
-
if (!title) return false
|
|
27
|
-
const firstWord = title.trim().split(' ')[0].toLowerCase()
|
|
28
|
-
return stopWords.includes(firstWord)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const primaryKeywordAtStart = (title: string, keywords: string[]): boolean => {
|
|
32
|
-
if (!title || keywords.length === 0) return true
|
|
33
|
-
return title.toLowerCase().startsWith(keywords[0].toLowerCase())
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const truncate = (text: string, maxLength: number): string =>
|
|
37
|
-
text.length > maxLength ? `${text.slice(0, maxLength)}…` : text
|
|
38
|
-
|
|
39
|
-
export const hasExcessivePunctuation = (title: string): boolean => /[!@#$%^&*]{2,}/.test(title)
|
|
40
|
-
|
|
41
|
-
export const getMetaTitleValidationMessages = (
|
|
42
|
-
title: string,
|
|
43
|
-
keywords: string[],
|
|
44
|
-
isParentseoField: boolean,
|
|
45
|
-
): FeedbackType[] => {
|
|
46
|
-
const feedback: FeedbackType[] = []
|
|
47
|
-
|
|
48
|
-
const minChar = 50
|
|
49
|
-
const maxChar = 60
|
|
50
|
-
const charCount = title?.length || 0
|
|
51
|
-
|
|
52
|
-
// Empty check
|
|
53
|
-
if (!title?.trim()) {
|
|
54
|
-
feedback.push({text: 'Meta Title is empty. Add content to improve SEO.', color: 'red'})
|
|
55
|
-
return feedback
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Length check
|
|
59
|
-
if (charCount < minChar)
|
|
60
|
-
feedback.push({
|
|
61
|
-
text: `Title is ${charCount} characters — below recommended ${minChar}.`,
|
|
62
|
-
color: 'orange',
|
|
63
|
-
})
|
|
64
|
-
else if (charCount > maxChar)
|
|
65
|
-
feedback.push({
|
|
66
|
-
text: `Title is ${charCount} characters — exceeds recommended ${maxChar}.`,
|
|
67
|
-
color: 'red',
|
|
68
|
-
})
|
|
69
|
-
else feedback.push({text: `Title length (${charCount}) looks good for SEO.`, color: 'green'})
|
|
70
|
-
|
|
71
|
-
// Keyword checks
|
|
72
|
-
if (isParentseoField) {
|
|
73
|
-
if (keywords.length > 0) {
|
|
74
|
-
const hasKeyword = hasMatchingKeyword(title, keywords)
|
|
75
|
-
feedback.push({
|
|
76
|
-
text: hasKeyword
|
|
77
|
-
? 'Keyword found in title — good job!'
|
|
78
|
-
: 'Keywords defined but missing in title.',
|
|
79
|
-
color: hasKeyword ? 'green' : 'red',
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
if (hasKeywordOveruse(title, keywords)) {
|
|
83
|
-
feedback.push({
|
|
84
|
-
text: 'Keyword appears too many times — avoid keyword stuffing.',
|
|
85
|
-
color: 'orange',
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
feedback.push({
|
|
90
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
91
|
-
color: 'orange',
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Stop word check
|
|
97
|
-
if (startsWithStopWord(title))
|
|
98
|
-
feedback.push({text: 'Title starts with a stop word — consider rephrasing.', color: 'orange'})
|
|
99
|
-
|
|
100
|
-
// Punctuation check
|
|
101
|
-
if (hasExcessivePunctuation(title))
|
|
102
|
-
feedback.push({text: 'Title contains excessive punctuation — simplify it.', color: 'orange'})
|
|
103
|
-
|
|
104
|
-
return feedback
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export const getMetaDescriptionValidationMessages = (
|
|
108
|
-
description: string,
|
|
109
|
-
keywords: string[],
|
|
110
|
-
isParentseoField: boolean,
|
|
111
|
-
): FeedbackType[] => {
|
|
112
|
-
const feedback: FeedbackType[] = []
|
|
113
|
-
|
|
114
|
-
const minChar = 120
|
|
115
|
-
const maxChar = 160
|
|
116
|
-
const charCount = description?.length || 0
|
|
117
|
-
|
|
118
|
-
if (!description?.trim()) {
|
|
119
|
-
feedback.push({text: 'Meta description is empty. Add content to improve SEO.', color: 'red'})
|
|
120
|
-
return feedback
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Length check
|
|
124
|
-
if (charCount < minChar)
|
|
125
|
-
feedback.push({
|
|
126
|
-
text: `Description is ${charCount} chars — below recommended ${minChar}.`,
|
|
127
|
-
color: 'orange',
|
|
128
|
-
})
|
|
129
|
-
else if (charCount > maxChar)
|
|
130
|
-
feedback.push({
|
|
131
|
-
text: `Description is ${charCount} chars — exceeds recommended ${maxChar}.`,
|
|
132
|
-
color: 'red',
|
|
133
|
-
})
|
|
134
|
-
else
|
|
135
|
-
feedback.push({text: `Description length (${charCount}) looks good for SEO.`, color: 'green'})
|
|
136
|
-
|
|
137
|
-
// Keyword checks
|
|
138
|
-
if (isParentseoField) {
|
|
139
|
-
if (keywords.length > 0) {
|
|
140
|
-
const hasKeyword = hasMatchingKeyword(description, keywords)
|
|
141
|
-
feedback.push({
|
|
142
|
-
text: hasKeyword
|
|
143
|
-
? 'Keyword found in description — good job!'
|
|
144
|
-
: 'Keywords defined but missing in description.',
|
|
145
|
-
color: hasKeyword ? 'green' : 'red',
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
if (hasKeywordOveruse(description, keywords)) {
|
|
149
|
-
feedback.push({
|
|
150
|
-
text: 'Keyword appears too many times — avoid keyword stuffing.',
|
|
151
|
-
color: 'orange',
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
} else {
|
|
155
|
-
feedback.push({
|
|
156
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
157
|
-
color: 'orange',
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Stop word / filler check
|
|
163
|
-
if (startsWithStopWord(description))
|
|
164
|
-
feedback.push({
|
|
165
|
-
text: 'Description starts with a stop word — consider rephrasing.',
|
|
166
|
-
color: 'orange',
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// Punctuation / special characters
|
|
170
|
-
if (hasExcessivePunctuation(description))
|
|
171
|
-
feedback.push({
|
|
172
|
-
text: 'Description contains excessive punctuation — simplify it.',
|
|
173
|
-
color: 'orange',
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
return feedback
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export const getOgTitleValidation = (
|
|
180
|
-
title: string,
|
|
181
|
-
keywords: string[] = [],
|
|
182
|
-
isParentseoField: boolean,
|
|
183
|
-
): FeedbackType[] => {
|
|
184
|
-
const feedback: FeedbackType[] = []
|
|
185
|
-
const min = 40
|
|
186
|
-
const max = 60
|
|
187
|
-
const count = title?.length || 0
|
|
188
|
-
|
|
189
|
-
// Empty check
|
|
190
|
-
if (!title?.trim()) {
|
|
191
|
-
feedback.push({text: 'OG Title is empty. Add content for better social preview.', color: 'red'})
|
|
192
|
-
return feedback
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Length check
|
|
196
|
-
if (count < min)
|
|
197
|
-
feedback.push({
|
|
198
|
-
text: `OG Title is ${count} chars — shorter than recommended ${min}.`,
|
|
199
|
-
color: 'orange',
|
|
200
|
-
})
|
|
201
|
-
else if (count > max)
|
|
202
|
-
feedback.push({text: `OG Title is ${count} chars — exceeds recommended ${max}.`, color: 'red'})
|
|
203
|
-
else feedback.push({text: `OG Title length (${count}) looks good.`, color: 'green'})
|
|
204
|
-
|
|
205
|
-
if (isParentseoField) {
|
|
206
|
-
// Keyword checks
|
|
207
|
-
if (keywords.length > 0) {
|
|
208
|
-
const hasKeyword = hasMatchingKeyword(title, keywords)
|
|
209
|
-
feedback.push({
|
|
210
|
-
text: hasKeyword
|
|
211
|
-
? 'Keyword found in OG title — good job!'
|
|
212
|
-
: 'Keywords defined but missing in OG title.',
|
|
213
|
-
color: hasKeyword ? 'green' : 'red',
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
if (hasKeywordOveruse(title, keywords)) {
|
|
217
|
-
feedback.push({
|
|
218
|
-
text: 'Keyword appears too many times in OG title — avoid keyword stuffing.',
|
|
219
|
-
color: 'orange',
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
feedback.push({
|
|
224
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
225
|
-
color: 'orange',
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Additional OG-specific checks
|
|
231
|
-
if (startsWithStopWord(title))
|
|
232
|
-
feedback.push({
|
|
233
|
-
text: 'OG Title starts with a stop word — consider rephrasing.',
|
|
234
|
-
color: 'orange',
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
if (hasExcessivePunctuation(title))
|
|
238
|
-
feedback.push({text: 'OG Title contains excessive punctuation — simplify it.', color: 'orange'})
|
|
239
|
-
|
|
240
|
-
return feedback
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export const getOgDescriptionValidation = (
|
|
244
|
-
desc: string,
|
|
245
|
-
keywords: string[] = [],
|
|
246
|
-
isParentseoField: boolean,
|
|
247
|
-
): FeedbackType[] => {
|
|
248
|
-
const feedback: FeedbackType[] = []
|
|
249
|
-
const min = 90
|
|
250
|
-
const max = 120
|
|
251
|
-
const count = desc?.length || 0
|
|
252
|
-
|
|
253
|
-
// Empty check
|
|
254
|
-
if (!desc?.trim()) {
|
|
255
|
-
feedback.push({
|
|
256
|
-
text: 'OG Description is empty. Add content for better social preview.',
|
|
257
|
-
color: 'red',
|
|
258
|
-
})
|
|
259
|
-
return feedback
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Length check
|
|
263
|
-
if (count < min)
|
|
264
|
-
feedback.push({
|
|
265
|
-
text: `OG Description is ${count} chars — shorter than recommended ${min}.`,
|
|
266
|
-
color: 'orange',
|
|
267
|
-
})
|
|
268
|
-
else if (count > max)
|
|
269
|
-
feedback.push({
|
|
270
|
-
text: `OG Description is ${count} chars — exceeds recommended ${max}.`,
|
|
271
|
-
color: 'red',
|
|
272
|
-
})
|
|
273
|
-
else feedback.push({text: `OG Description length (${count}) looks good.`, color: 'green'})
|
|
274
|
-
|
|
275
|
-
// Keyword checks
|
|
276
|
-
if (isParentseoField) {
|
|
277
|
-
if (keywords.length > 0) {
|
|
278
|
-
const hasKeyword = hasMatchingKeyword(desc, keywords)
|
|
279
|
-
feedback.push({
|
|
280
|
-
text: hasKeyword
|
|
281
|
-
? 'Keyword found in OG description — good job!'
|
|
282
|
-
: 'Keywords defined but missing in OG description.',
|
|
283
|
-
color: hasKeyword ? 'green' : 'red',
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
if (hasKeywordOveruse(desc, keywords)) {
|
|
287
|
-
feedback.push({
|
|
288
|
-
text: 'Keyword appears too many times in OG description — avoid keyword stuffing.',
|
|
289
|
-
color: 'orange',
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
} else {
|
|
293
|
-
feedback.push({
|
|
294
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
295
|
-
color: 'orange',
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Additional OG-specific checks
|
|
301
|
-
if (startsWithStopWord(desc))
|
|
302
|
-
feedback.push({
|
|
303
|
-
text: 'OG Description starts with a stop word — consider rephrasing.',
|
|
304
|
-
color: 'orange',
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
if (hasExcessivePunctuation(desc))
|
|
308
|
-
feedback.push({
|
|
309
|
-
text: 'OG Description contains excessive punctuation — simplify it.',
|
|
310
|
-
color: 'orange',
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
return feedback
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export const getTwitterTitleValidation = (
|
|
317
|
-
title: string,
|
|
318
|
-
keywords: string[] = [],
|
|
319
|
-
isParentseoField: boolean,
|
|
320
|
-
): FeedbackType[] => {
|
|
321
|
-
const feedback: FeedbackType[] = []
|
|
322
|
-
const min = 30
|
|
323
|
-
const max = 70
|
|
324
|
-
const count = title?.length || 0
|
|
325
|
-
|
|
326
|
-
if (!title?.trim()) {
|
|
327
|
-
feedback.push({text: 'X Title is empty. Add content for better SEO.', color: 'red'})
|
|
328
|
-
return feedback
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Length check
|
|
332
|
-
if (count < min)
|
|
333
|
-
feedback.push({
|
|
334
|
-
text: `X Title is ${count} chars — shorter than recommended ${min}.`,
|
|
335
|
-
color: 'orange',
|
|
336
|
-
})
|
|
337
|
-
else if (count > max)
|
|
338
|
-
feedback.push({
|
|
339
|
-
text: `X Title is ${count} chars — exceeds recommended ${max}.`,
|
|
340
|
-
color: 'red',
|
|
341
|
-
})
|
|
342
|
-
else feedback.push({text: `X Title length (${count}) looks good.`, color: 'green'})
|
|
343
|
-
|
|
344
|
-
if (isParentseoField) {
|
|
345
|
-
// Keyword checks
|
|
346
|
-
if (keywords.length > 0) {
|
|
347
|
-
const hasKeyword = hasMatchingKeyword(title, keywords)
|
|
348
|
-
feedback.push({
|
|
349
|
-
text: hasKeyword
|
|
350
|
-
? 'Keyword found in X title — good job!'
|
|
351
|
-
: 'Keywords defined but missing in X title.',
|
|
352
|
-
color: hasKeyword ? 'green' : 'red',
|
|
353
|
-
})
|
|
354
|
-
} else {
|
|
355
|
-
feedback.push({
|
|
356
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
357
|
-
color: 'orange',
|
|
358
|
-
})
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Punctuation check
|
|
363
|
-
if (/[!@#$%^&*]{2,}/.test(title))
|
|
364
|
-
feedback.push({text: 'X Title has excessive punctuation — simplify it.', color: 'orange'})
|
|
365
|
-
|
|
366
|
-
return feedback
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export const getTwitterDescriptionValidation = (
|
|
370
|
-
desc: string,
|
|
371
|
-
keywords: string[] = [],
|
|
372
|
-
isParentseoField: boolean,
|
|
373
|
-
): FeedbackType[] => {
|
|
374
|
-
const feedback: FeedbackType[] = []
|
|
375
|
-
const min = 50
|
|
376
|
-
const max = 200
|
|
377
|
-
const count = desc?.length || 0
|
|
378
|
-
|
|
379
|
-
if (!desc?.trim()) {
|
|
380
|
-
feedback.push({text: 'X Description is empty. Add content for better SEO.', color: 'red'})
|
|
381
|
-
return feedback
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Length check
|
|
385
|
-
if (count < min)
|
|
386
|
-
feedback.push({
|
|
387
|
-
text: `X Description is ${count} chars — shorter than recommended ${min}.`,
|
|
388
|
-
color: 'orange',
|
|
389
|
-
})
|
|
390
|
-
else if (count > max)
|
|
391
|
-
feedback.push({
|
|
392
|
-
text: `X Description is ${count} chars — exceeds recommended ${max}.`,
|
|
393
|
-
color: 'red',
|
|
394
|
-
})
|
|
395
|
-
else feedback.push({text: `X Description length (${count}) looks good.`, color: 'green'})
|
|
396
|
-
|
|
397
|
-
if (isParentseoField) {
|
|
398
|
-
// Keyword checks
|
|
399
|
-
if (keywords.length > 0) {
|
|
400
|
-
const hasKeyword = hasMatchingKeyword(desc, keywords)
|
|
401
|
-
feedback.push({
|
|
402
|
-
text: hasKeyword
|
|
403
|
-
? 'Keyword found in X description — good job!'
|
|
404
|
-
: 'Keywords defined but missing in X description.',
|
|
405
|
-
color: hasKeyword ? 'green' : 'red',
|
|
406
|
-
})
|
|
407
|
-
} else {
|
|
408
|
-
feedback.push({
|
|
409
|
-
text: 'No keywords defined. Consider adding relevant keywords.',
|
|
410
|
-
color: 'orange',
|
|
411
|
-
})
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Punctuation check
|
|
416
|
-
if (/[!@#$%^&*]{2,}/.test(desc))
|
|
417
|
-
feedback.push({
|
|
418
|
-
text: 'X Description has excessive punctuation — simplify it.',
|
|
419
|
-
color: 'orange',
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
return feedback
|
|
423
|
-
}
|
package/src/utils/utils.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export const isEmpty = (value: unknown): boolean => {
|
|
2
|
-
return (
|
|
3
|
-
value === null ||
|
|
4
|
-
value === undefined ||
|
|
5
|
-
(typeof value === 'string' && value.trim() === '') ||
|
|
6
|
-
(Array.isArray(value) && value.length === 0) ||
|
|
7
|
-
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)
|
|
8
|
-
)
|
|
9
|
-
}
|
package/v2-incompatible.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
})
|