Headless CMS installation
Testimonial Widgets in a Headless CMS
A headless CMS hands your front end data, not finished pages, so pasting embed code into a rich text field is the wrong move. Model the widget as content instead: editors pick where it goes and type an id, and the code that turns that id into a widget stays in your repository.
- A schema object editors can drop into a page
- A renderer that turns the stored id into a div
- One script tag in the shared layout
Code generator
Model, render, load: the three pieces
Enter your widget id to see it in the sample output. The schema is written for Sanity Studio; for another CMS, create the same two fields in its content model and keep the renderer and layout pieces as they are.
Part 1 · Content model: a widget object editors can insert
schemaTypes/testimonialWidget.ts
// schemaTypes/testimonialWidget.ts (Sanity Studio)
import { defineField, defineType } from 'sanity';
export const testimonialWidget = defineType({
name: 'testimonialWidget',
title: 'Testimonial widget',
type: 'object',
fields: [
defineField({
name: 'widgetId',
title: 'StoryProof widget id',
type: 'string',
description: 'Paste only the id, never the embed code.',
validation: (Rule) => Rule.required(),
}),
defineField({ name: 'label', title: 'Note for editors', type: 'string' }),
],
});
// In a page or post body, allow it beside ordinary rich text:
// of: [{ type: 'block' }, { type: 'testimonialWidget' }]Part 2 · Front end: render the stored id as a div
your rich text or block renderer
// The front end turns the stored id into markup.
// Framework neutral: adapt to your Portable Text or rich text renderer.
function renderTestimonialWidget(node) {
const id = String(node.widgetId || '').trim();
if (!id) return '';
// Only the id comes from the CMS. The markup lives in code.
const safe = id.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
return '<div data-storyproof-widget="' + safe + '"></div>';
}
// Example output for one stored entry:
// <div data-storyproof-widget="YOUR_WIDGET_ID"></div>Part 3 · Front end layout: load the script once for every page
shared layout, just before the closing body tag
<script src="https://app.storyproof.io/embed.js" async></script>Insert a widget object into a draft page in the CMS, publish it, rebuild or revalidate the front end if your site is static, and load the page in a browser. One widget div should appear where the editor placed the object.
The widget id you type stays in your browser; nothing is sent.
The problem, solved
Why embed code and a headless CMS do not mix, and the content model fix
- Problem 1
Pasted embed code shows up as text
Sanity, for one, stores rich text as an array of JSON objects, and your own renderer decides what markup comes out. A pasted script is just a string to it, so nothing runs.
How StoryProof solves it
The StoryProof embed is only a div carrying an id plus one script, so the CMS needs to hold nothing but the id. Your renderer builds the div from it.
- Problem 2
I do not want editors pasting scripts
Letting a CMS field output raw HTML opens every editor account to script injection on the live site.
How StoryProof solves it
Editors type an id, not code. The script tag lives once in your layout, and that single copy fills every StoryProof widget div on the page.
- Problem 3
Marketing wants different widgets per page
A widget hard coded into a template means a developer ticket every time a landing page needs a different set of quotes.
How StoryProof solves it
Create one StoryProof widget per purpose with its own layout and chosen testimonials. Editors reference whichever id fits the page.
- Problem 4
Who checks the quotes before they ship?
If testimonials were typed into the CMS as text, anyone with publish rights could add an unchecked quote.
How StoryProof solves it
Testimonials are approved in StoryProof before they can appear in any widget, so the CMS never stores the quotes themselves.
In StoryProof
Wire the CMS to StoryProof
Step 1 of 5
Add the widget type to the content model
Create an object or content type with a required text field for the widget id and an optional note for editors. In Sanity, add it to the of list of the body field beside block so it can sit between paragraphs.
Step 2 of 5
Teach the renderer the new type
Wherever your front end maps rich text nodes to markup, add a case for the widget type that outputs an empty div with data-storyproof-widget set to the stored id. Leave the div empty; the script fills it.
Step 3 of 5
Load embed.js once in the layout
Add the async script to the layout every page shares, not to the renderer. One copy serves every widget div on the page.
Step 4 of 5
Hand editors the widget ids
Name each StoryProof widget for its job and list the ids in the CMS field description or an internal doc, so editors pick the right one.
Step 5 of 5
Publish and check the built page
Publish a test entry, trigger your build or revalidation, and confirm the widget renders on the live URL, not just in a CMS preview.
Where should the widget be decided?
Before you start
What to know before you build it
No CMS app or plugin
StoryProof has no Sanity, Contentful or other CMS integration. The CMS stores a plain id that your own code turns into the embed.
Client side navigation
The script fills the widget divs present when it runs. If your front end changes pages without a full load and a widget mounted later stays empty, send visitors to that page with a full page link and tell StoryProof support.
Previews may not show it
CMS preview panes often render in a sandbox or skip layout scripts. Judge the widget on the deployed page.
Badge and widget allowance
Each widget counts toward your workspace allowance, and a "Powered by StoryProof" badge shows until Pro.
Docs as of 2026-11-13
The schema follows the Sanity documentation as read on 2026-11-13. Nobody has followed these steps on a live project yet; other headless CMSs name their fields differently.
FAQ
Questions people ask
Short answers to what usually comes up next.
Can I store the whole embed snippet in a text field instead?
You can, but your front end would then have to output raw HTML from the CMS, which is the risk this setup avoids. Storing the id keeps the markup in reviewed code.
Does this work with Contentful, Strapi or Storyblok?
The pattern does: a short text field for the id, a renderer case, and one script in the layout. Field and renderer names differ, so follow your CMS documentation for those parts.
Will the testimonials be in my statically built HTML?
No. Your build outputs the empty div, and the script fills it in the visitor’s browser.
What if an editor types a wrong id?
The div renders but the widget will not appear. Add the id list to the field description, and check every new page on the live site after publishing.
Is there somewhere to send people without any code?
Yes. Every plan includes a hosted Wall of Love page with its own link, which an editor can add as an ordinary link.
Still have a question? Ask the StoryProof team or book a demo.
Give your CMS editors a widget id
Collect and approve testimonials in StoryProof, then let editors place widgets by id.
Sources
- String type, Sanity Docs. Checked 2026-11-13.
- Block type, Sanity Docs. Checked 2026-11-13.
Last updated 2026-11-13.