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.
- package/README.md +71 -0
- package/dist/{SeoPreview-LMZAWWOE.cjs → SeoPreview-A2XZVMT2.cjs} +4 -4
- package/dist/{SeoPreview-LMZAWWOE.cjs.map → SeoPreview-A2XZVMT2.cjs.map} +1 -1
- package/dist/{SeoPreview-UNQBKTQO.js → SeoPreview-TATJQFMP.js} +2 -2
- package/dist/chunk-CRCXC45D.js +56 -0
- package/dist/chunk-CRCXC45D.js.map +1 -0
- package/dist/{chunk-HHO2AKAP.js → chunk-DB5MBPRK.js} +13 -13
- package/dist/chunk-DB5MBPRK.js.map +1 -0
- package/dist/{chunk-3OK3S2F7.cjs → chunk-EUG4MNRC.cjs} +60 -20
- package/dist/chunk-EUG4MNRC.cjs.map +1 -0
- package/dist/{chunk-Y46ACXM4.cjs → chunk-LHUFL5LS.cjs} +13 -13
- package/dist/chunk-LHUFL5LS.cjs.map +1 -0
- package/dist/{chunk-Z5AOUU4H.js → chunk-NLEB47UV.js} +60 -20
- package/dist/chunk-NLEB47UV.js.map +1 -0
- package/dist/{chunk-FUUWBQE2.cjs → chunk-OULUDJPI.cjs} +66 -7
- package/dist/chunk-OULUDJPI.cjs.map +1 -0
- package/dist/{chunk-KIEO6QG3.js → chunk-XD4HKLEA.js} +66 -7
- package/dist/chunk-XD4HKLEA.js.map +1 -0
- package/dist/chunk-XYAZJ3WX.cjs +56 -0
- package/dist/chunk-XYAZJ3WX.cjs.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/head.cjs +10 -2
- package/dist/head.cjs.map +1 -1
- package/dist/head.d.cts +61 -1
- package/dist/head.d.ts +61 -1
- package/dist/head.js +9 -1
- package/dist/hreflang-_VbSuWYD.d.cts +29 -0
- package/dist/hreflang-_VbSuWYD.d.ts +29 -0
- package/dist/index.cjs +173 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +336 -201
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +11 -3
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +9 -1
- package/dist/next.js.map +1 -1
- package/dist/{plugin-DoGeZP79.d.cts → plugin-HFuWZsJu.d.cts} +68 -1
- package/dist/{plugin-DQdThyxG.d.ts → plugin-oQUCim56.d.ts} +68 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +6 -8
- package/dist/server.d.ts +6 -8
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-3OK3S2F7.cjs.map +0 -1
- package/dist/chunk-FUUWBQE2.cjs.map +0 -1
- package/dist/chunk-HHO2AKAP.js.map +0 -1
- package/dist/chunk-KIEO6QG3.js.map +0 -1
- package/dist/chunk-Y46ACXM4.cjs.map +0 -1
- package/dist/chunk-Z5AOUU4H.js.map +0 -1
- /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
|
|
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
|
-
|
|
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 ?
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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":";AAEO,IAAM,YAAY,CAAC,OAAO,KAAK,MAAM,OAAO,MAAM,KAAK;AAEvD,IAAM,qBAAqB,CAAC,OAAe,gBAAmC;AACnF,MAAI,CAAC,SAAS,YAAY,WAAW,EAAG,QAAO;AAC/C,QAAM,aAAa,MAAM,YAAY;AACrC,SAAO,YAAY,KAAK,CAAC,YAAY,WAAW,WAAW,SAAS,QAAQ,YAAY,CAAC,CAAC;AAC5F;AAEA,IAAM,eAAe,CAAC,UAA0B,MAAM,QAAQ,uBAAuB,MAAM;AAEpF,IAAM,oBAAoB,CAC/B,OACA,aACA,iBAAiB,MACL;AACZ,MAAI,CAAC,SAAS,YAAY,WAAW,EAAG,QAAO;AAC/C,QAAM,aAAa,MAAM,YAAY;AACrC,SAAO,YAAY,KAAK,CAAC,YAAY;AACnC,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,UAAU,WAAW,MAAM,IAAI,OAAO,aAAa,QAAQ,YAAY,CAAC,GAAG,GAAG,CAAC;AACrF,WAAO,UAAU,QAAQ,SAAS,iBAAiB;AAAA,EACrD,CAAC;AACH;AAEO,IAAM,qBAAqB,CAAC,UAA2B;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,MAAM,KAAK,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,YAAY;AACzD,SAAO,UAAU,SAAS,SAAS;AACrC;AAEO,IAAM,wBAAwB,CAAC,OAAe,aAAgC;AACnF,MAAI,CAAC,SAAS,SAAS,WAAW,EAAG,QAAO;AAC5C,SAAO,MAAM,YAAY,EAAE,WAAW,SAAS,CAAC,EAAE,YAAY,CAAC;AACjE;AAEO,IAAM,WAAW,CAAC,MAAc,cACrC,KAAK,SAAS,YAAY,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC,WAAM;AAWtD,IAAM,2BAA2B,CACtC,cACA,OACA,gBAC0B;AAC1B,QAAM,WAAW,gBAAgB,IAAI,KAAK;AAC1C,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,iBAAiB;AAAA,MACjB,SAAS;AAAA,MACT,gBAAgB;AAAA,MAChB,eAAe;AAAA,MACf,WAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,cAAc,CAAC,OAAO;AAC5B,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,SAAS,mBAAmB,SAAS,IAAI,WAAW;AAAA,IACpD,gBAAgB,CAAC,CAAC,SAAS,sBAAsB,OAAO,WAAW;AAAA,IACnE,eAAe,mBAAmB,eAAe,IAAI,WAAW;AAAA,IAChE,WAAW,kBAAkB,SAAS,IAAI,aAAa,CAAC;AAAA,EAC1D;AACF;AAEO,IAAM,0BAA0B,CAAC,UAA2B,iBAAiB,KAAK,KAAK;AAEvF,IAAM,iCAAiC,CAC5C,OACA,UACA,kBACA,eAAe,GACf,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAElC,QAAM,UAAU;AAChB,QAAM,UAAU;AAChB,QAAM,aAAY,+BAAO,WAAU;AACnC,QAAM,iBAAiB,YAAY;AACnC,QAAM,gBAAgB,eAAe,IAAI,8BAA8B,YAAY,aAAa;AAGhG,MAAI,EAAC,+BAAO,SAAQ;AAClB,aAAS,KAAK,EAAC,MAAM,oDAAoD,OAAO,MAAK,CAAC;AACtF,WAAO;AAAA,EACT;AAGA,MAAI,iBAAiB;AACnB,aAAS,KAAK;AAAA,MACZ,MAAM,YAAY,cAAc,wCAAmC,OAAO,IAAI,aAAa;AAAA,MAC3F,OAAO;AAAA,IACT,CAAC;AAAA,WACM,iBAAiB;AACxB,aAAS,KAAK;AAAA,MACZ,MAAM,YAAY,cAAc,0CAAqC,OAAO,IAAI,aAAa;AAAA,MAC7F,OAAO;AAAA,IACT,CAAC;AAAA;AAED,aAAS,KAAK;AAAA,MACZ,MAAM,iBAAiB,cAAc,wBAAwB,aAAa;AAAA,MAC1E,OAAO;AAAA,IACT,CAAC;AAGH,MAAI,oBAAoB,iBAAiB;AACvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,OAAO,QAAQ;AACrD,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,4CACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAED,UAAI,kBAAkB,OAAO,QAAQ,GAAG;AACtC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,mBAAmB,KAAK;AAC1B,aAAS,KAAK,EAAC,MAAM,6DAAwD,OAAO,SAAQ,CAAC;AAG/F,MAAI,wBAAwB,KAAK;AAC/B,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,SAAQ,CAAC;AAE9F,SAAO;AACT;AAEO,IAAM,uCAAuC,CAClD,aACA,UACA,kBACA,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAElC,QAAM,UAAU;AAChB,QAAM,UAAU;AAChB,QAAM,aAAY,2CAAa,WAAU;AAEzC,MAAI,EAAC,2CAAa,SAAQ;AACxB,aAAS,KAAK,EAAC,MAAM,0DAA0D,OAAO,MAAK,CAAC;AAC5F,WAAO;AAAA,EACT;AAGA,MAAI,YAAY;AACd,aAAS,KAAK;AAAA,MACZ,MAAM,kBAAkB,SAAS,mCAA8B,OAAO;AAAA,MACtE,OAAO;AAAA,IACT,CAAC;AAAA,WACM,YAAY;AACnB,aAAS,KAAK;AAAA,MACZ,MAAM,kBAAkB,SAAS,qCAAgC,OAAO;AAAA,MACxE,OAAO;AAAA,IACT,CAAC;AAAA;AAED,aAAS,KAAK,EAAC,MAAM,uBAAuB,SAAS,yBAAyB,OAAO,QAAO,CAAC;AAG/F,MAAI,oBAAoB,iBAAiB;AACvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,aAAa,QAAQ;AAC3D,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,kDACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAED,UAAI,kBAAkB,aAAa,QAAQ,GAAG;AAC5C,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,mBAAmB,WAAW;AAChC,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAGH,MAAI,wBAAwB,WAAW;AACrC,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAEH,SAAO;AACT;AAEO,IAAM,uBAAuB,CAClC,OACA,WAAqB,CAAC,GACtB,kBACA,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAClC,QAAM,MAAM;AACZ,QAAM,MAAM;AACZ,QAAM,SAAQ,+BAAO,WAAU;AAG/B,MAAI,EAAC,+BAAO,SAAQ;AAClB,aAAS,KAAK,EAAC,MAAM,6DAA6D,OAAO,MAAK,CAAC;AAC/F,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ;AACV,aAAS,KAAK;AAAA,MACZ,MAAM,eAAe,KAAK,0CAAqC,GAAG;AAAA,MAClE,OAAO;AAAA,IACT,CAAC;AAAA,WACM,QAAQ;AACf,aAAS,KAAK,EAAC,MAAM,eAAe,KAAK,qCAAgC,GAAG,KAAK,OAAO,MAAK,CAAC;AAAA,MAC3F,UAAS,KAAK,EAAC,MAAM,oBAAoB,KAAK,iBAAiB,OAAO,QAAO,CAAC;AAEnF,MAAI,oBAAoB,iBAAiB;AAEvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,OAAO,QAAQ;AACrD,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,+CACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAED,UAAI,kBAAkB,OAAO,QAAQ,GAAG;AACtC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,mBAAmB,KAAK;AAC1B,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAEH,MAAI,wBAAwB,KAAK;AAC/B,aAAS,KAAK,EAAC,MAAM,+DAA0D,OAAO,SAAQ,CAAC;AAEjG,SAAO;AACT;AAEO,IAAM,6BAA6B,CACxC,MACA,WAAqB,CAAC,GACtB,kBACA,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAClC,QAAM,MAAM;AACZ,QAAM,MAAM;AACZ,QAAM,SAAQ,6BAAM,WAAU;AAG9B,MAAI,EAAC,6BAAM,SAAQ;AACjB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ;AACV,aAAS,KAAK;AAAA,MACZ,MAAM,qBAAqB,KAAK,0CAAqC,GAAG;AAAA,MACxE,OAAO;AAAA,IACT,CAAC;AAAA,WACM,QAAQ;AACf,aAAS,KAAK;AAAA,MACZ,MAAM,qBAAqB,KAAK,qCAAgC,GAAG;AAAA,MACnE,OAAO;AAAA,IACT,CAAC;AAAA,MACE,UAAS,KAAK,EAAC,MAAM,0BAA0B,KAAK,iBAAiB,OAAO,QAAO,CAAC;AAGzF,MAAI,oBAAoB,iBAAiB;AACvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,MAAM,QAAQ;AACpD,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,qDACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAED,UAAI,kBAAkB,MAAM,QAAQ,GAAG;AACrC,iBAAS,KAAK;AAAA,UACZ,MAAM;AAAA,UACN,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAAA,IACF,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,mBAAmB,IAAI;AACzB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAEH,MAAI,wBAAwB,IAAI;AAC9B,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAEH,SAAO;AACT;AAEO,IAAM,4BAA4B,CACvC,OACA,WAAqB,CAAC,GACtB,kBACA,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAClC,QAAM,MAAM;AACZ,QAAM,MAAM;AACZ,QAAM,SAAQ,+BAAO,WAAU;AAE/B,MAAI,EAAC,+BAAO,SAAQ;AAClB,aAAS,KAAK,EAAC,MAAM,iDAAiD,OAAO,MAAK,CAAC;AACnF,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ;AACV,aAAS,KAAK;AAAA,MACZ,MAAM,cAAc,KAAK,0CAAqC,GAAG;AAAA,MACjE,OAAO;AAAA,IACT,CAAC;AAAA,WACM,QAAQ;AACf,aAAS,KAAK;AAAA,MACZ,MAAM,cAAc,KAAK,qCAAgC,GAAG;AAAA,MAC5D,OAAO;AAAA,IACT,CAAC;AAAA,MACE,UAAS,KAAK,EAAC,MAAM,mBAAmB,KAAK,iBAAiB,OAAO,QAAO,CAAC;AAElF,MAAI,oBAAoB,iBAAiB;AAEvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,OAAO,QAAQ;AACrD,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,8CACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAAA,IACH,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,iBAAiB,KAAK,KAAK;AAC7B,aAAS,KAAK,EAAC,MAAM,yDAAoD,OAAO,SAAQ,CAAC;AAE3F,SAAO;AACT;AAEO,IAAM,kCAAkC,CAC7C,MACA,WAAqB,CAAC,GACtB,kBACA,kBAAkB,SACC;AACnB,QAAM,WAA2B,CAAC;AAClC,QAAM,MAAM;AACZ,QAAM,MAAM;AACZ,QAAM,SAAQ,6BAAM,WAAU;AAE9B,MAAI,EAAC,6BAAM,SAAQ;AACjB,aAAS,KAAK,EAAC,MAAM,uDAAuD,OAAO,MAAK,CAAC;AACzF,WAAO;AAAA,EACT;AAGA,MAAI,QAAQ;AACV,aAAS,KAAK;AAAA,MACZ,MAAM,oBAAoB,KAAK,0CAAqC,GAAG;AAAA,MACvE,OAAO;AAAA,IACT,CAAC;AAAA,WACM,QAAQ;AACf,aAAS,KAAK;AAAA,MACZ,MAAM,oBAAoB,KAAK,qCAAgC,GAAG;AAAA,MAClE,OAAO;AAAA,IACT,CAAC;AAAA,MACE,UAAS,KAAK,EAAC,MAAM,yBAAyB,KAAK,iBAAiB,OAAO,QAAO,CAAC;AAExF,MAAI,oBAAoB,iBAAiB;AAEvC,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,aAAa,mBAAmB,MAAM,QAAQ;AACpD,eAAS,KAAK;AAAA,QACZ,MAAM,aACF,oDACA;AAAA,QACJ,OAAO,aAAa,UAAU;AAAA,MAChC,CAAC;AAAA,IACH,OAAO;AACL,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,iBAAiB,KAAK,IAAI;AAC5B,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAEH,SAAO;AACT;AAKO,IAAM,gBAAgB,CAAC,WAAgE;AA7d9F;AA8dE,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,cAAc,MAAO,QAAO,CAAC,GAAE,YAAO,aAAP,mBAA4B;AACtE,QAAM,MAAM,OAAO;AACnB,SAAO,CAAC,EAAC,2BAAK;AAChB;AAGA,IAAM,iBAAiB,CAAC,cAAmE;AACzF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,YAAY,UAAU;AAC5B,SAAO,CAAC,EAAC,uCAAW;AACtB;AAEO,IAAM,yBAAyB,CACpC,UACA,cACmB;AACnB,QAAM,WAA2B,CAAC;AAElC,MAAI,CAAC,UAAU;AACb,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAEA,WAAS,KAAK,EAAC,MAAM,8DAAyD,OAAO,QAAO,CAAC;AAE7F,QAAM,QAAQ,cAAc,uCAAW,SAAgD;AACvF,QAAM,QAAQ,cAAc,uCAAW,OAA8C;AAErF,MAAI,CAAC,SAAS,CAAC,OAAO;AACpB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAAA,EACH,WAAW,CAAC,OAAO;AACjB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAAA,EACH,WAAW,OAAO;AAChB,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,QAAO,CAAC;AAAA,EAC7F,OAAO;AACL,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEO,IAAM,uBAAuB,CAClC,UACA,SACA,cACmB;AACnB,QAAM,WAA2B,CAAC;AAElC,MAAI,CAAC,UAAU;AACb,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAEA,WAAS,KAAK,EAAC,MAAM,mDAA8C,OAAO,QAAO,CAAC;AAElF,MAAI,mCAAS,QAAQ;AACnB,aAAS,KAAK,EAAC,MAAM,kDAA6C,OAAO,QAAO,CAAC;AAAA,EACnF,OAAO;AACL,aAAS,KAAK,EAAC,MAAM,sDAAsD,OAAO,SAAQ,CAAC;AAAA,EAC7F;AAEA,QAAM,UAAU,eAAe,SAAS;AACxC,QAAM,QAAQ,cAAc,uCAAW,OAA8C;AAErF,MAAI,WAAW,OAAO;AACpB,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,QAAO,CAAC;AAAA,EAC7F,OAAO;AACL,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AACH,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,EACL;AAEA,SAAO;AACT;AAEO,IAAM,0BAA0B,CACrC,UACA,cACmB;AACnB,QAAM,WAA2B,CAAC;AAElC,MAAI,EAAC,qCAAU,SAAQ;AACrB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAEA,WAAS,KAAK,EAAC,MAAM,uDAAkD,OAAO,QAAO,CAAC;AAEtF,QAAM,UAAU,eAAe,SAAS;AACxC,QAAM,QAAQ,cAAc,uCAAW,OAA8C;AAErF,MAAI,WAAW,OAAO;AACpB,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,QAAO,CAAC;AAAA,EAC7F,OAAO;AACL,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AACH,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,EACL;AAEA,SAAO;AACT;AAEO,IAAM,4BAA4B,CACvC,UACA,SACA,cACmB;AACnB,QAAM,WAA2B,CAAC;AAElC,MAAI,CAAC,UAAU;AACb,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAEA,WAAS,KAAK,EAAC,MAAM,mDAA8C,OAAO,QAAO,CAAC;AAElF,MAAI,mCAAS,QAAQ;AACnB,aAAS,KAAK,EAAC,MAAM,kDAA6C,OAAO,QAAO,CAAC;AAAA,EACnF,OAAO;AACL,aAAS,KAAK,EAAC,MAAM,sDAAsD,OAAO,SAAQ,CAAC;AAAA,EAC7F;AAEA,QAAM,UAAU,eAAe,SAAS;AACxC,QAAM,QAAQ,cAAc,uCAAW,SAAgD;AAEvF,MAAI,WAAW,OAAO;AACpB,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,QAAO,CAAC;AAAA,EAC7F,OAAO;AACL,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AACH,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,EACL;AAEA,SAAO;AACT;AAEO,IAAM,+BAA+B,CAC1C,UACA,cACmB;AACnB,QAAM,WAA2B,CAAC;AAElC,MAAI,EAAC,qCAAU,SAAQ;AACrB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACT;AAEA,WAAS,KAAK,EAAC,MAAM,uDAAkD,OAAO,QAAO,CAAC;AAEtF,QAAM,UAAU,eAAe,SAAS;AACxC,QAAM,QAAQ,cAAc,uCAAW,SAAgD;AAEvF,MAAI,WAAW,OAAO;AACpB,aAAS,KAAK,EAAC,MAAM,4DAAuD,OAAO,QAAO,CAAC;AAAA,EAC7F,OAAO;AACL,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AACH,QAAI,CAAC;AACH,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,OAAO;AAAA,MACT,CAAC;AAAA,EACL;AAEA,SAAO;AACT;","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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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-
|
|
713
|
+
//# sourceMappingURL=chunk-EUG4MNRC.cjs.map
|