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

@mapled/vanilla

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mapled/vanilla

The browser runtime of Mapled, the hosted headless CMS for sites built with AI — published content in a plain HTML site: one script tag and data-mapled attributes, no framework and no build step.

npmnpm
Version
0.1.0
Version published
Weekly downloads
167
595.83%
Maintainers
1
Weekly downloads
 
Created
Source

@mapled/vanilla

Mapled — the hosted headless CMS for sites built with AI — in a plain HTML site: one script tag, data-mapled-* attributes on the elements it should fill, no framework and no build step. The HTML you already have stays in the page as the fallback; editors change the words, pictures and lists in Mapled and press Publish.

  • A React single-page app uses @mapled/react, a Next.js site @mapled/next, any other JavaScript @mapled/client — this package is for pages that have no JavaScript of their own to speak of.
  • What search engines and link previews read is the page's own HTML, not what a script fills in later. Keep the fallback meaningful; a site that lives on search traffic is better served by a framework that renders on a server.

Add it to a page

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <title data-mapled-bind="home.headline">Studio Nord</title>
    <script src="https://cdn.jsdelivr.net/npm/@mapled/vanilla@0.1.0/dist/mapled.js" data-mapled-key="mk_live_…" defer></script>
  </head>
  <body>
    <h1 data-mapled-bind="home.headline">Websites, built by hand</h1>
  </body>
</html>

home.headline is the field headline of the single home. When the page loads, the script reads the release that is live and puts the headline into both elements; until then — and whenever Mapled can't be reached — the page shows what you wrote.

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 belongs in a page. The script refuses to start with anything else: a server key or a signing secret never goes into HTML.

Pin the version. A version on a CDN never changes; an address without one (…/@mapled/vanilla/dist/mapled.js, @latest) runs whatever is published next. Better still, add integrity="sha384-…" crossorigin="anonymous" — the value ships in the package as dist/mapled.js.sha384 — or copy dist/mapled.js next to your pages and load it from there. The file is about 12 KB over the wire and has no dependencies.

What a page can declare

<!-- landing.html -->
<header>
  <h1 data-mapled-bind="home.headline">Websites, built by hand</h1>

  <!-- rich text: Markdown from Mapled, built into elements -->
  <div data-mapled-bind="home.intro" data-mapled-format="markdown"><p>We make small sites.</p></div>

  <!-- a picture (an asset of the project) at the width the layout needs, and its description -->
  <img data-mapled-bind="home.hero" data-mapled-width="1280" data-mapled-bind-alt="home.hero-alt" src="/img/hero.jpg" alt="The studio">

  <!-- an attribute: href, src, alt, title, aria-label, placeholder, datetime -->
  <a data-mapled-bind="home.cta-label" data-mapled-bind-href="home.cta-link" href="/contact.html">Get in touch</a>

  <!-- dates and numbers in the words of the page's language -->
  <p>Since <time data-mapled-bind="home.founded" data-mapled-format="date" data-mapled-bind-datetime="home.founded">2019</time></p>

  <!-- shown only while the field has a value (data-mapled-hide is the opposite) -->
  <p data-mapled-show="home.notice" data-mapled-bind="home.notice" hidden></p>
</header>

<!-- a repeatable group of the single: the template once per item -->
<ul data-mapled-each="home.features">
  <template>
    <li><strong data-mapled-bind="title"></strong> <span data-mapled-bind="text"></span></li>
  </template>
  <li><strong>Designed</strong> by people</li>
</ul>

<!-- a collection: the template once per record -->
<ul data-mapled-list="posts" data-mapled-sort="-date" data-mapled-limit="5" data-mapled-filter="featured=true">
  <template>
    <li>
      <a data-mapled-link="/post.html?slug={slug}" data-mapled-bind="title"></a>
      <time data-mapled-bind="date" data-mapled-format="date"></time>
    </li>
  </template>
  <li>Journal entries appear here.</li>
  <li data-mapled-empty hidden>Nothing published yet.</li>
