@tandem-language-exchange/content-store
v1.1.2
Published
SDK for fetching CMS content bundles from S3 and querying them locally from the filesystem. Supports content synced from **Contentful** and **Sanity**.
Readme
Content Store
SDK for fetching CMS content bundles from S3 and querying them locally from the filesystem. Supports content synced from Contentful and Sanity.
For the server, CLI, and deployment documentation, see the Server & CLI README.
Package entry points
@tandem-language-exchange/content-store(default) — types only at runtime. Safe to import from shared code that Next.js, Vite, or Turbopack may bundle for the browser.@tandem-language-exchange/content-store/node—ContentStoreSDK,fetchBundles,queryBundle,ContentStore, andtrimDepth. Real implementations use the filesystem and S3 and run only under the Node (node) export condition (Route Handlers,getServerSideProps, CLI, etc.).For browser bundles (including anything imported from
_app.tsx, client components, or shared modules that reach the client graph), bundlers should resolve thebrowser/edge-lightconditions to a stub that does not importfs. That stub throws if you call server-only APIs;trimDepthis fully implemented and safe on the client.Prefer not importing
/nodefrom files that_appor layouts load: keep SDK usage in server-only modules and pass data in as props. If the stub throws at runtime, move the import to server-only code.
Installation
npm install content-storeInitialisation
import { ContentStoreSDK } from '@tandem-language-exchange/content-store/node';
const sdk = new ContentStoreSDK({
s3: {
bucket: 'beta-content-store',
region: 'eu-central-1',
accessKeyId: process.env.AWS_ACCESS_KEY!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
outputDir: './content-cache',
});| Option | Description |
| --- | --- |
| s3.bucket | S3 bucket where content bundles are stored |
| s3.region | AWS region of the bucket |
| s3.accessKeyId | AWS IAM access key |
| s3.secretAccessKey | AWS IAM secret key |
| outputDir | Local directory where bundle JSON files are written |
S3 config via environment variables
The CLI commands (fetch-content-bundles, query) read S3 credentials automatically from the following environment variables — no code needed:
| Variable | Description |
| --- | --- |
| CONTENT_STORE_S3_BUCKET | S3 bucket name |
| CONTENT_STORE_S3_REGION | AWS region (default: eu-central-1) |
| AWS_ACCESS_KEY | AWS IAM access key |
| AWS_SECRET_ACCESS_KEY | AWS IAM secret key |
When using the SDK class directly, pass the values explicitly as shown above. You can load them from env vars yourself:
const sdk = new ContentStoreSDK({
s3: {
bucket: process.env.CONTENT_STORE_S3_BUCKET!,
region: process.env.CONTENT_STORE_S3_REGION!,
accessKeyId: process.env.AWS_ACCESS_KEY!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
outputDir: './content-cache',
});fetchBundles(options)
Downloads the latest content bundles from S3 and saves them as JSON files to outputDir.
const files = await sdk.fetchBundles({
cms: 'contentful',
contentTypes: ['gridLayout', 'iconWithText', 'page'],
});Parameters:
| Field | Type | Description |
| --- | --- | --- |
| cms | 'contentful' \| 'sanity' | Which CMS the bundles were synced from |
| contentTypes | string[] | Content types to download |
Returns: Record<string, string> — a map of content type to absolute file path.
{
gridLayout: '/abs/path/content-contentful-gridLayout.json',
iconWithText: '/abs/path/content-contentful-iconWithText.json',
page: '/abs/path/content-contentful-page.json'
}Files are written to outputDir with the naming pattern {cms}-{contentType}.json.
queryBundle(cms, contentType, options?)
Reads a previously fetched bundle from the local filesystem and returns a filtered, shaped result set.
const results = await sdk.queryBundle('contentful', 'gridLayout', {
fields: { columns: '2' },
select: ['title', 'bodyBefore'],
limit: 10,
include: 2,
});Parameters:
| Field | Type | Description |
| --- | --- | --- |
| cms | 'contentful' \| 'sanity' | CMS provider |
| contentType | string | Content type to query |
| options | QueryOptions | Optional filtering/shaping (see below) |
QueryOptions
All fields are optional.
| Field | Type | Description |
| --- | --- | --- |
| fields | Record<string, unknown> | Filter by top-level properties (see Filtering) |
| select | string[] | Properties to include in each result object |
| limit | number | Maximum number of items to return |
| include | number | Depth of nested references to include (see Include depth) |
Filtering
The fields option matches items by their top-level properties.
Exact match — value must be strictly equal:
{ fields: { columns: '2' } }IN match — value must be one of the provided options:
{ fields: { variant: ['A', 'B', 'E'] } }Multiple fields are combined with AND logic:
{ fields: { columns: '2', refsType: 'Icon with Text' } }Include depth
The include option controls how many levels of nested referenced objects are returned. Omit it to get the full depth.
Given this bundle item:
{
"title": "Page Title",
"columns": "2",
"refs": [
{
"heading": "Child heading",
"icon": {
"title": "Icon title",
"file": { "url": "//images.ctfassets.net/..." }
}
}
]
}include: 1 — scalar properties only, all nested objects/refs are null:
{
"title": "Page Title",
"columns": "2",
"refs": null
}include: 2 — the item including its direct refs, but refs' own nested objects are null:
{
"title": "Page Title",
"columns": "2",
"refs": [
{
"heading": "Child heading",
"icon": null
}
]
}include: 3 — three levels deep; icon is included but icon.file is null:
{
"title": "Page Title",
"columns": "2",
"refs": [
{
"heading": "Child heading",
"icon": {
"title": "Icon title",
"file": null
}
}
]
}Processing order
Query options are applied in this order:
fields— filter the full item setlimit— cap the result countinclude— trim nested depthselect— pick output properties
Standalone functions
The core fetchBundles and queryBundle functions are also available as standalone imports for use outside the SDK class:
import { fetchBundles, queryBundle, ContentStore } from '@tandem-language-exchange/content-store/node';
const store = new ContentStore({
bucket: 'beta-content-store',
region: 'eu-central-1',
accessKeyId: process.env.AWS_ACCESS_KEY!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
});
await fetchBundles(store, './content-cache', {
cms: 'contentful',
contentTypes: ['gridLayout'],
});
const results = await queryBundle('./content-cache', 'contentful', 'gridLayout', {
fields: { columns: '2' },
limit: 5,
});Full example
import { ContentStoreSDK } from '@tandem-language-exchange/content-store/node';
const sdk = new ContentStoreSDK({
s3: {
bucket: process.env.CONTENT_STORE_S3_BUCKET!,
region: process.env.CONTENT_STORE_S3_REGION!,
accessKeyId: process.env.AWS_ACCESS_KEY!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
outputDir: './.content-cache',
});
// 1. Pull latest bundles from S3 to disk
await sdk.fetchBundles({
cms: 'contentful',
contentTypes: ['page', 'gridLayout'],
});
// 2. Query locally — no further network calls
const grids = await sdk.queryBundle('contentful', 'gridLayout', {
fields: { columns: '2' },
select: ['title', 'refs'],
include: 2,
limit: 5,
});
console.log(grids);CLI
The package ships two CLI entry points that can be called from npm scripts in a consuming application.
fetch-content-bundles — download bundles from S3
Downloads the latest version of each requested content type bundle from S3 and writes them as JSON files to a local directory. Reads S3 credentials from environment variables (see S3 config via environment variables).
npx fetch-content-bundles --cms contentful --types gridLayout,page| Flag | Required | Default | Description |
| --- | --- | --- | --- |
| --cms <provider> | Yes | | contentful or sanity |
| --types <types> | Yes | | Comma-separated content types |
| --output <dir> | No | ./content-cache | Local directory to write bundle files to |
Files are written as {cms}-{contentType}.json inside the output directory.
Typical use in a host app's package.json:
"scripts": {
"fetch-content": "fetch-content-bundles --cms contentful --types gridLayout,iconWithText,page --output ./content-cache"
}content-store query — query a local bundle
Reads a previously fetched bundle from disk and prints filtered results as JSON. Invoke via npx or as a local script using node:
npx @tandem-language-exchange/content-store query \
--cms contentful --type gridLayout \
--fields '{"columns":"2"}' \
--select title,bodyBefore \
--limit 5 \
--include 2| Flag | Required | Default | Description |
| --- | --- | --- | --- |
| --cms <provider> | Yes | | contentful or sanity |
| --type <type> | Yes | | Content type to query |
| --output <dir> | No | ./content-cache | Directory where bundles are stored |
| --fields <json> | No | | JSON filter object (e.g. '{"columns":"2"}') |
| --select <props> | No | | Comma-separated properties to include in results |
| --limit <n> | No | | Maximum number of results |
| --include <n> | No | | Depth of nested references to include |
