@blueforge-studio/service-content

Content service factory: createContentService with memory, committed-snapshot and marketing-os (CMS read) adapters

Información del paquete

Latest Version0.2.0
LicenseMIT
Keywords-
Repositorygit+https://github.com/blueforge-studio/blueforge-org.git
Author-
Homepagehttps://github.com/blueforge-studio/blueforge-org#readme
Bugs[object Object]

Versiones

VersiónPublicada
0.2.02026-10-16
0.1.02026-10-16

Cadena de suministro por versión

VersiónFirmaSBOMAtt
0.2.0
0.1.0

Haz clic en una versión para ver sus detalles.

Cadena de suministro

Procedencia

Publicado por
web:5
Cuándo
2026-10-16T19:42:37Z
sha256 (calculado por el registro)
3b0d27e15015236ee1444f550cb55c1fe1c3055f2b72ad506b18a518ce2ddc04

Observado por el registro en el momento de publicar.

Declarado por la compilación

Quien publicó no declaró nada sobre su compilación.

Adjuntos

Firma
ninguno
SBOM
ninguno

Dependencias

Cargando el grafo de dependencias...

README

@blueforge-studio/service-content

Provider-neutral, read-only content access for apps: createContentService() reads CONTENT_PROVIDER and returns a stable ContentService (getItem, listItems). Same pattern as service-support / service-analytics.

CONTENT_PROVIDERWhat it isConfig
memory (default)In-memory, empty. Dev and tests; new MemoryContentAdapter([...]) to seed.none
snapshotA JSON file committed to the app repo. The safe default for a deploy: a CMS outage cannot block it.CONTENT_SNAPSHOT_PATH (default content-snapshot.json)
marketing-osThe marketing-os CMS read API, with ETag revalidation, timeout and stale-while-error.MARKETING_OS_API_URL, MARKETING_OS_CMS_KEY (a tenant-bound cms:read key)
import { createContentService } from "@blueforge-studio/service-content";

const content = createContentService();
const terms = await content.getItem("legal", "terms-of-service", { appId: "contracts-os" });
if (terms) render(terms.body, terms.fields);          // keep terms.id + terms.version if you must record what was shown

The item

{ id, type, slug, title, body, fields, version, scope?, locked?, publishedAt?, updatedAt?, source, stale? }. body is opaque (the editor document, or Markdown, depending on the type); fields is {} for plain types. Store id + version (not a label) when you must record exactly what a user saw, e.g. with the essentials-kit consent store's content reference.

Behaviour you can rely on

  • Published items only. The package cannot write, approve or publish.
  • Scope resolution (marketing-os): pass appId and you get the app's own item, else its family's, else the portfolio's; a locked ancestor wins over any descendant. Without appId you get the item as stored at the default scope.
  • Resilience (marketing-os): responses are cached with their ETag and revalidated with If-None-Match (304 reuses the cached item). If the provider times out, is unreachable or returns 5xx, the last good copy comes back with stale: true; with nothing cached the error is thrown. 404 on getItem is null. Other 4xx throw MarketingOSContentError with status.
  • No locale. The marketing-os read API has no locale parameter yet, so the option does not exist here rather than being ignored.

Snapshot workflow (recommended for deploys)

A snapshot is a JSON file committed to the app repo: { "version": 1, "generatedAt": "...", "items": [ ... ] }. In a hand-written snapshot each item needs id, type, slug, title, body, fields, version; source is optional (the adapter stamps "snapshot"), and locked, scope, publishedAt, updatedAt are optional too.

Generate one from any source (normally marketing-os) in a build step or by hand, and commit it:

import { createContentService, writeContentSnapshot } from "@blueforge-studio/service-content";
// run with CONTENT_PROVIDER=marketing-os; the file is provider-neutral (source/stale are not written)
await writeContentSnapshot(createContentService(), { types: ["legal", "page"], appId: "contracts-os" });

Bundled apps (Next output: "standalone", serverless, edge): pass the snapshot OBJECT

CONTENT_PROVIDER=snapshot with a path reads the file from disk at runtime, relative to the process cwd. A bundler only includes files it sees imported, so in a standalone/serverless image that file can be missing. Import the JSON so it is bundled, and hand it over:

import snapshot from "./content-snapshot.json";
import { createContentService } from "@blueforge-studio/service-content";

const content = createContentService({ snapshot });   // used when CONTENT_PROVIDER=snapshot; no file access

or new SnapshotContentAdapter(snapshot). The object is validated immediately (a bad snapshot fails at startup, not on the first request); parseContentSnapshot(x) does the same check on its own. With CONTENT_PROVIDER=memory the snapshot option is ignored, so the flag still switches between "snapshot" and "nothing" (your in-repo fallback).

MemoryContentAdapter

Exported, takes ContentItem[] (here source IS required, or use .set() which defaults it to "memory"):

import { MemoryContentAdapter } from "@blueforge-studio/service-content";
const content = new MemoryContentAdapter([{ id: "1", type: "page", slug: "home", title: "Home", body: "...", fields: {}, version: 1, source: "memory" }]);
content.set({ id: "2", type: "page", slug: "about", title: "About", body: "...", fields: {}, version: 1 });

Use it in tests, or to seed content you already have in code. It returns null for unknown items and never throws.

What is NOT verified

The marketing-os adapter is tested against a fake fetch only. When it was written the CMS was merged in marketing-os but not deployed. These come from the marketing-os session's description (2026-10-16), not from a live response, and must be checked against a real server before anyone relies on them: the routes (/v1/dashboard/cms/types/{type}/items/{slug}[/resolve?app=], /items?type=), the Authorization: Bearer header, the item JSON shape (the adapter accepts a bare item or { item }), and that the gateway passes ETag / 304 through. Pin operation ids against marketing-os packages/sdk-core/src/generated/operations.ts (not read here). Whether marketing-os may be the source of an app's content is the operator's decision, and legal text additionally needs counsel.