@focus-reactive/payload-plugin-seo 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +198 -6
  2. package/dist/admin.css +97 -1
  3. package/dist/client-config/registry.d.ts +7 -0
  4. package/dist/client-config/registry.d.ts.map +1 -0
  5. package/dist/client-config/registry.js +16 -0
  6. package/dist/client-config/registry.js.map +1 -0
  7. package/dist/components/SeoField/Meter.d.ts +10 -0
  8. package/dist/components/SeoField/Meter.d.ts.map +1 -0
  9. package/dist/components/SeoField/Meter.js +59 -0
  10. package/dist/components/SeoField/Meter.js.map +1 -0
  11. package/dist/components/SeoField/icons.d.ts +5 -0
  12. package/dist/components/SeoField/icons.d.ts.map +1 -0
  13. package/dist/components/SeoField/icons.js +12 -0
  14. package/dist/components/SeoField/icons.js.map +1 -0
  15. package/dist/components/SeoField/index.d.ts +20 -0
  16. package/dist/components/SeoField/index.d.ts.map +1 -0
  17. package/dist/components/SeoField/index.js +118 -0
  18. package/dist/components/SeoField/index.js.map +1 -0
  19. package/dist/components/SeoField/useGenerate.d.ts +15 -0
  20. package/dist/components/SeoField/useGenerate.d.ts.map +1 -0
  21. package/dist/components/SeoField/useGenerate.js +102 -0
  22. package/dist/components/SeoField/useGenerate.js.map +1 -0
  23. package/dist/constants/generation.d.ts +13 -0
  24. package/dist/constants/generation.d.ts.map +1 -0
  25. package/dist/constants/generation.js +21 -0
  26. package/dist/constants/generation.js.map +1 -0
  27. package/dist/engine/helpers/title-progress.d.ts +2 -1
  28. package/dist/engine/helpers/title-progress.d.ts.map +1 -1
  29. package/dist/engine/helpers/title-progress.js +1 -1
  30. package/dist/engine/helpers/title-progress.js.map +1 -1
  31. package/dist/fields/index.d.ts +3 -0
  32. package/dist/fields/index.d.ts.map +1 -0
  33. package/dist/fields/index.js +5 -0
  34. package/dist/fields/index.js.map +1 -0
  35. package/dist/fields/onPublishHook.d.ts +8 -0
  36. package/dist/fields/onPublishHook.d.ts.map +1 -0
  37. package/dist/fields/onPublishHook.js +95 -0
  38. package/dist/fields/onPublishHook.js.map +1 -0
  39. package/dist/fields/seoTextField.d.ts +23 -0
  40. package/dist/fields/seoTextField.d.ts.map +1 -0
  41. package/dist/fields/seoTextField.js +43 -0
  42. package/dist/fields/seoTextField.js.map +1 -0
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/measure/measure.d.ts +16 -0
  48. package/dist/measure/measure.d.ts.map +1 -0
  49. package/dist/measure/measure.js +38 -0
  50. package/dist/measure/measure.js.map +1 -0
  51. package/dist/providers/SeoClientConfigProvider.d.ts +8 -0
  52. package/dist/providers/SeoClientConfigProvider.d.ts.map +1 -0
  53. package/dist/providers/SeoClientConfigProvider.js +16 -0
  54. package/dist/providers/SeoClientConfigProvider.js.map +1 -0
  55. package/dist/server/generate/apiKey.d.ts +3 -0
  56. package/dist/server/generate/apiKey.d.ts.map +1 -0
  57. package/dist/server/generate/apiKey.js +11 -0
  58. package/dist/server/generate/apiKey.js.map +1 -0
  59. package/dist/server/generate/endpoint.d.ts +3 -0
  60. package/dist/server/generate/endpoint.d.ts.map +1 -0
  61. package/dist/server/generate/endpoint.js +47 -0
  62. package/dist/server/generate/endpoint.js.map +1 -0
  63. package/dist/server/generate/generateForField.d.ts +18 -0
  64. package/dist/server/generate/generateForField.d.ts.map +1 -0
  65. package/dist/server/generate/generateForField.js +41 -0
  66. package/dist/server/generate/generateForField.js.map +1 -0
  67. package/dist/server/generate/openai.d.ts +9 -0
  68. package/dist/server/generate/openai.d.ts.map +1 -0
  69. package/dist/server/generate/openai.js +43 -0
  70. package/dist/server/generate/openai.js.map +1 -0
  71. package/dist/server/generate/prompts.d.ts +19 -0
  72. package/dist/server/generate/prompts.d.ts.map +1 -0
  73. package/dist/server/generate/prompts.js +32 -0
  74. package/dist/server/generate/prompts.js.map +1 -0
  75. package/dist/server/generate/serverResolveDocs.d.ts +4 -0
  76. package/dist/server/generate/serverResolveDocs.d.ts.map +1 -0
  77. package/dist/server/generate/serverResolveDocs.js +38 -0
  78. package/dist/server/generate/serverResolveDocs.js.map +1 -0
  79. package/dist/types/config.d.ts +26 -0
  80. package/dist/types/config.d.ts.map +1 -1
  81. package/dist/utils/config/overrideAdmin.d.ts.map +1 -1
  82. package/dist/utils/config/overrideAdmin.js +27 -1
  83. package/dist/utils/config/overrideAdmin.js.map +1 -1
  84. package/package.json +13 -1
