sanity-plugin-seofields 1.6.5 → 1.8.0

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.
Files changed (82) hide show
  1. package/README.md +355 -79
  2. package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.cts +78 -0
  3. package/dist/SchemaOrgJsonLdPreview-DJLkIPq4.d.ts +78 -0
  4. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js +4 -0
  5. package/dist/SeoHealthDashboard-AEKVVMUR-7NWV3C3Y.js.map +1 -0
  6. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs +10 -0
  7. package/dist/SeoHealthDashboard-AEKVVMUR-XWKJUH24.cjs.map +1 -0
  8. package/dist/SeoHealthTool-2KKQJLJL.cjs +11 -0
  9. package/dist/{SeoHealthDashboard-BEZVLLFG.js.map → SeoHealthTool-2KKQJLJL.cjs.map} +1 -1
  10. package/dist/SeoHealthTool-3CUKGADZ.js +5 -0
  11. package/dist/{SeoHealthDashboard-D3XU3CUB.cjs.map → SeoHealthTool-3CUKGADZ.js.map} +1 -1
  12. package/dist/{SeoPreview-WYH7NYNM.cjs → SeoPreview-3EXR6FD4.cjs} +3 -3
  13. package/dist/{SeoPreview-WYH7NYNM.cjs.map → SeoPreview-3EXR6FD4.cjs.map} +1 -1
  14. package/dist/{SeoPreview-KEGGQSPJ.js → SeoPreview-4PPAF4NO.js} +3 -3
  15. package/dist/{SeoPreview-KEGGQSPJ.js.map → SeoPreview-4PPAF4NO.js.map} +1 -1
  16. package/dist/{chunk-S367Y35J.cjs → chunk-254YHUN3.cjs} +2 -15
  17. package/dist/{chunk-S367Y35J.cjs.map → chunk-254YHUN3.cjs.map} +1 -1
  18. package/dist/{chunk-HQTYQMP5.js → chunk-27XSYENH.js} +80 -29
  19. package/dist/chunk-27XSYENH.js.map +1 -0
  20. package/dist/{chunk-YHXUWGNA.cjs → chunk-B5IVI5LP.cjs} +918 -157
  21. package/dist/chunk-B5IVI5LP.cjs.map +1 -0
  22. package/dist/chunk-BNXMFD3T.cjs +431 -0
  23. package/dist/chunk-BNXMFD3T.cjs.map +1 -0
  24. package/dist/{chunk-2NMEKWO5.js → chunk-DDAAVRWG.js} +3 -15
  25. package/dist/{chunk-2NMEKWO5.js.map → chunk-DDAAVRWG.js.map} +1 -1
  26. package/dist/chunk-P6IOWIO2.cjs +480 -0
  27. package/dist/chunk-P6IOWIO2.cjs.map +1 -0
  28. package/dist/{chunk-XXA6WCWS.js → chunk-UOCZFYYP.js} +18 -203
  29. package/dist/chunk-UOCZFYYP.js.map +1 -0
  30. package/dist/{chunk-KTL6NYNG.js → chunk-VR44E62I.js} +897 -148
  31. package/dist/chunk-VR44E62I.js.map +1 -0
  32. package/dist/chunk-WKXHX3GO.js +424 -0
  33. package/dist/chunk-WKXHX3GO.js.map +1 -0
  34. package/dist/{chunk-BHIZQLY7.cjs → chunk-XZDLJ2N6.cjs} +84 -30
  35. package/dist/chunk-XZDLJ2N6.cjs.map +1 -0
  36. package/dist/cli.js +18 -18
  37. package/dist/{component-H52blyfc.d.ts → component-BzI-LHPw.d.cts} +9 -27
  38. package/dist/{component-CIE6Sy4F.d.cts → component-DEXwtemT.d.ts} +9 -27
  39. package/dist/define-cli.cjs +1 -1
  40. package/dist/define-cli.js +1 -1
  41. package/dist/index.cjs +1050 -94
  42. package/dist/index.cjs.map +1 -1
  43. package/dist/index.d.cts +75 -202
  44. package/dist/index.d.ts +75 -202
  45. package/dist/index.js +1009 -63
  46. package/dist/index.js.map +1 -1
  47. package/dist/next.cjs +138 -116
  48. package/dist/next.cjs.map +1 -1
  49. package/dist/next.d.cts +9 -3
  50. package/dist/next.d.ts +9 -3
  51. package/dist/next.js +38 -32
  52. package/dist/next.js.map +1 -1
  53. package/dist/schema/next.cjs +105 -89
  54. package/dist/schema/next.d.cts +4 -4
  55. package/dist/schema/next.d.ts +4 -4
  56. package/dist/schema/next.js +4 -4
  57. package/dist/schema.cjs +219 -157
  58. package/dist/schema.cjs.map +1 -1
  59. package/dist/schema.d.cts +30 -16
  60. package/dist/schema.d.ts +30 -16
  61. package/dist/schema.js +41 -8
  62. package/dist/schema.js.map +1 -1
  63. package/dist/{types-Dp9Pfnt9.d.ts → types-BXSCbl6p.d.cts} +47 -2
  64. package/dist/{types-CyRdIF-3.d.cts → types-B_vjMPtM.d.cts} +1 -1
  65. package/dist/{types-BxcJinOf.d.cts → types-C19cQx5D.d.ts} +47 -2
  66. package/dist/{types-CbYb4MsN.d.ts → types-CW8qFdAn.d.ts} +1 -1
  67. package/dist/{types-BtSRRG6C.d.cts → types-yVmQfby9.d.cts} +8 -16
  68. package/dist/{types-BtSRRG6C.d.ts → types-yVmQfby9.d.ts} +8 -16
  69. package/package.json +2 -1
  70. package/dist/SeoHealthDashboard-BEZVLLFG.js +0 -4
  71. package/dist/SeoHealthDashboard-D3XU3CUB.cjs +0 -10
  72. package/dist/SeoHealthTool-4VZMD4TR.cjs +0 -14
  73. package/dist/SeoHealthTool-4VZMD4TR.cjs.map +0 -1
  74. package/dist/SeoHealthTool-BSTYAKQS.js +0 -12
  75. package/dist/SeoHealthTool-BSTYAKQS.js.map +0 -1
  76. package/dist/chunk-BCTPOWXA.cjs +0 -669
  77. package/dist/chunk-BCTPOWXA.cjs.map +0 -1
  78. package/dist/chunk-BHIZQLY7.cjs.map +0 -1
  79. package/dist/chunk-HQTYQMP5.js.map +0 -1
  80. package/dist/chunk-KTL6NYNG.js.map +0 -1
  81. package/dist/chunk-XXA6WCWS.js.map +0 -1
  82. package/dist/chunk-YHXUWGNA.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,468 @@
1
+ import { tokenizeHtml } from './chunk-VR44E62I.js';
2
+ export { SchemaOrgJsonLdPreview as SchemaOrgDocumentJsonLdPreview, SchemaOrgJsonLdPreview } from './chunk-VR44E62I.js';
1
3
  import { getMetaTitleValidationMessages, getMetaDescriptionValidationMessages, getMetaImageValidation, getOgTitleValidation, getOgDescriptionValidation, getOgImageValidation, getOgImageUrlValidation, getTwitterTitleValidation, getTwitterDescriptionValidation, getTwitterImageValidation, getTwitterImageUrlValidation } from './chunk-HDZZQCH7.js';
2
- import { __spreadValues, __spreadProps, __objRest } from './chunk-2NMEKWO5.js';
4
+ import { createSeoPublishGateAction, buildGEOProChecks, GEOChecklistProChecks, GEOLockedTeaser } from './chunk-WKXHX3GO.js';
5
+ export { createSeoHealthPane } from './chunk-WKXHX3GO.js';
6
+ import './chunk-27XSYENH.js';
7
+ import { __spreadValues, __spreadProps } from './chunk-DDAAVRWG.js';
3
8
  import { lazy, createElement, Suspense, useMemo, useState, useEffect } from 'react';