</ul>
  • Paths. From the document a path starts with a single's key: home.headline, home.seo.title for a field inside a group. Inside data-mapled-list, data-mapled-each and data-mapled-record it starts at the record: title, author.name for a linked record — name the relation in data-mapled-expand="author" on the list or the record element.
  • data-mapled-bind fills the element with text — on an <img>, with the picture. data-mapled-format says how: markdown for a rich text field, date, datetime and number for those, written the way the nearest lang attribute says.
  • data-mapled-bind-… fills an attribute: href (on a, area), src (img, video, audio, source), alt, title, aria-label, placeholder, datetime. Nothing else — no style, no class, no on….
  • data-mapled-list reads a collection: data-mapled-sort="-date", data-mapled-limit (1–100), data-mapled-filter="featured=true; category=news". Whatever else sits inside the element is its fallback and leaves when records arrive; an element with data-mapled-empty shows when there are none.
  • data-mapled-link builds an address from a template. Every {field} is percent-encoded before it goes in, so a value fills its place and nothing more.

One record per page

<!-- post.html -->
<article data-mapled-record="posts" data-mapled-slug-param="slug">
  <h1 data-mapled-bind="title">Journal</h1>
  <div data-mapled-bind="body" data-mapled-format="markdown"></div>
</article>

post.html?slug=hello-world shows the post whose slug field is hello-world. data-mapled-slug="about-us" names the record in the page instead. Tell Mapled the page's address as /post.html?slug=[slug] in the site manifest (with a route_param binding on the slug field), and Preview in the record's editor opens the right page.

Forms

<!-- contact.html -->
<form data-mapled-form="contact">
  <input name="name" required>
  <input name="email" type="email" required>
  <input name="_gotcha" hidden tabindex="-1" autocomplete="off">
  <label><input type="checkbox" name="consent" data-mapled-consent required> I agree to be contacted about my request.</label>
  <button>Send</button>
  <p data-mapled-form-sent hidden>Thanks — we'll write back.</p>
  <p data-mapled-form-failed hidden></p>
</form>

contact is the form's key in Mapled (Forms → New form, npx @mapled/cli forms create, or your AI agent's create_form). The script posts the fields, shows one of the two messages — an empty data-mapled-form-failed gets Mapled's own words — and sets data-mapled-form-state on the form to sending, sent or failed. Keep the hidden _gotcha input: people leave it empty, and what bots put there is filed as spam. The label of a ticked data-mapled-consent checkbox is kept with the submission. Submissions never wait for Publish.

What it does when a value isn't there

The report saysWhat happenedWhat the page shows
missingNo such single, record or collection in the release — or nothing in the address to find a record byThe HTML you wrote
emptyThe record is there, a value at this path isn't: an editor left the field empty — or the key is misspelledNothing: the text is emptied, a picture is hidden, an attribute is removed (alt becomes "")
failedThe read didn't come backThe HTML you wrote
unsafe-urlAn address that isn't http:, https: — or mailto:, tel: in a link — or that carries credentialsThe HTML you wrote
typeA value that has no text: a group bound as text, a word where a date goesThe HTML you wrote
refused-element<script>, <style>, <iframe>, <object>, <embed>, <base>, <link>, <meta>, <template>, <noscript> are never written intoThe HTML you wrote
unsupportedAn attribute Mapled doesn't read — data-mapled-bind-style, a misspelled name — or a list without a <template>The HTML you wrote
bad-pathA path, a key or an option that isn't oneThe HTML you wrote
<!-- debug.html -->
<script>
  document.addEventListener("mapled:ready", (event) => {
    const { state, release, preview, bound, issues } = event.detail;
    console.table(issues); // path, reason, on — never a value
  });
</script>

state is ready, unpublished (nothing is published yet) or fallback (Mapled couldn't be reached); the same word is on <html data-mapled-state="…">, loading before the first fill — a hook for CSS, set by the script only, so a page whose script never loaded hides nothing. mapled:update follows every later fill. In the console, Mapled.page.report is the last report and Mapled.page.refresh() fills again — call it after your own script adds elements with attributes. Every fill starts from the HTML you wrote, so a value that stops fitting gives its place back to your fallback, not to the previous release.

Preview

Nothing to set up in the page. Preview in Mapled opens the page with a one-time token — https://your-site/post.html?slug=hello&mapled-preview=mpv_… — and the script, 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. The page shows drafts until the tab closes or the session ends, and <html> carries data-mapled-preview meanwhile:

<!-- preview-bar.html -->
<p class="preview-bar">You are looking at a preview. <button data-mapled-end-preview>Show what is published</button></p>
<style>
  html:not([data-mapled-preview]) .preview-bar { display: none; }
</style>

Mapled has to know where the site lives and that it renders in the browser: the site manifest's framework is "plain-html" (your AI agent's push_site_manifest, or npx @mapled/cli bindings push) — or the project was created as plain HTML — and Production domain in Project settings holds the site's public address (an agent sets it with set_site_url). npx @mapled/cli doctor checks all of it, the script tag's version included.

