sanity-plugin-seofields 1.9.0 → 1.10.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.
Files changed (56) hide show
  1. package/README.md +71 -0
  2. package/dist/{SeoPreview-LMZAWWOE.cjs → SeoPreview-A2XZVMT2.cjs} +4 -4
  3. package/dist/{SeoPreview-LMZAWWOE.cjs.map → SeoPreview-A2XZVMT2.cjs.map} +1 -1
  4. package/dist/{SeoPreview-UNQBKTQO.js → SeoPreview-TATJQFMP.js} +2 -2
  5. package/dist/chunk-CRCXC45D.js +56 -0
  6. package/dist/chunk-CRCXC45D.js.map +1 -0
  7. package/dist/{chunk-HHO2AKAP.js → chunk-DB5MBPRK.js} +13 -13
  8. package/dist/chunk-DB5MBPRK.js.map +1 -0
  9. package/dist/{chunk-3OK3S2F7.cjs → chunk-EUG4MNRC.cjs} +60 -20
  10. package/dist/chunk-EUG4MNRC.cjs.map +1 -0
  11. package/dist/{chunk-Y46ACXM4.cjs → chunk-LHUFL5LS.cjs} +13 -13
  12. package/dist/chunk-LHUFL5LS.cjs.map +1 -0
  13. package/dist/{chunk-Z5AOUU4H.js → chunk-NLEB47UV.js} +60 -20
  14. package/dist/chunk-NLEB47UV.js.map +1 -0
  15. package/dist/{chunk-FUUWBQE2.cjs → chunk-OULUDJPI.cjs} +66 -7
  16. package/dist/chunk-OULUDJPI.cjs.map +1 -0
  17. package/dist/{chunk-KIEO6QG3.js → chunk-XD4HKLEA.js} +66 -7
  18. package/dist/chunk-XD4HKLEA.js.map +1 -0
  19. package/dist/chunk-XYAZJ3WX.cjs +56 -0
  20. package/dist/chunk-XYAZJ3WX.cjs.map +1 -0
  21. package/dist/cli.js +1 -1
  22. package/dist/head.cjs +10 -2
  23. package/dist/head.cjs.map +1 -1
  24. package/dist/head.d.cts +61 -1
  25. package/dist/head.d.ts +61 -1
  26. package/dist/head.js +9 -1
  27. package/dist/hreflang-_VbSuWYD.d.cts +29 -0
  28. package/dist/hreflang-_VbSuWYD.d.ts +29 -0
  29. package/dist/index.cjs +173 -38
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +3 -2
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.js +336 -201
  34. package/dist/index.js.map +1 -1
  35. package/dist/next.cjs +11 -3
  36. package/dist/next.cjs.map +1 -1
  37. package/dist/next.d.cts +2 -1
  38. package/dist/next.d.ts +2 -1
  39. package/dist/next.js +9 -1
  40. package/dist/next.js.map +1 -1
  41. package/dist/{plugin-DoGeZP79.d.cts → plugin-HFuWZsJu.d.cts} +68 -1
  42. package/dist/{plugin-DQdThyxG.d.ts → plugin-oQUCim56.d.ts} +68 -1
  43. package/dist/server.cjs +2 -2
  44. package/dist/server.cjs.map +1 -1
  45. package/dist/server.d.cts +6 -8
  46. package/dist/server.d.ts +6 -8
  47. package/dist/server.js +1 -1
  48. package/dist/server.js.map +1 -1
  49. package/package.json +2 -2
  50. package/dist/chunk-3OK3S2F7.cjs.map +0 -1
  51. package/dist/chunk-FUUWBQE2.cjs.map +0 -1
  52. package/dist/chunk-HHO2AKAP.js.map +0 -1
  53. package/dist/chunk-KIEO6QG3.js.map +0 -1
  54. package/dist/chunk-Y46ACXM4.cjs.map +0 -1
  55. package/dist/chunk-Z5AOUU4H.js.map +0 -1
  56. /package/dist/{SeoPreview-UNQBKTQO.js.map → SeoPreview-TATJQFMP.js.map} +0 -0
package/README.md CHANGED
@@ -334,6 +334,7 @@ seofields({
334
334
  | `fieldVisibility` | Hide specific SEO fields for specific document types |
335
335
  | `fieldGroups` | Customize how fields are grouped in the `seoFields` object |
336
336
  | `apiVersion` | Sanity API version used by plugin clients |
337
+ | `hreflang` | Auto-populate hreflangs from translations — see [Hreflang auto-populate](#hreflang-auto-populate) |
337
338
  | `dashboard` | Enable and configure the SEO Health Dashboard tool |
338
339
  | `licenseKey` | License key for pro features (dashboard, publish gate, pro industries) |
339
340
  | `ai` | Enable "Generate with AI" fields — see [AI Content Generation](#ai-content-generation) |
@@ -381,6 +382,18 @@ export const {POST, OPTIONS} = createNextRouteHandler({
381
382
 
382
383
  Without `industry` set, generation uses generic prompts, which are always free.
383
384
 
385
+ **Custom prompts** — write your own prompt wording with `customPrompt` (free: one function) or `customPrompts` (paid: up to 5 generic + 5 per industry, behind a license). Your function receives all extracted document values (`content`, `focusKeyword`, `keywords`, `meta`, `field`, `industry`) and returns the prompt string. Custom prompts replace the built-in pool by default; set `merge: true` to mix them in. In proxy mode set them on the server handler config. See [AI.md → Custom Prompts](./AI.md#custom-prompts).
386
+
387
+ ```ts
388
+ seofields({
389
+ ai: {
390
+ provider: 'openai',
391
+ apiKey: process.env.SANITY_STUDIO_OPENAI_API_KEY,
392
+ customPrompt: (v) => `Write a 55-char SEO ${v.field} about: ${v.content.slice(0, 200)}`,
393
+ },
394
+ })
395
+ ```
396
+
384
397
  **Content source** (`ai.content`) — defaults to the document's `body` field. Accepts a single field, an array of fields in priority order, or a per-document-type mapping with a `default` fallback. Nested Portable Text is found automatically.
385
398
 
386
399
  **Refinement pipeline** — each generation attempt is checked for target character length, required keyword presence (injected if missing), readability (simplified if too complex), and focus-keyword verbatim match against existing title/description. Controlled by `maxRetries` and `keepFirstOnValidationFail`.
@@ -840,6 +853,64 @@ Docs: [Frontend integration](https://sanity-plugin-seofields.thehardik.in/docs/f
840
853
 
841
854
  ---
842
855
 
856
+ ## Hreflang auto-populate
857
+
858
+ For projects using [`@sanity/document-internationalization`](https://github.com/sanity-io/document-internationalization), derive hreflang alternates from your translation references instead of typing them by hand.
859
+
860
+ **Frontend** — `buildHreflangs()` turns the resolved `_translations` array into entries and feeds `buildSeoMeta`:
861
+
862
+ ```ts
863
+ import {buildSeoMeta, buildHreflangs} from 'sanity-plugin-seofields/next' // or /head
864
+
865
+ // GROQ: "_translations": *[_type=="translation.metadata" && references(^._id)].translations[].value->{ language, "slug": slug.current }
866
+ export async function generateMetadata() {
867
+ return buildSeoMeta({
868
+ seo: data.seo,
869
+ baseUrl: 'https://example.com',
870
+ path: `/${data.slug.current}`,
871
+ hreflangs: buildHreflangs(data._translations, {
872
+ baseUrl: 'https://example.com',
873
+ xDefault: 'en',
874
+ // resolvePath: (t) => `/${t.language}/${t.slug}`,
875
+ }),
876
+ })
877
+ }
878
+ ```
879
+
880
+ **Studio** — enable `hreflang.autoFill` to add a **Sync from translations** button to the `hreflangs` field (entries stay editable):
881
+
882
+ ```ts
883
+ seofields({
884
+ baseUrl: 'https://example.com',
885
+ hreflang: {autoFill: true /*, localeField: 'language', resolvePath */},
886
+ })
887
+ ```
888
+
889
+ The Studio sync reads `translation.metadata` via the standard client — no extra dependency required.
890
+
891
+ ---
892
+
893
+ ## llms.txt generator
894
+
895
+ Generate an [llms.txt](https://llmstxt.org) file from your Sanity content with `buildLlmsTxt()` + `docsToLlmsSection()` (framework-neutral, exported from `/head` and `/next`):
896
+
897
+ ```ts
898
+ import {buildLlmsTxt, docsToLlmsSection} from 'sanity-plugin-seofields/head'
899
+
900
+ const body = buildLlmsTxt({
901
+ title: 'Acme',
902
+ summary: 'Everything Acme, for humans and LLMs.',
903
+ baseUrl: 'https://acme.com',
904
+ sections: [
905
+ docsToLlmsSection(posts, {title: 'Blog', baseUrl: 'https://acme.com'}),
906
+ docsToLlmsSection(docsPages, {title: 'Docs', baseUrl: 'https://acme.com'}),
907
+ ],
908
+ })
909
+ // serve `body` from /llms.txt (route handler or build step)
910
+ ```
911
+
912
+ ---
913
+
843
914
  ## CLI
844
915
 
845
916
  Run the CLI:
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
2
 
3
- var _chunkY46ACXM4cjs = require('./chunk-Y46ACXM4.cjs');
3
+ var _chunkLHUFL5LScjs = require('./chunk-LHUFL5LS.cjs');
4
4
  require('./chunk-A57XNQC7.cjs');
5
5
 
6
6
  // src/components/SeoPreview.tsx
@@ -163,7 +163,7 @@ var SeoPreview = (props) => {
163
163
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, PreviewBody, { children: [
164
164
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SerpUrl, { children: finalUrl ? urlDisplay : "example.com \u203A page-url" }),
165
165
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SerpTitle, { children: title && title.length > 0 ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
166
- _chunkY46ACXM4cjs.truncate.call(void 0, title, Math.max(1, 60 - (titleSuffix ? titleSuffix.length + 3 : 0))),
166
+ _chunkLHUFL5LScjs.truncate.call(void 0, title, Math.max(1, 60 - (titleSuffix ? titleSuffix.length + 3 : 0))),
167
167
  titleSuffix && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
168
168
  "span",
169
169
  {
@@ -176,7 +176,7 @@ var SeoPreview = (props) => {
176
176
  }
177
177
  )
178
178
  ] }) : "Your SEO Title will appear here" }),
179
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SerpDescription, { children: description && description.length > 0 ? _chunkY46ACXM4cjs.truncate.call(void 0, description, 160) : "Your meta description will show up here. Make it compelling!" })
179
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, SerpDescription, { children: description && description.length > 0 ? _chunkLHUFL5LScjs.truncate.call(void 0, description, 160) : "Your meta description will show up here. Make it compelling!" })
180
180
  ] })
181
181
  ] }) });
