Home · Install guides · Headless and frameworks

Add VANE it to a headless site

There is no SDK and no npm package, on purpose. In a framework site you do two things: load embed.js once in your root layout, and render an inert placeholder tag wherever a vane should appear. embed.js finds the placeholders on load and after every client-side navigation. It works with SSR and static export. Below: the recipe for each stack, plus how to let CMS editors place vanes without touching code.

The whole thing
<!-- once, in your root layout -->
<script async src="https://vaneit.com/embed.js"></script>

<!-- where each vane goes -->
<script type="text/plain" data-vane="your-vane-id"></script>

Before you start

You need your Vane ID. In your VANE it dashboard, open the vane, find Embed on your site, and click Copy ID under the snippet. Pick a placement there too if you want one: it is the data-place value in the copied line (float-br, float-bl, over-tl, over-tr, over-bl, over-br, over-c or section; leave it off for inline). The vane must be set live.

Next.js

App Router (Pages Router: same idea in _app)

Load embed.js once in the root layout with next/script. Where each vane goes, render an inert placeholder tag.

In your code
  1. In app/layout.tsx, add <Script src="https://vaneit.com/embed.js" /> inside <body>. Once, for the whole site.
  2. Where the vane belongs, render <script type="text/plain" data-vane="your-vane-id" />. Add data-place for a placement other than inline.
  3. That is all. Server components are fine, and new pages reached by client-side navigation render their vanes on their own.

Do not put data-vane on the <Script> itself. next/script appends to the end of the body and loads each address only once, so only one vane would render, at the bottom of the page.

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://vaneit.com/embed.js" />
      </body>
    </html>
  );
}

// any page or component
export default function Post() {
  return (
    <article>
      <p>Twenty years taught us to roast light.</p>
      <script type="text/plain" data-vane="your-vane-id" />
    </article>
  );
}
In the browser

Did it work? Run the dev server and the hex renders right next to the placeholder. Navigate to another route with a vane and it renders there too; navigate away and it is removed.

React (Vite, Create React App, Remix)

a small component

Same pattern: embed.js once in index.html (or your root route), a placeholder wherever a vane goes.

In your code
  1. Add <script async src="https://vaneit.com/embed.js"></script> once to index.html, with no data-vane.
  2. Make a tiny Vane component that renders the placeholder, and use it wherever you like.
  3. Give it key={id} so that switching to a different vane swaps the placeholder rather than editing it in place.

A type="text/plain" script never executes, so React leaves it where it is and does not move it to the head. If you ever need to force a render (you should not), call window.VaneIt?.scan() in a useEffect.

<!-- index.html, once -->
<script async src="https://vaneit.com/embed.js"></script>

// Vane.jsx
export function Vane({ id, place }) {
  return <script key={id} type="text/plain" data-vane={id} data-place={place} />;
}

// anywhere
<Vane id="your-vane-id" />
<Vane id="your-vane-id" place="section" />
In the browser

Did it work? The hex appears next to each placeholder, including in components that mount after a route change or when a modal opens.

Vue and Nuxt

component is="script"

Vue removes script tags from templates, so render the placeholder through a dynamic component instead.

In your code
  1. Load embed.js once: in index.html for Vue, or in app.head.script in nuxt.config for Nuxt.
  2. Where the vane goes, use <component is="script" type="text/plain" …>. Vue renders that as a real, inert script tag.
  3. Bind the id with :data-vane and add :key if the id can change.

A plain <script> in a Vue template is dropped by the compiler with a “Tags with side effect” warning. The component is="script" form avoids that.

<!-- Vane.vue -->
<script setup>
defineProps({ id: String, place: String });
</script>

<template>
  <component is="script" type="text/plain" :key="id" :data-vane="id" :data-place="place" />
</template>
In the browser

Did it work? The hex renders next to the placeholder, and pages reached through Vue Router render their vanes without a reload.

Svelte and SvelteKit

svelte:element this="script"

Svelte reserves script tags for component code, so render the placeholder with svelte:element.

