In a headless CMS, editors write the content and your code decides how it looks. So the setup is split in two. A developer adds a VANE it poll block once, which takes about fifteen minutes. After that, editors add a vane the way they add an image: insert the block, paste a vane ID, publish.
- Load
embed.jsin the site layout. - Add a VANE it poll block type with a Vane ID field.
- Render that block with the small
Vanecomponent.
- Copy the vane's ID from the VANE it dashboard.
- Insert a VANE it poll block and paste the ID.
- Publish.
Step 1: load the script once
Add this to the layout every page uses. It's under 1 KB, loads after your page, and finds every vane on the page, including ones added later by client-side navigation.
<!-- once, in the layout every page uses, just before </body> --> <script async src="https://vaneit.com/embed.js"></script>
Then add one small component. It outputs an inert placeholder that the script turns into the vane. Every recipe below uses it.
// components/Vane.jsx: every recipe below uses this.
// (Vue and Svelte versions are in the headless guide.)
export function Vane({ id, place }) {
if (!id) return null;
return (
<script key={id} type="text/plain" data-vane={id}
data-place={place && place !== 'inline' ? place : undefined} />
);
} Why a placeholder and not the normal embed code? Rich text from a CMS is usually inserted as HTML or rendered by a framework, and browsers never run a script added that way. The placeholder doesn't need to run. The script already on the page finds it.
Sanity
Custom object in Portable TextPortable Text doesn't allow raw HTML, so a pasted script would show up as text. Add a vanePoll object type instead. Editors get a VANE it poll option in the insert menu of the body field, with a Vane ID box and a choice of how it shows.
- Create
schemaTypes/vanePoll.tswith the code below and register it inschemaTypes/index.ts. - Add
defineArrayMember({type: 'vanePoll'})to theoflist of your body field. - In your front end, add a
vanePollentry to thetypesin yourPortableTextcomponents. - Deploy the Studio (
npx sanity deploy, or however you host it) and the site.
import {defineType, defineField} from 'sanity'
export const vanePoll = defineType({
name: 'vanePoll',
title: 'VANE it poll',
type: 'object',
fields: [
defineField({
name: 'vaneId',
title: 'Vane ID',
type: 'string',
description: 'In VANE it: open the vane, then Embed on your site, then Copy ID.',
validation: (rule) => rule.required(),
}),
defineField({
name: 'place',
title: 'How it shows',
type: 'string',
initialValue: 'inline',
options: {
layout: 'radio',
list: [
{title: 'A button in the text', value: 'inline'},
{title: 'An open card', value: 'section'},
{title: 'Floating in the corner', value: 'float-br'},
],
},
}),
],
preview: {
select: {subtitle: 'vaneId'},
prepare: ({subtitle}) => ({title: 'VANE it poll', subtitle}),
},
}) schemaTypes/post.ts (your existing body field) defineField({
name: 'body',
type: 'array',
of: [
defineArrayMember({type: 'block'}),
defineArrayMember({type: 'image'}),
defineArrayMember({type: 'vanePoll'}), // add this line
],
})
// and register the type in schemaTypes/index.ts
export const schemaTypes = [post, vanePoll /* , ... */] wherever you render the body import {PortableText} from '@portabletext/react'
import {Vane} from '@/components/Vane'
<PortableText
value={post.body}
components={{
types: {
vanePoll: ({value}) => <Vane id={value.vaneId} place={value.place} />,
},
}}
/> The block's data comes back with the rest of the body in your existing GROQ query. There's no reference to expand.
Did it work? In Studio, the body field's insert menu shows VANE it poll. After publishing, the vane appears at that point in the post.
Contentful
Embedded entry in Rich TextRich Text has no raw HTML node. Make a small content type and allow it as an embedded entry.
- In Content model, add a content type named VANE it poll with the ID
vanePoll, and a Short text fieldvaneId(plusplaceif you want the placement choice). - Open your Rich Text field's settings and allow Embedded entries, limited to VANE it poll.
- Render
BLOCKS.EMBEDDED_ENTRYwith the code below.
import {documentToReactComponents} from '@contentful/rich-text-react-renderer'
import {BLOCKS} from '@contentful/rich-text-types'
import {Vane} from '@/components/Vane'
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node) => {
const entry = node.data.target
if (entry?.sys?.contentType?.sys?.id !== 'vanePoll') return null
return <Vane id={entry.fields.vaneId} place={entry.fields.place} />
},
},
}
documentToReactComponents(post.fields.body, options) With the REST API the embedded entry comes back resolved in node.data.target. With GraphQL, fetch it through links { entries { block } } and look it up by ID.
Did it work? Editors see Embed entry in the Rich Text toolbar and can pick or create a VANE it poll. The vane shows at that point on the page.
Storyblok
Nestable blockMake a nestable block that editors can drop into any Blocks field, or into Richtext.
- In Block library, create a nestable block
vane_pollwith a Text fieldvane_id. - Allow it in the Blocks field where editors should use it.
- Register the component below with
storyblokInit.
import {storyblokEditable} from '@storyblok/react'
import {Vane} from '@/components/Vane'
export default function VanePoll({blok}) {
return (
<div {...storyblokEditable(blok)}>
<Vane id={blok.vane_id} place={blok.place} />
</div>
)
}
// where you call storyblokInit
storyblokInit({
accessToken: process.env.STORYBLOK_TOKEN,
use: [apiPlugin],
components: {vane_poll: VanePoll /* , ... */},
}) Blocks inside Richtext render through the SDK's rich text renderer using the same registered component.
Did it work? In the Visual Editor, VANE it poll is in the add-block menu. The vane shows in the live preview once the vane is live.
Strapi
Component in a dynamic zoneCreate a component and add it to the dynamic zone your pages already use.
- In Content-Type Builder, create a component VANE it poll in an
embedscategory, with a Text fieldvaneId. Or add the JSON below. - Add it to your content type's dynamic zone.
- Make sure your API call populates the dynamic zone, then render it as below.
{
"collectionName": "components_embeds_vane_polls",
"info": { "displayName": "VANE it poll" },
"attributes": {
"vaneId": { "type": "string", "required": true }
}
}
// in your content type's schema.json
"body": { "type": "dynamiczone", "components": ["embeds.vane-poll"] } rendering the dynamic zone {page.body?.map((block) =>
block.__component === 'embeds.vane-poll'
? <Vane key={block.id} id={block.vaneId} />
: null /* your other components */
)} Did it work? Editors can add VANE it poll to the dynamic zone. The vane shows at that position on the page.
Payload
Block in a blocks fieldAdd a block to your layout's blocks field. It also works inside the Lexical rich text editor through BlocksFeature.
- Create
blocks/VanePoll.tsbelow and add it to yourblocksfield. - Render it where you map your other blocks.
- For rich text, add it to
BlocksFeature({ blocks: [VanePoll] })and giveRichTextavanePollconverter that returns theVanecomponent.
import type {Block} from 'payload'
export const VanePoll: Block = {
slug: 'vanePoll',
labels: {singular: 'VANE it poll', plural: 'VANE it polls'},
fields: [{name: 'vaneId', label: 'Vane ID', type: 'text', required: true}],
}
// in your collection
{name: 'layout', type: 'blocks', blocks: [VanePoll /* , ... */]} rendering the blocks {page.layout?.map((block) =>
block.blockType === 'vanePoll'
? <Vane key={block.id} id={block.vaneId} />
: null /* your other blocks */
)} Did it work? Editors see VANE it poll in the block chooser. The vane shows at that position on the page.
Prismic
SliceRich Text has no raw HTML, and the Embed field only takes oEmbed links. Use a slice.
- In Slice Machine, create a slice VanePoll with a Key Text field
vane_id. - Replace its generated component with the code below.
- Push the slice and add it to your page type.
import type {SliceComponentProps} from '@prismicio/react'
import {Vane} from '@/components/Vane'
export default function VanePoll({slice}: SliceComponentProps<any>) {
return (
<section>
<Vane id={slice.primary.vane_id} />
</section>
)
} Did it work? Editors can add the VanePoll slice to a page. The vane shows where the slice sits.
DatoCMS
Block in Structured Text or Modular ContentCreate a block model and allow it in your Structured Text or Modular Content field.
- Create a block model VANE it poll (API key
vane_poll) with a single-line stringvane_id. - Allow it in your Structured Text field.
- Add the block to your GraphQL query and render it with
renderBlock.
import {StructuredText} from 'react-datocms'
import {Vane} from '@/components/Vane'
<StructuredText
data={post.content}
renderBlock={({record}) =>
record.__typename === 'VanePollRecord' ? <Vane id={record.vaneId} /> : null
}
/>
// and in your GraphQL query
content { value blocks { __typename ... on VanePollRecord { id vaneId } } } In Structured Text, blocks can only sit between paragraphs, not inside one. That's fine for a vane.
Did it work? Editors can insert VANE it poll between paragraphs. The vane shows at that point.
Hygraph
Embed in Rich TextMake a VanePoll model with a vaneId field and allow it as a block embed in your Rich Text field.
- Create the VanePoll model with a single-line text field
vaneId. - In your Rich Text field, enable embeds and allow VanePoll.
- Query the embedded references and render them with a custom renderer that returns the
Vanecomponent.
Did it work? Editors can embed a VanePoll in the text. The vane shows at that point.
Builder.io
Custom Code blockBuilder.io has a Custom Code block that takes HTML, including scripts, so editors can paste the normal embed code.
- In the visual editor, insert a Custom Code block where the vane should go.
- Paste the embed code from the VANE it dashboard.
- Publish.
For a tidier editor experience, register a custom component with a Vane ID input that renders the Vane component.
Did it work? The vane shows in the preview and on the published page.
Ghost
HTML cardGhost is the easy one. Its HTML card is published exactly as written, so the normal embed code works with no developer needed.
- In the post editor, type
/htmland choose HTML. - Paste the embed code from the VANE it dashboard.
- Update or publish the post.
Email clients don't run scripts, so the vane won't appear in the newsletter version of a post. It shows on the web version. Add a line such as "Vote on the website" linked to the post.
Did it work? Open the post on your site. The vane shows where you put the card.
Markdown CMSs
Raw HTML in MarkdownDecap, Tina, Keystatic, Pages CMS and others save Markdown files, and most site generators pass raw HTML in Markdown straight through. Paste the embed code on its own line.
- Switch the editor to raw or source mode if it has one, and paste the embed code on its own line, with a blank line above and below.
- Publish, and check the built page.
- If the tag shows as text or disappears, your generator is stripping HTML. Hugo does this by default; the setting is below.
Twenty years taught us to roast light. <script async src="https://vaneit.com/embed.js" data-vane="your-vane-id"></script> Come and argue about it at the counter.Hugo only, in hugo.toml
[markup.goldmark.renderer] unsafe = true # Hugo drops raw HTML in Markdown without this
Some rich text editors remove tags they don't recognise. If yours does, add a custom component instead: Tina rich-text templates and Keystatic content components can both output the Vane placeholder.
Did it work? The vane shows on the built page where the line sits.
For editors: adding a vane
Once your developer has set up the block, this is all there is to it.
- In VANE it, build or open the vane and make sure it's set live.
- Under Embed on your site, click Copy ID.
- In your CMS, insert a VANE it poll block where you want it and paste the ID. Pick how it shows if there's a choice.
- Publish. To change the question later, edit it in VANE it. You don't need to touch the CMS again.
Did it work? The vane shows on the page where the block sits. If it doesn't, check the vane is live and the ID has no spaces.
Common questions
Why not just paste the embed code into a rich text field?
Most headless CMSs don't store raw HTML in rich text, and the ones that do usually get inserted in a way browsers don't run. A block with a Vane ID field is safer, easier for editors, and can't break the page. Ghost and most Markdown setups are the exception, and there pasting works.
Does it work with static builds and server rendering?
Yes. The placeholder is plain markup and nothing runs while the page is built, so SSR, SSG and static export all work. The vane loads in the browser after the page.
Will editors see the vane in the CMS preview?
In live previews that render your real site (Sanity Presentation, Storyblok Visual Editor, Builder.io), yes, as long as the vane is live. Plain form views show only the ID.
Can one page have several vanes?
Yes. Add as many blocks as you like. The script loads once and renders each one.
What if the CMS or site sets a Content Security Policy?
Allow https://vaneit.com in script-src and img-src, https://vzccllblesgmopqapmja.supabase.co in connect-src and img-src, 'unsafe-inline' in style-src, and data: in font-src.
Is there an npm package or official plugin?
No. There's nothing to install and nothing to keep updated. One script, one small component, and the block type in your schema.
Set it up once. Editors take it from there.
Fifteen minutes for a developer, then every editor can put a vane on any page.
Start free React, Vue and Svelte recipes