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/dist/index.js ADDED
@@ -0,0 +1,730 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), react = require("react"), ui = require("@sanity/ui");
4
+ const stopWords = ["the", "a", "an", "and", "or", "but"], hasMatchingKeyword = (title, keywordList) => {
5
+ if (!title || keywordList.length === 0) return !1;
6
+ const lowerTitle = title.toLowerCase();
7
+ return keywordList.some((keyword) => keyword && lowerTitle.includes(keyword.toLowerCase()));
8
+ }, hasKeywordOveruse = (title, keywordList, maxOccurrences = 3) => {
9
+ if (!title || keywordList.length === 0) return !1;
10
+ const lowerTitle = title.toLowerCase();
11
+ return keywordList.some((keyword) => {
12
+ if (!keyword) return !1;
13
+ const matches = lowerTitle.match(new RegExp(keyword.toLowerCase(), "g"));
14
+ return matches ? matches.length > maxOccurrences : !1;
15
+ });
16
+ }, startsWithStopWord = (title) => {
17
+ if (!title) return !1;
18
+ const firstWord = title.trim().split(" ")[0].toLowerCase();
19
+ return stopWords.includes(firstWord);
20
+ }, truncate = (text, maxLength) => text.length > maxLength ? text.slice(0, maxLength) + "\u2026" : text, hasExcessivePunctuation = (title) => /[!@#$%^&*]{2,}/.test(title), getMetaTitleValidationMessages = (title, keywords) => {
21
+ const feedback = [], charCount = title?.length || 0;
22
+ if (!title?.trim())
23
+ return feedback.push({ text: "Meta Title is empty. Add content to improve SEO.", color: "red" }), feedback;
24
+ if (charCount < 50 ? feedback.push({
25
+ text: `Title is ${charCount} characters \u2014 below recommended 50.`,
26
+ color: "orange"
27
+ }) : charCount > 60 ? feedback.push({
28
+ text: `Title is ${charCount} characters \u2014 exceeds recommended 60.`,
29
+ color: "red"
30
+ }) : feedback.push({ text: `Title length (${charCount}) looks good for SEO.`, color: "green" }), keywords.length > 0) {
31
+ const hasKeyword = hasMatchingKeyword(title, keywords);
32
+ feedback.push({
33
+ text: hasKeyword ? "Keyword found in title \u2014 good job!" : "Keywords defined but missing in title.",
34
+ color: hasKeyword ? "green" : "red"
35
+ }), hasKeywordOveruse(title, keywords) && feedback.push({
36
+ text: "Keyword appears too many times \u2014 avoid keyword stuffing.",
37
+ color: "orange"
38
+ });
39
+ } else
40
+ feedback.push({
41
+ text: "No keywords defined. Consider adding relevant keywords.",
42
+ color: "orange"
43
+ });
44
+ return startsWithStopWord(title) && feedback.push({ text: "Title starts with a stop word \u2014 consider rephrasing.", color: "orange" }), hasExcessivePunctuation(title) && feedback.push({ text: "Title contains excessive punctuation \u2014 simplify it.", color: "orange" }), feedback;
45
+ }, getMetaDescriptionValidationMessages = (description, keywords) => {
46
+ const feedback = [], charCount = description?.length || 0;
47
+ if (!description?.trim())
48
+ return feedback.push({ text: "Meta description is empty. Add content to improve SEO.", color: "red" }), feedback;
49
+ if (charCount < 150 ? feedback.push({
50
+ text: `Description is ${charCount} chars \u2014 below recommended 150.`,
51
+ color: "orange"
52
+ }) : charCount > 160 ? feedback.push({
53
+ text: `Description is ${charCount} chars \u2014 exceeds recommended 160.`,
54
+ color: "red"
55
+ }) : feedback.push({ text: `Description length (${charCount}) looks good for SEO.`, color: "green" }), keywords.length > 0) {
56
+ const hasKeyword = hasMatchingKeyword(description, keywords);
57
+ feedback.push({
58
+ text: hasKeyword ? "Keyword found in description \u2014 good job!" : "Keywords defined but missing in description.",
59
+ color: hasKeyword ? "green" : "red"
60
+ }), hasKeywordOveruse(description, keywords) && feedback.push({
61
+ text: "Keyword appears too many times \u2014 avoid keyword stuffing.",
62
+ color: "orange"
63
+ });
64
+ } else
65
+ feedback.push({
66
+ text: "No keywords defined. Consider adding relevant keywords.",
67
+ color: "orange"
68
+ });
69
+ return startsWithStopWord(description) && feedback.push({
70
+ text: "Description starts with a stop word \u2014 consider rephrasing.",
71
+ color: "orange"
72
+ }), hasExcessivePunctuation(description) && feedback.push({
73
+ text: "Description contains excessive punctuation \u2014 simplify it.",
74
+ color: "orange"
75
+ }), feedback;
76
+ }, getOgTitleValidation = (title, keywords = []) => {
77
+ const feedback = [], count = title?.length || 0;
78
+ if (!title?.trim())
79
+ return feedback.push({ text: "OG Title is empty. Add content for better social preview.", color: "red" }), feedback;
80
+ if (count < 40 ? feedback.push({
81
+ text: `OG Title is ${count} chars \u2014 shorter than recommended 40.`,
82
+ color: "orange"
83
+ }) : count > 60 ? feedback.push({ text: `OG Title is ${count} chars \u2014 exceeds recommended 60.`, color: "red" }) : feedback.push({ text: `OG Title length (${count}) looks good.`, color: "green" }), keywords.length > 0) {
84
+ const hasKeyword = hasMatchingKeyword(title, keywords);
85
+ feedback.push({
86
+ text: hasKeyword ? "Keyword found in OG title \u2014 good job!" : "Keywords defined but missing in OG title.",
87
+ color: hasKeyword ? "green" : "red"
88
+ }), hasKeywordOveruse(title, keywords) && feedback.push({
89
+ text: "Keyword appears too many times in OG title \u2014 avoid keyword stuffing.",
90
+ color: "orange"
91
+ });
92
+ } else
93
+ feedback.push({
94
+ text: "No keywords defined. Consider adding relevant keywords.",
95
+ color: "orange"
96
+ });
97
+ return startsWithStopWord(title) && feedback.push({
98
+ text: "OG Title starts with a stop word \u2014 consider rephrasing.",
99
+ color: "orange"
100
+ }), hasExcessivePunctuation(title) && feedback.push({ text: "OG Title contains excessive punctuation \u2014 simplify it.", color: "orange" }), feedback;
101
+ }, getOgDescriptionValidation = (desc, keywords = []) => {
102
+ const feedback = [], count = desc?.length || 0;
103
+ if (!desc?.trim())
104
+ return feedback.push({
105
+ text: "OG Description is empty. Add content for better social preview.",
106
+ color: "red"
107
+ }), feedback;
108
+ if (count < 90 ? feedback.push({
109
+ text: `OG Description is ${count} chars \u2014 shorter than recommended 90.`,
110
+ color: "orange"
111
+ }) : count > 120 ? feedback.push({
112
+ text: `OG Description is ${count} chars \u2014 exceeds recommended 120.`,
113
+ color: "red"
114
+ }) : feedback.push({ text: `OG Description length (${count}) looks good.`, color: "green" }), keywords.length > 0) {
115
+ const hasKeyword = hasMatchingKeyword(desc, keywords);
116
+ feedback.push({
117
+ text: hasKeyword ? "Keyword found in OG description \u2014 good job!" : "Keywords defined but missing in OG description.",
118
+ color: hasKeyword ? "green" : "red"
119
+ }), hasKeywordOveruse(desc, keywords) && feedback.push({
120
+ text: "Keyword appears too many times in OG description \u2014 avoid keyword stuffing.",
121
+ color: "orange"
122
+ });
123
+ } else
124
+ feedback.push({
125
+ text: "No keywords defined. Consider adding relevant keywords.",
126
+ color: "orange"
127
+ });
128
+ return startsWithStopWord(desc) && feedback.push({
129
+ text: "OG Description starts with a stop word \u2014 consider rephrasing.",
130
+ color: "orange"
131
+ }), hasExcessivePunctuation(desc) && feedback.push({
132
+ text: "OG Description contains excessive punctuation \u2014 simplify it.",
133
+ color: "orange"
134
+ }), feedback;
135
+ }, getTwitterTitleValidation = (title, keywords = []) => {
136
+ const feedback = [], count = title?.length || 0;
137
+ if (!title?.trim())
138
+ return feedback.push({ text: "Twitter Title is empty. Add content for better SEO.", color: "red" }), feedback;
139
+ if (count < 30 ? feedback.push({
140
+ text: `Twitter Title is ${count} chars \u2014 shorter than recommended 30.`,
141
+ color: "orange"
142
+ }) : count > 70 ? feedback.push({
143
+ text: `Twitter Title is ${count} chars \u2014 exceeds recommended 70.`,
144
+ color: "red"
145
+ }) : feedback.push({ text: `Twitter Title length (${count}) looks good.`, color: "green" }), keywords.length > 0) {
146
+ const hasKeyword = hasMatchingKeyword(title, keywords);
147
+ feedback.push({
148
+ text: hasKeyword ? "Keyword found in Twitter title \u2014 good job!" : "Keywords defined but missing in Twitter title.",
149
+ color: hasKeyword ? "green" : "red"
150
+ });
151
+ } else
152
+ feedback.push({
153
+ text: "No keywords defined. Consider adding relevant keywords.",
154
+ color: "orange"
155
+ });
156
+ return /[!@#$%^&*]{2,}/.test(title) && feedback.push({ text: "Twitter Title has excessive punctuation \u2014 simplify it.", color: "orange" }), feedback;
157
+ }, getTwitterDescriptionValidation = (desc, keywords = []) => {
158
+ const feedback = [], count = desc?.length || 0;
159
+ if (!desc?.trim())
160
+ return feedback.push({ text: "Twitter Description is empty. Add content for better SEO.", color: "red" }), feedback;
161
+ if (count < 50 ? feedback.push({
162
+ text: `Twitter Description is ${count} chars \u2014 shorter than recommended 50.`,
163
+ color: "orange"
164
+ }) : count > 200 ? feedback.push({
165
+ text: `Twitter Description is ${count} chars \u2014 exceeds recommended 200.`,
166
+ color: "red"
167
+ }) : feedback.push({ text: `Twitter Description length (${count}) looks good.`, color: "green" }), keywords.length > 0) {
168
+ const hasKeyword = hasMatchingKeyword(desc, keywords);
169
+ feedback.push({
170
+ text: hasKeyword ? "Keyword found in Twitter description \u2014 good job!" : "Keywords defined but missing in Twitter description.",
171
+ color: hasKeyword ? "green" : "red"
172
+ });
173
+ } else
174
+ feedback.push({
175
+ text: "No keywords defined. Consider adding relevant keywords.",
176
+ color: "orange"
177
+ });
178
+ return /[!@#$%^&*]{2,}/.test(desc) && feedback.push({
179
+ text: "Twitter Description has excessive punctuation \u2014 simplify it.",
180
+ color: "orange"
181
+ }), feedback;
182
+ }, MetaTitle = (props) => {
183
+ const client = sanity.useClient({ apiVersion: "2024-05-05" }), { value, onChange, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [];
184
+ react.useEffect(() => {
185
+ value || (async () => {
186
+ const data = await client.fetch("*[_type=='homePage'][0]{'title':seo.title}");
187
+ data?.title && !value && onChange(sanity.set(data.title));
188
+ })();
189
+ }, [client, onChange, value]);
190
+ const feedbackItems = react.useMemo(
191
+ () => getMetaTitleValidationMessages(value || "", keywords),
192
+ [value, keywords]
193
+ );
194
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
195
+ renderDefault(props),
196
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
197
+ /* @__PURE__ */ jsxRuntime.jsx(
198
+ "div",
199
+ {
200
+ style: {
201
+ minWidth: 10,
202
+ height: 10,
203
+ borderRadius: "50%",
204
+ backgroundColor: item.color
205
+ }
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
209
+ ] }, item.text)) })
210
+ ] });
211
+ }, MetaDescription = (props) => {
212
+ const client = sanity.useClient({ apiVersion: "2024-05-05" }), { value, onChange, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [];
213
+ react.useEffect(() => {
214
+ value || (async () => {
215
+ const data = await client.fetch("*[_type=='homePage'][0]{'description':seo.description}");
216
+ data?.description && !value && onChange(sanity.set(data.description));
217
+ })();
218
+ }, [client, onChange, value]);
219
+ const feedbackItems = react.useMemo(
220
+ () => getMetaDescriptionValidationMessages(value || "", keywords),
221
+ [value, keywords]
222
+ );
223
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
224
+ renderDefault(props),
225
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
226
+ /* @__PURE__ */ jsxRuntime.jsx(
227
+ "div",
228
+ {
229
+ style: { width: 10, height: 10, borderRadius: "50%", backgroundColor: item.color }
230
+ }
231
+ ),
232
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
233
+ ] }, item.text)) })
234
+ ] });
235
+ }, SeoPreview = (props) => {
236
+ const { path } = props, parent = sanity.useFormValue([path[0]]) || {
237
+ title: "",
238
+ description: "",
239
+ canonicalUrl: ""
240
+ };
241
+ console.log("SEO Preview Parent:", parent);
242
+ const {
243
+ title,
244
+ description,
245
+ canonicalUrl: url
246
+ } = parent;
247
+ return /* @__PURE__ */ jsxRuntime.jsx(
248
+ ui.Box,
249
+ {
250
+ padding: 3,
251
+ style: {
252
+ maxWidth: 600,
253
+ fontFamily: "Arial, sans-serif"
254
+ },
255
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
256
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, style: { color: "#006621", fontSize: 12, lineHeight: 1.3, marginBottom: 3 }, children: url || "https://www.example.com/page-url" }),
257
+ /* @__PURE__ */ jsxRuntime.jsx(
258
+ ui.Text,
259
+ {
260
+ size: 3,
261
+ weight: "bold",
262
+ style: {
263
+ color: "#1a0dab",
264
+ fontSize: 18,
265
+ lineHeight: 1.4,
266
+ marginBottom: 3
267
+ },
268
+ children: title ? truncate(title, 60) : "Meta Title Preview"
269
+ }
270
+ ),
271
+ /* @__PURE__ */ jsxRuntime.jsx(
272
+ ui.Text,
273
+ {
274
+ size: 2,
275
+ style: {
276
+ color: "#545454",
277
+ fontSize: 14,
278
+ lineHeight: 1.6
279
+ },
280
+ children: description ? truncate(description, 160) : "Meta description will appear here\u2026"
281
+ }
282
+ )
283
+ ] })
284
+ }
285
+ );
286
+ };
287
+ var seoFields = sanity.defineType({
288
+ name: "seoFields",
289
+ title: "SEO Fields",
290
+ type: "object",
291
+ fields: [
292
+ sanity.defineField({
293
+ name: "robots",
294
+ title: "Robots Settings",
295
+ type: "robots"
296
+ // Use the separate robots type here
297
+ }),
298
+ sanity.defineField({
299
+ name: "preview",
300
+ title: "SEO Preview",
301
+ type: "string",
302
+ components: {
303
+ input: SeoPreview
304
+ // custom React component
305
+ },
306
+ // you can mark it read-only if you don't want editors to change it
307
+ readOnly: !0
308
+ }),
309
+ sanity.defineField({
310
+ name: "title",
311
+ title: "Meta Title",
312
+ type: "string",
313
+ description: "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.",
314
+ components: {
315
+ input: MetaTitle
316
+ }
317
+ // validation: (Rule) => Rule.max(60).warning('Meta title should be under 60 characters.'),
318
+ }),
319
+ sanity.defineField({
320
+ name: "description",
321
+ title: "Meta Description",
322
+ type: "text",
323
+ rows: 3,
324
+ description: "Provide a concise summary of the page content. This description may be used by search engines in search results.",
325
+ components: {
326
+ input: MetaDescription
327
+ }
328
+ // validation: (Rule) => Rule.max(160).warning('Meta description should be under 160 characters.'),
329
+ }),
330
+ sanity.defineField({
331
+ name: "metaImage",
332
+ title: "Meta Image",
333
+ type: "image",
334
+ options: {
335
+ hotspot: !0
336
+ },
337
+ description: "Upload an image that represents the content of the page. This image may be used in social media previews and search engine results."
338
+ }),
339
+ sanity.defineField({
340
+ name: "metaAttributes",
341
+ title: "Additional Meta Attributes",
342
+ type: "array",
343
+ of: [{ type: "metaAttribute" }],
344
+ description: "Add custom meta attributes to the head of the document for additional SEO and social media integration."
345
+ }),
346
+ sanity.defineField({
347
+ name: "keywords",
348
+ title: "Keywords",
349
+ type: "array",
350
+ of: [{ type: "string" }],
351
+ description: "Add relevant keywords for this page. These keywords will be used for SEO purposes."
352
+ }),
353
+ sanity.defineField({
354
+ name: "canonicalUrl",
355
+ title: "Canonical URL",
356
+ type: "url",
357
+ description: "Specify the canonical URL for this page. This helps prevent duplicate content issues by indicating the preferred version of a page."
358
+ }),
359
+ sanity.defineField({
360
+ name: "openGraph",
361
+ title: "Open Graph Settings",
362
+ type: "openGraph"
363
+ }),
364
+ sanity.defineField({
365
+ name: "twitter",
366
+ title: "Twitter Card Settings",
367
+ type: "twitter"
368
+ })
369
+ ]
370
+ }), metaAttribute = sanity.defineType({
371
+ name: "metaAttribute",
372
+ title: "Meta Attribute",
373
+ type: "object",
374
+ fields: [
375
+ sanity.defineField({
376
+ name: "key",
377
+ title: "Attribute Name",
378
+ type: "string"
379
+ }),
380
+ sanity.defineField({
381
+ name: "type",
382
+ title: "Attribute Value Type",
383
+ type: "string",
384
+ options: {
385
+ list: [
386
+ { title: "String", value: "string" },
387
+ { title: "Image", value: "image" }
388
+ ]
389
+ },
390
+ initialValue: "string"
391
+ }),
392
+ sanity.defineField({
393
+ name: "value",
394
+ title: "Attribute Value",
395
+ type: "string",
396
+ hidden: ({ parent }) => parent?.type === "image"
397
+ }),
398
+ sanity.defineField({
399
+ name: "image",
400
+ title: "Attribute Image Value",
401
+ type: "image",
402
+ hidden: ({ parent }) => parent?.type === "string"
403
+ })
404
+ ],
405
+ preview: {
406
+ select: {
407
+ attributeName: "key",
408
+ attributeValueString: "value",
409
+ attributeValueImage: "image"
410
+ },
411
+ prepare({
412
+ attributeName,
413
+ attributeValueString,
414
+ attributeValueImage
415
+ }) {
416
+ return {
417
+ title: attributeName || "No Attribute Name",
418
+ subtitle: attributeValueString ? `Value: ${attributeValueString}` : attributeValueImage ? "Value: [Image]" : "No Attribute Value",
419
+ media: attributeValueImage
420
+ };
421
+ }
422
+ }
423
+ });
424
+ const OgTitle = (props) => {
425
+ const { value, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [], feedbackItems = react.useMemo(
426
+ () => getOgTitleValidation(value || "", keywords),
427
+ [value, keywords]
428
+ );
429
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
430
+ renderDefault(props),
431
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
432
+ /* @__PURE__ */ jsxRuntime.jsx(
433
+ "div",
434
+ {
435
+ style: {
436
+ minWidth: 10,
437
+ height: 10,
438
+ borderRadius: "50%",
439
+ backgroundColor: item.color
440
+ }
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
444
+ ] }, item.text)) })
445
+ ] });
446
+ }, OgDescription = (props) => {
447
+ const { value, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [], feedbackItems = react.useMemo(
448
+ () => getOgDescriptionValidation(value || "", keywords),
449
+ [value, keywords]
450
+ );
451
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
452
+ renderDefault(props),
453
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
454
+ /* @__PURE__ */ jsxRuntime.jsx(
455
+ "div",
456
+ {
457
+ style: {
458
+ minWidth: 10,
459
+ height: 10,
460
+ borderRadius: "50%",
461
+ backgroundColor: item.color
462
+ }
463
+ }
464
+ ),
465
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
466
+ ] }, item.text)) })
467
+ ] });
468
+ };
469
+ var openGraph = sanity.defineType({
470
+ name: "openGraph",
471
+ title: "Open Graph Settings",
472
+ type: "object",
473
+ fields: [
474
+ sanity.defineField({
475
+ name: "title",
476
+ title: "Open Graph Title",
477
+ type: "string",
478
+ components: {
479
+ input: OgTitle
480
+ // Can also wrap with a string input + preview
481
+ }
482
+ }),
483
+ sanity.defineField({
484
+ name: "description",
485
+ title: "Open Graph Description",
486
+ type: "text",
487
+ rows: 3,
488
+ components: {
489
+ input: OgDescription
490
+ // Can also wrap with a text area + preview
491
+ }
492
+ }),
493
+ sanity.defineField({
494
+ name: "siteName",
495
+ title: "Open Graph Site Name",
496
+ type: "string",
497
+ description: "The name of your website. This is often the site title."
498
+ }),
499
+ sanity.defineField({
500
+ name: "type",
501
+ title: "Open Graph Type",
502
+ type: "string",
503
+ options: {
504
+ list: [
505
+ { title: "Website", value: "website" },
506
+ { title: "Article", value: "article" },
507
+ { title: "Profile", value: "profile" },
508
+ { title: "Book", value: "book" },
509
+ { title: "Music", value: "music" },
510
+ { title: "Video", value: "video" },
511
+ { title: "Product", value: "product" }
512
+ ]
513
+ // layout: 'radio', // Display as radio buttons
514
+ },
515
+ initialValue: "website",
516
+ description: "Select the type of content for Open Graph."
517
+ }),
518
+ // upload image or ask for url
519
+ sanity.defineField({
520
+ name: "imageType",
521
+ title: "Image Type",
522
+ type: "string",
523
+ options: {
524
+ list: [
525
+ { title: "Upload Image", value: "upload" },
526
+ { title: "Image URL", value: "url" }
527
+ ]
528
+ },
529
+ initialValue: "upload"
530
+ }),
531
+ sanity.defineField({
532
+ name: "image",
533
+ title: "Open Graph Image",
534
+ type: "image",
535
+ options: {
536
+ hotspot: !0
537
+ },
538
+ hidden: ({ parent }) => parent?.imageType !== "upload",
539
+ description: "Recommended size: 1200x630px (minimum 600x315px). Max file size: 5MB. Aspect ratio 1.91:1."
540
+ }),
541
+ sanity.defineField({
542
+ name: "imageUrl",
543
+ title: "Open Graph Image URL",
544
+ type: "url",
545
+ hidden: ({ parent }) => parent?.imageType !== "url",
546
+ description: "Enter the full URL of the image. Ensure the image is accessible and meets the recommended size of 1200x630px (minimum 600x315px)."
547
+ })
548
+ ]
549
+ });
550
+ const TwitterTitle = (props) => {
551
+ const { value, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [], feedbackItems = react.useMemo(
552
+ () => getTwitterTitleValidation(value || "", keywords),
553
+ [value, keywords]
554
+ );
555
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
556
+ renderDefault(props),
557
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
558
+ /* @__PURE__ */ jsxRuntime.jsx(
559
+ "div",
560
+ {
561
+ style: {
562
+ minWidth: 10,
563
+ height: 10,
564
+ borderRadius: "50%",
565
+ backgroundColor: item.color
566
+ }
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
570
+ ] }, item.text)) })
571
+ ] });
572
+ }, TwitterDescription = (props) => {
573
+ const { value, renderDefault, path } = props, keywords = sanity.useFormValue([path[0]])?.keywords || [], feedbackItems = react.useMemo(
574
+ () => getTwitterDescriptionValidation(value || "", keywords),
575
+ [value, keywords]
576
+ );
577
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
578
+ renderDefault(props),
579
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
580
+ /* @__PURE__ */ jsxRuntime.jsx(
581
+ "div",
582
+ {
583
+ style: {
584
+ minWidth: 10,
585
+ height: 10,
586
+ borderRadius: "50%",
587
+ backgroundColor: item.color
588
+ }
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", muted: !0, size: 14, children: item.text })
592
+ ] }, item.text)) })
593
+ ] });
594
+ };
595
+ var twitter = sanity.defineType({
596
+ name: "twitter",
597
+ title: "Twitter",
598
+ type: "object",
599
+ fields: [
600
+ sanity.defineField({
601
+ name: "card",
602
+ title: "Card Type",
603
+ type: "string",
604
+ options: {
605
+ list: [
606
+ { title: "Summary", value: "summary" },
607
+ { title: "Summary with Large Image", value: "summary_large_image" },
608
+ { title: "App", value: "app" },
609
+ { title: "Player", value: "player" }
610
+ ]
611
+ },
612
+ initialValue: "summary_large_image"
613
+ // good default
614
+ }),
615
+ sanity.defineField({
616
+ name: "site",
617
+ title: "Site Twitter Handle",
618
+ type: "string",
619
+ description: "The Twitter handle of the website (e.g., @example)"
620
+ }),
621
+ sanity.defineField({
622
+ name: "title",
623
+ title: "Twitter Title",
624
+ type: "string",
625
+ description: "The title of the content as it should appear on Twitter.",
626
+ components: {
627
+ input: TwitterTitle
628
+ }
629
+ }),
630
+ sanity.defineField({
631
+ name: "description",
632
+ title: "Twitter Description",
633
+ type: "text",
634
+ rows: 3,
635
+ description: "A brief description of the content for Twitter.",
636
+ components: {
637
+ input: TwitterDescription
638
+ }
639
+ }),
640
+ sanity.defineField({
641
+ name: "image",
642
+ title: "Twitter Image",
643
+ type: "image",
644
+ description: 'An image URL which should be at least 120x120px for "summary" card and 280x150px for "summary_large_image" card.',
645
+ options: {
646
+ hotspot: !0
647
+ },
648
+ fields: [
649
+ sanity.defineField({
650
+ name: "alt",
651
+ title: "Image Alt Text",
652
+ type: "string",
653
+ description: "Short alt text describing the image"
654
+ })
655
+ ]
656
+ })
657
+ ]
658
+ }), robots = sanity.defineType({
659
+ name: "robots",
660
+ title: "Robots Settings",
661
+ type: "object",
662
+ fields: [
663
+ sanity.defineField({
664
+ name: "noIndex",
665
+ title: "No Index",
666
+ type: "boolean",
667
+ initialValue: !1,
668
+ description: "Enable this to prevent search engines from indexing this page. The page will not appear in search results."
669
+ }),
670
+ sanity.defineField({
671
+ name: "noFollow",
672
+ title: "No Follow",
673
+ type: "boolean",
674
+ initialValue: !1,
675
+ description: "Enable this to prevent search engines from following links on this page. Links will not pass SEO value."
676
+ })
677
+ ],
678
+ description: "Select how search engines should index and follow links on this page."
679
+ }), metaTag = sanity.defineType({
680
+ name: "metaTag",
681
+ title: "Meta Tag",
682
+ type: "object",
683
+ fields: [
684
+ {
685
+ name: "metaAttributes",
686
+ title: "Meta Attributes",
687
+ type: "array",
688
+ of: [{ type: "metaAttribute" }],
689
+ description: "Add custom meta attributes to the head of the document for additional SEO and social media integration."
690
+ }
691
+ ]
692
+ }), types = [seoFields, openGraph, metaAttribute, twitter, robots, metaTag];
693
+ const seofields = sanity.definePlugin((config = {}) => ({
694
+ name: "sanity-plugin-seofields",
695
+ schema: { types }
696
+ })), isSeoFields = (obj) => obj && obj._type === "seoFields", isOpenGraphSettings = (obj) => obj && obj._type === "openGraph", isTwitterCardSettings = (obj) => obj && obj._type === "twitter", isMetaAttribute = (obj) => obj && obj._type === "metaAttribute", defaultSeoValidationRules = {
697
+ title: {
698
+ maxLength: 70,
699
+ warningLength: 60
700
+ },
701
+ description: {
702
+ maxLength: 160,
703
+ warningLength: 150
704
+ },
705
+ openGraphTitle: {
706
+ maxLength: 95
707
+ },
708
+ openGraphDescription: {
709
+ maxLength: 200
710
+ },
711
+ twitterTitle: {
712
+ maxLength: 70
713
+ },
714
+ twitterDescription: {
715
+ maxLength: 200
716
+ }
717
+ };
718
+ exports.allSchemas = types;
719
+ exports.defaultSeoValidationRules = defaultSeoValidationRules;
720
+ exports.isMetaAttribute = isMetaAttribute;
721
+ exports.isOpenGraphSettings = isOpenGraphSettings;
722
+ exports.isSeoFields = isSeoFields;
723
+ exports.isTwitterCardSettings = isTwitterCardSettings;
724
+ exports.metaAttributeSchema = metaAttribute;
725
+ exports.metaTagSchema = metaTag;
726
+ exports.openGraphSchema = openGraph;
727
+ exports.seoFieldsSchema = seoFields;
728
+ exports.seofields = seofields;
729
+ exports.twitterSchema = twitter;
730
+ //# sourceMappingURL=index.js.map