4
- import { defineType, defineField, definePlugin, useFormValue, useClient } from 'sanity';
5
- import { Stack, Text } from '@sanity/ui';
9
+ import { defineType, defineField, definePlugin, useFormValue, useClient, useColorScheme } from 'sanity';
6
10
  import { jsxs, jsx } from 'react/jsx-runtime';
11
+ import { Stack, Text, Card, Flex, Button, Box } from '@sanity/ui';
7
12
 
13
+ var GEOChecklist = (props) => {
14
+ var _a;
15
+ const { path, schemaType } = props;
16
+ const [expanded, setExpanded] = useState(false);
17
+ const licenseKey = (_a = schemaType.options) == null ? void 0 : _a.licenseKey;
18
+ const seoFields = useFormValue([path[0]]);
19
+ const rootDoc = useFormValue([]);
20
+ const freeChecks = useMemo(() => {
21
+ var _a2, _b, _c, _d;
22
+ const desc = (seoFields == null ? void 0 : seoFields.description) || "";
23
+ const title = (seoFields == null ? void 0 : seoFields.title) || "";
24
+ return [
25
+ {
26
+ id: "description_answer_ready",
27
+ label: "Description is answer-ready (100\u2013160 chars)",
28
+ pass: !!desc && desc.length >= 100 && desc.length <= 160,
29
+ hint: "Write a concise 100\u2013160 char answer that directly addresses user intent."
30
+ },
31
+ {
32
+ id: "not_noindexed",
33
+ label: "Page is indexable (not noindex)",
34
+ pass: !((_a2 = seoFields == null ? void 0 : seoFields.robots) == null ? void 0 : _a2.noIndex),
35
+ hint: "Page is set to noindex \u2014 AI overviews cannot cite noindexed pages."
36
+ },
37
+ {
38
+ id: "title_concise",
39
+ label: "Title is 30\u201360 characters",
40
+ pass: !!(title && title.length >= 30 && title.length <= 60),
41
+ hint: "Keep your title between 30\u201360 characters for optimal SERP and AI snippet display."
42
+ },
43
+ {
44
+ id: "og_image_present",
45
+ label: "OG or meta image present",
46
+ pass: !!(((_b = seoFields == null ? void 0 : seoFields.metaImage) == null ? void 0 : _b.asset) || ((_d = (_c = seoFields == null ? void 0 : seoFields.openGraph) == null ? void 0 : _c.image) == null ? void 0 : _d.asset)),
47
+ hint: "Add a meta or OG image for rich results and social previews."
48
+ },
49
+ {
50
+ id: "has_keywords",
51
+ label: "Keywords added",
52
+ pass: !!((seoFields == null ? void 0 : seoFields.keywords) && seoFields.keywords.filter((k) => k && k.trim().length > 0).length > 0),
53
+ hint: "Add at least one keyword to improve topical signal for AI classifiers."
54
+ }
55
+ ];
56
+ }, [seoFields]);
57
+ const proChecks = useMemo(
58
+ () => licenseKey ? buildGEOProChecks(seoFields, rootDoc) : null,
59
+ [licenseKey, seoFields, rootDoc]
60
+ );
61
+ const freePassing = freeChecks.filter((c) => c.pass).length;
62
+ const proPassing = proChecks ? proChecks.filter((c) => c.pass).length : 0;
63
+ const totalPassing = freePassing + (proChecks ? proPassing : 0);
64
+ const totalChecks = freeChecks.length + (proChecks ? proChecks.length : 0);
65
+ const percentage = totalChecks > 0 ? totalPassing / totalChecks * 100 : 0;
66
+ let scoreColor = "#dc2626";
67
+ let scoreBg = "#fef2f2";
68
+ let scoreText = "#b91c1c";
69
+ if (percentage >= 80) {
70
+ scoreColor = "#16a34a";
71
+ scoreBg = "#f0fdf4";
72
+ scoreText = "#15803d";
73
+ } else if (percentage >= 50) {
74
+ scoreColor = "#ca8a04";
75
+ scoreBg = "#fefce8";
76
+ scoreText = "#a16207";
77
+ }
78
+ return /* @__PURE__ */ jsxs(
79
+ "div",
80
+ {
81
+ style: {
82
+ borderRadius: 10,
83
+ border: "1px solid #e5e7eb",
84
+ overflow: "hidden",
85
+ fontFamily: "inherit"
86
+ },
87
+ children: [
88
+ /* @__PURE__ */ jsxs(
89
+ "div",
90
+ {
91
+ style: {
92
+ padding: "16px 20px 14px",
93
+ borderBottom: "1px solid #f3f4f6",
94
+ display: "flex",
95
+ alignItems: "center",
96
+ justifyContent: "space-between",
97
+ gap: 12
98
+ },
99
+ children: [
100
+ /* @__PURE__ */ jsxs("div", { children: [
101
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 600, color: "#111827", marginBottom: 2 }, children: "AI Overview Readiness" }),
102
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af" }, children: "Signals that improve visibility in AI-generated overviews" })
103
+ ] }),
104
+ /* @__PURE__ */ jsxs(
105
+ "div",
106
+ {
107
+ style: {
108
+ backgroundColor: scoreBg,
109
+ borderRadius: 20,
110
+ padding: "4px 12px",
111
+ display: "flex",
112
+ alignItems: "center",
113
+ gap: 4,
114
+ flexShrink: 0
115
+ },
116
+ children: [
117
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 16, fontWeight: 700, color: scoreColor }, children: totalPassing }),
118
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 12, color: scoreText, opacity: 0.7 }, children: [
119
+ "/ ",
120
+ totalChecks
121
+ ] })
122
+ ]
123
+ }
124
+ )
125
+ ]
126
+ }
127
+ ),
128
+ /* @__PURE__ */ jsx("div", { style: { height: 3, backgroundColor: "#f3f4f6" }, children: /* @__PURE__ */ jsx(
129
+ "div",
130
+ {
131
+ style: {
132
+ height: "100%",
133
+ width: `${percentage}%`,
134
+ backgroundColor: scoreColor,
135
+ transition: "width 0.4s ease"
136
+ }
137
+ }
138
+ ) }),
139
+ /* @__PURE__ */ jsx("div", { style: { padding: "10px 20px" }, children: /* @__PURE__ */ jsxs(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ onClick: () => setExpanded((prev) => !prev),
144
+ style: {
145
+ background: "none",
146
+ border: "none",
147
+ padding: 0,
148
+ cursor: "pointer",
149
+ fontSize: 12,
150
+ color: "#6366f1",
151
+ fontWeight: 500,
152
+ display: "flex",
153
+ alignItems: "center",
154
+ gap: 4
155
+ },
156
+ children: [
157
+ /* @__PURE__ */ jsx(
158
+ "svg",
159
+ {
160
+ xmlns: "http://www.w3.org/2000/svg",
161
+ width: "12",
162
+ height: "12",
163
+ viewBox: "0 0 24 24",
164
+ fill: "none",
165
+ stroke: "currentColor",
166
+ strokeWidth: "2",
167
+ strokeLinecap: "round",
168
+ strokeLinejoin: "round",
169
+ style: {
170
+ display: "inline-block",
171
+ transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
172
+ transition: "transform 0.2s ease",
173
+ flexShrink: 0
174
+ },
175
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
176
+ }
177
+ ),
178
+ expanded ? "Hide checks" : "Show all checks"
179
+ ]
180
+ }
181
+ ) }),
182
+ expanded && /* @__PURE__ */ jsxs("div", { children: [
183
+ /* @__PURE__ */ jsxs("div", { style: { padding: "0 20px 16px" }, children: [
184
+ /* @__PURE__ */ jsx(
185
+ "div",
186
+ {
187
+ style: {
188
+ fontSize: 10,
189
+ fontWeight: 700,
190
+ letterSpacing: "0.08em",
191
+ color: "#9ca3af",
192
+ marginBottom: 10,
193
+ textTransform: "uppercase"
194
+ },
195
+ children: "Core Checks"
196
+ }
197
+ ),
198
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: freeChecks.map((check) => /* @__PURE__ */ jsx(CheckRow, { check }, check.id)) })
199
+ ] }),
200
+ /* @__PURE__ */ jsxs(
201
+ "div",
202
+ {
203
+ style: {
204
+ margin: "0 12px 12px",
205
+ borderRadius: 8,
206
+ backgroundColor: licenseKey ? "#faf5ff" : "#f9fafb",
207
+ border: `1px solid ${licenseKey ? "#e9d5ff" : "#f3f4f6"}`,
208
+ overflow: "hidden"
209
+ },
210
+ children: [
211
+ /* @__PURE__ */ jsxs(
212
+ "div",
213
+ {
214
+ style: {
215
+ padding: "10px 16px",
216
+ borderBottom: `1px solid ${licenseKey ? "#e9d5ff" : "#f3f4f6"}`,
217
+ display: "flex",
218
+ alignItems: "center",
219
+ gap: 8
220
+ },
221
+ children: [
222
+ /* @__PURE__ */ jsx(
223
+ "span",
224
+ {
225
+ style: {
226
+ fontSize: 10,
227
+ fontWeight: 700,
228
+ letterSpacing: "0.08em",
229
+ color: licenseKey ? "#7c3aed" : "#9ca3af",
230
+ textTransform: "uppercase"
231
+ },
232
+ children: "Pro Insights"
233
+ }
234
+ ),
235
+ !licenseKey && /* @__PURE__ */ jsx(
236
+ "span",
237
+ {
238
+ style: {
239
+ fontSize: 10,
240
+ backgroundColor: "#f3e8ff",
241
+ color: "#7c3aed",
242
+ borderRadius: 4,
243
+ padding: "1px 6px",
244
+ fontWeight: 600
245
+ },
246
+ children: "Unlock with licenseKey"
247
+ }
248
+ ),
249
+ licenseKey && proChecks && /* @__PURE__ */ jsxs(
250
+ "span",
251
+ {
252
+ style: {
253
+ fontSize: 10,
254
+ backgroundColor: "#d8b4fe",
255
+ color: "#5b21b6",
256
+ borderRadius: 4,
257
+ padding: "1px 6px",
258
+ fontWeight: 600
259
+ },
260
+ children: [
261
+ proPassing,
262
+ " / ",
263
+ proChecks.length,
264
+ " passing"
265
+ ]
266
+ }
267
+ )
268
+ ]
269
+ }
270
+ ),
271
+ /* @__PURE__ */ jsx("div", { style: { padding: "10px 16px" }, children: proChecks ? /* @__PURE__ */ jsx(GEOChecklistProChecks, { checks: proChecks }) : /* @__PURE__ */ jsx(GEOLockedTeaser, {}) })
272
+ ]
273
+ }
274
+ )
275
+ ] })
276
+ ]
277
+ }
278
+ );
279
+ };
280
+ function CheckRow({ check }) {
281
+ return /* @__PURE__ */ jsxs(
282
+ "div",
283
+ {
284
+ style: {
285
+ display: "flex",
286
+ alignItems: "flex-start",
287
+ gap: 10,
288
+ padding: "9px 12px",
289
+ borderRadius: 7,
290
+ backgroundColor: check.pass ? "transparent" : "#fff8f8",
291
+ border: `1px solid ${check.pass ? "transparent" : "#fee2e2"}`
292
+ },
293
+ children: [
294
+ /* @__PURE__ */ jsx(
295
+ "div",
296
+ {
297
+ style: {
298
+ width: 20,
299
+ height: 20,
300
+ borderRadius: "50%",
301
+ backgroundColor: check.pass ? "#dcfce7" : "#fee2e2",
302
+ display: "flex",
303
+ alignItems: "center",
304
+ justifyContent: "center",
305
+ flexShrink: 0,
306
+ marginTop: 1
307
+ },
308
+ children: /* @__PURE__ */ jsx(
309
+ "span",
310
+ {
311
+ style: {
312
+ fontSize: 10,
313
+ fontWeight: 800,
314
+ color: check.pass ? "#16a34a" : "#dc2626",
315
+ lineHeight: 1
316
+ },
317
+ children: check.pass ? "\u2713" : "\u2717"
318
+ }
319
+ )
320
+ }
321
+ ),
322
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
323
+ /* @__PURE__ */ jsx(
324
+ "div",
325
+ {
326
+ style: {
327
+ fontSize: 12,
328
+ fontWeight: 500,
329
+ color: check.pass ? "#374151" : "#1f2937",
330
+ lineHeight: 1.4
331
+ },
332
+ children: check.label
333
+ }
334
+ ),
335
+ !check.pass && /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", marginTop: 3, lineHeight: 1.5 }, children: check.hint })
336
+ ] })
337
+ ]
338
+ }
339
+ );
340
+ }
341
+ var GEOChecklist_default = GEOChecklist;
342
+ var FocusKeywordInput = (props) => {
343
+ var _a;
344
+ const { value, renderDefault, path } = props;
345
+ const parent = useFormValue([path[0]]);
346
+ const rootDoc = useFormValue([]);
347
+ const feedbackItems = useMemo(() => {
348
+ var _a2, _b;
349
+ if (!value) return [];
350
+ const kw = value.toLowerCase();
351
+ const title = ((parent == null ? void 0 : parent.title) || "").toLowerCase();
352
+ const description = ((parent == null ? void 0 : parent.description) || "").toLowerCase();
353
+ const slug = ((_a2 = rootDoc == null ? void 0 : rootDoc.slug) == null ? void 0 : _a2.current) || "";
354
+ const items = [];
355
+ if (title) {
356
+ if (title.startsWith(kw)) {
357
+ items.push({ text: "Keyword at start of title \u2014 excellent", color: "green" });
358
+ } else if (title.includes(kw)) {
359
+ items.push({
360
+ text: "Keyword in title \u2014 move it to the start for more prominence",
361
+ color: "orange"
362
+ });
363
+ } else {
364
+ items.push({ text: "Keyword missing from meta title", color: "red" });
365
+ }
366
+ }
367
+ if (description) {
368
+ if (description.includes(kw)) {
369
+ items.push({ text: "Keyword in meta description", color: "green" });
370
+ } else {
371
+ items.push({ text: "Not in description \u2014 include it naturally", color: "orange" });
372
+ }
373
+ }
374
+ if (slug) {
375
+ const kwSlug = kw.replace(/\s+/g, "-");
376
+ if (slug.includes(kwSlug)) {
377
+ items.push({ text: "Keyword in URL slug", color: "green" });
378
+ } else {
379
+ items.push({
380
+ text: "Keyword not in URL slug \u2014 consider it for new pages",
381
+ color: "orange"
382
+ });
383
+ }
384
+ }
385
+ const safeKw = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
386
+ if (((_b = title.match(new RegExp(safeKw, "g"))) != null ? _b : []).length > 2) {
387
+ items.push({
388
+ text: "Keyword appears too many times in title \u2014 avoid stuffing",
389
+ color: "red"
390
+ });
391
+ }
392
+ return items;
393
+ }, [value, parent == null ? void 0 : parent.title, parent == null ? void 0 : parent.description, (_a = rootDoc == null ? void 0 : rootDoc.slug) == null ? void 0 : _a.current]);
394
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
395
+ renderDefault(props),
396
+ /* @__PURE__ */ jsx(Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
397
+ /* @__PURE__ */ jsx(
398
+ "div",
399
+ {
400
+ style: {
401
+ minWidth: 10,
402
+ height: 10,
403
+ borderRadius: "50%",
404
+ backgroundColor: item.color
405
+ }
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsx(Text, { weight: "bold", muted: true, size: 14, children: item.text })
409
+ ] }, item.text)) })
410
+ ] });
411
+ };
412
+ var FocusKeywordInput_default = FocusKeywordInput;
413
+
414
+ // src/utils/readability.ts
415
+ function countSyllables(word) {
416
+ var _a;
417
+ const w = word.toLowerCase().replace(/[^a-z]/g, "");
418
+ if (w.length <= 3) return 1;
419
+ const groups = (_a = w.match(/[aeiouy]+/g)) != null ? _a : [];
420
+ let count = groups.length;
421
+ if (w.endsWith("e") && w.length > 1 && !/[aeiouy]/.test(w[w.length - 2])) {
422
+ count = Math.max(1, count - 1);
423
+ }
424
+ return Math.max(1, count);
425
+ }
426
+ function countSentences(text) {
427
+ const matches = text.match(/[.!?]+/g);
428
+ return matches ? matches.length : 1;
429
+ }
430
+ function analyzeReadability(text) {
431
+ const wordList = text.trim().split(/\s+/).filter(Boolean);
432
+ const words = wordList.length;
433
+ if (words < 10) return null;
434
+ const sentences = countSentences(text);
435
+ const syllables = wordList.reduce((acc, word) => acc + countSyllables(word), 0);
436
+ const rawScore = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
437
+ const score = Math.round(rawScore * 10) / 10;
438
+ const rawGrade = 0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59;
439
+ const gradeLevel = Math.round(rawGrade * 10) / 10;
440
+ const avgWordsPerSentence = Math.round(words / sentences * 10) / 10;
441
+ let label;
442
+ let color;
443
+ if (score >= 70) {
444
+ label = "Easy to read";
445
+ color = "green";
446
+ } else if (score >= 50) {
447
+ label = "Fairly easy";
448
+ color = "green";
449
+ } else if (score >= 30) {
450
+ label = "Moderately difficult";
451
+ color = "orange";
452
+ } else {
453
+ label = "Difficult to read";
454
+ color = "red";
455
+ }
456
+ return {
457
+ score,
458
+ gradeLevel,
459
+ avgWordsPerSentence,
460
+ words,
461
+ sentences,
462
+ label,
463
+ color
464
+ };
465
+ }
8
466
  var MetaDescription = (props) => {
9
467
  const { value, renderDefault, path } = props;
10
468
  const parent = useFormValue([path[0]]);
@@ -14,6 +472,7 @@ var MetaDescription = (props) => {
14
472
  () => getMetaDescriptionValidationMessages(value || "", keywords, isParentseoField),
15
473
  [value, keywords, isParentseoField]
16
474
  );
475
+ const readability = useMemo(() => analyzeReadability(value || ""), [value]);
17
476
  return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
18
477
  renderDefault(props),
19
478
  /* @__PURE__ */ jsx(Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
@@ -24,7 +483,30 @@ var MetaDescription = (props) => {
24
483
  }
25
484
  ),
26
485
  /* @__PURE__ */ jsx(Text, { weight: "bold", muted: true, size: 14, children: item.text })
27
- ] }, item.text)) })
486
+ ] }, item.text)) }),
487
+ readability && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
488
+ /* @__PURE__ */ jsx(
489
+ "div",
490
+ {
491
+ style: {
492
+ minWidth: 10,
493
+ height: 10,
494
+ borderRadius: "50%",
495
+ backgroundColor: readability.color
496
+ }
497
+ }
498
+ ),
499
+ /* @__PURE__ */ jsxs(Text, { weight: "bold", muted: true, size: 14, children: [
500
+ "Readability: ",
501
+ readability.label,
502
+ " (Flesch ",
503
+ readability.score,
504
+ " \xB7 Grade",
505
+ " ",
506
+ readability.gradeLevel,
507
+ ")"
508
+ ] })
509
+ ] })
28
510
  ] });
