{"_id":"@base-assist/content-management","_rev":"6-02384cc68e5ed8291607ebe4d108f3a2","name":"@base-assist/content-management","dist-tags":{"latest":"0.1.5"},"versions":{"0.1.0":{"name":"@base-assist/content-management","version":"0.1.0","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"author":{"name":"Base Assist"},"license":"MIT","_id":"@base-assist/content-management@0.1.0","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"dist":{"shasum":"a14f2ba3553db89a6fbbc15ccedb7cd13c008148","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.0.tgz","fileCount":9,"integrity":"sha512-s3o3vWJ49sW0sQ78DnC61YGblJBX7yzQF9lRLA6cWclAgbmEyjqby1VsBFl/00zucVr1khPVJ2i1+5LHX6XvUQ==","signatures":[{"sig":"MEYCIQCHESm2MtHxXO0+uKDw+BYo9pg+wWsbE75vM8jLSFpt8QIhAKp9i0W2qn82OSekjDPMfr6URWhxO4fsfnch2fDN9/pn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":43612},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"d316eed885176fd2340f127f78acc8747132054b","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"repository":{"url":"git+https://github.com/MaisonDeCode/base-assist.git","type":"git","directory":"packages/content-management"},"_npmVersion":"11.12.1","description":"Official client SDK for the Base Assist headless content management API.","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/content-management_0.1.0_1787074442053_0.16175394192662407","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@base-assist/content-management","version":"0.1.1","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"author":{"name":"Base Assist"},"license":"MIT","_id":"@base-assist/content-management@0.1.1","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"dist":{"shasum":"11368dc10fd3019e490715e8839c42f7b13b1b74","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.1.tgz","fileCount":9,"integrity":"sha512-zJ9gThdwDl6Ec3CUoTXdo/b4qg5kMGKQjjqS4Di3E1flnLb5Rt1QUdY1GqhIJWkV74LtzY//IarlWicjs2mZkA==","signatures":[{"sig":"MEUCIAt6RRbLV3BQHG8M/W4Bwf7beFvz/E3/R5XQlH/xUgzFAiEAjgGFXDGj5YxntowHoGqboWmEEK5BF3J+uunNQchd7eI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":43582},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"d316eed885176fd2340f127f78acc8747132054b","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"repository":{"url":"git+https://github.com/MaisonDeCode/base-assist.git","type":"git","directory":"packages/content-management"},"_npmVersion":"11.12.1","description":"Official client SDK for the Base Assist headless content management API.","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/content-management_0.1.1_1787076643734_0.6105888144118374","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@base-assist/content-management","version":"0.1.2","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"author":{"name":"Base Assist"},"license":"MIT","_id":"@base-assist/content-management@0.1.2","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"dist":{"shasum":"fb7c2f4b4c4ee8aeb0fc5363debd13605c16e45f","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.2.tgz","fileCount":15,"integrity":"sha512-RXUako9SIQ5HBtGBjJrN+tLVBqSWw1AwbcMkpgIqgrU6t+FLahKbA+/7DZEt3nF88cqctrRzcmfiv9TpPQCFFg==","signatures":[{"sig":"MEUCIAGd1UiCx78T0jBFsKc/0o9CZaGpI4OhUmoDwLrtIl8pAiEA3JBEBTAksKNrEcYNAsO4SmNFECrf6d41bfc2dTiGLN8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":82886},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./tracker":{"types":"./dist/tracker.d.ts","import":"./dist/tracker.js","require":"./dist/tracker.cjs"}},"gitHead":"3e9afa98404accf2e717893545507b269877b324","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"repository":{"url":"git+https://github.com/MaisonDeCode/base-assist.git","type":"git","directory":"packages/content-management"},"_npmVersion":"11.12.1","description":"Official client SDK for the Base Assist headless content management API.","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/content-management_0.1.2_1787306835043_0.8114746106440089","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@base-assist/content-management","version":"0.1.3","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"author":{"name":"Base Assist"},"license":"MIT","_id":"@base-assist/content-management@0.1.3","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"dist":{"shasum":"0b9c9c6c986d6e956eb9812c088ba87140664626","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.3.tgz","fileCount":17,"integrity":"sha512-sIOoF2LDO3ohiT8hLAiE/fLGwkZhMWCzmMXzXVSeU4/iLlvH4aeuCg1rMuMKON362IQJxamM9KIKO/KOxYuK7g==","signatures":[{"sig":"MEQCIDuCUynxSIK8REXGraSODHn2wV/k0KeMNzqChynjuAj6AiBtRNfWTJpacfetf4iePLZ8nLOBglPe4/UUvkKfIuvpbg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":83995},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./tracker":{"types":"./dist/tracker.d.ts","import":"./dist/tracker.js","require":"./dist/tracker.cjs"}},"gitHead":"1e0be2be6f5a96455dff7e922fdac4660ea02efe","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"repository":{"url":"git+https://github.com/MaisonDeCode/base-assist.git","type":"git","directory":"packages/content-management"},"_npmVersion":"11.12.1","description":"Official client SDK for the Base Assist headless content management API.","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/content-management_0.1.3_1787308066236_0.5974915377788088","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@base-assist/content-management","version":"0.1.4","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"author":{"name":"Base Assist"},"license":"MIT","_id":"@base-assist/content-management@0.1.4","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"dist":{"shasum":"3beb117ba6703bf54ed1539728dbce98455b9596","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.4.tgz","fileCount":17,"integrity":"sha512-PFI+xiaeb0j3WHHxBuQtdfQmH60ROII8g7uzgu6U/TB0jWX88RNtgfuSQjWKWn/swgEU6PSgMazTNu1CKpDxKw==","signatures":[{"sig":"MEUCIQDUPifxq8UVAT/k43bdk3PjD8g4ngoB00r4pZ2vBfKN6AIgOrXEUFMK+97rTgESQRIQzykP9ZWQNmi6njlFQJnXHhA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":88010},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./tracker":{"types":"./dist/tracker.d.ts","import":"./dist/tracker.js","require":"./dist/tracker.cjs"}},"gitHead":"febdc8e4851951388e1dfd61f9ffba95da324939","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"repository":{"url":"git+https://github.com/MaisonDeCode/base-assist.git","type":"git","directory":"packages/content-management"},"_npmVersion":"11.12.1","description":"Official client SDK for the Base Assist headless content management API.","directories":{},"_nodeVersion":"25.9.0","dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"_npmOperationalInternal":{"tmp":"tmp/content-management_0.1.4_1787308531298_0.10769778794122398","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"@base-assist/content-management","version":"0.1.5","description":"Official client SDK for the Base Assist headless content management API.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./tracker":{"types":"./dist/tracker.d.ts","import":"./dist/tracker.js","require":"./dist/tracker.cjs"}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"node --experimental-strip-types --test src/*.test.ts","prepare":"npm run build","prepublishOnly":"npm run build"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/MaisonDeCode/base-assist.git","directory":"packages/content-management"},"homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"keywords":["base-assist","cms","headless-cms","content-management","sdk"],"license":"MIT","author":{"name":"Base Assist"},"engines":{"node":">=18"},"dependencies":{"@tiptap/core":"^2.9.0","@tiptap/html":"^2.9.0","@tiptap/starter-kit":"^2.9.0"},"devDependencies":{"tsup":"^8.3.0","typescript":"^5.5.0"},"gitHead":"b5151aaccf03e0a41975446d8398d35469a58fd6","_id":"@base-assist/content-management@0.1.5","_nodeVersion":"24.20.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-0KWIiXdAl5gFHN366L7Tp3lhx/1hZhNfe1j2dGkRXgYF2eMC+crAGu4m6Z3JpcmWBYfI5Zcjp4f4D8VZyKVOLw==","shasum":"18365108c226ca08dc15141c5480413c8c6f0d2e","tarball":"https://registry.npmjs.org/@base-assist/content-management/-/content-management-0.1.5.tgz","fileCount":17,"unpackedSize":98239,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICc6eMT1gj0gwwoyop70vAC9eVrTGw5YPJ3O7AbPZOzEAiEAmuOOOMvrXS/5PVVrI0ovt1Ws5Iof05EFJ8c/gvzCDyU="}]},"_npmUser":{"name":"lesampsonconnor","email":"csampson@maisondecode.com"},"directories":{},"maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/content-management_0.1.5_1789444995230_0.5140171230703219"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-18T17:34:01.838Z","modified":"2026-09-15T04:03:15.555Z","0.1.0":"2026-08-18T17:34:02.246Z","0.1.1":"2026-08-18T18:10:43.876Z","0.1.2":"2026-08-21T10:07:15.206Z","0.1.3":"2026-08-21T10:27:46.380Z","0.1.4":"2026-08-21T10:35:31.440Z","0.1.5":"2026-09-15T04:03:15.374Z"},"bugs":{"url":"https://github.com/MaisonDeCode/base-assist/issues"},"author":{"name":"Base Assist"},"license":"MIT","homepage":"https://github.com/MaisonDeCode/base-assist/tree/main/packages/content-management#readme","keywords":["base-assist","cms","headless-cms","content-management","sdk"],"repository":{"type":"git","url":"git+https://github.com/MaisonDeCode/base-assist.git","directory":"packages/content-management"},"description":"Official client SDK for the Base Assist headless content management API.","maintainers":[{"name":"lesampsonconnor","email":"csampson@maisondecode.com"}],"readme":"# @base-assist/content-management\n\nOfficial client SDK for the Base Assist headless content management API. It wraps the public content-delivery endpoints (`GET /content-management/public/:siteId/...`) with a small, fully-typed, dependency-light client - no axios, built on native `fetch`, so it runs equally well in Node, Next.js Server Components, and edge runtimes.\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Getting an API key](#getting-an-api-key)\n- [Configuration](#configuration)\n- [API reference](#api-reference)\n- [Typing your content](#typing-your-content)\n- [Relations](#relations)\n- [Modules](#modules)\n  - [Variants](#variants)\n- [Media](#media)\n- [Rich text](#rich-text)\n- [Internationalization (i18n)](#internationalization-i18n)\n- [Draft / preview content](#draft--preview-content)\n- [Site tracking](#site-tracking)\n- [Error handling](#error-handling)\n- [Runtime support](#runtime-support)\n- [Versioning](#versioning)\n\n## Install\n\n```bash\nnpm install @base-assist/content-management\n```\n\n## Quick start\n\n```ts\nimport { createCmsClient } from '@base-assist/content-management';\n\nconst cms = createCmsClient({\n\tapiKey: process.env.CMS_API_KEY!,\n\tsiteId: process.env.CMS_SITE_ID!,\n});\n\nconst home = await cms.getHome();\nconst { items: posts } = await cms.listEntries('blog-post', { limit: 10 });\nconst post = await cms.getEntry('blog-post', 'my-first-post');\n```\n\n**This client is server-only.** The API key it's configured with must never be sent to a browser - call it from a server (an API route, a Server Component, a backend service), not from client-side JavaScript.\n\n## Getting an API key\n\n1. In the Base Assist app, go to **Content Management → (your site) → Settings → API keys**.\n2. Click **New API key**, give it a name (e.g. `\"Website Prod\"`), and pick a scope (`read`, and `preview` if you also want draft content - see [Draft / preview content](#draft--preview-content)).\n3. Copy the key immediately - it's shown once and can't be retrieved again. If you lose it, revoke it and create a new one.\n4. You'll also need the site's `siteId`, visible in the site's URL in the admin (`/content-management/<siteId>/...`) or from `GET /content-management/sites`.\n\nKeys are scoped to a single site and can be individually revoked at any time from the same Settings page - create a separate key per consumer (e.g. one for your production website, a different one for a staging environment) so each can be rotated independently.\n\n## Configuration\n\n```ts\ninterface CmsClientConfig {\n\tapiKey: string; // live key (published content only)\n\tpreviewApiKey?: string; // preview-scoped key, used when preview is on\n\tsiteId: string;\n\tpreview?: boolean; // when true, every read uses preview credentials + draft content\n\tbaseUrl?: string; // defaults to https://api.baseassist.com/content-management/public\n\tdefaultLocale?: string; // used when a call doesn't pass its own `locale`\n\tfetch?: typeof fetch; // override fetch (custom runtime, testing, logging wrapper)\n}\n```\n\n## API reference\n\n### `listEntries(contentType, opts?)`\n\nPaginated list of published entries for a content type.\n\n```ts\nconst { items, total, page, totalPages } = await cms.listEntries('support-article', {\n\tpage: 1,\n\tlimit: 20,\n\tlocale: 'fr',\n});\n```\n\n| Option | Type | Default |\n|---|---|---|\n| `locale` | `string` | `config.defaultLocale` |\n| `page` | `number` | `1` |\n| `limit` | `number` | `20` (server caps at `100`) |\n| `preview` | `boolean` | `false` |\n\n### `getEntry(contentType, slug, opts?)`\n\nA single entry by content type + slug. Returns `null` on a 404 instead of throwing.\n\n```ts\nconst post = await cms.getEntry('blog-post', 'my-first-post');\n```\n\n### `getSingleton(contentType, opts?)`\n\nConvenience for `kind: 'single'` content types (e.g. a homepage). Defaults the lookup slug to the content type's own slug - the convention used when seeding singleton entries (see `cms-schemas/README.md` in the main repo).\n\n```ts\nconst home = await cms.getSingleton('home');\n// equivalent to: cms.getEntry('home', 'home')\n```\n\n### `getHome(opts?)`\n\nShorthand for `getSingleton('home', opts)`.\n\n### `listMedia(opts?)` / `getMedia(mediaId)`\n\n```ts\nconst { items } = await cms.listMedia({ limit: 50 });\nconst asset = await cms.getMedia('64f...'); // -> { url, mimeType, width, height, alt, ... } | null\n```\n\n### `getLocales(opts?)`\n\nReturns the site's locale configuration and the resolved fallback chain for a given locale.\n\n```ts\nconst { defaultLocale, locales, fallbackChain } = await cms.getLocales({ locale: 'fr-CA' });\n// fallbackChain e.g. ['fr-CA', 'fr', 'en']\n```\n\n### `renderRichText(doc)`\n\nConverts a `richtext` field's stored Tiptap/ProseMirror JSON into an HTML string.\n\n```ts\nconst html = cms.renderRichText(post.fields.body);\n```\n\nSanitize before rendering if the field could ever contain editor-supplied markup you don't fully trust (Base Assist's rich text editor only exposes `StarterKit` formatting, but treat `dangerouslySetInnerHTML`-style rendering with the same care you would for any HTML string).\n\n## Typing your content\n\nEvery method accepts a generic for the entry's `fields` shape:\n\n```ts\ninterface BlogPostFields {\n\ttitle: string;\n\texcerpt?: string;\n\tauthor?: string;\n\tpublishDate: string;\n\tbody: unknown; // richtext JSON - pass to renderRichText()\n}\n\nconst post = await cms.getEntry<BlogPostFields>('blog-post', 'my-first-post');\npost?.fields.title; // typed as string\n```\n\nThere's no runtime validation - the generic only affects TypeScript's view of the response, so keep it in sync with the content type's actual field schema in the admin (or the JSON files in `cms-schemas/` if you're working in this monorepo).\n\n## Relations\n\n`relation` fields (top-level or nested inside a `group`) are resolved server-side into a lightweight reference - you never get a bare, unusable id back:\n\n```ts\ninterface CmsRelationRef {\n\tid: string;\n\tslug: string;\n\tcontentTypeSlug: string;\n}\n```\n\nTo get the referenced entry's actual data (e.g. a product's name), fetch it by `contentTypeSlug` + `slug`:\n\n```ts\nconst productRef = productPage.fields.product; // { id, slug, contentTypeSlug: 'product' }\nconst product = await cms.getEntry(productRef.contentTypeSlug, productRef.slug);\n```\n\n## Modules\n\nModules are reusable page-builder blocks (a feature grid, a testimonial, a subscribe form). A page keeps its unique fields (hero, title, body) and then an ordered `module_zone` list where editors stack as many modules as they want, including the same module more than once:\n\nFeature Grid, Testimonial, Call to Action, Feature Grid, Subscribe, FAQ.\n\nPage content types expose this as `fields.main.sections`. The public API resolves each instance to:\n\n```ts\ninterface CmsModuleBlock {\n\ttype: string; // module slug, e.g. \"feature-grid\"\n\tvariant?: string; // variant key, e.g. \"dark\", when the editor picked one\n\tfields: Record<string, CmsFieldValue>;\n}\n```\n\n```ts\ninterface HomeFields {\n\theroHeadline: string;\n\tsections?: CmsModuleBlock[];\n}\n\nconst home = await cms.getHome<HomeFields>();\nfor (const block of home?.fields.sections ?? []) {\n\t// block.type is the CmsModule slug; switch on it to pick a React component\n\t// block.variant is set when the module has named layouts (see Variants below)\n}\n```\n\nAuthoring:\n\n1. Content Management -> (site) -> Modules: define each block's fields (or paste a JSON schema).\n2. Optional: add **variants** on the module (a key, a label, and a preview screenshot) so editors can pick a layout and see what it looks like.\n3. Open a page entry. Unique fields stay at the top. The **Modules** sidebar lists every module (and variant) the content type allows. Drag onto the Modules area, or click to append.\n4. On the website, map `block.type` (and `block.variant` if you use it) to a component. Unknown slugs should render a fallback instead of breaking the page.\n\nThe admin stores `{ moduleSlug, variantKey?, fields }` on the entry. Delivery rewrites that to `{ type, variant?, fields }` so consumers never have to know the internal keys.\n\n### Variants\n\nA module can have more than one look (for example a light Feature Grid and a dark one). Variants live on the module definition, not as separate modules:\n\n```json\n{\n  \"key\": \"dark\",\n  \"label\": \"Dark\",\n  \"description\": \"Inverted cards on a dark band\",\n  \"previewMediaId\": \"<cms media id>\"\n}\n```\n\n`previewMediaId` is admin-only. It is a screenshot so editors can recognize the layout in the sidebar. It is not included in the public API response.\n\nBy default a variant inherits the module's field schema. Set `fields` on the variant to give that layout its own list (add, remove, or rearrange). The public payload is still `{ type, variant?, fields }`. Field keys may differ between variants of the same `type`, so branch on `variant` before reading fields:\n\n```ts\nif (block.type === 'feature-grid' && block.variant === 'dark') {\n  return <FeatureGridDark fields={block.fields} />;\n}\n```\n\nIf `variant` is omitted, treat it as the module's default layout. Adding a variant is backwards compatible: existing blocks keep working with no `variant` field.\n\n## Media\n\n`media` fields store a raw `CmsMedia` id (they are **not** auto-resolved the way `relation` fields are). Fetch the asset separately to get its URL and type. Use `mimeType` to tell images from videos (`image/` vs `video/`). Schema fields can set `mediaKind` to `image`, `video`, or `any` so the admin picker only offers matching files.\n\n```ts\nconst media = await cms.getMedia(post.fields.featuredImage as string);\nmedia?.url; // https://....s3.amazonaws.com/...\nmedia?.mimeType; // image/jpeg or video/mp4\n```\n\n```tsx\nfunction CmsAsset({ media }: { media: { url: string; mimeType: string } }) {\n\tif (media.mimeType.startsWith('video/')) {\n\t\treturn <video src={media.url} controls playsInline />;\n\t}\n\treturn <img src={media.url} alt=\"\" />;\n}\n```\n\n## Rich text\n\n`richtext` fields store Tiptap/ProseMirror JSON, not HTML. Convert with `renderRichText()`:\n\n```tsx\nfunction Body({ doc }: { doc: unknown }) {\n\tconst html = cms.renderRichText(doc);\n\treturn <div dangerouslySetInnerHTML={{ __html: html }} />;\n}\n```\n\n## Internationalization (i18n)\n\nA `CmsSite` has a `defaultLocale`, a list of enabled `locales`, and an optional `localeFallbackChain` (e.g. `fr-CA` falls back to `fr`, then to the site default). Content-type fields are individually marked `localized: true` or not by whoever designs the schema:\n\n- **Localized fields** are stored per-locale (e.g. a blog post's `title` might exist in `en` and `fr`, but not yet in `es`).\n- **Non-localized fields** (dates, numbers, media ids, relations, etc.) have a single value shared across all locales.\n\nEvery read method accepts a `locale` option; when a localized field has no value for the requested locale, the API automatically walks `localeFallbackChain` → `defaultLocale` before giving up.\n\n```ts\n// Site configured with defaultLocale: 'en', locales: ['en', 'fr', 'es'],\n// localeFallbackChain: { 'fr-CA': ['fr'] }\n\n// 1. Explicit locale per call\nconst frenchPost = await cms.getEntry<BlogPostFields>('blog-post', 'my-first-post', { locale: 'fr' });\n\n// 2. A locale with no translated title yet falls back automatically -\n// if 'es' has no title set, you transparently get the 'en' (default) value instead.\nconst partiallyTranslated = await cms.getEntry<BlogPostFields>('blog-post', 'my-first-post', { locale: 'es' });\n\n// 3. Set defaultLocale once on the client instead of passing `locale` everywhere\nconst cms = createCmsClient({\n\tapiKey: process.env.CMS_API_KEY!,\n\tsiteId: process.env.CMS_SITE_ID!,\n\tdefaultLocale: 'fr',\n});\nconst posts = await cms.listEntries<BlogPostFields>('blog-post'); // locale: 'fr' by default\n\n// 4. Discover what's actually configured before building a language switcher\nconst { locales, defaultLocale } = await cms.getLocales();\n```\n\n### Example: a localized Next.js route\n\n```tsx\n// app/[locale]/blog/[slug]/page.tsx\nimport { cms } from '@/lib/cms';\nimport type { BlogPostFields } from '@/lib/content-types';\n\nexport default async function BlogPostPage({ params }: { params: { locale: string; slug: string } }) {\n\tconst post = await cms.getEntry<BlogPostFields>('blog-post', params.slug, { locale: params.locale });\n\tif (!post) return null;\n\n\treturn (\n\t\t<article>\n\t\t\t<h1>{post.fields.title}</h1>\n\t\t\t<div dangerouslySetInnerHTML={{ __html: cms.renderRichText(post.fields.body) }} />\n\t\t</article>\n\t);\n}\n\nexport async function generateStaticParams() {\n\tconst { locales } = await cms.getLocales();\n\tconst { items } = await cms.listEntries<BlogPostFields>('blog-post', { limit: 100 });\n\treturn locales.flatMap((locale) => items.map((entry) => ({ locale, slug: entry.slug })));\n}\n```\n\nBecause untranslated locales fall back automatically, this route never 404s for a slug that simply hasn't been translated yet - it silently serves the fallback language instead. If you need to detect \"this is a fallback, not a real translation\" (e.g. to show a \"not yet available in your language\" banner), compare the returned content against a call with no `locale` override, or track translation completeness as its own field in the content type.\n\n## Draft / preview content\n\nBy default, every method returns the last **published** snapshot. Saving an entry in the admin does not change what a live key returns until someone publishes.\n\nA key created with the `preview` scope can see the working copy (drafts and unpublished edits) when preview is on:\n\n```ts\n// Dedicated preview client (staging, draft mode)\nconst previewCms = createCmsClient({\n\tapiKey: process.env.CMS_API_KEY!,\n\tpreviewApiKey: process.env.CMS_PREVIEW_API_KEY,\n\tsiteId: process.env.CMS_SITE_ID!,\n\tpreview: true,\n});\n\n// Or one client, switched per call / environment\nconst cms = createCmsClient({\n\tapiKey: process.env.CMS_API_KEY!,\n\tpreviewApiKey: process.env.CMS_PREVIEW_API_KEY,\n\tsiteId: process.env.CMS_SITE_ID!,\n\tpreview: process.env.CMS_PREVIEW === 'true',\n});\n\nconst draft = await cms.getEntry('blog-post', 'unreleased-post', { preview: true });\n```\n\n`preview: true` against a live-only key is served published-only results. It does not error. Use a dedicated preview key for staging so you can revoke it without touching production.\n\n## Site tracking\n\nOptional pageview tracking so Base Assist can show visits, session time, locations, top pages, and devices for your site. It is **off by default**. The tracker runs in the browser, posts to a public collect endpoint, and never uses your content API key.\n\nImport it from `@base-assist/content-management/tracker` (not the main client) so your server-only CMS client stays off the page.\n\n```tsx\n'use client';\n\nimport { useEffect } from 'react';\nimport { usePathname } from 'next/navigation';\nimport { createCmsTracker } from '@base-assist/content-management/tracker';\n\nconst tracker = createCmsTracker({\n\tsiteId: process.env.NEXT_PUBLIC_CMS_SITE_ID!, // or pass siteId from a server layout\n\tenabled: true, // omit or false to turn tracking off\n});\n\nexport function CmsAnalytics() {\n\tconst pathname = usePathname();\n\tuseEffect(() => {\n\t\tvoid tracker.pageview({ path: pathname });\n\t}, [pathname]);\n\treturn null;\n}\n```\n\nMount that component in your root layout. Pass `enabled: false` (or stop rendering it) to disable tracking without removing the integration.\n\nThe tracker:\n\n- Sends path, title, referrer, locale, and a random visitor/session id\n- Measures engaged time (visible tab) and sends it on navigate, hide, and heartbeat so you can see average session duration\n- Resolves city and country from IP on the server. The IP itself is not stored\n- Respects `Do Not Track`\n- Skips known bots on the server\n- Does not send the API key or other personal data\n\nVisits show up under **Content Management → (site) → Stats**.\n\n## Error handling\n\nNon-2xx responses (other than 404, which resolves to `null`) throw `CmsApiError`:\n\n```ts\nimport { CmsApiError } from '@base-assist/content-management';\n\ntry {\n\tawait cms.listEntries('blog-post');\n} catch (error) {\n\tif (error instanceof CmsApiError) {\n\t\tconsole.error(error.status, error.message); // e.g. 401 \"Invalid API key\"\n\t}\n\tthrow error;\n}\n```\n\n## Runtime support\n\n- Node.js 18+ (native `fetch`)\n- Next.js Server Components / Route Handlers, and edge runtime (`export const runtime = 'edge'`) - no Node-only APIs are used\n- Not intended for the browser - see [Quick start](#quick-start)\n\n## Versioning\n\nThis package follows semver. Breaking changes to the response shape (e.g. how relations or media are resolved) bump the major version.\n","readmeFilename":"README.md"}