@brandfine/client 0.1.0 → 0.3.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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @brandfine/client
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c41e51d: Add `imageUrl` to `BrandfineNavItem`. Navigation items can now carry an
8
+ optional image URL (icon, thumbnail, mega-menu art) on any item type. The
9
+ field is served by `GET /external/navigations/:key` and resolved by the
10
+ navigation resolver. `null` when unset. Additive, no breaking changes.
11
+
12
+ ## 0.2.0
13
+
14
+ ### Minor Changes
15
+
16
+ - d486975: Type `BrandfinePost.translations[]` (returned by `bf.posts.getBySlug`) and add
17
+ `publishedAt` to each translation entry. Consumers can now compute a true
18
+ cross-locale sitemap `<lastmod>` (`max(publishedAt)` across siblings) without
19
+ extra fetches. Also clarifies in the `BrandfinePost` JSDoc that `publishedAt`
20
+ is the last-published timestamp — re-stamped on every republish with a new
21
+ contentHash, untouched on no-op republishes — so it's the right field to feed
22
+ into a sitemap `<lastmod>`.
23
+
24
+ New named export: `BrandfinePostTranslation`.
25
+
26
+ Additive, no breaking changes.
27
+
3
28
  ## 0.1.0
4
29
 
5
30
  ### Minor Changes
@@ -52,6 +52,7 @@ function hydrate(item, children, locale, opts) {
52
52
  href: resolveHref(item, locale, opts),
53
53
  label: resolveLabel(item, locale, opts.defaultLocale),
54
54
  type: item.type,
55
+ imageUrl: item.imageUrl ?? null,
55
56
  children: children.map((c) => hydrate(c, [], locale, opts))
56
57
  };
57
58
  }
@@ -103,5 +104,5 @@ exports.localizePath = localizePath;
103
104
  exports.pickLocale = pickLocale;
104
105
  exports.resolveNavigation = resolveNavigation;
105
106
  exports.stripLocalePrefix = stripLocalePrefix;
106
- //# sourceMappingURL=chunk-OKIEA3AD.cjs.map
107
- //# sourceMappingURL=chunk-OKIEA3AD.cjs.map
107
+ //# sourceMappingURL=chunk-2VCPYNF5.cjs.map
108
+ //# sourceMappingURL=chunk-2VCPYNF5.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-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"]}
@@ -50,6 +50,7 @@ function hydrate(item, children, locale, opts) {
50
50
  href: resolveHref(item, locale, opts),
51
51
  label: resolveLabel(item, locale, opts.defaultLocale),
52
52
  type: item.type,
53
+ imageUrl: item.imageUrl ?? null,
53
54
  children: children.map((c) => hydrate(c, [], locale, opts))
54
55
  };
55
56
  }
@@ -97,5 +98,5 @@ function resolveLabel(item, locale, defaultLocale) {
97
98
  }
98
99
 
99
100
  export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix };
100
- //# sourceMappingURL=chunk-MTBSSTTG.js.map
101
- //# sourceMappingURL=chunk-MTBSSTTG.js.map
101
+ //# sourceMappingURL=chunk-3OXDF4ZY.js.map
102
+ //# sourceMappingURL=chunk-3OXDF4ZY.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-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"]}
@@ -35,12 +35,42 @@ type BrandfinePost<TConfig = unknown> = {
35
35
  category: BrandfineCategory | null;
36
36
  authorName: string | null;
37
37
  customConfig: TConfig | null;
38
+ /**
39
+ * ISO 8601 timestamp — when the *served content* was last
40
+ * published. Re-stamped on every republish that actually changes
41
+ * content (no-op republishes are skipped server-side), so this is
42
+ * the correct value to use for a sitemap `<lastmod>`.
43
+ */
38
44
  publishedAt: string;
39
45
  contentHtml: string | null;
40
46
  contentMarkdown: string | null;
41
47
  /** Optional per-post JSON-LD payload. Shape is consumer-defined;
42
48
  * Brandfine round-trips it untouched. */
43
49
  jsonLd: unknown;
50
+ /**
51
+ * Sibling translations of this post (every published locale
52
+ * variant in the same translation group, including this post
53
+ * itself). Only present on the singular detail response
54
+ * (`bf.posts.getBySlug`), not on list rows. Sorted by locale.
55
+ *
56
+ * Use cases:
57
+ * - hreflang `<link>` tags for SEO
58
+ * - per-locale URL switcher in the UI
59
+ * - cross-locale sitemap `<lastmod>` (take `max(publishedAt)`
60
+ * across siblings when a single URL represents the article
61
+ * family)
62
+ */
63
+ translations?: BrandfinePostTranslation[];
64
+ };
65
+ /**
66
+ * One sibling translation row, as returned in
67
+ * `BrandfinePost.translations[]`. `publishedAt` is the per-locale
68
+ * last-published timestamp (same semantics as the top-level field).
69
+ */
70
+ type BrandfinePostTranslation = {
71
+ locale: string;
72
+ slug: string;
73
+ publishedAt: string;
44
74
  };
