
Security News
vlt 1.5.0 Adds Agent Skills Management
vlt 1.5.0 links skills from JavaScript dependencies to coding agents, with lockfile versioning and Socket data to filter out known malicious packages.
@mapled/next
Advanced tools
Read published Mapled content in a Next.js site — delivery client, ISR tags, and a revalidation webhook handler.
Read published Mapled content in a Next.js site — a delivery client with ISR cache tags, a revalidation webhook handler so publishes show up instantly, and live updates for the tabs that are already open.
Mapled is a hosted headless CMS built for sites created with AI. Your agent (or you) models collections and fills in content; this package is how the site reads what's published.
npm install @mapled/next
Grab the delivery key in Mapled: project settings (the gear in the project's header) → Integrations → Your site, and put it in your site's env:
MAPLED_KEY=mk_live_…
An AI agent connected through @mapled/mcp does all of this for you. Nothing in it needs one, though: this is the same integration, step by step — from a Next.js site with hardcoded content to a site its editors run from Mapled. Plan on an hour for a small site.
You need a Mapled project where you can change the structure (the builder plan), a Next.js 14+ site on the App Router, and Node 18 or newer. The example is a site with a homepage and a blog; the keys (homepage, articles) are whatever you choose in step 1.
In Mapled, open the project and press the wrench in the header (Structure) → Collections. Create a single for every one-off part of the site (homepage, footer) and a collection for every list of alike things (articles, team), then Add field for each text, image and link the code hardcodes today.
getRecords("articles"), data.title. A key can be renamed later and the old one keeps working as an alias, but it is a migration: choose them with care.getRecordBySlug finds records by it.Open Content, move the hardcoded texts and images into records, then Review & publish. The site only ever reads published releases: before the first publish the delivery API answers 404 “Nothing published yet.” — getSingle and getRecord read that as null, getRecords throws.
npx @mapled/cli auth login
npx @mapled/cli project link
npx @mapled/cli types generate
npx @mapled/cli schema pull
Your browser opens Mapled, you pick the project and authorize; project link writes mapled.json, types generate writes mapled-types.ts — an interface per collection and single, tied together by MapledSchema — and schema pull records the schema the site is built against in mapled/schema.json, which schema diff and doctor compare with Mapled later. (@mapled/cli has the details of every command used here.)
Install the package and set MAPLED_KEY (Install) in .env.local and in your hosting's environment. Then one client for the whole site:
// lib/mapled.ts
import { createClient } from "@mapled/next";
import type { MapledSchema } from "../mapled-types";
export const mapled = createClient<MapledSchema>({ key: process.env.MAPLED_KEY! });
With MapledSchema every read is typed by its key, and a misspelled collection or field is a build error.
Read in Server Components — no client-side fetching, no loading states. A single:
// app/page.tsx
import { assetUrl } from "@mapled/next";
import { mapled } from "../lib/mapled";
export default async function Home() {
const home = await mapled.getSingle("homepage");
if (!home) return null; // nothing published yet
return (
<main>
<h1>{home.data.headline}</h1>
<p>{home.data.intro}</p>
{home.data.cover ? <img src={assetUrl(home.data.cover, { width: 1280 })} alt="" /> : null}
</main>
);
}
A list:
// app/blog/page.tsx
import Link from "next/link";
import { mapled } from "../../lib/mapled";
export default async function Blog() {
const { records } = await mapled.getRecords("articles", { sort: "-date", limit: 20 });
return (
<ul>
{records.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.data.slug}`}>{post.data.title}</Link>
</li>
))}
</ul>
);
}
A page per record — new records get their page without a deploy:
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import Markdown from "react-markdown";
import { mapled } from "../../../lib/mapled";
export async function generateStaticParams() {
const { records } = await mapled.getRecords("articles", { fields: ["slug"] });
return records.map((post) => ({ slug: post.data.slug }));
}
export default async function Article({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await mapled.getRecordBySlug("articles", slug);
if (!post) notFound();
return (
<article>
<h1>{post.data.title}</h1>
<Markdown>{post.data.body}</Markdown>
</article>
);
}
(params is a Promise since Next.js 15; on 14 it is the plain object.) Rich text is Markdown — render it with a component such as react-markdown, never as raw HTML. Leave in the code what editors shouldn't touch: layout, navigation structure, anything that is logic rather than content.
Reads are cached (revalidate: 3600) and tagged; a webhook refreshes them the moment someone publishes.
// app/api/mapled/revalidate/route.ts
import { createRevalidateHandler } from "@mapled/next/server";
export const POST = createRevalidateHandler({
secret: process.env.MAPLED_WEBHOOK_SECRET!,
});
Deploy, then in Mapled → Integrations → Your site save https://your-site.com/api/mapled/revalidate as the Revalidation webhook, copy the Signing secret into MAPLED_WEBHOOK_SECRET (hosting env and .env.local), redeploy, and press Send test ping: the delivery shows up under Recent deliveries as Delivered. The URL has to be public — Mapled refuses addresses that resolve to a private network.
Optional: pages that are already open can follow a publish too, without a reload — a route and one component, see Live updates.
// app/api/mapled/preview/route.ts
import { createPreviewHandler } from "@mapled/next/server";
export const GET = createPreviewHandler();
// app/api/mapled/preview/exit/route.ts
import { createExitPreviewHandler } from "@mapled/next/server";
export const GET = createExitPreviewHandler();
The Preview button in Mapled opens the site at the origin of the webhook URL from step 6, on exactly this path — /api/mapled/preview — so keep the route there. It turns on Next.js draft mode, and every read of the client switches to live drafts, uncached, with no changes in your components.
A contact or signup form posts straight to Mapled; submissions land in Forms, with e-mail notifications and spam protection. Create the form once:
npx @mapled/cli forms create "Contact"
and post to it from the site — see Forms for the component.
Orders, bookings, votes — records the site's backend writes and reads immediately, without Publish — go to collections created with the mode Live data and are reached with the server key: see Live data.
npx @mapled/cli scan --write
npx @mapled/cli bindings push
npx @mapled/cli md pull
scan reads the code and writes mapled/manifest.json: which page reads which collection and field. bindings push sends it to Mapled, so editors see where a field appears and get a warning before a change would break the site (Structure → Bindings). md pull writes MAPLED.md, the guide for whoever works on the site next — person or AI agent. Commit mapled.json, mapled-types.ts, mapled/schema.json, mapled/manifest.json and MAPLED.md; never commit an env file.
npx @mapled/cli doctor
It checks the integration from both sides — the repository and what Mapled sees of the deployed site. The list below is what an agent's setup has to pass before Mapled calls it complete; doctor answers all of it except the last two lines, which are yours to check:
Later, when the structure changes in Mapled, npx @mapled/cli schema diff says what changed and whether it breaks the site; after updating the code, run types generate, scan --write, bindings push and md pull again.
import { createClient } from "@mapled/next";
const mapled = createClient({ key: process.env.MAPLED_KEY! });
// A collection, with filters, sort and pagination
const { records } = await mapled.getRecords("articles", {
filter: { featured: true, date: { gte: "2026-01-01" }, title: { contains: "launch" } },
sort: "-date",
limit: 10,
});
// One record — by id or by its slug field
const post = await mapled.getRecord("articles", id);
const about = await mapled.getRecordBySlug("pages", "about");
// A single (e.g. the homepage)
const home = await mapled.getSingle("homepage");
// Linked records come along under `expanded` (two levels at most)
const { records: posts } = await mapled.getRecords("articles", {
expand: ["author", "related.author"],
fields: ["title", "slug"],
});
posts[0].expanded?.author; // { id, data: { name: "Ada" } } or null
Every call reads the latest published release — drafts never leak. Fetches are tagged mapled and mapled:<collection> and default to revalidate: 3600; the webhook below refreshes them the moment someone publishes.
npx @mapled/cli types generate writes mapled-types.ts: one interface per collection and single, and a MapledSchema that ties them together. Hand it to the client once and every read is typed by its key:
import type { MapledSchema } from "./mapled-types";
const mapled = createClient<MapledSchema>({ key: process.env.MAPLED_KEY! });
const { records } = await mapled.getRecords("articles"); // records[0].data is an Article
const home = await mapled.getSingle("homepage"); // Homepage | null
await mapled.getRecords("artcles"); // type error — no such collection
Without a schema the client behaves as before: any key, Record<string, unknown> data, or the type you name per call (getRecords<Article>("articles")). createAppClient<MapledSchema> types live data the same way — db.create("orders", data) wants an Order. mapled doctor tells you when the generated file is behind the schema.
When a field's key is renamed in Mapled, nothing breaks: the old key stays as an alias — delivered next to the new one, accepted in filter, sort, fields, expand and in live-data writes — until someone removes it in Field settings. Answers that still serve a previous key name it (aliases: { products: { price: "amount" } } on a list result), and outside production the client says so once per key:
[mapled] “price” in “products” was renamed to “amount”. The old key keeps working until its alias is removed in Mapled — use “amount” instead.
Move the code to the new key, run mapled types generate (the old key is typed @deprecated until then), and remove the alias. In production the client stays silent.
A page that makes several reads can pin them all to one release, so a publish landing mid-render can't mix two versions:
const live = await mapled.getRelease(); // { release: 12, publishedAt } — null before the first publish
const { records } = await mapled.getRecords("articles", { release: live!.release });
const about = await mapled.getRecordBySlug("pages", "about", { release: live!.release });
createClient({ key, release: 12 }) pins every read — a way to hold a site on a known-good release without publishing anything. A release never changes, so pinned reads are cached until the next publish (revalidate: false unless you pass your own); a release that doesn't exist fails with a 404 RELEASE_NOT_FOUND rather than reading as a missing record. In preview (draft mode) the pin is dropped: drafts are unversioned.
A project with several languages (Mapled: Settings → Languages) translates the fields an editor marked as translated. Every read takes locale:
const about = await mapled.getRecordBySlug("pages", "about", { locale: "ru" });
const { records, locale } = await mapled.getRecords("articles", { locale: "pt-BR", sort: "-date" }); // locale: "pt-BR"
A translated field answers in that language, or in the first language of the project's fallback chain that has a value — the default language at the end — so a value is never missing where the default language has one; shared fields come as they are. Filters, sorts and slug lookups see that language too, and the answer names the language it is in (locale). createClient({ key, locale: "ru" }) reads everything in that language (a read's own locale wins). Without locale a read answers in the project's default language, as before. A language the project doesn't have — or that the release you read was published without — fails with a 400 INVALID_QUERY.
rich_text values are Markdown (headings, lists, links, bold/italic, images as ). Render them with a Markdown component such as react-markdown — never inject them as raw HTML.
Image fields hold asset ids. assetUrl turns one into a URL — the original, or a resized variant that Mapled makes on the first request and keeps:
import { assetUrl } from "@mapled/next";
<img src={assetUrl(post.data.cover, { width: 640 })} alt={post.data.title} />
// width/height snap to presets (64 … 2560) and never upscale; fit: inside | cover | contain;
// format: webp (default) | avif | jpeg | png; quality 30 … 100
Mount the webhook handler:
// app/api/mapled/revalidate/route.ts
import { createRevalidateHandler } from "@mapled/next/server";
export const POST = createRevalidateHandler({
secret: process.env.MAPLED_WEBHOOK_SECRET!,
});
Then in Mapled (Integrations → Your site) set the revalidation webhook to https://your-site.com/api/mapled/revalidate and copy the signing secret into MAPLED_WEBHOOK_SECRET. Every publish sends a signed ping; the handler verifies the x-mapled-signature HMAC and revalidates the mapled tags.
Each ping is tried up to three times (after 2 and 6 seconds, 5 seconds to answer); Recent deliveries keeps them for 30 days, and a failed one can be sent again from there. Rotate secret replaces the signing secret at once — deliveries fail until the site has the new one.
The webhook refreshes the site's cache; a page someone already has open still shows what it rendered. Two files make open tabs follow Publish as well. A route that says which release the site serves:
// app/api/mapled/release/route.ts
import { createReleaseHandler } from "@mapled/next/server";
export const GET = createReleaseHandler({ key: process.env.MAPLED_KEY! });
and one component in the root layout:
// app/layout.tsx
import type { ReactNode } from "react";
import { MapledLive } from "@mapled/next/live";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
{children}
<MapledLive />
</body>
</html>
);
}
<MapledLive /> renders nothing. While the tab is visible it asks the route every 10 seconds (interval, in seconds), and when a publish lands it calls router.refresh(): Server Components re-render with the new release in place — scroll position and client state stay. A hidden tab doesn't ask; it checks the moment it is shown again. The first check counts too: when the release it finds was acknowledged by your site's cache after the page was requested — a publish between the render and that check, or while a tab opened in the background waited to be shown — the page refreshes, and onPublish gets previous as null (which release the page rendered, the tab can't tell). A page requested within ten seconds after the site answered a publish's webhook may refresh once for nothing. To offer the update instead of applying it under the reader, pass onPublish:
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { MapledLive } from "@mapled/next/live";
export function UpdateNotice() {
const router = useRouter();
const [fresh, setFresh] = useState(false);
return (
<>
<MapledLive onPublish={() => setFresh(true)} />
{fresh ? <button onClick={() => { router.refresh(); setFresh(false); }}>New content — refresh</button> : null}
</>
);
}
ttl seconds (default 5) — in the instance and, through s-maxage, at the CDN in front of it — and a check that finds nothing new is a 304. However many tabs are open, Mapled hears from the site a few times a minute, and the key's rate limit is left for reads.mapled cache tag as the content, and never names one newer than the release your site's cache last answered the publish webhook for: the webhook handler revalidates the cache before it answers, its answer carries the cache's mark (x-mapled-cache), and Mapled records the answer with the mark. So a cache entry filled early — after a deploy, an eviction, or when its revalidate window runs out between a publish and its webhook — doesn't move the route ahead of the pages. Only while a publish's webhook is on its way — or failing — does the route ask Mapled past its cache, once per ttl. Live updates therefore need the webhook from Refresh on publish: until your site has answered a publish's webhook, the route names the release before it (null before the first one it answered), and without a webhook it names nothing new — nothing tells it when your pages have a release, so open tabs don't follow Publish. A new webhook address starts over: the site there hasn't answered anything yet.cacheMark()), so deployments that share a cache share the mark, and one with a cache of its own has another. After a deploy onto a fresh cache — or once the cache has lost the mark — the route names no release until the next publish's webhook has been answered from that cache. (Before 0.11.0 such a deployment followed the webhook's one and could refresh its tabs into the release they already showed.)mapled tag, say — answers with the cache's mark, or the route names no release (it logs once why):import { revalidateTag } from "next/cache";
import { cacheMark, verifySignature } from "@mapled/next/server";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-mapled-signature") ?? "";
if (!(await verifySignature(process.env.MAPLED_WEBHOOK_SECRET!, body, signature))) {
return Response.json({ error: "Invalid signature." }, { status: 401 });
}
revalidateTag("mapled");
// after revalidating: the mark says which cache has let go of the old release
return Response.json({ revalidated: true }, { headers: { "x-mapled-cache": await cacheMark() } });
}
{ release, publishedAt, acknowledgedAt } — the number of publishes, the time of the last one and when your site's cache answered its webhook (0.11.3 and later; the Pages Router's route answers the first two), nothing else — and may be cached by anyone. While Mapled can't be reached it keeps answering the last release it knew.router.refresh() asks for the page's Server Components at the same URL every time, and a static page answers with s-maxage and stale-while-revalidate but no max-age: stale for the browser at once, which Chrome and Edge serve anyway while they fetch a fresh copy behind it. So from the first publish a tab hears about, every refresh of the page goes with cache: "no-cache", and your site may still answer 304. That covers the component's own refresh, the one your onPublish makes (like the button above), and any your code makes. The layer changes only the cache mode, and only of requests your server reads as a refresh of the page the tab is on. Navigations, server actions, your own requests and any request that sets its own cache go as they did. A layer over fetch, put in place with the first publish, does it; a fetch wrapper of your own keeps working either way. (Before 0.11.1 an open tab showed the release before the one it was told about, from its second publish on.)<MapledLive interval={3} /> and createReleaseHandler({ key, ttl: 1 }) make it quicker at the price of more requests to your site.basePath in next.config, the route answers under it, and so does the default: <MapledLive /> asks /docs/api/mapled/release on a site with basePath: "/docs" — Next writes the basePath into the bundle, and the component reads it from there (0.10.0 and later; earlier versions ask /api/mapled/release). An endpoint you pass is asked as written, basePath included: <MapledLive endpoint="/docs/live" />.createReleaseHandler sets how it is cached. dynamic = "force-static" turns it into a copy Next builds without the tab's request, and fetchCache = "force-no-store" reads the release past the cache tag, so Mapled hears from the route every ttl. dynamic = "force-dynamic" does the same when the handler's revalidate is false or 0; at its default it is harmless. On the webhook route, leave fetchCache off too: with "force-no-store" every answer names a new cache mark, and the release route may name no release. Middleware that runs on the route has to let the tab's GET through: no redirect, rewrite or sign-in wall. output: "export" has no server to run the route at all.Outside React, watchRelease({ onPublish }) from @mapled/next is the same watch as a plain function — it returns the function that stops it.
A site on pages/ mounts the route as an API route and renders the component from @mapled/next/live/pages in its custom App:
// Pages Router: pages/api/mapled/release.ts
import { createPagesReleaseHandler } from "@mapled/next/server";
export default createPagesReleaseHandler({ key: process.env.MAPLED_KEY! });
// Pages Router: pages/_app.tsx
import type { AppProps } from "next/app";
import { MapledLive } from "@mapled/next/live/pages";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<MapledLive />
</>
);
}
On a publish it replaces the page with itself through next/router, so the page's data runs again and it re-renders with the new props in place: scroll position and client state stay. A #hash in the address goes: while there is one, a replace to the same page runs no data, and every render the router makes scrolls back to the hash's anchor. For the router it is a navigation to the same URL, so routeChangeStart and routeChangeComplete fire — a pageview counter listening to them counts it too. A publish that lands while the reader is on the way to another page, or to an anchor on this one, doesn't cancel that navigation: it waits for the page they land on and refreshes that one. The first check counts too: when the release it finds went live after the page was requested — a publish between getServerSideProps and that check, or while a tab opened in the background waited to be shown — the page refreshes, and onPublish gets previous as null (which release the page read, the tab can't tell). A page requested within ten seconds after a publish may refresh once for nothing. endpoint, interval and onPublish work as above. (<MapledLive /> from @mapled/next/live would reload the whole tab here.)
getServerSideProps. The Pages Router has no cache tag the route could share with a page, so createPagesReleaseHandler answers the live release as Mapled has it, and it moves the moment someone publishes. A page that reads in getServerSideProps reads that same release, so the refresh always renders the new content. Keep such a page out of shared caches — Next.js sends it uncached unless you set Cache-Control yourself.getStaticProps page can't keep up: it is a copy Next.js regenerates on its own schedule — when its revalidate window runs out, or when a webhook of yours calls res.revalidate — and the route can't wait for that. A tab that refreshes before the copy does gets the old copy back and won't hear of this publish again. Read the pages that should follow Publish in getServerSideProps instead.onPublish.Mount the preview routes:
// app/api/mapled/preview/route.ts
import { createPreviewHandler } from "@mapled/next/server";
export const GET = createPreviewHandler();
// app/api/mapled/preview/exit/route.ts
import { createExitPreviewHandler } from "@mapled/next/server";
export const GET = createExitPreviewHandler();
The Preview button in Mapled opens your site through a one-time link: the route turns on Next.js draft mode, and every getRecords/getRecord/getSingle call automatically switches to live drafts, uncached. No code changes in your components — the client detects draft mode on its own. Sessions last two hours; /api/mapled/preview/exit leaves preview.
Mapled finds the site by the revalidation webhook's URL — preview opens at its origin, on /api/mapled/preview — so set the webhook up first and keep the route on that path.
A form is created once — npx @mapled/cli forms create "Contact", or by your AI agent — and gets a key (contact). The site posts submissions as flat JSON with the delivery key; they show up in Mapled under Forms right away, and the form's recipients — the project's owners, unless its settings name others — get an e-mail. Forms never wait for Publish.
// app/contact/ContactForm.tsx
"use client";
import { useState, type FormEvent } from "react";
const CONSENT = "I agree to be contacted about my request.";
export function ContactForm() {
const [state, setState] = useState<"idle" | "sending" | "sent" | "failed">("idle");
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setState("sending");
const form = new FormData(event.currentTarget);
const res = await fetch("https://api.mapled.io/v1/delivery/forms/contact", {
method: "POST",
headers: { "content-type": "application/json", "x-mapled-key": process.env.NEXT_PUBLIC_MAPLED_KEY! },
body: JSON.stringify({
name: form.get("name"),
email: form.get("email"),
message: form.get("message"),
_gotcha: form.get("_gotcha"), // the honeypot: people leave it empty
_consent: form.get("consent") ? CONSENT : undefined,
_page: window.location.pathname,
}),
});
setState(res.ok ? "sent" : "failed");
}
if (state === "sent") return <p>Thank you — we'll be in touch.</p>;
return (
<form onSubmit={submit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<input name="_gotcha" tabIndex={-1} autoComplete="off" aria-hidden="true" style={{ display: "none" }} />
<label>
<input name="consent" type="checkbox" required /> {CONSENT}
</label>
<button disabled={state === "sending"}>Send</button>
{state === "failed" ? <p>That didn't go through. Try again in a minute.</p> : null}
</form>
);
}
NEXT_PUBLIC_MAPLED_KEY; never the server key or the webhook secret. To keep even that out of the bundle, post from a Server Action with MAPLED_KEY instead — all visitors then share your server's address for the limit below._ are instructions, not answers: _gotcha (a hidden field bots fill in — such submissions are accepted and filed as spam), _consent (the exact label of the consent checkbox the visitor ticked, kept as evidence), _page (where the form was), _test: true (a test submission: no e-mail, not counted).201 { "ok": true } on success. 400 VALIDATION_FAILED for a body of another shape, 404 for an unknown key or form, 429 RATE_LIMITED past 30 submissions a minute per form and address, 429 FORMS_LIMIT when the project's monthly submissions are used up, 410 PROJECT_ARCHIVED for an archived project.npx @mapled/cli forms list shows the project's forms and their URLs.Operational collections skip the publish cycle — saves take effect immediately. Your site's backend reads and writes them through the Application Data API with the project's server key (Mapled: Integrations → Your site). Server-side only; the key must never reach the browser.
MAPLED_SERVER_KEY=msk_live_…
import { createAppClient } from "@mapled/next";
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" });
await db.update("orders", id, { ...data, status: "shipped" });
await db.remove("orders", id);
The server key reaches only operational collections with access class public/server — editorial content, internal and sensitive collections stay out of reach, and it grants nothing on the Management API.
A write is checked against the collection's fields: data that doesn't fit them is a 400 VALIDATION_FAILED, a slug another record uses a 409 SLUG_TAKEN. A write that ran while a field it sends was renamed, removed or changed is a 409 FIELDS_CHANGED and writes nothing — send it again against the fields as they are now.
The client is a thin layer over a plain HTTP API, so any site — or a build script — can read Mapled. The base URL is https://api.mapled.io; every request carries the delivery key in x-mapled-key.
| Request | Answer |
|---|---|
GET /v1/delivery/collections/{collection}/records | { records: [{ id, data, expanded? }], total, release } |
GET /v1/delivery/collections/{collection}/records/{id} | { record, release } — 404 when it isn't in the release |
GET /v1/delivery/collections/{collection}/records/by-slug/{slug} | { record, release } |
GET /v1/delivery/singles/{single} | { record, release } — record is null while the single is empty |
GET /v1/delivery/release | { release, publishedAt } — the release being served now; nulls before the first publish. With ?webhook=1 also webhook: { release, publishedAt, deliveredAt, cache } of the newest release your site answered the publish webhook for, with the x-mapled-cache mark the answer carried (nulls before the first, and again after the webhook's address changes), or null when the project has no webhook |
POST /v1/delivery/forms/{form} | 201 { ok: true } — see Forms |
curl -H "x-mapled-key: $MAPLED_KEY" \
"https://api.mapled.io/v1/delivery/collections/articles/records?filter[featured]=true&filter[date][gte]=2026-01-01&sort=-date&limit=10&expand=author&fields=title,slug"
filter[field]=value or filter[field][op]=value (eq, ne, lt, lte, gt, gte, in — comma-separated, contains), sort=field / sort=-field, limit (1–100, default 100) and offset. Every read takes expand (relation keys, dotted for a second level), fields, release=N to read a published release by its number and 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, filters and slug lookups see that language, and the answer carries locale. A query the schema can't answer is a 400 INVALID_QUERY that says why.https://api.mapled.io/files/{id} is the file, with ?w=, h=, fit=, fmt=, q= for a resized variant.Cache-Control: private — with an ETag: send it back as If-None-Match and an unchanged answer is a 304. A read pinned with release=N never changes and may be cached for good. GET /v1/delivery/release is the cheap way to learn that something was published: relay it from a route of your own server, and watchRelease({ endpoint, onPublish }) from @mapled/next — no framework in it — tells open tabs (Live updates).{ error: { code, message, requestId } }. A key gets 1,200 requests a minute; past that, 429 RATE_LIMITED with retry-after.POST with a JSON body { event, project, release, at, id } and the headers x-mapled-event (release.published, project.restored or test), x-mapled-event-id (the same on every retry — use it to deduplicate), x-mapled-attempt and x-mapled-signature: sha256= followed by the hex HMAC-SHA256 of the raw body with the signing secret. Compare in constant time, answer 2xx within 5 seconds. verifySignature from @mapled/next/server does the check anywhere Web Crypto exists. An answer may carry x-mapled-cache — up to 64 letters, digits, - or _ naming the cache the handler revalidated; Mapled records it with the release, and the ?webhook=1 pointer gives it back.GET / POST /v1/app/collections/{collection}/records and GET / PATCH / DELETE /v1/app/collections/{collection}/records/{id} with the server key in x-mapled-server-key; writes send { "data": { … } }.Preview (drafts on the site) is built on Next.js draft mode and ships with this package only.
createClient({ key, apiUrl?, release? }) → getRecords(collection, opts?), getRecord(collection, id, opts?), getRecordBySlug(collection, slug, opts?), getSingle(key, opts?), getRelease(); createClient<MapledSchema>(…) with the generated MapledSchema types every read by its key (MapledClient<S>, MapledAppClient<S> name the typed clients)opts: filter (a value for equality, or { eq, ne, lt, lte, gt, gte, in, contains } — text takes eq/ne/in/contains, numbers and dates comparisons, booleans eq/ne, image and relation ids eq/ne/in; a one-to-many relation matches when it includes the id), sort ("field" / "-field", not on relations or images), limit (≤100), offsetexpand (relation keys, dotted for a second level), fields (data keys to keep), release (pin to a published release), locale (read in one of the project's languages, through its fallback chain), revalidate, tags; a query the schema can't answer fails with a 400 INVALID_QUERY message, and errors carry the API's code on MapledErrorcreateAppClient({ serverKey, apiUrl? }) → list(collection, opts?), get(collection, id, opts?), create(collection, data), update(collection, id, data), remove(collection, id) — list and get take locale tooassetUrl(id, { width?, height?, fit?, format?, quality?, apiUrl? }) — URL of an image or file valuecreateRevalidateHandler({ secret, tags? }) — App Router POST handlercreatePreviewHandler({ apiUrl?, appUrl? }), createExitPreviewHandler() — App Router GET handlers of the preview routescreateReleaseHandler({ key, apiUrl?, ttl?, revalidate? }) — App Router GET handler of the release route; <MapledLive endpoint? interval? onPublish? /> from @mapled/next/live and watchRelease({ endpoint?, interval?, onPublish }) ask itcreatePagesReleaseHandler({ key, apiUrl?, ttl? }) — Pages Router API route of the release route (the default export of pages/api/mapled/release.ts); <MapledLive endpoint? interval? onPublish? /> from @mapled/next/live/pages asks itverifySignature(secret, body, signature) — if you'd rather build your own handlercacheMark() — the mark of this deployment's Next.js cache: a webhook handler of your own answers with it in x-mapled-cache (createRevalidateHandler does), and createReleaseHandler follows only an answer that carries its ownThe delivery key only reads published, public content — safe to use anywhere.
FAQs
Read published Mapled content in a Next.js site — delivery client, ISR tags, and a revalidation webhook handler.
The npm package @mapled/next receives a total of 196 weekly downloads. As such, @mapled/next popularity was classified as not popular.
We found that @mapled/next demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
vlt 1.5.0 links skills from JavaScript dependencies to coding agents, with lockfile versioning and Socket data to filter out known malicious packages.

Security News
Rust 1.100 will add opt-in dependency cooldowns to Cargo, giving security scanners and maintainers time to catch malicious crate releases.

Security News
Cloudflare is acquiring Deno’s team, which will provide another year of bug fixes and security updates before ending support for the runtime.