182
182
  };
@@ -184,4 +184,4 @@ var SeoPreview_default = SeoPreview;
184
184
 
185
185
 
186
186
  exports.default = SeoPreview_default;
187
- //# sourceMappingURL=SeoPreview-LMZAWWOE.cjs.map
187
+ //# sourceMappingURL=SeoPreview-A2XZVMT2.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoPreview-LMZAWWOE.cjs","../src/components/SeoPreview.tsx"],"names":[],"mappings":"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","file":"/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoPreview-LMZAWWOE.cjs","sourcesContent":[null,"import {Box} from '@sanity/ui'\nimport {type ReactElement, useEffect, useState} from 'react'\nimport {StringInputProps, useClient, useFormValue} from 'sanity'\nimport styled from 'styled-components'\n\nimport {truncate} from '../utils/seoUtils'\n\nconst PreviewContainer = styled.div`\n max-width: 600px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #ffffff;\n border: 1px solid #dadce0;\n border-radius: 8px;\n overflow: hidden;\n`\n\nconst PreviewHeader = styled.div`\n background: #f8f9fa;\n padding: 12px 16px;\n border-bottom: 1px solid #dadce0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n`\n\nconst PreviewBody = styled.div`\n padding: 16px;\n`\n\nconst SerpUrl = styled.p`\n margin: 0 0 4px;\n color: #006621;\n font-size: 13px;\n line-height: 1.4;\n word-break: break-word;\n`\n\nconst SerpTitle = styled.h3`\n margin: 0 0 8px;\n color: #1a0dab;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.4;\n word-break: break-word;\n\n &:hover {\n text-decoration: underline;\n }\n`\n\nconst SerpDescription = styled.p`\n margin: 0;\n color: #545454;\n font-size: 14px;\n line-height: 1.6;\n word-break: break-word;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n`\n\nconst LiveIndicator = styled.span`\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #4f46e5;\n background: #f0f4ff;\n padding: 4px 8px;\n border-radius: 4px;\n`\n\nconst SeoPreview = (props: StringInputProps): ReactElement => {\n const {path, schemaType} = props\n const {options} = schemaType as {\n options?: {\n baseUrl?: string\n apiVersion?: string\n prefix?: ((doc: {_type?: string} & Record<string, unknown>) => string) | string\n titleSuffix?: ((doc: {_type?: string} & Record<string, unknown>) => string) | string\n titleSuffixInheritColor?: boolean\n titleSuffixQuery?: string\n }\n }\n const baseUrl = options?.baseUrl || 'https://www.example.com'\n const prefixFunction = options?.prefix as\n | ((doc: {_type?: string} & Record<string, unknown>) => string)\n | undefined\n const titleSuffixOption = options?.titleSuffix\n const titleSuffixInheritColor = options?.titleSuffixInheritColor ?? false\n const titleSuffixQuery = options?.titleSuffixQuery\n\n const client = useClient({apiVersion: options?.apiVersion ?? '2024-01-01'})\n const [groqTitleSuffix, setGroqTitleSuffix] = useState<string>('')\n\n useEffect(() => {\n if (!titleSuffixQuery) return\n client\n .fetch<string>(titleSuffixQuery)\n .then((result) => {\n setGroqTitleSuffix(result === null || result === undefined ? '' : String(result))\n })\n .catch(() => {\n setGroqTitleSuffix('')\n })\n }, [titleSuffixQuery, client])\n const parent = useFormValue([path[0]]) || {\n title: '',\n description: '',\n canonicalUrl: '',\n }\n const rootDoc: {\n slug?: {current: string}\n } = useFormValue([]) || {\n slug: {current: ''},\n }\n const slug: string = rootDoc?.slug?.current || ''\n\n const {\n title,\n description,\n canonicalUrl: url,\n } = parent as {\n title?: string\n description?: string\n canonicalUrl?: string\n }\n\n const getTitleSuffix = (): string => {\n if (titleSuffixQuery) return groqTitleSuffix\n if (!titleSuffixOption) return ''\n if (typeof titleSuffixOption === 'function') {\n return titleSuffixOption(rootDoc as {_type?: string} & Record<string, unknown>)\n }\n return titleSuffixOption\n }\n const titleSuffix: string = getTitleSuffix()\n\n // Build full URL\n const base = (url || baseUrl)?.replace(/\\/+$/, '')\n const slugStr = String(slug || '').replace(/^\\/+/, '')\n const pref = String(\n prefixFunction ? prefixFunction(rootDoc as {slug?: {current: string}}) : '',\n ).replace(/^\\/+|\\/+$/g, '')\n const urlPath = [pref, slugStr].filter(Boolean).join('/')\n const finalUrl = urlPath ? `${base}/${urlPath}` : base\n\n // Extract domain for display\n const domain = (() => {\n try {\n const u = new URL(finalUrl || base)\n return u.hostname\n } catch {\n return 'example.com'\n }\n })()\n\n // Format URL display with › separator\n const urlDisplay = `${domain}${urlPath ? ` › ${urlPath.split('/').slice(-1)[0]}` : ''}`\n\n return (\n <Box padding={3}>\n <PreviewContainer>\n <PreviewHeader>\n <span\n style={{\n fontSize: '11px',\n color: '#5f6368',\n textTransform: 'uppercase',\n letterSpacing: '0.05em',\n }}\n >\n Search Preview\n </span>\n <LiveIndicator>\n <span\n style={{\n width: '4px',\n height: '4px',\n borderRadius: '50%',\n backgroundColor: '#4f46e5',\n display: 'inline-block',\n }}\n />\n Live\n </LiveIndicator>\n </PreviewHeader>\n\n <PreviewBody>\n <SerpUrl>{finalUrl ? urlDisplay : 'example.com › page-url'}</SerpUrl>\n <SerpTitle>\n {title && title.length > 0 ? (\n <>\n {truncate(title, Math.max(1, 60 - (titleSuffix ? titleSuffix.length + 3 : 0)))}\n {titleSuffix && (\n <span\n style={\n titleSuffixInheritColor ? undefined : {color: '#70757a', fontWeight: 400}\n }\n >\n {' '}\n | {titleSuffix}\n </span>\n )}\n </>\n ) : (\n 'Your SEO Title will appear here'\n )}\n </SerpTitle>\n <SerpDescription>\n {description && description.length > 0\n ? truncate(description, 160)\n : 'Your meta description will show up here. Make it compelling!'}\n </SerpDescription>\n </PreviewBody>\n </PreviewContainer>\n </Box>\n )\n}\n\nexport default SeoPreview\n"]}
1
+ {"version":3,"sources":["/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoPreview-A2XZVMT2.cjs","../src/components/SeoPreview.tsx"],"names":[],"mappings":"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","file":"/Users/hardik/GITHUB/sanity-plugin-seofields/npm/dist/SeoPreview-A2XZVMT2.cjs","sourcesContent":[null,"import {Box} from '@sanity/ui'\nimport {type ReactElement, useEffect, useState} from 'react'\nimport {StringInputProps, useClient, useFormValue} from 'sanity'\nimport styled from 'styled-components'\n\nimport {truncate} from '../utils/seoUtils'\n\nconst PreviewContainer = styled.div`\n max-width: 600px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #ffffff;\n border: 1px solid #dadce0;\n border-radius: 8px;\n overflow: hidden;\n`\n\nconst PreviewHeader = styled.div`\n background: #f8f9fa;\n padding: 12px 16px;\n border-bottom: 1px solid #dadce0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n`\n\nconst PreviewBody = styled.div`\n padding: 16px;\n`\n\nconst SerpUrl = styled.p`\n margin: 0 0 4px;\n color: #006621;\n font-size: 13px;\n line-height: 1.4;\n word-break: break-word;\n`\n\nconst SerpTitle = styled.h3`\n margin: 0 0 8px;\n color: #1a0dab;\n font-size: 18px;\n font-weight: 500;\n line-height: 1.4;\n word-break: break-word;\n\n &:hover {\n text-decoration: underline;\n }\n`\n\nconst SerpDescription = styled.p`\n margin: 0;\n color: #545454;\n font-size: 14px;\n line-height: 1.6;\n word-break: break-word;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n`\n\nconst LiveIndicator = styled.span`\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #4f46e5;\n background: #f0f4ff;\n padding: 4px 8px;\n border-radius: 4px;\n`\n\nconst SeoPreview = (props: StringInputProps): ReactElement => {\n const {path, schemaType} = props\n const {options} = schemaType as {\n options?: {\n baseUrl?: string\n apiVersion?: string\n prefix?: ((doc: {_type?: string} & Record<string, unknown>) => string) | string\n titleSuffix?: ((doc: {_type?: string} & Record<string, unknown>) => string) | string\n titleSuffixInheritColor?: boolean\n titleSuffixQuery?: string\n }\n }\n const baseUrl = options?.baseUrl || 'https://www.example.com'\n const prefixFunction = options?.prefix as\n | ((doc: {_type?: string} & Record<string, unknown>) => string)\n | undefined\n const titleSuffixOption = options?.titleSuffix\n const titleSuffixInheritColor = options?.titleSuffixInheritColor ?? false\n const titleSuffixQuery = options?.titleSuffixQuery\n\n const client = useClient({apiVersion: options?.apiVersion ?? '2024-01-01'})\n const [groqTitleSuffix, setGroqTitleSuffix] = useState<string>('')\n\n useEffect(() => {\n if (!titleSuffixQuery) return\n client\n .fetch<string>(titleSuffixQuery)\n .then((result) => {\n setGroqTitleSuffix(result === null || result === undefined ? '' : String(result))\n })\n .catch(() => {\n setGroqTitleSuffix('')\n })\n }, [titleSuffixQuery, client])\n const parent = useFormValue([path[0]]) || {\n title: '',\n description: '',\n canonicalUrl: '',\n }\n const rootDoc: {\n slug?: {current: string}\n } = useFormValue([]) || {\n slug: {current: ''},\n }\n const slug: string = rootDoc?.slug?.current || ''\n\n const {\n title,\n description,\n canonicalUrl: url,\n } = parent as {\n title?: string\n description?: string\n canonicalUrl?: string\n }\n\n const getTitleSuffix = (): string => {\n if (titleSuffixQuery) return groqTitleSuffix\n if (!titleSuffixOption) return ''\n if (typeof titleSuffixOption === 'function') {\n return titleSuffixOption(rootDoc as {_type?: string} & Record<string, unknown>)\n }\n return titleSuffixOption\n }\n const titleSuffix: string = getTitleSuffix()\n\n // Build full URL\n const base = (url || baseUrl)?.replace(/\\/+$/, '')\n const slugStr = String(slug || '').replace(/^\\/+/, '')\n const pref = String(\n prefixFunction ? prefixFunction(rootDoc as {slug?: {current: string}}) : '',\n ).replace(/^\\/+|\\/+$/g, '')\n const urlPath = [pref, slugStr].filter(Boolean).join('/')\n const finalUrl = urlPath ? `${base}/${urlPath}` : base\n\n // Extract domain for display\n const domain = (() => {\n try {\n const u = new URL(finalUrl || base)\n return u.hostname\n } catch {\n return 'example.com'\n }\n })()\n\n // Format URL display with › separator\n const urlDisplay = `${domain}${urlPath ? ` › ${urlPath.split('/').slice(-1)[0]}` : ''}`\n\n return (\n <Box padding={3}>\n <PreviewContainer>\n <PreviewHeader>\n <span\n style={{\n fontSize: '11px',\n color: '#5f6368',\n textTransform: 'uppercase',\n letterSpacing: '0.05em',\n }}\n >\n Search Preview\n </span>\n <LiveIndicator>\n <span\n style={{\n width: '4px',\n height: '4px',\n borderRadius: '50%',\n backgroundColor: '#4f46e5',\n display: 'inline-block',\n }}\n />\n Live\n </LiveIndicator>\n </PreviewHeader>\n\n <PreviewBody>\n <SerpUrl>{finalUrl ? urlDisplay : 'example.com › page-url'}</SerpUrl>\n <SerpTitle>\n {title && title.length > 0 ? (\n <>\n {truncate(title, Math.max(1, 60 - (titleSuffix ? titleSuffix.length + 3 : 0)))}\n {titleSuffix && (\n <span\n style={\n titleSuffixInheritColor ? undefined : {color: '#70757a', fontWeight: 400}\n }\n >\n {' '}\n | {titleSuffix}\n </span>\n )}\n </>\n ) : (\n 'Your SEO Title will appear here'\n )}\n </SerpTitle>\n <SerpDescription>\n {description && description.length > 0\n ? truncate(description, 160)\n : 'Your meta description will show up here. Make it compelling!'}\n </SerpDescription>\n </PreviewBody>\n </PreviewContainer>\n </Box>\n )\n}\n\nexport default SeoPreview\n"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  truncate
3
- } from "./chunk-HHO2AKAP.js";
3
+ } from "./chunk-DB5MBPRK.js";
4
4
  import "./chunk-7N4MLTMR.js";
