@businessdash/sdk 0.9.60

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +899 -0
  3. package/dist/adapters-revalidate.d.ts +44 -0
  4. package/dist/adapters-revalidate.d.ts.map +1 -0
  5. package/dist/adapters-revalidate.js +68 -0
  6. package/dist/adapters-revalidate.js.map +1 -0
  7. package/dist/alpha.d.ts +21 -0
  8. package/dist/alpha.d.ts.map +1 -0
  9. package/dist/alpha.js +21 -0
  10. package/dist/alpha.js.map +1 -0
  11. package/dist/analytics-core.d.ts +34 -0
  12. package/dist/analytics-core.d.ts.map +1 -0
  13. package/dist/analytics-core.js +138 -0
  14. package/dist/analytics-core.js.map +1 -0
  15. package/dist/analytics-server.d.ts +54 -0
  16. package/dist/analytics-server.d.ts.map +1 -0
  17. package/dist/analytics-server.js +78 -0
  18. package/dist/analytics-server.js.map +1 -0
  19. package/dist/attribution-logo.d.ts +2 -0
  20. package/dist/attribution-logo.d.ts.map +1 -0
  21. package/dist/attribution-logo.js +5 -0
  22. package/dist/attribution-logo.js.map +1 -0
  23. package/dist/auth-handler.d.ts +92 -0
  24. package/dist/auth-handler.d.ts.map +1 -0
  25. package/dist/auth-handler.js +277 -0
  26. package/dist/auth-handler.js.map +1 -0
  27. package/dist/biab-forms.css +484 -0
  28. package/dist/cli.d.ts +46 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +1824 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/client.d.ts +1681 -0
  33. package/dist/client.d.ts.map +1 -0
  34. package/dist/client.js +2845 -0
  35. package/dist/client.js.map +1 -0
  36. package/dist/collections.d.ts +13 -0
  37. package/dist/collections.d.ts.map +1 -0
  38. package/dist/collections.js +13 -0
  39. package/dist/collections.js.map +1 -0
  40. package/dist/contracts.d.ts +14609 -0
  41. package/dist/contracts.d.ts.map +1 -0
  42. package/dist/contracts.js +2428 -0
  43. package/dist/contracts.js.map +1 -0
  44. package/dist/data-model-schema.d.ts +962 -0
  45. package/dist/data-model-schema.d.ts.map +1 -0
  46. package/dist/data-model-schema.js +2295 -0
  47. package/dist/data-model-schema.js.map +1 -0
  48. package/dist/distribution.d.ts +38 -0
  49. package/dist/distribution.d.ts.map +1 -0
  50. package/dist/distribution.js +61 -0
  51. package/dist/distribution.js.map +1 -0
  52. package/dist/element.d.ts +121 -0
  53. package/dist/element.d.ts.map +1 -0
  54. package/dist/element.js +276 -0
  55. package/dist/element.js.map +1 -0
  56. package/dist/enums.d.ts +25 -0
  57. package/dist/enums.d.ts.map +1 -0
  58. package/dist/enums.js +38 -0
  59. package/dist/enums.js.map +1 -0
  60. package/dist/errors.d.ts +195 -0
  61. package/dist/errors.d.ts.map +1 -0
  62. package/dist/errors.js +188 -0
  63. package/dist/errors.js.map +1 -0
  64. package/dist/forms-core/autocomplete.d.ts +37 -0
  65. package/dist/forms-core/autocomplete.d.ts.map +1 -0
  66. package/dist/forms-core/autocomplete.js +82 -0
  67. package/dist/forms-core/autocomplete.js.map +1 -0
  68. package/dist/forms-core/business-function.d.ts +61 -0
  69. package/dist/forms-core/business-function.d.ts.map +1 -0
  70. package/dist/forms-core/business-function.js +149 -0
  71. package/dist/forms-core/business-function.js.map +1 -0
  72. package/dist/forms-core/calculation.d.ts +72 -0
  73. package/dist/forms-core/calculation.d.ts.map +1 -0
  74. package/dist/forms-core/calculation.js +103 -0
  75. package/dist/forms-core/calculation.js.map +1 -0
  76. package/dist/forms-core/controller.d.ts +249 -0
  77. package/dist/forms-core/controller.d.ts.map +1 -0
  78. package/dist/forms-core/controller.js +590 -0
  79. package/dist/forms-core/controller.js.map +1 -0
  80. package/dist/forms-core/elements.d.ts +44 -0
  81. package/dist/forms-core/elements.d.ts.map +1 -0
  82. package/dist/forms-core/elements.js +68 -0
  83. package/dist/forms-core/elements.js.map +1 -0
  84. package/dist/forms-core/email-domains.d.ts +10 -0
  85. package/dist/forms-core/email-domains.d.ts.map +1 -0
  86. package/dist/forms-core/email-domains.js +69 -0
  87. package/dist/forms-core/email-domains.js.map +1 -0
  88. package/dist/forms-core/file-rules.d.ts +51 -0
  89. package/dist/forms-core/file-rules.d.ts.map +1 -0
  90. package/dist/forms-core/file-rules.js +105 -0
  91. package/dist/forms-core/file-rules.js.map +1 -0
  92. package/dist/forms-core/formula.d.ts +114 -0
  93. package/dist/forms-core/formula.d.ts.map +1 -0
  94. package/dist/forms-core/formula.js +1170 -0
  95. package/dist/forms-core/formula.js.map +1 -0
  96. package/dist/forms-core/index.d.ts +24 -0
  97. package/dist/forms-core/index.d.ts.map +1 -0
  98. package/dist/forms-core/index.js +28 -0
  99. package/dist/forms-core/index.js.map +1 -0
  100. package/dist/forms-core/units.d.ts +49 -0
  101. package/dist/forms-core/units.d.ts.map +1 -0
  102. package/dist/forms-core/units.js +255 -0
  103. package/dist/forms-core/units.js.map +1 -0
  104. package/dist/forms-core/us-states.d.ts +14 -0
  105. package/dist/forms-core/us-states.d.ts.map +1 -0
  106. package/dist/forms-core/us-states.js +66 -0
  107. package/dist/forms-core/us-states.js.map +1 -0
  108. package/dist/forms-core/validation.d.ts +266 -0
  109. package/dist/forms-core/validation.d.ts.map +1 -0
  110. package/dist/forms-core/validation.js +850 -0
  111. package/dist/forms-core/validation.js.map +1 -0
  112. package/dist/forms.d.ts +19 -0
  113. package/dist/forms.d.ts.map +1 -0
  114. package/dist/forms.js +29 -0
  115. package/dist/forms.js.map +1 -0
  116. package/dist/gallery-types.d.ts +32 -0
  117. package/dist/gallery-types.d.ts.map +1 -0
  118. package/dist/gallery-types.js +2 -0
  119. package/dist/gallery-types.js.map +1 -0
  120. package/dist/graphql.d.ts +97 -0
  121. package/dist/graphql.d.ts.map +1 -0
  122. package/dist/graphql.js +212 -0
  123. package/dist/graphql.js.map +1 -0
  124. package/dist/index.cjs +19448 -0
  125. package/dist/index.d.cts +19 -0
  126. package/dist/index.d.mts +19 -0
  127. package/dist/index.d.ts +19 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +20 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/marketing-schema.d.ts +216 -0
  132. package/dist/marketing-schema.d.ts.map +1 -0
  133. package/dist/marketing-schema.js +200 -0
  134. package/dist/marketing-schema.js.map +1 -0
  135. package/dist/mcp.d.ts +42 -0
  136. package/dist/mcp.d.ts.map +1 -0
  137. package/dist/mcp.js +87 -0
  138. package/dist/mcp.js.map +1 -0
  139. package/dist/native.d.ts +45 -0
  140. package/dist/native.d.ts.map +1 -0
  141. package/dist/native.js +43 -0
  142. package/dist/native.js.map +1 -0
  143. package/dist/next-revalidate.d.ts +43 -0
  144. package/dist/next-revalidate.d.ts.map +1 -0
  145. package/dist/next-revalidate.js +84 -0
  146. package/dist/next-revalidate.js.map +1 -0
  147. package/dist/parallel-pages.d.ts +102 -0
  148. package/dist/parallel-pages.d.ts.map +1 -0
  149. package/dist/parallel-pages.js +108 -0
  150. package/dist/parallel-pages.js.map +1 -0
  151. package/dist/payment-icons.d.ts +66 -0
  152. package/dist/payment-icons.d.ts.map +1 -0
  153. package/dist/payment-icons.js +160 -0
  154. package/dist/payment-icons.js.map +1 -0
  155. package/dist/proxy.d.ts +67 -0
  156. package/dist/proxy.d.ts.map +1 -0
  157. package/dist/proxy.js +97 -0
  158. package/dist/proxy.js.map +1 -0
  159. package/dist/react-analytics.d.ts +39 -0
  160. package/dist/react-analytics.d.ts.map +1 -0
  161. package/dist/react-analytics.js +59 -0
  162. package/dist/react-analytics.js.map +1 -0
  163. package/dist/react-attribution.d.ts +99 -0
  164. package/dist/react-attribution.d.ts.map +1 -0
  165. package/dist/react-attribution.js +52 -0
  166. package/dist/react-attribution.js.map +1 -0
  167. package/dist/react-marketing.d.ts +181 -0
  168. package/dist/react-marketing.d.ts.map +1 -0
  169. package/dist/react-marketing.js +215 -0
  170. package/dist/react-marketing.js.map +1 -0
  171. package/dist/react.d.ts +727 -0
  172. package/dist/react.d.ts.map +1 -0
  173. package/dist/react.js +2418 -0
  174. package/dist/react.js.map +1 -0
  175. package/dist/reserved-paths.d.ts +24 -0
  176. package/dist/reserved-paths.d.ts.map +1 -0
  177. package/dist/reserved-paths.js +60 -0
  178. package/dist/reserved-paths.js.map +1 -0
  179. package/dist/revalidate-verify.d.ts +30 -0
  180. package/dist/revalidate-verify.d.ts.map +1 -0
  181. package/dist/revalidate-verify.js +74 -0
  182. package/dist/revalidate-verify.js.map +1 -0
  183. package/dist/sdk.d.ts +678 -0
  184. package/dist/sdk.d.ts.map +1 -0
  185. package/dist/sdk.js +801 -0
  186. package/dist/sdk.js.map +1 -0
  187. package/dist/seo.d.ts +218 -0
  188. package/dist/seo.d.ts.map +1 -0
  189. package/dist/seo.js +309 -0
  190. package/dist/seo.js.map +1 -0
  191. package/dist/socials.d.ts +47 -0
  192. package/dist/socials.d.ts.map +1 -0
  193. package/dist/socials.js +172 -0
  194. package/dist/socials.js.map +1 -0
  195. package/dist/static.d.ts +14 -0
  196. package/dist/static.d.ts.map +1 -0
  197. package/dist/static.js +14 -0
  198. package/dist/static.js.map +1 -0
  199. package/dist/url-pattern.d.ts +47 -0
  200. package/dist/url-pattern.d.ts.map +1 -0
  201. package/dist/url-pattern.js +124 -0
  202. package/dist/url-pattern.js.map +1 -0
  203. package/dist/vanilla.d.ts +77 -0
  204. package/dist/vanilla.d.ts.map +1 -0
  205. package/dist/vanilla.js +903 -0
  206. package/dist/vanilla.js.map +1 -0
  207. package/openapi.json +13779 -0
  208. package/package.json +204 -0
  209. package/src/adapters-revalidate.ts +88 -0
  210. package/src/alpha.ts +89 -0
  211. package/src/analytics-core.ts +175 -0
  212. package/src/analytics-server.ts +99 -0
  213. package/src/angular/biab-form.component.ts +1443 -0
  214. package/src/angular/index.ts +26 -0
  215. package/src/attribution-logo.ts +5 -0
  216. package/src/auth-handler.ts +352 -0
  217. package/src/cli.ts +2517 -0
  218. package/src/client.ts +3578 -0
  219. package/src/collections.ts +101 -0
  220. package/src/contracts.ts +3390 -0
  221. package/src/data-model-schema.ts +4042 -0
  222. package/src/distribution.ts +73 -0
  223. package/src/element.ts +321 -0
  224. package/src/enums.ts +65 -0
  225. package/src/errors.ts +338 -0
  226. package/src/forms-core/autocomplete.ts +97 -0
  227. package/src/forms-core/business-function.ts +209 -0
  228. package/src/forms-core/calculation.ts +167 -0
  229. package/src/forms-core/controller.ts +922 -0
  230. package/src/forms-core/elements.ts +86 -0
  231. package/src/forms-core/email-domains.ts +68 -0
  232. package/src/forms-core/file-rules.ts +123 -0
  233. package/src/forms-core/formula.ts +1339 -0
  234. package/src/forms-core/index.ts +122 -0
  235. package/src/forms-core/units.ts +308 -0
  236. package/src/forms-core/us-states.ts +68 -0
  237. package/src/forms-core/validation.ts +1037 -0
  238. package/src/forms.ts +63 -0
  239. package/src/gallery-types.ts +31 -0
  240. package/src/graphql.ts +295 -0
  241. package/src/index.ts +505 -0
  242. package/src/marketing-schema.ts +395 -0
  243. package/src/mcp.ts +108 -0
  244. package/src/native.ts +73 -0
  245. package/src/next-revalidate.ts +124 -0
  246. package/src/parallel-pages.ts +192 -0
  247. package/src/payment-icons.ts +205 -0
  248. package/src/proxy.ts +173 -0
  249. package/src/qwik.tsx +1234 -0
  250. package/src/react-analytics.tsx +76 -0
  251. package/src/react-attribution.tsx +192 -0
  252. package/src/react-marketing.tsx +338 -0
  253. package/src/react.tsx +5495 -0
  254. package/src/reserved-paths.ts +65 -0
  255. package/src/revalidate-verify.ts +98 -0
  256. package/src/sdk.ts +1671 -0
  257. package/src/seo.ts +488 -0
  258. package/src/socials.ts +230 -0
  259. package/src/solid.tsx +1466 -0
  260. package/src/static.ts +21 -0
  261. package/src/styles/biab-forms.css +484 -0
  262. package/src/svelte/BiabForm.svelte +1069 -0
  263. package/src/svelte/context.ts +57 -0
  264. package/src/svelte/index.ts +79 -0
  265. package/src/url-pattern.ts +153 -0
  266. package/src/vanilla.ts +1290 -0
  267. package/src/vue/BiabForm.vue +1538 -0
  268. package/src/vue/index.ts +119 -0