29
511
  };
30
512
  var MetaDescription_default = MetaDescription;
@@ -55,8 +537,204 @@ var MetaImage = (props) => {
55
537
  ] });
56
538
  };
57
539
  var MetaImage_default = MetaImage;
540
+ function resolveImageUrl(asset, projectId, dataset) {
541
+ const ref = asset == null ? void 0 : asset._ref;
542
+ if (!ref) return null;
543
+ const parts = ref.replace("image-", "").split("-");
544
+ const ext = parts[parts.length - 1];
545
+ const dims = parts[parts.length - 2];
546
+ const id = parts.slice(0, parts.length - 2).join("-");
547
+ return `https://cdn.sanity.io/images/${projectId}/${dataset}/${id}-${dims}.${ext}`;
548
+ }
549
+ var MetaTagsPreview = (props) => {
550
+ const { path } = props;
551
+ const [expanded, setExpanded] = useState(false);
552
+ const [copyLabel, setCopyLabel] = useState("Copy");
553
+ const { scheme } = useColorScheme();
554
+ const seoFields = useFormValue([path[0]]);
555
+ const client = useClient({ apiVersion: "2024-01-01" });
556
+ const { projectId = "", dataset = "" } = client.config();
557
+ const rawHtml = useMemo(() => {
558
+ var _a, _b, _c, _d, _e, _f, _g;
559
+ const lines = [];
560
+ lines.push('<meta charset="UTF-8" />');
561
+ lines.push('<meta name="viewport" content="width=device-width, initial-scale=1" />');
562
+ if (seoFields == null ? void 0 : seoFields.title) lines.push(`<title>${seoFields.title}</title>`);
563
+ if (seoFields == null ? void 0 : seoFields.description)
564
+ lines.push(`<meta name="description" content="${seoFields.description}" />`);
565
+ if (seoFields == null ? void 0 : seoFields.robots) {
566
+ const parts = [];
567
+ if (seoFields.robots.noIndex) parts.push("noindex");
568
+ if (seoFields.robots.noFollow) parts.push("nofollow");
569
+ if (seoFields.robots.noTranslate) parts.push("notranslate");
570
+ if (seoFields.robots.noImageIndex) parts.push("noimageindex");
571
+ if (parts.length) lines.push(`<meta name="robots" content="${parts.join(", ")}" />`);
572
+ }
573
+ if ((_a = seoFields == null ? void 0 : seoFields.keywords) == null ? void 0 : _a.length)
574
+ lines.push(`<meta name="keywords" content="${seoFields.keywords.join(", ")}" />`);
575
+ if (seoFields == null ? void 0 : seoFields.canonicalUrl)
576
+ lines.push(`<link rel="canonical" href="${seoFields.canonicalUrl}" />`);
577
+ const og = seoFields == null ? void 0 : seoFields.openGraph;
578
+ if (og == null ? void 0 : og.type) lines.push(`<meta property="og:type" content="${og.type}" />`);
579
+ if (og == null ? void 0 : og.title) lines.push(`<meta property="og:title" content="${og.title}" />`);
580
+ if (og == null ? void 0 : og.description)
581
+ lines.push(`<meta property="og:description" content="${og.description}" />`);
582
+ if (og == null ? void 0 : og.siteName) lines.push(`<meta property="og:site_name" content="${og.siteName}" />`);
583
+ if (og == null ? void 0 : og.url) lines.push(`<meta property="og:url" content="${og.url}" />`);
584
+ const ogImg = (og == null ? void 0 : og.imageUrl) || resolveImageUrl((_b = og == null ? void 0 : og.image) == null ? void 0 : _b.asset, projectId, dataset) || resolveImageUrl((_c = seoFields == null ? void 0 : seoFields.metaImage) == null ? void 0 : _c.asset, projectId, dataset);
585
+ if (ogImg) lines.push(`<meta property="og:image" content="${ogImg}" />`);
586
+ const tw = seoFields == null ? void 0 : seoFields.twitter;
587
+ if (tw == null ? void 0 : tw.card) lines.push(`<meta name="twitter:card" content="${tw.card}" />`);
588
+ if (tw == null ? void 0 : tw.site) lines.push(`<meta name="twitter:site" content="${tw.site}" />`);
589
+ if (tw == null ? void 0 : tw.creator) lines.push(`<meta name="twitter:creator" content="${tw.creator}" />`);
590
+ if (tw == null ? void 0 : tw.title) lines.push(`<meta name="twitter:title" content="${tw.title}" />`);
591
+ if (tw == null ? void 0 : tw.description)
592
+ lines.push(`<meta name="twitter:description" content="${tw.description}" />`);
593
+ const twImg = (tw == null ? void 0 : tw.imageUrl) || resolveImageUrl((_d = tw == null ? void 0 : tw.image) == null ? void 0 : _d.asset, projectId, dataset);
594
+ if (twImg) lines.push(`<meta name="twitter:image" content="${twImg}" />`);
595
+ if ((_e = seoFields == null ? void 0 : seoFields.hreflangs) == null ? void 0 : _e.length) {
596
+ for (const e of seoFields.hreflangs) {
597
+ if (e.locale && e.url)
598
+ lines.push(`<link rel="alternate" hreflang="${e.locale}" href="${e.url}" />`);
599
+ }
600
+ }
601
+ if ((_f = seoFields == null ? void 0 : seoFields.metaAttributes) == null ? void 0 : _f.length) {
602
+ for (const attr of seoFields.metaAttributes) {
603
+ if (attr.key && attr.type === "string" && attr.value) {
604
+ lines.push(`<meta name="${attr.key}" content="${attr.value}" />`);
605
+ } else if (attr.key && attr.type === "image" && ((_g = attr.image) == null ? void 0 : _g.asset)) {
606
+ const imgUrl = resolveImageUrl(attr.image.asset, projectId, dataset);
607
+ if (imgUrl) lines.push(`<meta name="${attr.key}" content="${imgUrl}" />`);
608
+ }
609
+ }
610
+ }
611
+ return lines.join("\n");
612
+ }, [seoFields, projectId, dataset]);
613
+ const tagCount = useMemo(() => rawHtml.split("\n").filter(Boolean).length, [rawHtml]);
614
+ const tokens = useMemo(() => tokenizeHtml(rawHtml, scheme), [rawHtml, scheme]);
615
+ const handleCopy = () => {
616
+ navigator.clipboard.writeText(rawHtml).then(() => {
617
+ setCopyLabel("Copied!");
618
+ setTimeout(() => setCopyLabel("Copy"), 2e3);
619
+ });
620
+ };
621
+ const isDark = scheme === "dark";
622
+ const codeBg = isDark ? "#161b22" : "#f6f8fa";
623
+ const codeBorder = isDark ? "#30363d" : "#d0d7de";
624
+ const headerBg = isDark ? "#21262d" : "#f0f2f4";
625
+ const headerBorder = isDark ? "#30363d" : "#d0d7de";
626
+ const headerText = isDark ? "#e6edf3" : "#24292f";
627
+ return /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "default", border: true, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
628
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
629
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", size: 1, children: "Meta Tags Preview" }),
630
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
631
+ /* @__PURE__ */ jsxs(Text, { size: 0, muted: true, children: [
632
+ tagCount,
633
+ " tags generated"
634
+ ] }),
635
+ /* @__PURE__ */ jsx(
636
+ Button,
637
+ {
638
+ text: copyLabel,
639
+ tone: "default",
640
+ mode: "ghost",
641
+ fontSize: 1,
642
+ padding: 2,
643
+ onClick: handleCopy
644
+ }
645
+ )
646
+ ] })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs(
649
+ "button",
650
+ {
651
+ type: "button",
652
+ onClick: () => setExpanded((p) => !p),
653
+ style: {
654
+ display: "flex",
655
+ alignItems: "center",
656
+ justifyContent: "center",
657
+ gap: 6,
658
+ width: "100%",
659
+ background: headerBg,
660
+ border: `1px solid ${headerBorder}`,
661
+ borderRadius: 6,
662
+ padding: "6px 12px",
663
+ cursor: "pointer",
664
+ fontSize: 12,
665
+ fontWeight: 500,
666
+ color: headerText
667
+ },
668
+ children: [
669
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10 }, children: expanded ? "\u25B2" : "\u25BC" }),
670
+ expanded ? "Hide tags" : "Show tags"
671
+ ]
672
+ }
673
+ ),
674
+ expanded && /* @__PURE__ */ jsxs(
675
+ "div",
676
+ {
677
+ style: {
678
+ borderRadius: 6,
679
+ border: `1px solid ${codeBorder}`,
680
+ overflow: "hidden"
681
+ },
682
+ children: [
683
+ /* @__PURE__ */ jsxs(
684
+ "div",
685
+ {
686
+ style: {
687
+ background: headerBg,
688
+ borderBottom: `1px solid ${headerBorder}`,
689
+ padding: "6px 14px",
690
+ display: "flex",
691
+ alignItems: "center",
692
+ gap: 6
693
+ },
694
+ children: [
695
+ /* @__PURE__ */ jsx(
696
+ "span",
697
+ {
698
+ style: {
699
+ width: 8,
700
+ height: 8,
701
+ borderRadius: "50%",
702
+ background: isDark ? "#388bfd" : "#0550ae",
703
+ display: "inline-block"
704
+ }
705
+ }
706
+ ),
707
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: headerText, fontFamily: "monospace" }, children: "HTML" })
708
+ ]
709
+ }
710
+ ),
711
+ /* @__PURE__ */ jsx(
712
+ Box,
713
+ {
714
+ as: "pre",
715
+ padding: 3,
716
+ style: {
717
+ margin: 0,
718
+ maxHeight: 420,
719
+ overflow: "auto",
720
+ fontSize: 12,
721
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
722
+ lineHeight: 1.7,
723
+ whiteSpace: "pre-wrap",
724
+ wordBreak: "break-word",
725
+ backgroundColor: codeBg
726
+ },
727
+ children: tokens.map((token, idx) => /* @__PURE__ */ jsx("span", { style: { color: token.color }, children: token.text }, idx))
728
+ }
729
+ )
730
+ ]
731
+ }
732
+ )
733
+ ] }) });
734
+ };
735
+ var MetaTagsPreview_default = MetaTagsPreview;
58
736
  var MetaTitle = (props) => {
59
- var _a, _b;
737
+ var _a, _b, _c;
60
738
  const { value, renderDefault, path, schemaType } = props;
61
739
  const { options } = schemaType;
62
740
  const parent = useFormValue([path[0]]);
@@ -65,6 +743,7 @@ var MetaTitle = (props) => {
65
743
  const rootDoc = (_a = useFormValue([])) != null ? _a : {};
66
744
  const client = useClient({ apiVersion: (_b = options == null ? void 0 : options.apiVersion) != null ? _b : "2024-01-01" });
67
745
  const [groqTitleSuffix, setGroqTitleSuffix] = useState("");
746
+ const [isDuplicateTitle, setIsDuplicateTitle] = useState(false);
68
747
  const titleSuffixQuery = options == null ? void 0 : options.titleSuffixQuery;
69
748
  const titleSuffixOption = options == null ? void 0 : options.titleSuffix;
70
749
  useEffect(() => {
@@ -75,6 +754,21 @@ var MetaTitle = (props) => {
75
754
  setGroqTitleSuffix("");
76
755
  });
77
756
  }, [titleSuffixQuery, client]);
757
+ const currentDocId = (_c = rootDoc._id) != null ? _c : "";
758
+ useEffect(() => {
759
+ if (!(value == null ? void 0 : value.trim()) || !currentDocId) {
760
+ setIsDuplicateTitle(false);
761
+ return () => {
762
+ };
763
+ }
764
+ const timer = setTimeout(() => {
765
+ client.fetch(
766
+ `count(*[seo.title == $title && _id != $id && !(_id in path("drafts.**"))])`,
767
+ { title: value, id: currentDocId }
768
+ ).then((count) => setIsDuplicateTitle(count > 0)).catch(() => setIsDuplicateTitle(false));
769
+ }, 600);
770
+ return () => clearTimeout(timer);
771
+ }, [value, client, currentDocId]);
78
772
  const resolvedSuffix = useMemo(() => {
79
773
  if (titleSuffixQuery) return groqTitleSuffix;
80
774
  if (!titleSuffixOption) return "";
@@ -90,20 +784,36 @@ var MetaTitle = (props) => {
90
784
  );
91
785
  return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
92
786
  renderDefault(props),
93
- /* @__PURE__ */ jsx(Stack, { space: 2, children: feedbackItems.map((item) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
94
- /* @__PURE__ */ jsx(
95
- "div",
96
- {
97
- style: {
98
- minWidth: 10,
99
- height: 10,
100
- borderRadius: "50%",
101
- backgroundColor: item.color
787
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
788
+ feedbackItems.map((item) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
789
+ /* @__PURE__ */ jsx(
790
+ "div",
791
+ {
792
+ style: {
793
+ minWidth: 10,
794
+ height: 10,
795
+ borderRadius: "50%",
796
+ backgroundColor: item.color
797
+ }
102
798
  }
103
- }
104
- ),
105
- /* @__PURE__ */ jsx(Text, { weight: "bold", muted: true, size: 14, children: item.text })
106
- ] }, item.text)) })
799
+ ),
800
+ /* @__PURE__ */ jsx(Text, { weight: "bold", muted: true, size: 14, children: item.text })
801
+ ] }, item.text)),
802
+ isDuplicateTitle && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 7 }, children: [
803
+ /* @__PURE__ */ jsx(
804
+ "div",
805
+ {
806
+ style: {
807
+ minWidth: 10,
808
+ height: 10,
809
+ borderRadius: "50%",
810
+ backgroundColor: "orange"
811
+ }
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsx(Text, { weight: "bold", muted: true, size: 14, children: "Duplicate meta title \u2014 another published document uses this exact title." })
815
+ ] })
816
+ ] })
107
817
  ] });