5
5
 
6
6
  // src/components/SeoPreview.tsx
@@ -184,4 +184,4 @@ var SeoPreview_default = SeoPreview;
184
184
  export {
185
185
  SeoPreview_default as default
186
186
  };
187
- //# sourceMappingURL=SeoPreview-UNQBKTQO.js.map
187
+ //# sourceMappingURL=SeoPreview-TATJQFMP.js.map
@@ -0,0 +1,56 @@
1
+ // src/helpers/url.ts
2
+ function joinUrl(base, path) {
3
+ const normalizedBase = base.replace(/\/+$/, "");
4
+ const normalizedPath = path.replace(/^\/+/, "");
5
+ return [normalizedBase, normalizedPath].filter(Boolean).join("/");
6
+ }
7
+
8
+ // src/helpers/hreflang.ts
9
+ function normalizeSlug(slug) {
10
+ var _a;
11
+ if (typeof slug === "string") return slug || void 0;
12
+ if (slug && typeof slug === "object") return (_a = slug.current) != null ? _a : void 0;
13
+ return void 0;
14
+ }
15
+ function resolveUrl(t, baseUrl, resolvePath) {
16
+ if (t.url) return t.url;
17
+ let path;
18
+ if (resolvePath) {
19
+ path = resolvePath(t);
20
+ } else if (t.path) {
21
+ path = t.path;
22
+ } else {
23
+ const slug = normalizeSlug(t.slug);
24
+ path = slug ? `/${slug.replace(/^\/+/, "")}` : void 0;
25
+ }
26
+ if (path === null || path === void 0 || path === "") return void 0;
27
+ return baseUrl ? joinUrl(baseUrl, path) : path;
28
+ }
29
+ function buildHreflangs(translations, options = {}) {
30
+ var _a;
31
+ const { baseUrl, resolvePath, xDefault, current } = options;
32
+ const all = current ? [current, ...translations != null ? translations : []] : [...translations != null ? translations : []];
33
+ const entries = [];
34
+ const seen = /* @__PURE__ */ new Set();
35
+ for (const t of all) {
36
+ const locale = (_a = t == null ? void 0 : t.language) == null ? void 0 : _a.trim();
37
+ if (!locale || seen.has(locale)) continue;
38
+ const url = resolveUrl(t, baseUrl, resolvePath);
39
+ if (!url) continue;
40
+ seen.add(locale);
41
+ entries.push({ locale, url });
42
+ }
43
+ if (xDefault) {
44
+ const match = entries.find((e) => e.locale === xDefault);
45
+ if (match && !seen.has("x-default")) {
46
+ entries.push({ locale: "x-default", url: match.url });
47
+ }
48
+ }
49
+ return entries;
50
+ }
51
+
52
+ export {
53
+ joinUrl,
54
+ buildHreflangs
55
+ };
56
+ //# sourceMappingURL=chunk-CRCXC45D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/url.ts","../src/helpers/hreflang.ts"],"sourcesContent":["/**\n * Join a base URL and a path into a single URL, tolerating leading/trailing slashes on either side.\n *\n * @example\n * joinUrl('https://example.com/', '/about') // 'https://example.com/about'\n * joinUrl('https://example.com', '') // 'https://example.com'\n * joinUrl('', 'about') // 'about'\n */\nexport function joinUrl(base: string, path: string): string {\n const normalizedBase = base.replace(/\\/+$/, '') // remove trailing /\n const normalizedPath = path.replace(/^\\/+/, '') // remove leading /\n return [normalizedBase, normalizedPath].filter(Boolean).join('/')\n}\n","/**\n * Build hreflang alternate-link entries from resolved document translations.\n *\n * Designed for `@sanity/document-internationalization`: a standard GROQ query resolves translations as\n *\n * ```groq\n * \"_translations\": *[_type == \"translation.metadata\" && references(^._id)].translations[].value->{\n * language, \"slug\": slug.current\n * }\n * ```\n *\n * Pass that array here to get entries matching the plugin's `hreflangEntry` shape (`{locale, url}`),\n * which are assignable to `seo.hreflangs` or to `buildSeoMeta({ hreflangs })`.\n */\nimport {joinUrl} from './url'\n\nexport interface HreflangTranslation {\n /** BCP 47 language/region tag, e.g. \"en\", \"fr-FR\". */\n language?: string | null\n /** Sanity slug — either the raw string or the `{ current }` object. */\n slug?: {current?: string | null} | string | null\n /** Pre-built path (e.g. \"/fr/about\"). Wins over `slug`. */\n path?: string | null\n /** Absolute URL. Wins over `baseUrl` + path. */\n url?: string | null\n}\n\nexport interface BuildHreflangsOptions {\n /** Site origin, e.g. \"https://example.com\". Prepended to each translation's path. */\n baseUrl?: string\n /** Build the path for a translation. Defaults to `/${slug}`. Return null/undefined to skip. */\n resolvePath?: (t: HreflangTranslation) => string | null | undefined\n /** Language tag to also emit as an `x-default` entry (pointing at that language's URL). */\n xDefault?: string\n /** Include the current document itself in the set. */\n current?: HreflangTranslation\n}\n\nexport interface HreflangLinkEntry {\n locale: string\n url: string\n}\n\nfunction normalizeSlug(slug: HreflangTranslation['slug']): string | undefined {\n if (typeof slug === 'string') return slug || undefined\n if (slug && typeof slug === 'object') return slug.current ?? undefined\n return undefined\n}\n\nfunction resolveUrl(\n t: HreflangTranslation,\n baseUrl: string | undefined,\n resolvePath: BuildHreflangsOptions['resolvePath'],\n): string | undefined {\n if (t.url) return t.url\n\n let path: string | null | undefined\n if (resolvePath) {\n path = resolvePath(t)\n } else if (t.path) {\n path = t.path\n } else {\n const slug = normalizeSlug(t.slug)\n path = slug ? `/${slug.replace(/^\\/+/, '')}` : undefined\n }\n\n if (path === null || path === undefined || path === '') return undefined\n return baseUrl ? joinUrl(baseUrl, path) : path\n}\n\nexport function buildHreflangs(\n translations: HreflangTranslation[] | null | undefined,\n options: BuildHreflangsOptions = {},\n): HreflangLinkEntry[] {\n const {baseUrl, resolvePath, xDefault, current} = options\n const all = current ? [current, ...(translations ?? [])] : [...(translations ?? [])]\n\n const entries: HreflangLinkEntry[] = []\n const seen = new Set<string>()\n\n for (const t of all) {\n const locale = t?.language?.trim()\n if (!locale || seen.has(locale)) continue\n const url = resolveUrl(t, baseUrl, resolvePath)\n if (!url) continue\n seen.add(locale)\n entries.push({locale, url})\n }\n\n if (xDefault) {\n const match = entries.find((e) => e.locale === xDefault)\n if (match && !seen.has('x-default')) {\n entries.push({locale: 'x-default', url: match.url})\n }\n }\n\n return entries\n}\n"],"mappings":";AAQO,SAAS,QAAQ,MAAc,MAAsB;AAC1D,QAAM,iBAAiB,KAAK,QAAQ,QAAQ,EAAE;AAC9C,QAAM,iBAAiB,KAAK,QAAQ,QAAQ,EAAE;AAC9C,SAAO,CAAC,gBAAgB,cAAc,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAClE;;;AC+BA,SAAS,cAAc,MAAuD;AA3C9E;AA4CE,MAAI,OAAO,SAAS,SAAU,QAAO,QAAQ;AAC7C,MAAI,QAAQ,OAAO,SAAS,SAAU,SAAO,UAAK,YAAL,YAAgB;AAC7D,SAAO;AACT;AAEA,SAAS,WACP,GACA,SACA,aACoB;AACpB,MAAI,EAAE,IAAK,QAAO,EAAE;AAEpB,MAAI;AACJ,MAAI,aAAa;AACf,WAAO,YAAY,CAAC;AAAA,EACtB,WAAW,EAAE,MAAM;AACjB,WAAO,EAAE;AAAA,EACX,OAAO;AACL,UAAM,OAAO,cAAc,EAAE,IAAI;AACjC,WAAO,OAAO,IAAI,KAAK,QAAQ,QAAQ,EAAE,CAAC,KAAK;AAAA,EACjD;AAEA,MAAI,SAAS,QAAQ,SAAS,UAAa,SAAS,GAAI,QAAO;AAC/D,SAAO,UAAU,QAAQ,SAAS,IAAI,IAAI;AAC5C;AAEO,SAAS,eACd,cACA,UAAiC,CAAC,GACb;AAzEvB;AA0EE,QAAM,EAAC,SAAS,aAAa,UAAU,QAAO,IAAI;AAClD,QAAM,MAAM,UAAU,CAAC,SAAS,GAAI,sCAAgB,CAAC,CAAE,IAAI,CAAC,GAAI,sCAAgB,CAAC,CAAE;AAEnF,QAAM,UAA+B,CAAC;AACtC,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,KAAK,KAAK;AACnB,UAAM,UAAS,4BAAG,aAAH,mBAAa;AAC5B,QAAI,CAAC,UAAU,KAAK,IAAI,MAAM,EAAG;AACjC,UAAM,MAAM,WAAW,GAAG,SAAS,WAAW;AAC9C,QAAI,CAAC,IAAK;AACV,SAAK,IAAI,MAAM;AACf,YAAQ,KAAK,EAAC,QAAQ,IAAG,CAAC;AAAA,EAC5B;AAEA,MAAI,UAAU;AACZ,UAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ;AACvD,QAAI,SAAS,CAAC,KAAK,IAAI,WAAW,GAAG;AACnC,cAAQ,KAAK,EAAC,QAAQ,aAAa,KAAK,MAAM,IAAG,CAAC;AAAA,IACpD;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -46,7 +46,7 @@ var getFocusKeywordPlacement = (focusKeyword, title, description) => {
46
46
  };
