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

@mapled/nuxt

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mapled/nuxt

Read published Mapled content in a Nuxt site — a server-side client with a tag cache, the publish webhook (purge the Nitro cache or call a deploy hook), preview, live updates and a Nuxt module that mounts it all.

latest
npmnpm
Version
0.1.1
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

@mapled/nuxt

Read published Mapled content in a Nuxt site — a server-side client with a tag cache, a publish webhook that purges it and Nitro's cache (or rebuilds a static site), preview of drafts, live updates for the tabs that are already open, and a Nuxt module that mounts it all.

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 a Nuxt site reads what's published. It is @mapled/client with Nuxt's idioms around it: the same reads, the same types from mapled types generate.

Install

npm install @mapled/nuxt

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 with the webhook secret:

MAPLED_KEY=mk_live_…
MAPLED_WEBHOOK_SECRET=whsec_…

Add the module — it mounts the webhook, the preview routes and the release route under /api/mapled, the middleware that keeps draft pages out of caches, the useMapled() composable and the <MapledLive /> component:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@mapled/nuxt/module"],
});

The module takes options as ["@mapled/nuxt/module", { prefix: "/api/mapled", browser: true, routes: true, imports: true }]. The delivery key is the one key that may be public: with browser: true (the default) it also goes into runtimeConfig.public.mapled, so a read that runs in the browser after a client-side navigation works; the webhook secret and a server key never leave the server. browser: false keeps the key out of the client bundle and makes useMapled() server-only.

Read published content

useMapled() is the site's client, typed by the schema npx @mapled/cli types generate writes. Read in useAsyncData, so the render waits for the content:

<!-- pages/index.vue -->
<script setup lang="ts">
import type { MapledSchema } from "~/mapled-types";

const cms = useMapled<MapledSchema>();
const { data: home } = await useAsyncData("home", () => cms.getSingle("homepage"));
const { data: posts } = await useAsyncData("posts", () => cms.getRecords("posts", { sort: "-date", limit: 10 }));
</script>

<template>
  <h1>{{ home?.data.headline }}</h1>
  <ul>
    <li v-for="p in posts?.records" :key="p.id"><NuxtLink :to="`/blog/${p.data.slug}`">{{ p.data.title }}</NuxtLink></li>
  </ul>
</template>
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const cms = useMapled();
const { data: post } = await useAsyncData(`post:${route.params.slug}`, () => cms.getRecordBySlug("posts", String(route.params.slug)));
if (!post.value) throw createError({ statusCode: 404, statusMessage: "Not found" });
</script>

On the server, useMapled() reads MAPLED_KEY and, when set, MAPLED_API_URL from the environment, and sees the request's preview cookie. The reads are those of @mapled/client: getRecords(collection, { filter, sort, limit, offset, fields, expand, search, locale }), getRecord, getRecordBySlug, getSingle, getRelease, isPreview. Each published answer is kept in the adapter's cache — in the server process, under the tags mapled and mapled:<collection> — for an hour, or for as long as you say:

// server/utils/reads.ts
import { mapled } from "@mapled/nuxt";

export async function homepage() {
  const cms = mapled();
  // kept until the next publish purges it; `revalidate: 60` keeps it a minute, `revalidate: 0` never
  return cms.getSingle("homepage", { revalidate: false, tags: ["layout"] });
}

A page and its layout asking for the same record during one render make one request. The cache is this process's: a long-running server (the Node preset) holds one, serverless functions hold one each — keep revalidate short there, or generate the site statically. Route rules that cache whole pages (isr, swr, cache) sit on top: the webhook drops those too, wherever Nitro keeps them itself (the Node preset, useStorage("cache")). On Vercel, Nitro's preset hands an isr or swr rule to Vercel's own cache instead: the platform answers a hit without running the site, so the webhook can't drop that page and a draft request gets the published one — keep those rules off the pages that read Mapled there. mapled(event) is the client by hand on the server — the h3 event of the request, so drafts are seen — and createClient({ key, apiUrl, draft, cache: false }) builds one from parts, with the cache off when you say so.