In your code
  1. Load embed.js once: in src/app.html for SvelteKit, or index.html for plain Svelte.
  2. Where the vane goes, use <svelte:element this="script" type="text/plain" data-vane={id} />.
  3. Wrap it in {#key id} if the id can change.

Nothing else is needed for SvelteKit navigation: new placeholders are picked up when a page mounts, and removed vanes go with their page.

<!-- Vane.svelte -->
<script>
  let { id, place = undefined } = $props();
</script>

{#key id}
  <svelte:element this="script" type="text/plain" data-vane={id} data-place={place} />
{/key}
In the browser

Did it work? The hex renders next to the placeholder on first load and after client-side navigation.

Astro

plain snippet, is:inline

Astro pages are HTML, so the normal snippet works. Mark it is:inline so Astro leaves it exactly as written.

In your code
  1. Paste the snippet from your dashboard into the .astro file where the vane belongs, and add is:inline.
  2. Inside a React, Vue or Svelte island, use that framework’s placeholder recipe above instead.
  3. With view transitions (<ClientRouter />), vanes on the new page render after each navigation.

Several snippets on one page are fine: each renders its own vane and the engine loads once.

<p>Twenty years taught us to roast light.</p>
<script is:inline async src="https://vaneit.com/embed.js" data-vane="your-vane-id"></script>
In the browser

Did it work? The hex renders where the snippet sits in the built page.

Sanity, Contentful and friends

a field on the content model

Let editors place vanes without touching code by storing the vane id as a field, then rendering the placeholder in your frontend when the field is set.

In your code
  1. Add a plain text field to the content type, for example vaneId, with a note telling editors where to find it.
  2. Optionally add a placement field with the same values the dashboard offers.
  3. In your frontend, render your framework’s placeholder (above) only when the field has a value.
  4. For Portable Text or a rich text field, register a custom block type so editors can drop a vane mid-article.

Tell editors to paste the Vane ID from the dashboard, not the whole snippet. There is a Copy ID button under the code for exactly this. If they paste the snippet anyway, strip it server-side by pulling the value out of data-vane.

In the browser

Did it work? Set the field on one entry, publish, and the vane appears on that entry only. Leave it blank on another and nothing renders.

SSR, CSP and routing

ConcernAnswer
Server renderingSafe. The placeholder is plain markup and nothing runs during render, so SSR, SSG and static export all work. Nothing to guard with a typeof window check.
HydrationThe placeholder is inert, so the server and client markup match. The vane is added next to it only after the page has loaded and the vane has arrived from our servers.
Client-side routingembed.js watches for placeholders added after load, so components mounted on navigation render their vanes. When a placeholder is removed, its vane goes with it. To force a check yourself, call window.VaneIt?.scan().
Content Security Policyscript-src https://vaneit.com; connect-src https://vzccllblesgmopqapmja.supabase.co; style-src 'unsafe-inline'; font-src data:; img-src https://vaneit.com https://vzccllblesgmopqapmja.supabase.co; frame-src https://vaneit.com (the last only if you use the iframe). No eval.
Bundle sizeZero. Nothing enters your bundle; the loader is fetched from our CDN and cached for four hours.
TypeScriptNothing to import. If your setup complains about window.VaneIt, declare it as { scan(): void }.

Common questions

Is there an npm package?

No, deliberately. The integration is one script in your layout plus a placeholder tag, so a package would only wrap something already small and give you a dependency to keep updated. If a React component would genuinely help your team, tell us what you would want from it.

Why not just put the normal script tag in my component?

Frameworks treat a script tag with src specially. next/script appends it to the end of the body and loads each src only once, so only one vane renders and it lands at the bottom of the page. React 19 moves async scripts into the head and removes duplicates. Vue drops script tags from templates altogether. The placeholder has type="text/plain", which no framework runs, moves or removes, so it stays exactly where you put it.

Does it work with server-side rendering?

Yes. The placeholder is plain markup on the server and nothing runs until the browser loads embed.js. There is no window access during render, so it will not break hydration or a static export.

Does it survive client-side route changes?

Yes. embed.js watches the page for new placeholders, so a vane inside a component that mounts after navigation renders on its own. When a placeholder leaves the page, its vane (including a floating one) is removed with it. If you hit a case where it does not, tell us the framework and router and we will look.

Where do I put the id in a headless CMS setup?

Two patterns. Either hardcode it in the component if the same question runs across a template, or store the id as a plain text field on the content model so editors can choose per entry. The second is worth it only when each entry genuinely needs its own tally.

Content Security Policy?

script-src https://vaneit.com; connect-src https://vzccllblesgmopqapmja.supabase.co; style-src 'unsafe-inline'; font-src data:; img-src https://vaneit.com https://vzccllblesgmopqapmja.supabase.co; frame-src https://vaneit.com (the last only if you use the iframe). No eval.

Can I self-host embed.js?

We would rather you did not. Pinning a copy means you stop getting format additions and fixes, and the loader is versioned against the API. If you have a policy reason you cannot load third-party scripts, get in touch and we will talk through the options honestly rather than pretend there is a clean one.

One script, any stack

No SDK to install, nothing in your bundle, nothing to keep updated.

Start free Plain HTML