47
47
  };
48
48
  var hasExcessivePunctuation = (title) => /[!@#$%^&*]{2,}/.test(title);
49
- var getMetaTitleValidationMessages = (title, keywords, isParentseoField, suffixLength = 0) => {
49
+ var getMetaTitleValidationMessages = (title, keywords, isParentseoField, suffixLength = 0, keywordsVisible = true) => {
50
50
  const feedback = [];
51
51
  const minChar = 50;
52
52
  const maxChar = 60;
@@ -72,7 +72,7 @@ var getMetaTitleValidationMessages = (title, keywords, isParentseoField, suffixL
72
72
  text: `Title length (${combinedLength}) looks good for SEO.${suffixMessage}`,
73
73
  color: "green"
74
74
  });
75
- if (isParentseoField) {
75
+ if (isParentseoField && keywordsVisible) {
76
76
  if (keywords.length > 0) {
77
77
  const hasKeyword = hasMatchingKeyword(title, keywords);
78
78
  feedback.push({
@@ -98,7 +98,7 @@ var getMetaTitleValidationMessages = (title, keywords, isParentseoField, suffixL
98
98
  feedback.push({ text: "Title contains excessive punctuation \u2014 simplify it.", color: "orange" });
99
99
  return feedback;
100
100
  };
101
- var getMetaDescriptionValidationMessages = (description, keywords, isParentseoField) => {
101
+ var getMetaDescriptionValidationMessages = (description, keywords, isParentseoField, keywordsVisible = true) => {
102
102
  const feedback = [];
103
103
  const minChar = 120;
104
104
  const maxChar = 160;
@@ -119,7 +119,7 @@ var getMetaDescriptionValidationMessages = (description, keywords, isParentseoFi
119
119
  });
120
120
  else
121
121
  feedback.push({ text: `Description length (${charCount}) looks good for SEO.`, color: "green" });
122
- if (isParentseoField) {
122
+ if (isParentseoField && keywordsVisible) {
123
123
  if (keywords.length > 0) {
124
124
  const hasKeyword = hasMatchingKeyword(description, keywords);
125
125
  feedback.push({
@@ -151,7 +151,7 @@ var getMetaDescriptionValidationMessages = (description, keywords, isParentseoFi
151
151
  });
152
152
  return feedback;
153
153
  };
154
- var getOgTitleValidation = (title, keywords = [], isParentseoField) => {
154
+ var getOgTitleValidation = (title, keywords = [], isParentseoField, keywordsVisible = true) => {
155
155
  const feedback = [];
156
156
  const min = 40;
157
157
  const max = 60;
@@ -168,7 +168,7 @@ var getOgTitleValidation = (title, keywords = [], isParentseoField) => {
168
168
  else if (count > max)
169
169
  feedback.push({ text: `OG Title is ${count} chars \u2014 exceeds recommended ${max}.`, color: "red" });
170
170
  else feedback.push({ text: `OG Title length (${count}) looks good.`, color: "green" });
171
- if (isParentseoField) {
171
+ if (isParentseoField && keywordsVisible) {
172
172
  if (keywords.length > 0) {
173
173
  const hasKeyword = hasMatchingKeyword(title, keywords);
174
174
  feedback.push({
@@ -197,7 +197,7 @@ var getOgTitleValidation = (title, keywords = [], isParentseoField) => {
197
197
  feedback.push({ text: "OG Title contains excessive punctuation \u2014 simplify it.", color: "orange" });
198
198
  return feedback;
199
199
  };
200
- var getOgDescriptionValidation = (desc, keywords = [], isParentseoField) => {
200
+ var getOgDescriptionValidation = (desc, keywords = [], isParentseoField, keywordsVisible = true) => {
201
201
  const feedback = [];
202
202
  const min = 90;
203
203
  const max = 120;
@@ -220,7 +220,7 @@ var getOgDescriptionValidation = (desc, keywords = [], isParentseoField) => {
220
220
  color: "red"
221
221
  });
222
222
  else feedback.push({ text: `OG Description length (${count}) looks good.`, color: "green" });
223
- if (isParentseoField) {
223
+ if (isParentseoField && keywordsVisible) {
224
224
  if (keywords.length > 0) {
225
225
  const hasKeyword = hasMatchingKeyword(desc, keywords);
226
226
  feedback.push({
@@ -252,7 +252,7 @@ var getOgDescriptionValidation = (desc, keywords = [], isParentseoField) => {
252
252
  });
253
253
  return feedback;
254
254
  };
255
- var getTwitterTitleValidation = (title, keywords = [], isParentseoField) => {
255
+ var getTwitterTitleValidation = (title, keywords = [], isParentseoField, keywordsVisible = true) => {
256
256
  const feedback = [];
257
257
  const min = 30;
258
258
  const max = 70;
@@ -272,7 +272,7 @@ var getTwitterTitleValidation = (title, keywords = [], isParentseoField) => {
272
272
  color: "red"
273
273
  });
274
274
  else feedback.push({ text: `X Title length (${count}) looks good.`, color: "green" });
275
- if (isParentseoField) {
275
+ if (isParentseoField && keywordsVisible) {
276
276
  if (keywords.length > 0) {
277
277
  const hasKeyword = hasMatchingKeyword(title, keywords);
278
278
  feedback.push({
@@ -290,7 +290,7 @@ var getTwitterTitleValidation = (title, keywords = [], isParentseoField) => {
290
290
  feedback.push({ text: "X Title has excessive punctuation \u2014 simplify it.", color: "orange" });
291
291
  return feedback;
292
292
  };
293
- var getTwitterDescriptionValidation = (desc, keywords = [], isParentseoField) => {
293
+ var getTwitterDescriptionValidation = (desc, keywords = [], isParentseoField, keywordsVisible = true) => {
294
294
  const feedback = [];
295
295
  const min = 50;
296
296
  const max = 200;
@@ -310,7 +310,7 @@ var getTwitterDescriptionValidation = (desc, keywords = [], isParentseoField) =>
310
310
  color: "red"
311
311
  });
312
312
  else feedback.push({ text: `X Description length (${count}) looks good.`, color: "green" });
313
- if (isParentseoField) {
313
+ if (isParentseoField && keywordsVisible) {
314
314
  if (keywords.length > 0) {
315
315
  const hasKeyword = hasMatchingKeyword(desc, keywords);
316
316
  feedback.push({
@@ -515,4 +515,4 @@ export {
515
515
  getTwitterImageValidation,
516
516
  getTwitterImageUrlValidation
517
517
  };
518
- //# sourceMappingURL=chunk-HHO2AKAP.js.map
518
+ //# sourceMappingURL=chunk-DB5MBPRK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/seoUtils.ts"],"sourcesContent":["import {FeedbackType} from '../types'\n\nexport const stopWords = ['the', 'a', 'an', 'and', 'or', 'but']\n\nexport const hasMatchingKeyword = (title: string, keywordList: string[]): boolean => {\n if (!title || keywordList.length === 0) return false\n const lowerTitle = title.toLowerCase()\n return keywordList.some((keyword) => keyword && lowerTitle.includes(keyword.toLowerCase()))\n}\n\nconst escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\nexport const hasKeywordOveruse = (\n title: string,\n keywordList: string[],\n maxOccurrences = 3,\n): boolean => {\n if (!title || keywordList.length === 0) return false\n const lowerTitle = title.toLowerCase()\n return keywordList.some((keyword) => {\n if (!keyword) return false\n const matches = lowerTitle.match(new RegExp(escapeRegExp(keyword.toLowerCase()), 'g'))\n return matches ? matches.length > maxOccurrences : false\n })\n}\n\nexport const startsWithStopWord = (title: string): boolean => {\n if (!title) return false\n const firstWord = title.trim().split(' ')[0].toLowerCase()\n return stopWords.includes(firstWord)\n}\n\nexport const primaryKeywordAtStart = (title: string, keywords: string[]): boolean => {\n if (!title || keywords.length === 0) return true\n return title.toLowerCase().startsWith(keywords[0].toLowerCase())\n}\n\nexport const truncate = (text: string, maxLength: number): string =>\n text.length > maxLength ? `${text.slice(0, maxLength)}…` : text\n\nexport interface FocusKeywordPlacement {\n hasFocusKeyword: boolean\n inTitle: boolean\n atStartOfTitle: boolean\n inDescription: boolean\n isStuffed: boolean\n}\n\n/** Shared placement facts for a single focus keyword, used by both the live editor UI and health scoring. */\nexport const getFocusKeywordPlacement = (\n focusKeyword: string | undefined,\n title: string | undefined,\n description: string | undefined,\n): FocusKeywordPlacement => {\n const keyword = (focusKeyword || '').trim()\n if (!keyword) {\n return {\n hasFocusKeyword: false,\n inTitle: false,\n atStartOfTitle: false,\n inDescription: false,\n isStuffed: false,\n }\n }\n\n const keywordList = [keyword]\n return {\n hasFocusKeyword: true,\n inTitle: hasMatchingKeyword(title || '', keywordList),\n atStartOfTitle: !!title && primaryKeywordAtStart(title, keywordList),\n inDescription: hasMatchingKeyword(description || '', keywordList),\n isStuffed: hasKeywordOveruse(title || '', keywordList, 2),\n }\n}\n\nexport const hasExcessivePunctuation = (title: string): boolean => /[!@#$%^&*]{2,}/.test(title)\n\nexport const getMetaTitleValidationMessages = (\n title: string,\n keywords: string[],\n isParentseoField: boolean,\n suffixLength = 0,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n const minChar = 50\n const maxChar = 60\n const charCount = title?.length || 0\n const combinedLength = charCount + suffixLength\n const suffixMessage = suffixLength > 0 ? ` Suffix value is included (${suffixLength} chars).` : ''\n\n // Empty check\n if (!title?.trim()) {\n feedback.push({text: 'Meta Title is empty. Add content to improve SEO.', color: 'red'})\n return feedback\n }\n\n // Length check (evaluated against combined title + suffix length)\n if (combinedLength < minChar)\n feedback.push({\n text: `Title is ${combinedLength} characters — below recommended ${minChar}.${suffixMessage}`,\n color: 'orange',\n })\n else if (combinedLength > maxChar)\n feedback.push({\n text: `Title is ${combinedLength} characters — exceeds recommended ${maxChar}.${suffixMessage}`,\n color: 'red',\n })\n else\n feedback.push({\n text: `Title length (${combinedLength}) looks good for SEO.${suffixMessage}`,\n color: 'green',\n })\n\n // Keyword checks\n if (isParentseoField && keywordsVisible) {\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(title, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in title — good job!'\n : 'Keywords defined but missing in title.',\n color: hasKeyword ? 'green' : 'red',\n })\n\n if (hasKeywordOveruse(title, keywords)) {\n feedback.push({\n text: 'Keyword appears too many times — avoid keyword stuffing.',\n color: 'orange',\n })\n }\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Stop word check\n if (startsWithStopWord(title))\n feedback.push({text: 'Title starts with a stop word — consider rephrasing.', color: 'orange'})\n\n // Punctuation check\n if (hasExcessivePunctuation(title))\n feedback.push({text: 'Title contains excessive punctuation — simplify it.', color: 'orange'})\n\n return feedback\n}\n\nexport const getMetaDescriptionValidationMessages = (\n description: string,\n keywords: string[],\n isParentseoField: boolean,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n const minChar = 120\n const maxChar = 160\n const charCount = description?.length || 0\n\n if (!description?.trim()) {\n feedback.push({text: 'Meta description is empty. Add content to improve SEO.', color: 'red'})\n return feedback\n }\n\n // Length check\n if (charCount < minChar)\n feedback.push({\n text: `Description is ${charCount} chars — below recommended ${minChar}.`,\n color: 'orange',\n })\n else if (charCount > maxChar)\n feedback.push({\n text: `Description is ${charCount} chars — exceeds recommended ${maxChar}.`,\n color: 'red',\n })\n else\n feedback.push({text: `Description length (${charCount}) looks good for SEO.`, color: 'green'})\n\n // Keyword checks\n if (isParentseoField && keywordsVisible) {\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(description, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in description — good job!'\n : 'Keywords defined but missing in description.',\n color: hasKeyword ? 'green' : 'red',\n })\n\n if (hasKeywordOveruse(description, keywords)) {\n feedback.push({\n text: 'Keyword appears too many times — avoid keyword stuffing.',\n color: 'orange',\n })\n }\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Stop word / filler check\n if (startsWithStopWord(description))\n feedback.push({\n text: 'Description starts with a stop word — consider rephrasing.',\n color: 'orange',\n })\n\n // Punctuation / special characters\n if (hasExcessivePunctuation(description))\n feedback.push({\n text: 'Description contains excessive punctuation — simplify it.',\n color: 'orange',\n })\n\n return feedback\n}\n\nexport const getOgTitleValidation = (\n title: string,\n keywords: string[] = [],\n isParentseoField: boolean,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n const min = 40\n const max = 60\n const count = title?.length || 0\n\n // Empty check\n if (!title?.trim()) {\n feedback.push({text: 'OG Title is empty. Add content for better social preview.', color: 'red'})\n return feedback\n }\n\n // Length check\n if (count < min)\n feedback.push({\n text: `OG Title is ${count} chars — shorter than recommended ${min}.`,\n color: 'orange',\n })\n else if (count > max)\n feedback.push({text: `OG Title is ${count} chars — exceeds recommended ${max}.`, color: 'red'})\n else feedback.push({text: `OG Title length (${count}) looks good.`, color: 'green'})\n\n if (isParentseoField && keywordsVisible) {\n // Keyword checks\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(title, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in OG title — good job!'\n : 'Keywords defined but missing in OG title.',\n color: hasKeyword ? 'green' : 'red',\n })\n\n if (hasKeywordOveruse(title, keywords)) {\n feedback.push({\n text: 'Keyword appears too many times in OG title — avoid keyword stuffing.',\n color: 'orange',\n })\n }\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Additional OG-specific checks\n if (startsWithStopWord(title))\n feedback.push({\n text: 'OG Title starts with a stop word — consider rephrasing.',\n color: 'orange',\n })\n\n if (hasExcessivePunctuation(title))\n feedback.push({text: 'OG Title contains excessive punctuation — simplify it.', color: 'orange'})\n\n return feedback\n}\n\nexport const getOgDescriptionValidation = (\n desc: string,\n keywords: string[] = [],\n isParentseoField: boolean,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n const min = 90\n const max = 120\n const count = desc?.length || 0\n\n // Empty check\n if (!desc?.trim()) {\n feedback.push({\n text: 'OG Description is empty. Add content for better social preview.',\n color: 'red',\n })\n return feedback\n }\n\n // Length check\n if (count < min)\n feedback.push({\n text: `OG Description is ${count} chars — shorter than recommended ${min}.`,\n color: 'orange',\n })\n else if (count > max)\n feedback.push({\n text: `OG Description is ${count} chars — exceeds recommended ${max}.`,\n color: 'red',\n })\n else feedback.push({text: `OG Description length (${count}) looks good.`, color: 'green'})\n\n // Keyword checks\n if (isParentseoField && keywordsVisible) {\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(desc, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in OG description — good job!'\n : 'Keywords defined but missing in OG description.',\n color: hasKeyword ? 'green' : 'red',\n })\n\n if (hasKeywordOveruse(desc, keywords)) {\n feedback.push({\n text: 'Keyword appears too many times in OG description — avoid keyword stuffing.',\n color: 'orange',\n })\n }\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Additional OG-specific checks\n if (startsWithStopWord(desc))\n feedback.push({\n text: 'OG Description starts with a stop word — consider rephrasing.',\n color: 'orange',\n })\n\n if (hasExcessivePunctuation(desc))\n feedback.push({\n text: 'OG Description contains excessive punctuation — simplify it.',\n color: 'orange',\n })\n\n return feedback\n}\n\nexport const getTwitterTitleValidation = (\n title: string,\n keywords: string[] = [],\n isParentseoField: boolean,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n const min = 30\n const max = 70\n const count = title?.length || 0\n\n if (!title?.trim()) {\n feedback.push({text: 'X Title is empty. Add content for better SEO.', color: 'red'})\n return feedback\n }\n\n // Length check\n if (count < min)\n feedback.push({\n text: `X Title is ${count} chars — shorter than recommended ${min}.`,\n color: 'orange',\n })\n else if (count > max)\n feedback.push({\n text: `X Title is ${count} chars — exceeds recommended ${max}.`,\n color: 'red',\n })\n else feedback.push({text: `X Title length (${count}) looks good.`, color: 'green'})\n\n if (isParentseoField && keywordsVisible) {\n // Keyword checks\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(title, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in X title — good job!'\n : 'Keywords defined but missing in X title.',\n color: hasKeyword ? 'green' : 'red',\n })\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Punctuation check\n if (/[!@#$%^&*]{2,}/.test(title))\n feedback.push({text: 'X Title has excessive punctuation — simplify it.', color: 'orange'})\n\n return feedback\n}\n\nexport const getTwitterDescriptionValidation = (\n desc: string,\n keywords: string[] = [],\n isParentseoField: boolean,\n keywordsVisible = true,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n const min = 50\n const max = 200\n const count = desc?.length || 0\n\n if (!desc?.trim()) {\n feedback.push({text: 'X Description is empty. Add content for better SEO.', color: 'red'})\n return feedback\n }\n\n // Length check\n if (count < min)\n feedback.push({\n text: `X Description is ${count} chars — shorter than recommended ${min}.`,\n color: 'orange',\n })\n else if (count > max)\n feedback.push({\n text: `X Description is ${count} chars — exceeds recommended ${max}.`,\n color: 'red',\n })\n else feedback.push({text: `X Description length (${count}) looks good.`, color: 'green'})\n\n if (isParentseoField && keywordsVisible) {\n // Keyword checks\n if (keywords.length > 0) {\n const hasKeyword = hasMatchingKeyword(desc, keywords)\n feedback.push({\n text: hasKeyword\n ? 'Keyword found in X description — good job!'\n : 'Keywords defined but missing in X description.',\n color: hasKeyword ? 'green' : 'red',\n })\n } else {\n feedback.push({\n text: 'No keywords defined. Consider adding relevant keywords.',\n color: 'orange',\n })\n }\n }\n\n // Punctuation check\n if (/[!@#$%^&*]{2,}/.test(desc))\n feedback.push({\n text: 'X Description has excessive punctuation — simplify it.',\n color: 'orange',\n })\n\n return feedback\n}\n\n// ── Image Validation Helpers ──\n\n/** Check if an image is set in an OG/Twitter sub-object (handles upload vs URL) */\nexport const isSubImageSet = (subObj: Record<string, unknown> | null | undefined): boolean => {\n if (!subObj) return false\n if (subObj.imageType === 'url') return !!(subObj.imageUrl as string)?.trim()\n const img = subObj.image as Record<string, unknown> | undefined\n return !!img?.asset\n}\n\n/** Check if metaImage asset is set */\nconst isMetaImageSet = (seoParent: Record<string, unknown> | null | undefined): boolean => {\n if (!seoParent) return false\n const metaImage = seoParent.metaImage as Record<string, unknown> | undefined\n return !!metaImage?.asset\n}\n\nexport const getMetaImageValidation = (\n hasImage: boolean,\n seoParent: Record<string, unknown> | null | undefined,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n if (!hasImage) {\n feedback.push({\n text: 'No meta image provided. Adding an image improves click-through rates.',\n color: 'red',\n })\n return feedback\n }\n\n feedback.push({text: 'Meta image is set — great for SEO and social sharing.', color: 'green'})\n\n const ogSet = isSubImageSet(seoParent?.openGraph as Record<string, unknown> | undefined)\n const twSet = isSubImageSet(seoParent?.twitter as Record<string, unknown> | undefined)\n\n if (!ogSet && !twSet) {\n feedback.push({\n text: 'OG and Twitter images are missing — add them for full social coverage.',\n color: 'orange',\n })\n } else if (!ogSet) {\n feedback.push({\n text: 'OG image is missing — add it for better Facebook/LinkedIn previews.',\n color: 'orange',\n })\n } else if (twSet) {\n feedback.push({text: 'All images set (Meta, OG, Twitter) — full coverage!', color: 'green'})\n } else {\n feedback.push({\n text: 'Twitter image is missing — add it for better X (Twitter) cards.',\n color: 'orange',\n })\n }\n\n return feedback\n}\n\nexport const getOgImageValidation = (\n hasImage: boolean,\n altText: string | undefined,\n seoParent: Record<string, unknown> | null | undefined,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n if (!hasImage) {\n feedback.push({\n text: 'No OG image provided. Social shares will lack a visual preview.',\n color: 'red',\n })\n return feedback\n }\n\n feedback.push({text: 'OG image is set — good for social sharing.', color: 'green'})\n\n if (altText?.trim()) {\n feedback.push({text: 'Alt text is set — good for accessibility.', color: 'green'})\n } else {\n feedback.push({text: 'Consider adding alt text for better accessibility.', color: 'orange'})\n }\n\n const metaSet = isMetaImageSet(seoParent)\n const twSet = isSubImageSet(seoParent?.twitter as Record<string, unknown> | undefined)\n\n if (metaSet && twSet) {\n feedback.push({text: 'All images set (Meta, OG, Twitter) — full coverage!', color: 'green'})\n } else {\n if (!metaSet)\n feedback.push({\n text: 'Meta image is missing — add it for search engine results.',\n color: 'orange',\n })\n if (!twSet)\n feedback.push({\n text: 'Twitter image is missing — add it for X (Twitter) cards.',\n color: 'orange',\n })\n }\n\n return feedback\n}\n\nexport const getOgImageUrlValidation = (\n imageUrl: string | undefined,\n seoParent: Record<string, unknown> | null | undefined,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n if (!imageUrl?.trim()) {\n feedback.push({\n text: 'No OG image URL provided. Social shares will lack a visual preview.',\n color: 'red',\n })\n return feedback\n }\n\n feedback.push({text: 'OG image URL is set — good for social sharing.', color: 'green'})\n\n const metaSet = isMetaImageSet(seoParent)\n const twSet = isSubImageSet(seoParent?.twitter as Record<string, unknown> | undefined)\n\n if (metaSet && twSet) {\n feedback.push({text: 'All images set (Meta, OG, Twitter) — full coverage!', color: 'green'})\n } else {\n if (!metaSet)\n feedback.push({\n text: 'Meta image is missing — add it for search engine results.',\n color: 'orange',\n })\n if (!twSet)\n feedback.push({\n text: 'Twitter image is missing — add it for X (Twitter) cards.',\n color: 'orange',\n })\n }\n\n return feedback\n}\n\nexport const getTwitterImageValidation = (\n hasImage: boolean,\n altText: string | undefined,\n seoParent: Record<string, unknown> | null | undefined,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n if (!hasImage) {\n feedback.push({\n text: 'No Twitter image provided. Posts on X will lack a visual.',\n color: 'red',\n })\n return feedback\n }\n\n feedback.push({text: 'Twitter image is set — good for X sharing.', color: 'green'})\n\n if (altText?.trim()) {\n feedback.push({text: 'Alt text is set — good for accessibility.', color: 'green'})\n } else {\n feedback.push({text: 'Consider adding alt text for better accessibility.', color: 'orange'})\n }\n\n const metaSet = isMetaImageSet(seoParent)\n const ogSet = isSubImageSet(seoParent?.openGraph as Record<string, unknown> | undefined)\n\n if (metaSet && ogSet) {\n feedback.push({text: 'All images set (Meta, OG, Twitter) — full coverage!', color: 'green'})\n } else {\n if (!metaSet)\n feedback.push({\n text: 'Meta image is missing — add it for search engine results.',\n color: 'orange',\n })\n if (!ogSet)\n feedback.push({\n text: 'OG image is missing — add it for Facebook/LinkedIn sharing.',\n color: 'orange',\n })\n }\n\n return feedback\n}\n\nexport const getTwitterImageUrlValidation = (\n imageUrl: string | undefined,\n seoParent: Record<string, unknown> | null | undefined,\n): FeedbackType[] => {\n const feedback: FeedbackType[] = []\n\n if (!imageUrl?.trim()) {\n feedback.push({\n text: 'No Twitter image URL provided. Posts on X will lack a visual.',\n color: 'red',\n })\n return feedback\n }\n\n feedback.push({text: 'Twitter image URL is set — good for X sharing.', color: 'green'})\n\n const metaSet = isMetaImageSet(seoParent)\n const ogSet = isSubImageSet(seoParent?.openGraph as Record<string, unknown> | undefined)\n\n if (metaSet && ogSet) {\n feedback.push({text: 'All images set (Meta, OG, Twitter) — full coverage!', color: 'green'})\n } else {\n if (!metaSet)\n feedback.push({\n text: 'Meta image is missing — add it for search engine results.',\n color: 'orange',\n })\n if (!ogSet)\n feedback.push({\n text: 'OG image is missing — add it for Facebook/LinkedIn sharing.',\n color: 'orange',\n })\n }\n\n return feedback\n}\n"],"mappings":"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","names":[]}
@@ -287,28 +287,64 @@ var INDUSTRY_PROMPTS = Object.fromEntries(
287
287
  ])
288
288
  );
289
289
  var DEFAULT_PROMPTS = buildFieldPrompts(GENERIC_CTX, PUBLIC_ANGLE_COUNT);
290
- async function resolvePromptPool(field, industry, licenseKey, projectId) {
290
+ function normalizeCustomPrompts(customPrompt, customPrompts) {
291
+ if (customPrompts) return customPrompts;
292
+ if (customPrompt) return { generic: [customPrompt] };
293
+ return void 0;
294
+ }
295
+ function toPromptFn(fn, field, industry) {
296
+ return (p) => fn({
297
+ field,
298
+ content: p.content,
299
+ focusKeyword: p.keyword,
300
+ keywords: p.keywords,
301
+ meta: p.meta,
302
+ industry
303
+ });
304
+ }
305
+ function wrapCustom(custom, field, industry) {
306
+ var _a, _b, _c;
307
+ const genericFns = (_a = custom.generic) != null ? _a : [];
308
+ const industryFns = industry ? (_c = (_b = custom.byIndustry) == null ? void 0 : _b[industry]) != null ? _c : [] : [];
309
+ return {
310
+ genericPrompts: genericFns.map((fn) => toPromptFn(fn, field, industry)),
311
+ industryPrompts: industryFns.map((fn) => toPromptFn(fn, field, industry)),
312
+ merge: custom.merge === true
313
+ };
314
+ }
315
+ async function resolvePromptPool(field, industry, licenseKey, projectId, custom) {
316
+ var _a;
291
317
  const isFreeIndustry = Boolean(
292
318
  industry && FREE_INDUSTRIES.includes(industry)
293
319
  );
294
320
  const publicPrompts = isFreeIndustry ? INDUSTRY_PROMPTS[industry][field] : DEFAULT_PROMPTS[field];
295
- if (!licenseKey) return publicPrompts;
296
- try {
297
- const mod = await Promise.resolve().then(() => _interopRequireWildcard(require("seofields-pro")));
298
- if (typeof mod.resolveLicensedPromptPool !== "function") return publicPrompts;
299
- return mod.resolveLicensedPromptPool({
300
- licenseKey,
301
- projectId,
302
- field,
303
- industry,
304
- publicPrompts
305
- });
306
- } catch (e) {
307
- return publicPrompts;
321
+ const wrapped = custom ? wrapCustom(custom, field, industry) : void 0;
322
+ if (licenseKey) {
323
+ try {
324
+ const mod = await Promise.resolve().then(() => _interopRequireWildcard(require("seofields-pro")));
325
+ if (typeof mod.resolveLicensedPromptPool === "function") {
326
+ return mod.resolveLicensedPromptPool({
327
+ licenseKey,
328
+ projectId,
329
+ field,
330
+ industry,
331
+ publicPrompts,
332
+ custom: wrapped
333
+ });
334
+ }
335
+ } catch (e) {
336
+ }
337
+ }
338
+ if (wrapped) {
339
+ const single = (_a = wrapped.industryPrompts[0]) != null ? _a : wrapped.genericPrompts[0];
340
+ if (single) {
341
+ return wrapped.merge ? [...publicPrompts, single] : [single];
342
+ }
308
343
  }
344
+ return publicPrompts;
309
345
  }
310
- async function pickPrompt(field, params, industry, licenseKey, projectId) {
311
- const pool = await resolvePromptPool(field, industry, licenseKey, projectId);
346
+ async function pickPrompt(field, params, { industry, licenseKey, projectId, custom } = {}) {
347
+ const pool = await resolvePromptPool(field, industry, licenseKey, projectId, custom);
312
348
  const fn = pool[Math.floor(Math.random() * pool.length)];
313
349
  return fn(params);
314
350
  }
@@ -644,6 +680,7 @@ async function generateSeoText(field, content, focusKeyword, keywords, { config,
644
680
  const callApi = buildCallApi(config, (_a = config.temperature) != null ? _a : 0.7, abortSignal);
645
681
  const maxRetries = (_b = config.maxRetries) != null ? _b : DEFAULT_MAX_RETRIES;
646
682
  const isLicensed = Boolean(config._licenseKey);
683
+ const custom = normalizeCustomPrompts(config.customPrompt, config.customPrompts);
647
684
  const constraint = FIELD_CONSTRAINTS[field];
648
685
  let firstRaw = "";
649
686
  let lastRaw = "";
@@ -652,9 +689,12 @@ async function generateSeoText(field, content, focusKeyword, keywords, { config,
652
689
  const prompt = await pickPrompt(
653
690
  field,
654
691
  { content, keyword: focusKeyword, keywords, meta },
655
- config.industry,
656
- isLicensed ? config._licenseKey : void 0,
657
- config._projectId
692
+ {
693
+ industry: config.industry,
694
+ licenseKey: isLicensed ? config._licenseKey : void 0,
695
+ projectId: config._projectId,
696
+ custom
697
+ }
658
698
  );
659
699
  const raw = stripQuotes(await callApi(prompt));
660
700
  if (attempt === 0) firstRaw = raw;
@@ -670,4 +710,4 @@ async function generateSeoText(field, content, focusKeyword, keywords, { config,
670
710
 
671
711
 
672
712
  exports.analyzeReadability = analyzeReadability; exports.pickTestOutput = pickTestOutput; exports.generateSeoText = generateSeoText;
673
- //# sourceMappingURL=chunk-3OK3S2F7.cjs.map
713
+ //# sourceMappingURL=chunk-EUG4MNRC.cjs.map