New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@mapled/client

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mapled/client

Mapled's JavaScript client — read published content from any JavaScript runtime: a browser, Node, an edge function. Preview, publish notifications and forms included; no framework, no dependencies.

npmnpm
Version
0.2.1
Version published
Weekly downloads
177
-28.05%
Maintainers
1
Weekly downloads
 
Created
Source

@mapled/client

The JavaScript client for Mapled, the hosted headless CMS for sites built with AI. It reads published content from any JavaScript runtime — a browser, Node, an edge function — and has no framework in it and no dependencies.

Install

npm install @mapled/client

Read published content

// src/content.ts
import { createClient } from "@mapled/client";

const mapled = createClient({ key: "mk_live_…" });

const home = await mapled.getSingle("homepage");
const { records, total } = await mapled.getRecords("posts", { sort: "-date", limit: 10, expand: ["author"] });
const post = await mapled.getRecordBySlug("posts", "hello-world");

The key is the project's delivery key (Mapled: Integrations → Your site). It reads published content of collections with access Public and nothing else, so it is the one key that is safe in a browser — in a Vite app keep it in .env as VITE_MAPLED_KEY. The server key and the webhook signing secret are never safe there.

  • getSingle(key) — the record of a single, or null while it is empty.
  • getRecords(collection, options) — { records, total, release }. Options: filter ({ featured: true, price: { gte: 10, lt: 100 }, tag: { in: ["a", "b"] }, title: { contains: "an" } }), sort ("date" / "-date"), limit (1–100), offset, fields, expand (relation fields, dotted for a second level — linked records arrive under record.expanded).
  • getRecord(collection, id) / getRecordBySlug(collection, slug) — one record, or null when it isn't published.
  • getRelease() — { release, publishedAt } of what is being served now, null before the first publish.
  • Every read takes release: N to read a published release by its number, locale: "ru" to read in one of the project's languages (a translated field answers in it, or in the first language of the project's fallback chain that has a value; the answer names the language it is in as locale), and signal to abandon it. createClient({ key, locale: "ru" }) reads everything in that language; a language the project doesn't have is a 400 INVALID_QUERY.

A record is { id, data }. Rich text values are Markdown — render them with a Markdown component, never as raw HTML. Image and file values are asset ids:

import { assetUrl } from "@mapled/client";

const src = assetUrl(imageId, { width: 1200, format: "webp" });

A failed read throws a MapledError with the API's status, code ("INVALID_QUERY", "RELEASE_NOT_FOUND", "RATE_LIMITED", …) and message. A key gets 1,200 requests a minute.

Typed by your schema

npx @mapled/cli types generate
// src/mapled.ts
import { createClient } from "@mapled/client";
import type { MapledSchema } from "./mapled-types";

export const mapled = createClient<MapledSchema>({ key: import.meta.env.VITE_MAPLED_KEY });
// src/home.ts
import { mapled } from "./mapled";

const home = await mapled.getSingle("homepage");
document.querySelector("h1")!.textContent = home?.data.headline ?? ""; // typed: an unknown key or field is a compile error

Freshness

Content changes when someone presses Publish in Mapled. Answers for the latest release may be reused by the browser for a minute; an answer pinned with release: N never changes and is cached for good. So a page that was loaded before a publish shows it when it is loaded again — or sooner, if it watches for it.

Preview

Mapled's Preview shows drafts on your real site. For a site rendered in the browser there is nothing to set up in code: Mapled opens the page with a one-time token — https://your-site/blog/hello?mapled-preview=mpv_… — and the client, before its first read, takes the token out of the address bar, trades it for a draft session and keeps the session for the tab. Reads made in that tab show drafts until the session ends.

Mapled has to know where the site lives and that it renders in the browser:

  • the site manifest's framework is "react-spa" or "plain-html" (npx @mapled/cli scan --write and bindings push, or your AI agent's push_site_manifest) — or the project was created with that stack;
  • Production domain in Project settings holds the site's public address (an AI agent sets it with set_site_url).
// src/preview-bar.ts
import { endPreview } from "@mapled/client";
import { mapled } from "./mapled";

if (await mapled.isPreview()) {
  // show "You are looking at a preview" with a way out
  document.querySelector("#exit-preview")?.addEventListener("click", () => {
    endPreview();
    location.reload();
  });
}

resolvePreview() does the trade on its own, previewSession() answers the tab's session. On a server, keep the draft session wherever your preview route put it and name it per request:

// server/mapled.ts
import { createClient } from "@mapled/client";