@@ -0,0 +1,95 @@
1
+ import { getPluginConfig } from "../config";
2
+ import { PLUGIN_NAME } from "../constants";
3
+ import { DESCRIPTION_RANGE, TITLE_RANGE } from "../constants/generation";
4
+ import {
5
+ compact,
6
+ heading,
7
+ html,
8
+ image,
9
+ link,
10
+ paragraph,
11
+ richText,
12
+ video
13
+ } from "../content/schema/helpers";
14
+ import { serialize } from "../content/schema/serialize";
15
+ import { resolveApiKey } from "../server/generate/apiKey";
16
+ import { createServerResolveDocs } from "../server/generate/serverResolveDocs";
17
+ import { generateForField } from "../server/generate/generateForField";
18
+ const helpers = {
19
+ heading,
20
+ paragraph,
21
+ link,
22
+ image,
23
+ video,
24
+ html,
25
+ richText,
26
+ compact
27
+ };
28
+ const CONTEXT_KEY = "__fr_seo_content__";
29
+ function isEmpty(v) {
30
+ return typeof v !== "string" || v.trim().length === 0;
31
+ }
32
+ function rangeFor(kind, override) {
33
+ const base = kind === "title" ? TITLE_RANGE : DESCRIPTION_RANGE;
34
+ return {
35
+ min: override?.min ?? base.min,
36
+ max: override?.max ?? base.max,
37
+ unit: kind === "title" ? "px" : "char"
38
+ };
39
+ }
40
+ function makeGenerateOnPublishHook(args) {
41
+ return async ({ value, data, collection, req, operation }) => {
42
+ if (!isEmpty(value))
43
+ return value;
44
+ if (operation !== "create" && operation !== "update")
45
+ return value;
46
+ const status = data?._status;
47
+ if (status && status !== "published")
48
+ return value;
49
+ const config = getPluginConfig();
50
+ const apiKey = resolveApiKey(config.generation);
51
+ if (!apiKey)
52
+ return value;
53
+ const slug = collection?.slug;
54
+ const seoCfg = config.collections.find((c) => c.slug === slug);
55
+ const extractor = seoCfg?.serverExtractContent;
56
+ if (!extractor || !slug)
57
+ return value;
58
+ try {
59
+ const localeCode = typeof req.locale === "string" ? req.locale : void 0;
60
+ const bucket = req.context[CONTEXT_KEY] ??= {};
61
+ const cacheKey = `${slug}:${localeCode ?? ""}`;
62
+ let contentHtml = bucket[cacheKey];
63
+ if (contentHtml === void 0) {
64
+ const toolkit = {
65
+ resolveDocs: createServerResolveDocs(req.payload, localeCode),
66
+ helpers
67
+ };
68
+ const ir = await extractor(
69
+ data,
70
+ { locale: localeCode },
71
+ toolkit
72
+ );
73
+ contentHtml = serialize(ir);
74
+ bucket[cacheKey] = contentHtml;
75
+ }
76
+ return await generateForField({
77
+ kind: args.kind,
78
+ contentHtml,
79
+ range: rangeFor(args.kind, args.range),
80
+ locale: localeCode,
81
+ config: config.generation ?? {},
82
+ apiKey
83
+ });
84
+ } catch (err) {
85
+ req.payload.logger.error(
86
+ `[${PLUGIN_NAME}] on-publish generation failed: ${err.message}`
87
+ );
88
+ return value;
89
+ }
90
+ };
91
+ }
92
+ export {
93
+ makeGenerateOnPublishHook
94
+ };
95
+ //# sourceMappingURL=onPublishHook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/fields/onPublishHook.ts"],"sourcesContent":["import type { FieldHook } from \"payload\";\nimport { getPluginConfig } from \"../config\";\nimport { PLUGIN_NAME } from \"../constants\";\nimport { DESCRIPTION_RANGE, TITLE_RANGE } from \"../constants/generation\";\nimport {\n compact,\n heading,\n html,\n image,\n link,\n paragraph,\n richText,\n video,\n} from \"../content/schema/helpers\";\nimport { serialize } from \"../content/schema/serialize\";\nimport type { ContentHelpers, ExtractToolkit, SeoCollectionConfig } from \"../types/config\";\nimport type { RangeOverride } from \"../measure/measure\";\nimport { resolveApiKey } from \"../server/generate/apiKey\";\nimport { createServerResolveDocs } from \"../server/generate/serverResolveDocs\";\nimport { generateForField } from \"../server/generate/generateForField\";\nimport type { SeoFieldKind } from \"../server/generate/prompts\";\n\nconst helpers: ContentHelpers = {\n heading,\n paragraph,\n link,\n image,\n video,\n html,\n richText,\n compact,\n};\n\nconst CONTEXT_KEY = \"__fr_seo_content__\";\n\nfunction isEmpty(v: unknown): boolean {\n return typeof v !== \"string\" || v.trim().length === 0;\n}\n\nfunction rangeFor(kind: SeoFieldKind, override: RangeOverride | undefined) {\n const base = kind === \"title\" ? TITLE_RANGE : DESCRIPTION_RANGE;\n\n return {\n min: override?.min ?? base.min,\n max: override?.max ?? base.max,\n unit: (kind === \"title\" ? \"px\" : \"char\") as \"px\" | \"char\",\n };\n}\n\nexport function makeGenerateOnPublishHook(args: {\n kind: SeoFieldKind;\n range: RangeOverride | undefined;\n}): FieldHook {\n return async ({ value, data, collection, req, operation }) => {\n if (!isEmpty(value)) return value;\n if (operation !== \"create\" && operation !== \"update\") return value;\n\n const status = (data as { _status?: string } | undefined)?._status;\n if (status && status !== \"published\") return value;\n\n const config = getPluginConfig();\n const apiKey = resolveApiKey(config.generation);\n if (!apiKey) return value;\n\n const slug = collection?.slug;\n const seoCfg = config.collections.find((c: SeoCollectionConfig) => c.slug === slug);\n const extractor = seoCfg?.serverExtractContent;\n if (!extractor || !slug) return value;\n\n try {\n const localeCode = typeof req.locale === \"string\" ? req.locale : undefined;\n\n const bucket = (req.context[CONTEXT_KEY] ??= {}) as Record<string, string>;\n const cacheKey = `${slug}:${localeCode ?? \"\"}`;\n let contentHtml = bucket[cacheKey];\n if (contentHtml === undefined) {\n const toolkit: ExtractToolkit = {\n resolveDocs: createServerResolveDocs(req.payload, localeCode),\n helpers,\n };\n const ir = await extractor(\n data as Record<string, unknown>,\n { locale: localeCode },\n toolkit\n );\n contentHtml = serialize(ir);\n bucket[cacheKey] = contentHtml;\n }\n\n return await generateForField({\n kind: args.kind,\n contentHtml,\n range: rangeFor(args.kind, args.range),\n locale: localeCode,\n config: config.generation ?? {},\n apiKey,\n });\n } catch (err) {\n req.payload.logger.error(\n `[${PLUGIN_NAME}] on-publish generation failed: ${(err as Error).message}`\n );\n\n return value;\n }\n };\n}\n"],"mappings":"AACA,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB,mBAAmB;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAG1B,SAAS,qBAAqB;AAC9B,SAAS,+BAA+B;AACxC,SAAS,wBAAwB;AAGjC,MAAM,UAA0B;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,cAAc;AAEpB,SAAS,QAAQ,GAAqB;AACpC,SAAO,OAAO,MAAM,YAAY,EAAE,KAAK,EAAE,WAAW;AACtD;AAEA,SAAS,SAAS,MAAoB,UAAqC;AACzE,QAAM,OAAO,SAAS,UAAU,cAAc;AAE9C,SAAO;AAAA,IACL,KAAK,UAAU,OAAO,KAAK;AAAA,IAC3B,KAAK,UAAU,OAAO,KAAK;AAAA,IAC3B,MAAO,SAAS,UAAU,OAAO;AAAA,EACnC;AACF;AAEO,SAAS,0BAA0B,MAG5B;AACZ,SAAO,OAAO,EAAE,OAAO,MAAM,YAAY,KAAK,UAAU,MAAM;AAC5D,QAAI,CAAC,QAAQ,KAAK;AAAG,aAAO;AAC5B,QAAI,cAAc,YAAY,cAAc;AAAU,aAAO;AAE7D,UAAM,SAAU,MAA2C;AAC3D,QAAI,UAAU,WAAW;AAAa,aAAO;AAE7C,UAAM,SAAS,gBAAgB;AAC/B,UAAM,SAAS,cAAc,OAAO,UAAU;AAC9C,QAAI,CAAC;AAAQ,aAAO;AAEpB,UAAM,OAAO,YAAY;AACzB,UAAM,SAAS,OAAO,YAAY,KAAK,CAAC,MAA2B,EAAE,SAAS,IAAI;AAClF,UAAM,YAAY,QAAQ;AAC1B,QAAI,CAAC,aAAa,CAAC;AAAM,aAAO;AAEhC,QAAI;AACF,YAAM,aAAa,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;AAEjE,YAAM,SAAU,IAAI,QAAQ,WAAW,MAAM,CAAC;AAC9C,YAAM,WAAW,GAAG,IAAI,IAAI,cAAc,EAAE;AAC5C,UAAI,cAAc,OAAO,QAAQ;AACjC,UAAI,gBAAgB,QAAW;AAC7B,cAAM,UAA0B;AAAA,UAC9B,aAAa,wBAAwB,IAAI,SAAS,UAAU;AAAA,UAC5D;AAAA,QACF;AACA,cAAM,KAAK,MAAM;AAAA,UACf;AAAA,UACA,EAAE,QAAQ,WAAW;AAAA,UACrB;AAAA,QACF;AACA,sBAAc,UAAU,EAAE;AAC1B,eAAO,QAAQ,IAAI;AAAA,MACrB;AAEA,aAAO,MAAM,iBAAiB;AAAA,QAC5B,MAAM,KAAK;AAAA,QACX;AAAA,QACA,OAAO,SAAS,KAAK,MAAM,KAAK,KAAK;AAAA,QACrC,QAAQ;AAAA,QACR,QAAQ,OAAO,cAAc,CAAC;AAAA,QAC9B;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,QAAQ,OAAO;AAAA,QACjB,IAAI,WAAW,mCAAoC,IAAc,OAAO;AAAA,MAC1E;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,23 @@
1
+ import type { TextareaField, TextField } from "payload";
2
+ import type { RangeOverride } from "../measure/measure";
3
+ import type { SeoFieldKind } from "../server/generate/prompts";
4
+ export interface SeoTextFieldOptions {
5
+ name: string;
6
+ kind: SeoFieldKind;
7
+ label?: TextField["label"];
8
+ required?: boolean;
9
+ localized?: boolean;
10
+ admin?: TextField["admin"];
11
+ /** Show the manual Generate button.
12
+ * @default false
13
+ */
14
+ showButton?: boolean;
15
+ /** Generate on publish when the field is empty
16
+ * @default false
17
+ */
18
+ generateOnPublish?: boolean;
19
+ /** Length range override in the kind's unit (px for title, chars for description). */
20
+ range?: RangeOverride;
21
+ }
22
+ export declare function seoTextField(options: SeoTextFieldOptions): TextField | TextareaField;
23
+ //# sourceMappingURL=seoTextField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seoTextField.d.ts","sourceRoot":"","sources":["../../src/fields/seoTextField.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3B;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,sFAAsF;IACtF,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,GAAG,aAAa,CA2CpF"}
@@ -0,0 +1,43 @@
1
+ import { getComponentPath } from "../utils/config/getComponentPath";
2
+ import { makeGenerateOnPublishHook } from "./onPublishHook";
3
+ function seoTextField(options) {
4
+ const { name, kind, label, required, localized, admin, range } = options;
5
+ const showButton = options.showButton ?? false;
6
+ const generateOnPublish = options.generateOnPublish ?? false;
7
+ const common = {
8
+ name,
9
+ ...label === void 0 ? {} : { label },
10
+ ...required === void 0 ? {} : { required },
11
+ ...localized === void 0 ? {} : { localized },
12
+ admin: {
13
+ ...admin,
14
+ components: {
15
+ ...admin?.components,
16
+ Field: {
17
+ path: getComponentPath("components/SeoField", "SeoField"),
18
+ clientProps: {
19
+ kind,
20
+ showButton,
21
+ generateOnPublish,
22
+ range
23
+ }
24
+ }
25
+ }
26
+ }
27
+ };
28
+ const field = kind === "description" ? { ...common, type: "textarea" } : { ...common, type: "text" };
29
+ if (generateOnPublish) {
30
+ field.hooks = {
31
+ ...field.hooks,
32
+ beforeChange: [
33
+ ...field.hooks?.beforeChange ?? [],
34
+ makeGenerateOnPublishHook({ kind, range })
35
+ ]
36
+ };
37
+ }
38
+ return field;
39
+ }
40
+ export {
41
+ seoTextField
42
+ };
43
+ //# sourceMappingURL=seoTextField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/fields/seoTextField.ts"],"sourcesContent":["import type { TextareaField, TextField } from \"payload\";\nimport { getComponentPath } from \"../utils/config/getComponentPath\";\nimport { makeGenerateOnPublishHook } from \"./onPublishHook\";\nimport type { RangeOverride } from \"../measure/measure\";\nimport type { SeoFieldKind } from \"../server/generate/prompts\";\n\nexport interface SeoTextFieldOptions {\n name: string;\n kind: SeoFieldKind;\n label?: TextField[\"label\"];\n required?: boolean;\n localized?: boolean;\n admin?: TextField[\"admin\"];\n /** Show the manual Generate button.\n * @default false\n */\n showButton?: boolean;\n /** Generate on publish when the field is empty\n * @default false\n */\n generateOnPublish?: boolean;\n /** Length range override in the kind's unit (px for title, chars for description). */\n range?: RangeOverride;\n}\n\nexport function seoTextField(options: SeoTextFieldOptions): TextField | TextareaField {\n const { name, kind, label, required, localized, admin, range } = options;\n const showButton = options.showButton ?? false;\n const generateOnPublish = options.generateOnPublish ?? false;\n\n const common = {\n name,\n ...(label === undefined ? {} : { label }),\n ...(required === undefined ? {} : { required }),\n ...(localized === undefined ? {} : { localized }),\n admin: {\n ...admin,\n components: {\n ...admin?.components,\n Field: {\n path: getComponentPath(\"components/SeoField\", \"SeoField\"),\n clientProps: {\n kind,\n showButton,\n generateOnPublish,\n range,\n },\n },\n },\n },\n };\n\n const field: TextField | TextareaField =\n kind === \"description\"\n ? ({ ...common, type: \"textarea\" } as TextareaField)\n : ({ ...common, type: \"text\" } as TextField);\n\n if (generateOnPublish) {\n field.hooks = {\n ...field.hooks,\n beforeChange: [\n ...(field.hooks?.beforeChange ?? []),\n makeGenerateOnPublishHook({ kind, range }),\n ],\n };\n }\n\n return field;\n}\n"],"mappings":"AACA,SAAS,wBAAwB;AACjC,SAAS,iCAAiC;AAuBnC,SAAS,aAAa,SAAyD;AACpF,QAAM,EAAE,MAAM,MAAM,OAAO,UAAU,WAAW,OAAO,MAAM,IAAI;AACjE,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,oBAAoB,QAAQ,qBAAqB;AAEvD,QAAM,SAAS;AAAA,IACb;AAAA,IACA,GAAI,UAAU,SAAY,CAAC,IAAI,EAAE,MAAM;AAAA,IACvC,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA,IAC7C,GAAI,cAAc,SAAY,CAAC,IAAI,EAAE,UAAU;AAAA,IAC/C,OAAO;AAAA,MACL,GAAG;AAAA,MACH,YAAY;AAAA,QACV,GAAG,OAAO;AAAA,QACV,OAAO;AAAA,UACL,MAAM,iBAAiB,uBAAuB,UAAU;AAAA,UACxD,aAAa;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QACJ,SAAS,gBACJ,EAAE,GAAG,QAAQ,MAAM,WAAW,IAC9B,EAAE,GAAG,QAAQ,MAAM,OAAO;AAEjC,MAAI,mBAAmB;AACrB,UAAM,QAAQ;AAAA,MACZ,GAAG,MAAM;AAAA,MACT,cAAc;AAAA,QACZ,GAAI,MAAM,OAAO,gBAAgB,CAAC;AAAA,QAClC,0BAA0B,EAAE,MAAM,MAAM,CAAC;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { seoPlugin } from "./plugin";
2
- export type { ContentExtractor, SeoPluginConfig, SeoCollectionConfig, SeoFieldPaths, SeoSiteConfig, } from "./types/config";
2
+ export { seoTextField } from "./fields/seoTextField";
3
+ export type { SeoTextFieldOptions } from "./fields/seoTextField";
4
+ export type { ContentExtractor, SeoPluginConfig, SeoCollectionConfig, SeoFieldPaths, SeoSiteConfig, SeoGenerationConfig, } from "./types/config";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,aAAa,GACd,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { seoPlugin } from "./plugin";
2
+ import { seoTextField } from "./fields/seoTextField";
2
3
  export {
3
- seoPlugin
4
+ seoPlugin,
5
+ seoTextField
4
6
  };
