Two consumer-side clients ship in the box, both intentionally
read-first. Writes go through rpc() β the same dispatch as the
HTTP POST /rpc β because most consumers are renderers, not
editors. Editing happens through the admin GUI, the inline editor,
or an MCP-connected agent.
@ledric/sdk(TypeScript)- Generate types from the live schema
Ledric\LedricClient(PHP)- Inline editor helpers
@ledric/sdk (TypeScript)
Install:
npm install @ledric/sdk
# pnpm add @ledric/sdk
# yarn add @ledric/sdkConstruct:
import { LedricClient } from '@ledric/sdk';
const client = new LedricClient({
baseUrl: 'https://cms.example.com',
apiKey: process.env.LEDRIC_READER_KEY // optional under default reads-open mode
});Options:
| Option | Type | Notes |
|---|---|---|
baseUrl |
string | Origin of your ledric server. Required. |
apiKey |
string | API key. Optional under reads-open mode. |
headers |
object | Extra headers merged into every request. |
fetch |
function | Custom fetch implementation (e.g. node-fetch for old Node, or a tracing-instrumented fetch). |
Reading content
// Single entry
const post = await client.read({ type: 'blog_post', slug: 'why-kysely' });
// or as a string:
const post = await client.read('blog_post/why-kysely');
// With options
const post = await client.read('blog_post/why-kysely', {
expandAssets: true, // inline asset metadata + URLs
expandAssets: ['hero'], // or just specific fields
resolveRefs: true, // walk markdown for :::ref{}
version: 3, // historical version
locale: 'fr' // localized projection
});
// Returns null on 404. Follows slug-rename redirects transparently.
if (!post) { /* ... */ }
console.log(post.fields.title);Listing
const { results, total, offset } = await client.find('blog_post', {
tags: ['featured'], // AND semantics
limit: 10,
offset: 0,
locale: 'fr',
expandAssets: true
});Generate types from the live schema
ledric types reads your schema and writes a ledric.types.ts file
next to your ledric.config.json. Re-run after any create_type /
alter_type to keep consumer code in sync β every content type
becomes an interface, every references field a typed EntryRef<T>[],
every asset a branded AssetId, every date a DateString.
ledric types # writes ./ledric.types.ts (reads local DB)
ledric types --from http://127.0.0.1:3000 # β¦ via HTTP against a remote ledric
ledric types --augment-sdk # β¦ plus a declare-module block so
# client.read<'blog_post'>('hello')
# picks up your shapes automatically
ledric types --stdout # β¦ print to stdout instead of writingUse the generated types either by passing the interface as a generic:
import type { BlogPost, Entries } from './ledric.types';
const post = await client.read<BlogPost>('blog_post/why-kysely');
post?.fields.title; // string β typedβ¦or, with --augment-sdk enabled, by letting the type-name string
drive inference:
const post = await client.read<'blog_post'>('blog_post/why-kysely');
// post.fields.title β typed automaticallyTypes
const model = await client.types(); // every type's full def
const blogPost = await client.type('blog_post'); // one type, or nullAssets
const meta = await client.asset(refKeyOrId); // metadata
const url = client.assetUrl(refKeyOrId, { // build a URL with imgix params
w: 800,
fm: 'webp',
auto: 'format'
});
const bytes = await client.assetBytes(refKeyOrId); // ArrayBuffer
const list = await client.assets({ kind: 'image', tags: ['hero'] });Tags
await client.tags(); // all tags + counts
await client.addEntryTags({ type: 'blog_post', slug: 'why-kysely' }, ['featured']);
await client.removeEntryTags({ type: 'blog_post', slug: 'why-kysely' }, ['draft']);
await client.addAssetTags(assetId, ['hero']);
await client.updateTag('featured', 'Featured Posts');Writes (via rpc)
The TS SDK exposes a generic rpc() for everything else. Same
shape as the HTTP POST /rpc and the MCP tool catalogue.
const draft = await client.rpc('draft', {
type: 'blog_post',
fields: { title: 'Hello', slug: 'hello', body: '# Hi' }
});
await client.rpc('publish', {
ref: { type: 'blog_post', slug: 'hello' }
});
await client.rpc('alter_type', {
name: 'blog_post',
parent_version: 3,
merge_patch: { fields: { reading_time: { type: 'number', integer: true } } }
});See MCP tools for every tool name and arg shape.
Errors
LedricApiError (extends Error) is thrown on non-2xx responses.
It carries:
try {
await client.rpc('draft', { /* ... */ });
} catch (err) {
if (err instanceof LedricApiError) {
err.status; // HTTP status
err.code; // ledric error code (VALIDATION_FAILED, VERSION_CONFLICT, ...)
err.errors; // [{ path, message }] on validation failures
}
}Ledric\LedricClient (PHP)
Install:
composer require ledric/sdkConstruct:
use Ledric\LedricClient;
$client = new LedricClient('https://cms.example.com', [
'apiKey' => getenv('LEDRIC_READER_KEY')
]);Method surface mirrors the TS SDK:
$post = $client->read('blog_post/why-kysely');
$post = $client->read('blog_post/why-kysely', [
'expandAssets' => true,
'resolveRefs' => true,
'locale' => 'fr'
]);
$list = $client->find('blog_post', [
'tags' => ['featured'],
'limit' => 10
]);
$model = $client->types();
$type = $client->type('blog_post');
$asset = $client->asset($refKeyOrId);
$url = $client->assetUrl($refKeyOrId, ['w' => 800, 'fm' => 'webp']);
$bytes = $client->assetBytes($refKeyOrId);
$client->addEntryTags(['type' => 'blog_post', 'slug' => 'why-kysely'], ['featured']);
$client->updateTag('featured', 'Featured Posts');
// Writes via generic rpc()
$client->rpc('draft', [
'type' => 'blog_post',
'fields' => ['title' => 'Hello', 'slug' => 'hello', 'body' => '# Hi']
]);PHP-specific notes:
- Uses
ext-curlandext-json. No Guzzle / no extra HTTP layer. - Returns associative arrays (not objects) for entries / types /
assets β so
$post['fields']['title']rather than$post->fields->title. nullon 404 (same shape as the TS SDK).- Throws
Ledric\LedricApiErroron non-2xx β same fields (status,code,errors).
Inline editor helpers
Both SDKs export a refAttrs() helper for building the
data-ledric-ref / data-ledric-field attributes the inline editor
uses. See Inline editor for the full
walkthrough.
// TypeScript / JSX
import { refAttrs } from '@ledric/sdk';
<article {...refAttrs(post)}>
<h1 {...refAttrs(post, 'title')}>{post.fields.title}</h1>
</article>// PHP
<article <?= $client->refAttrs($post) ?>>
<h1 <?= $client->refAttrs($post, 'title') ?>><?= htmlspecialchars($post['fields']['title']) ?></h1>
</article>Returns empty (object / string) when the entry is null β safe to spread/inject without conditional logic.