Rich text and images

rich_text values are Markdown — render them with a Markdown component, never as raw HTML. Image values are asset ids: assetUrl(id, { width, format }) gives the URL of a resized variant.

// utils/images.ts
import { assetUrl } from "@mapled/nuxt";

export const cover = (id: string) => assetUrl(id, { width: 1280, format: "webp" });

Refresh on publish

Point the project at the webhook the module mounted (Integrations → Your site → Revalidation webhook: https://your-site/api/mapled/revalidate, or let your agent call configure_revalidation). Every publish is signed with MAPLED_WEBHOOK_SECRET; the handler purges the adapter's cache and Nitro's (useStorage("cache"): the pages route rules cached, defineCachedEventHandler, defineCachedFunction), so the next render reads the new release.

A prerendered site can't re-render on its own: set MAPLED_DEPLOY_HOOK_URL to your host's build hook (Vercel, Netlify, Cloudflare Pages…) and the same handler calls it once per publish — a retry of the same publish doesn't build twice, and a hook that refuses fails the delivery so Mapled retries. The URL is read from the environment on the first webhook and never written anywhere. The handler needs a server to run on: prerender the pages with route rules (routeRules: { "/": { prerender: true } }) and nuxt build, which keeps /api/mapled/* as server routes. nuxt generate writes files and no server at all — nothing of such a site can take the webhook.

Preview drafts

Mapled opens the site through a one-time link to /api/mapled/preview, which trades it for a draft session, keeps the session in the mapled_draft cookie (HttpOnly, Secure, SameSite=Lax) and redirects into the site. From then on useMapled() reads drafts for that visitor on the server, uncached, until /api/mapled/preview/exit clears the cookie — published content for everyone else. The middleware sees to the rest, through Nitro's page caches too (isr, swr, cache route rules serve a hit without rendering and render a miss without the cookie): it puts the session on the event's context, where useMapled() reads it first, gives the request a cache key nobody asks for twice (a random mark, never the session), so a draft request is never a cache hit — not the published page's, not an earlier draft's, not after the session expired, and forces cache-control: private, no-store on the answer, so no CDN keeps it. Both routes redirect only to a path of the site itself. A draft is read on the server: after a client-side navigation, reload the page to see the draft again.

Live updates

Open tabs can follow Publish. Render the component once, in app.vue — it asks the release route (/api/mapled/release, or under the module's prefix) every few seconds and reloads the page when a publish lands:

<!-- app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
    <MapledLive />
  </NuxtLayout>
</template>

The route names the release this server last acknowledged through the webhook — never one its cache hasn't been purged for — so a tab that reloads on it renders the new content, not the old one again. It keeps the delivery key on the server and never asks Mapled. A site without a server (nuxt generate) passes the delivery key instead, <MapledLive :delivery-key="config.public.mapled.key" /> — the one key that is safe in a page — and the tab asks Mapled directly, once a minute. From a script of your own: startLive({ endpoint }) (or { key }) from @mapled/nuxt/live reloads on a publish and returns a stop function; watchRelease is the client's own, for anything else.

Without the module

Every piece is a function you can mount yourself. Each handler takes the h3 event (or a Request) and answers a Response, so it is a Nitro route as it is:

// server/api/mapled/revalidate.post.ts
import { createRevalidateHandler } from "@mapled/nuxt/server";

// the secret is MAPLED_WEBHOOK_SECRET of the server, read when the first webhook arrives; `{ secret }` passes one by hand
const handler = createRevalidateHandler({ storage: useStorage("cache") });
export default defineEventHandler((event) => handler(event));
// server/api/mapled/preview.get.ts
import { createPreviewHandler } from "@mapled/nuxt/server";

// trades the link at MAPLED_API_URL when the site's env has one — the API the reads go to
export default defineEventHandler(createPreviewHandler());
// server/api/mapled/preview/exit.get.ts
import { createExitPreviewHandler } from "@mapled/nuxt/server";

export default defineEventHandler(createExitPreviewHandler());
// server/api/mapled/release.get.ts
import { createReleaseHandler } from "@mapled/nuxt/server";

export default defineEventHandler(createReleaseHandler());
// server/middleware/mapled.ts
import { onRequest } from "@mapled/nuxt/middleware";

export default defineEventHandler((event) => onRequest(event));
// composables/useCms.ts
import { mapled } from "@mapled/nuxt";
import type { MapledSchema } from "../mapled-types";

/** the server's client for the request being served: drafts under the preview cookie */
export const useCms = () => mapled<MapledSchema>(useRequestEvent());

A handler of your own can verify a request with verifySignature(secret, body, header), drop what it needs with purge(["mapled:posts"]), read the Request of an event with requestOf(event) and its body with boundedText(request, WEBHOOK_BODY_LIMIT) — the webhook reads at most WEBHOOK_BODY_LIMIT bytes before it checks the signature and answers 413 past it (BodyTooLarge) — and answer with the header x-mapled-cache: cacheMark() so live updates follow it. draftOf(event) answers the draft session of a request, or null — the one the middleware left on event.context under DRAFT_CONTEXT_KEY, else the cookie's; draftCookieOf(event) the cookie's alone; cookieHeaderOf(event) the Cookie header it reads; DRAFT_KEY_MARKER is what the middleware appends to the request's originalUrl for Nitro's cache key, followed by draftCacheNonce(), a fresh random mark; NITRO_CACHE_BASES names the groups of Nitro's cache the webhook clears.

Live data

Operational collections — content the site's backend writes, which takes effect without a publish — are createAppClient({ serverKey }) from the same package, server-side only. Its calls, Site Identity and the data of signed-in people are documented in @mapled/client.

// server/api/orders.post.ts
import { createAppClient } from "@mapled/nuxt";

const db = createAppClient({ serverKey: process.env.MAPLED_SERVER_KEY! });
export default defineEventHandler(async (event) => {
  const body = (await readBody(event)) as { email: string };
  const order = await db.create("orders", { email: body.email });
  return { id: order.id };
});

API

ImportWhat it is
useMapled() (auto-imported by the module)The site's client: on the server with drafts and the cache, in the browser with the public delivery key
mapled(event?) from @mapled/nuxtThe site's client from the environment; with the h3 event (or a request) it reads drafts under the preview cookie
createClient({ key, apiUrl, release, locale, draft, fetch, cache })A client by hand
draftOf(from), draftCookieOf(from), cookieHeaderOf(from), DRAFT_CONTEXT_KEYThe draft session a request carries (off the event's context, else the cookie), or null; the cookie's alone; the Cookie header it is read from; the context key
assetUrl, createAppClient, MapledError, watchReleaseRe-exported from @mapled/client
default from @mapled/nuxt/moduleThe Nuxt module: routes, middleware, useMapled, <MapledLive />; RUNTIME_FILES lists what it mounts
createRevalidateHandler({ secret, tags, storage, storageBases, deployHook, fetch }) from @mapled/nuxt/serverThe publish webhook: purges the cache and Nitro's, calls the deploy hook, answers with x-mapled-cache; secret defaults to MAPLED_WEBHOOK_SECRET of the server
createPreviewHandler({ apiUrl, appUrl }), createExitPreviewHandler()Into preview and out of it
createReleaseHandler({ ttl })The release route open tabs ask
verifySignature, purge, cacheMark, requestOf, boundedText, WEBHOOK_BODY_LIMIT, BodyTooLarge, NITRO_CACHE_BASESFor a webhook handler of your own
onRequest from @mapled/nuxt/middleware; DRAFT_KEY_MARKER, draftCacheNonceA draft request renders the draft through Nitro's page caches and answers private, no-store
startLive from @mapled/nuxt/live; MapledLive.vueOpen tabs follow Publish

Errors are MapledError with the API's status and code. Every handler takes the h3 event or a Request and returns a Response, so it is a Nitro route (h3 1.8+, Nuxt 3.7+) as it is.

Keywords

mapled

FAQs

Package last updated on 07 Oct 2026

Related posts