108
818
  };
109
819
  var MetaTitle_default = MetaTitle;
@@ -132,7 +842,7 @@ var DEFAULT_FIELD_INFO = {
132
842
  },
133
843
  robots: {
134
844
  title: "Robots Settings",
135
- description: "Configure how search engine crawlers should index and follow links on this page."
845
+ description: "Configure how search engine crawlers should index, follow links, translate content, and index images on this page."
136
846
  },
137
847
  metaAttributes: {
138
848
  title: "Additional Meta Attributes",
@@ -201,6 +911,22 @@ var DEFAULT_FIELD_INFO = {
201
911
  twitterImageUrl: {
202
912
  title: "X Image URL",
203
913
  description: "Enter the full URL of the image for X (formerly Twitter). Ensure the image is accessible and meets the recommended size."
914
+ },
915
+ focusKeyword: {
916
+ title: "Focus Keyword",
917
+ description: "The primary keyword you want this page to rank for. Used to check keyword placement in title, description, and URL."
918
+ },
919
+ hreflangs: {
920
+ title: "Hreflang Entries",
921
+ description: "Add alternate language/region versions of this page. Include x-default for the fallback URL."
922
+ },
923
+ geoChecklist: {
924
+ title: "AI Overview Readiness",
925
+ description: "Checklist of signals that improve visibility in AI-generated overviews."
926
+ },
927
+ metaTagsPreview: {
928
+ title: "Meta Tags Preview",
929
+ description: "Preview the full HTML <head> meta tags generated for this page."
204
930
  }
205
931
  };
206
932
  var getFieldInfo = (fieldName, fieldOverrides) => {
@@ -677,7 +1403,7 @@ function twitter(config = {}) {
677
1403
  }
678
1404
 
679
1405
  // src/schemas/index.ts
680
- var LazySeoPreview = lazy(() => import('./SeoPreview-KEGGQSPJ.js'));
1406
+ var LazySeoPreview = lazy(() => import('./SeoPreview-4PPAF4NO.js'));
681
1407
  var SeoPreviewWrapper = (props) => createElement(Suspense, { fallback: null }, createElement(LazySeoPreview, props));
682
1408
  function buildFieldGroupMap(groups) {
683
1409
  const map = /* @__PURE__ */ new Map();
@@ -822,6 +1548,60 @@ function seoFieldsSchema(config = {}) {
822
1548
  })),
823
1549
  fieldGroupMap
824
1550
  ),