export function mapledFor(request: Request) {
  const session = /(?:^|;\s*)mapled_draft=([^;]+)/.exec(request.headers.get("cookie") ?? "")?.[1] ?? null;
  return createClient({ key: process.env.MAPLED_KEY!, draft: session });
}

draft: null never reads drafts.

Live updates

A page that stays open can hear about a publish:

// src/live.ts
import { mapled } from "./mapled";

export const stop = mapled.watch(async (next) => {
  // read again, pinned to the new release: the browser's cache can't answer with the old one
  const home = await mapled.getSingle("homepage", { release: next.release });
  document.querySelector("h1")!.textContent = home?.data.headline ?? "";
});

The tab asks Mapled for the release pointer — once a minute by default ({ interval: 30 } for thirty seconds, never under fifteen), only while it is visible, at once when it is shown again. Every check is a request of the delivery key's budget (a quiet one is a 304), so keep the interval honest: a thousand open tabs at a minute is most of the budget.

The watch starts from the release the page shows, so a publish that landed between the page's reads and the first check — or while a tab opened in the background waited to be shown — is heard at once instead of at the publish after it: mapled.watch(…) takes it from the client's own reads (every published answer names its release — and a read that answers after the watch started, from an older release than the watch is on, brings that release to onPublish too, once), and { from: 3 } says it outright — a render pinned to release: 3, or from: null for a page that shows nothing published yet. Without either, a first answer whose release was published after the page was loaded counts as a publish.

A site with a server can spare Mapled the traffic: relay GET /v1/delivery/release from a route of your own, cache it there for a few seconds, and point the watch at it.

// src/live-relay.ts
import { watchRelease } from "@mapled/client";

export const stop = watchRelease({ endpoint: "/api/mapled/release", interval: 10, onPublish: () => location.reload() });

Forms

A form is created once — npx @mapled/cli forms create "Contact", or by your AI agent — and gets a key. Submissions show up in Mapled under Forms right away.

// src/contact.ts
import { submitForm } from "@mapled/client";

const form = document.querySelector<HTMLFormElement>("#contact")!;
const status = document.querySelector("#contact-status")!;

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const consent = form.querySelector<HTMLInputElement>("[name=consent]");
  const answer = await submitForm("contact", form, {
    key: "mk_live_…",
    consent: consent?.checked ? "I agree to be contacted about my request." : undefined,
  });
  status.textContent = answer.ok ? "Thank you — we'll be in touch." : "That didn't go through. Try again in a minute.";
});

submitForm takes a <form>, a FormData or a plain object of text, numbers and booleans, and never throws: it answers { ok: true } or { ok: false, status, code, message }. Keep a hidden _gotcha input in the form — people leave it empty, what bots fill in is filed as spam. consent is the exact label of the checkbox the visitor ticked, kept with the submission as evidence; page defaults to the page's path; test: true sends a test submission. A form posted in a tab that shows a preview sends the tab's draft session with it, and Mapled files the submission as a test one: it isn't counted, nobody is notified and no workflow starts.

Live data

Operational collections skip the publish cycle. Your server reads and writes them with the project's server key — the client refuses to be made in a browser.

// server/orders.ts
import { createAppClient } from "@mapled/client";

const db = createAppClient({ serverKey: process.env.MAPLED_SERVER_KEY! });
await db.create("orders", { customer: "Ada Lovelace", total: 120 });
const { records } = await db.list("orders", { filter: { status: "new" }, sort: "-total" });

Check the integration

npx @mapled/cli doctor

doctor knows a site rendered in the browser: it looks for VITE_MAPLED_KEY, reads ○ «Not needed» for the webhook and the routes such a site doesn't have, and fails any env file that puts a server key or a signing secret under a public prefix.

API

Export
createClient({ key, apiUrl?, release?, locale?, draft?, appUrl?, fetch? })The delivery client: getSingle, getRecords, getRecord, getRecordBySlug, getRelease, isPreview, watch(onPublish, { interval?, from? })
createAppClient({ serverKey, apiUrl?, fetch? })Live data, server-side only: list, get, create, update, remove — list and get take locale too
assetUrl(id, { width?, height?, fit?, format?, quality? })The URL of an image or file value
watchRelease({ key | endpoint, interval?, from?, onPublish })Publish notifications without a client
resolvePreview(options?), previewSession(), endPreview(), PREVIEW_PARAMPreview in a browser
submitForm(form, fields, { key, consent?, page?, test? })A visitor's form submission
MapledErrorstatus, code, message of a failed read

The HTTP API underneath — endpoints, the query grammar, caching, the webhook's signature — is described in «Without Next.js: the HTTP API» of the @mapled/next README.

Keywords

mapled

FAQs

Package last updated on 30 Sep 2026

Related posts