npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mapled/nuxt

v0.1.1

Published

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.

Readme

@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

| Import | What 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/nuxt | The 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_KEY | The 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, watchRelease | Re-exported from @mapled/client | | default from @mapled/nuxt/module | The Nuxt module: routes, middleware, useMapled, <MapledLive />; RUNTIME_FILES lists what it mounts | | createRevalidateHandler({ secret, tags, storage, storageBases, deployHook, fetch }) from @mapled/nuxt/server | The 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_BASES | For a webhook handler of your own | | onRequest from @mapled/nuxt/middleware; DRAFT_KEY_MARKER, draftCacheNonce | A draft request renders the draft through Nitro's page caches and answers private, no-store | | startLive from @mapled/nuxt/live; MapledLive.vue | Open 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.