@brandfine/client 0.2.0 → 0.4.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/CHANGELOG.md +39 -0
- package/dist/{chunk-OKIEA3AD.cjs → chunk-FCK7QJBC.cjs} +4 -2
- package/dist/chunk-FCK7QJBC.cjs.map +1 -0
- package/dist/{chunk-MTBSSTTG.js → chunk-U6VJX7PP.js} +4 -2
- package/dist/chunk-U6VJX7PP.js.map +1 -0
- package/dist/{index-CnR601Y2.d.cts → index-32jeQf1C.d.cts} +22 -8
- package/dist/{index-CnR601Y2.d.ts → index-32jeQf1C.d.ts} +22 -8
- package/dist/index.cjs +35 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -5
- package/dist/index.d.ts +74 -5
- package/dist/index.js +30 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/index.cjs +6 -6
- package/dist/resolvers/index.d.cts +1 -1
- package/dist/resolvers/index.d.ts +1 -1
- package/dist/resolvers/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-MTBSSTTG.js.map +0 -1
- package/dist/chunk-OKIEA3AD.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 34d0a9d: Add `bf.analytics` namespace for the self-hosted Brandfine analytics integration.
|
|
8
|
+
- `bf.analytics.getConfig()` — fetches this workspace's analytics setup from
|
|
9
|
+
Brandfine. Returns `{ enabled: false }` when analytics isn't enabled in the
|
|
10
|
+
cms, or `{ enabled: true, websiteId, scriptUrl }` when it is.
|
|
11
|
+
- `bf.analytics.install(opts?)` — injects the tracker script into
|
|
12
|
+
`document.head` once. Idempotent across re-renders via a marker attribute.
|
|
13
|
+
By default fetches the config first; pass `{ config }` to skip the round-trip
|
|
14
|
+
when you already have it (e.g. server-component prefetch in static-export
|
|
15
|
+
Next.js sites).
|
|
16
|
+
|
|
17
|
+
The runtime path (`install()` with no args) reflects enable/disable state
|
|
18
|
+
on the next page load — costs one HTTP round-trip per page. The build-time
|
|
19
|
+
path (`install({ config })`) injects with zero round-trips but ignores
|
|
20
|
+
Brandfine state changes until the consumer's next deploy. Static sites
|
|
21
|
+
typically want the latter; dynamic sites the former.
|
|
22
|
+
|
|
23
|
+
New `AnalyticsConfig`, `AnalyticsInstallResult`, and `InstallOptions` types
|
|
24
|
+
are exported from the root entry.
|
|
25
|
+
|
|
26
|
+
## 0.3.0
|
|
27
|
+
|
|
28
|
+
### Minor Changes
|
|
29
|
+
|
|
30
|
+
- c41e51d: Navigation items gain two new fields, both served by
|
|
31
|
+
`GET /external/navigations/:key` and passed through the navigation resolver:
|
|
32
|
+
- `imageUrl` — optional image URL (icon, thumbnail, mega-menu art) on any
|
|
33
|
+
item type. `null` when unset.
|
|
34
|
+
- `customConfig` — free-form per-item JSON, round-tripped untouched (same
|
|
35
|
+
contract as `BrandfinePost.customConfig`). `BrandfineNavItem`,
|
|
36
|
+
`BrandfineNavigation`, `bf.navigations.get`, `resolveNavigation`, and
|
|
37
|
+
`HydratedNavItem` are now generic over `TConfig` (default `unknown`) so
|
|
38
|
+
consumers can type it: `bf.navigations.get<MyNavConfig>('header')`.
|
|
39
|
+
|
|
40
|
+
Additive, no breaking changes — existing untyped usage keeps working.
|
|
41
|
+
|
|
3
42
|
## 0.2.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
|
@@ -52,6 +52,8 @@ function hydrate(item, children, locale, opts) {
|
|
|
52
52
|
href: resolveHref(item, locale, opts),
|
|
53
53
|
label: resolveLabel(item, locale, opts.defaultLocale),
|
|
54
54
|
type: item.type,
|
|
55
|
+
imageUrl: item.imageUrl ?? null,
|
|
56
|
+
customConfig: item.customConfig ?? null,
|
|
55
57
|
children: children.map((c) => hydrate(c, [], locale, opts))
|
|
56
58
|
};
|
|
57
59
|
}
|
|
@@ -103,5 +105,5 @@ exports.localizePath = localizePath;
|
|
|
103
105
|
exports.pickLocale = pickLocale;
|
|
104
106
|
exports.resolveNavigation = resolveNavigation;
|
|
105
107
|
exports.stripLocalePrefix = stripLocalePrefix;
|
|
106
|
-
//# sourceMappingURL=chunk-
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-FCK7QJBC.cjs.map
|
|
109
|
+
//# sourceMappingURL=chunk-FCK7QJBC.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-FCK7QJBC.cjs","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem<TConfig = unknown> = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n /** Optional image URL, passed through from the item. Not\n * per-locale; `null` when unset. */\n imageUrl: string | null\n /** Free-form per-item config, passed through untouched from the\n * item. Default `unknown`; parameterize `TConfig` to type it. */\n customConfig: TConfig | null\n children: HydratedNavItem<TConfig>[]\n}\n\nexport type HydratedNav<TConfig = unknown> = {\n key: string\n name: string\n items: HydratedNavItem<TConfig>[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation<TConfig = unknown>(\n nav: BrandfineNavigation<TConfig>,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav<TConfig> {\n const childrenByParent = new Map<string, BrandfineNavItem<TConfig>[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate<TConfig>(\n item: BrandfineNavItem<TConfig>,\n children: BrandfineNavItem<TConfig>[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem<TConfig> {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n imageUrl: item.imageUrl ?? null,\n customConfig: item.customConfig ?? null,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}
|
|
@@ -50,6 +50,8 @@ function hydrate(item, children, locale, opts) {
|
|
|
50
50
|
href: resolveHref(item, locale, opts),
|
|
51
51
|
label: resolveLabel(item, locale, opts.defaultLocale),
|
|
52
52
|
type: item.type,
|
|
53
|
+
imageUrl: item.imageUrl ?? null,
|
|
54
|
+
customConfig: item.customConfig ?? null,
|
|
53
55
|
children: children.map((c) => hydrate(c, [], locale, opts))
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -97,5 +99,5 @@ function resolveLabel(item, locale, defaultLocale) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix };
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
101
|
-
//# sourceMappingURL=chunk-
|
|
102
|
+
//# sourceMappingURL=chunk-U6VJX7PP.js.map
|
|
103
|
+
//# sourceMappingURL=chunk-U6VJX7PP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-U6VJX7PP.js","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem<TConfig = unknown> = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n /** Optional image URL, passed through from the item. Not\n * per-locale; `null` when unset. */\n imageUrl: string | null\n /** Free-form per-item config, passed through untouched from the\n * item. Default `unknown`; parameterize `TConfig` to type it. */\n customConfig: TConfig | null\n children: HydratedNavItem<TConfig>[]\n}\n\nexport type HydratedNav<TConfig = unknown> = {\n key: string\n name: string\n items: HydratedNavItem<TConfig>[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation<TConfig = unknown>(\n nav: BrandfineNavigation<TConfig>,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav<TConfig> {\n const childrenByParent = new Map<string, BrandfineNavItem<TConfig>[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate<TConfig>(\n item: BrandfineNavItem<TConfig>,\n children: BrandfineNavItem<TConfig>[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem<TConfig> {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n imageUrl: item.imageUrl ?? null,\n customConfig: item.customConfig ?? null,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}
|
|
@@ -108,13 +108,21 @@ type BrandfineWorkspace<TCustomConfig = Record<string, unknown>, TSchemaOrg = Re
|
|
|
108
108
|
schemaOrg: TSchemaOrg | null;
|
|
109
109
|
};
|
|
110
110
|
type BrandfineNavItemType = 'CUSTOM_URL' | 'POST' | 'HEADING';
|
|
111
|
-
type BrandfineNavItem = {
|
|
111
|
+
type BrandfineNavItem<TConfig = unknown> = {
|
|
112
112
|
id: string;
|
|
113
113
|
parentId: string | null;
|
|
114
114
|
position: number;
|
|
115
115
|
type: BrandfineNavItemType;
|
|
116
116
|
/** CUSTOM_URL: free-form path or absolute URL. */
|
|
117
117
|
customUrl: string | null;
|
|
118
|
+
/** Optional image URL for the item (icon, thumbnail, mega-menu
|
|
119
|
+
* art). Set on any item type; not per-locale. Null when unset. */
|
|
120
|
+
imageUrl: string | null;
|
|
121
|
+
/** Free-form per-item config the consumer interprets (badges,
|
|
122
|
+
* layout hints, flags). Brandfine round-trips it untouched —
|
|
123
|
+
* same contract as `BrandfinePost.customConfig`. Default
|
|
124
|
+
* `unknown`; parameterize `TConfig` to type it. Null when unset. */
|
|
125
|
+
customConfig: TConfig | null;
|
|
118
126
|
/** `{ [locale]: string }` map. May be null on POST items —
|
|
119
127
|
* consumers fall back to the post's per-locale `title` then. */
|
|
120
128
|
labels: Record<string, string> | null;
|
|
@@ -142,10 +150,10 @@ type BrandfineNavPost = {
|
|
|
142
150
|
title: string;
|
|
143
151
|
}>;
|
|
144
152
|
};
|
|
145
|
-
type BrandfineNavigation = {
|
|
153
|
+
type BrandfineNavigation<TConfig = unknown> = {
|
|
146
154
|
key: string;
|
|
147
155
|
name: string;
|
|
148
|
-
items: BrandfineNavItem[];
|
|
156
|
+
items: BrandfineNavItem<TConfig>[];
|
|
149
157
|
};
|
|
150
158
|
type ListPostsOptions = {
|
|
151
159
|
/** Post-type slug. Default server-side is `'blog'`; pass `'*'` to
|
|
@@ -258,19 +266,25 @@ declare function stripLocalePrefix(pathname: string, opts: LocaleOptions): strin
|
|
|
258
266
|
* - `hiddenLocales` drops the item entirely for that locale.
|
|
259
267
|
*/
|
|
260
268
|
|
|
261
|
-
type HydratedNavItem = {
|
|
269
|
+
type HydratedNavItem<TConfig = unknown> = {
|
|
262
270
|
/** Final URL, ready to render in an `<a href>`. `null` for
|
|
263
271
|
* HEADING items (label-only) and for POST items whose post
|
|
264
272
|
* has been deleted (orphan). */
|
|
265
273
|
href: string | null;
|
|
266
274
|
label: string;
|
|
267
275
|
type: 'CUSTOM_URL' | 'POST' | 'HEADING';
|
|
268
|
-
|
|
276
|
+
/** Optional image URL, passed through from the item. Not
|
|
277
|
+
* per-locale; `null` when unset. */
|
|
278
|
+
imageUrl: string | null;
|
|
279
|
+
/** Free-form per-item config, passed through untouched from the
|
|
280
|
+
* item. Default `unknown`; parameterize `TConfig` to type it. */
|
|
281
|
+
customConfig: TConfig | null;
|
|
282
|
+
children: HydratedNavItem<TConfig>[];
|
|
269
283
|
};
|
|
270
|
-
type HydratedNav = {
|
|
284
|
+
type HydratedNav<TConfig = unknown> = {
|
|
271
285
|
key: string;
|
|
272
286
|
name: string;
|
|
273
|
-
items: HydratedNavItem[];
|
|
287
|
+
items: HydratedNavItem<TConfig>[];
|
|
274
288
|
};
|
|
275
289
|
type ResolveNavigationOptions = {
|
|
276
290
|
defaultLocale: string;
|
|
@@ -287,6 +301,6 @@ type ResolveNavigationOptions = {
|
|
|
287
301
|
defaultLocale: string;
|
|
288
302
|
}) => string | null;
|
|
289
303
|
};
|
|
290
|
-
declare function resolveNavigation(nav: BrandfineNavigation
|
|
304
|
+
declare function resolveNavigation<TConfig = unknown>(nav: BrandfineNavigation<TConfig>, locale: string, opts: ResolveNavigationOptions): HydratedNav<TConfig>;
|
|
291
305
|
|
|
292
306
|
export { type BrandfinePost as B, type HydratedNav as H, type ListPostsOptions as L, type ResolveNavigationOptions as R, type ListCategoriesOptions as a, type BrandfineCategory as b, type BrandfineWorkspace as c, type BrandfineNavigation as d, type BrandfineNavItem as e, type BrandfineNavItemType as f, type BrandfineNavPost as g, type BrandfinePostListResponse as h, type BrandfinePostTranslation as i, type HydratedNavItem as j, type LocaleOptions as k, isLocale as l, localizePath as m, pickLocale as p, resolveNavigation as r, stripLocalePrefix as s };
|
|
@@ -108,13 +108,21 @@ type BrandfineWorkspace<TCustomConfig = Record<string, unknown>, TSchemaOrg = Re
|
|
|
108
108
|
schemaOrg: TSchemaOrg | null;
|
|
109
109
|
};
|
|
110
110
|
type BrandfineNavItemType = 'CUSTOM_URL' | 'POST' | 'HEADING';
|
|
111
|
-
type BrandfineNavItem = {
|
|
111
|
+
type BrandfineNavItem<TConfig = unknown> = {
|
|
112
112
|
id: string;
|
|
113
113
|
parentId: string | null;
|
|
114
114
|
position: number;
|
|
115
115
|
type: BrandfineNavItemType;
|
|
116
116
|
/** CUSTOM_URL: free-form path or absolute URL. */
|
|
117
117
|
customUrl: string | null;
|
|
118
|
+
/** Optional image URL for the item (icon, thumbnail, mega-menu
|
|
119
|
+
* art). Set on any item type; not per-locale. Null when unset. */
|
|
120
|
+
imageUrl: string | null;
|
|
121
|
+
/** Free-form per-item config the consumer interprets (badges,
|
|
122
|
+
* layout hints, flags). Brandfine round-trips it untouched —
|
|
123
|
+
* same contract as `BrandfinePost.customConfig`. Default
|
|
124
|
+
* `unknown`; parameterize `TConfig` to type it. Null when unset. */
|
|
125
|
+
customConfig: TConfig | null;
|
|
118
126
|
/** `{ [locale]: string }` map. May be null on POST items —
|
|
119
127
|
* consumers fall back to the post's per-locale `title` then. */
|
|
120
128
|
labels: Record<string, string> | null;
|
|
@@ -142,10 +150,10 @@ type BrandfineNavPost = {
|
|
|
142
150
|
title: string;
|
|
143
151
|
}>;
|
|
144
152
|
};
|
|
145
|
-
type BrandfineNavigation = {
|
|
153
|
+
type BrandfineNavigation<TConfig = unknown> = {
|
|
146
154
|
key: string;
|
|
147
155
|
name: string;
|
|
148
|
-
items: BrandfineNavItem[];
|
|
156
|
+
items: BrandfineNavItem<TConfig>[];
|
|
149
157
|
};
|
|
150
158
|
type ListPostsOptions = {
|
|
151
159
|
/** Post-type slug. Default server-side is `'blog'`; pass `'*'` to
|
|
@@ -258,19 +266,25 @@ declare function stripLocalePrefix(pathname: string, opts: LocaleOptions): strin
|
|
|
258
266
|
* - `hiddenLocales` drops the item entirely for that locale.
|
|
259
267
|
*/
|
|
260
268
|
|
|
261
|
-
type HydratedNavItem = {
|
|
269
|
+
type HydratedNavItem<TConfig = unknown> = {
|
|
262
270
|
/** Final URL, ready to render in an `<a href>`. `null` for
|
|
263
271
|
* HEADING items (label-only) and for POST items whose post
|
|
264
272
|
* has been deleted (orphan). */
|
|
265
273
|
href: string | null;
|
|
266
274
|
label: string;
|
|
267
275
|
type: 'CUSTOM_URL' | 'POST' | 'HEADING';
|
|
268
|
-
|
|
276
|
+
/** Optional image URL, passed through from the item. Not
|
|
277
|
+
* per-locale; `null` when unset. */
|
|
278
|
+
imageUrl: string | null;
|
|
279
|
+
/** Free-form per-item config, passed through untouched from the
|
|
280
|
+
* item. Default `unknown`; parameterize `TConfig` to type it. */
|
|
281
|
+
customConfig: TConfig | null;
|
|
282
|
+
children: HydratedNavItem<TConfig>[];
|
|
269
283
|
};
|
|
270
|
-
type HydratedNav = {
|
|
284
|
+
type HydratedNav<TConfig = unknown> = {
|
|
271
285
|
key: string;
|
|
272
286
|
name: string;
|
|
273
|
-
items: HydratedNavItem[];
|
|
287
|
+
items: HydratedNavItem<TConfig>[];
|
|
274
288
|
};
|
|
275
289
|
type ResolveNavigationOptions = {
|
|
276
290
|
defaultLocale: string;
|
|
@@ -287,6 +301,6 @@ type ResolveNavigationOptions = {
|
|
|
287
301
|
defaultLocale: string;
|
|
288
302
|
}) => string | null;
|
|
289
303
|
};
|
|
290
|
-
declare function resolveNavigation(nav: BrandfineNavigation
|
|
304
|
+
declare function resolveNavigation<TConfig = unknown>(nav: BrandfineNavigation<TConfig>, locale: string, opts: ResolveNavigationOptions): HydratedNav<TConfig>;
|
|
291
305
|
|
|
292
306
|
export { type BrandfinePost as B, type HydratedNav as H, type ListPostsOptions as L, type ResolveNavigationOptions as R, type ListCategoriesOptions as a, type BrandfineCategory as b, type BrandfineWorkspace as c, type BrandfineNavigation as d, type BrandfineNavItem as e, type BrandfineNavItemType as f, type BrandfineNavPost as g, type BrandfinePostListResponse as h, type BrandfinePostTranslation as i, type HydratedNavItem as j, type LocaleOptions as k, isLocale as l, localizePath as m, pickLocale as p, resolveNavigation as r, stripLocalePrefix as s };
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
|
|
4
|
-
var
|
|
4
|
+
var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
|
|
5
5
|
var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
@@ -21,6 +21,7 @@ var BrandfineApiError = class extends Error {
|
|
|
21
21
|
this.url = args.url;
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
+
var INSTALLED_MARKER = "data-brandfine-analytics";
|
|
24
25
|
var DEFAULT_USER_AGENT = "@brandfine/client";
|
|
25
26
|
function createBrandfineClient(config) {
|
|
26
27
|
if (!config.baseUrl)
|
|
@@ -106,7 +107,34 @@ function createBrandfineClient(config) {
|
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
};
|
|
109
|
-
|
|
110
|
+
const analytics = {
|
|
111
|
+
getConfig() {
|
|
112
|
+
return get("/external/analytics-config");
|
|
113
|
+
},
|
|
114
|
+
async install(opts = {}) {
|
|
115
|
+
if (typeof document === "undefined") {
|
|
116
|
+
return { installed: false, reason: "ssr" };
|
|
117
|
+
}
|
|
118
|
+
const cfg = opts.config ?? await analytics.getConfig();
|
|
119
|
+
if (!cfg.enabled) {
|
|
120
|
+
return { installed: false, reason: "disabled" };
|
|
121
|
+
}
|
|
122
|
+
const existing = document.querySelector(
|
|
123
|
+
`script[${INSTALLED_MARKER}="${cfg.websiteId}"]`
|
|
124
|
+
);
|
|
125
|
+
if (existing) {
|
|
126
|
+
return { installed: false, reason: "already-installed" };
|
|
127
|
+
}
|
|
128
|
+
const script = document.createElement("script");
|
|
129
|
+
script.defer = true;
|
|
130
|
+
script.src = cfg.scriptUrl;
|
|
131
|
+
script.setAttribute("data-website-id", cfg.websiteId);
|
|
132
|
+
script.setAttribute(INSTALLED_MARKER, cfg.websiteId);
|
|
133
|
+
document.head.appendChild(script);
|
|
134
|
+
return { installed: true, websiteId: cfg.websiteId };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return { get, posts, categories, workspace, navigations, analytics };
|
|
110
138
|
}
|
|
111
139
|
|
|
112
140
|
// src/index.ts
|
|
@@ -122,23 +150,23 @@ Object.defineProperty(exports, "createKeyedCache", {
|
|
|
122
150
|
});
|
|
123
151
|
Object.defineProperty(exports, "isLocale", {
|
|
124
152
|
enumerable: true,
|
|
125
|
-
get: function () { return
|
|
153
|
+
get: function () { return chunkFCK7QJBC_cjs.isLocale; }
|
|
126
154
|
});
|
|
127
155
|
Object.defineProperty(exports, "localizePath", {
|
|
128
156
|
enumerable: true,
|
|
129
|
-
get: function () { return
|
|
157
|
+
get: function () { return chunkFCK7QJBC_cjs.localizePath; }
|
|
130
158
|
});
|
|
131
159
|
Object.defineProperty(exports, "pickLocale", {
|
|
132
160
|
enumerable: true,
|
|
133
|
-
get: function () { return
|
|
161
|
+
get: function () { return chunkFCK7QJBC_cjs.pickLocale; }
|
|
134
162
|
});
|
|
135
163
|
Object.defineProperty(exports, "resolveNavigation", {
|
|
136
164
|
enumerable: true,
|
|
137
|
-
get: function () { return
|
|
165
|
+
get: function () { return chunkFCK7QJBC_cjs.resolveNavigation; }
|
|
138
166
|
});
|
|
139
167
|
Object.defineProperty(exports, "stripLocalePrefix", {
|
|
140
168
|
enumerable: true,
|
|
141
|
-
get: function () { return
|
|
169
|
+
get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
|
|
142
170
|
});
|
|
143
171
|
Object.defineProperty(exports, "createBrandfineWebhookHandler", {
|
|
144
172
|
enumerable: true,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":";;;;;;;AA8CO,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EACzB,IAAA,GAAO,mBAAA;AAAA,EAChB,MAAA;AAAA,EACA,UAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EAET,YAAY,IAAA,EAKT;AACD,IAAA,KAAA;AAAA,MACE,CAAA,YAAA,EAAe,IAAA,CAAK,MAAM,CAAA,CAAA,EAAI,KAAK,UAAU,CAAA,IAAA,EAAO,IAAA,CAAK,GAAG,WAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,KAC3F;AACA,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,UAAA;AACvB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA;AAAA,EAClB;AACF;AAoDA,IAAM,kBAAA,GAAqB,mBAAA;AAEpB,SAAS,sBACd,MAAA,EACiB;AACjB,EAAA,IAAI,CAAC,MAAA,CAAO,OAAA;AACV,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAChE,EAAA,IAAI,CAAC,MAAA,CAAO,MAAA;AACV,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAE/D,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,EAAA,MAAM,SAAS,MAAA,CAAO,MAAA;AAGtB,EAAA,MAAM,SAAA,GAA0B,MAAA,CAAO,KAAA,IAAS,UAAA,CAAW,KAAA;AAC3D,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,kBAAA;AAEtC,EAAA,eAAe,GAAA,CAAO,IAAA,EAAc,IAAA,GAAuB,EAAC,EAAe;AACzE,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAA,EAAK;AAAA,MAC/B,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,WAAA,EAAa,MAAA;AAAA,QACb,MAAA,EAAQ,kBAAA;AAAA,QACR,YAAA,EAAc;AAAA,OAChB;AAAA,MACA,QAAQ,IAAA,CAAK;AAAA,KACd,CAAA;AACD,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,GAAA,IAAO,IAAA,CAAK,WAAA,EAAa;AAI1C,MAAA,MAAM,GAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC/B,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC5C,MAAA,MAAM,IAAI,iBAAA,CAAkB;AAAA,QAC1B,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,YAAY,GAAA,CAAI,UAAA;AAAA,QAChB,IAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AACA,IAAA,OAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,EACzB;AAEA,EAAA,MAAM,KAAA,GAAkB;AAAA,IACtB,MAAM,IAAA,CAAwB,IAAA,GAAyB,EAAC,EAAG;AACzD,MAAA,MAAM,MAAgC,EAAC;AACvC,MAAA,IAAI,IAAA,GAAO,CAAA;AACX,MAAA,MAAM,SAAA,GAAY,KAAK,IAAA,GAAO,CAAA,MAAA,EAAS,mBAAmB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,GAAK,EAAA;AACzE,MAAA,MAAM,WAAA,GAAc,KAAK,MAAA,GACrB,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAC1C,EAAA;AAIJ,MAAA,MAAM,YAAY,IAAA,CAAK,UAAA,GACnB,CAAA,aAAA,EAAgB,IAAA,CAAK,UAAU,CAAA,CAAA,GAC/B,WAAA;AAIJ,MAAA,MAAM,SAAA,GAAY,GAAA;AAClB,MAAA,OAAO,QAAQ,SAAA,EAAW;AACxB,QAAA,MAAM,OAAO,MAAM,GAAA;AAAA,UACjB,kCAAkC,SAAS,CAAA,MAAA,EAAS,IAAI,CAAA,EAAG,SAAS,GAAG,WAAW,CAAA;AAAA,SACpF;AACA,QAAA,GAAA,CAAI,IAAA,CAAK,GAAG,IAAA,CAAK,KAAK,CAAA;AACtB,QAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS;AAC5B,QAAA,IAAA,IAAQ,CAAA;AAAA,MACV;AACA,MAAA,OAAO,GAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,UAA6B,IAAA,EAAc;AAC/C,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,gBAAA,EAAmB,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAAA,QAC3C,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,UAAA,GAA4B;AAAA,IAChC,MAAM,IAAA,CAAK,IAAA,GAA8B,EAAC,EAAG;AAC3C,MAAA,MAAM,EAAA,GAAK,KAAK,MAAA,GAAS,CAAA,QAAA,EAAW,mBAAmB,IAAA,CAAK,MAAM,CAAC,CAAA,CAAA,GAAK,EAAA;AACxE,MAAA,MAAM,OAAO,MAAM,GAAA;AAAA,QACjB,uBAAuB,EAAE,CAAA;AAAA,OAC3B;AACA,MAAA,OAAO,IAAA,CAAK,KAAA;AAAA,IACd;AAAA,GACF;AAEA,EAAA,MAAM,SAAA,GAA0B;AAAA,IAC9B,GAAA,GAGI;AACF,MAAA,OAAO,GAAA;AAAA,QACL;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,MAAM,WAAA,GAA8B;AAAA,IAClC,IAAI,GAAA,EAAa;AACf,MAAA,OAAO,GAAA;AAAA,QACL,CAAA,sBAAA,EAAyB,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AAAA,QAChD,EAAE,aAAa,IAAA;AAAK,OACtB;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO,EAAE,GAAA,EAAK,KAAA,EAAO,UAAA,EAAY,WAAW,WAAA,EAAY;AAC1D;;;AC5NO,IAAM,WAAA,GAAc","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. */\n get: (key: string) => Promise<BrandfineNavigation | null>\n}\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get(key: string) {\n return get<BrandfineNavigation | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n return { get, posts, categories, workspace, navigations }\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type BrandfineClient,\n type BrandfineClientConfig,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n return { get, posts, categories, workspace, navigations, analytics }\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type InstallOptions,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-
|
|
2
|
-
export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, i as BrandfinePostTranslation, H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-
|
|
1
|
+
import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-32jeQf1C.cjs';
|
|
2
|
+
export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, i as BrandfinePostTranslation, H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-32jeQf1C.cjs';
|
|
3
3
|
export { Cache, CacheOptions, KeyedCache, KeyedCacheOptions, createCache, createKeyedCache } from './cache/index.cjs';
|
|
4
4
|
export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhookPayload, createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './webhook/index.cjs';
|
|
5
5
|
|
|
@@ -66,6 +66,7 @@ type BrandfineClient = {
|
|
|
66
66
|
categories: CategoriesApi;
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
|
+
analytics: AnalyticsApi;
|
|
69
70
|
};
|
|
70
71
|
type PostsApi = {
|
|
71
72
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -85,8 +86,76 @@ type WorkspaceApi = {
|
|
|
85
86
|
type NavigationsApi = {
|
|
86
87
|
/** Navigation by its workspace-scoped `key` (e.g. `'header'`).
|
|
87
88
|
* Returns `null` for 404 so consumers can fall back to a
|
|
88
|
-
* hardcoded default without try/catch.
|
|
89
|
-
|
|
89
|
+
* hardcoded default without try/catch. `TConfig` narrows each
|
|
90
|
+
* item's `customConfig` (default `unknown`). */
|
|
91
|
+
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
92
|
+
};
|
|
93
|
+
type AnalyticsConfig = {
|
|
94
|
+
enabled: false;
|
|
95
|
+
} | {
|
|
96
|
+
enabled: true;
|
|
97
|
+
websiteId: string;
|
|
98
|
+
scriptUrl: string;
|
|
99
|
+
};
|
|
100
|
+
type AnalyticsInstallResult = {
|
|
101
|
+
installed: false;
|
|
102
|
+
reason: 'disabled' | 'ssr' | 'already-installed';
|
|
103
|
+
} | {
|
|
104
|
+
installed: true;
|
|
105
|
+
websiteId: string;
|
|
106
|
+
};
|
|
107
|
+
type InstallOptions = {
|
|
108
|
+
/**
|
|
109
|
+
* Pre-known config. When provided, `install()` skips the round-
|
|
110
|
+
* trip to `/external/analytics-config` and injects the script
|
|
111
|
+
* immediately. Use this when you've baked the values into your
|
|
112
|
+
* build (env vars, CMS-side config dump, etc.) — typical for
|
|
113
|
+
* static sites where the analytics state is decided at deploy
|
|
114
|
+
* time, not per page load.
|
|
115
|
+
*
|
|
116
|
+
* Trade-off vs the default fetch path: if you disable analytics
|
|
117
|
+
* in Brandfine, the tracker keeps loading until your next
|
|
118
|
+
* deploy. That's usually the right trade for static sites
|
|
119
|
+
* (which redeploy on every content change anyway) and the wrong
|
|
120
|
+
* trade for dynamic sites where the api round-trip is cheap
|
|
121
|
+
* relative to the rest of the page.
|
|
122
|
+
*
|
|
123
|
+
* Pass `{ enabled: false }` to force a no-op without touching
|
|
124
|
+
* the api (e.g. to disable analytics for one environment without
|
|
125
|
+
* changing Brandfine's state).
|
|
126
|
+
*/
|
|
127
|
+
config?: AnalyticsConfig;
|
|
128
|
+
};
|
|
129
|
+
type AnalyticsApi = {
|
|
130
|
+
/**
|
|
131
|
+
* Injects the Brandfine analytics tracker into `document.head`
|
|
132
|
+
* once. Safe to call on every page load — idempotent via a
|
|
133
|
+
* marker attribute on the injected script tag.
|
|
134
|
+
*
|
|
135
|
+
* Two paths:
|
|
136
|
+
* - `install()` — fetches the config from Brandfine, then
|
|
137
|
+
* injects. Reflects enable/disable state on next page load.
|
|
138
|
+
* - `install({ config })` — uses caller-provided config, skips
|
|
139
|
+
* the fetch. Faster, no round-trip; ignores Brandfine state
|
|
140
|
+
* changes until the consumer's next deploy.
|
|
141
|
+
*
|
|
142
|
+
* Returns details about what happened:
|
|
143
|
+
* - `{ installed: true, websiteId }` — script was just injected.
|
|
144
|
+
* - `{ installed: false, reason: 'disabled' }` — config says
|
|
145
|
+
* analytics is off; no-op.
|
|
146
|
+
* - `{ installed: false, reason: 'ssr' }` — no `document` in
|
|
147
|
+
* scope (server-side). Call again on the client.
|
|
148
|
+
* - `{ installed: false, reason: 'already-installed' }` — a
|
|
149
|
+
* prior call (or another tab in the same SPA) already injected.
|
|
150
|
+
*
|
|
151
|
+
* Throws `BrandfineApiError` on non-2xx responses other than the
|
|
152
|
+
* disabled case (which is a valid `{ enabled: false }` body).
|
|
153
|
+
*/
|
|
154
|
+
install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>;
|
|
155
|
+
/** Lower-level helper — fetches the raw config without touching
|
|
156
|
+
* the DOM. Useful when you want to inject the script yourself
|
|
157
|
+
* (e.g. via a framework's <Script> component for nonce/csp). */
|
|
158
|
+
getConfig: () => Promise<AnalyticsConfig>;
|
|
90
159
|
};
|
|
91
160
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
92
161
|
|
|
@@ -104,4 +173,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
104
173
|
*/
|
|
105
174
|
declare const SDK_VERSION: "0.0.0";
|
|
106
175
|
|
|
107
|
-
export { BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
176
|
+
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-
|
|
2
|
-
export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, i as BrandfinePostTranslation, H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-
|
|
1
|
+
import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-32jeQf1C.js';
|
|
2
|
+
export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, i as BrandfinePostTranslation, H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-32jeQf1C.js';
|
|
3
3
|
export { Cache, CacheOptions, KeyedCache, KeyedCacheOptions, createCache, createKeyedCache } from './cache/index.js';
|
|
4
4
|
export { BrandfineWebhookEvent, BrandfineWebhookHandlerOptions, BrandfineWebhookPayload, createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './webhook/index.js';
|
|
5
5
|
|
|
@@ -66,6 +66,7 @@ type BrandfineClient = {
|
|
|
66
66
|
categories: CategoriesApi;
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
|
+
analytics: AnalyticsApi;
|
|
69
70
|
};
|
|
70
71
|
type PostsApi = {
|
|
71
72
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -85,8 +86,76 @@ type WorkspaceApi = {
|
|
|
85
86
|
type NavigationsApi = {
|
|
86
87
|
/** Navigation by its workspace-scoped `key` (e.g. `'header'`).
|
|
87
88
|
* Returns `null` for 404 so consumers can fall back to a
|
|
88
|
-
* hardcoded default without try/catch.
|
|
89
|
-
|
|
89
|
+
* hardcoded default without try/catch. `TConfig` narrows each
|
|
90
|
+
* item's `customConfig` (default `unknown`). */
|
|
91
|
+
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
92
|
+
};
|
|
93
|
+
type AnalyticsConfig = {
|
|
94
|
+
enabled: false;
|
|
95
|
+
} | {
|
|
96
|
+
enabled: true;
|
|
97
|
+
websiteId: string;
|
|
98
|
+
scriptUrl: string;
|
|
99
|
+
};
|
|
100
|
+
type AnalyticsInstallResult = {
|
|
101
|
+
installed: false;
|
|
102
|
+
reason: 'disabled' | 'ssr' | 'already-installed';
|
|
103
|
+
} | {
|
|
104
|
+
installed: true;
|
|
105
|
+
websiteId: string;
|
|
106
|
+
};
|
|
107
|
+
type InstallOptions = {
|
|
108
|
+
/**
|
|
109
|
+
* Pre-known config. When provided, `install()` skips the round-
|
|
110
|
+
* trip to `/external/analytics-config` and injects the script
|
|
111
|
+
* immediately. Use this when you've baked the values into your
|
|
112
|
+
* build (env vars, CMS-side config dump, etc.) — typical for
|
|
113
|
+
* static sites where the analytics state is decided at deploy
|
|
114
|
+
* time, not per page load.
|
|
115
|
+
*
|
|
116
|
+
* Trade-off vs the default fetch path: if you disable analytics
|
|
117
|
+
* in Brandfine, the tracker keeps loading until your next
|
|
118
|
+
* deploy. That's usually the right trade for static sites
|
|
119
|
+
* (which redeploy on every content change anyway) and the wrong
|
|
120
|
+
* trade for dynamic sites where the api round-trip is cheap
|
|
121
|
+
* relative to the rest of the page.
|
|
122
|
+
*
|
|
123
|
+
* Pass `{ enabled: false }` to force a no-op without touching
|
|
124
|
+
* the api (e.g. to disable analytics for one environment without
|
|
125
|
+
* changing Brandfine's state).
|
|
126
|
+
*/
|
|
127
|
+
config?: AnalyticsConfig;
|
|
128
|
+
};
|
|
129
|
+
type AnalyticsApi = {
|
|
130
|
+
/**
|
|
131
|
+
* Injects the Brandfine analytics tracker into `document.head`
|
|
132
|
+
* once. Safe to call on every page load — idempotent via a
|
|
133
|
+
* marker attribute on the injected script tag.
|
|
134
|
+
*
|
|
135
|
+
* Two paths:
|
|
136
|
+
* - `install()` — fetches the config from Brandfine, then
|
|
137
|
+
* injects. Reflects enable/disable state on next page load.
|
|
138
|
+
* - `install({ config })` — uses caller-provided config, skips
|
|
139
|
+
* the fetch. Faster, no round-trip; ignores Brandfine state
|
|
140
|
+
* changes until the consumer's next deploy.
|
|
141
|
+
*
|
|
142
|
+
* Returns details about what happened:
|
|
143
|
+
* - `{ installed: true, websiteId }` — script was just injected.
|
|
144
|
+
* - `{ installed: false, reason: 'disabled' }` — config says
|
|
145
|
+
* analytics is off; no-op.
|
|
146
|
+
* - `{ installed: false, reason: 'ssr' }` — no `document` in
|
|
147
|
+
* scope (server-side). Call again on the client.
|
|
148
|
+
* - `{ installed: false, reason: 'already-installed' }` — a
|
|
149
|
+
* prior call (or another tab in the same SPA) already injected.
|
|
150
|
+
*
|
|
151
|
+
* Throws `BrandfineApiError` on non-2xx responses other than the
|
|
152
|
+
* disabled case (which is a valid `{ enabled: false }` body).
|
|
153
|
+
*/
|
|
154
|
+
install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>;
|
|
155
|
+
/** Lower-level helper — fetches the raw config without touching
|
|
156
|
+
* the DOM. Useful when you want to inject the script yourself
|
|
157
|
+
* (e.g. via a framework's <Script> component for nonce/csp). */
|
|
158
|
+
getConfig: () => Promise<AnalyticsConfig>;
|
|
90
159
|
};
|
|
91
160
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
92
161
|
|
|
@@ -104,4 +173,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
104
173
|
*/
|
|
105
174
|
declare const SDK_VERSION: "0.0.0";
|
|
106
175
|
|
|
107
|
-
export { BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
176
|
+
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
|
|
2
|
-
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-
|
|
2
|
+
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
|
|
3
3
|
export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
|
|
4
4
|
|
|
5
5
|
// src/client.ts
|
|
@@ -19,6 +19,7 @@ var BrandfineApiError = class extends Error {
|
|
|
19
19
|
this.url = args.url;
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
+
var INSTALLED_MARKER = "data-brandfine-analytics";
|
|
22
23
|
var DEFAULT_USER_AGENT = "@brandfine/client";
|
|
23
24
|
function createBrandfineClient(config) {
|
|
24
25
|
if (!config.baseUrl)
|
|
@@ -104,7 +105,34 @@ function createBrandfineClient(config) {
|
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
};
|
|
107
|
-
|
|
108
|
+
const analytics = {
|
|
109
|
+
getConfig() {
|
|
110
|
+
return get("/external/analytics-config");
|
|
111
|
+
},
|
|
112
|
+
async install(opts = {}) {
|
|
113
|
+
if (typeof document === "undefined") {
|
|
114
|
+
return { installed: false, reason: "ssr" };
|
|
115
|
+
}
|
|
116
|
+
const cfg = opts.config ?? await analytics.getConfig();
|
|
117
|
+
if (!cfg.enabled) {
|
|
118
|
+
return { installed: false, reason: "disabled" };
|
|
119
|
+
}
|
|
120
|
+
const existing = document.querySelector(
|
|
121
|
+
`script[${INSTALLED_MARKER}="${cfg.websiteId}"]`
|
|
122
|
+
);
|
|
123
|
+
if (existing) {
|
|
124
|
+
return { installed: false, reason: "already-installed" };
|
|
125
|
+
}
|
|
126
|
+
const script = document.createElement("script");
|
|
127
|
+
script.defer = true;
|
|
128
|
+
script.src = cfg.scriptUrl;
|
|
129
|
+
script.setAttribute("data-website-id", cfg.websiteId);
|
|
130
|
+
script.setAttribute(INSTALLED_MARKER, cfg.websiteId);
|
|
131
|
+
document.head.appendChild(script);
|
|
132
|
+
return { installed: true, websiteId: cfg.websiteId };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return { get, posts, categories, workspace, navigations, analytics };
|
|
108
136
|
}
|
|
109
137
|
|
|
110
138
|
// src/index.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. */\n get: (key: string) => Promise<BrandfineNavigation | null>\n}\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get(key: string) {\n return get<BrandfineNavigation | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n return { get, posts, categories, workspace, navigations }\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type BrandfineClient,\n type BrandfineClientConfig,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n analytics: AnalyticsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. `TConfig` narrows each\n * item's `customConfig` (default `unknown`). */\n get: <TConfig = unknown>(\n key: string,\n ) => Promise<BrandfineNavigation<TConfig> | null>\n}\n\nexport type AnalyticsConfig =\n | { enabled: false }\n | { enabled: true; websiteId: string; scriptUrl: string }\n\nexport type AnalyticsInstallResult =\n | { installed: false; reason: 'disabled' | 'ssr' | 'already-installed' }\n | { installed: true; websiteId: string }\n\nexport type InstallOptions = {\n /**\n * Pre-known config. When provided, `install()` skips the round-\n * trip to `/external/analytics-config` and injects the script\n * immediately. Use this when you've baked the values into your\n * build (env vars, CMS-side config dump, etc.) — typical for\n * static sites where the analytics state is decided at deploy\n * time, not per page load.\n *\n * Trade-off vs the default fetch path: if you disable analytics\n * in Brandfine, the tracker keeps loading until your next\n * deploy. That's usually the right trade for static sites\n * (which redeploy on every content change anyway) and the wrong\n * trade for dynamic sites where the api round-trip is cheap\n * relative to the rest of the page.\n *\n * Pass `{ enabled: false }` to force a no-op without touching\n * the api (e.g. to disable analytics for one environment without\n * changing Brandfine's state).\n */\n config?: AnalyticsConfig\n}\n\ntype AnalyticsApi = {\n /**\n * Injects the Brandfine analytics tracker into `document.head`\n * once. Safe to call on every page load — idempotent via a\n * marker attribute on the injected script tag.\n *\n * Two paths:\n * - `install()` — fetches the config from Brandfine, then\n * injects. Reflects enable/disable state on next page load.\n * - `install({ config })` — uses caller-provided config, skips\n * the fetch. Faster, no round-trip; ignores Brandfine state\n * changes until the consumer's next deploy.\n *\n * Returns details about what happened:\n * - `{ installed: true, websiteId }` — script was just injected.\n * - `{ installed: false, reason: 'disabled' }` — config says\n * analytics is off; no-op.\n * - `{ installed: false, reason: 'ssr' }` — no `document` in\n * scope (server-side). Call again on the client.\n * - `{ installed: false, reason: 'already-installed' }` — a\n * prior call (or another tab in the same SPA) already injected.\n *\n * Throws `BrandfineApiError` on non-2xx responses other than the\n * disabled case (which is a valid `{ enabled: false }` body).\n */\n install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>\n\n /** Lower-level helper — fetches the raw config without touching\n * the DOM. Useful when you want to inject the script yourself\n * (e.g. via a framework's <Script> component for nonce/csp). */\n getConfig: () => Promise<AnalyticsConfig>\n}\n\n/** Attribute we stamp on the injected <script> so `install()` is\n * idempotent across re-renders and SPA route changes. */\nconst INSTALLED_MARKER = 'data-brandfine-analytics'\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get<TConfig = unknown>(key: string) {\n return get<BrandfineNavigation<TConfig> | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n const analytics: AnalyticsApi = {\n getConfig() {\n return get<AnalyticsConfig>('/external/analytics-config')\n },\n async install(opts: InstallOptions = {}) {\n // SSR safety: nothing to inject without a DOM. Consumers\n // call this from useEffect / onMount, but defensive anyway\n // (some frameworks still execute the file body on the server).\n if (typeof document === 'undefined') {\n return { installed: false, reason: 'ssr' as const }\n }\n\n // Use caller-provided config if present (build-time path),\n // otherwise fetch (runtime path).\n const cfg = opts.config ?? (await analytics.getConfig())\n if (!cfg.enabled) {\n return { installed: false, reason: 'disabled' as const }\n }\n\n // Idempotency: a prior call (StrictMode double-invoke, SPA\n // re-mount, second instance with the same workspace) may\n // have already injected. The marker attribute is the source\n // of truth — checking by script src would also miss the case\n // where two workspaces share the same scriptUrl.\n const existing = document.querySelector<HTMLScriptElement>(\n `script[${INSTALLED_MARKER}=\"${cfg.websiteId}\"]`,\n )\n if (existing) {\n return { installed: false, reason: 'already-installed' as const }\n }\n\n const script = document.createElement('script')\n script.defer = true\n script.src = cfg.scriptUrl\n script.setAttribute('data-website-id', cfg.websiteId)\n // The marker doubles as a sentinel + a debug aid (you can\n // grep the DOM for `data-brandfine-analytics` to confirm\n // an install).\n script.setAttribute(INSTALLED_MARKER, cfg.websiteId)\n document.head.appendChild(script)\n return { installed: true, websiteId: cfg.websiteId }\n },\n }\n\n return { get, posts, categories, workspace, navigations, analytics }\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type AnalyticsConfig,\n type AnalyticsInstallResult,\n type BrandfineClient,\n type BrandfineClientConfig,\n type InstallOptions,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfinePostTranslation,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
|
package/dist/resolvers/index.cjs
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkFCK7QJBC_cjs = require('../chunk-FCK7QJBC.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, "isLocale", {
|
|
8
8
|
enumerable: true,
|
|
9
|
-
get: function () { return
|
|
9
|
+
get: function () { return chunkFCK7QJBC_cjs.isLocale; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "localizePath", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkFCK7QJBC_cjs.localizePath; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "pickLocale", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkFCK7QJBC_cjs.pickLocale; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "resolveNavigation", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
21
|
+
get: function () { return chunkFCK7QJBC_cjs.resolveNavigation; }
|
|
22
22
|
});
|
|
23
23
|
Object.defineProperty(exports, "stripLocalePrefix", {
|
|
24
24
|
enumerable: true,
|
|
25
|
-
get: function () { return
|
|
25
|
+
get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
|
|
26
26
|
});
|
|
27
27
|
//# sourceMappingURL=index.cjs.map
|
|
28
28
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-
|
|
1
|
+
export { H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-32jeQf1C.cjs';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-
|
|
1
|
+
export { H as HydratedNav, j as HydratedNavItem, k as LocaleOptions, R as ResolveNavigationOptions, l as isLocale, m as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-32jeQf1C.js';
|
package/dist/resolvers/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from '../chunk-
|
|
1
|
+
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from '../chunk-U6VJX7PP.js';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandfine/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Brandfine consumer SDK — typed HTTP client, server-side caches, locale + navigation resolvers, and webhook helpers for landing-page integrations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-MTBSSTTG.js","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n children: HydratedNavItem[]\n}\n\nexport type HydratedNav = {\n key: string\n name: string\n items: HydratedNavItem[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation(\n nav: BrandfineNavigation,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav {\n const childrenByParent = new Map<string, BrandfineNavItem[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate(\n item: BrandfineNavItem,\n children: BrandfineNavItem[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-OKIEA3AD.cjs","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n children: HydratedNavItem[]\n}\n\nexport type HydratedNav = {\n key: string\n name: string\n items: HydratedNavItem[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation(\n nav: BrandfineNavigation,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav {\n const childrenByParent = new Map<string, BrandfineNavItem[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate(\n item: BrandfineNavItem,\n children: BrandfineNavItem[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}
|