1551
+ withGroup(
1552
+ defineField(__spreadProps(__spreadValues({
1553
+ name: "focusKeyword"
1554
+ }, getFieldInfo("focusKeyword", config.fieldOverrides)), {
1555
+ type: "string",
1556
+ components: {
1557
+ input: FocusKeywordInput_default
1558
+ },
1559
+ hidden: getFieldHiddenFunction("focusKeyword", config)
1560
+ })),
1561
+ fieldGroupMap
1562
+ ),
1563
+ withGroup(
1564
+ defineField(__spreadProps(__spreadValues({
1565
+ name: "hreflangs"
1566
+ }, getFieldInfo("hreflangs", config.fieldOverrides)), {
1567
+ type: "array",
1568
+ of: [{ type: "hreflangEntry" }],
1569
+ description: "Add alternate language/region versions. Include x-default for the fallback URL.",
1570
+ hidden: getFieldHiddenFunction("hreflangs", config)
1571
+ })),
1572
+ fieldGroupMap
1573
+ ),
1574
+ ...config.geo === false ? [] : [
1575
+ withGroup(
1576
+ defineField(__spreadProps(__spreadValues({
1577
+ name: "geoChecklist"
1578
+ }, getFieldInfo("geoChecklist", config.fieldOverrides)), {
1579
+ type: "string",
1580
+ readOnly: true,
1581
+ components: {
1582
+ input: GEOChecklist_default
1583
+ },
1584
+ options: __spreadValues({}, config.licenseKey ? { licenseKey: config.licenseKey } : {}),
1585
+ hidden: getFieldHiddenFunction("geoChecklist", config)
1586
+ })),
1587
+ fieldGroupMap
1588
+ )
1589
+ ],
1590
+ ...config.metaTagsPreview === false ? [] : [
1591
+ withGroup(
1592
+ defineField(__spreadProps(__spreadValues({
1593
+ name: "metaTagsPreview"
1594
+ }, getFieldInfo("metaTagsPreview", config.fieldOverrides)), {
1595
+ type: "string",
1596
+ readOnly: true,
1597
+ components: {
1598
+ input: MetaTagsPreview_default
1599
+ },
1600
+ hidden: getFieldHiddenFunction("metaTagsPreview", config)
1601
+ })),
1602
+ fieldGroupMap
1603
+ )
1604
+ ],
825
1605
  withGroup(openGraph(config), fieldGroupMap),
