sanity-plugin-seofields 1.0.8 → 1.0.10

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.
@@ -1,26 +1,28 @@
1
+ import {Stack, Text} from '@sanity/ui'
1
2
  import React, {useMemo} from 'react'
2
3
  import {StringInputProps, useFormValue} from 'sanity'
3
- import {Stack, Text} from '@sanity/ui'
4
+
5
+ import {FeedbackType} from '../../types'
4
6
  import {getTwitterDescriptionValidation} from '../../utils/seoUtils'
5
7
 
6
- const TwitterDescription = (props: StringInputProps) => {
8
+ const TwitterDescription = (props: StringInputProps): React.ReactElement => {
7
9
  const {value, renderDefault, path} = props
8
10
 
9
11
  // Access parent object to get keywords
10
12
  const parent = useFormValue([path[0]]) as {keywords?: string[]; _type?: string}
11
- const isParentseoField = parent && parent?._type === 'seoFields'
12
- const keywords = parent?.keywords || []
13
+ const isParentseoField = parent && parent?._type === 'seoFields'
14
+ const keywords = useMemo(() => parent?.keywords || [], [parent?.keywords])
13
15
 
14
16
  const feedbackItems = useMemo(
15
- () => getTwitterDescriptionValidation(value || '', keywords,isParentseoField),
16
- [value, keywords],
17
+ () => getTwitterDescriptionValidation(value || '', keywords, isParentseoField),
18
+ [value, keywords, isParentseoField],
17
19
  )
18
20
 
19
21
  return (
20
22
  <Stack space={3}>
21
23
  {renderDefault(props)}
22
24
  <Stack space={2}>
23
- {feedbackItems.map((item: any) => (
25
+ {feedbackItems.map((item: FeedbackType) => (
24
26
  <div key={item.text} style={{display: 'flex', alignItems: 'center', gap: 7}}>
25
27
  <div
26
28
  style={{
@@ -1,26 +1,28 @@
1
+ import {Stack, Text} from '@sanity/ui'
1
2
  import React, {useMemo} from 'react'
2
3
  import {StringInputProps, useFormValue} from 'sanity'
3
- import {Stack, Text} from '@sanity/ui'
4
+
5
+ import {FeedbackType} from '../../types'
4
6
  import {getTwitterTitleValidation} from '../../utils/seoUtils'
5
7
 
6
- const TwitterTitle = (props: StringInputProps) => {
8
+ const TwitterTitle = (props: StringInputProps): React.ReactElement => {
7
9
  const {value, renderDefault, path} = props
8
10
 
9
11
  // Access parent object to get keywords
10
12
  const parent = useFormValue([path[0]]) as {keywords?: string[]; _type?: string}
11
13
  const isParentseoField = parent && parent?._type === 'seoFields'
12
- const keywords = parent?.keywords || []
14
+ const keywords = useMemo(() => parent?.keywords || [], [parent?.keywords])
13
15
 
14
16
  const feedbackItems = useMemo(
15
17
  () => getTwitterTitleValidation(value || '', keywords, isParentseoField),
16
- [value, keywords],
18
+ [value, keywords, isParentseoField],
17
19
  )
18
20
 
19
21
  return (
20
22
  <Stack space={3}>
21
23
  {renderDefault(props)}
22
24
  <Stack space={2}>
23
- {feedbackItems.map((item: any) => (
25
+ {feedbackItems.map((item: FeedbackType) => (
24
26
  <div key={item.text} style={{display: 'flex', alignItems: 'center', gap: 7}}>
25
27
  <div
26
28
  style={{
package/src/index.ts CHANGED
@@ -7,14 +7,11 @@ export default seofields
7
7
  // Re-export everything from plugin.ts
8
8
  export * from './plugin'
9
9
 
10
- // Export all TypeScript types
11
- export * from './types'
12
-
13
10
  // Export schema types for external use
14
11
  export {default as seoFieldsSchema} from './schemas'
15
- export {default as openGraphSchema} from './schemas/types/openGraph'
16
- export {default as twitterSchema} from './schemas/types/twitter'
12
+ export {default as allSchemas} from './schemas/types'
17
13
  export {default as metaAttributeSchema} from './schemas/types/metaAttribute'
18
14
  export {default as metaTagSchema} from './schemas/types/metaTag'
19
- export {default as allSchemas} from './schemas/types'
15
+ export {default as openGraphSchema} from './schemas/types/openGraph'
20
16
  export {default as robotsSchema} from './schemas/types/robots'
17
+ export {default as twitterSchema} from './schemas/types/twitter'
package/src/plugin.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // plugin.ts
2
2
  import {definePlugin} from 'sanity'
3
+
3
4
  import types from './schemas/types'
4
5
 
5
6
  export interface SeoFieldConfig {
@@ -38,9 +39,13 @@ export type twitterFieldKeys =
38
39
 
39
40
  export type AllFieldKeys = SeoFieldKeys | openGraphFieldKeys | twitterFieldKeys
40
41
 
42
+ export type ValidHiddenFieldKeys = Exclude<
43
+ AllFieldKeys,
44
+ 'openGraphImageUrl' | 'twitterImageUrl' | 'openGraphImageType' | 'twitterImageType'
45
+ >
46
+
41
47
  export interface FieldVisibilityConfig {
42
- hiddenFields?: AllFieldKeys[]
43
- postType?: string
48
+ hiddenFields?: ValidHiddenFieldKeys[]
44
49
  }
45
50
 
46
51
  export interface SeoFieldsPluginConfig {
@@ -67,22 +72,18 @@ export interface SeoFieldsPluginConfig {
67
72
  * This allows customization of field titles and descriptions.
68
73
  * For example, to change the title of the 'title' field:
69
74
  */
70
- fieldOverrides?: {
71
- [key in AllFieldKeys]?: SeoFieldConfig
72
- }
75
+ fieldOverrides?: Partial<Record<AllFieldKeys, SeoFieldConfig>>
73
76
  /**
74
77
  * A mapping of document types to field visibility configurations.
75
78
  * This allows you to specify which fields should be hidden for specific document types.
76
79
  */
77
- fieldVisibility?: {
78
- [postType: string]: FieldVisibilityConfig
79
- }
80
+ fieldVisibility?: Record<string, FieldVisibilityConfig>
80
81
 
81
82
  /**
82
83
  * A list of fields that should be hidden by default in all document types.
83
84
  * This can be overridden by specific document type settings in `fieldVisibility`.
84
85
  */
85
- defaultHiddenFields?: AllFieldKeys[]
86
+ defaultHiddenFields?: ValidHiddenFieldKeys[]
86
87
  /**
87
88
  * The base URL of your website, used for generating full URLs in the SEO preview.
88
89
  * Defaults to 'https://www.example.com' if not provided.
@@ -1,12 +1,15 @@
1
- import {defineField, defineType} from 'sanity'
2
- import MetaTitle from '../components/meta/MetaTitle'
1
+ import {defineField, defineType, SchemaTypeDefinition} from 'sanity'
2
+
3
3
  import MetaDescription from '../components/meta/MetaDescription'
4
+ import MetaTitle from '../components/meta/MetaTitle'
4
5
  import SeoPreview from '../components/SeoPreview'
5
6
  import {SeoFieldsPluginConfig} from '../plugin'
6
- import {getFieldInfo, getFieldHiddenFunction} from '../utils/fieldsUtils'
7
+ import {getFieldHiddenFunction, getFieldInfo} from '../utils/fieldsUtils'
7
8
  import {isEmpty} from '../utils/utils'
9
+ import openGraph from './types/openGraph'
10
+ import twitter from './types/twitter'
8
11
 
9
- export default function seoFieldsSchema(config: SeoFieldsPluginConfig = {}) {
12
+ export default function seoFieldsSchema(config: SeoFieldsPluginConfig = {}): SchemaTypeDefinition {
10
13
  return defineType({
11
14
  name: 'seoFields',
12
15
  title: 'SEO Fields',
@@ -34,7 +37,7 @@ export default function seoFieldsSchema(config: SeoFieldsPluginConfig = {}) {
34
37
  config.seoPreview.prefix
35
38
  ? {prefix: config.seoPreview.prefix}
36
39
  : {}),
37
- } as any,
40
+ } as Record<string, unknown>,
38
41
  // Use a readOnly field to prevent editing
39
42
  // This field is just for preview purposes
40
43
  initialValue: '' as string, // Set an initial value
@@ -111,16 +114,8 @@ export default function seoFieldsSchema(config: SeoFieldsPluginConfig = {}) {
111
114
  'Specify the canonical URL for this page. This helps prevent duplicate content issues by indicating the preferred version of a page.',
112
115
  hidden: getFieldHiddenFunction('canonicalUrl', config),
113
116
  }),
114
- defineField({
115
- name: 'openGraph',
116
- title: 'Open Graph Settings',
117
- type: 'openGraph',
118
- }),
119
- defineField({
120
- name: 'twitter',
121
- title: 'X (Formerly Twitter) Card Settings',
122
- type: 'twitter',
123
- }),
117
+ openGraph(config) as any,
118
+ twitter(config) as any,
124
119
  ],
125
120
  })
126
121
  }
@@ -1,17 +1,19 @@
1
+ import {SchemaTypeDefinition} from 'sanity'
2
+
3
+ import {SeoFieldsPluginConfig} from '../../plugin'
1
4
  import seoFields from '..'
2
5
  import metaAttribute from './metaAttribute'
6
+ import metaTag from './metaTag'
3
7
  import openGraph from './openGraph'
4
- import twitter from './twitter'
5
8
  import robots from './robots'
6
- import metaTag from './metaTag'
7
- import {SeoFieldsPluginConfig} from '../../plugin'
9
+ import twitter from './twitter'
8
10
 
9
- export default function types(config: SeoFieldsPluginConfig = {}) {
11
+ export default function types(config: SeoFieldsPluginConfig = {}): SchemaTypeDefinition[] {
10
12
  return [
11
13
  seoFields(config), // pass config here
12
14
  openGraph(config), // pass config here
13
15
  twitter(config), // pass config here
14
- metaAttribute,
16
+ metaAttribute as SchemaTypeDefinition,
15
17
  metaTag,
16
18
  robots,
17
19
  ]
@@ -41,22 +41,18 @@ export default defineType({
41
41
  attributeValueString: 'value',
42
42
  attributeValueImage: 'image',
43
43
  },
44
- prepare({
45
- attributeName,
46
- attributeValueString,
47
- attributeValueImage,
48
- }: {
49
- attributeName: string
50
- attributeValueString: string
51
- attributeValueImage: any
52
- }) {
44
+ prepare({attributeName, attributeValueString, attributeValueImage}) {
45
+ let subtitle = ''
46
+ if (attributeValueString) {
47
+ subtitle = `Value: ${attributeValueString}`
48
+ } else if (attributeValueImage) {
49
+ subtitle = 'Value: [Image]'
50
+ } else {
51
+ subtitle = 'No Attribute Value'
52
+ }
53
53
  return {
54
54
  title: attributeName || 'No Attribute Name',
55
- subtitle: attributeValueString
56
- ? `Value: ${attributeValueString}`
57
- : attributeValueImage
58
- ? 'Value: [Image]'
59
- : 'No Attribute Value',
55
+ subtitle: subtitle,
60
56
  media: attributeValueImage,
61
57
  }
62
58
  },
@@ -1,16 +1,17 @@
1
- import { defineType } from "sanity";
1
+ import {defineType} from 'sanity'
2
2
 
3
3
  export default defineType({
4
- name: "metaTag",
5
- title: "Meta Tag",
6
- type: "object",
7
- fields: [
8
- {
9
- name: "metaAttributes",
10
- title: "Meta Attributes",
11
- type: "array",
12
- of: [{ type: "metaAttribute" }],
13
- description: "Add custom meta attributes to the head of the document for additional SEO and social media integration.",
14
- },
15
- ],
16
- })
4
+ name: 'metaTag',
5
+ title: 'Meta Tag',
6
+ type: 'object',
7
+ fields: [
8
+ {
9
+ name: 'metaAttributes',
10
+ title: 'Meta Attributes',
11
+ type: 'array',
12
+ of: [{type: 'metaAttribute'}],
13
+ description:
14
+ 'Add custom meta attributes to the head of the document for additional SEO and social media integration.',
15
+ },
16
+ ],
17
+ })
@@ -1,10 +1,11 @@
1
- import {defineField, defineType} from 'sanity'
2
- import OgTitle from '../../../components/openGraph/OgTitle'
1
+ import {defineField, defineType, SchemaTypeDefinition} from 'sanity'
2
+
3
3
  import OgDescription from '../../../components/openGraph/OgDescription'
4
+ import OgTitle from '../../../components/openGraph/OgTitle'
4
5
  import {SeoFieldsPluginConfig} from '../../../plugin'
5
6
  import {getFieldHiddenFunction, getFieldInfo} from '../../../utils/fieldsUtils'
6
7
 
7
- export default function openGraph(config: SeoFieldsPluginConfig = {}) {
8
+ export default function openGraph(config: SeoFieldsPluginConfig = {}): SchemaTypeDefinition {
8
9
  return defineType({
9
10
  name: 'openGraph',
10
11
  title: 'Open Graph Settings',
@@ -90,13 +91,23 @@ export default function openGraph(config: SeoFieldsPluginConfig = {}) {
90
91
  description: 'A description of the image for accessibility purposes.',
91
92
  }),
92
93
  ],
93
- hidden: ({parent}) => parent?.imageType !== 'upload',
94
+ hidden: (context) => {
95
+ const {parent} = context
96
+ if (parent?.imageType !== 'upload') return true
97
+ const hiddenFn = getFieldHiddenFunction('openGraphImage', config)
98
+ return typeof hiddenFn === 'function' ? hiddenFn(context) : hiddenFn
99
+ },
94
100
  }),
95
101
  defineField({
96
102
  name: 'imageUrl',
97
103
  ...getFieldInfo('openGraphImageUrl', config.fieldOverrides),
98
104
  type: 'url',
99
- hidden: ({parent}) => parent?.imageType !== 'url',
105
+ hidden: (context) => {
106
+ const {parent} = context
107
+ if (parent?.imageType !== 'url') return true
108
+ const hiddenFn = getFieldHiddenFunction('openGraphImage', config)
109
+ return typeof hiddenFn === 'function' ? hiddenFn(context) : hiddenFn
110
+ },
100
111
  }),
101
112
  ],
102
113
  })
@@ -1,10 +1,11 @@
1
- import {defineField, defineType} from 'sanity'
2
- import TwitterTitle from '../../../components/twitter/twitterTitle'
1
+ import {defineField, defineType, SchemaTypeDefinition} from 'sanity'
2
+
3
3
  import TwitterDescription from '../../../components/twitter/twitterDescription'
4
+ import TwitterTitle from '../../../components/twitter/twitterTitle'
4
5
  import {SeoFieldsPluginConfig} from '../../../plugin'
5
6
  import {getFieldHiddenFunction, getFieldInfo} from '../../../utils/fieldsUtils'
6
7
 
7
- export default function twitter(config: SeoFieldsPluginConfig = {}) {
8
+ export default function twitter(config: SeoFieldsPluginConfig = {}): SchemaTypeDefinition {
8
9
  return defineType({
9
10
  name: 'twitter',
10
11
  title: 'X (Formerly Twitter)',
@@ -84,12 +85,23 @@ export default function twitter(config: SeoFieldsPluginConfig = {}) {
84
85
  description: 'A description of the image for accessibility purposes.',
85
86
  }),
86
87
  ],
88
+ hidden: (context) => {
89
+ const {parent} = context
90
+ if (parent?.imageType !== 'upload') return true
91
+ const hiddenFn = getFieldHiddenFunction('twitterImage', config)
92
+ return typeof hiddenFn === 'function' ? hiddenFn(context) : hiddenFn
93
+ },
87
94
  }),
88
95
  defineField({
89
96
  name: 'imageUrl',
90
97
  ...getFieldInfo('twitterImageUrl', config.fieldOverrides),
91
98
  type: 'url',
92
- hidden: ({parent}) => parent?.imageType !== 'url',
99
+ hidden: (context) => {
100
+ const {parent} = context
101
+ if (parent?.imageType !== 'url') return true
102
+ const hiddenFn = getFieldHiddenFunction('twitterImage', config)
103
+ return typeof hiddenFn === 'function' ? hiddenFn(context) : hiddenFn
104
+ },
93
105
  }),
94
106
  ],
95
107
  })
package/src/types.ts CHANGED
@@ -78,69 +78,8 @@ export interface SeoFields {
78
78
  openGraph?: OpenGraphSettings
79
79
  twitter?: TwitterCardSettings
80
80
  }
81
-
82
- // Type guards
83
- export const isSeoFields = (obj: any): obj is SeoFields => {
84
- return obj && obj._type === 'seoFields'
85
- }
86
-
87
- export const isOpenGraphSettings = (obj: any): obj is OpenGraphSettings => {
88
- return obj && obj._type === 'openGraph'
89
- }
90
-
91
- export const isTwitterCardSettings = (obj: any): obj is TwitterCardSettings => {
92
- return obj && obj._type === 'twitter'
93
- }
94
-
95
- export const isMetaAttribute = (obj: any): obj is MetaAttribute => {
96
- return obj && obj._type === 'metaAttribute'
97
- }
98
-
99
- // Utility types for form validation
100
- export interface SeoValidationRules {
101
- title: {
102
- maxLength: number
103
- warningLength: number
104
- }
105
- description: {
106
- maxLength: number
107
- warningLength: number
108
- }
109
- openGraphTitle: {
110
- maxLength: number
111
- }
112
- openGraphDescription: {
113
- maxLength: number
114
- }
115
- twitterTitle: {
116
- maxLength: number
117
- }
118
- twitterDescription: {
119
- maxLength: number
120
- }
121
- }
122
-
123
- export const defaultSeoValidationRules: SeoValidationRules = {
124
- title: {
125
- maxLength: 70,
126
- warningLength: 60,
127
- },
128
- description: {
129
- maxLength: 160,
130
- warningLength: 150,
131
- },
132
- openGraphTitle: {
133
- maxLength: 95,
134
- },
135
- openGraphDescription: {
136
- maxLength: 200,
137
- },
138
- twitterTitle: {
139
- maxLength: 70,
140
- },
141
- twitterDescription: {
142
- maxLength: 200,
143
- },
81
+ export type FeedbackTypeColors = 'green' | 'orange' | 'red'
82
+ export type FeedbackType = {
83
+ text: string
84
+ color: FeedbackTypeColors
144
85
  }
145
-
146
- // All types are already exported above with individual export statements
@@ -1,94 +1,6 @@
1
- // import {defineField, defineType} from 'sanity'
2
- // import OgTitle from '../../../components/openGraph/OgTitle'
3
- // import OgDescription from '../../../components/openGraph/OgDescription'
1
+ import {AllFieldKeys, SeoFieldsPluginConfig, ValidHiddenFieldKeys} from '../plugin'
4
2
 
5
- import {SeoFieldsPluginConfig, AllFieldKeys} from '../plugin'
6
- import twitter from '../schemas/types/twitter'
7
-
8
- // export default defineType({
9
- // name: 'openGraph',
10
- // title: 'Open Graph Settings',
11
- // type: 'object',
12
- // fields: [
13
- // defineField({
14
- // name: 'title',
15
- // title: 'Open Graph Title',
16
- // type: 'string',
17
- // components: {
18
- // input: OgTitle, // Can also wrap with a string input + preview
19
- // },
20
- // }),
21
- // defineField({
22
- // name: 'description',
23
- // title: 'Open Graph Description',
24
- // type: 'text',
25
- // rows: 3,
26
- // components: {
27
- // input: OgDescription, // Can also wrap with a text area + preview
28
- // },
29
- // }),
30
- // defineField({
31
- // name: 'siteName',
32
- // title: 'Open Graph Site Name',
33
- // type: 'string',
34
- // description: 'The name of your website. This is often the site title.',
35
- // }),
36
- // defineField({
37
- // name: 'type',
38
- // title: 'Open Graph Type',
39
- // type: 'string',
40
- // options: {
41
- // list: [
42
- // {title: 'Website', value: 'website'},
43
- // {title: 'Article', value: 'article'},
44
- // {title: 'Profile', value: 'profile'},
45
- // {title: 'Book', value: 'book'},
46
- // {title: 'Music', value: 'music'},
47
- // {title: 'Video', value: 'video'},
48
- // {title: 'Product', value: 'product'},
49
- // ],
50
- // // layout: 'radio', // Display as radio buttons
51
- // },
52
- // initialValue: 'website',
53
- // description: 'Select the type of content for Open Graph.',
54
- // }),
55
-
56
- // // upload image or ask for url
57
- // defineField({
58
- // name: 'imageType',
59
- // title: 'Image Type',
60
- // type: 'string',
61
- // options: {
62
- // list: [
63
- // {title: 'Upload Image', value: 'upload'},
64
- // {title: 'Image URL', value: 'url'},
65
- // ],
66
- // },
67
- // initialValue: 'upload',
68
- // }),
69
- // defineField({
70
- // name: 'image',
71
- // title: 'Open Graph Image',
72
- // type: 'image',
73
- // options: {
74
- // hotspot: true,
75
- // },
76
- // hidden: ({parent}) => parent?.imageType !== 'upload',
77
- // description:
78
- // 'Recommended size: 1200x630px (minimum 600x315px). Max file size: 5MB. Aspect ratio 1.91:1.',
79
- // }),
80
- // defineField({
81
- // name: 'imageUrl',
82
- // title: 'Open Graph Image URL',
83
- // type: 'url',
84
- // hidden: ({parent}) => parent?.imageType !== 'url',
85
- // description:
86
- // 'Enter the full URL of the image. Ensure the image is accessible and meets the recommended size of 1200x630px (minimum 600x315px).',
87
- // }),
88
- // ],
89
- // })
90
-
91
- const DEFAULT_FIELD_INFO = {
3
+ const DEFAULT_FIELD_INFO: Record<AllFieldKeys, {title: string; description: string}> = {
92
4
  title: {
93
5
  title: 'Meta Title',
94
6
  description:
@@ -159,7 +71,7 @@ const DEFAULT_FIELD_INFO = {
159
71
  'Enter the full URL of the image. Ensure the image is accessible and meets the recommended size of 1200x630px (minimum 600x315px).',
160
72
  },
161
73
  twitterCard: {
162
- title: 'Card Type',
74
+ title: 'X Card Type',
163
75
  description: '',
164
76
  },
165
77
  twitterSite: {
@@ -198,12 +110,12 @@ const DEFAULT_FIELD_INFO = {
198
110
  export const getFieldInfo = (
199
111
  fieldName: string,
200
112
  fieldOverrides: SeoFieldsPluginConfig['fieldOverrides'],
201
- ) => {
113
+ ): {title: string; description: string} => {
202
114
  const fieldInfo =
203
115
  (fieldOverrides && fieldOverrides[fieldName as keyof typeof fieldOverrides]) ||
204
116
  DEFAULT_FIELD_INFO[fieldName as keyof typeof DEFAULT_FIELD_INFO]
205
117
  return fieldInfo
206
- ? {title: fieldInfo.title, description: fieldInfo.description}
118
+ ? {title: fieldInfo.title || '', description: fieldInfo.description || ''}
207
119
  : {title: '', description: ''}
208
120
  }
209
121
 
@@ -211,7 +123,7 @@ export const getFieldInfo = (
211
123
  * Check if a field should be hidden based on the current document type
212
124
  */
213
125
  export const isFieldHidden = (
214
- fieldName: AllFieldKeys,
126
+ fieldName: ValidHiddenFieldKeys,
215
127
  config: SeoFieldsPluginConfig,
216
128
  documentType?: string,
217
129
  ): boolean => {
@@ -231,8 +143,17 @@ export const isFieldHidden = (
231
143
  /**
232
144
  * Get the hidden function for any field
233
145
  */
234
- export const getFieldHiddenFunction = (fieldName: AllFieldKeys, config: SeoFieldsPluginConfig) => {
235
- return ({document}: {document?: any}) => {
146
+ export const getFieldHiddenFunction = (
147
+ fieldName: ValidHiddenFieldKeys,
148
+ config: SeoFieldsPluginConfig,
149
+ ) => {
150
+ return ({
151
+ document,
152
+ }: {
153
+ document?: {
154
+ _type?: string
155
+ }
156
+ }): boolean => {
236
157
  const documentType = document?._type
237
158
  return isFieldHidden(fieldName, config, documentType)
238
159
  }
@@ -1,4 +1,4 @@
1
- import {PathSegment, useFormValue} from 'sanity'
1
+ import {FeedbackType} from '../types'
2
2
 
3
3
  export const stopWords = ['the', 'a', 'an', 'and', 'or', 'but']
4
4
 
@@ -33,8 +33,8 @@ export const primaryKeywordAtStart = (title: string, keywords: string[]): boolea
33
33
  return title.toLowerCase().startsWith(keywords[0].toLowerCase())
34
34
  }
35
35
 
36
- export const truncate = (text: string, maxLength: number) =>
37
- text.length > maxLength ? text.slice(0, maxLength) + '…' : text
36
+ export const truncate = (text: string, maxLength: number): string =>
37
+ text.length > maxLength ? `${text.slice(0, maxLength)}…` : text
38
38
 
39
39
  export const hasExcessivePunctuation = (title: string): boolean => /[!@#$%^&*]{2,}/.test(title)
40
40
 
@@ -42,8 +42,8 @@ export const getMetaTitleValidationMessages = (
42
42
  title: string,
43
43
  keywords: string[],
44
44
  isParentseoField: boolean,
45
- ) => {
46
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
45
+ ): FeedbackType[] => {
46
+ const feedback: FeedbackType[] = []
47
47
 
48
48
  const minChar = 50
49
49
  const maxChar = 60
@@ -108,8 +108,8 @@ export const getMetaDescriptionValidationMessages = (
108
108
  description: string,
109
109
  keywords: string[],
110
110
  isParentseoField: boolean,
111
- ) => {
112
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
111
+ ): FeedbackType[] => {
112
+ const feedback: FeedbackType[] = []
113
113
 
114
114
  const minChar = 150
115
115
  const maxChar = 160
@@ -180,8 +180,8 @@ export const getOgTitleValidation = (
180
180
  title: string,
181
181
  keywords: string[] = [],
182
182
  isParentseoField: boolean,
183
- ) => {
184
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
183
+ ): FeedbackType[] => {
184
+ const feedback: FeedbackType[] = []
185
185
  const min = 40
186
186
  const max = 60
187
187
  const count = title?.length || 0
@@ -244,8 +244,8 @@ export const getOgDescriptionValidation = (
244
244
  desc: string,
245
245
  keywords: string[] = [],
246
246
  isParentseoField: boolean,
247
- ) => {
248
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
247
+ ): FeedbackType[] => {
248
+ const feedback: FeedbackType[] = []
249
249
  const min = 90
250
250
  const max = 120
251
251
  const count = desc?.length || 0
@@ -317,8 +317,8 @@ export const getTwitterTitleValidation = (
317
317
  title: string,
318
318
  keywords: string[] = [],
319
319
  isParentseoField: boolean,
320
- ) => {
321
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
320
+ ): FeedbackType[] => {
321
+ const feedback: FeedbackType[] = []
322
322
  const min = 30
323
323
  const max = 70
324
324
  const count = title?.length || 0
@@ -370,8 +370,8 @@ export const getTwitterDescriptionValidation = (
370
370
  desc: string,
371
371
  keywords: string[] = [],
372
372
  isParentseoField: boolean,
373
- ) => {
374
- const feedback: {text: string; color: 'green' | 'orange' | 'red'}[] = []
373
+ ): FeedbackType[] => {
374
+ const feedback: FeedbackType[] = []
375
375
  const min = 50
376
376
  const max = 200
377
377
  const count = desc?.length || 0