@bettercms-ai/sdk 1.32.0 → 1.34.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.
package/dist/seo.cjs ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/seo.ts
21
+ var seo_exports = {};
22
+ __export(seo_exports, {
23
+ entrySeoInput: () => entrySeoInput,
24
+ resolveSeo: () => resolveSeo
25
+ });
26
+ module.exports = __toCommonJS(seo_exports);
27
+ function derivedCanonical({ siteUrl, path }) {
28
+ if (!siteUrl || !path) return "";
29
+ try {
30
+ return new URL(path, siteUrl.endsWith("/") ? siteUrl : `${siteUrl}/`).toString();
31
+ } catch {
32
+ return "";
33
+ }
34
+ }
35
+ function jsonLdNodes(schema) {
36
+ if (!schema) return [];
37
+ const arr = Array.isArray(schema) ? schema : [schema];
38
+ const out = [];
39
+ for (const node of arr) {
40
+ if (!node || Object.keys(node).length === 0) continue;
41
+ const graph = node["@graph"];
42
+ if (Array.isArray(graph)) {
43
+ for (const child of graph) {
44
+ if (child && typeof child === "object" && !Array.isArray(child) && Object.keys(child).length > 0) {
45
+ out.push(child);
46
+ }
47
+ }
48
+ continue;
49
+ }
50
+ out.push(node);
51
+ }
52
+ return out;
53
+ }
54
+ var SCHEMA_TYPE_CHOICES = [
55
+ "WebPage",
56
+ "AboutPage",
57
+ "ContactPage",
58
+ "CollectionPage",
59
+ "Blog",
60
+ "BlogPosting",
61
+ "Article",
62
+ "Product",
63
+ "FAQPage",
64
+ "Event",
65
+ "Organization",
66
+ "LocalBusiness"
67
+ ];
68
+ function compact(node) {
69
+ return Object.fromEntries(Object.entries(node).filter(([, value]) => value != null && value !== ""));
70
+ }
71
+ function schemaNode(type, { name, description, image, url }) {
72
+ switch (type) {
73
+ case "BlogPosting":
74
+ case "Article":
75
+ return compact({ "@type": type, headline: name, description, image, url, mainEntityOfPage: url });
76
+ case "Product":
77
+ case "Event":
78
+ return compact({ "@type": type, name, description, image, url });
79
+ case "FAQPage":
80
+ case "WebSite":
81
+ return compact({ "@type": type, name, description, url });
82
+ case "Organization":
83
+ return compact({ "@type": type, name, description, url, logo: image });
84
+ default:
85
+ return compact({ "@type": type, name, description, url, image });
86
+ }
87
+ }
88
+ function pageJsonLd(page, meta, canonical, siteSchema) {
89
+ const custom = jsonLdNodes(meta.schema);
90
+ if (custom.length > 0) return custom;
91
+ const props = {
92
+ name: (page.metaTitle || page.title || "").trim(),
93
+ description: (page.metaDescription || "").trim(),
94
+ image: (meta.og?.image || "").trim(),
95
+ url: canonical.trim()
96
+ };
97
+ const schemaType = meta.schemaType;
98
+ if (typeof schemaType === "string" && SCHEMA_TYPE_CHOICES.includes(schemaType)) {
99
+ return [schemaNode(schemaType, props)];
100
+ }
101
+ if (page.slug === "home") {
102
+ return jsonLdNodes(siteSchema).length > 0 ? [] : [schemaNode("WebSite", props)];
103
+ }
104
+ return [schemaNode("WebPage", props)];
105
+ }
106
+ function entrySeoInput(entry) {
107
+ const { metaTitle, metaDescription, noindex, ...metaJson } = entry.seo ?? {};
108
+ return {
109
+ title: metaTitle || entry.slug,
110
+ metaTitle: metaTitle ?? null,
111
+ metaDescription: metaDescription ?? null,
112
+ metaJson,
113
+ noindex: noindex === true
114
+ };
115
+ }
116
+ function resolveSeo(page, defaults = {}, location = {}) {
117
+ const meta = page.metaJson ?? {};
118
+ const title = page.metaTitle || page.title || "";
119
+ const description = page.metaDescription || defaults.metaDescription || "";
120
+ const ogImage = meta.og?.image || defaults.og?.image || defaults.ogImage || "";
121
+ const ogTitle = meta.og?.title || title;
122
+ const ogDescription = meta.og?.description || description;
123
+ const twImage = meta.twitter?.image || ogImage;
124
+ const canonical = meta.canonical || derivedCanonical(location);
125
+ return {
126
+ title,
127
+ description,
128
+ canonical,
129
+ og: {
130
+ title: ogTitle,
131
+ description: ogDescription,
132
+ type: meta.og?.type || defaults.og?.type || "website",
133
+ image: ogImage,
134
+ url: canonical
135
+ },
136
+ twitter: {
137
+ card: meta.twitter?.card || defaults.twitter?.card || (twImage ? "summary_large_image" : "summary"),
138
+ title: meta.twitter?.title || ogTitle,
139
+ description: meta.twitter?.description || ogDescription,
140
+ image: twImage,
141
+ site: defaults.twitterHandle || ""
142
+ },
143
+ jsonLd: [...jsonLdNodes(defaults.siteSchema), ...pageJsonLd(page, meta, canonical, defaults.siteSchema)],
144
+ noindex: page.noindex === true,
145
+ robots: { index: page.noindex !== true, follow: true }
146
+ };
147
+ }
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ entrySeoInput,
151
+ resolveSeo
152
+ });
153
+ //# sourceMappingURL=seo.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/seo.ts"],"sourcesContent":["/**\n * resolveSeo — framework-agnostic page-over-site SEO merge for headless consumers.\n *\n * The single source of truth for \"per-page meta layered over project defaults\",\n * mirroring the server renderer's `mergeSeoMeta` so `*.bettercms.site` and headless\n * sites (@bettercms-ai/next, @bettercms-ai/astro) emit identical <head> SEO. Pure: empty\n * inputs collapse to empty strings (\"omit the tag\"), never fabricated values.\n *\n * const seo = resolveSeo(page, siteDefaults);\n * // → { title, description, canonical, og, twitter, jsonLd }\n *\n * Next consumers map this to a `Metadata` via `buildMetadata` (@bettercms-ai/next);\n * Astro consumers read the fields straight into `<head>`.\n */\nimport type { DeliveryEntrySeo, PageMetaJson, SiteSeoDefaults } from \"@bettercms-ai/types\";\n\n/** Minimal page shape `resolveSeo` reads — a `DeliveryPage`/`BetterCMSPage` satisfies it. */\nexport interface SeoInput {\n title: string;\n metaTitle?: string | null;\n metaDescription?: string | null;\n metaJson?: PageMetaJson | null;\n /** `\"home\"` makes the Automatic JSON-LD a WebSite, as the hosted renderer does. */\n slug?: string | null;\n /** Exclude from search engines. Surfaces as `ResolvedSeo.noindex` / `robots`. */\n noindex?: boolean | null;\n}\n\n/** Fully-resolved SEO values, page-over-site. Empty string ⇒ omit that tag. */\nexport interface ResolvedSeo {\n title: string;\n description: string;\n /**\n * Canonical URL — the page's own `canonical`, else `siteUrl` + `path` when both are given,\n * else \"\". Empty means the consumer must omit the tag: a RELATIVE canonical is worse than\n * none at all.\n */\n canonical: string;\n og: {\n title: string;\n description: string;\n type: string;\n image: string;\n url: string;\n };\n twitter: {\n card: string;\n title: string;\n description: string;\n image: string;\n site: string;\n };\n /**\n * JSON-LD nodes (site schema first, then the page's), already flattened + de-empted. The page's\n * part follows the renderer's precedence: Custom (`metaJson.schema`) > `metaJson.schemaType` >\n * Automatic (WebPage; WebSite on the home page).\n */\n jsonLd: Array<Record<string, unknown>>;\n /** True when the page is excluded from search engines (the renderer's `<meta name=\"robots\" content=\"noindex\">`). */\n noindex: boolean;\n /** `noindex` in the `{ index, follow }` shape Next's `Metadata.robots` takes. */\n robots: { index: boolean; follow: boolean };\n}\n\n/**\n * `siteUrl` + `path` → an absolute canonical, or \"\" when either is missing or unparseable.\n * Never returns a relative URL: a relative canonical is worse than no canonical.\n */\nfunction derivedCanonical({ siteUrl, path }: SeoLocation): string {\n if (!siteUrl || !path) return \"\";\n try {\n return new URL(path, siteUrl.endsWith(\"/\") ? siteUrl : `${siteUrl}/`).toString();\n } catch {\n return \"\";\n }\n}\n\n/** Flatten one-or-many JSON-LD into a node array, dropping empty objects. */\nfunction jsonLdNodes(\n schema: Record<string, unknown> | Array<Record<string, unknown>> | undefined,\n): Array<Record<string, unknown>> {\n if (!schema) return [];\n const arr = Array.isArray(schema) ? schema : [schema];\n const out: Array<Record<string, unknown>> = [];\n for (const node of arr) {\n if (!node || Object.keys(node).length === 0) continue;\n const graph = node[\"@graph\"];\n if (Array.isArray(graph)) {\n for (const child of graph) {\n if (child && typeof child === \"object\" && !Array.isArray(child) && Object.keys(child).length > 0) {\n out.push(child as Record<string, unknown>);\n }\n }\n continue;\n }\n out.push(node);\n }\n return out;\n}\n\n/*\n * Structured data for a page whose `metaJson.schema` is empty. A port of the page path of\n * `resolveStructuredData` in the backend (src/lib/seo/structured-data.ts, called from\n * `pageStructuredData` in src/lib/sites/render-page.ts), with `site.include: false` as the\n * Delivery API calls it: the site-wide schema is emitted separately, from `siteSchema`.\n * A page has no field data, so only the properties the page's own meta supplies are filled.\n * Keep in step with that file; the parity test imports it.\n */\nconst SCHEMA_TYPE_CHOICES = [\n \"WebPage\",\n \"AboutPage\",\n \"ContactPage\",\n \"CollectionPage\",\n \"Blog\",\n \"BlogPosting\",\n \"Article\",\n \"Product\",\n \"FAQPage\",\n \"Event\",\n \"Organization\",\n \"LocalBusiness\",\n] as const;\n\ntype PageProps = { name: string; description: string; image: string; url: string };\n\n/** Drop \"\", null and undefined so a node never carries a blank. */\nfunction compact(node: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(node).filter(([, value]) => value != null && value !== \"\"));\n}\n\nfunction schemaNode(type: string, { name, description, image, url }: PageProps): Record<string, unknown> {\n switch (type) {\n case \"BlogPosting\":\n case \"Article\":\n return compact({ \"@type\": type, headline: name, description, image, url, mainEntityOfPage: url });\n case \"Product\":\n case \"Event\":\n return compact({ \"@type\": type, name, description, image, url });\n case \"FAQPage\":\n case \"WebSite\":\n return compact({ \"@type\": type, name, description, url });\n case \"Organization\":\n return compact({ \"@type\": type, name, description, url, logo: image });\n default:\n // WebPage, AboutPage, ContactPage, CollectionPage, Blog, LocalBusiness\n return compact({ \"@type\": type, name, description, url, image });\n }\n}\n\n/** The page's own JSON-LD nodes: Custom > explicit `schemaType` > Automatic. */\nfunction pageJsonLd(\n page: SeoInput,\n meta: PageMetaJson,\n canonical: string,\n siteSchema: SiteSeoDefaults[\"siteSchema\"],\n): Array<Record<string, unknown>> {\n const custom = jsonLdNodes(meta.schema);\n if (custom.length > 0) return custom;\n const props: PageProps = {\n name: (page.metaTitle || page.title || \"\").trim(),\n description: (page.metaDescription || \"\").trim(),\n image: (meta.og?.image || \"\").trim(),\n url: canonical.trim(),\n };\n const schemaType = (meta as { schemaType?: unknown }).schemaType;\n if (typeof schemaType === \"string\" && (SCHEMA_TYPE_CHOICES as readonly string[]).includes(schemaType)) {\n return [schemaNode(schemaType, props)];\n }\n if (page.slug === \"home\") {\n // The home page IS the WebSite, unless the site-wide schema already describes it.\n return jsonLdNodes(siteSchema).length > 0 ? [] : [schemaNode(\"WebSite\", props)];\n }\n return [schemaNode(\"WebPage\", props)];\n}\n\n/**\n * A Delivery API entry as a `resolveSeo` input. The server already resolved the collection\n * template's `{{field}}` tokens for this entry and merged the entry's own SEO over it, so this is\n * a reshape, not a resolver:\n *\n * const entry = await bcms.getEntry(\"astro\");\n * const seo = resolveSeo(entrySeoInput(entry), siteDefaults, { siteUrl, path: `/tags/${entry.slug}` });\n * // seo.jsonLd holds the template's schema, resolved for this entry\n */\nexport function entrySeoInput(entry: { slug: string; seo?: DeliveryEntrySeo | null }): SeoInput {\n const { metaTitle, metaDescription, noindex, ...metaJson } = entry.seo ?? {};\n return {\n title: metaTitle || entry.slug,\n metaTitle: metaTitle ?? null,\n metaDescription: metaDescription ?? null,\n metaJson,\n noindex: noindex === true,\n };\n}\n\n/** Where this page lives, so a canonical can be derived when the page sets none. */\nexport interface SeoLocation {\n /** Absolute site origin, e.g. `https://acme.com`. Trailing slash optional. */\n siteUrl?: string;\n /** This page's path, e.g. `/about` or `/`. */\n path?: string;\n}\n\n/**\n * Merge a page's own SEO over the site defaults: page meta wins, then site defaults, then\n * sensible fallbacks.\n *\n * The CANONICAL is the one field that cannot be resolved from content alone, and getting this\n * wrong is silent. The hosted renderer (`mergeSeoMeta` in the backend) falls back to a canonical\n * it derives from the request's own origin and path — headless consumers have no request, so\n * they must pass `location`. Without it this returns \"\" and the caller ships no\n * `<link rel=\"canonical\">` and no `og:url`, exactly as it did before `location` existed.\n */\nexport function resolveSeo(\n page: SeoInput,\n defaults: SiteSeoDefaults = {},\n location: SeoLocation = {},\n): ResolvedSeo {\n const meta = page.metaJson ?? {};\n const title = page.metaTitle || page.title || \"\";\n const description = page.metaDescription || defaults.metaDescription || \"\";\n const ogImage = meta.og?.image || defaults.og?.image || defaults.ogImage || \"\";\n const ogTitle = meta.og?.title || title;\n const ogDescription = meta.og?.description || description;\n const twImage = meta.twitter?.image || ogImage;\n const canonical = meta.canonical || derivedCanonical(location);\n return {\n title,\n description,\n canonical,\n og: {\n title: ogTitle,\n description: ogDescription,\n type: meta.og?.type || defaults.og?.type || \"website\",\n image: ogImage,\n url: canonical,\n },\n twitter: {\n card: meta.twitter?.card || defaults.twitter?.card || (twImage ? \"summary_large_image\" : \"summary\"),\n title: meta.twitter?.title || ogTitle,\n description: meta.twitter?.description || ogDescription,\n image: twImage,\n site: defaults.twitterHandle || \"\",\n },\n jsonLd: [...jsonLdNodes(defaults.siteSchema), ...pageJsonLd(page, meta, canonical, defaults.siteSchema)],\n noindex: page.noindex === true,\n robots: { index: page.noindex !== true, follow: true },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/seo.d.cts ADDED
@@ -0,0 +1,99 @@
1
+ import { PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults } from '@bettercms-ai/types';
2
+
3
+ /**
4
+ * resolveSeo — framework-agnostic page-over-site SEO merge for headless consumers.
5
+ *
6
+ * The single source of truth for "per-page meta layered over project defaults",
7
+ * mirroring the server renderer's `mergeSeoMeta` so `*.bettercms.site` and headless
8
+ * sites (@bettercms-ai/next, @bettercms-ai/astro) emit identical <head> SEO. Pure: empty
9
+ * inputs collapse to empty strings ("omit the tag"), never fabricated values.
10
+ *
11
+ * const seo = resolveSeo(page, siteDefaults);
12
+ * // → { title, description, canonical, og, twitter, jsonLd }
13
+ *
14
+ * Next consumers map this to a `Metadata` via `buildMetadata` (@bettercms-ai/next);
15
+ * Astro consumers read the fields straight into `<head>`.
16
+ */
17
+
18
+ /** Minimal page shape `resolveSeo` reads — a `DeliveryPage`/`BetterCMSPage` satisfies it. */
19
+ interface SeoInput {
20
+ title: string;
21
+ metaTitle?: string | null;
22
+ metaDescription?: string | null;
23
+ metaJson?: PageMetaJson | null;
24
+ /** `"home"` makes the Automatic JSON-LD a WebSite, as the hosted renderer does. */
25
+ slug?: string | null;
26
+ /** Exclude from search engines. Surfaces as `ResolvedSeo.noindex` / `robots`. */
27
+ noindex?: boolean | null;
28
+ }
29
+ /** Fully-resolved SEO values, page-over-site. Empty string ⇒ omit that tag. */
30
+ interface ResolvedSeo {
31
+ title: string;
32
+ description: string;
33
+ /**
34
+ * Canonical URL — the page's own `canonical`, else `siteUrl` + `path` when both are given,
35
+ * else "". Empty means the consumer must omit the tag: a RELATIVE canonical is worse than
36
+ * none at all.
37
+ */
38
+ canonical: string;
39
+ og: {
40
+ title: string;
41
+ description: string;
42
+ type: string;
43
+ image: string;
44
+ url: string;
45
+ };
46
+ twitter: {
47
+ card: string;
48
+ title: string;
49
+ description: string;
50
+ image: string;
51
+ site: string;
52
+ };
53
+ /**
54
+ * JSON-LD nodes (site schema first, then the page's), already flattened + de-empted. The page's
55
+ * part follows the renderer's precedence: Custom (`metaJson.schema`) > `metaJson.schemaType` >
56
+ * Automatic (WebPage; WebSite on the home page).
57
+ */
58
+ jsonLd: Array<Record<string, unknown>>;
59
+ /** True when the page is excluded from search engines (the renderer's `<meta name="robots" content="noindex">`). */
60
+ noindex: boolean;
61
+ /** `noindex` in the `{ index, follow }` shape Next's `Metadata.robots` takes. */
62
+ robots: {
63
+ index: boolean;
64
+ follow: boolean;
65
+ };
66
+ }
67
+ /**
68
+ * A Delivery API entry as a `resolveSeo` input. The server already resolved the collection
69
+ * template's `{{field}}` tokens for this entry and merged the entry's own SEO over it, so this is
70
+ * a reshape, not a resolver:
71
+ *
72
+ * const entry = await bcms.getEntry("astro");
73
+ * const seo = resolveSeo(entrySeoInput(entry), siteDefaults, { siteUrl, path: `/tags/${entry.slug}` });
74
+ * // seo.jsonLd holds the template's schema, resolved for this entry
75
+ */
76
+ declare function entrySeoInput(entry: {
77
+ slug: string;
78
+ seo?: DeliveryEntrySeo | null;
79
+ }): SeoInput;
80
+ /** Where this page lives, so a canonical can be derived when the page sets none. */
81
+ interface SeoLocation {
82
+ /** Absolute site origin, e.g. `https://acme.com`. Trailing slash optional. */
83
+ siteUrl?: string;
84
+ /** This page's path, e.g. `/about` or `/`. */
85
+ path?: string;
86
+ }
87
+ /**
88
+ * Merge a page's own SEO over the site defaults: page meta wins, then site defaults, then
89
+ * sensible fallbacks.
90
+ *
91
+ * The CANONICAL is the one field that cannot be resolved from content alone, and getting this
92
+ * wrong is silent. The hosted renderer (`mergeSeoMeta` in the backend) falls back to a canonical
93
+ * it derives from the request's own origin and path — headless consumers have no request, so
94
+ * they must pass `location`. Without it this returns "" and the caller ships no
95
+ * `<link rel="canonical">` and no `og:url`, exactly as it did before `location` existed.
96
+ */
97
+ declare function resolveSeo(page: SeoInput, defaults?: SiteSeoDefaults, location?: SeoLocation): ResolvedSeo;
98
+
99
+ export { type ResolvedSeo, type SeoInput, type SeoLocation, entrySeoInput, resolveSeo };
package/dist/seo.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults } from '@bettercms-ai/types';
2
+
3
+ /**
4
+ * resolveSeo — framework-agnostic page-over-site SEO merge for headless consumers.
5
+ *
6
+ * The single source of truth for "per-page meta layered over project defaults",
7
+ * mirroring the server renderer's `mergeSeoMeta` so `*.bettercms.site` and headless
8
+ * sites (@bettercms-ai/next, @bettercms-ai/astro) emit identical <head> SEO. Pure: empty
9
+ * inputs collapse to empty strings ("omit the tag"), never fabricated values.
10
+ *
11
+ * const seo = resolveSeo(page, siteDefaults);
12
+ * // → { title, description, canonical, og, twitter, jsonLd }
13
+ *
14
+ * Next consumers map this to a `Metadata` via `buildMetadata` (@bettercms-ai/next);
15
+ * Astro consumers read the fields straight into `<head>`.
16
+ */
17
+
18
+ /** Minimal page shape `resolveSeo` reads — a `DeliveryPage`/`BetterCMSPage` satisfies it. */
19
+ interface SeoInput {
20
+ title: string;
21
+ metaTitle?: string | null;
22
+ metaDescription?: string | null;
23
+ metaJson?: PageMetaJson | null;
24
+ /** `"home"` makes the Automatic JSON-LD a WebSite, as the hosted renderer does. */
25
+ slug?: string | null;
26
+ /** Exclude from search engines. Surfaces as `ResolvedSeo.noindex` / `robots`. */
27
+ noindex?: boolean | null;
28
+ }
29
+ /** Fully-resolved SEO values, page-over-site. Empty string ⇒ omit that tag. */
30
+ interface ResolvedSeo {
31
+ title: string;
32
+ description: string;
33
+ /**
34
+ * Canonical URL — the page's own `canonical`, else `siteUrl` + `path` when both are given,
35
+ * else "". Empty means the consumer must omit the tag: a RELATIVE canonical is worse than
36
+ * none at all.
37
+ */
38
+ canonical: string;
39
+ og: {
40
+ title: string;
41
+ description: string;
42
+ type: string;
43
+ image: string;
44
+ url: string;
45
+ };
46
+ twitter: {
47
+ card: string;
48
+ title: string;
49
+ description: string;
50
+ image: string;
51
+ site: string;
52
+ };
53
+ /**
54
+ * JSON-LD nodes (site schema first, then the page's), already flattened + de-empted. The page's
55
+ * part follows the renderer's precedence: Custom (`metaJson.schema`) > `metaJson.schemaType` >
56
+ * Automatic (WebPage; WebSite on the home page).
57
+ */
58
+ jsonLd: Array<Record<string, unknown>>;
59
+ /** True when the page is excluded from search engines (the renderer's `<meta name="robots" content="noindex">`). */
60
+ noindex: boolean;
61
+ /** `noindex` in the `{ index, follow }` shape Next's `Metadata.robots` takes. */
62
+ robots: {
63
+ index: boolean;
64
+ follow: boolean;
65
+ };
66
+ }
67
+ /**
68
+ * A Delivery API entry as a `resolveSeo` input. The server already resolved the collection
69
+ * template's `{{field}}` tokens for this entry and merged the entry's own SEO over it, so this is
70
+ * a reshape, not a resolver:
71
+ *
72
+ * const entry = await bcms.getEntry("astro");
73
+ * const seo = resolveSeo(entrySeoInput(entry), siteDefaults, { siteUrl, path: `/tags/${entry.slug}` });
74
+ * // seo.jsonLd holds the template's schema, resolved for this entry
75
+ */
76
+ declare function entrySeoInput(entry: {
77
+ slug: string;
78
+ seo?: DeliveryEntrySeo | null;
79
+ }): SeoInput;
80
+ /** Where this page lives, so a canonical can be derived when the page sets none. */
81
+ interface SeoLocation {
82
+ /** Absolute site origin, e.g. `https://acme.com`. Trailing slash optional. */
83
+ siteUrl?: string;
84
+ /** This page's path, e.g. `/about` or `/`. */
85
+ path?: string;
86
+ }
87
+ /**
88
+ * Merge a page's own SEO over the site defaults: page meta wins, then site defaults, then
89
+ * sensible fallbacks.
90
+ *
91
+ * The CANONICAL is the one field that cannot be resolved from content alone, and getting this
92
+ * wrong is silent. The hosted renderer (`mergeSeoMeta` in the backend) falls back to a canonical
93
+ * it derives from the request's own origin and path — headless consumers have no request, so
94
+ * they must pass `location`. Without it this returns "" and the caller ships no
95
+ * `<link rel="canonical">` and no `og:url`, exactly as it did before `location` existed.
96
+ */
97
+ declare function resolveSeo(page: SeoInput, defaults?: SiteSeoDefaults, location?: SeoLocation): ResolvedSeo;
98
+
99
+ export { type ResolvedSeo, type SeoInput, type SeoLocation, entrySeoInput, resolveSeo };
package/dist/seo.js ADDED
@@ -0,0 +1,127 @@
1
+ // src/seo.ts
2
+ function derivedCanonical({ siteUrl, path }) {
3
+ if (!siteUrl || !path) return "";
4
+ try {
5
+ return new URL(path, siteUrl.endsWith("/") ? siteUrl : `${siteUrl}/`).toString();
6
+ } catch {
7
+ return "";
8
+ }
9
+ }
10
+ function jsonLdNodes(schema) {
11
+ if (!schema) return [];
12
+ const arr = Array.isArray(schema) ? schema : [schema];
13
+ const out = [];
14
+ for (const node of arr) {
15
+ if (!node || Object.keys(node).length === 0) continue;
16
+ const graph = node["@graph"];
17
+ if (Array.isArray(graph)) {
18
+ for (const child of graph) {
19
+ if (child && typeof child === "object" && !Array.isArray(child) && Object.keys(child).length > 0) {
20
+ out.push(child);
21
+ }
22
+ }
23
+ continue;
24
+ }
25
+ out.push(node);
26
+ }
27
+ return out;
28
+ }
29
+ var SCHEMA_TYPE_CHOICES = [
30
+ "WebPage",
31
+ "AboutPage",
32
+ "ContactPage",
33
+ "CollectionPage",
34
+ "Blog",
35
+ "BlogPosting",
36
+ "Article",
37
+ "Product",
38
+ "FAQPage",
39
+ "Event",
40
+ "Organization",
41
+ "LocalBusiness"
42
+ ];
43
+ function compact(node) {
44
+ return Object.fromEntries(Object.entries(node).filter(([, value]) => value != null && value !== ""));
45
+ }
46
+ function schemaNode(type, { name, description, image, url }) {
47
+ switch (type) {
48
+ case "BlogPosting":
49
+ case "Article":
50
+ return compact({ "@type": type, headline: name, description, image, url, mainEntityOfPage: url });
51
+ case "Product":
52
+ case "Event":
53
+ return compact({ "@type": type, name, description, image, url });
54
+ case "FAQPage":
55
+ case "WebSite":
56
+ return compact({ "@type": type, name, description, url });
57
+ case "Organization":
58
+ return compact({ "@type": type, name, description, url, logo: image });
59
+ default:
60
+ return compact({ "@type": type, name, description, url, image });
61
+ }
62
+ }
63
+ function pageJsonLd(page, meta, canonical, siteSchema) {
64
+ const custom = jsonLdNodes(meta.schema);
65
+ if (custom.length > 0) return custom;
66
+ const props = {
67
+ name: (page.metaTitle || page.title || "").trim(),
68
+ description: (page.metaDescription || "").trim(),
69
+ image: (meta.og?.image || "").trim(),
70
+ url: canonical.trim()
71
+ };
72
+ const schemaType = meta.schemaType;
73
+ if (typeof schemaType === "string" && SCHEMA_TYPE_CHOICES.includes(schemaType)) {
74
+ return [schemaNode(schemaType, props)];
75
+ }
76
+ if (page.slug === "home") {
77
+ return jsonLdNodes(siteSchema).length > 0 ? [] : [schemaNode("WebSite", props)];
78
+ }
79
+ return [schemaNode("WebPage", props)];
80
+ }
81
+ function entrySeoInput(entry) {
82
+ const { metaTitle, metaDescription, noindex, ...metaJson } = entry.seo ?? {};
83
+ return {
84
+ title: metaTitle || entry.slug,
85
+ metaTitle: metaTitle ?? null,
86
+ metaDescription: metaDescription ?? null,
87
+ metaJson,
88
+ noindex: noindex === true
89
+ };
90
+ }
91
+ function resolveSeo(page, defaults = {}, location = {}) {
92
+ const meta = page.metaJson ?? {};
93
+ const title = page.metaTitle || page.title || "";
94
+ const description = page.metaDescription || defaults.metaDescription || "";
95
+ const ogImage = meta.og?.image || defaults.og?.image || defaults.ogImage || "";
96
+ const ogTitle = meta.og?.title || title;
97
+ const ogDescription = meta.og?.description || description;
98
+ const twImage = meta.twitter?.image || ogImage;
99
+ const canonical = meta.canonical || derivedCanonical(location);
100
+ return {
101
+ title,
102
+ description,
103
+ canonical,
104
+ og: {
105
+ title: ogTitle,
106
+ description: ogDescription,
107
+ type: meta.og?.type || defaults.og?.type || "website",
108
+ image: ogImage,
109
+ url: canonical
110
+ },
111
+ twitter: {
112
+ card: meta.twitter?.card || defaults.twitter?.card || (twImage ? "summary_large_image" : "summary"),
113
+ title: meta.twitter?.title || ogTitle,
114
+ description: meta.twitter?.description || ogDescription,
115
+ image: twImage,
116
+ site: defaults.twitterHandle || ""
117
+ },
118
+ jsonLd: [...jsonLdNodes(defaults.siteSchema), ...pageJsonLd(page, meta, canonical, defaults.siteSchema)],
119
+ noindex: page.noindex === true,
120
+ robots: { index: page.noindex !== true, follow: true }
121
+ };
122
+ }
123
+ export {
124
+ entrySeoInput,
125
+ resolveSeo
126
+ };
127
+ //# sourceMappingURL=seo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/seo.ts"],"sourcesContent":["/**\n * resolveSeo — framework-agnostic page-over-site SEO merge for headless consumers.\n *\n * The single source of truth for \"per-page meta layered over project defaults\",\n * mirroring the server renderer's `mergeSeoMeta` so `*.bettercms.site` and headless\n * sites (@bettercms-ai/next, @bettercms-ai/astro) emit identical <head> SEO. Pure: empty\n * inputs collapse to empty strings (\"omit the tag\"), never fabricated values.\n *\n * const seo = resolveSeo(page, siteDefaults);\n * // → { title, description, canonical, og, twitter, jsonLd }\n *\n * Next consumers map this to a `Metadata` via `buildMetadata` (@bettercms-ai/next);\n * Astro consumers read the fields straight into `<head>`.\n */\nimport type { DeliveryEntrySeo, PageMetaJson, SiteSeoDefaults } from \"@bettercms-ai/types\";\n\n/** Minimal page shape `resolveSeo` reads — a `DeliveryPage`/`BetterCMSPage` satisfies it. */\nexport interface SeoInput {\n title: string;\n metaTitle?: string | null;\n metaDescription?: string | null;\n metaJson?: PageMetaJson | null;\n /** `\"home\"` makes the Automatic JSON-LD a WebSite, as the hosted renderer does. */\n slug?: string | null;\n /** Exclude from search engines. Surfaces as `ResolvedSeo.noindex` / `robots`. */\n noindex?: boolean | null;\n}\n\n/** Fully-resolved SEO values, page-over-site. Empty string ⇒ omit that tag. */\nexport interface ResolvedSeo {\n title: string;\n description: string;\n /**\n * Canonical URL — the page's own `canonical`, else `siteUrl` + `path` when both are given,\n * else \"\". Empty means the consumer must omit the tag: a RELATIVE canonical is worse than\n * none at all.\n */\n canonical: string;\n og: {\n title: string;\n description: string;\n type: string;\n image: string;\n url: string;\n };\n twitter: {\n card: string;\n title: string;\n description: string;\n image: string;\n site: string;\n };\n /**\n * JSON-LD nodes (site schema first, then the page's), already flattened + de-empted. The page's\n * part follows the renderer's precedence: Custom (`metaJson.schema`) > `metaJson.schemaType` >\n * Automatic (WebPage; WebSite on the home page).\n */\n jsonLd: Array<Record<string, unknown>>;\n /** True when the page is excluded from search engines (the renderer's `<meta name=\"robots\" content=\"noindex\">`). */\n noindex: boolean;\n /** `noindex` in the `{ index, follow }` shape Next's `Metadata.robots` takes. */\n robots: { index: boolean; follow: boolean };\n}\n\n/**\n * `siteUrl` + `path` → an absolute canonical, or \"\" when either is missing or unparseable.\n * Never returns a relative URL: a relative canonical is worse than no canonical.\n */\nfunction derivedCanonical({ siteUrl, path }: SeoLocation): string {\n if (!siteUrl || !path) return \"\";\n try {\n return new URL(path, siteUrl.endsWith(\"/\") ? siteUrl : `${siteUrl}/`).toString();\n } catch {\n return \"\";\n }\n}\n\n/** Flatten one-or-many JSON-LD into a node array, dropping empty objects. */\nfunction jsonLdNodes(\n schema: Record<string, unknown> | Array<Record<string, unknown>> | undefined,\n): Array<Record<string, unknown>> {\n if (!schema) return [];\n const arr = Array.isArray(schema) ? schema : [schema];\n const out: Array<Record<string, unknown>> = [];\n for (const node of arr) {\n if (!node || Object.keys(node).length === 0) continue;\n const graph = node[\"@graph\"];\n if (Array.isArray(graph)) {\n for (const child of graph) {\n if (child && typeof child === \"object\" && !Array.isArray(child) && Object.keys(child).length > 0) {\n out.push(child as Record<string, unknown>);\n }\n }\n continue;\n }\n out.push(node);\n }\n return out;\n}\n\n/*\n * Structured data for a page whose `metaJson.schema` is empty. A port of the page path of\n * `resolveStructuredData` in the backend (src/lib/seo/structured-data.ts, called from\n * `pageStructuredData` in src/lib/sites/render-page.ts), with `site.include: false` as the\n * Delivery API calls it: the site-wide schema is emitted separately, from `siteSchema`.\n * A page has no field data, so only the properties the page's own meta supplies are filled.\n * Keep in step with that file; the parity test imports it.\n */\nconst SCHEMA_TYPE_CHOICES = [\n \"WebPage\",\n \"AboutPage\",\n \"ContactPage\",\n \"CollectionPage\",\n \"Blog\",\n \"BlogPosting\",\n \"Article\",\n \"Product\",\n \"FAQPage\",\n \"Event\",\n \"Organization\",\n \"LocalBusiness\",\n] as const;\n\ntype PageProps = { name: string; description: string; image: string; url: string };\n\n/** Drop \"\", null and undefined so a node never carries a blank. */\nfunction compact(node: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(Object.entries(node).filter(([, value]) => value != null && value !== \"\"));\n}\n\nfunction schemaNode(type: string, { name, description, image, url }: PageProps): Record<string, unknown> {\n switch (type) {\n case \"BlogPosting\":\n case \"Article\":\n return compact({ \"@type\": type, headline: name, description, image, url, mainEntityOfPage: url });\n case \"Product\":\n case \"Event\":\n return compact({ \"@type\": type, name, description, image, url });\n case \"FAQPage\":\n case \"WebSite\":\n return compact({ \"@type\": type, name, description, url });\n case \"Organization\":\n return compact({ \"@type\": type, name, description, url, logo: image });\n default:\n // WebPage, AboutPage, ContactPage, CollectionPage, Blog, LocalBusiness\n return compact({ \"@type\": type, name, description, url, image });\n }\n}\n\n/** The page's own JSON-LD nodes: Custom > explicit `schemaType` > Automatic. */\nfunction pageJsonLd(\n page: SeoInput,\n meta: PageMetaJson,\n canonical: string,\n siteSchema: SiteSeoDefaults[\"siteSchema\"],\n): Array<Record<string, unknown>> {\n const custom = jsonLdNodes(meta.schema);\n if (custom.length > 0) return custom;\n const props: PageProps = {\n name: (page.metaTitle || page.title || \"\").trim(),\n description: (page.metaDescription || \"\").trim(),\n image: (meta.og?.image || \"\").trim(),\n url: canonical.trim(),\n };\n const schemaType = (meta as { schemaType?: unknown }).schemaType;\n if (typeof schemaType === \"string\" && (SCHEMA_TYPE_CHOICES as readonly string[]).includes(schemaType)) {\n return [schemaNode(schemaType, props)];\n }\n if (page.slug === \"home\") {\n // The home page IS the WebSite, unless the site-wide schema already describes it.\n return jsonLdNodes(siteSchema).length > 0 ? [] : [schemaNode(\"WebSite\", props)];\n }\n return [schemaNode(\"WebPage\", props)];\n}\n\n/**\n * A Delivery API entry as a `resolveSeo` input. The server already resolved the collection\n * template's `{{field}}` tokens for this entry and merged the entry's own SEO over it, so this is\n * a reshape, not a resolver:\n *\n * const entry = await bcms.getEntry(\"astro\");\n * const seo = resolveSeo(entrySeoInput(entry), siteDefaults, { siteUrl, path: `/tags/${entry.slug}` });\n * // seo.jsonLd holds the template's schema, resolved for this entry\n */\nexport function entrySeoInput(entry: { slug: string; seo?: DeliveryEntrySeo | null }): SeoInput {\n const { metaTitle, metaDescription, noindex, ...metaJson } = entry.seo ?? {};\n return {\n title: metaTitle || entry.slug,\n metaTitle: metaTitle ?? null,\n metaDescription: metaDescription ?? null,\n metaJson,\n noindex: noindex === true,\n };\n}\n\n/** Where this page lives, so a canonical can be derived when the page sets none. */\nexport interface SeoLocation {\n /** Absolute site origin, e.g. `https://acme.com`. Trailing slash optional. */\n siteUrl?: string;\n /** This page's path, e.g. `/about` or `/`. */\n path?: string;\n}\n\n/**\n * Merge a page's own SEO over the site defaults: page meta wins, then site defaults, then\n * sensible fallbacks.\n *\n * The CANONICAL is the one field that cannot be resolved from content alone, and getting this\n * wrong is silent. The hosted renderer (`mergeSeoMeta` in the backend) falls back to a canonical\n * it derives from the request's own origin and path — headless consumers have no request, so\n * they must pass `location`. Without it this returns \"\" and the caller ships no\n * `<link rel=\"canonical\">` and no `og:url`, exactly as it did before `location` existed.\n */\nexport function resolveSeo(\n page: SeoInput,\n defaults: SiteSeoDefaults = {},\n location: SeoLocation = {},\n): ResolvedSeo {\n const meta = page.metaJson ?? {};\n const title = page.metaTitle || page.title || \"\";\n const description = page.metaDescription || defaults.metaDescription || \"\";\n const ogImage = meta.og?.image || defaults.og?.image || defaults.ogImage || \"\";\n const ogTitle = meta.og?.title || title;\n const ogDescription = meta.og?.description || description;\n const twImage = meta.twitter?.image || ogImage;\n const canonical = meta.canonical || derivedCanonical(location);\n return {\n title,\n description,\n canonical,\n og: {\n title: ogTitle,\n description: ogDescription,\n type: meta.og?.type || defaults.og?.type || \"website\",\n image: ogImage,\n url: canonical,\n },\n twitter: {\n card: meta.twitter?.card || defaults.twitter?.card || (twImage ? \"summary_large_image\" : \"summary\"),\n title: meta.twitter?.title || ogTitle,\n description: meta.twitter?.description || ogDescription,\n image: twImage,\n site: defaults.twitterHandle || \"\",\n },\n jsonLd: [...jsonLdNodes(defaults.siteSchema), ...pageJsonLd(page, meta, canonical, defaults.siteSchema)],\n noindex: page.noindex === true,\n robots: { index: page.noindex !== true, follow: true },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/sdk",
3
- "version": "1.32.0",
3
+ "version": "1.34.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -15,6 +15,26 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js",
17
17
  "default": "./dist/index.js"
18
+ },
19
+ "./draft": {
20
+ "types": "./dist/draft.d.ts",
21
+ "import": "./dist/draft.js",
22
+ "default": "./dist/draft.js"
23
+ },
24
+ "./config": {
25
+ "types": "./dist/config.d.ts",
26
+ "import": "./dist/config.js",
27
+ "default": "./dist/config.js"
28
+ },
29
+ "./seo": {
30
+ "types": "./dist/seo.d.ts",
31
+ "import": "./dist/seo.js",
32
+ "default": "./dist/seo.js"
33
+ },
34
+ "./read": {
35
+ "types": "./dist/read.d.ts",
36
+ "import": "./dist/read.js",
37
+ "default": "./dist/read.js"
18
38
  }
19
39
  },
20
40
  "dependencies": {