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.
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/index.d.mts +226 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +730 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +733 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -0
- package/sanity.json +8 -0
- package/src/components/SeoPreview.tsx +75 -0
- package/src/components/meta/MetaDescription.tsx +47 -0
- package/src/components/meta/MetaTitle.tsx +52 -0
- package/src/components/openGraph/OgDescription.tsx +43 -0
- package/src/components/openGraph/OgTitle.tsx +42 -0
- package/src/components/twitter/twitterDescription.tsx +42 -0
- package/src/components/twitter/twitterTitle.tsx +42 -0
- package/src/index.ts +15 -0
- package/src/plugin.ts +28 -0
- package/src/schemas/index.ts +93 -0
- package/src/schemas/types/index.ts +7 -0
- package/src/schemas/types/metaAttribute/index.ts +64 -0
- package/src/schemas/types/metaTag/index.ts +16 -0
- package/src/schemas/types/openGraph/index.ts +86 -0
- package/src/schemas/types/robots/index.ts +26 -0
- package/src/schemas/types/twitter/index.ts +68 -0
- package/src/types/index.ts +239 -0
- package/src/types.ts +144 -0
- package/src/utils/generaeDynamicJsonLd.ts +295 -0
- package/src/utils/seoUtils.ts +386 -0
- package/v2-incompatible.js +11 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// TypeScript interfaces for SEO Fields Plugin
|
|
2
|
+
|
|
3
|
+
// Base Sanity types
|
|
4
|
+
export interface SanityImage {
|
|
5
|
+
_type: 'image'
|
|
6
|
+
asset: {
|
|
7
|
+
_ref: string
|
|
8
|
+
_type: 'reference'
|
|
9
|
+
}
|
|
10
|
+
hotspot?: {
|
|
11
|
+
x: number
|
|
12
|
+
y: number
|
|
13
|
+
height: number
|
|
14
|
+
width: number
|
|
15
|
+
}
|
|
16
|
+
crop?: {
|
|
17
|
+
top: number
|
|
18
|
+
bottom: number
|
|
19
|
+
left: number
|
|
20
|
+
right: number
|
|
21
|
+
}
|
|
22
|
+
alt?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SanityImageWithAlt extends SanityImage {
|
|
26
|
+
alt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Robots settings
|
|
30
|
+
export interface RobotsSettings {
|
|
31
|
+
noIndex?: boolean
|
|
32
|
+
noFollow?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Meta Attribute
|
|
36
|
+
export interface MetaAttribute {
|
|
37
|
+
_type: 'metaAttribute'
|
|
38
|
+
key: string
|
|
39
|
+
type: 'string' | 'image'
|
|
40
|
+
value?: string
|
|
41
|
+
image?: SanityImage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Open Graph settings
|
|
45
|
+
export interface OpenGraphSettings {
|
|
46
|
+
_type: 'openGraph'
|
|
47
|
+
title?: string
|
|
48
|
+
description?: string
|
|
49
|
+
siteName?: string
|
|
50
|
+
type?: 'website' | 'article' | 'profile' | 'book' | 'music' | 'video' | 'product'
|
|
51
|
+
imageType?: 'upload' | 'url'
|
|
52
|
+
image?: SanityImage
|
|
53
|
+
imageUrl?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Twitter Card settings
|
|
57
|
+
export interface TwitterCardSettings {
|
|
58
|
+
_type: 'twitter'
|
|
59
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
60
|
+
site?: string
|
|
61
|
+
title?: string
|
|
62
|
+
description?: string
|
|
63
|
+
image?: SanityImageWithAlt
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Main SEO Fields interface
|
|
67
|
+
export interface SeoFields {
|
|
68
|
+
_type: 'seoFields'
|
|
69
|
+
robots?: RobotsSettings
|
|
70
|
+
preview?: string
|
|
71
|
+
title?: string
|
|
72
|
+
description?: string
|
|
73
|
+
metaImage?: SanityImage
|
|
74
|
+
keywords?: string[]
|
|
75
|
+
canonicalUrl?: string
|
|
76
|
+
openGraph?: OpenGraphSettings
|
|
77
|
+
twitter?: TwitterCardSettings
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Type guards
|
|
81
|
+
export const isSeoFields = (obj: any): obj is SeoFields => {
|
|
82
|
+
return obj && obj._type === 'seoFields'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const isOpenGraphSettings = (obj: any): obj is OpenGraphSettings => {
|
|
86
|
+
return obj && obj._type === 'openGraph'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const isTwitterCardSettings = (obj: any): obj is TwitterCardSettings => {
|
|
90
|
+
return obj && obj._type === 'twitter'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const isMetaAttribute = (obj: any): obj is MetaAttribute => {
|
|
94
|
+
return obj && obj._type === 'metaAttribute'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Utility types for form validation
|
|
98
|
+
export interface SeoValidationRules {
|
|
99
|
+
title: {
|
|
100
|
+
maxLength: number
|
|
101
|
+
warningLength: number
|
|
102
|
+
}
|
|
103
|
+
description: {
|
|
104
|
+
maxLength: number
|
|
105
|
+
warningLength: number
|
|
106
|
+
}
|
|
107
|
+
openGraphTitle: {
|
|
108
|
+
maxLength: number
|
|
109
|
+
}
|
|
110
|
+
openGraphDescription: {
|
|
111
|
+
maxLength: number
|
|
112
|
+
}
|
|
113
|
+
twitterTitle: {
|
|
114
|
+
maxLength: number
|
|
115
|
+
}
|
|
116
|
+
twitterDescription: {
|
|
117
|
+
maxLength: number
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const defaultSeoValidationRules: SeoValidationRules = {
|
|
122
|
+
title: {
|
|
123
|
+
maxLength: 70,
|
|
124
|
+
warningLength: 60,
|
|
125
|
+
},
|
|
126
|
+
description: {
|
|
127
|
+
maxLength: 160,
|
|
128
|
+
warningLength: 150,
|
|
129
|
+
},
|
|
130
|
+
openGraphTitle: {
|
|
131
|
+
maxLength: 95,
|
|
132
|
+
},
|
|
133
|
+
openGraphDescription: {
|
|
134
|
+
maxLength: 200,
|
|
135
|
+
},
|
|
136
|
+
twitterTitle: {
|
|
137
|
+
maxLength: 70,
|
|
138
|
+
},
|
|
139
|
+
twitterDescription: {
|
|
140
|
+
maxLength: 200,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// All types are already exported above with individual export statements
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {SchemaTypeDefinition} from 'sanity'
|
|
2
|
+
// Import all schema definitions
|
|
3
|
+
import allSchemas from '../schemas/types'
|
|
4
|
+
|
|
5
|
+
interface JsonSchemaProperty {
|
|
6
|
+
type?: string | string[]
|
|
7
|
+
items?: JsonSchemaProperty
|
|
8
|
+
properties?: Record<string, JsonSchemaProperty>
|
|
9
|
+
required?: string[]
|
|
10
|
+
enum?: string[]
|
|
11
|
+
const?: any
|
|
12
|
+
default?: any
|
|
13
|
+
description?: string
|
|
14
|
+
maxLength?: number
|
|
15
|
+
minLength?: number
|
|
16
|
+
pattern?: string
|
|
17
|
+
format?: string
|
|
18
|
+
$ref?: string
|
|
19
|
+
allOf?: JsonSchemaProperty[]
|
|
20
|
+
additionalProperties?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface JsonSchema {
|
|
24
|
+
$schema: string
|
|
25
|
+
title: string
|
|
26
|
+
description: string
|
|
27
|
+
type: string
|
|
28
|
+
definitions: Record<string, JsonSchemaProperty>
|
|
29
|
+
properties: Record<string, JsonSchemaProperty>
|
|
30
|
+
additionalProperties: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts Sanity field type to JSON Schema type
|
|
35
|
+
*/
|
|
36
|
+
function mapSanityTypeToJsonSchema(sanityType: string): string | string[] {
|
|
37
|
+
const typeMap: Record<string, string | string[]> = {
|
|
38
|
+
string: 'string',
|
|
39
|
+
text: 'string',
|
|
40
|
+
number: 'number',
|
|
41
|
+
boolean: 'boolean',
|
|
42
|
+
array: 'array',
|
|
43
|
+
object: 'object',
|
|
44
|
+
image: 'object',
|
|
45
|
+
url: 'string',
|
|
46
|
+
date: 'string',
|
|
47
|
+
datetime: 'string',
|
|
48
|
+
reference: 'object',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return typeMap[sanityType] || 'string'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts Sanity field definition to JSON Schema property
|
|
56
|
+
*/
|
|
57
|
+
function convertSanityFieldToJsonSchema(field: any): JsonSchemaProperty {
|
|
58
|
+
const jsonSchemaProperty: JsonSchemaProperty = {
|
|
59
|
+
type: mapSanityTypeToJsonSchema(field.type),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Add description
|
|
63
|
+
if (field.description) {
|
|
64
|
+
jsonSchemaProperty.description = field.description
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle validation rules
|
|
68
|
+
if (field.validation) {
|
|
69
|
+
// This would need to be expanded based on your validation rules
|
|
70
|
+
// For now, we'll handle common cases
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle different field types
|
|
74
|
+
switch (field.type) {
|
|
75
|
+
case 'string':
|
|
76
|
+
if (field.options?.list) {
|
|
77
|
+
jsonSchemaProperty.enum = field.options.list.map((item: any) =>
|
|
78
|
+
typeof item === 'string' ? item : item.value,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (field.validation) {
|
|
82
|
+
// Add length constraints if available
|
|
83
|
+
}
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
case 'text':
|
|
87
|
+
jsonSchemaProperty.type = 'string'
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
case 'url':
|
|
91
|
+
jsonSchemaProperty.type = 'string'
|
|
92
|
+
jsonSchemaProperty.format = 'uri'
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
case 'array':
|
|
96
|
+
jsonSchemaProperty.type = 'array'
|
|
97
|
+
if (field.of && field.of[0]) {
|
|
98
|
+
if (typeof field.of[0] === 'string') {
|
|
99
|
+
jsonSchemaProperty.items = {type: 'string'}
|
|
100
|
+
} else if (field.of[0].type) {
|
|
101
|
+
jsonSchemaProperty.items = convertSanityFieldToJsonSchema(field.of[0])
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
case 'object':
|
|
107
|
+
jsonSchemaProperty.type = 'object'
|
|
108
|
+
if (field.fields) {
|
|
109
|
+
jsonSchemaProperty.properties = {}
|
|
110
|
+
const required: string[] = []
|
|
111
|
+
|
|
112
|
+
field.fields.forEach((subField: any) => {
|
|
113
|
+
if (subField.name) {
|
|
114
|
+
jsonSchemaProperty.properties![subField.name] = convertSanityFieldToJsonSchema(subField)
|
|
115
|
+
if (subField.required) {
|
|
116
|
+
required.push(subField.name)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (required.length > 0) {
|
|
122
|
+
jsonSchemaProperty.required = required
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
jsonSchemaProperty.additionalProperties = false
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
case 'image':
|
|
129
|
+
jsonSchemaProperty.$ref = '#/definitions/SanityImage'
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
case 'reference':
|
|
133
|
+
jsonSchemaProperty.type = 'object'
|
|
134
|
+
jsonSchemaProperty.properties = {
|
|
135
|
+
_ref: {type: 'string'},
|
|
136
|
+
_type: {type: 'string', const: 'reference'},
|
|
137
|
+
}
|
|
138
|
+
jsonSchemaProperty.required = ['_ref', '_type']
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle initial values as defaults
|
|
143
|
+
if (field.initialValue !== undefined) {
|
|
144
|
+
jsonSchemaProperty.default = field.initialValue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle hidden fields (not really applicable to JSON Schema but we can note it)
|
|
148
|
+
if (field.hidden) {
|
|
149
|
+
jsonSchemaProperty.description = (jsonSchemaProperty.description || '') + ' (Hidden field)'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Handle read-only fields
|
|
153
|
+
if (field.readOnly) {
|
|
154
|
+
jsonSchemaProperty.description = (jsonSchemaProperty.description || '') + ' (Read-only)'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return jsonSchemaProperty
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Converts a Sanity schema definition to JSON Schema definition
|
|
162
|
+
*/
|
|
163
|
+
function convertSanitySchemaToJsonSchema(schemaDefinition: any): JsonSchemaProperty {
|
|
164
|
+
const jsonSchemaDefinition: JsonSchemaProperty = {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {},
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add _type property
|
|
171
|
+
if (schemaDefinition.name) {
|
|
172
|
+
jsonSchemaDefinition.properties!._type = {
|
|
173
|
+
type: 'string',
|
|
174
|
+
const: schemaDefinition.name,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Add title and description
|
|
179
|
+
if (schemaDefinition.title) {
|
|
180
|
+
jsonSchemaDefinition.description = schemaDefinition.title
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Convert fields
|
|
184
|
+
if ('fields' in schemaDefinition && schemaDefinition.fields) {
|
|
185
|
+
const required: string[] = ['_type']
|
|
186
|
+
|
|
187
|
+
schemaDefinition.fields.forEach((field: any) => {
|
|
188
|
+
if (field.name) {
|
|
189
|
+
jsonSchemaDefinition.properties![field.name] = convertSanityFieldToJsonSchema(field)
|
|
190
|
+
if (field.required) {
|
|
191
|
+
required.push(field.name)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
if (required.length > 1) {
|
|
197
|
+
// More than just _type
|
|
198
|
+
jsonSchemaDefinition.required = required
|
|
199
|
+
} else {
|
|
200
|
+
jsonSchemaDefinition.required = ['_type']
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return jsonSchemaDefinition
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Generates a complete JSON Schema from Sanity schema definitions
|
|
209
|
+
*/
|
|
210
|
+
function generateSchemaJson(): JsonSchema {
|
|
211
|
+
const schemas = allSchemas
|
|
212
|
+
|
|
213
|
+
const jsonSchema: JsonSchema = {
|
|
214
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
215
|
+
title: 'Sanity SEO Fields Plugin Schema',
|
|
216
|
+
description:
|
|
217
|
+
'Schema definitions for SEO fields including meta tags, Open Graph, and Twitter Card settings',
|
|
218
|
+
type: 'object',
|
|
219
|
+
definitions: {},
|
|
220
|
+
properties: {},
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Add SanityImage definition
|
|
225
|
+
jsonSchema.definitions.SanityImage = {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
_type: {type: 'string', const: 'image'},
|
|
229
|
+
asset: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
properties: {
|
|
232
|
+
_ref: {type: 'string'},
|
|
233
|
+
_type: {type: 'string', const: 'reference'},
|
|
234
|
+
},
|
|
235
|
+
required: ['_ref', '_type'],
|
|
236
|
+
},
|
|
237
|
+
hotspot: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
x: {type: 'number'},
|
|
241
|
+
y: {type: 'number'},
|
|
242
|
+
height: {type: 'number'},
|
|
243
|
+
width: {type: 'number'},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
crop: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
top: {type: 'number'},
|
|
250
|
+
bottom: {type: 'number'},
|
|
251
|
+
left: {type: 'number'},
|
|
252
|
+
right: {type: 'number'},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
alt: {type: 'string'},
|
|
256
|
+
},
|
|
257
|
+
required: ['_type', 'asset'],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Convert each schema
|
|
261
|
+
schemas.forEach((schema) => {
|
|
262
|
+
if (schema.name) {
|
|
263
|
+
const jsonSchemaDefinition = convertSanitySchemaToJsonSchema(schema)
|
|
264
|
+
jsonSchema.definitions[schema.name] = jsonSchemaDefinition
|
|
265
|
+
jsonSchema.properties[schema.name] = {$ref: `#/definitions/${schema.name}`}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return jsonSchema
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Writes the generated schema to a file
|
|
274
|
+
*/
|
|
275
|
+
export function writeSchemaJson(outputPath: string = './schema.json'): void {
|
|
276
|
+
const schema = generateSchemaJson()
|
|
277
|
+
const fs = require('fs')
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
fs.writeFileSync(outputPath, JSON.stringify(schema, null, 2))
|
|
281
|
+
console.log(`✅ Schema.json generated successfully at: ${outputPath}`)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('❌ Error writing schema.json:', error)
|
|
284
|
+
throw error
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns the generated schema as an object
|
|
290
|
+
*/
|
|
291
|
+
export function getSchemaJson(): JsonSchema {
|
|
292
|
+
return generateSchemaJson()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default generateSchemaJson
|