@brandfine/client 0.3.0 → 0.5.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 +62 -4
- package/README.md +65 -16
- package/dist/{chunk-2VCPYNF5.cjs → chunk-FCK7QJBC.cjs} +3 -2
- package/dist/chunk-FCK7QJBC.cjs.map +1 -0
- package/dist/{chunk-3OXDF4ZY.js → chunk-U6VJX7PP.js} +3 -2
- package/dist/chunk-U6VJX7PP.js.map +1 -0
- package/dist/{index-D4SS91_D.d.cts → index-32jeQf1C.d.cts} +16 -8
- package/dist/{index-D4SS91_D.d.ts → index-32jeQf1C.d.ts} +16 -8
- package/dist/index.cjs +79 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -5
- package/dist/index.d.ts +109 -5
- package/dist/index.js +63 -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-2VCPYNF5.cjs.map +0 -1
- package/dist/chunk-3OXDF4ZY.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,71 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 43231d6: Add `bf.submissions` namespace for posting contact-form submissions
|
|
8
|
+
to the workspace from any consumer site.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
await bf.submissions.create({
|
|
12
|
+
name: "Yiğit",
|
|
13
|
+
email: "yigit@example.com",
|
|
14
|
+
message: "Hello from the contact form!",
|
|
15
|
+
// optional:
|
|
16
|
+
phone: "+90 …",
|
|
17
|
+
subject: "Demo request",
|
|
18
|
+
source: "/contact",
|
|
19
|
+
metadata: { utm_source: "twitter" },
|
|
20
|
+
});
|
|
21
|
+
// → { id, createdAt }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Wraps `POST /external/submissions` (already on the api). Validation
|
|
25
|
+
errors throw `BrandfineApiError` with the api's response body
|
|
26
|
+
attached so the caller can render field-level messages.
|
|
27
|
+
|
|
28
|
+
New `CreateSubmissionInput` and `Submission` types are exported
|
|
29
|
+
from the root entry.
|
|
30
|
+
|
|
31
|
+
## 0.4.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- 34d0a9d: Add `bf.analytics` namespace for the self-hosted Brandfine analytics integration.
|
|
36
|
+
- `bf.analytics.getConfig()` — fetches this workspace's analytics setup from
|
|
37
|
+
Brandfine. Returns `{ enabled: false }` when analytics isn't enabled in the
|
|
38
|
+
cms, or `{ enabled: true, websiteId, scriptUrl }` when it is.
|
|
39
|
+
- `bf.analytics.install(opts?)` — injects the tracker script into
|
|
40
|
+
`document.head` once. Idempotent across re-renders via a marker attribute.
|
|
41
|
+
By default fetches the config first; pass `{ config }` to skip the round-trip
|
|
42
|
+
when you already have it (e.g. server-component prefetch in static-export
|
|
43
|
+
Next.js sites).
|
|
44
|
+
|
|
45
|
+
The runtime path (`install()` with no args) reflects enable/disable state
|
|
46
|
+
on the next page load — costs one HTTP round-trip per page. The build-time
|
|
47
|
+
path (`install({ config })`) injects with zero round-trips but ignores
|
|
48
|
+
Brandfine state changes until the consumer's next deploy. Static sites
|
|
49
|
+
typically want the latter; dynamic sites the former.
|
|
50
|
+
|
|
51
|
+
New `AnalyticsConfig`, `AnalyticsInstallResult`, and `InstallOptions` types
|
|
52
|
+
are exported from the root entry.
|
|
53
|
+
|
|
3
54
|
## 0.3.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
|
6
57
|
|
|
7
|
-
- c41e51d:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
58
|
+
- c41e51d: Navigation items gain two new fields, both served by
|
|
59
|
+
`GET /external/navigations/:key` and passed through the navigation resolver:
|
|
60
|
+
- `imageUrl` — optional image URL (icon, thumbnail, mega-menu art) on any
|
|
61
|
+
item type. `null` when unset.
|
|
62
|
+
- `customConfig` — free-form per-item JSON, round-tripped untouched (same
|
|
63
|
+
contract as `BrandfinePost.customConfig`). `BrandfineNavItem`,
|
|
64
|
+
`BrandfineNavigation`, `bf.navigations.get`, `resolveNavigation`, and
|
|
65
|
+
`HydratedNavItem` are now generic over `TConfig` (default `unknown`) so
|
|
66
|
+
consumers can type it: `bf.navigations.get<MyNavConfig>('header')`.
|
|
67
|
+
|
|
68
|
+
Additive, no breaking changes — existing untyped usage keeps working.
|
|
11
69
|
|
|
12
70
|
## 0.2.0
|
|
13
71
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @brandfine/client
|
|
2
2
|
|
|
3
|
-
Typed HTTP client, server-side caches, locale + navigation resolvers,
|
|
3
|
+
Typed HTTP client, server-side caches, locale + navigation resolvers, webhook helpers, and analytics tracker installer for landing pages consuming the [Brandfine CMS](https://docs.brandfine.co).
|
|
4
4
|
|
|
5
5
|
> **Status:** early development (`0.x.y`). API may change in minor bumps before `1.0`.
|
|
6
6
|
|
|
@@ -15,26 +15,75 @@ Available on public npm with [provenance](https://docs.npmjs.com/generating-prov
|
|
|
15
15
|
## Subpath exports
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
createBrandfineClient,
|
|
20
|
+
type AnalyticsConfig,
|
|
21
|
+
type AnalyticsInstallResult,
|
|
22
|
+
} from '@brandfine/client'
|
|
23
|
+
import { createCache, createKeyedCache } from '@brandfine/client/cache'
|
|
20
24
|
import { resolveNavigation, localizePath } from '@brandfine/client/resolvers'
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
verifyWebhookSecret,
|
|
27
|
+
parseWebhookPayload,
|
|
28
|
+
} from '@brandfine/client/webhook'
|
|
22
29
|
```
|
|
23
30
|
|
|
24
31
|
Pick the import path that scopes to what you actually use — tree-shaking does the rest, but subpath imports keep consumer bundle analysis honest.
|
|
25
32
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const bf = createBrandfineClient({
|
|
37
|
+
baseUrl: 'https://api.brandfine.co',
|
|
38
|
+
apiKey: process.env.BRANDFINE_API_KEY!,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Content reads
|
|
42
|
+
await bf.posts.list({ type: 'blog', locale: 'pt' })
|
|
43
|
+
await bf.posts.getBySlug('uk-eta-2026')
|
|
44
|
+
await bf.workspace.get()
|
|
45
|
+
await bf.categories.list()
|
|
46
|
+
await bf.navigations.get('header')
|
|
47
|
+
|
|
48
|
+
// Analytics — auto-inject the Brandfine tracker into <head>
|
|
49
|
+
await bf.analytics.install()
|
|
50
|
+
|
|
51
|
+
// Submissions — POST a contact-form submission
|
|
52
|
+
await bf.submissions.create({
|
|
53
|
+
name: 'Alex',
|
|
54
|
+
email: 'alex@example.com',
|
|
55
|
+
message: 'Hello!',
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The full surface lives under six namespaces — `posts`, `categories`, `workspace`, `navigations`, `analytics`, `submissions` — plus a `get(path)` escape hatch.
|
|
60
|
+
|
|
61
|
+
## Analytics
|
|
62
|
+
|
|
63
|
+
`bf.analytics.install()` fetches this workspace's tracker config from Brandfine and injects `<script defer src="…" data-website-id="…">` into `document.head` exactly once. Idempotent across StrictMode double-invokes, SPA route changes, and multi-instance scenarios via a marker attribute on the injected tag.
|
|
64
|
+
|
|
65
|
+
For static sites (Next.js `output: 'export'`, Astro), use the **build-time** variant to keep the API key out of the browser bundle and skip the runtime round-trip:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// Server-side at build/request time:
|
|
69
|
+
const config = await bf.analytics.getConfig()
|
|
70
|
+
// Pass `config` to a client component that calls:
|
|
71
|
+
bf.analytics.install({ config })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Full walkthrough with framework recipes: [docs.brandfine.co/docs/sdk/analytics](https://docs.brandfine.co/docs/sdk/analytics).
|
|
75
|
+
|
|
76
|
+
## Caching
|
|
77
|
+
|
|
78
|
+
`createCache(opts)` and `createKeyedCache(opts)` are minimal SWR caches with TTL + background revalidation. They take any async fetcher — including `bf.posts.list` — and make the cached value the source of truth for hot paths. Pair with `verifyWebhookSecret` to invalidate on publish.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [SDK quickstart](https://docs.brandfine.co/docs/sdk/quickstart) — minimal Astro integration end-to-end.
|
|
83
|
+
- [`createBrandfineClient`](https://docs.brandfine.co/docs/sdk/client) — full options + method reference.
|
|
84
|
+
- [Analytics install](https://docs.brandfine.co/docs/sdk/analytics) — runtime vs build-time, framework recipes.
|
|
85
|
+
- [Webhook handler](https://docs.brandfine.co/docs/sdk/webhooks) — verify + parse + dispatch.
|
|
86
|
+
- [REST API reference](https://docs.brandfine.co/docs/api/authentication) — for non-TypeScript consumers.
|
|
38
87
|
|
|
39
88
|
## License
|
|
40
89
|
|
|
@@ -53,6 +53,7 @@ function hydrate(item, children, locale, opts) {
|
|
|
53
53
|
label: resolveLabel(item, locale, opts.defaultLocale),
|
|
54
54
|
type: item.type,
|
|
55
55
|
imageUrl: item.imageUrl ?? null,
|
|
56
|
+
customConfig: item.customConfig ?? null,
|
|
56
57
|
children: children.map((c) => hydrate(c, [], locale, opts))
|
|
57
58
|
};
|
|
58
59
|
}
|
|
@@ -104,5 +105,5 @@ exports.localizePath = localizePath;
|
|
|
104
105
|
exports.pickLocale = pickLocale;
|
|
105
106
|
exports.resolveNavigation = resolveNavigation;
|
|
106
107
|
exports.stripLocalePrefix = stripLocalePrefix;
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
108
|
-
//# 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"]}
|
|
@@ -51,6 +51,7 @@ function hydrate(item, children, locale, opts) {
|
|
|
51
51
|
label: resolveLabel(item, locale, opts.defaultLocale),
|
|
52
52
|
type: item.type,
|
|
53
53
|
imageUrl: item.imageUrl ?? null,
|
|
54
|
+
customConfig: item.customConfig ?? null,
|
|
54
55
|
children: children.map((c) => hydrate(c, [], locale, opts))
|
|
55
56
|
};
|
|
56
57
|
}
|
|
@@ -98,5 +99,5 @@ function resolveLabel(item, locale, defaultLocale) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix };
|
|
101
|
-
//# sourceMappingURL=chunk-
|
|
102
|
-
//# 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,7 +108,7 @@ 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;
|
|
@@ -118,6 +118,11 @@ type BrandfineNavItem = {
|
|
|
118
118
|
/** Optional image URL for the item (icon, thumbnail, mega-menu
|
|
119
119
|
* art). Set on any item type; not per-locale. Null when unset. */
|
|
120
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;
|
|
121
126
|
/** `{ [locale]: string }` map. May be null on POST items —
|
|
122
127
|
* consumers fall back to the post's per-locale `title` then. */
|
|
123
128
|
labels: Record<string, string> | null;
|
|
@@ -145,10 +150,10 @@ type BrandfineNavPost = {
|
|
|
145
150
|
title: string;
|
|
146
151
|
}>;
|
|
147
152
|
};
|
|
148
|
-
type BrandfineNavigation = {
|
|
153
|
+
type BrandfineNavigation<TConfig = unknown> = {
|
|
149
154
|
key: string;
|
|
150
155
|
name: string;
|
|
151
|
-
items: BrandfineNavItem[];
|
|
156
|
+
items: BrandfineNavItem<TConfig>[];
|
|
152
157
|
};
|
|
153
158
|
type ListPostsOptions = {
|
|
154
159
|
/** Post-type slug. Default server-side is `'blog'`; pass `'*'` to
|
|
@@ -261,7 +266,7 @@ declare function stripLocalePrefix(pathname: string, opts: LocaleOptions): strin
|
|
|
261
266
|
* - `hiddenLocales` drops the item entirely for that locale.
|
|
262
267
|
*/
|
|
263
268
|
|
|
264
|
-
type HydratedNavItem = {
|
|
269
|
+
type HydratedNavItem<TConfig = unknown> = {
|
|
265
270
|
/** Final URL, ready to render in an `<a href>`. `null` for
|
|
266
271
|
* HEADING items (label-only) and for POST items whose post
|
|
267
272
|
* has been deleted (orphan). */
|
|
@@ -271,12 +276,15 @@ type HydratedNavItem = {
|
|
|
271
276
|
/** Optional image URL, passed through from the item. Not
|
|
272
277
|
* per-locale; `null` when unset. */
|
|
273
278
|
imageUrl: string | null;
|
|
274
|
-
|
|
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>[];
|
|
275
283
|
};
|
|
276
|
-
type HydratedNav = {
|
|
284
|
+
type HydratedNav<TConfig = unknown> = {
|
|
277
285
|
key: string;
|
|
278
286
|
name: string;
|
|
279
|
-
items: HydratedNavItem[];
|
|
287
|
+
items: HydratedNavItem<TConfig>[];
|
|
280
288
|
};
|
|
281
289
|
type ResolveNavigationOptions = {
|
|
282
290
|
defaultLocale: string;
|
|
@@ -293,6 +301,6 @@ type ResolveNavigationOptions = {
|
|
|
293
301
|
defaultLocale: string;
|
|
294
302
|
}) => string | null;
|
|
295
303
|
};
|
|
296
|
-
declare function resolveNavigation(nav: BrandfineNavigation
|
|
304
|
+
declare function resolveNavigation<TConfig = unknown>(nav: BrandfineNavigation<TConfig>, locale: string, opts: ResolveNavigationOptions): HydratedNav<TConfig>;
|
|
297
305
|
|
|
298
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,7 +108,7 @@ 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;
|
|
@@ -118,6 +118,11 @@ type BrandfineNavItem = {
|
|
|
118
118
|
/** Optional image URL for the item (icon, thumbnail, mega-menu
|
|
119
119
|
* art). Set on any item type; not per-locale. Null when unset. */
|
|
120
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;
|
|
121
126
|
/** `{ [locale]: string }` map. May be null on POST items —
|
|
122
127
|
* consumers fall back to the post's per-locale `title` then. */
|
|
123
128
|
labels: Record<string, string> | null;
|
|
@@ -145,10 +150,10 @@ type BrandfineNavPost = {
|
|
|
145
150
|
title: string;
|
|
146
151
|
}>;
|
|
147
152
|
};
|
|
148
|
-
type BrandfineNavigation = {
|
|
153
|
+
type BrandfineNavigation<TConfig = unknown> = {
|
|
149
154
|
key: string;
|
|
150
155
|
name: string;
|
|
151
|
-
items: BrandfineNavItem[];
|
|
156
|
+
items: BrandfineNavItem<TConfig>[];
|
|
152
157
|
};
|
|
153
158
|
type ListPostsOptions = {
|
|
154
159
|
/** Post-type slug. Default server-side is `'blog'`; pass `'*'` to
|
|
@@ -261,7 +266,7 @@ declare function stripLocalePrefix(pathname: string, opts: LocaleOptions): strin
|
|
|
261
266
|
* - `hiddenLocales` drops the item entirely for that locale.
|
|
262
267
|
*/
|
|
263
268
|
|
|
264
|
-
type HydratedNavItem = {
|
|
269
|
+
type HydratedNavItem<TConfig = unknown> = {
|
|
265
270
|
/** Final URL, ready to render in an `<a href>`. `null` for
|
|
266
271
|
* HEADING items (label-only) and for POST items whose post
|
|
267
272
|
* has been deleted (orphan). */
|
|
@@ -271,12 +276,15 @@ type HydratedNavItem = {
|
|
|
271
276
|
/** Optional image URL, passed through from the item. Not
|
|
272
277
|
* per-locale; `null` when unset. */
|
|
273
278
|
imageUrl: string | null;
|
|
274
|
-
|
|
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>[];
|
|
275
283
|
};
|
|
276
|
-
type HydratedNav = {
|
|
284
|
+
type HydratedNav<TConfig = unknown> = {
|
|
277
285
|
key: string;
|
|
278
286
|
name: string;
|
|
279
|
-
items: HydratedNavItem[];
|
|
287
|
+
items: HydratedNavItem<TConfig>[];
|
|
280
288
|
};
|
|
281
289
|
type ResolveNavigationOptions = {
|
|
282
290
|
defaultLocale: string;
|
|
@@ -293,6 +301,6 @@ type ResolveNavigationOptions = {
|
|
|
293
301
|
defaultLocale: string;
|
|
294
302
|
}) => string | null;
|
|
295
303
|
};
|
|
296
|
-
declare function resolveNavigation(nav: BrandfineNavigation
|
|
304
|
+
declare function resolveNavigation<TConfig = unknown>(nav: BrandfineNavigation<TConfig>, locale: string, opts: ResolveNavigationOptions): HydratedNav<TConfig>;
|
|
297
305
|
|
|
298
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,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
|
|
4
|
-
var chunk2VCPYNF5_cjs = require('./chunk-2VCPYNF5.cjs');
|
|
5
4
|
var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
|
|
5
|
+
var chunkFCK7QJBC_cjs = require('./chunk-FCK7QJBC.cjs');
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
8
|
var BrandfineApiError = class extends Error {
|
|
@@ -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,67 @@ 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
|
+
const submissions = {
|
|
138
|
+
async create(input) {
|
|
139
|
+
const url = `${baseUrl}/external/submissions`;
|
|
140
|
+
const res = await fetchImpl(url, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
"X-Api-Key": apiKey,
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
"User-Agent": userAgent
|
|
147
|
+
},
|
|
148
|
+
body: JSON.stringify(input)
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const body = await res.text().catch(() => "");
|
|
152
|
+
throw new BrandfineApiError({
|
|
153
|
+
status: res.status,
|
|
154
|
+
statusText: res.statusText,
|
|
155
|
+
body,
|
|
156
|
+
url
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return await res.json();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
get,
|
|
164
|
+
posts,
|
|
165
|
+
categories,
|
|
166
|
+
workspace,
|
|
167
|
+
navigations,
|
|
168
|
+
analytics,
|
|
169
|
+
submissions
|
|
170
|
+
};
|
|
110
171
|
}
|
|
111
172
|
|
|
112
173
|
// src/index.ts
|
|
@@ -120,37 +181,37 @@ Object.defineProperty(exports, "createKeyedCache", {
|
|
|
120
181
|
enumerable: true,
|
|
121
182
|
get: function () { return chunkXJFKL2HU_cjs.createKeyedCache; }
|
|
122
183
|
});
|
|
123
|
-
Object.defineProperty(exports, "
|
|
184
|
+
Object.defineProperty(exports, "createBrandfineWebhookHandler", {
|
|
124
185
|
enumerable: true,
|
|
125
|
-
get: function () { return
|
|
186
|
+
get: function () { return chunkKHHMR2NX_cjs.createBrandfineWebhookHandler; }
|
|
126
187
|
});
|
|
127
|
-
Object.defineProperty(exports, "
|
|
188
|
+
Object.defineProperty(exports, "parseWebhookPayload", {
|
|
128
189
|
enumerable: true,
|
|
129
|
-
get: function () { return
|
|
190
|
+
get: function () { return chunkKHHMR2NX_cjs.parseWebhookPayload; }
|
|
130
191
|
});
|
|
131
|
-
Object.defineProperty(exports, "
|
|
192
|
+
Object.defineProperty(exports, "verifyWebhookSecret", {
|
|
132
193
|
enumerable: true,
|
|
133
|
-
get: function () { return
|
|
194
|
+
get: function () { return chunkKHHMR2NX_cjs.verifyWebhookSecret; }
|
|
134
195
|
});
|
|
135
|
-
Object.defineProperty(exports, "
|
|
196
|
+
Object.defineProperty(exports, "isLocale", {
|
|
136
197
|
enumerable: true,
|
|
137
|
-
get: function () { return
|
|
198
|
+
get: function () { return chunkFCK7QJBC_cjs.isLocale; }
|
|
138
199
|
});
|
|
139
|
-
Object.defineProperty(exports, "
|
|
200
|
+
Object.defineProperty(exports, "localizePath", {
|
|
140
201
|
enumerable: true,
|
|
141
|
-
get: function () { return
|
|
202
|
+
get: function () { return chunkFCK7QJBC_cjs.localizePath; }
|
|
142
203
|
});
|
|
143
|
-
Object.defineProperty(exports, "
|
|
204
|
+
Object.defineProperty(exports, "pickLocale", {
|
|
144
205
|
enumerable: true,
|
|
145
|
-
get: function () { return
|
|
206
|
+
get: function () { return chunkFCK7QJBC_cjs.pickLocale; }
|
|
146
207
|
});
|
|
147
|
-
Object.defineProperty(exports, "
|
|
208
|
+
Object.defineProperty(exports, "resolveNavigation", {
|
|
148
209
|
enumerable: true,
|
|
149
|
-
get: function () { return
|
|
210
|
+
get: function () { return chunkFCK7QJBC_cjs.resolveNavigation; }
|
|
150
211
|
});
|
|
151
|
-
Object.defineProperty(exports, "
|
|
212
|
+
Object.defineProperty(exports, "stripLocalePrefix", {
|
|
152
213
|
enumerable: true,
|
|
153
|
-
get: function () { return
|
|
214
|
+
get: function () { return chunkFCK7QJBC_cjs.stripLocalePrefix; }
|
|
154
215
|
});
|
|
155
216
|
exports.BrandfineApiError = BrandfineApiError;
|
|
156
217
|
exports.SDK_VERSION = SDK_VERSION;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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}\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 submissions: SubmissionsApi\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 CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\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 const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\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 Submission\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n }\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 CreateSubmissionInput,\n type InstallOptions,\n type Submission,\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,8 @@ type BrandfineClient = {
|
|
|
66
66
|
categories: CategoriesApi;
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
|
+
analytics: AnalyticsApi;
|
|
70
|
+
submissions: SubmissionsApi;
|
|
69
71
|
};
|
|
70
72
|
type PostsApi = {
|
|
71
73
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -85,8 +87,110 @@ type WorkspaceApi = {
|
|
|
85
87
|
type NavigationsApi = {
|
|
86
88
|
/** Navigation by its workspace-scoped `key` (e.g. `'header'`).
|
|
87
89
|
* Returns `null` for 404 so consumers can fall back to a
|
|
88
|
-
* hardcoded default without try/catch.
|
|
89
|
-
|
|
90
|
+
* hardcoded default without try/catch. `TConfig` narrows each
|
|
91
|
+
* item's `customConfig` (default `unknown`). */
|
|
92
|
+
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
93
|
+
};
|
|
94
|
+
type CreateSubmissionInput = {
|
|
95
|
+
/** Required. Display name of the submitter. */
|
|
96
|
+
name: string;
|
|
97
|
+
/** Required. Validated server-side. */
|
|
98
|
+
email: string;
|
|
99
|
+
/** Optional. Free-text up to 40 chars. */
|
|
100
|
+
phone?: string;
|
|
101
|
+
/** Optional. Free-text up to 200 chars. */
|
|
102
|
+
subject?: string;
|
|
103
|
+
/** Required. The message body — up to 10,000 chars. */
|
|
104
|
+
message: string;
|
|
105
|
+
/** Optional. Where the submission came from — e.g. a route path
|
|
106
|
+
* like `/contact`, or a marketing campaign label. Up to 500 chars. */
|
|
107
|
+
source?: string;
|
|
108
|
+
/** Optional. Free-form JSON metadata the consumer attaches; the
|
|
109
|
+
* cms surfaces it verbatim in the submissions admin view. */
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
type Submission = {
|
|
113
|
+
id: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
};
|
|
116
|
+
type SubmissionsApi = {
|
|
117
|
+
/**
|
|
118
|
+
* Posts a contact-form submission to `POST /external/submissions`
|
|
119
|
+
* for this workspace. The cms surfaces the submission in the
|
|
120
|
+
* Submissions inbox.
|
|
121
|
+
*
|
|
122
|
+
* Throws `BrandfineApiError` on validation failures (400) or
|
|
123
|
+
* any other non-2xx — caller decides whether to surface that as
|
|
124
|
+
* a user-visible error or a silent retry.
|
|
125
|
+
*/
|
|
126
|
+
create: (input: CreateSubmissionInput) => Promise<Submission>;
|
|
127
|
+
};
|
|
128
|
+
type AnalyticsConfig = {
|
|
129
|
+
enabled: false;
|
|
130
|
+
} | {
|
|
131
|
+
enabled: true;
|
|
132
|
+
websiteId: string;
|
|
133
|
+
scriptUrl: string;
|
|
134
|
+
};
|
|
135
|
+
type AnalyticsInstallResult = {
|
|
136
|
+
installed: false;
|
|
137
|
+
reason: 'disabled' | 'ssr' | 'already-installed';
|
|
138
|
+
} | {
|
|
139
|
+
installed: true;
|
|
140
|
+
websiteId: string;
|
|
141
|
+
};
|
|
142
|
+
type InstallOptions = {
|
|
143
|
+
/**
|
|
144
|
+
* Pre-known config. When provided, `install()` skips the round-
|
|
145
|
+
* trip to `/external/analytics-config` and injects the script
|
|
146
|
+
* immediately. Use this when you've baked the values into your
|
|
147
|
+
* build (env vars, CMS-side config dump, etc.) — typical for
|
|
148
|
+
* static sites where the analytics state is decided at deploy
|
|
149
|
+
* time, not per page load.
|
|
150
|
+
*
|
|
151
|
+
* Trade-off vs the default fetch path: if you disable analytics
|
|
152
|
+
* in Brandfine, the tracker keeps loading until your next
|
|
153
|
+
* deploy. That's usually the right trade for static sites
|
|
154
|
+
* (which redeploy on every content change anyway) and the wrong
|
|
155
|
+
* trade for dynamic sites where the api round-trip is cheap
|
|
156
|
+
* relative to the rest of the page.
|
|
157
|
+
*
|
|
158
|
+
* Pass `{ enabled: false }` to force a no-op without touching
|
|
159
|
+
* the api (e.g. to disable analytics for one environment without
|
|
160
|
+
* changing Brandfine's state).
|
|
161
|
+
*/
|
|
162
|
+
config?: AnalyticsConfig;
|
|
163
|
+
};
|
|
164
|
+
type AnalyticsApi = {
|
|
165
|
+
/**
|
|
166
|
+
* Injects the Brandfine analytics tracker into `document.head`
|
|
167
|
+
* once. Safe to call on every page load — idempotent via a
|
|
168
|
+
* marker attribute on the injected script tag.
|
|
169
|
+
*
|
|
170
|
+
* Two paths:
|
|
171
|
+
* - `install()` — fetches the config from Brandfine, then
|
|
172
|
+
* injects. Reflects enable/disable state on next page load.
|
|
173
|
+
* - `install({ config })` — uses caller-provided config, skips
|
|
174
|
+
* the fetch. Faster, no round-trip; ignores Brandfine state
|
|
175
|
+
* changes until the consumer's next deploy.
|
|
176
|
+
*
|
|
177
|
+
* Returns details about what happened:
|
|
178
|
+
* - `{ installed: true, websiteId }` — script was just injected.
|
|
179
|
+
* - `{ installed: false, reason: 'disabled' }` — config says
|
|
180
|
+
* analytics is off; no-op.
|
|
181
|
+
* - `{ installed: false, reason: 'ssr' }` — no `document` in
|
|
182
|
+
* scope (server-side). Call again on the client.
|
|
183
|
+
* - `{ installed: false, reason: 'already-installed' }` — a
|
|
184
|
+
* prior call (or another tab in the same SPA) already injected.
|
|
185
|
+
*
|
|
186
|
+
* Throws `BrandfineApiError` on non-2xx responses other than the
|
|
187
|
+
* disabled case (which is a valid `{ enabled: false }` body).
|
|
188
|
+
*/
|
|
189
|
+
install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>;
|
|
190
|
+
/** Lower-level helper — fetches the raw config without touching
|
|
191
|
+
* the DOM. Useful when you want to inject the script yourself
|
|
192
|
+
* (e.g. via a framework's <Script> component for nonce/csp). */
|
|
193
|
+
getConfig: () => Promise<AnalyticsConfig>;
|
|
90
194
|
};
|
|
91
195
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
92
196
|
|
|
@@ -104,4 +208,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
104
208
|
*/
|
|
105
209
|
declare const SDK_VERSION: "0.0.0";
|
|
106
210
|
|
|
107
|
-
export { BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
211
|
+
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, 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,8 @@ type BrandfineClient = {
|
|
|
66
66
|
categories: CategoriesApi;
|
|
67
67
|
workspace: WorkspaceApi;
|
|
68
68
|
navigations: NavigationsApi;
|
|
69
|
+
analytics: AnalyticsApi;
|
|
70
|
+
submissions: SubmissionsApi;
|
|
69
71
|
};
|
|
70
72
|
type PostsApi = {
|
|
71
73
|
/** Paginated list of published posts. Handles the cms's
|
|
@@ -85,8 +87,110 @@ type WorkspaceApi = {
|
|
|
85
87
|
type NavigationsApi = {
|
|
86
88
|
/** Navigation by its workspace-scoped `key` (e.g. `'header'`).
|
|
87
89
|
* Returns `null` for 404 so consumers can fall back to a
|
|
88
|
-
* hardcoded default without try/catch.
|
|
89
|
-
|
|
90
|
+
* hardcoded default without try/catch. `TConfig` narrows each
|
|
91
|
+
* item's `customConfig` (default `unknown`). */
|
|
92
|
+
get: <TConfig = unknown>(key: string) => Promise<BrandfineNavigation<TConfig> | null>;
|
|
93
|
+
};
|
|
94
|
+
type CreateSubmissionInput = {
|
|
95
|
+
/** Required. Display name of the submitter. */
|
|
96
|
+
name: string;
|
|
97
|
+
/** Required. Validated server-side. */
|
|
98
|
+
email: string;
|
|
99
|
+
/** Optional. Free-text up to 40 chars. */
|
|
100
|
+
phone?: string;
|
|
101
|
+
/** Optional. Free-text up to 200 chars. */
|
|
102
|
+
subject?: string;
|
|
103
|
+
/** Required. The message body — up to 10,000 chars. */
|
|
104
|
+
message: string;
|
|
105
|
+
/** Optional. Where the submission came from — e.g. a route path
|
|
106
|
+
* like `/contact`, or a marketing campaign label. Up to 500 chars. */
|
|
107
|
+
source?: string;
|
|
108
|
+
/** Optional. Free-form JSON metadata the consumer attaches; the
|
|
109
|
+
* cms surfaces it verbatim in the submissions admin view. */
|
|
110
|
+
metadata?: Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
type Submission = {
|
|
113
|
+
id: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
};
|
|
116
|
+
type SubmissionsApi = {
|
|
117
|
+
/**
|
|
118
|
+
* Posts a contact-form submission to `POST /external/submissions`
|
|
119
|
+
* for this workspace. The cms surfaces the submission in the
|
|
120
|
+
* Submissions inbox.
|
|
121
|
+
*
|
|
122
|
+
* Throws `BrandfineApiError` on validation failures (400) or
|
|
123
|
+
* any other non-2xx — caller decides whether to surface that as
|
|
124
|
+
* a user-visible error or a silent retry.
|
|
125
|
+
*/
|
|
126
|
+
create: (input: CreateSubmissionInput) => Promise<Submission>;
|
|
127
|
+
};
|
|
128
|
+
type AnalyticsConfig = {
|
|
129
|
+
enabled: false;
|
|
130
|
+
} | {
|
|
131
|
+
enabled: true;
|
|
132
|
+
websiteId: string;
|
|
133
|
+
scriptUrl: string;
|
|
134
|
+
};
|
|
135
|
+
type AnalyticsInstallResult = {
|
|
136
|
+
installed: false;
|
|
137
|
+
reason: 'disabled' | 'ssr' | 'already-installed';
|
|
138
|
+
} | {
|
|
139
|
+
installed: true;
|
|
140
|
+
websiteId: string;
|
|
141
|
+
};
|
|
142
|
+
type InstallOptions = {
|
|
143
|
+
/**
|
|
144
|
+
* Pre-known config. When provided, `install()` skips the round-
|
|
145
|
+
* trip to `/external/analytics-config` and injects the script
|
|
146
|
+
* immediately. Use this when you've baked the values into your
|
|
147
|
+
* build (env vars, CMS-side config dump, etc.) — typical for
|
|
148
|
+
* static sites where the analytics state is decided at deploy
|
|
149
|
+
* time, not per page load.
|
|
150
|
+
*
|
|
151
|
+
* Trade-off vs the default fetch path: if you disable analytics
|
|
152
|
+
* in Brandfine, the tracker keeps loading until your next
|
|
153
|
+
* deploy. That's usually the right trade for static sites
|
|
154
|
+
* (which redeploy on every content change anyway) and the wrong
|
|
155
|
+
* trade for dynamic sites where the api round-trip is cheap
|
|
156
|
+
* relative to the rest of the page.
|
|
157
|
+
*
|
|
158
|
+
* Pass `{ enabled: false }` to force a no-op without touching
|
|
159
|
+
* the api (e.g. to disable analytics for one environment without
|
|
160
|
+
* changing Brandfine's state).
|
|
161
|
+
*/
|
|
162
|
+
config?: AnalyticsConfig;
|
|
163
|
+
};
|
|
164
|
+
type AnalyticsApi = {
|
|
165
|
+
/**
|
|
166
|
+
* Injects the Brandfine analytics tracker into `document.head`
|
|
167
|
+
* once. Safe to call on every page load — idempotent via a
|
|
168
|
+
* marker attribute on the injected script tag.
|
|
169
|
+
*
|
|
170
|
+
* Two paths:
|
|
171
|
+
* - `install()` — fetches the config from Brandfine, then
|
|
172
|
+
* injects. Reflects enable/disable state on next page load.
|
|
173
|
+
* - `install({ config })` — uses caller-provided config, skips
|
|
174
|
+
* the fetch. Faster, no round-trip; ignores Brandfine state
|
|
175
|
+
* changes until the consumer's next deploy.
|
|
176
|
+
*
|
|
177
|
+
* Returns details about what happened:
|
|
178
|
+
* - `{ installed: true, websiteId }` — script was just injected.
|
|
179
|
+
* - `{ installed: false, reason: 'disabled' }` — config says
|
|
180
|
+
* analytics is off; no-op.
|
|
181
|
+
* - `{ installed: false, reason: 'ssr' }` — no `document` in
|
|
182
|
+
* scope (server-side). Call again on the client.
|
|
183
|
+
* - `{ installed: false, reason: 'already-installed' }` — a
|
|
184
|
+
* prior call (or another tab in the same SPA) already injected.
|
|
185
|
+
*
|
|
186
|
+
* Throws `BrandfineApiError` on non-2xx responses other than the
|
|
187
|
+
* disabled case (which is a valid `{ enabled: false }` body).
|
|
188
|
+
*/
|
|
189
|
+
install: (opts?: InstallOptions) => Promise<AnalyticsInstallResult>;
|
|
190
|
+
/** Lower-level helper — fetches the raw config without touching
|
|
191
|
+
* the DOM. Useful when you want to inject the script yourself
|
|
192
|
+
* (e.g. via a framework's <Script> component for nonce/csp). */
|
|
193
|
+
getConfig: () => Promise<AnalyticsConfig>;
|
|
90
194
|
};
|
|
91
195
|
declare function createBrandfineClient(config: BrandfineClientConfig): BrandfineClient;
|
|
92
196
|
|
|
@@ -104,4 +208,4 @@ declare function createBrandfineClient(config: BrandfineClientConfig): Brandfine
|
|
|
104
208
|
*/
|
|
105
209
|
declare const SDK_VERSION: "0.0.0";
|
|
106
210
|
|
|
107
|
-
export { BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, createBrandfineClient };
|
|
211
|
+
export { type AnalyticsConfig, type AnalyticsInstallResult, BrandfineApiError, BrandfineCategory, type BrandfineClient, type BrandfineClientConfig, BrandfineNavigation, BrandfinePost, BrandfineWorkspace, type CreateSubmissionInput, type InstallOptions, ListCategoriesOptions, ListPostsOptions, SDK_VERSION, type Submission, createBrandfineClient };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
|
|
2
|
-
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-3OXDF4ZY.js';
|
|
3
2
|
export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
|
|
3
|
+
export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-U6VJX7PP.js';
|
|
4
4
|
|
|
5
5
|
// src/client.ts
|
|
6
6
|
var BrandfineApiError = class extends Error {
|
|
@@ -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,67 @@ 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
|
+
const submissions = {
|
|
136
|
+
async create(input) {
|
|
137
|
+
const url = `${baseUrl}/external/submissions`;
|
|
138
|
+
const res = await fetchImpl(url, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"X-Api-Key": apiKey,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
Accept: "application/json",
|
|
144
|
+
"User-Agent": userAgent
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(input)
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const body = await res.text().catch(() => "");
|
|
150
|
+
throw new BrandfineApiError({
|
|
151
|
+
status: res.status,
|
|
152
|
+
statusText: res.statusText,
|
|
153
|
+
body,
|
|
154
|
+
url
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return await res.json();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
get,
|
|
162
|
+
posts,
|
|
163
|
+
categories,
|
|
164
|
+
workspace,
|
|
165
|
+
navigations,
|
|
166
|
+
analytics,
|
|
167
|
+
submissions
|
|
168
|
+
};
|
|
108
169
|
}
|
|
109
170
|
|
|
110
171
|
// src/index.ts
|
package/dist/index.js.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.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 submissions: SubmissionsApi\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 CreateSubmissionInput = {\n /** Required. Display name of the submitter. */\n name: string\n /** Required. Validated server-side. */\n email: string\n /** Optional. Free-text up to 40 chars. */\n phone?: string\n /** Optional. Free-text up to 200 chars. */\n subject?: string\n /** Required. The message body — up to 10,000 chars. */\n message: string\n /** Optional. Where the submission came from — e.g. a route path\n * like `/contact`, or a marketing campaign label. Up to 500 chars. */\n source?: string\n /** Optional. Free-form JSON metadata the consumer attaches; the\n * cms surfaces it verbatim in the submissions admin view. */\n metadata?: Record<string, unknown>\n}\n\nexport type Submission = {\n id: string\n createdAt: string\n}\n\ntype SubmissionsApi = {\n /**\n * Posts a contact-form submission to `POST /external/submissions`\n * for this workspace. The cms surfaces the submission in the\n * Submissions inbox.\n *\n * Throws `BrandfineApiError` on validation failures (400) or\n * any other non-2xx — caller decides whether to surface that as\n * a user-visible error or a silent retry.\n */\n create: (input: CreateSubmissionInput) => Promise<Submission>\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 const submissions: SubmissionsApi = {\n async create(input: CreateSubmissionInput) {\n const url = `${baseUrl}/external/submissions`\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n 'X-Api-Key': apiKey,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n body: JSON.stringify(input),\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 Submission\n },\n }\n\n return {\n get,\n posts,\n categories,\n workspace,\n navigations,\n analytics,\n submissions,\n }\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 CreateSubmissionInput,\n type InstallOptions,\n type Submission,\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.5.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-2VCPYNF5.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 /** Optional image URL, passed through from the item. Not\n * per-locale; `null` when unset. */\n imageUrl: string | null\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 imageUrl: item.imageUrl ?? 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"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-3OXDF4ZY.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 /** Optional image URL, passed through from the item. Not\n * per-locale; `null` when unset. */\n imageUrl: string | null\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 imageUrl: item.imageUrl ?? 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"]}
|