Live updates

A publish shows when the page is loaded again. To follow publishes while a page is open, add data-mapled-live to the script tag — data-mapled-live="30" for the seconds between checks (60 by default, 15 at least). A check is a request of the delivery key's budget (1,200 a minute), made only while the tab is visible; a quiet one costs next to nothing.

What content can and can't do to the page

An editor changes what the site says, never what it runs:

  • text goes in as text — markup typed into a field is shown, not parsed;
  • rich text is Markdown built into a fixed set of elements (paragraphs, headings, lists, quotes, code, emphasis, links, images); raw HTML in it is shown as text;
  • addresses pass one policy — http: and https:, plus mailto: and tel: in links — read by the browser's own URL parser, so java script: in any disguise is refused;
  • only the listed attributes are ever set, and never on <script>, <style>, <iframe> and the like;
  • the script uses no eval and no HTML-parsing call, and works under a strict Content-Security-Policy and Trusted Types. A site with a CSP adds connect-src https://api.mapled.io and img-src https://api.mapled.io https://media.mapled.io.

Don't undo this with your own innerHTML = record.data.… — if you need content in JavaScript, read it with @mapled/client and put it in as text.

With a bundler

// src/main.ts
import { mount } from "@mapled/vanilla";

const page = mount({ key: "mk_live_…", live: true });
const report = await page.ready;
if (report.state === "fallback") console.info("Showing the page as written.");

mount({ key, apiUrl?, root?, live?, fetch? }) answers { ready, report, refresh(), endPreview(), stop(), client } — client is the @mapled/client the page reads through, for reads of your own. The script tag does the same from its attributes (data-mapled-key, data-mapled-api, data-mapled-live) and leaves the result at Mapled.page; a tag without a key only defines Mapled.mount. VERSION is the package's version, Mapled.version in a page.

Every attribute

AttributeOnSays
data-mapled-key, data-mapled-api, data-mapled-livethe script tagthe delivery key; another API origin; follow publishes
data-mapled-bindan elementthe path whose value fills it
data-mapled-formatwith data-mapled-bindtext (default), markdown, date, datetime, number
data-mapled-widthan <img>the width of the picture to ask for, 16–4096
data-mapled-bind-href, -src, -alt, -title, -aria-label, -placeholder, -datetimean elementthe path whose value fills the attribute
data-mapled-show, data-mapled-hidean elementshown (hidden) while the path has a value; false counts as none
data-mapled-linka, areaan address template with {field} places
data-mapled-listan element with a <template>the collection to repeat the template for
data-mapled-sort, data-mapled-limit, data-mapled-filterwith data-mapled-listthe order, how many, which
data-mapled-eachan element with a <template>the path of a group to repeat the template for
data-mapled-emptyinside a list or an eachshown when there is nothing to repeat
data-mapled-recordan elementthe collection of the one record its content shows
data-mapled-slug, data-mapled-slug-paramwith data-mapled-recordthe record's slug; the query parameter that holds it
data-mapled-expanda list, a record, a bound elementrelation fields to read along
data-mapled-forma <form>the key of the Mapled form it posts to
data-mapled-consent, data-mapled-form-sent, data-mapled-form-failed, data-mapled-end-previewinside a form; anywherethe consent checkbox; the two outcomes; a way out of preview
data-mapled-state, data-mapled-preview, data-mapled-form-stateset by the script<html>: how the fill went, whether drafts are shown; a form: sending, sent, failed

Keywords

mapled

FAQs

Package last updated on 19 Sep 2026

Related posts