@blueforge-studio/service-content
Content service factory: createContentService with memory, committed-snapshot and marketing-os (CMS read) adapters
Package Info
Versions
| Version | Published |
|---|---|
| 0.2.0 | 2026-10-16 |
| 0.1.0 | 2026-10-16 |
Per-version supply chain
| Version | Sig | SBOM | Att |
|---|---|---|---|
| 0.2.0 | |||
| 0.1.0 |
Click a version to view its details.
Supply chain
Provenance
- Published by
- web:5
- When
- 2026-10-16T19:42:37Z
- sha256 (registry-computed)
- 3b0d27e15015236ee1444f550cb55c1fe1c3055f2b72ad506b18a518ce2ddc04
Observed by the registry at publish.
Claimed by the build
The publisher claimed nothing about its build.
Attachments
- Signature
- none
- SBOM
- none
Dependencies
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_PROVIDER | What it is | Config |
|---|---|---|
memory (default) | In-memory, empty. Dev and tests; new MemoryContentAdapter([...]) to seed. | none |
snapshot | A 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-os | The 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
appIdand you get the app's own item, else its family's, else the portfolio's; a locked ancestor wins over any descendant. WithoutappIdyou 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 withstale: true; with nothing cached the error is thrown.404ongetItemisnull. Other 4xx throwMarketingOSContentErrorwithstatus. - 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.