package/src/seo.ts ADDED
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Typed JSON-LD / schema.org builders for the marketing-page flow.
3
+ *
4
+ * Mirror of `UrbanAirNYC/src/lib/schema.ts` lifted up into the SDK so
5
+ * every consumer can compose AIEO-ready structured data with the same
6
+ * helpers. Each builder returns a plain JSON-LD node — the SDK's React
7
+ * `<MarketingHead>` (or the consumer's own `<head>` renderer) drops them
8
+ * into `<script type="application/ld+json">` tags.
9
+ *
10
+ * Builders accept the bits a consumer typically has at render time:
11
+ * `siteUrl` for the `@id` graph, plus the per-node payload. They never
12
+ * pull from the network — they're pure data shapers.
13
+ */
14
+
15
+ const SCHEMA_CTX = "https://schema.org";
16
+
17
+ export type JsonLdNode = Record<string, unknown>;
18
+
19
+ // ── Organization / business ─────────────────────────────────────────
20
+
21
+ export type LocalBusinessInput = {
22
+ siteUrl: string;
23
+ /** Override the default `${siteUrl}#business` identity. */
24
+ id?: string;
25
+ /** Override the public URL when this node represents a branch page. */
26
+ url?: string;
27
+ /** Link this branch to its parent Organization node. */
28
+ parentOrganizationId?: string;
29
+ /** schema.org `@type` — defaults to `LocalBusiness`. Pass e.g. `"HVACBusiness"`,
30
+ * `"GeneralContractor"`, `"Restaurant"` to claim a narrower category. */
31
+ businessType?: string;
32
+ name: string;
33
+ alternateName?: string;
34
+ description?: string;
35
+ telephone?: string;
36
+ email?: string;
37
+ priceRange?: string;
38
+ address?: {
39
+ streetAddress?: string;
40
+ addressLocality?: string;
41
+ addressRegion?: string;
42
+ postalCode?: string;
43
+ addressCountry?: string;
44
+ };
45
+ geo?: {
46
+ latitude: number;
47
+ longitude: number;
48
+ };
49
+ hasMap?: string;
50
+ areaServed?: Array<
51
+ | string
52
+ | {
53
+ name: string;
54
+ type?: "City" | "State" | "Country" | "AdministrativeArea";
55
+ }
56
+ >;
57
+ openingHours?: Array<{
58
+ dayOfWeek: string[];
59
+ opens: string;
60
+ closes: string;
61
+ }>;
62
+ knowsAbout?: string[];
63
+ aggregateRating?: {
64
+ ratingValue: string | number;
65
+ reviewCount: string | number;
66
+ bestRating?: string | number;
67
+ };
68
+ contactPoint?: Array<{
69
+ telephone: string;
70
+ contactType: string;
71
+ availableLanguage?: string[];
72
+ areaServed?: string;
73
+ }>;
74
+ };
75
+
76
+ export function localBusiness(input: LocalBusinessInput): JsonLdNode {
77
+ const node: JsonLdNode = {
78
+ "@context": SCHEMA_CTX,
79
+ "@type": input.businessType ?? "LocalBusiness",
80
+ "@id": input.id ?? `${input.siteUrl}#business`,
81
+ name: input.name,
82
+ url: input.url ?? input.siteUrl,
83
+ };
84
+ if (input.parentOrganizationId) {
85
+ node["parentOrganization"] = { "@id": input.parentOrganizationId };
86
+ }
87
+ if (input.alternateName) node["alternateName"] = input.alternateName;
88
+ if (input.description) node["description"] = input.description;
89
+ if (input.telephone) node["telephone"] = input.telephone;
90
+ if (input.email) node["email"] = input.email;
91
+ if (input.priceRange) node["priceRange"] = input.priceRange;
92
+ if (input.address) {
93
+ node["address"] = { "@type": "PostalAddress", ...input.address };
94
+ }
95
+ if (input.geo) {
96
+ node["geo"] = { "@type": "GeoCoordinates", ...input.geo };
97
+ }
98
+ if (input.hasMap) node["hasMap"] = input.hasMap;
99
+ if (input.areaServed?.length) {
100
+ node["areaServed"] = input.areaServed.map((a) =>
101
+ typeof a === "string"
102
+ ? { "@type": "City", name: a }
103
+ : { "@type": a.type ?? "AdministrativeArea", name: a.name },
104
+ );
105
+ }
106
+ if (input.openingHours?.length) {
107
+ node["openingHoursSpecification"] = input.openingHours.map((h) => ({
108
+ "@type": "OpeningHoursSpecification",
109
+ dayOfWeek: h.dayOfWeek,
110
+ opens: h.opens,
111
+ closes: h.closes,
112
+ }));
113
+ }
114
+ if (input.knowsAbout?.length) node["knowsAbout"] = [...input.knowsAbout];
115
+ if (input.aggregateRating) {
116
+ node["aggregateRating"] = {
117
+ "@type": "AggregateRating",
118
+ ratingValue: String(input.aggregateRating.ratingValue),
119
+ reviewCount: String(input.aggregateRating.reviewCount),
120
+ bestRating: String(input.aggregateRating.bestRating ?? "5"),
121
+ };
122
+ }
123
+ if (input.contactPoint?.length) {
124
+ node["contactPoint"] = input.contactPoint.map((c) => ({
125
+ "@type": "ContactPoint",
126
+ ...c,
127
+ }));
128
+ }
129
+ return node;
130
+ }
131
+
132
+ export type LocalBusinessLocationInput = Omit<
133
+ LocalBusinessInput,
134
+ "siteUrl" | "id" | "parentOrganizationId"
135
+ > & {
136
+ /** Stable URL/JSON-LD key for this branch. */
137
+ slug: string;
138
+ /** Set false for warehouses or internal offices that must not be published. */
139
+ isPublic?: boolean;
140
+ };
141
+
142
+ export type LocalBusinessLocationsInput = {
143
+ siteUrl: string;
144
+ organizationId?: string;
145
+ locations: LocalBusinessLocationInput[];
146
+ };
147
+
148
+ /**
149
+ * Build one independently addressable LocalBusiness node per public location.
150
+ * Search engines should receive distinct nodes rather than an address array on
151
+ * one LocalBusiness.
152
+ */
153
+ export function localBusinessLocations(
154
+ input: LocalBusinessLocationsInput,
155
+ ): JsonLdNode[] {
156
+ const organizationId =
157
+ input.organizationId ?? `${input.siteUrl}#organization`;
158
+ return input.locations
159
+ .filter((location) => location.isPublic !== false)
160
+ .map(({ slug, isPublic: _isPublic, ...location }) =>
161
+ localBusiness({
162
+ ...location,
163
+ siteUrl: input.siteUrl,
164
+ id: `${input.siteUrl}#location-${slug}`,
165
+ parentOrganizationId: organizationId,
166
+ }),
167
+ );
168
+ }
169
+
170
+ export type OrganizationInput = {
171
+ siteUrl: string;
172
+ name: string;
173
+ logoUrl?: string;
174
+ sameAs?: string[];
175
+ founder?: string;
176
+ foundingDate?: string;
177
+ };
178
+
179
+ export function organization(input: OrganizationInput): JsonLdNode {
180
+ const node: JsonLdNode = {
181
+ "@context": SCHEMA_CTX,
182
+ "@type": "Organization",
183
+ "@id": `${input.siteUrl}#organization`,
184
+ name: input.name,
185
+ url: input.siteUrl,
186
+ };
187
+ if (input.logoUrl) node["logo"] = input.logoUrl;
188
+ if (input.sameAs?.length) node["sameAs"] = [...input.sameAs];
189
+ if (input.founder) node["founder"] = input.founder;
190
+ if (input.foundingDate) node["foundingDate"] = input.foundingDate;
191
+ return node;
192
+ }
193
+
194
+ // ── Website ─────────────────────────────────────────────────────────
195
+
196
+ export type WebsiteInput = {
197
+ siteUrl: string;
198
+ name: string;
199
+ description?: string;
200
+ publisherId?: string;
201
+ searchAction?: { target: string; queryInput: string };
202
+ };
203
+
204
+ export function website(input: WebsiteInput): JsonLdNode {
205
+ const node: JsonLdNode = {
206
+ "@context": SCHEMA_CTX,
207
+ "@type": "WebSite",
208
+ "@id": `${input.siteUrl}#website`,
209
+ url: input.siteUrl,
210
+ name: input.name,
211
+ };
212
+ if (input.description) node["description"] = input.description;
213
+ if (input.publisherId) node["publisher"] = { "@id": input.publisherId };
214
+ if (input.searchAction) {
215
+ node["potentialAction"] = {
216
+ "@type": "SearchAction",
217
+ target: input.searchAction.target,
218
+ "query-input": input.searchAction.queryInput,
219
+ };
220
+ }
221
+ return node;
222
+ }
223
+
224
+ // ── Service ─────────────────────────────────────────────────────────
225
+
226
+ export type ServiceInput = {
227
+ siteUrl: string;
228
+ slug: string;
229
+ title: string;
230
+ description: string;
231
+ audience?: string;
232
+ areaServed?: string[];
233
+ providerId?: string;
234
+ };
235
+
236
+ export function service(input: ServiceInput): JsonLdNode {
237
+ const node: JsonLdNode = {
238
+ "@context": SCHEMA_CTX,
239
+ "@type": "Service",
240
+ serviceType: input.title,
241
+ name: input.title,
242
+ description: input.description,
243
+ url: `${input.siteUrl}/services#${input.slug}`,
244
+ provider: { "@id": input.providerId ?? `${input.siteUrl}#business` },
245
+ };
246
+ if (input.areaServed?.length) node["areaServed"] = [...input.areaServed];
247
+ if (input.audience) {
248
+ node["audience"] = { "@type": "Audience", audienceType: input.audience };
249
+ }
250
+ return node;
251
+ }
252
+
253
+ // ── Product ─────────────────────────────────────────────────────────
254
+
255
+ export type ProductInput = {
256
+ /** Canonical product page URL. */
257
+ url: string;
258
+ name: string;
259
+ description?: string;
260
+ image?: string | string[];
261
+ brand?: string;
262
+ sku?: string;
263
+ offer?: {
264
+ /** Integer cents — formatted to schema.org's decimal string here. */
265
+ priceCents: number;
266
+ /** ISO-4217, defaults to USD. */
267
+ currency?: string;
268
+ availability?: "InStock" | "OutOfStock" | "PreOrder";
269
+ };
270
+ sellerId?: string;
271
+ };
272
+
273
+ /**
274
+ * schema.org Product (+ nested Offer) — what shopping surfaces and answer
275
+ * engines read off a product page. Pair with the AI Distribution product
276
+ * feed: the feed gets your product INTO the engines' catalogs, this
277
+ * markup is what their crawlers verify on the landing page itself.
278
+ */
279
+ export function product(input: ProductInput): JsonLdNode {
280
+ const node: JsonLdNode = {
281
+ "@context": SCHEMA_CTX,
282
+ "@type": "Product",
283
+ name: input.name,
284
+ url: input.url,
285
+ };
286
+ if (input.description) node["description"] = input.description;
287
+ if (input.image) {
288
+ node["image"] = Array.isArray(input.image) ? [...input.image] : input.image;
289
+ }
290
+ if (input.brand) node["brand"] = { "@type": "Brand", name: input.brand };
291
+ if (input.sku) node["sku"] = input.sku;
292
+ if (input.offer) {
293
+ const offer: JsonLdNode = {
294
+ "@type": "Offer",
295
+ price: (input.offer.priceCents / 100).toFixed(2),
296
+ priceCurrency: input.offer.currency ?? "USD",
297
+ availability: `${SCHEMA_CTX}/${input.offer.availability ?? "InStock"}`,
298
+ url: input.url,
299
+ };
300
+ if (input.sellerId) offer["seller"] = { "@id": input.sellerId };
301
+ node["offers"] = offer;
302
+ }
303
+ return node;
304
+ }
305
+
306
+ // ── FAQ ─────────────────────────────────────────────────────────────
307
+
308
+ export type FaqInput = Array<{ question: string; answer: string }>;
309
+
310
+ export function faq(items: FaqInput): JsonLdNode {
311
+ return {
312
+ "@context": SCHEMA_CTX,
313
+ "@type": "FAQPage",
314
+ mainEntity: items.map(({ question, answer }) => ({
315
+ "@type": "Question",
316
+ name: question,
317
+ acceptedAnswer: { "@type": "Answer", text: answer },
318
+ })),
319
+ };
320
+ }
321
+
322
+ // ── Breadcrumb ──────────────────────────────────────────────────────
323
+
324
+ export type BreadcrumbInput = {
325
+ siteUrl: string;
326
+ items: Array<{ name: string; url: string }>;
327
+ };
328
+
329
+ export function breadcrumb(input: BreadcrumbInput): JsonLdNode {
330
+ return {
331
+ "@context": SCHEMA_CTX,
332
+ "@type": "BreadcrumbList",
333
+ itemListElement: input.items.map((item, i) => ({
334
+ "@type": "ListItem",
335
+ position: i + 1,
336
+ name: item.name,
337
+ item: item.url.startsWith("http")
338
+ ? item.url
339
+ : `${input.siteUrl}${item.url}`,
340
+ })),
341
+ };
342
+ }
343
+
344
+ // ── Article / blog post ─────────────────────────────────────────────
345
+
346
+ export type ArticleInput = {
347
+ siteUrl: string;
348
+ headline: string;
349
+ description: string;
350
+ slug: string;
351
+ pubDate: Date | string;
352
+ updatedDate?: Date | string;
353
+ authorName?: string;
354
+ authorOrgId?: string;
355
+ publisherId?: string;
356
+ heroImageUrl?: string;
357
+ };
358
+
359
+ function toIsoDate(d: Date | string): string {
360
+ if (typeof d === "string") return d;
361
+ return d.toISOString();
362
+ }
363
+
364
+ export function article(input: ArticleInput): JsonLdNode {
365
+ const node: JsonLdNode = {
366
+ "@context": SCHEMA_CTX,
367
+ "@type": "BlogPosting",
368
+ headline: input.headline,
369
+ description: input.description,
370
+ datePublished: toIsoDate(input.pubDate),
371
+ dateModified: toIsoDate(input.updatedDate ?? input.pubDate),
372
+ mainEntityOfPage: `${input.siteUrl}/blog/${input.slug}`,
373
+ author: input.authorOrgId
374
+ ? { "@id": input.authorOrgId }
375
+ : { "@type": "Person", name: input.authorName ?? "Editorial team" },
376
+ publisher: { "@id": input.publisherId ?? `${input.siteUrl}#business` },
377
+ };
378
+ if (input.heroImageUrl) node["image"] = input.heroImageUrl;
379
+ return node;
380
+ }
381
+
382
+ // ── Review ──────────────────────────────────────────────────────────
383
+
384
+ export type ReviewInput = {
385
+ siteUrl: string;
386
+ body: string;
387
+ authorName: string;
388
+ rating?: number;
389
+ location?: string;
390
+ reviewedId?: string;
391
+ };
392
+
393
+ export function review(input: ReviewInput): JsonLdNode {
394
+ const node: JsonLdNode = {
395
+ "@context": SCHEMA_CTX,
396
+ "@type": "Review",
397
+ reviewRating: {
398
+ "@type": "Rating",
399
+ ratingValue: String(input.rating ?? 5),
400
+ bestRating: "5",
401
+ },
402
+ author: { "@type": "Person", name: input.authorName },
403
+ reviewBody: input.body,
404
+ itemReviewed: { "@id": input.reviewedId ?? `${input.siteUrl}#business` },
405
+ };
406
+ if (input.location) node["locationCreated"] = input.location;
407
+ return node;
408
+ }
409
+
410
+ // ── Product (for the Pricing managed-data section's structured data) ─
411
+
412
+ export type ProductOfferInput = {
413
+ siteUrl: string;
414
+ slug: string;
415
+ name: string;
416
+ description?: string;
417
+ priceCurrency: string;
418
+ price: string | number;
419
+ availability?: "InStock" | "OutOfStock" | "PreOrder";
420
+ brandName?: string;
421
+ imageUrl?: string;
422
+ };
423
+
424
+ export function productOffer(input: ProductOfferInput): JsonLdNode {
425
+ const node: JsonLdNode = {
426
+ "@context": SCHEMA_CTX,
427
+ "@type": "Product",
428
+ name: input.name,
429
+ url: `${input.siteUrl}/products/${input.slug}`,
430
+ offers: {
431
+ "@type": "Offer",
432
+ priceCurrency: input.priceCurrency,
433
+ price: String(input.price),
434
+ availability: `https://schema.org/${input.availability ?? "InStock"}`,
435
+ },
436
+ };
437
+ if (input.description) node["description"] = input.description;
438
+ if (input.brandName)
439
+ node["brand"] = { "@type": "Brand", name: input.brandName };
440
+ if (input.imageUrl) node["image"] = input.imageUrl;
441
+ return node;
442
+ }
443
+
444
+ // ── Renderers ───────────────────────────────────────────────────────
445
+
446
+ /**
447
+ * Render an array of JSON-LD nodes as serialized `<script>` tags.
448
+ * Returns the raw HTML strings — the caller chooses how to inject them
449
+ * (Next's `next/script`, plain `dangerouslySetInnerHTML`, Astro's
450
+ * `set:html`, etc.). One block per node so Google indexes them
451
+ * independently.
452
+ *
453
+ * The escaping rule is the same Google recommends: serialize the JSON
454
+ * normally, then replace `</` with `<\/` so an angle-bracket-bearing
455
+ * string can't close the surrounding `<script>` tag.
456
+ */
457
+ export function renderJsonLdNodes(
458
+ nodes: JsonLdNode[],
459
+ ): { type: string; html: string }[] {
460
+ return nodes.map((node) => ({
461
+ type: "application/ld+json",
462
+ html: JSON.stringify(node).replace(/<\/(script>)/gi, "<\\/$1"),
463
+ }));
464
+ }
465
+
466
+ /**
467
+ * Concatenate every node's serialized JSON-LD into a single HTML string
468
+ * — convenient for SSR'd `<head>` injection where the framework wants
469
+ * one `dangerouslySetInnerHTML`-style string instead of an array.
470
+ */
471
+ export function renderJsonLdToHtml(nodes: JsonLdNode[]): string {
472
+ return renderJsonLdNodes(nodes)
473
+ .map(({ type, html }) => `<script type="${type}">${html}</script>`)
474
+ .join("\n");
475
+ }
476
+
477
+ export const biabSchemas = {
478
+ localBusiness,
479
+ localBusinessLocations,
480
+ organization,
481
+ website,
482
+ service,
483
+ faq,
484
+ breadcrumb,
485
+ article,
486
+ review,
487
+ productOffer,
488
+ };