45
75
  type BrandfinePostListResponse<TConfig = unknown> = {
46
76
  items: BrandfinePost<TConfig>[];
@@ -85,6 +115,9 @@ type BrandfineNavItem = {
85
115
  type: BrandfineNavItemType;
86
116
  /** CUSTOM_URL: free-form path or absolute URL. */
87
117
  customUrl: string | null;
118
+ /** Optional image URL for the item (icon, thumbnail, mega-menu
119
+ * art). Set on any item type; not per-locale. Null when unset. */
120
+ imageUrl: string | null;
88
121
  /** `{ [locale]: string }` map. May be null on POST items —
89
122
  * consumers fall back to the post's per-locale `title` then. */
90
123
  labels: Record<string, string> | null;
@@ -235,6 +268,9 @@ type HydratedNavItem = {
235
268
  href: string | null;
236
269
  label: string;
237
270
  type: 'CUSTOM_URL' | 'POST' | 'HEADING';
271
+ /** Optional image URL, passed through from the item. Not
272
+ * per-locale; `null` when unset. */
273
+ imageUrl: string | null;
238
274
  children: HydratedNavItem[];
239
275
  };
240
276
  type HydratedNav = {
@@ -259,4 +295,4 @@ type ResolveNavigationOptions = {
259
295
  };
260
296
  declare function resolveNavigation(nav: BrandfineNavigation, locale: string, opts: ResolveNavigationOptions): HydratedNav;
261
297
 
262
- 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 HydratedNavItem as i, type LocaleOptions as j, isLocale as k, localizePath as l, pickLocale as p, resolveNavigation as r, stripLocalePrefix as s };
298
+ 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 };
@@ -35,12 +35,42 @@ type BrandfinePost<TConfig = unknown> = {
35
35
  category: BrandfineCategory | null;
36
36
  authorName: string | null;
37
37
  customConfig: TConfig | null;
38
+ /**
39
+ * ISO 8601 timestamp — when the *served content* was last
40
+ * published. Re-stamped on every republish that actually changes
41
+ * content (no-op republishes are skipped server-side), so this is
42
+ * the correct value to use for a sitemap `<lastmod>`.
43
+ */
38
44
  publishedAt: string;
39
45
  contentHtml: string | null;
40
46
  contentMarkdown: string | null;
41
47
  /** Optional per-post JSON-LD payload. Shape is consumer-defined;
42
48
  * Brandfine round-trips it untouched. */
43
49
  jsonLd: unknown;
50
+ /**
51
+ * Sibling translations of this post (every published locale
52
+ * variant in the same translation group, including this post
53
+ * itself). Only present on the singular detail response
54
+ * (`bf.posts.getBySlug`), not on list rows. Sorted by locale.
55
+ *
56
+ * Use cases:
57
+ * - hreflang `<link>` tags for SEO
58
+ * - per-locale URL switcher in the UI
59
+ * - cross-locale sitemap `<lastmod>` (take `max(publishedAt)`
60
+ * across siblings when a single URL represents the article
61
+ * family)
62
+ */
63
+ translations?: BrandfinePostTranslation[];
64
+ };
65
+ /**
66
+ * One sibling translation row, as returned in
67
+ * `BrandfinePost.translations[]`. `publishedAt` is the per-locale
68
+ * last-published timestamp (same semantics as the top-level field).
69
+ */
70
+ type BrandfinePostTranslation = {
71
+ locale: string;
72
+ slug: string;
73
+ publishedAt: string;
44
74
  };
45
75
  type BrandfinePostListResponse<TConfig = unknown> = {
46
76
  items: BrandfinePost<TConfig>[];
@@ -85,6 +115,9 @@ type BrandfineNavItem = {
85
115
  type: BrandfineNavItemType;
86
116
  /** CUSTOM_URL: free-form path or absolute URL. */
87
117
  customUrl: string | null;
118
+ /** Optional image URL for the item (icon, thumbnail, mega-menu
119
+ * art). Set on any item type; not per-locale. Null when unset. */
120
+ imageUrl: string | null;
88
121
  /** `{ [locale]: string }` map. May be null on POST items —
89
122
  * consumers fall back to the post's per-locale `title` then. */
90
123
  labels: Record<string, string> | null;
@@ -235,6 +268,9 @@ type HydratedNavItem = {
235
268
  href: string | null;
236
269
  label: string;
237
270
  type: 'CUSTOM_URL' | 'POST' | 'HEADING';
271
+ /** Optional image URL, passed through from the item. Not
272
+ * per-locale; `null` when unset. */
273
+ imageUrl: string | null;
238
274
  children: HydratedNavItem[];
239
275
  };
240
276
  type HydratedNav = {
@@ -259,4 +295,4 @@ type ResolveNavigationOptions = {
259
295
  };
260
296
  declare function resolveNavigation(nav: BrandfineNavigation, locale: string, opts: ResolveNavigationOptions): HydratedNav;
261
297
 
262
- 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 HydratedNavItem as i, type LocaleOptions as j, isLocale as k, localizePath as l, pickLocale as p, resolveNavigation as r, stripLocalePrefix as s };
298
+ export { type BrandfinePost as B, type HydratedNav as H, type ListPostsOptions as L, type ResolveNavigationOptions as R, type ListCategoriesOptions as a, type BrandfineCategory as b, type BrandfineWorkspace as c, type BrandfineNavigation as d, type BrandfineNavItem as e, type BrandfineNavItemType as f, type BrandfineNavPost as g, type BrandfinePostListResponse as h, type BrandfinePostTranslation as i, type HydratedNavItem as j, type LocaleOptions as k, isLocale as l, localizePath as m, pickLocale as p, resolveNavigation as r, stripLocalePrefix as s };
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkXJFKL2HU_cjs = require('./chunk-XJFKL2HU.cjs');
4
- var chunkOKIEA3AD_cjs = require('./chunk-OKIEA3AD.cjs');
4
+ var chunk2VCPYNF5_cjs = require('./chunk-2VCPYNF5.cjs');
5
5
  var chunkKHHMR2NX_cjs = require('./chunk-KHHMR2NX.cjs');
6
6
 
7
7
  // src/client.ts
@@ -122,23 +122,23 @@ Object.defineProperty(exports, "createKeyedCache", {
122
122
  });
123
123
  Object.defineProperty(exports, "isLocale", {
124
124
  enumerable: true,
125
- get: function () { return chunkOKIEA3AD_cjs.isLocale; }
125
+ get: function () { return chunk2VCPYNF5_cjs.isLocale; }
126
126
  });
127
127
  Object.defineProperty(exports, "localizePath", {
128
128
  enumerable: true,
129
- get: function () { return chunkOKIEA3AD_cjs.localizePath; }
129
+ get: function () { return chunk2VCPYNF5_cjs.localizePath; }
130
130
  });
131
131
  Object.defineProperty(exports, "pickLocale", {
132
132
  enumerable: true,
133
- get: function () { return chunkOKIEA3AD_cjs.pickLocale; }
133
+ get: function () { return chunk2VCPYNF5_cjs.pickLocale; }
134
134
  });
135
135
  Object.defineProperty(exports, "resolveNavigation", {
136
136
  enumerable: true,
137
- get: function () { return chunkOKIEA3AD_cjs.resolveNavigation; }
137
+ get: function () { return chunk2VCPYNF5_cjs.resolveNavigation; }
138
138
  });
139
139
  Object.defineProperty(exports, "stripLocalePrefix", {
140
140
  enumerable: true,
141
- get: function () { return chunkOKIEA3AD_cjs.stripLocalePrefix; }
141
+ get: function () { return chunk2VCPYNF5_cjs.stripLocalePrefix; }
142
142
  });
143
143
  Object.defineProperty(exports, "createBrandfineWebhookHandler", {
144
144
  enumerable: true,
@@ -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 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}\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"]}
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-_XBy9Y81.cjs';
2
- export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, H as HydratedNav, i as HydratedNavItem, j as LocaleOptions, R as ResolveNavigationOptions, k as isLocale, l as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-_XBy9Y81.cjs';
1
+ import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-D4SS91_D.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-D4SS91_D.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
 
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-_XBy9Y81.js';
2
- export { e as BrandfineNavItem, f as BrandfineNavItemType, g as BrandfineNavPost, h as BrandfinePostListResponse, H as HydratedNav, i as HydratedNavItem, j as LocaleOptions, R as ResolveNavigationOptions, k as isLocale, l as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from './index-_XBy9Y81.js';
1
+ import { L as ListPostsOptions, B as BrandfinePost, a as ListCategoriesOptions, b as BrandfineCategory, c as BrandfineWorkspace, d as BrandfineNavigation } from './index-D4SS91_D.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-D4SS91_D.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
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createCache, createKeyedCache } from './chunk-DHQHUIFO.js';
2
- export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-MTBSSTTG.js';
2
+ export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from './chunk-3OXDF4ZY.js';
3
3
  export { createBrandfineWebhookHandler, parseWebhookPayload, verifyWebhookSecret } from './chunk-QQLAYITF.js';
4
4
 
5
5
  // src/client.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `createBrandfineClient` — the SDK's entry point.\n *\n * Returns a stateless, multi-instance-safe handle scoped to a\n * single `(baseUrl, apiKey)` pair. Pattern follows the Stripe /\n * Algolia / OpenAI SDKs — explicit construction with config,\n * namespaced methods (`bf.posts.list(...)`, `bf.workspace.get()`),\n * no module-level singletons.\n *\n * Why factory not module-level state: multi-tenant consumers\n * sometimes need two clients in the same process (e.g. main site\n * + admin preview). Module-level env reading makes that impossible\n * without monkey-patching.\n */\n\nimport type {\n BrandfineCategory,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n\nexport type BrandfineClientConfig = {\n /** Base URL of the Brandfine API. No trailing slash — the client\n * trims one if you pass it anyway. e.g. `https://api.brandfine.co` */\n baseUrl: string\n /** Workspace-scoped API key. Generated from the cms's Workspace\n * settings; identifies which workspace the client talks to. */\n apiKey: string\n /** Optional fetch override. Useful for tests (inject a stub),\n * for runtimes that need a custom implementation (edge workers\n * with non-standard fetch), or to add cross-cutting concerns\n * like tracing / retries. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch\n /** Optional User-Agent header. Falls back to a generic SDK tag. */\n userAgent?: string\n}\n\n/**\n * Structured error thrown by every request helper on non-2xx\n * responses. Carries the raw body so consumers can log it for\n * debugging without re-fetching.\n */\nexport class BrandfineApiError extends Error {\n override readonly name = 'BrandfineApiError'\n readonly status: number\n readonly statusText: string\n readonly body: string\n readonly url: string\n\n constructor(args: {\n status: number\n statusText: string\n body: string\n url: string\n }) {\n super(\n `[brandfine] ${args.status} ${args.statusText} on ${args.url} — ${args.body.slice(0, 200)}`,\n )\n this.status = args.status\n this.statusText = args.statusText\n this.body = args.body\n this.url = args.url\n }\n}\n\ntype RequestOptions = {\n /** When true and the response is 404, return `null` instead of\n * throwing. Used by endpoints where 404 is a meaningful empty\n * state (navigation by key, single post by slug). */\n nullable404?: boolean\n signal?: AbortSignal\n}\n\nexport type BrandfineClient = {\n /** Low-level GET. Reserved for endpoints we don't have a typed\n * helper for yet. Adds the X-Api-Key header automatically. */\n get: <T>(path: string, opts?: RequestOptions) => Promise<T>\n posts: PostsApi\n categories: CategoriesApi\n workspace: WorkspaceApi\n navigations: NavigationsApi\n}\n\ntype PostsApi = {\n /** Paginated list of published posts. Handles the cms's\n * pagination transparently — caller gets a flat array. */\n list: <TConfig = unknown>(\n opts?: ListPostsOptions,\n ) => Promise<BrandfinePost<TConfig>[]>\n /** Single post by per-locale URL slug, scoped to the active\n * locale on the workspace's content. Returns `null` for 404 so\n * callers can render their own \"not found\" page without try/catch. */\n getBySlug: <TConfig = unknown>(\n slug: string,\n ) => Promise<BrandfinePost<TConfig> | null>\n}\n\ntype CategoriesApi = {\n list: (opts?: ListCategoriesOptions) => Promise<BrandfineCategory[]>\n}\n\ntype WorkspaceApi = {\n get: <\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() => Promise<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>\n}\n\ntype NavigationsApi = {\n /** Navigation by its workspace-scoped `key` (e.g. `'header'`).\n * Returns `null` for 404 so consumers can fall back to a\n * hardcoded default without try/catch. */\n get: (key: string) => Promise<BrandfineNavigation | null>\n}\n\nconst DEFAULT_USER_AGENT = '@brandfine/client'\n\nexport function createBrandfineClient(\n config: BrandfineClientConfig,\n): BrandfineClient {\n if (!config.baseUrl)\n throw new Error('createBrandfineClient: `baseUrl` is required')\n if (!config.apiKey)\n throw new Error('createBrandfineClient: `apiKey` is required')\n\n const baseUrl = config.baseUrl.replace(/\\/$/, '')\n const apiKey = config.apiKey\n // Resolve fetch lazily so consumers in environments without a\n // global fetch can polyfill before constructing the client.\n const fetchImpl: typeof fetch = config.fetch ?? globalThis.fetch\n const userAgent = config.userAgent ?? DEFAULT_USER_AGENT\n\n async function get<T>(path: string, opts: RequestOptions = {}): Promise<T> {\n const url = `${baseUrl}${path}`\n const res = await fetchImpl(url, {\n method: 'GET',\n headers: {\n 'X-Api-Key': apiKey,\n Accept: 'application/json',\n 'User-Agent': userAgent,\n },\n signal: opts.signal,\n })\n if (res.status === 404 && opts.nullable404) {\n // Drain the body so the underlying socket can be reused —\n // fetch implementations that don't auto-drain (older Node)\n // can leak otherwise.\n await res.text().catch(() => '')\n return null as T\n }\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new BrandfineApiError({\n status: res.status,\n statusText: res.statusText,\n body,\n url,\n })\n }\n return (await res.json()) as T\n }\n\n const posts: PostsApi = {\n async list<TConfig = unknown>(opts: ListPostsOptions = {}) {\n const out: BrandfinePost<TConfig>[] = []\n let page = 1\n const typeQuery = opts.type ? `&type=${encodeURIComponent(opts.type)}` : ''\n const localeQuery = opts.locale\n ? `&locale=${encodeURIComponent(opts.locale)}`\n : ''\n // Default pagination at the cms's 50-per-page cap. `forceLimit`\n // opts past it for content types that would otherwise need\n // many round-trips.\n const sizeQuery = opts.forceLimit\n ? `&force_limit=${opts.forceLimit}`\n : '&limit=50'\n // Pathological safety brake — 200 pages × 50 = 10k posts. If\n // a workspace ever needs more, callers should hit the API\n // directly with their own pagination logic.\n const MAX_PAGES = 200\n while (page <= MAX_PAGES) {\n const data = await get<BrandfinePostListResponse<TConfig>>(\n `/external/posts?include=content${sizeQuery}&page=${page}${typeQuery}${localeQuery}`,\n )\n out.push(...data.items)\n if (!data.pageInfo.hasNext) break\n page += 1\n }\n return out\n },\n async getBySlug<TConfig = unknown>(slug: string) {\n return get<BrandfinePost<TConfig> | null>(\n `/external/posts/${encodeURIComponent(slug)}`,\n { nullable404: true },\n )\n },\n }\n\n const categories: CategoriesApi = {\n async list(opts: ListCategoriesOptions = {}) {\n const qs = opts.locale ? `?locale=${encodeURIComponent(opts.locale)}` : ''\n const data = await get<{ items: BrandfineCategory[] }>(\n `/external/categories${qs}`,\n )\n return data.items\n },\n }\n\n const workspace: WorkspaceApi = {\n get<\n TCustomConfig = Record<string, unknown>,\n TSchemaOrg = Record<string, unknown>,\n >() {\n return get<BrandfineWorkspace<TCustomConfig, TSchemaOrg>>(\n '/external/workspace',\n )\n },\n }\n\n const navigations: NavigationsApi = {\n get(key: string) {\n return get<BrandfineNavigation | null>(\n `/external/navigations/${encodeURIComponent(key)}`,\n { nullable404: true },\n )\n },\n }\n\n return { get, posts, categories, workspace, navigations }\n}\n","/**\n * @brandfine/client — root entry.\n *\n * The full SDK surface is exposed here for \"import everything from\n * one place\" usage. Tree-shaking + `sideEffects: false` mean\n * consumers don't pay a bundle cost for what they don't import.\n *\n * Heavier or framework-coupled pieces still live under subpath\n * exports (`@brandfine/client/cache`, `/resolvers`, `/webhook`) so\n * consumers with poor tree-shaking — or who only need one slice —\n * can scope their imports.\n */\n\nexport const SDK_VERSION = '0.0.0' as const\n\nexport {\n BrandfineApiError,\n createBrandfineClient,\n type BrandfineClient,\n type BrandfineClientConfig,\n} from './client'\n\nexport {\n createCache,\n createKeyedCache,\n type Cache,\n type CacheOptions,\n type KeyedCache,\n type KeyedCacheOptions,\n} from './cache/index'\n\nexport {\n isLocale,\n localizePath,\n pickLocale,\n resolveNavigation,\n stripLocalePrefix,\n type HydratedNav,\n type HydratedNavItem,\n type LocaleOptions,\n type ResolveNavigationOptions,\n} from './resolvers/index'\n\nexport {\n createBrandfineWebhookHandler,\n parseWebhookPayload,\n verifyWebhookSecret,\n type BrandfineWebhookEvent,\n type BrandfineWebhookHandlerOptions,\n type BrandfineWebhookPayload,\n} from './webhook/index'\n\nexport type {\n BrandfineCategory,\n BrandfineNavItem,\n BrandfineNavItemType,\n BrandfineNavPost,\n BrandfineNavigation,\n BrandfinePost,\n BrandfinePostListResponse,\n BrandfineWorkspace,\n ListCategoriesOptions,\n ListPostsOptions,\n} from './types'\n"]}
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,28 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var chunkOKIEA3AD_cjs = require('../chunk-OKIEA3AD.cjs');
3
+ var chunk2VCPYNF5_cjs = require('../chunk-2VCPYNF5.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "isLocale", {
8
8
  enumerable: true,
9
- get: function () { return chunkOKIEA3AD_cjs.isLocale; }
9
+ get: function () { return chunk2VCPYNF5_cjs.isLocale; }
10
10
  });
11
11
  Object.defineProperty(exports, "localizePath", {
12
12
  enumerable: true,
13
- get: function () { return chunkOKIEA3AD_cjs.localizePath; }
13
+ get: function () { return chunk2VCPYNF5_cjs.localizePath; }
14
14
  });
15
15
  Object.defineProperty(exports, "pickLocale", {
16
16
  enumerable: true,
17
- get: function () { return chunkOKIEA3AD_cjs.pickLocale; }
17
+ get: function () { return chunk2VCPYNF5_cjs.pickLocale; }
18
18
  });
19
19
  Object.defineProperty(exports, "resolveNavigation", {
20
20
  enumerable: true,
21
- get: function () { return chunkOKIEA3AD_cjs.resolveNavigation; }
21
+ get: function () { return chunk2VCPYNF5_cjs.resolveNavigation; }
22
22
  });
23
23
  Object.defineProperty(exports, "stripLocalePrefix", {
24
24
  enumerable: true,
25
- get: function () { return chunkOKIEA3AD_cjs.stripLocalePrefix; }
25
+ get: function () { return chunk2VCPYNF5_cjs.stripLocalePrefix; }
26
26
  });
27
27
  //# sourceMappingURL=index.cjs.map
28
28
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- export { H as HydratedNav, i as HydratedNavItem, j as LocaleOptions, R as ResolveNavigationOptions, k as isLocale, l as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-_XBy9Y81.cjs';
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-D4SS91_D.cjs';
@@ -1 +1 @@
1
- export { H as HydratedNav, i as HydratedNavItem, j as LocaleOptions, R as ResolveNavigationOptions, k as isLocale, l as localizePath, p as pickLocale, r as resolveNavigation, s as stripLocalePrefix } from '../index-_XBy9Y81.js';
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-D4SS91_D.js';
@@ -1,3 +1,3 @@
1
- export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from '../chunk-MTBSSTTG.js';
1
+ export { isLocale, localizePath, pickLocale, resolveNavigation, stripLocalePrefix } from '../chunk-3OXDF4ZY.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.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Brandfine consumer SDK — typed HTTP client, server-side caches, locale + navigation resolvers, and webhook helpers for landing-page integrations.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-MTBSSTTG.js","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n children: HydratedNavItem[]\n}\n\nexport type HydratedNav = {\n key: string\n name: string\n items: HydratedNavItem[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation(\n nav: BrandfineNavigation,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav {\n const childrenByParent = new Map<string, BrandfineNavItem[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate(\n item: BrandfineNavItem,\n children: BrandfineNavItem[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/resolvers/locale.ts","../src/resolvers/navigation.ts"],"names":[],"mappings":"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","file":"chunk-OKIEA3AD.cjs","sourcesContent":["/**\n * Locale helpers.\n *\n * Pure functions over BCP47 locale codes. Olavisa's in-tree\n * version reads `LOCALES` and `DEFAULT_LOCALE` from module-level\n * constants — the package takes them as options instead so a\n * single SDK instance can serve callers with different locale\n * sets (multi-tenant, preview environments, tests).\n *\n * The conventional URL shape these helpers assume:\n * - The `defaultLocale` is served at the bare URL: `/about`\n * - Other locales get a path prefix: `/pt/about`, `/es/about`\n * - `pickLocale` falls back to `defaultLocale` for unknown\n * inputs. Don't throw — `Astro.currentLocale` is `string |\n * undefined`, and callers shouldn't have to defend against\n * every framework's quirk.\n */\n\nexport type LocaleOptions = {\n /** Locales the consumer serves, in any order. The list is used\n * for membership checks (`isLocale`), prefix detection\n * (`stripLocalePrefix`), and as the codomain of `pickLocale`. */\n locales: readonly string[]\n /** The locale served at bare URLs. Must appear in `locales`. */\n defaultLocale: string\n}\n\n/**\n * Type guard. Narrows `unknown` inputs to a known locale so the\n * caller can use them without further coercion.\n *\n * if (isLocale(value, ['en', 'pt'])) {\n * // value: string (known to be 'en' | 'pt' at runtime)\n * }\n */\nexport function isLocale(\n value: unknown,\n locales: readonly string[],\n): value is string {\n return typeof value === 'string' && locales.includes(value)\n}\n\n/**\n * Coerce arbitrary input into a known locale, falling back to\n * `defaultLocale` for unknowns. Convenient at the consumer's\n * framework boundary (Astro.currentLocale, request headers, etc.).\n */\nexport function pickLocale(input: unknown, opts: LocaleOptions): string {\n return isLocale(input, opts.locales) ? input : opts.defaultLocale\n}\n\n/**\n * Convert a canonical (default-locale) path into the locale-prefixed\n * variant for `locale`. The default locale's URLs are bare; every\n * other locale prefixes with `/<locale>`.\n *\n * localizePath('/services/uk-eta', 'en', { defaultLocale: 'en' })\n * → '/services/uk-eta'\n * localizePath('/services/uk-eta', 'pt', { defaultLocale: 'en' })\n * → '/pt/services/uk-eta'\n * localizePath('/', 'pt', { defaultLocale: 'en' })\n * → '/pt'\n */\nexport function localizePath(\n path: string,\n locale: string,\n opts: { defaultLocale: string },\n): string {\n if (locale === opts.defaultLocale) return path\n if (path === '/') return `/${locale}`\n return `/${locale}${path.startsWith('/') ? path : `/${path}`}`\n}\n\n/**\n * Strip a non-default locale prefix off a pathname. The inverse\n * of `localizePath` — useful for normalising back to a canonical\n * path before re-localising for a different locale (the language\n * switcher's main job).\n *\n * stripLocalePrefix('/pt/about', ...) → '/about'\n * stripLocalePrefix('/about', ...) → '/about' (already canonical)\n * stripLocalePrefix('/pt', ...) → '/'\n * stripLocalePrefix('/en/foo', { defaultLocale: 'en', … })\n * → '/en/foo' (en is default — no prefix to strip)\n */\nexport function stripLocalePrefix(\n pathname: string,\n opts: LocaleOptions,\n): string {\n for (const locale of opts.locales) {\n if (locale === opts.defaultLocale) continue\n const prefix = `/${locale}`\n if (pathname === prefix || pathname === `${prefix}/`) return '/'\n if (pathname.startsWith(`${prefix}/`)) return pathname.slice(prefix.length)\n }\n return pathname\n}\n","/**\n * Navigation resolver.\n *\n * Turns a `BrandfineNavigation` (locale-agnostic shape from the\n * external API) into a `HydratedNav` ready for a specific locale —\n * URLs computed, labels picked, hidden items dropped, children\n * grouped under their parent. Header / Footer components consume\n * the hydrated form directly.\n *\n * The semantics encoded here come from olavisa but are\n * generalised:\n * - POST items resolve URLs via `post.locales`. If the active\n * locale has no translation, fall back to the default-locale\n * URL. Consumers can override the URL pattern entirely via\n * `urlForPost`.\n * - CUSTOM_URL items: paths (start with `/`) run through\n * `localizePath`; anything with a scheme (https://, mailto:)\n * passes through unchanged.\n * - Labels: per-locale override → default-locale label → any\n * populated label (last-resort, covers single-locale fills) →\n * for POST items, the post's per-locale title.\n * - `hiddenLocales` drops the item entirely for that locale.\n */\n\nimport type {\n BrandfineNavItem,\n BrandfineNavPost,\n BrandfineNavigation,\n} from '../types'\nimport { localizePath } from './locale'\n\nexport type HydratedNavItem = {\n /** Final URL, ready to render in an `<a href>`. `null` for\n * HEADING items (label-only) and for POST items whose post\n * has been deleted (orphan). */\n href: string | null\n label: string\n type: 'CUSTOM_URL' | 'POST' | 'HEADING'\n children: HydratedNavItem[]\n}\n\nexport type HydratedNav = {\n key: string\n name: string\n items: HydratedNavItem[]\n}\n\nexport type ResolveNavigationOptions = {\n defaultLocale: string\n /** Override the URL pattern for POST items. Default:\n * `localizePath('/${postTypeSlug}/${sibling.slug}', linkLocale)`.\n *\n * Use when a consumer routes posts under a non-default prefix\n * (e.g. `/blog/<slug>` instead of `/posts/<slug>`), or wants\n * to omit the locale prefix for specific post types. */\n urlForPost?: (args: {\n post: BrandfineNavPost\n sibling: BrandfineNavPost['locales'][number]\n locale: string\n defaultLocale: string\n }) => string | null\n}\n\nexport function resolveNavigation(\n nav: BrandfineNavigation,\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNav {\n const childrenByParent = new Map<string, BrandfineNavItem[]>()\n for (const item of nav.items) {\n if (!item.parentId) continue\n const arr = childrenByParent.get(item.parentId) ?? []\n arr.push(item)\n childrenByParent.set(item.parentId, arr)\n }\n // Array-position dictates sibling order within a parent —\n // matches the cms editor's render contract.\n const topLevel = nav.items.filter((i) => i.parentId === null)\n\n return {\n key: nav.key,\n name: nav.name,\n items: topLevel\n .filter((item) => !item.hiddenLocales.includes(locale))\n .map((item) =>\n hydrate(\n item,\n (childrenByParent.get(item.id) ?? []).filter(\n (c) => !c.hiddenLocales.includes(locale),\n ),\n locale,\n opts,\n ),\n ),\n }\n}\n\nfunction hydrate(\n item: BrandfineNavItem,\n children: BrandfineNavItem[],\n locale: string,\n opts: ResolveNavigationOptions,\n): HydratedNavItem {\n return {\n href: resolveHref(item, locale, opts),\n label: resolveLabel(item, locale, opts.defaultLocale),\n type: item.type,\n children: children.map((c) => hydrate(c, [], locale, opts)),\n }\n}\n\nfunction resolveHref(\n item: BrandfineNavItem,\n locale: string,\n opts: ResolveNavigationOptions,\n): string | null {\n if (item.type === 'HEADING') return null\n\n if (item.type === 'CUSTOM_URL') {\n const url = item.customUrl ?? ''\n if (!url) return null\n // External (scheme:) and protocol-relative URLs pass through;\n // `localizePath` would mangle them. Anything else is treated\n // as a path on the consumer site.\n if (/^([a-z][a-z0-9+.-]*:)|^\\/\\//i.test(url)) return url\n return localizePath(\n url.startsWith('/') ? url : `/${url}`,\n locale,\n { defaultLocale: opts.defaultLocale },\n )\n }\n\n // POST\n const post = item.post\n if (!post) return null\n\n // Pick the sibling for the active locale, falling back to the\n // default-locale sibling, then any sibling. If none exist, the\n // post has no published translations.\n const sibling =\n post.locales.find((s) => s.locale === locale) ??\n post.locales.find((s) => s.locale === opts.defaultLocale) ??\n post.locales[0]\n if (!sibling) return null\n\n // The URL prefix the user navigates to — `locale` when this\n // post has a translation in `locale`, else `defaultLocale` so\n // we land on the default-locale page (Astro's i18n rewrite or\n // similar serves the right content under the right URL).\n const linkLocale = post.locales.some((s) => s.locale === locale)\n ? locale\n : opts.defaultLocale\n\n if (opts.urlForPost) {\n return opts.urlForPost({\n post,\n sibling,\n locale: linkLocale,\n defaultLocale: opts.defaultLocale,\n })\n }\n\n // Default convention: `/<postTypeSlug>/<sibling.slug>`.\n // Orphan posts (postType deleted) have no path — return null so\n // the consumer can render an unlinked label or skip the item.\n if (!post.postTypeSlug) return null\n return localizePath(\n `/${post.postTypeSlug}/${sibling.slug}`,\n linkLocale,\n { defaultLocale: opts.defaultLocale },\n )\n}\n\nfunction resolveLabel(\n item: BrandfineNavItem,\n locale: string,\n defaultLocale: string,\n): string {\n const labels = item.labels ?? {}\n // Active locale override → default-locale label → any other\n // populated label (last-resort, useful when editors only fill\n // one locale). Empty strings are treated as \"not set\".\n const override =\n labels[locale]?.trim() ||\n labels[defaultLocale]?.trim() ||\n Object.values(labels)\n .map((v) => v?.trim())\n .find((v) => Boolean(v)) ||\n ''\n if (override) return override\n\n if (item.type === 'POST' && item.post) {\n const sibling =\n item.post.locales.find((s) => s.locale === locale) ??\n item.post.locales.find((s) => s.locale === defaultLocale) ??\n item.post.locales[0]\n if (sibling) return sibling.title\n }\n return ''\n}\n"]}