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/astro

v0.1.1

Published

Read published Mapled content in an Astro site — a server-side client with a tag cache, the publish webhook (purge or deploy hook), preview and live updates.

Readme

@mapled/astro

Read published Mapled content in an Astro site — a server-side client with a tag cache, a publish webhook that purges it (or rebuilds a static site), preview of drafts, 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 an Astro site reads what's published. It is @mapled/client with Astro's idioms around it: the same reads, the same types from mapled types generate.

Install

npm install @mapled/astro

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. The adapter reads it on the server, so no PUBLIC_ prefix is needed (the delivery key is the one key that may be public; the webhook secret and a server key never are):

MAPLED_KEY=mk_live_…
MAPLED_WEBHOOK_SECRET=whsec_…

Two places hold a site's env, and the adapter looks in both. A .env file (and astro dev) fills the site's own import.meta.env, when the site is built — the snippets below hand those values over. A host's environment variables are the server's process.env, which the adapter reads itself when a request arrives — so a site built without a value still finds it where it runs.

Pages that read Mapled render on demand: output: "server" in astro.config.mjs (with an adapter), or export const prerender = false on those pages. A static build works too — the webhook then asks your host to rebuild (Refresh on publish).

Read published content

One client for the whole site, typed by the schema npx @mapled/cli types generate writes:

// src/lib/mapled.ts
import { mapled } from "@mapled/astro";
import type { MapledSchema } from "../mapled-types";

/** Pass `Astro` (or an endpoint's context) so the request's preview cookie is seen; without it, published content only. */
export const cms = (from?: Request | { request: Request }) => mapled<MapledSchema>(from, { key: import.meta.env.MAPLED_KEY });

mapled() takes the key it is handed — the one of a .env file, named as import.meta.env.MAPLED_KEY so the build keeps it — else MAPLED_KEY of the server's environment (apiUrl and MAPLED_API_URL likewise). In a page's frontmatter:

---
// src/pages/index.astro
import { cms } from "../lib/mapled";
const home = await cms(Astro).getSingle("homepage");
const { records: posts } = await cms(Astro).getRecords("posts", { sort: "-date", limit: 10 });
---
<h1>{home?.data.headline}</h1>
<ul>{posts.map((p) => <li><a href={`/blog/${p.data.slug}`}>{p.data.title}</a></li>)}</ul>
---
// src/pages/blog/[slug].astro
export const prerender = false;
import { cms } from "../../lib/mapled";
const post = await cms(Astro).getRecordBySlug("posts", Astro.params.slug!);
if (!post) return Astro.redirect("/404");
---
<article><h1>{post.data.title}</h1></article>

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:

// src/lib/reads.ts
import { cms } from "./mapled";