5
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { seoPlugin } from \"./plugin\";\nexport type {\n ContentExtractor,\n SeoPluginConfig,\n SeoCollectionConfig,\n SeoFieldPaths,\n SeoSiteConfig,\n} from \"./types/config\";\n"],"mappings":"AAAA,SAAS,iBAAiB;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { seoPlugin } from \"./plugin\";\nexport { seoTextField } from \"./fields/seoTextField\";\nexport type { SeoTextFieldOptions } from \"./fields/seoTextField\";\nexport type {\n ContentExtractor,\n SeoPluginConfig,\n SeoCollectionConfig,\n SeoFieldPaths,\n SeoSiteConfig,\n SeoGenerationConfig,\n} from \"./types/config\";\n"],"mappings":"AAAA,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;","names":[]}
@@ -0,0 +1,16 @@
1
+ export type LengthStatus = "good" | "short" | "long";
2
+ export type LengthUnit = "px" | "char";
3
+ export interface Measurement {
4
+ unit: LengthUnit;
5
+ value: number;
6
+ min: number;
7
+ max: number;
8
+ status: LengthStatus;
9
+ }
10
+ export interface RangeOverride {
11
+ min?: number;
12
+ max?: number;
13
+ }
14
+ export declare function measureTitle(text: string, range?: RangeOverride): Measurement;
15
+ export declare function measureDescription(text: string, range?: RangeOverride): Measurement;
16
+ //# sourceMappingURL=measure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../src/measure/measure.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,WAAW,CAY7E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,WAAW,CAYnF"}
@@ -0,0 +1,38 @@
1
+ import { getTitleProgressGuarded } from "../engine/helpers/title-progress";
2
+ import { DESCRIPTION_RANGE, TITLE_RANGE } from "../constants/generation";
3
+ function statusFor(value, min, max) {
4
+ if (value > max)
5
+ return "long";
6
+ if (value < min)
7
+ return "short";
8
+ return "good";
9
+ }
10
+ function measureTitle(text, range) {
11
+ const progress = getTitleProgressGuarded(text ?? "");
12
+ const min = range?.min ?? TITLE_RANGE.min;
13
+ const max = range?.max ?? progress.max ?? TITLE_RANGE.max;
14
+ return {
15
+ unit: "px",
16
+ value: progress.actual,
17
+ min,
18
+ max,
19
+ status: statusFor(progress.actual, min, max)
20
+ };
21
+ }
22
+ function measureDescription(text, range) {
23
+ const value = (text ?? "").length;
24
+ const min = range?.min ?? DESCRIPTION_RANGE.min;
25
+ const max = range?.max ?? DESCRIPTION_RANGE.max;
26
+ return {
27
+ unit: "char",
28
+ value,
29
+ min,
30
+ max,
31
+ status: statusFor(value, min, max)
32
+ };
33
+ }
34
+ export {
35
+ measureDescription,
36
+ measureTitle
37
+ };
38
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/measure/measure.ts"],"sourcesContent":["import { getTitleProgressGuarded } from \"../engine/helpers/title-progress\";\nimport { DESCRIPTION_RANGE, TITLE_RANGE } from \"../constants/generation\";\n\nexport type LengthStatus = \"good\" | \"short\" | \"long\";\nexport type LengthUnit = \"px\" | \"char\";\n\nexport interface Measurement {\n unit: LengthUnit;\n value: number;\n min: number;\n max: number;\n status: LengthStatus;\n}\n\nexport interface RangeOverride {\n min?: number;\n max?: number;\n}\n\nfunction statusFor(value: number, min: number, max: number): LengthStatus {\n if (value > max) return \"long\";\n if (value < min) return \"short\";\n return \"good\";\n}\n\nexport function measureTitle(text: string, range?: RangeOverride): Measurement {\n const progress = getTitleProgressGuarded(text ?? \"\");\n const min = range?.min ?? TITLE_RANGE.min;\n const max = range?.max ?? progress.max ?? TITLE_RANGE.max;\n\n return {\n unit: \"px\",\n value: progress.actual,\n min,\n max,\n status: statusFor(progress.actual, min, max),\n };\n}\n\nexport function measureDescription(text: string, range?: RangeOverride): Measurement {\n const value = (text ?? \"\").length;\n const min = range?.min ?? DESCRIPTION_RANGE.min;\n const max = range?.max ?? DESCRIPTION_RANGE.max;\n\n return {\n unit: \"char\",\n value,\n min,\n max,\n status: statusFor(value, min, max),\n };\n}\n"],"mappings":"AAAA,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,mBAAmB;AAkB/C,SAAS,UAAU,OAAe,KAAa,KAA2B;AACxE,MAAI,QAAQ;AAAK,WAAO;AACxB,MAAI,QAAQ;AAAK,WAAO;AACxB,SAAO;AACT;AAEO,SAAS,aAAa,MAAc,OAAoC;AAC7E,QAAM,WAAW,wBAAwB,QAAQ,EAAE;AACnD,QAAM,MAAM,OAAO,OAAO,YAAY;AACtC,QAAM,MAAM,OAAO,OAAO,SAAS,OAAO,YAAY;AAEtD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,SAAS;AAAA,IAChB;AAAA,IACA;AAAA,IACA,QAAQ,UAAU,SAAS,QAAQ,KAAK,GAAG;AAAA,EAC7C;AACF;AAEO,SAAS,mBAAmB,MAAc,OAAoC;AACnF,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,MAAM,OAAO,OAAO,kBAAkB;AAC5C,QAAM,MAAM,OAAO,OAAO,kBAAkB;AAE5C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,UAAU,OAAO,KAAK,GAAG;AAAA,EACnC;AACF;","names":[]}
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SeoClientConfig } from "../client-config/registry";
3
+ export declare function SeoClientConfigProvider({ config, children, }: {
4
+ config: SeoClientConfig;
5
+ children: ReactNode;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ export default SeoClientConfigProvider;
8
+ //# sourceMappingURL=SeoClientConfigProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SeoClientConfigProvider.d.ts","sourceRoot":"","sources":["../../src/providers/SeoClientConfigProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAIA;AAED,eAAe,uBAAuB,CAAC"}
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ import { registerSeoClientConfig } from "../client-config/registry";
4
+ function SeoClientConfigProvider({
5
+ config,
6
+ children
7
+ }) {
8
+ registerSeoClientConfig(config);
9
+ return /* @__PURE__ */ jsx(Fragment, { children });
10
+ }
11
+ var SeoClientConfigProvider_default = SeoClientConfigProvider;
12
+ export {
13
+ SeoClientConfigProvider,
14
+ SeoClientConfigProvider_default as default
15
+ };
16
+ //# sourceMappingURL=SeoClientConfigProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/SeoClientConfigProvider.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { registerSeoClientConfig } from \"../client-config/registry\";\nimport type { SeoClientConfig } from \"../client-config/registry\";\n\nexport function SeoClientConfigProvider({\n config,\n children,\n}: {\n config: SeoClientConfig;\n children: ReactNode;\n}) {\n registerSeoClientConfig(config);\n\n return <>{children}</>;\n}\n\nexport default SeoClientConfigProvider;\n"],"mappings":";AAeS;AAZT,SAAS,+BAA+B;AAGjC,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,GAGG;AACD,0BAAwB,MAAM;AAE9B,SAAO,gCAAG,UAAS;AACrB;AAEA,IAAO,kCAAQ;","names":[]}
@@ -0,0 +1,3 @@
1
+ import type { SeoGenerationConfig } from "../../types/config";
2
+ export declare function resolveApiKey(config: SeoGenerationConfig | undefined): string | undefined;
3
+ //# sourceMappingURL=apiKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apiKey.d.ts","sourceRoot":"","sources":["../../../src/server/generate/apiKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMzF"}
@@ -0,0 +1,11 @@
1
+ function resolveApiKey(config) {
2
+ const explicit = config?.apiKey?.trim();
3
+ if (explicit)
4
+ return explicit;
5
+ const env = process.env.OPENAI_API_KEY?.trim();
6
+ return env || void 0;
7
+ }
8
+ export {
9
+ resolveApiKey
10
+ };
11
+ //# sourceMappingURL=apiKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/server/generate/apiKey.ts"],"sourcesContent":["import type { SeoGenerationConfig } from \"../../types/config\";\n\nexport function resolveApiKey(config: SeoGenerationConfig | undefined): string | undefined {\n const explicit = config?.apiKey?.trim();\n if (explicit) return explicit;\n\n const env = process.env.OPENAI_API_KEY?.trim();\n return env || undefined;\n}\n"],"mappings":"AAEO,SAAS,cAAc,QAA6D;AACzF,QAAM,WAAW,QAAQ,QAAQ,KAAK;AACtC,MAAI;AAAU,WAAO;AAErB,QAAM,MAAM,QAAQ,IAAI,gBAAgB,KAAK;AAC7C,SAAO,OAAO;AAChB;","names":[]}
@@ -0,0 +1,3 @@
1
+ import type { Endpoint } from "payload";
2
+ export declare function createGenerateEndpoint(): Endpoint;
3
+ //# sourceMappingURL=endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../../../src/server/generate/endpoint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAC;AAuBxD,wBAAgB,sBAAsB,IAAI,QAAQ,CAsCjD"}
@@ -0,0 +1,47 @@
1
+ import { getPluginConfig } from "../../config";
2
+ import { PLUGIN_NAME } from "../../constants";
3
+ import { GENERATE_ENDPOINT_PATH } from "../../constants/generation";
4
+ import { resolveApiKey } from "./apiKey";
5
+ import { generateForField } from "./generateForField";
6
+ function json(data, status) {
7
+ return Response.json(data, {
8
+ status,
9
+ headers: { "Content-Type": "application/json" }
10
+ });
11
+ }
12
+ function createGenerateEndpoint() {
13
+ return {
14
+ path: GENERATE_ENDPOINT_PATH,
15
+ method: "post",
16
+ handler: async (req) => {
17
+ if (!req.user)
18
+ return json({ error: "Unauthorized" }, 401);
19
+ const config = getPluginConfig();
20
+ const apiKey = resolveApiKey(config.generation);
21
+ if (!apiKey)
22
+ return json({ error: "Generation is not configured" }, 503);
23
+ const body = await req.json?.() ?? {};
24
+ if (body.kind !== "title" && body.kind !== "description" || typeof body.contentHtml !== "string" || !body.range) {
25
+ return json({ error: "Invalid request body" }, 400);
26
+ }
27
+ try {
28
+ const text = await generateForField({
29
+ kind: body.kind,
30
+ contentHtml: body.contentHtml,
31
+ range: body.range,
32
+ locale: body.locale,
33
+ config: config.generation ?? {},
34
+ apiKey
35
+ });
36
+ return json({ text }, 200);
37
+ } catch (err) {
38
+ req.payload.logger.error(`[${PLUGIN_NAME}] generation failed: ${err.message}`);
39
+ return json({ error: "Generation failed" }, 502);
40
+ }
41
+ }
42
+ };
43
+ }
44
+ export {
45
+ createGenerateEndpoint
46
+ };
47
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/server/generate/endpoint.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from \"payload\";\nimport { getPluginConfig } from \"../../config\";\nimport { PLUGIN_NAME } from \"../../constants\";\nimport { GENERATE_ENDPOINT_PATH } from \"../../constants/generation\";\nimport type { LengthUnit } from \"../../measure/measure\";\nimport { resolveApiKey } from \"./apiKey\";\nimport { generateForField } from \"./generateForField\";\nimport type { SeoFieldKind } from \"./prompts\";\n\ninterface GenerateBody {\n kind?: SeoFieldKind;\n contentHtml?: string;\n locale?: string;\n range?: { min: number; max: number; unit: LengthUnit };\n}\n\nfunction json(data: unknown, status: number): Response {\n return Response.json(data, {\n status,\n headers: { \"Content-Type\": \"application/json\" },\n });\n}\n\nexport function createGenerateEndpoint(): Endpoint {\n return {\n path: GENERATE_ENDPOINT_PATH,\n method: \"post\",\n handler: async (req: PayloadRequest): Promise<Response> => {\n if (!req.user) return json({ error: \"Unauthorized\" }, 401);\n\n const config = getPluginConfig();\n const apiKey = resolveApiKey(config.generation);\n if (!apiKey) return json({ error: \"Generation is not configured\" }, 503);\n\n const body = ((await req.json?.()) ?? {}) as GenerateBody;\n if (\n (body.kind !== \"title\" && body.kind !== \"description\") ||\n typeof body.contentHtml !== \"string\" ||\n !body.range\n ) {\n return json({ error: \"Invalid request body\" }, 400);\n }\n\n try {\n const text = await generateForField({\n kind: body.kind,\n contentHtml: body.contentHtml,\n range: body.range,\n locale: body.locale,\n config: config.generation ?? {},\n apiKey,\n });\n\n return json({ text }, 200);\n } catch (err) {\n req.payload.logger.error(`[${PLUGIN_NAME}] generation failed: ${(err as Error).message}`);\n\n return json({ error: \"Generation failed\" }, 502);\n }\n },\n };\n}\n"],"mappings":"AACA,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,8BAA8B;AAEvC,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;AAUjC,SAAS,KAAK,MAAe,QAA0B;AACrD,SAAO,SAAS,KAAK,MAAM;AAAA,IACzB;AAAA,IACA,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,EAChD,CAAC;AACH;AAEO,SAAS,yBAAmC;AACjD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,OAAO,QAA2C;AACzD,UAAI,CAAC,IAAI;AAAM,eAAO,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAEzD,YAAM,SAAS,gBAAgB;AAC/B,YAAM,SAAS,cAAc,OAAO,UAAU;AAC9C,UAAI,CAAC;AAAQ,eAAO,KAAK,EAAE,OAAO,+BAA+B,GAAG,GAAG;AAEvE,YAAM,OAAS,MAAM,IAAI,OAAO,KAAM,CAAC;AACvC,UACG,KAAK,SAAS,WAAW,KAAK,SAAS,iBACxC,OAAO,KAAK,gBAAgB,YAC5B,CAAC,KAAK,OACN;AACA,eAAO,KAAK,EAAE,OAAO,uBAAuB,GAAG,GAAG;AAAA,MACpD;AAEA,UAAI;AACF,cAAM,OAAO,MAAM,iBAAiB;AAAA,UAClC,MAAM,KAAK;AAAA,UACX,aAAa,KAAK;AAAA,UAClB,OAAO,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,QAAQ,OAAO,cAAc,CAAC;AAAA,UAC9B;AAAA,QACF,CAAC;AAED,eAAO,KAAK,EAAE,KAAK,GAAG,GAAG;AAAA,MAC3B,SAAS,KAAK;AACZ,YAAI,QAAQ,OAAO,MAAM,IAAI,WAAW,wBAAyB,IAAc,OAAO,EAAE;AAExF,eAAO,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,18 @@
1
+ import type { SeoGenerationConfig } from "../../types/config";
2
+ import type { LengthUnit } from "../../measure/measure";
3
+ import type { SeoFieldKind } from "./prompts";
4
+ export interface GenerateForFieldArgs {
5
+ kind: SeoFieldKind;
6
+ contentHtml: string;
7
+ range: {
8
+ min: number;
9
+ max: number;
10
+ unit: LengthUnit;
11
+ };
12
+ locale?: string;
13
+ config: SeoGenerationConfig;
14
+ apiKey: string;
15
+ signal?: AbortSignal;
16
+ }
17
+ export declare function generateForField(args: GenerateForFieldArgs): Promise<string>;
18
+ //# sourceMappingURL=generateForField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateForField.d.ts","sourceRoot":"","sources":["../../../src/server/generate/generateForField.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAoBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBlF"}
@@ -0,0 +1,41 @@
1
+ import { DEFAULT_MAX_CONTENT_CHARS, DEFAULT_MODEL } from "../../constants/generation";
2
+ import { callOpenAIChat } from "./openai";
3
+ import { buildPrompt } from "./prompts";
4
+ const QUOTE_PAIRS = [
5
+ ['"', '"'],
6
+ ["'", "'"],
7
+ ["\u201C", "\u201D"]
8
+ ];
9
+ function stripQuotes(s) {
10
+ const t = s.trim();
11
+ if (t.length < 2)
12
+ return t;
13
+ for (const [open, close] of QUOTE_PAIRS) {
14
+ if (t.startsWith(open) && t.endsWith(close))
15
+ return t.slice(1, -1).trim();
16
+ }
17
+ return t;
18
+ }
19
+ async function generateForField(args) {
20
+ const max = args.config.maxContentChars ?? DEFAULT_MAX_CONTENT_CHARS;
21
+ const contentHtml = args.contentHtml.slice(0, max);
22
+ const { system, user } = buildPrompt({
23
+ kind: args.kind,
24
+ contentHtml,
25
+ range: args.range,
26
+ locale: args.locale,
27
+ config: args.config
28
+ });
29
+ const text = await callOpenAIChat({
30
+ apiKey: args.apiKey,
31
+ model: args.config.model ?? DEFAULT_MODEL,
32
+ system,
33
+ user,
34
+ signal: args.signal
35
+ });
36
+ return stripQuotes(text);
37
+ }
38
+ export {
39
+ generateForField
40
+ };
41
+ //# sourceMappingURL=generateForField.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/server/generate/generateForField.ts"],"sourcesContent":["import { DEFAULT_MAX_CONTENT_CHARS, DEFAULT_MODEL } from \"../../constants/generation\";\nimport type { SeoGenerationConfig } from \"../../types/config\";\nimport type { LengthUnit } from \"../../measure/measure\";\nimport { callOpenAIChat } from \"./openai\";\nimport type { SeoFieldKind } from \"./prompts\";\nimport { buildPrompt } from \"./prompts\";\n\nexport interface GenerateForFieldArgs {\n kind: SeoFieldKind;\n contentHtml: string;\n range: {\n min: number;\n max: number;\n unit: LengthUnit;\n };\n locale?: string;\n config: SeoGenerationConfig;\n apiKey: string;\n signal?: AbortSignal;\n}\n\nconst QUOTE_PAIRS: ReadonlyArray<readonly [string, string]> = [\n ['\"', '\"'],\n [\"'\", \"'\"],\n [\"“\", \"”\"],\n];\n\nfunction stripQuotes(s: string): string {\n const t = s.trim();\n\n if (t.length < 2) return t;\n\n for (const [open, close] of QUOTE_PAIRS) {\n if (t.startsWith(open) && t.endsWith(close)) return t.slice(1, -1).trim();\n }\n\n return t;\n}\n\nexport async function generateForField(args: GenerateForFieldArgs): Promise<string> {\n const max = args.config.maxContentChars ?? DEFAULT_MAX_CONTENT_CHARS;\n const contentHtml = args.contentHtml.slice(0, max);\n\n const { system, user } = buildPrompt({\n kind: args.kind,\n contentHtml,\n range: args.range,\n locale: args.locale,\n config: args.config,\n });\n\n const text = await callOpenAIChat({\n apiKey: args.apiKey,\n model: args.config.model ?? DEFAULT_MODEL,\n system,\n user,\n signal: args.signal,\n });\n\n return stripQuotes(text);\n}\n"],"mappings":"AAAA,SAAS,2BAA2B,qBAAqB;AAGzD,SAAS,sBAAsB;AAE/B,SAAS,mBAAmB;AAgB5B,MAAM,cAAwD;AAAA,EAC5D,CAAC,KAAK,GAAG;AAAA,EACT,CAAC,KAAK,GAAG;AAAA,EACT,CAAC,UAAK,QAAG;AACX;AAEA,SAAS,YAAY,GAAmB;AACtC,QAAM,IAAI,EAAE,KAAK;AAEjB,MAAI,EAAE,SAAS;AAAG,WAAO;AAEzB,aAAW,CAAC,MAAM,KAAK,KAAK,aAAa;AACvC,QAAI,EAAE,WAAW,IAAI,KAAK,EAAE,SAAS,KAAK;AAAG,aAAO,EAAE,MAAM,GAAG,EAAE,EAAE,KAAK;AAAA,EAC1E;AAEA,SAAO;AACT;AAEA,eAAsB,iBAAiB,MAA6C;AAClF,QAAM,MAAM,KAAK,OAAO,mBAAmB;AAC3C,QAAM,cAAc,KAAK,YAAY,MAAM,GAAG,GAAG;AAEjD,QAAM,EAAE,QAAQ,KAAK,IAAI,YAAY;AAAA,IACnC,MAAM,KAAK;AAAA,IACX;AAAA,IACA,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,QAAQ,KAAK;AAAA,EACf,CAAC;AAED,QAAM,OAAO,MAAM,eAAe;AAAA,IAChC,QAAQ,KAAK;AAAA,IACb,OAAO,KAAK,OAAO,SAAS;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,QAAQ,KAAK;AAAA,EACf,CAAC;AAED,SAAO,YAAY,IAAI;AACzB;","names":[]}
@@ -0,0 +1,9 @@
1
+ export interface OpenAIChatArgs {
2
+ apiKey: string;
3
+ model: string;
4
+ system: string;
5
+ user: string;
6
+ signal?: AbortSignal;
7
+ }
8
+ export declare function callOpenAIChat({ apiKey, model, system, user, signal, }: OpenAIChatArgs): Promise<string>;
9
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/server/generate/openai.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAQD,wBAAsB,cAAc,CAAC,EACnC,MAAM,EACN,KAAK,EACL,MAAM,EACN,IAAI,EACJ,MAAM,GACP,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAqClC"}
@@ -0,0 +1,43 @@
1
+ const ENDPOINT = "https://api.openai.com/v1/chat/completions";
2
+ async function callOpenAIChat({
3
+ apiKey,
4
+ model,
5
+ system,
6
+ user,
7
+ signal
8
+ }) {
9
+ const res = await fetch(ENDPOINT, {
10
+ method: "POST",
11
+ headers: {
12
+ "Content-Type": "application/json",
13
+ Authorization: `Bearer ${apiKey}`
14
+ },
15
+ body: JSON.stringify({
16
+ model,
17
+ temperature: 0.5,
18
+ messages: [
19
+ { role: "system", content: system },
20
+ { role: "user", content: user }
21
+ ]
22
+ }),
23
+ signal
24
+ });
25
+ if (!res.ok) {
26
+ let kind = "";
27
+ try {
28
+ const errBody = await res.json();
29
+ kind = errBody.error?.code ?? errBody.error?.type ?? "";
30
+ } catch {
31
+ }
32
+ throw new Error(`OpenAI request failed (${res.status}${kind ? `, ${kind}` : ""})`);
33
+ }
34
+ const body = await res.json();
35
+ const text = body.choices?.[0]?.message?.content?.trim();
36
+ if (!text)
37
+ throw new Error("OpenAI returned an empty completion");
38
+ return text;
39
+ }
40
+ export {
41
+ callOpenAIChat
42
+ };
43
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/server/generate/openai.ts"],"sourcesContent":["export interface OpenAIChatArgs {\n apiKey: string;\n model: string;\n system: string;\n user: string;\n signal?: AbortSignal;\n}\n\ninterface ChatCompletionResponse {\n choices?: Array<{ message?: { content?: string } }>;\n}\n\nconst ENDPOINT = \"https://api.openai.com/v1/chat/completions\";\n\nexport async function callOpenAIChat({\n apiKey,\n model,\n system,\n user,\n signal,\n}: OpenAIChatArgs): Promise<string> {\n const res = await fetch(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n temperature: 0.5,\n messages: [\n { role: \"system\", content: system },\n { role: \"user\", content: user },\n ],\n }),\n signal,\n });\n\n if (!res.ok) {\n let kind = \"\";\n\n try {\n const errBody = (await res.json()) as { error?: { type?: string; code?: string } };\n kind = errBody.error?.code ?? errBody.error?.type ?? \"\";\n } catch {\n // non-JSON error body — ignore it; do not surface raw text\n }\n\n throw new Error(`OpenAI request failed (${res.status}${kind ? `, ${kind}` : \"\"})`);\n }\n\n const body = (await res.json()) as ChatCompletionResponse;\n\n const text = body.choices?.[0]?.message?.content?.trim();\n if (!text) throw new Error(\"OpenAI returned an empty completion\");\n\n return text;\n}\n"],"mappings":"AAYA,MAAM,WAAW;AAEjB,eAAsB,eAAe;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoC;AAClC,QAAM,MAAM,MAAM,MAAM,UAAU;AAAA,IAChC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,MAAM;AAAA,IACjC;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB;AAAA,MACA,aAAa;AAAA,MACb,UAAU;AAAA,QACR,EAAE,MAAM,UAAU,SAAS,OAAO;AAAA,QAClC,EAAE,MAAM,QAAQ,SAAS,KAAK;AAAA,MAChC;AAAA,IACF,CAAC;AAAA,IACD;AAAA,EACF,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,OAAO;AAEX,QAAI;AACF,YAAM,UAAW,MAAM,IAAI,KAAK;AAChC,aAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,QAAQ;AAAA,IACvD,QAAQ;AAAA,IAER;AAEA,UAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,GAAG,OAAO,KAAK,IAAI,KAAK,EAAE,GAAG;AAAA,EACnF;AAEA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAE7B,QAAM,OAAO,KAAK,UAAU,CAAC,GAAG,SAAS,SAAS,KAAK;AACvD,MAAI,CAAC;AAAM,UAAM,IAAI,MAAM,qCAAqC;AAEhE,SAAO;AACT;","names":[]}
@@ -0,0 +1,19 @@
1
+ import type { SeoGenerationConfig } from "../../types/config";
2
+ import type { LengthUnit } from "../../measure/measure";
3
+ export type SeoFieldKind = "title" | "description";
4
+ export interface PromptArgs {
5
+ kind: SeoFieldKind;
6
+ contentHtml: string;
7
+ range: {
8
+ min: number;
9
+ max: number;
10
+ unit: LengthUnit;
11
+ };
12
+ locale?: string;
13
+ config: SeoGenerationConfig;
14
+ }
15
+ export declare function buildPrompt(args: PromptArgs): {
16
+ system: string;
17
+ user: string;
18
+ };
19
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../../src/server/generate/prompts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,aAAa,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,UAAU,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AA2BD,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAO9E"}
@@ -0,0 +1,32 @@
1
+ import { AVG_GLYPH_PX } from "../../constants/generation";
2
+ function charRangeFromPx(minPx, maxPx) {
3
+ return {
4
+ min: Math.ceil(minPx / AVG_GLYPH_PX),
5
+ max: Math.floor(maxPx / AVG_GLYPH_PX)
6
+ };
7
+ }
8
+ function defaultSystem(kind, range, locale) {
9
+ const { min, max } = range.unit === "px" ? charRangeFromPx(range.min, range.max) : range;
10
+ const what = kind === "title" ? "an SEO meta title for the web page described by the content below" : "an SEO meta description for the web page described by the content below";
11
+ const lines = [
12
+ `You write ${what}.`,
13
+ `The text MUST be strictly between ${min} and ${max} characters long \u2014 never fewer than ${min} and never more than ${max}, including spaces.`,
14
+ "Summarize the page's actual subject; do not invent facts not present in the content.",
15
+ "Return ONLY the text \u2014 no quotes, no markdown, no labels, no trailing punctuation unless natural."
16
+ ];
17
+ if (locale)
18
+ lines.push(`Write in the locale "${locale}".`);
19
+ return lines.join(" ");
20
+ }
21
+ function buildPrompt(args) {
22
+ const override = args.kind === "title" ? args.config.titlePrompt : args.config.descriptionPrompt;
23
+ const system = override ?? defaultSystem(args.kind, args.range, args.locale);
24
+ const user = `Page content:
25
+
26
+ ${args.contentHtml}`;
27
+ return { system, user };
28
+ }
29
+ export {
30
+ buildPrompt
31
+ };
32
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/server/generate/prompts.ts"],"sourcesContent":["import { AVG_GLYPH_PX } from \"../../constants/generation\";\nimport type { SeoGenerationConfig } from \"../../types/config\";\nimport type { LengthUnit } from \"../../measure/measure\";\n\nexport type SeoFieldKind = \"title\" | \"description\";\n\nexport interface PromptArgs {\n kind: SeoFieldKind;\n contentHtml: string;\n range: {\n min: number;\n max: number;\n unit: LengthUnit;\n };\n locale?: string;\n config: SeoGenerationConfig;\n}\n\nfunction charRangeFromPx(minPx: number, maxPx: number): { min: number; max: number } {\n return {\n min: Math.ceil(minPx / AVG_GLYPH_PX),\n max: Math.floor(maxPx / AVG_GLYPH_PX),\n };\n}\n\nfunction defaultSystem(kind: SeoFieldKind, range: PromptArgs[\"range\"], locale?: string): string {\n const { min, max } = range.unit === \"px\" ? charRangeFromPx(range.min, range.max) : range;\n const what =\n kind === \"title\"\n ? \"an SEO meta title for the web page described by the content below\"\n : \"an SEO meta description for the web page described by the content below\";\n const lines = [\n `You write ${what}.`,\n `The text MUST be strictly between ${min} and ${max} characters long — never fewer than ${min} and never more than ${max}, including spaces.`,\n \"Summarize the page's actual subject; do not invent facts not present in the content.\",\n \"Return ONLY the text — no quotes, no markdown, no labels, no trailing punctuation unless natural.\",\n ];\n\n if (locale) lines.push(`Write in the locale \"${locale}\".`);\n\n return lines.join(\" \");\n}\n\nexport function buildPrompt(args: PromptArgs): { system: string; user: string } {\n const override = args.kind === \"title\" ? args.config.titlePrompt : args.config.descriptionPrompt;\n\n const system = override ?? defaultSystem(args.kind, args.range, args.locale);\n const user = `Page content:\\n\\n${args.contentHtml}`;\n\n return { system, user };\n}\n"],"mappings":"AAAA,SAAS,oBAAoB;AAkB7B,SAAS,gBAAgB,OAAe,OAA6C;AACnF,SAAO;AAAA,IACL,KAAK,KAAK,KAAK,QAAQ,YAAY;AAAA,IACnC,KAAK,KAAK,MAAM,QAAQ,YAAY;AAAA,EACtC;AACF;AAEA,SAAS,cAAc,MAAoB,OAA4B,QAAyB;AAC9F,QAAM,EAAE,KAAK,IAAI,IAAI,MAAM,SAAS,OAAO,gBAAgB,MAAM,KAAK,MAAM,GAAG,IAAI;AACnF,QAAM,OACJ,SAAS,UACL,sEACA;AACN,QAAM,QAAQ;AAAA,IACZ,aAAa,IAAI;AAAA,IACjB,qCAAqC,GAAG,QAAQ,GAAG,4CAAuC,GAAG,wBAAwB,GAAG;AAAA,IACxH;AAAA,IACA;AAAA,EACF;AAEA,MAAI;AAAQ,UAAM,KAAK,wBAAwB,MAAM,IAAI;AAEzD,SAAO,MAAM,KAAK,GAAG;AACvB;AAEO,SAAS,YAAY,MAAoD;AAC9E,QAAM,WAAW,KAAK,SAAS,UAAU,KAAK,OAAO,cAAc,KAAK,OAAO;AAE/E,QAAM,SAAS,YAAY,cAAc,KAAK,MAAM,KAAK,OAAO,KAAK,MAAM;AAC3E,QAAM,OAAO;AAAA;AAAA,EAAoB,KAAK,WAAW;AAEjD,SAAO,EAAE,QAAQ,KAAK;AACxB;","names":[]}
@@ -0,0 +1,4 @@
1
+ import type { Payload } from "payload";
2
+ import type { DocQuery, DocStore } from "../../types/config";
3
+ export declare function createServerResolveDocs(payload: Payload, locale: string | undefined): (queries: DocQuery[]) => Promise<DocStore>;
4
+ //# sourceMappingURL=serverResolveDocs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverResolveDocs.d.ts","sourceRoot":"","sources":["../../../src/server/generate/serverResolveDocs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAqC7D,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAW5C"}