826
1606
  withGroup(twitter(config), fieldGroupMap)
827
1607
  ]
@@ -891,6 +1671,38 @@ function baseMeta(config = {}) {
891
1671
  ]
892
1672
  });
893
1673
  }
1674
+ var hreflang_default = defineType({
1675
+ name: "hreflangEntry",
1676
+ title: "Hreflang Entry",
1677
+ type: "object",
1678
+ fields: [
1679
+ defineField({
1680
+ name: "locale",
1681
+ title: "Language / Region",
1682
+ type: "string",
1683
+ description: "BCP 47 tag \u2014 e.g. en, fr-FR, x-default",
1684
+ validation: (Rule) => Rule.required().custom((value) => {
1685
+ if (!value) return "Required";
1686
+ if (!/^(x-default|[a-z]{2,3}(-[A-Z]{2,3})?)$/.test(value)) {
1687
+ return "Must be a valid BCP 47 tag (e.g. en, fr-FR, x-default)";
1688
+ }
1689
+ return true;
1690
+ })
1691
+ }),
1692
+ defineField({
1693
+ name: "url",
1694
+ title: "URL",
1695
+ type: "url",
1696
+ validation: (Rule) => Rule.required().uri({ scheme: ["http", "https"] })
1697
+ })
1698
+ ],
1699
+ preview: {
1700
+ select: {
1701
+ title: "locale",
1702
+ subtitle: "url"
1703
+ }
1704
+ }
1705
+ });
894
1706
  var metaAttribute_default = defineType({
895
1707
  name: "metaAttribute",
896
1708
  title: "Meta Attribute",
@@ -981,9 +1793,23 @@ var robots_default = defineType({
981
1793
  type: "boolean",
982
1794
  initialValue: false,
983
1795
  description: "Enable this to prevent search engines from following links on this page. Links will not pass SEO value."
1796
+ }),
1797
+ defineField({
1798
+ name: "noTranslate",
1799
+ title: "No Translate",
1800
+ type: "boolean",
1801
+ initialValue: false,
1802
+ description: "Enable this to prevent search engines from offering a translated version of this page in search results."
1803
+ }),
1804
+ defineField({
1805
+ name: "noImageIndex",
1806
+ title: "No Image Index",
1807
+ type: "boolean",
1808
+ initialValue: false,
1809
+ description: "Enable this to prevent images on this page from appearing in image search results."
984
1810
  })
985
1811
  ],
986
- description: "Select how search engines should index and follow links on this page."
1812
+ description: "Select how search engines should index, follow links, translate content, and index images on this page."
987
1813
  });