export async function homepage() {
  // 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. A prerendered page reads published content: mapled(Astro) leaves its request unread (Astro 5+ marks it isPrerendered), so a static build stays quiet. The cache is this process's: a long-running server (the Node adapter) holds one, serverless functions hold one each — keep revalidate short there, or build statically. createClient({ key, apiUrl, draft, cache: false }) makes a client by hand, 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.

// src/lib/images.ts
import { assetUrl } from "@mapled/astro";

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

Refresh on publish

Mount the webhook and point the project at it (Integrations → Your site → Revalidation webhook, or let your agent call configure_revalidation). Every publish is signed with the project's secret; the handler purges the cache, so the next render reads the new release:

// src/pages/api/mapled/revalidate.ts
import { createRevalidateHandler } from "@mapled/astro/server";

export const prerender = false;
export const POST = createRevalidateHandler({ secret: import.meta.env.MAPLED_WEBHOOK_SECRET });

import.meta.env holds the secret of a .env file; on a site built without it the handler reads MAPLED_WEBHOOK_SECRET from the server's environment when the first webhook arrives. Until either has it, it answers 503 and Mapled retries.

A statically built 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 asks it for a build. The route itself still runs on a server — output: "static" with an adapter prerenders the pages and keeps the routes marked prerender = false as functions; a host with no functions at all has nothing to take the webhook. The handler calls the hook 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. A handler of your own can verify a request with verifySignature(secret, body, header), drop what it needs with purge(["mapled:posts"]), and answer with the header x-mapled-cache: cacheMark() so live updates follow it.

Preview drafts

Mapled opens the site through a one-time link to the preview route, 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 mapled(Astro) reads drafts for that visitor, uncached, until the exit route clears the cookie — published content for everyone else.

// src/pages/api/mapled/preview.ts
import { createPreviewHandler } from "@mapled/astro/server";

export const prerender = false;
export const GET = createPreviewHandler();

(It trades the link at MAPLED_API_URL when the site's env has one — the API the reads go to.)

// src/pages/api/mapled/preview/exit.ts
import { createExitPreviewHandler } from "@mapled/astro/server";

export const prerender = false;
export const GET = createExitPreviewHandler();

A draft page must never be cached by your host or a CDN. The middleware marks it private, no-store while the cookie is there:

// src/middleware.ts
export { onRequest } from "@mapled/astro/middleware";

(Next to middleware of your own: sequence(onRequest, yours).) draftOf(request) answers the draft session of a request, or null — what mapled(Astro) uses; pass it as draft to createClient when you build the client yourself. Preview links point at /api/mapled/preview by default; both routes redirect only to a path of the site itself.

Live updates

Open tabs can follow Publish. Mount the release route and render the component once, in the layout — it asks the route every few seconds and reloads the page when a publish lands:

// src/pages/api/mapled/release.ts
import { createReleaseHandler } from "@mapled/astro/server";

export const prerender = false;
export const GET = createReleaseHandler();
---
// src/layouts/Layout.astro
import MapledLive from "@mapled/astro/MapledLive.astro";
---
<html><body><slot /><MapledLive /></body></html>

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 (a static build) passes the delivery key instead, <MapledLive key={import.meta.env.PUBLIC_MAPLED_KEY} /> — the one key that is safe in a page; a PUBLIC_ variable for it, never for a secret — and the tab asks Mapled directly, once a minute. From a script of your own: startLive({ endpoint }) (or { key }) from @mapled/astro/live reloads on a publish and returns a stop function; mountLive() is what the component's script calls; watchRelease is the client's own, for anything else.

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.

// src/pages/api/orders.ts
import { createAppClient } from "@mapled/astro";

export const prerender = false;
// the .env file's value, else the server's own environment — a build made without the key still finds it where it runs
const db = createAppClient({ serverKey: import.meta.env.MAPLED_SERVER_KEY ?? process.env.MAPLED_SERVER_KEY! });
export async function POST({ request }: { request: Request }) {
  const body = (await request.json()) as { email: string };
  const order = await db.create("orders", { email: body.email });
  return Response.json({ id: order.id }, { status: 201 });
}

API

| Import | What it is | | --- | --- | | mapled(from?, { key, apiUrl }?) from @mapled/astro | The site's client from the environment; with Astro, a request or an endpoint's context it reads drafts under the preview cookie | | createClient({ key, apiUrl, release, locale, draft, fetch, cache }) | A client by hand | | draftOf(from) | The draft session a request carries, or null | | assetUrl, createAppClient, MapledError, watchRelease | Re-exported from @mapled/client | | createRevalidateHandler({ secret, tags, deployHook, fetch }) from @mapled/astro/server | The publish webhook: purges the cache, calls the deploy hook, answers with x-mapled-cache; without a secret, 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 | For a webhook handler of your own | | onRequest from @mapled/astro/middleware | A draft page answers private, no-store | | startLive, mountLive from @mapled/astro/live; MapledLive.astro | Open tabs follow Publish |

Errors are MapledError with the API's status and code. Every handler takes a Request or Astro's context ({ request }) and returns a Response, so it is an Astro endpoint as it is.