@businessdash/sdk 0.9.60 → 0.9.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +991 -295
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +622 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +888 -1
- package/dist/client.js.map +1 -1
- package/dist/consent-core/index.cjs +107 -0
- package/dist/consent-core/index.d.ts +103 -0
- package/dist/consent-core/index.d.ts.map +1 -0
- package/dist/consent-core/index.js +157 -0
- package/dist/consent-core/index.js.map +1 -0
- package/dist/contracts.d.ts +3526 -767
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +571 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1355 -24
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4516 -1311
- package/package.json +128 -11
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +1097 -0
- package/src/consent-core/index.ts +213 -0
- package/src/contracts.ts +768 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- package/src/vue/index.ts +3 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/seo-core` — turn an org's SEO into whatever your
|
|
3
|
+
* framework puts in `<head>`.
|
|
4
|
+
*
|
|
5
|
+
* ## The gap this fills
|
|
6
|
+
*
|
|
7
|
+
* The platform already ships per-page SEO: every marketing page bundle carries
|
|
8
|
+
* a `seo` object with title, description, canonical, `noIndex`, Open Graph,
|
|
9
|
+
* Twitter card, keywords, JSON-LD and hreflang. Until now the SDK re-exported
|
|
10
|
+
* that as a TYPE and stopped there, so every consumer hand-mapped eleven fields
|
|
11
|
+
* into their framework's own metadata shape — and hand-mapped the fallbacks,
|
|
12
|
+
* the absolute-URL rules and the robots string with them.
|
|
13
|
+
*
|
|
14
|
+
* Eleven fields is enough that everyone does four of them and forgets the rest.
|
|
15
|
+
* The ones people forget are `noIndex` and `canonical`, which are exactly the
|
|
16
|
+
* two where being wrong is expensive.
|
|
17
|
+
*
|
|
18
|
+
* ## Page and layout, not just site
|
|
19
|
+
*
|
|
20
|
+
* SEO composes. A layout sets the defaults every page inherits (site name,
|
|
21
|
+
* default OG image, the base URL); a page overrides the parts it knows better
|
|
22
|
+
* (its own title, its own canonical). `mergeSeo` applies that precedence in one
|
|
23
|
+
* place so it is the same on every framework, and `resolveSeo` accepts the
|
|
24
|
+
* layers directly.
|
|
25
|
+
*
|
|
26
|
+
* Precedence, lowest to highest: **layout defaults → the page bundle from the
|
|
27
|
+
* platform → explicit overrides in code.** Code wins because a developer
|
|
28
|
+
* writing `resolveSeo({ overrides })` has context the CMS does not — a search
|
|
29
|
+
* page that must never be indexed, a canonical that points at the parent.
|
|
30
|
+
*
|
|
31
|
+
* ## No framework imports
|
|
32
|
+
*
|
|
33
|
+
* Every adapter here returns a plain object or array shaped the way a framework
|
|
34
|
+
* expects. Nothing imports Next, Nuxt, Qwik or anything else, so this module
|
|
35
|
+
* works in all of them and adds a dependency to none of them.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
/** The platform's per-page SEO, as it arrives on a marketing page bundle. */
|
|
40
|
+
export type PlatformSeo = {
|
|
41
|
+
seoTitle: string | null;
|
|
42
|
+
seoDescription: string | null;
|
|
43
|
+
canonicalUrl: string | null;
|
|
44
|
+
noIndex: boolean;
|
|
45
|
+
ogTitle: string | null;
|
|
46
|
+
ogDescription: string | null;
|
|
47
|
+
ogImageUrl: string | null;
|
|
48
|
+
twitterCard: "summary" | "summary_large_image" | "app" | "player";
|
|
49
|
+
keywords: string[];
|
|
50
|
+
jsonldNodes: unknown[];
|
|
51
|
+
hreflang: Record<string, string>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* SEO written in code — for a layout, a page, or an override on top of the
|
|
55
|
+
* platform's.
|
|
56
|
+
*
|
|
57
|
+
* Every field optional: this is a PATCH, not a replacement. Sending a partial
|
|
58
|
+
* object must not blank the fields it omits, or a page that sets only its title
|
|
59
|
+
* would silently drop the org's OG image.
|
|
60
|
+
*/
|
|
61
|
+
export type SeoInput = {
|
|
62
|
+
title?: string | null;
|
|
63
|
+
description?: string | null;
|
|
64
|
+
canonical?: string | null;
|
|
65
|
+
noIndex?: boolean;
|
|
66
|
+
ogTitle?: string | null;
|
|
67
|
+
ogDescription?: string | null;
|
|
68
|
+
ogImage?: string | null;
|
|
69
|
+
ogType?: string;
|
|
70
|
+
twitterCard?: PlatformSeo["twitterCard"];
|
|
71
|
+
twitterSite?: string;
|
|
72
|
+
keywords?: string[];
|
|
73
|
+
jsonLd?: unknown[];
|
|
74
|
+
hreflang?: Record<string, string>;
|
|
75
|
+
/**
|
|
76
|
+
* Appended to the page title, e.g. `" | Acme Plumbing"`.
|
|
77
|
+
*
|
|
78
|
+
* A layout concern almost always — which is why it is here rather than
|
|
79
|
+
* expected to be baked into every page's title by hand.
|
|
80
|
+
*/
|
|
81
|
+
titleSuffix?: string;
|
|
82
|
+
/** Absolute origin used to resolve relative canonicals and image URLs. */
|
|
83
|
+
baseUrl?: string;
|
|
84
|
+
/** Site name for `og:site_name`. */
|
|
85
|
+
siteName?: string;
|
|
86
|
+
/** Locale for `og:locale`, e.g. `en_US`. */
|
|
87
|
+
locale?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Tags this module has no opinion about — `article:published_time`,
|
|
90
|
+
* `product:availability`, a verification token.
|
|
91
|
+
*
|
|
92
|
+
* An escape hatch on purpose: enumerating every vertical's meta vocabulary
|
|
93
|
+
* here would be a losing race, and a caller who needs one should not have
|
|
94
|
+
* to fork the resolver to add it. Accumulates across layers rather than
|
|
95
|
+
* replacing, so a layout's verification tag survives a page adding its own.
|
|
96
|
+
*/
|
|
97
|
+
extraMeta?: MetaTag[];
|
|
98
|
+
};
|
|
99
|
+
/** One `<meta>` tag. */
|
|
100
|
+
export type MetaTag = {
|
|
101
|
+
/** `name`, or `property` for Open Graph. */
|
|
102
|
+
name?: string;
|
|
103
|
+
property?: string;
|
|
104
|
+
content: string;
|
|
105
|
+
};
|
|
106
|
+
/** One `<link>` tag. */
|
|
107
|
+
export type LinkTag = {
|
|
108
|
+
rel: string;
|
|
109
|
+
href: string;
|
|
110
|
+
hreflang?: string;
|
|
111
|
+
};
|
|
112
|
+
/** Everything a `<head>` needs, framework-neutral. */
|
|
113
|
+
export type ResolvedSeo = {
|
|
114
|
+
title: string | null;
|
|
115
|
+
description: string | null;
|
|
116
|
+
canonical: string | null;
|
|
117
|
+
/** `noindex, nofollow` when hidden; `index, follow` otherwise. */
|
|
118
|
+
robots: string;
|
|
119
|
+
noIndex: boolean;
|
|
120
|
+
meta: MetaTag[];
|
|
121
|
+
links: LinkTag[];
|
|
122
|
+
jsonLd: unknown[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Absolute-ise a URL against the base.
|
|
126
|
+
*
|
|
127
|
+
* A relative canonical is worse than no canonical: crawlers resolve it against
|
|
128
|
+
* whatever URL they happened to fetch, so a `/pricing` canonical on a
|
|
129
|
+
* paginated or parameterised URL points somewhere nobody intended. Same for
|
|
130
|
+
* `og:image`, which social scrapers will not resolve at all.
|
|
131
|
+
*/
|
|
132
|
+
export declare function absoluteUrl(url: string | null | undefined, baseUrl: string | null | undefined): string | null;
|
|
133
|
+
/** Convert the platform's shape into the same patch shape as code input. */
|
|
134
|
+
export declare function platformSeoToInput(seo: PlatformSeo | null | undefined): SeoInput;
|
|
135
|
+
/**
|
|
136
|
+
* Layer SEO patches, later winning over earlier.
|
|
137
|
+
*
|
|
138
|
+
* `undefined` means "not specified" and inherits. `null` means "explicitly
|
|
139
|
+
* cleared" and wins — that distinction is the whole reason this is not
|
|
140
|
+
* `Object.assign`: a page saying `canonical: null` is making a decision, and
|
|
141
|
+
* treating it the same as an omitted field would ignore it.
|
|
142
|
+
*
|
|
143
|
+
* Arrays REPLACE rather than concatenate, except `jsonLd`, which accumulates:
|
|
144
|
+
* a layout's Organization node and a page's Product node both belong in the
|
|
145
|
+
* document, and dropping one because the other exists is how rich results go
|
|
146
|
+
* missing.
|
|
147
|
+
*/
|
|
148
|
+
export declare function mergeSeo(...layers: (SeoInput | undefined | null)[]): SeoInput;
|
|
149
|
+
/**
|
|
150
|
+
* Resolve layered SEO into the tags a `<head>` needs.
|
|
151
|
+
*
|
|
152
|
+
* Pass layers lowest-precedence first — typically the layout's defaults, then
|
|
153
|
+
* `platformSeoToInput(bundle.seo)`, then any page-level overrides.
|
|
154
|
+
*/
|
|
155
|
+
export declare function resolveSeo(...layers: (SeoInput | undefined | null)[]): ResolvedSeo;
|
|
156
|
+
/**
|
|
157
|
+
* Next.js App Router — return from `generateMetadata`.
|
|
158
|
+
*
|
|
159
|
+
* ```ts
|
|
160
|
+
* export async function generateMetadata() {
|
|
161
|
+
* const bundle = await client.marketing.pageBundle("home");
|
|
162
|
+
* return toNextMetadata(resolveSeo(layoutDefaults, platformSeoToInput(bundle.seo)));
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*
|
|
166
|
+
* JSON-LD is NOT included: Next has no metadata slot for it. Render it with
|
|
167
|
+
* `jsonLdScriptProps` in the page body instead — the one thing this adapter
|
|
168
|
+
* cannot do for you.
|
|
169
|
+
*/
|
|
170
|
+
export declare function toNextMetadata(seo: ResolvedSeo): Record<string, unknown>;
|
|
171
|
+
/** One entry of a Remix / React Router 7 `meta` export. */
|
|
172
|
+
export type RemixMetaDescriptor = Record<string, unknown>;
|
|
173
|
+
/**
|
|
174
|
+
* Remix / React Router 7 — return from the route's `meta` export.
|
|
175
|
+
*
|
|
176
|
+
* JSON-LD rides along as a `script` descriptor, which Remix renders natively —
|
|
177
|
+
* so unlike Next, structured data needs nothing extra.
|
|
178
|
+
*/
|
|
179
|
+
export declare function toRemixMeta(seo: ResolvedSeo): RemixMetaDescriptor[];
|
|
180
|
+
/**
|
|
181
|
+
* Nuxt — pass straight to `useHead()`.
|
|
182
|
+
*
|
|
183
|
+
* `script` carries the JSON-LD; Nuxt serialises the object for you.
|
|
184
|
+
*/
|
|
185
|
+
export declare function toNuxtHead(seo: ResolvedSeo): Record<string, unknown>;
|
|
186
|
+
/** Qwik City — return from the route's `head` / `DocumentHead`. */
|
|
187
|
+
export declare function toQwikDocumentHead(seo: ResolvedSeo): Record<string, unknown>;
|
|
188
|
+
/** TanStack Start — return from a route's `head()`. */
|
|
189
|
+
export declare function toTanStackHead(seo: ResolvedSeo): Record<string, unknown>;
|
|
190
|
+
/** One head tag, for templating frameworks that build `<head>` by hand. */
|
|
191
|
+
export type HeadTag = {
|
|
192
|
+
tag: "title" | "meta" | "link" | "script";
|
|
193
|
+
attrs: Record<string, string>;
|
|
194
|
+
/** Only set for `script` — the JSON-LD payload. */
|
|
195
|
+
children?: string;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* A flat tag list for Astro, SvelteKit, Angular, HTMX or anything else that
|
|
199
|
+
* writes `<head>` itself.
|
|
200
|
+
*
|
|
201
|
+
* Deliberately the lowest common denominator: a framework that has no
|
|
202
|
+
* metadata API can still loop this into markup, and one that does can map it.
|
|
203
|
+
*/
|
|
204
|
+
export declare function toHeadTags(seo: ResolvedSeo): HeadTag[];
|
|
205
|
+
/**
|
|
206
|
+
* Render the tag list as an HTML string.
|
|
207
|
+
*
|
|
208
|
+
* For server-rendered templates that concatenate `<head>` — Laravel, Phoenix,
|
|
209
|
+
* plain Node, HTMX. Values are escaped: SEO text is org-authored, and an
|
|
210
|
+
* unescaped `"` in a meta description would otherwise break out of the
|
|
211
|
+
* attribute and mangle the document.
|
|
212
|
+
*/
|
|
213
|
+
export declare function renderHeadTags(seo: ResolvedSeo): string;
|
|
214
|
+
/**
|
|
215
|
+
* Apply the resolved SEO to the live document.
|
|
216
|
+
*
|
|
217
|
+
* For vanilla JS and any client-side router that changes pages without a
|
|
218
|
+
* reload. Existing tags this module owns are REPLACED rather than appended —
|
|
219
|
+
* otherwise every navigation adds another `og:title` and crawlers pick one at
|
|
220
|
+
* random.
|
|
221
|
+
*/
|
|
222
|
+
export declare function applyToDocument(seo: ResolvedSeo, doc?: Document | undefined): void;
|
|
223
|
+
export { type BlogListingSeoInput, type BlogPostSeoInput, type BlogPostSeoSource, blogListingSeo, blogPostSeo, } from "./blog.js";
|
|
224
|
+
export { type ListedProduct, type ProductListingSeoInput, productListingSeo, transactionalPageSeo, } from "./commerce.js";
|
|
225
|
+
export { type LocalPagePlan, type LocalProductPageInput, type LocalServicePageInput, localServicePageSeo, productPageSeo, type ServiceArea, type ServiceOffering, type ServicesTimesAreasInput, servicesTimesAreas, } from "./local.js";
|
|
226
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seo-core/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/seo-core` — turn an org's SEO into whatever your
|
|
3
|
+
* framework puts in `<head>`.
|
|
4
|
+
*
|
|
5
|
+
* ## The gap this fills
|
|
6
|
+
*
|
|
7
|
+
* The platform already ships per-page SEO: every marketing page bundle carries
|
|
8
|
+
* a `seo` object with title, description, canonical, `noIndex`, Open Graph,
|
|
9
|
+
* Twitter card, keywords, JSON-LD and hreflang. Until now the SDK re-exported
|
|
10
|
+
* that as a TYPE and stopped there, so every consumer hand-mapped eleven fields
|
|
11
|
+
* into their framework's own metadata shape — and hand-mapped the fallbacks,
|
|
12
|
+
* the absolute-URL rules and the robots string with them.
|
|
13
|
+
*
|
|
14
|
+
* Eleven fields is enough that everyone does four of them and forgets the rest.
|
|
15
|
+
* The ones people forget are `noIndex` and `canonical`, which are exactly the
|
|
16
|
+
* two where being wrong is expensive.
|
|
17
|
+
*
|
|
18
|
+
* ## Page and layout, not just site
|
|
19
|
+
*
|
|
20
|
+
* SEO composes. A layout sets the defaults every page inherits (site name,
|
|
21
|
+
* default OG image, the base URL); a page overrides the parts it knows better
|
|
22
|
+
* (its own title, its own canonical). `mergeSeo` applies that precedence in one
|
|
23
|
+
* place so it is the same on every framework, and `resolveSeo` accepts the
|
|
24
|
+
* layers directly.
|
|
25
|
+
*
|
|
26
|
+
* Precedence, lowest to highest: **layout defaults → the page bundle from the
|
|
27
|
+
* platform → explicit overrides in code.** Code wins because a developer
|
|
28
|
+
* writing `resolveSeo({ overrides })` has context the CMS does not — a search
|
|
29
|
+
* page that must never be indexed, a canonical that points at the parent.
|
|
30
|
+
*
|
|
31
|
+
* ## No framework imports
|
|
32
|
+
*
|
|
33
|
+
* Every adapter here returns a plain object or array shaped the way a framework
|
|
34
|
+
* expects. Nothing imports Next, Nuxt, Qwik or anything else, so this module
|
|
35
|
+
* works in all of them and adds a dependency to none of them.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
function isBlank(value) {
|
|
40
|
+
return value === null || value === undefined || value.trim() === "";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Absolute-ise a URL against the base.
|
|
44
|
+
*
|
|
45
|
+
* A relative canonical is worse than no canonical: crawlers resolve it against
|
|
46
|
+
* whatever URL they happened to fetch, so a `/pricing` canonical on a
|
|
47
|
+
* paginated or parameterised URL points somewhere nobody intended. Same for
|
|
48
|
+
* `og:image`, which social scrapers will not resolve at all.
|
|
49
|
+
*/
|
|
50
|
+
export function absoluteUrl(url, baseUrl) {
|
|
51
|
+
if (isBlank(url))
|
|
52
|
+
return null;
|
|
53
|
+
const value = url.trim();
|
|
54
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//")) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (isBlank(baseUrl)) {
|
|
58
|
+
// Returning the relative value would look like it worked. Dropping it
|
|
59
|
+
// is the honest failure: a missing canonical is recoverable, a wrong
|
|
60
|
+
// one silently consolidates ranking onto the wrong URL.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
64
|
+
return `${base}${value.startsWith("/") ? "" : "/"}${value}`;
|
|
65
|
+
}
|
|
66
|
+
/** Convert the platform's shape into the same patch shape as code input. */
|
|
67
|
+
export function platformSeoToInput(seo) {
|
|
68
|
+
if (!seo)
|
|
69
|
+
return {};
|
|
70
|
+
return {
|
|
71
|
+
title: seo.seoTitle,
|
|
72
|
+
description: seo.seoDescription,
|
|
73
|
+
canonical: seo.canonicalUrl,
|
|
74
|
+
noIndex: seo.noIndex,
|
|
75
|
+
ogTitle: seo.ogTitle,
|
|
76
|
+
ogDescription: seo.ogDescription,
|
|
77
|
+
ogImage: seo.ogImageUrl,
|
|
78
|
+
twitterCard: seo.twitterCard,
|
|
79
|
+
keywords: seo.keywords,
|
|
80
|
+
jsonLd: seo.jsonldNodes,
|
|
81
|
+
hreflang: seo.hreflang,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Layer SEO patches, later winning over earlier.
|
|
86
|
+
*
|
|
87
|
+
* `undefined` means "not specified" and inherits. `null` means "explicitly
|
|
88
|
+
* cleared" and wins — that distinction is the whole reason this is not
|
|
89
|
+
* `Object.assign`: a page saying `canonical: null` is making a decision, and
|
|
90
|
+
* treating it the same as an omitted field would ignore it.
|
|
91
|
+
*
|
|
92
|
+
* Arrays REPLACE rather than concatenate, except `jsonLd`, which accumulates:
|
|
93
|
+
* a layout's Organization node and a page's Product node both belong in the
|
|
94
|
+
* document, and dropping one because the other exists is how rich results go
|
|
95
|
+
* missing.
|
|
96
|
+
*/
|
|
97
|
+
export function mergeSeo(...layers) {
|
|
98
|
+
const out = {};
|
|
99
|
+
const jsonLd = [];
|
|
100
|
+
const extraMeta = [];
|
|
101
|
+
for (const layer of layers) {
|
|
102
|
+
if (!layer)
|
|
103
|
+
continue;
|
|
104
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
if (key === "jsonLd") {
|
|
108
|
+
if (Array.isArray(value))
|
|
109
|
+
jsonLd.push(...value);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (key === "extraMeta") {
|
|
113
|
+
if (Array.isArray(value))
|
|
114
|
+
extraMeta.push(...value);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (key === "hreflang") {
|
|
118
|
+
out.hreflang = {
|
|
119
|
+
...(out.hreflang ?? {}),
|
|
120
|
+
...value,
|
|
121
|
+
};
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
out[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (jsonLd.length > 0)
|
|
128
|
+
out.jsonLd = jsonLd;
|
|
129
|
+
if (extraMeta.length > 0)
|
|
130
|
+
out.extraMeta = extraMeta;
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve layered SEO into the tags a `<head>` needs.
|
|
135
|
+
*
|
|
136
|
+
* Pass layers lowest-precedence first — typically the layout's defaults, then
|
|
137
|
+
* `platformSeoToInput(bundle.seo)`, then any page-level overrides.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveSeo(...layers) {
|
|
140
|
+
const seo = mergeSeo(...layers);
|
|
141
|
+
const baseTitle = isBlank(seo.title) ? null : seo.title.trim();
|
|
142
|
+
const title = baseTitle && seo.titleSuffix ? `${baseTitle}${seo.titleSuffix}` : baseTitle;
|
|
143
|
+
const description = isBlank(seo.description)
|
|
144
|
+
? null
|
|
145
|
+
: seo.description.trim();
|
|
146
|
+
const canonical = absoluteUrl(seo.canonical, seo.baseUrl);
|
|
147
|
+
const ogImage = absoluteUrl(seo.ogImage, seo.baseUrl);
|
|
148
|
+
const noIndex = seo.noIndex === true;
|
|
149
|
+
const meta = [];
|
|
150
|
+
const push = (tag) => {
|
|
151
|
+
if (!isBlank(tag.content))
|
|
152
|
+
meta.push(tag);
|
|
153
|
+
};
|
|
154
|
+
if (description)
|
|
155
|
+
push({ name: "description", content: description });
|
|
156
|
+
if (seo.keywords?.length) {
|
|
157
|
+
push({ name: "keywords", content: seo.keywords.join(", ") });
|
|
158
|
+
}
|
|
159
|
+
// Always emitted, both ways. An absent robots tag means "index" by default,
|
|
160
|
+
// so relying on absence to express "index" is fine — but relying on it to
|
|
161
|
+
// express noindex is catastrophic, and emitting only one of the two makes
|
|
162
|
+
// the code look asymmetric enough that someone eventually "tidies" it.
|
|
163
|
+
const robots = noIndex ? "noindex, nofollow" : "index, follow";
|
|
164
|
+
push({ name: "robots", content: robots });
|
|
165
|
+
// Open Graph falls back to the page's own title/description. Sites almost
|
|
166
|
+
// never set these separately, and a missing og:title is what makes a shared
|
|
167
|
+
// link render as a bare URL.
|
|
168
|
+
const ogTitle = isBlank(seo.ogTitle) ? title : seo.ogTitle;
|
|
169
|
+
const ogDescription = isBlank(seo.ogDescription)
|
|
170
|
+
? description
|
|
171
|
+
: seo.ogDescription;
|
|
172
|
+
if (ogTitle)
|
|
173
|
+
push({ property: "og:title", content: ogTitle });
|
|
174
|
+
if (ogDescription)
|
|
175
|
+
push({ property: "og:description", content: ogDescription });
|
|
176
|
+
if (ogImage)
|
|
177
|
+
push({ property: "og:image", content: ogImage });
|
|
178
|
+
if (canonical)
|
|
179
|
+
push({ property: "og:url", content: canonical });
|
|
180
|
+
push({ property: "og:type", content: seo.ogType ?? "website" });
|
|
181
|
+
if (seo.siteName)
|
|
182
|
+
push({ property: "og:site_name", content: seo.siteName });
|
|
183
|
+
if (seo.locale)
|
|
184
|
+
push({ property: "og:locale", content: seo.locale });
|
|
185
|
+
push({
|
|
186
|
+
name: "twitter:card",
|
|
187
|
+
content: seo.twitterCard ?? "summary_large_image",
|
|
188
|
+
});
|
|
189
|
+
if (seo.twitterSite)
|
|
190
|
+
push({ name: "twitter:site", content: seo.twitterSite });
|
|
191
|
+
if (ogTitle)
|
|
192
|
+
push({ name: "twitter:title", content: ogTitle });
|
|
193
|
+
if (ogDescription)
|
|
194
|
+
push({ name: "twitter:description", content: ogDescription });
|
|
195
|
+
if (ogImage)
|
|
196
|
+
push({ name: "twitter:image", content: ogImage });
|
|
197
|
+
for (const tag of seo.extraMeta ?? [])
|
|
198
|
+
push(tag);
|
|
199
|
+
const links = [];
|
|
200
|
+
if (canonical)
|
|
201
|
+
links.push({ rel: "canonical", href: canonical });
|
|
202
|
+
for (const [lang, href] of Object.entries(seo.hreflang ?? {})) {
|
|
203
|
+
const resolved = absoluteUrl(href, seo.baseUrl);
|
|
204
|
+
if (resolved)
|
|
205
|
+
links.push({ rel: "alternate", href: resolved, hreflang: lang });
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
title,
|
|
209
|
+
description,
|
|
210
|
+
canonical,
|
|
211
|
+
robots,
|
|
212
|
+
noIndex,
|
|
213
|
+
meta,
|
|
214
|
+
links,
|
|
215
|
+
jsonLd: seo.jsonLd ?? [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// Framework adapters
|
|
220
|
+
//
|
|
221
|
+
// Each returns the plain shape its framework expects. None imports a framework,
|
|
222
|
+
// so this module stays dependency-free and usable from all of them.
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Next.js App Router — return from `generateMetadata`.
|
|
226
|
+
*
|
|
227
|
+
* ```ts
|
|
228
|
+
* export async function generateMetadata() {
|
|
229
|
+
* const bundle = await client.marketing.pageBundle("home");
|
|
230
|
+
* return toNextMetadata(resolveSeo(layoutDefaults, platformSeoToInput(bundle.seo)));
|
|
231
|
+
* }
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* JSON-LD is NOT included: Next has no metadata slot for it. Render it with
|
|
235
|
+
* `jsonLdScriptProps` in the page body instead — the one thing this adapter
|
|
236
|
+
* cannot do for you.
|
|
237
|
+
*/
|
|
238
|
+
export function toNextMetadata(seo) {
|
|
239
|
+
const openGraph = {};
|
|
240
|
+
const twitter = {};
|
|
241
|
+
const other = {};
|
|
242
|
+
for (const tag of seo.meta) {
|
|
243
|
+
if (tag.property?.startsWith("og:")) {
|
|
244
|
+
openGraph[tag.property.slice(3)] = tag.content;
|
|
245
|
+
}
|
|
246
|
+
else if (tag.name?.startsWith("twitter:")) {
|
|
247
|
+
twitter[tag.name.slice(8)] = tag.content;
|
|
248
|
+
}
|
|
249
|
+
else if (tag.name &&
|
|
250
|
+
tag.name !== "description" &&
|
|
251
|
+
tag.name !== "robots") {
|
|
252
|
+
other[tag.name] = tag.content;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const metadata = {
|
|
256
|
+
...(seo.title ? { title: seo.title } : {}),
|
|
257
|
+
...(seo.description ? { description: seo.description } : {}),
|
|
258
|
+
// Next wants a structured robots object, not the string.
|
|
259
|
+
robots: { index: !seo.noIndex, follow: !seo.noIndex },
|
|
260
|
+
...(seo.canonical ? { alternates: { canonical: seo.canonical } } : {}),
|
|
261
|
+
...(Object.keys(openGraph).length ? { openGraph } : {}),
|
|
262
|
+
...(Object.keys(twitter).length ? { twitter } : {}),
|
|
263
|
+
...(Object.keys(other).length ? { other } : {}),
|
|
264
|
+
};
|
|
265
|
+
const alternates = metadata.alternates;
|
|
266
|
+
const languages = {};
|
|
267
|
+
for (const link of seo.links) {
|
|
268
|
+
if (link.rel === "alternate" && link.hreflang) {
|
|
269
|
+
languages[link.hreflang] = link.href;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (Object.keys(languages).length > 0) {
|
|
273
|
+
metadata.alternates = { ...(alternates ?? {}), languages };
|
|
274
|
+
}
|
|
275
|
+
return metadata;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Remix / React Router 7 — return from the route's `meta` export.
|
|
279
|
+
*
|
|
280
|
+
* JSON-LD rides along as a `script` descriptor, which Remix renders natively —
|
|
281
|
+
* so unlike Next, structured data needs nothing extra.
|
|
282
|
+
*/
|
|
283
|
+
export function toRemixMeta(seo) {
|
|
284
|
+
const out = [];
|
|
285
|
+
if (seo.title)
|
|
286
|
+
out.push({ title: seo.title });
|
|
287
|
+
for (const tag of seo.meta) {
|
|
288
|
+
out.push(tag.property
|
|
289
|
+
? { property: tag.property, content: tag.content }
|
|
290
|
+
: { name: tag.name, content: tag.content });
|
|
291
|
+
}
|
|
292
|
+
for (const link of seo.links) {
|
|
293
|
+
out.push({ tagName: "link", ...link });
|
|
294
|
+
}
|
|
295
|
+
for (const node of seo.jsonLd) {
|
|
296
|
+
out.push({ "script:ld+json": node });
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Nuxt — pass straight to `useHead()`.
|
|
302
|
+
*
|
|
303
|
+
* `script` carries the JSON-LD; Nuxt serialises the object for you.
|
|
304
|
+
*/
|
|
305
|
+
export function toNuxtHead(seo) {
|
|
306
|
+
return {
|
|
307
|
+
...(seo.title ? { title: seo.title } : {}),
|
|
308
|
+
meta: seo.meta.map((tag) => tag.property
|
|
309
|
+
? { property: tag.property, content: tag.content }
|
|
310
|
+
: { name: tag.name, content: tag.content }),
|
|
311
|
+
link: seo.links.map((link) => ({ ...link })),
|
|
312
|
+
script: seo.jsonLd.map((node) => ({
|
|
313
|
+
type: "application/ld+json",
|
|
314
|
+
innerHTML: JSON.stringify(node),
|
|
315
|
+
})),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/** Qwik City — return from the route's `head` / `DocumentHead`. */
|
|
319
|
+
export function toQwikDocumentHead(seo) {
|
|
320
|
+
return {
|
|
321
|
+
...(seo.title ? { title: seo.title } : {}),
|
|
322
|
+
meta: seo.meta.map((tag) => tag.property
|
|
323
|
+
? { property: tag.property, content: tag.content }
|
|
324
|
+
: { name: tag.name ?? "", content: tag.content }),
|
|
325
|
+
links: seo.links.map((link) => ({ ...link })),
|
|
326
|
+
// Qwik has no JSON-LD slot on DocumentHead; carry it in `frontmatter`
|
|
327
|
+
// so the route can render it into the body.
|
|
328
|
+
frontmatter: { jsonLd: seo.jsonLd },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** TanStack Start — return from a route's `head()`. */
|
|
332
|
+
export function toTanStackHead(seo) {
|
|
333
|
+
const meta = [];
|
|
334
|
+
if (seo.title)
|
|
335
|
+
meta.push({ title: seo.title });
|
|
336
|
+
for (const tag of seo.meta) {
|
|
337
|
+
meta.push(tag.property
|
|
338
|
+
? { property: tag.property, content: tag.content }
|
|
339
|
+
: { name: tag.name, content: tag.content });
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
meta,
|
|
343
|
+
links: seo.links.map((link) => ({ ...link })),
|
|
344
|
+
scripts: seo.jsonLd.map((node) => ({
|
|
345
|
+
type: "application/ld+json",
|
|
346
|
+
children: JSON.stringify(node),
|
|
347
|
+
})),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* A flat tag list for Astro, SvelteKit, Angular, HTMX or anything else that
|
|
352
|
+
* writes `<head>` itself.
|
|
353
|
+
*
|
|
354
|
+
* Deliberately the lowest common denominator: a framework that has no
|
|
355
|
+
* metadata API can still loop this into markup, and one that does can map it.
|
|
356
|
+
*/
|
|
357
|
+
export function toHeadTags(seo) {
|
|
358
|
+
const tags = [];
|
|
359
|
+
if (seo.title)
|
|
360
|
+
tags.push({ tag: "title", attrs: {}, children: seo.title });
|
|
361
|
+
for (const tag of seo.meta) {
|
|
362
|
+
tags.push({
|
|
363
|
+
tag: "meta",
|
|
364
|
+
attrs: tag.property
|
|
365
|
+
? { property: tag.property, content: tag.content }
|
|
366
|
+
: { name: tag.name ?? "", content: tag.content },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
for (const link of seo.links) {
|
|
370
|
+
const attrs = { rel: link.rel, href: link.href };
|
|
371
|
+
if (link.hreflang)
|
|
372
|
+
attrs.hreflang = link.hreflang;
|
|
373
|
+
tags.push({ tag: "link", attrs });
|
|
374
|
+
}
|
|
375
|
+
for (const node of seo.jsonLd) {
|
|
376
|
+
tags.push({
|
|
377
|
+
tag: "script",
|
|
378
|
+
attrs: { type: "application/ld+json" },
|
|
379
|
+
children: JSON.stringify(node),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return tags;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Render the tag list as an HTML string.
|
|
386
|
+
*
|
|
387
|
+
* For server-rendered templates that concatenate `<head>` — Laravel, Phoenix,
|
|
388
|
+
* plain Node, HTMX. Values are escaped: SEO text is org-authored, and an
|
|
389
|
+
* unescaped `"` in a meta description would otherwise break out of the
|
|
390
|
+
* attribute and mangle the document.
|
|
391
|
+
*/
|
|
392
|
+
export function renderHeadTags(seo) {
|
|
393
|
+
const escapeAttr = (value) => value
|
|
394
|
+
.replace(/&/g, "&")
|
|
395
|
+
.replace(/"/g, """)
|
|
396
|
+
.replace(/</g, "<")
|
|
397
|
+
.replace(/>/g, ">");
|
|
398
|
+
return toHeadTags(seo)
|
|
399
|
+
.map((tag) => {
|
|
400
|
+
const attrs = Object.entries(tag.attrs)
|
|
401
|
+
.map(([key, value]) => `${key}="${escapeAttr(value)}"`)
|
|
402
|
+
.join(" ");
|
|
403
|
+
if (tag.tag === "title") {
|
|
404
|
+
return `<title>${escapeAttr(tag.children ?? "")}</title>`;
|
|
405
|
+
}
|
|
406
|
+
if (tag.tag === "script") {
|
|
407
|
+
// JSON-LD goes in a script body, where `<` is what actually
|
|
408
|
+
// breaks out. Escaping the whole payload would corrupt the JSON,
|
|
409
|
+
// so only the sequence that can close the tag is neutralised.
|
|
410
|
+
const payload = (tag.children ?? "").replace(/</g, "\\u003c");
|
|
411
|
+
return `<script ${attrs}>${payload}</script>`;
|
|
412
|
+
}
|
|
413
|
+
return `<${tag.tag} ${attrs}>`;
|
|
414
|
+
})
|
|
415
|
+
.join("\n");
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Apply the resolved SEO to the live document.
|
|
419
|
+
*
|
|
420
|
+
* For vanilla JS and any client-side router that changes pages without a
|
|
421
|
+
* reload. Existing tags this module owns are REPLACED rather than appended —
|
|
422
|
+
* otherwise every navigation adds another `og:title` and crawlers pick one at
|
|
423
|
+
* random.
|
|
424
|
+
*/
|
|
425
|
+
export function applyToDocument(seo, doc = typeof document === "undefined"
|
|
426
|
+
? undefined
|
|
427
|
+
: document) {
|
|
428
|
+
if (!doc)
|
|
429
|
+
return;
|
|
430
|
+
const OWNED = "data-biab-seo";
|
|
431
|
+
for (const existing of Array.from(doc.head.querySelectorAll(`[${OWNED}]`))) {
|
|
432
|
+
existing.remove();
|
|
433
|
+
}
|
|
434
|
+
if (seo.title !== null)
|
|
435
|
+
doc.title = seo.title;
|
|
436
|
+
for (const tag of toHeadTags(seo)) {
|
|
437
|
+
if (tag.tag === "title")
|
|
438
|
+
continue;
|
|
439
|
+
const element = doc.createElement(tag.tag);
|
|
440
|
+
for (const [key, value] of Object.entries(tag.attrs)) {
|
|
441
|
+
element.setAttribute(key, value);
|
|
442
|
+
}
|
|
443
|
+
if (tag.children)
|
|
444
|
+
element.textContent = tag.children;
|
|
445
|
+
element.setAttribute(OWNED, "");
|
|
446
|
+
doc.head.appendChild(element);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Blog and storefront listings.
|
|
450
|
+
export { blogListingSeo, blogPostSeo, } from "./blog.js";
|
|
451
|
+
export { productListingSeo, transactionalPageSeo, } from "./commerce.js";
|
|
452
|
+
// Local SEO — service areas x services, and products. Re-exported so one
|
|
453
|
+
// import covers a whole local landing page.
|
|
454
|
+
export { localServicePageSeo, productPageSeo, servicesTimesAreas, } from "./local.js";
|
|
455
|
+
//# sourceMappingURL=index.js.map
|