UNPKG

@cosmicjs/sdk

Version:

The official client module for Cosmic. This module helps you easily add dynamic content to your website or application using the Cosmic headless CMS.

201 lines (153 loc) 8.54 kB
# RFC: Rich-text blocks - rendering model, blocks endpoint, and `@cosmicjs/rich-text` - Status: Approved - rolling out (GA) - Author: Cosmic engineering - Affected repos: `cosmic-api`, `cosmic-backend`, `cosmic-dashboard`, `cosmic-sdk-js` - Related: `rich-text` metafield, Custom Blocks (bucket settings `content_blocks`) ## Summary The `rich-text` metafield stores markdown prose interleaved with `{{shortcode}}` block tokens. Each token references a reusable **block** defined per bucket in **Settings -> Custom Blocks** (`settings.content_blocks`). This RFC proposes a **control-first rendering model**: 1. The API serves `rich-text` values **verbatim**. It never expands tokens or attaches generated HTML to responses. 2. A dedicated, cacheable endpoint exposes block definitions: `GET buckets/{slug}/blocks`. 3. Consumers resolve tokens themselves, either with the official renderer (`@cosmicjs/rich-text`) or their own code. **Decision (GA rollout):** all three are shipping. `@cosmicjs/rich-text` is being published to npm as the official renderer (option A below), `publishConfig.access` is set to `public`, and the docs reference it as the recommended path. The remaining publish step (`npm publish` / changeset release) runs through the normal release pipeline with npm credentials. ## Background An earlier iteration prototyped **server-side expansion** (never released): requesting an object with `?expand=blocks` added an `expanded_html` field next to each `rich-text` metafield, containing sanitized `data-block` HTML rendered on the server. Problems with that approach: - **Limited control.** Consumers want React-level (or framework-level) control over how each block renders. A server-rendered HTML blob forces `dangerouslySetInnerHTML` and discards component boundaries. - **Duplicated, drift-prone logic.** The markdown renderer + block registry had to be reimplemented in `cosmic-api` and `cosmic-backend` as dependency-free JS, mirroring the dashboard/SDK implementation. Three copies, three chances to drift. - **Caching ambiguity.** Expanded HTML is a function of both the object value and the bucket's block definitions, so object caches must also invalidate on block edits. Coupling them on the object response makes invalidation harder. - **Payload bloat.** Every rich-text field carried a second, larger HTML copy. ## Proposal ### 1. Serve values verbatim (remove `expanded_html`) Remove the `?expand=blocks` / `expanded_html` plumbing from object get/list on both v2 (`cosmic-api/packages/api`) and v3 (`cosmic-api/packages/api-v3`), and delete the server-side richText expansion service in `cosmic-api` and `cosmic-backend`. Stored values are returned unchanged. ### 2. Dedicated blocks endpoint ``` GET buckets/{slug}/blocks -> 200 { "blocks": [ ...BlockDefinition ] } ``` - **Auth:** read key (same as object reads). - **Source:** `bucket.settings.content_blocks` (returns `[]` if unset). - **Caching:** Fastly-cached on the **bucket surrogate key**, so editing blocks invalidates the blocks response without touching object caches. Clients fetch it once and reuse it across many objects. - **Versions:** available on both v2 and v3 for parity. `BlockDefinition`: ```ts interface BlockDefinition { name: string; // shortcode slug, e.g. "callout" title?: string; description?: string; content: string; // markdown, plain text, or raw HTML editor: 'rich-text' | 'plain' | 'html'; } ``` > The same definitions remain available on the bucket object at > `settings.content_blocks`. The dedicated endpoint exists so renderers can > fetch just the blocks, with their own cache lifetime, instead of pulling the > whole bucket. ### 3. Client-side rendering Consumers resolve `{{shortcode}}` tokens against the fetched definitions: - **Official renderer** `@cosmicjs/rich-text`: `<RichText value blocks={blocks} />` or `renderRichText(value, { blocks })`, with an overridable component per block (`components={{ callout: Callout }}`). - **Bring your own**: the shortcode grammar + `data-block` HTML contract are documented; the parser is small and stable. `data-block` contract (what the default renderer emits per block): ```html <div class="cosmic-block cosmic-block-<name>" data-block="<name>"> <!-- block content rendered to HTML (markdown or escaped plain text) --> </div> ``` ### 4. Inline object embeds (additive primitive) Blocks are reusable, bucket-defined snippets. A distinct, additive primitive lets authors reference a **live Cosmic Object** inline. This is not a block: the content lives in the referenced object, not in bucket settings. Canonical token: a reserved, self-closing `object` shortcode. ``` {{object type="posts" id="64f0...abc" slug="hello-world" /}} ``` - `id` is authoritative; `type` drives renderer dispatch and editor preview; `slug` aids readability/resilience. - `object` (and `objects`) are reserved block names so a block can't shadow the token. - Requires no parser changes (attrs already round-trip) and no server-side expansion: values are still served verbatim and the `GET /blocks` endpoint is unchanged. Rendering stays control-first. `renderRichText` gains two options: - `objects?: Record<string, React.ComponentType<ObjectBlockProps>>` keyed by object type slug. - `resolveObject?: (ref: { id; type?; slug? }) => ResolvedObject | undefined` so the consumer (who already fetched the parent object, optionally with relationship depth) supplies resolution. `ObjectBlockProps` is `{ id, type?, slug?, object? }`. When no component is registered for a type, a `DefaultObjectBlock` placeholder renders (it never silently drops the reference). Dashboard read-only/preview views render the same kind of reference placeholder. ## `@cosmicjs/rich-text` (publish decision) The package lives at `cosmic-sdk-js/packages/rich-text`, builds, and exposes `RichText`, `renderRichText`, `BlockProps`, and `BlockDefinition`. Options considered: - **A. Publish as a standalone package (chosen).** Gives customers a first-class renderer immediately and matches the docs. We own support/versioning of a new public package. - **B. Hold; ship docs + endpoint only.** Customers render with their own code against the documented grammar and the blocks endpoint. - **C. Fold into `@cosmicjs/sdk`** as a subpath export (e.g. `@cosmicjs/sdk/blocks`). **Decision: A.** For GA we publish the standalone package so adoption isn't gated on customers hand-writing a parser. The endpoint + grammar remain the stable integration surface, so a future move to (C) would be a re-export, not a breaking change. ### Publish mechanics - `publishConfig.access` is `public` (scoped package). Initial version `0.1.0`. - Semver from `0.x`; treat the component `components` map and `BlockProps` as the primary stability surface. - `react` stays a peer dependency; ships ESM + CJS + types. - Release runs through the normal pipeline (build + `npm publish` / changeset) with npm credentials; not something done from a sandbox. - Follow-up: pin the shortcode grammar as the cross-package source of truth (dashboard + SDK must stay byte-for-byte compatible; add a shared fixture test). ## Alternatives considered - **Keep server-side expansion (status quo before this RFC).** Rejected for the control, drift, caching, and payload reasons above. - **Expansion as an opt-in, separate field on a separate endpoint.** Still duplicates rendering logic server-side; client rendering already covers the "I want HTML" case via the renderer. ## Migration / compatibility - **Not a breaking change.** `?expand=blocks` / `expanded_html` never shipped to production, so there are no external consumers to migrate and no deprecation window is needed. It is simply removed. - No change to how values are stored. Existing `rich-text` content is unaffected. - Dashboard editor is unaffected (it reads blocks from bucket settings directly). ## Resolved - **Publish `@cosmicjs/rich-text`?** Yes - publish standalone for GA (option A). A later fold into `@cosmicjs/sdk` (C) stays open as a non-breaking re-export. ## Open questions 1. Should the blocks endpoint support filtering (e.g. `?name=callout`) or is the full list sufficient? (Lean: full list; it's small and cacheable.) 2. Do we want a non-React renderer (vanilla/string) in the same package for server components / other frameworks? 3. Should we add a shared grammar conformance test across dashboard + SDK to prevent drift?