988
1814
 
989
1815
  // src/schemas/types/index.ts
@@ -999,13 +1825,14 @@ function types(config = {}) {
999
1825
  // pass config here
1000
1826
  metaAttribute_default,
1001
1827
  metaTag_default,
1002
- robots_default
1828
+ robots_default,
1829
+ hreflang_default
1003
1830
  ];
1004
1831
  }
1005
1832
 
1006
1833
  // src/plugin.ts
1007
- var resolveDashboardConfig = (healthDashboard, rootApiVersion) => {
1008
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
1834
+ var resolveDashboardConfig = (healthDashboard, rootApiVersion, rootLicenseKey) => {
1835
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
1009
1836
  const cfg = typeof healthDashboard === "object" ? healthDashboard : void 0;
1010
1837
  const deprecationWarnings = [];
1011
1838
  if ((cfg == null ? void 0 : cfg.apiVersion) !== void 0) {
@@ -1018,27 +1845,37 @@ var resolveDashboardConfig = (healthDashboard, rootApiVersion) => {
1018
1845
  '[sanity-plugin-seofields] "healthDashboard.apiVersion" is deprecated. Move it to the root "apiVersion" option instead.'
1019
1846
  );
1020
1847
  }
1848
+ if ((cfg == null ? void 0 : cfg.licenseKey) !== void 0 && !rootLicenseKey) {
1849
+ deprecationWarnings.push({
1850
+ key: "healthDashboard.licenseKey \u2192 licenseKey",
1851
+ version: "v1.8.0",
1852
+ changelogUrl: "https://github.com/hardik-143/sanity-plugin-seofields/blob/main/CHANGELOG.md"
1853
+ });
1854
+ console.warn(
1855
+ '[sanity-plugin-seofields] "healthDashboard.licenseKey" is deprecated. Move it to the root "licenseKey" option instead.'
1856
+ );
1857
+ }
1021
1858
  return {
1022
1859
  enabled: healthDashboard !== false,
1023
- toolTitle: (_b = (_a = cfg == null ? void 0 : cfg.tool) == null ? void 0 : _a.title) != null ? _b : "SEO Health",
1024
- toolName: (_d = (_c = cfg == null ? void 0 : cfg.tool) == null ? void 0 : _c.name) != null ? _d : "seo-health-dashboard",
1025
- icon: (_e = cfg == null ? void 0 : cfg.content) == null ? void 0 : _e.icon,
1026
- title: (_f = cfg == null ? void 0 : cfg.content) == null ? void 0 : _f.title,
1027
- description: (_g = cfg == null ? void 0 : cfg.content) == null ? void 0 : _g.description,
1860
+ toolTitle: (_c = (_b = (_a = cfg == null ? void 0 : cfg.tool) == null ? void 0 : _a.title) != null ? _b : cfg == null ? void 0 : cfg.toolTitle) != null ? _c : "SEO Health",
1861
+ toolName: (_e = (_d = cfg == null ? void 0 : cfg.tool) == null ? void 0 : _d.name) != null ? _e : "seo-health-dashboard",
1862
+ icon: (_f = cfg == null ? void 0 : cfg.content) == null ? void 0 : _f.icon,
1863
+ title: (_g = cfg == null ? void 0 : cfg.content) == null ? void 0 : _g.title,
1864
+ description: (_h = cfg == null ? void 0 : cfg.content) == null ? void 0 : _h.description,
1028
1865
  showTypeColumn: cfg == null ? void 0 : cfg.showTypeColumn,
1029
1866
  showDocumentId: cfg == null ? void 0 : cfg.showDocumentId,
1030
- queryTypes: (_h = cfg == null ? void 0 : cfg.query) == null ? void 0 : _h.types,
1031
- queryRequireSeo: (_i = cfg == null ? void 0 : cfg.query) == null ? void 0 : _i.requireSeo,
1032
- queryGroq: (_j = cfg == null ? void 0 : cfg.query) == null ? void 0 : _j.groq,
1033
- apiVersion: (_k = cfg == null ? void 0 : cfg.apiVersion) != null ? _k : rootApiVersion,
1034
- licenseKey: cfg == null ? void 0 : cfg.licenseKey,
1867
+ queryTypes: (_i = cfg == null ? void 0 : cfg.query) == null ? void 0 : _i.types,
1868
+ queryRequireSeo: (_j = cfg == null ? void 0 : cfg.query) == null ? void 0 : _j.requireSeo,
1869
+ queryGroq: (_k = cfg == null ? void 0 : cfg.query) == null ? void 0 : _k.groq,
1870
+ apiVersion: (_l = cfg == null ? void 0 : cfg.apiVersion) != null ? _l : rootApiVersion,
1871
+ licenseKey: rootLicenseKey != null ? rootLicenseKey : cfg == null ? void 0 : cfg.licenseKey,
1035
1872
  typeDisplayLabels: cfg == null ? void 0 : cfg.typeDisplayLabels,
1036
1873
  typeColumnMode: cfg == null ? void 0 : cfg.typeColumnMode,
1037
1874
  titleField: cfg == null ? void 0 : cfg.titleField,
1038
1875
  getDocumentBadge: cfg == null ? void 0 : cfg.getDocumentBadge,
1039
- loadingLicense: (_l = cfg == null ? void 0 : cfg.content) == null ? void 0 : _l.loadingLicense,
1040
- loadingDocuments: (_m = cfg == null ? void 0 : cfg.content) == null ? void 0 : _m.loadingDocuments,
1041
- noDocuments: (_n = cfg == null ? void 0 : cfg.content) == null ? void 0 : _n.noDocuments,
1876
+ loadingLicense: (_m = cfg == null ? void 0 : cfg.content) == null ? void 0 : _m.loadingLicense,
1877
+ loadingDocuments: (_n = cfg == null ? void 0 : cfg.content) == null ? void 0 : _n.loadingDocuments,
1878
+ noDocuments: (_o = cfg == null ? void 0 : cfg.content) == null ? void 0 : _o.noDocuments,
1042
1879
  previewMode: cfg == null ? void 0 : cfg.previewMode,
1043
1880
  structureTool: cfg == null ? void 0 : cfg.structureTool,
1044
1881
  exportEnabled: (() => {
@@ -1053,14 +1890,15 @@ var resolveDashboardConfig = (healthDashboard, rootApiVersion) => {
1053
1890
  if (typeof exportCfg === "object" && exportCfg.formats) return exportCfg.formats;
1054
1891
  return ["csv", "json"];
1055
1892
  })(),
1056
- compactStats: (_o = cfg == null ? void 0 : cfg.compactStats) != null ? _o : false,
1893
+ compactStats: (_p = cfg == null ? void 0 : cfg.compactStats) != null ? _p : false,
1057
1894
  deprecationWarnings
1058
1895
  };
1059
1896
  };
1060
1897
  var seofields = definePlugin((config = {}) => {
1061
- const { healthDashboard = true } = config;
1062
- const dash = resolveDashboardConfig(healthDashboard, config.apiVersion);
1063
- const LazySeoHealthTool = lazy(() => import('./SeoHealthTool-BSTYAKQS.js'));
1898
+ const cfg = config;
1899
+ const { healthDashboard = true, licenseKey, publishGate } = cfg;
1900
+ const dash = resolveDashboardConfig(healthDashboard, cfg.apiVersion, licenseKey);
1901
+ const LazySeoHealthTool = lazy(() => import('./SeoHealthTool-3CUKGADZ.js'));
1064
1902
  const BoundSeoHealthTool = () => createElement(
1065
1903
  Suspense,
1066
1904
  { fallback: null },
@@ -1090,12 +1928,23 @@ var seofields = definePlugin((config = {}) => {
1090
1928
  _deprecationWarnings: dash.deprecationWarnings
1091
1929
  })
1092
1930
  );
1093
- return __spreadValues({
1931
+ return __spreadValues(__spreadValues({
1094
1932
  name: "sanity-plugin-seofields",
1095
1933
  schema: {
1096
- types: types(config)
1934
+ types: types(cfg)
1097
1935
  }
1098
- }, dash.enabled && {
1936
+ }, publishGate && licenseKey && {
1937
+ document: {
1938
+ actions: (prev, context) => {
1939
+ var _a;
1940
+ if (((_a = publishGate.types) == null ? void 0 : _a.length) && !publishGate.types.includes(context.schemaType))
1941
+ return prev;
1942
+ return prev.map(
1943
+ (Action) => Action.action === "publish" ? createSeoPublishGateAction(Action, __spreadProps(__spreadValues({}, publishGate), { licenseKey })) : Action
1944
+ );
1945
+ }
1946
+ }
1947
+ }), dash.enabled && {
1099
1948
  tools: [
1100
1949
  {
1101
1950
  name: dash.toolName,
@@ -1107,28 +1956,125 @@ var seofields = definePlugin((config = {}) => {
1107
1956
  });
1108
1957
  });
1109
1958
  var plugin_default = seofields;
1110
- var LazySeoHealthDashboard = lazy(() => import('./SeoHealthDashboard-BEZVLLFG.js'));
1111
- function createSeoHealthPane(optionsOrS, optionsWhenS) {
1112
- const S = optionsOrS;
1113
- const _a = optionsWhenS != null ? optionsWhenS : {}, { query, openInPane = true, title: paneTitle } = _a, rest = __objRest(_a, ["query", "openInPane", "title"]);
1114
- const SeoHealthPane = () => /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
1115
- LazySeoHealthDashboard,
1116
- __spreadValues({
1117
- customQuery: query,
1118
- openInPane,
1119
- title: paneTitle
1120
- }, rest)
1121
- ) });
1122
- SeoHealthPane.displayName = "SeoHealthPane";
1123
- return S.component(SeoHealthPane).title(paneTitle != null ? paneTitle : "SEO Health").child((docId, { params }) => {
1124
- const builder = S.document().documentId(docId);
1125
- return (params == null ? void 0 : params.type) ? builder.schemaType(params.type) : builder;
1126
- });
1127
- }
1959
+
1960
+ // src/utils/scoring.ts
1961
+ var getStatusCategory = (score) => {
1962
+ if (score >= 80) return "excellent";
1963
+ if (score >= 60) return "good";
1964
+ if (score >= 40) return "fair";
1965
+ if (score > 0) return "poor";
1966
+ return "missing";
1967
+ };
1968
+ var scoreMetaTitle = (title) => {
1969
+ const issues = [];
1970
+ let score = 0;
1971
+ if (title && title.length >= 50 && title.length <= 60) {
1972
+ score = 15;
1973
+ } else if (title && title.length > 0) {
1974
+ score = 10;
1975
+ if (title.length < 50) issues.push("Meta title too short (< 50 chars)");
1976
+ if (title.length > 60) issues.push("Meta title too long (> 60 chars)");
1977
+ } else {
1978
+ issues.push("Missing meta title");
1979
+ }
1980
+ return { score, issues };
1981
+ };
1982
+ var scoreMetaDescription = (description) => {
1983
+ const issues = [];
1984
+ let score = 0;
1985
+ if (description && description.length >= 120 && description.length <= 160) {
1986
+ score = 15;
1987
+ } else if (description && description.length > 0) {
1988
+ score = 10;
1989
+ if (description.length < 120) issues.push("Meta description too short (< 120 chars)");
1990
+ if (description.length > 160) issues.push("Meta description too long (> 160 chars)");
1991
+ } else {
1992
+ issues.push("Missing meta description");
1993
+ }
1994
+ return { score, issues };
1995
+ };
1996
+ var scoreOpenGraph = (openGraph2) => {
1997
+ const issues = [];
1998
+ let score = 0;
1999
+ if (openGraph2) {
2000
+ if (openGraph2.title) score += 6;
2001
+ else issues.push("Missing OG title");
2002
+ if (openGraph2.description) score += 6;
2003
+ else issues.push("Missing OG description");
2004
+ if (openGraph2.image) score += 6;
2005
+ else issues.push("Missing OG image");
2006
+ if (openGraph2.type) score += 7;
2007
+ else issues.push("Missing OG type");
2008
+ } else {
2009
+ issues.push("Open Graph not configured");
2010
+ }
2011
+ return { score, issues };
2012
+ };
2013
+ var scoreTwitterCard = (twitter2) => {
2014
+ const issues = [];
2015
+ let score = 0;
2016
+ if (twitter2) {
2017
+ if (twitter2.title) score += 5;
2018
+ else issues.push("Missing Twitter title");
2019
+ if (twitter2.description) score += 5;
2020
+ else issues.push("Missing Twitter description");
2021
+ if (twitter2.image) score += 5;
2022
+ else issues.push("Missing Twitter image");
2023
+ } else {
2024
+ issues.push("Twitter Card not configured");
2025
+ }
2026
+ return { score, issues };
2027
+ };
2028
+ var calculateHealthScore = (doc) => {
2029
+ if (!doc.seo) {
2030
+ return { score: 0, status: "missing", issues: ["SEO fields not configured"] };
2031
+ }
2032
+ const seo = doc.seo;
2033
+ const title = seo.title;
2034
+ const description = seo.description;
2035
+ const keywords = seo.keywords;
2036
+ const robots = seo.robots;
2037
+ const openGraph2 = seo.openGraph;
2038
+ const twitter2 = seo.twitter;
2039
+ let score = 0;
2040
+ const issues = [];
2041
+ const titleResult = scoreMetaTitle(title);
2042
+ score += titleResult.score;
2043
+ issues.push(...titleResult.issues);
2044
+ const descResult = scoreMetaDescription(description);
2045
+ score += descResult.score;
2046
+ issues.push(...descResult.issues);
2047
+ if (seo.metaImage) score += 10;
2048
+ else issues.push("Missing meta image");
2049
+ if (keywords && keywords.length > 0) score += 10;
2050
+ else issues.push("No keywords defined");
2051
+ if (robots && !robots.noIndex) score += 5;
2052
+ else if (!robots) score += 5;
2053
+ const ogResult = scoreOpenGraph(openGraph2);
2054
+ score += ogResult.score;
2055
+ issues.push(...ogResult.issues);
2056
+ const twResult = scoreTwitterCard(twitter2);
2057
+ score += twResult.score;
2058
+ issues.push(...twResult.issues);
2059
+ const hasMetaImage = !!seo.metaImage;
2060
+ const hasOgImage = !!(openGraph2 && openGraph2.image);
2061
+ const hasTwitterImage = !!(twitter2 && twitter2.image);
2062
+ if (hasMetaImage && hasOgImage && hasTwitterImage) {
2063
+ score += 5;
2064
+ } else {
2065
+ const missingImages = [];
2066
+ if (!hasMetaImage) missingImages.push("meta image");
2067
+ if (!hasOgImage) missingImages.push("OG image");
2068
+ if (!hasTwitterImage) missingImages.push("Twitter image");
2069
+ issues.push(`Missing images for full score: ${missingImages.join(", ")}`);
2070
+ }
2071
+ const status = getStatusCategory(score);
2072
+ return { score, status, issues };
2073
+ };
1128
2074
 
1129
2075
  // src/index.ts
1130
2076
  var src_default = plugin_default;
1131
2077
 
1132
- export { types as allSchemas, baseMeta as baseMetaSchema, createSeoHealthPane, src_default as default, metaAttribute_default as metaAttributeSchema, metaTag_default as metaTagSchema, openGraph as openGraphSchema, robots_default as robotsSchema, seoFieldsSchema, twitter as twitterSchema };
2078
+ export { types as allSchemas, baseMeta as baseMetaSchema, calculateHealthScore, src_default as default, metaAttribute_default as metaAttributeSchema, metaTag_default as metaTagSchema, openGraph as openGraphSchema, robots_default as robotsSchema, seoFieldsSchema, twitter as twitterSchema };
1133
2079
  //# sourceMappingURL=index.js.map
1134
2080
  //# sourceMappingURL=index.js.map