Vue and Nuxt
Vue and Nuxt Testimonial Widget Component
Vue will not render a script tag from a component template; its compiler drops it with a warning. So the StoryProof embed becomes a tiny component: the div in the template, and embed.js loaded in code. In a client-rendered Vue app that happens after the component mounts. In Nuxt, useHead adds the script once to the end of the body of the server-rendered page.
- A Vue 3 component that loads the script on mount
- A Nuxt app.vue entry using useHead
- Why a pasted script tag never runs
Code generator
Components with your widget id filled in
Enter the widget id. Use the first block for a client-rendered Vue 3 app, or the second and third for Nuxt, where the script is declared once in app.vue.
Part 1 · Vue 3 (Vite): the component
src/components/StoryProofWidget.vue
<!-- src/components/StoryProofWidget.vue (Vue 3 with Vite, client-rendered) -->
<script setup lang="ts">
import { onMounted } from 'vue';
const props = defineProps<{ widgetId: string }>();
const EMBED_SRC = 'https://app.storyproof.io/embed.js';
onMounted(() => {
// The div below exists now. Add embed.js once per page load.
if (document.querySelector(`script[src="${EMBED_SRC}"]`)) return;
const tag = document.createElement('script');
tag.src = EMBED_SRC;
tag.async = true;
document.body.appendChild(tag);
});
</script>
<template>
<div :data-storyproof-widget="props.widgetId"></div>
</template>Part 2 · Nuxt: load the script once
app/app.vue
<!-- app/app.vue (Nuxt): load embed.js once, at the end of <body> -->
<script setup lang="ts">
useHead({
script: [{ src: 'https://app.storyproof.io/embed.js', async: true, tagPosition: 'bodyClose' }],
});
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>Part 3 · Nuxt: the component
app/components/StoryProofWidget.vue
<!-- app/components/StoryProofWidget.vue (Nuxt): markup only -->
<script setup lang="ts">
defineProps<{ widgetId: string }>();
</script>
<template>
<div :data-storyproof-widget="widgetId" />
</template>
<!-- Use it in any page: <StoryProofWidget widget-id="YOUR_WIDGET_ID" /> -->Run the dev server, open the route, and look for a single embed.js request in the network tab. In Nuxt, view the page source as well: the div arrives in the server HTML and the script sits before the closing body tag.
The code is put together in your browser from the id you type.
The problem, solved
Where a hosted widget trips up Vue, and how this one fits
- Problem 1
Pasted script tag, nothing happens
Vue’s template compiler ignores script and style tags in component templates, so a vendor snippet pasted into a .vue file loses its script.
How StoryProof solves it
StoryProof’s embed separates cleanly: an empty div carrying the widget id goes in the template, and the async embed.js script is loaded in code.
- Problem 2
Every component instance adds the script
A widget component reused on three routes, each injecting embed.js, multiplies requests and invites race conditions.
How StoryProof solves it
One copy of embed.js fills every StoryProof widget div on the page, so the Vue component checks for an existing tag and Nuxt declares it once in app.vue.
- Problem 3
Customer content in my production app
Rendering whatever a customer submitted into a shipped front end is a risk the developer ends up owning.
How StoryProof solves it
Nothing appears until someone approves it, and each widget id shows only the approved testimonials chosen for it.
- Problem 4
Marketing wants changes without tickets
If quotes live in a JSON file in the repo, every swap becomes a pull request for a developer.
How StoryProof solves it
Give marketers their own seats in the StoryProof space so they choose testimonials there; the component only holds the widget id.
- Problem 5
Our users would rather talk
Product users rarely write long reviews, yet a short recording says more about the experience.
How StoryProof solves it
The collection link lets them record a video, leave an audio note or type, in the browser with nothing to install.
In StoryProof
Wire it into Vue or Nuxt
Step 1 of 4
Note the widget id
Build the widget in StoryProof and copy the value of data-storyproof-widget from its embed code.
Step 2 of 4
Vue: add the component and render it
Create StoryProofWidget.vue from the generator and place it on a route. onMounted runs after the div exists, then adds embed.js unless it is already on the page.
Step 3 of 4
Nuxt: declare the script in app.vue
Add the useHead call with tagPosition set to bodyClose, which Nuxt documents for putting a tag at the end of the body. Then add the markup-only component.
Step 4 of 4
Check a fresh load and a route change
Load the route directly, then reach it by navigating inside the app. If the second case leaves the box empty, see the limits below.
The embed in a .vue file, before and after
Pasted as is
- Script tag inside <template>, dropped by the compiler
- The div renders, empty
- A warning in the console nobody reads
As a component
- Only the div in the template
- embed.js added once, on mount or via useHead
- The same component reused on any route
Before you start
Limits for Vue and Nuxt
In-app navigation
StoryProof has not documented whether embed.js fills a div that appears after it has run. If a widget stays empty after a router change, link to that route with a plain anchor so it loads fresh.
Content Security Policy
If your app sends a CSP header, allow the host of the StoryProof script in script-src or the browser will block it.
No npm package
StoryProof ships no Vue or Nuxt module. The component on this page is code you own and maintain.
Nuxt Scripts not covered
Nuxt also offers a separate scripts module. This guide uses useHead, which ships with Nuxt, and does not cover that module.
From Vue and Nuxt docs, untested
The code follows the Vue and Nuxt documentation as read on 2026-11-13. It has not yet been run against a live StoryProof widget in a Vue or Nuxt app for this guide.
FAQ
Questions people ask
Short answers to what usually comes up next.
Why not put the script in index.html for a Vite app?
In a client-rendered app the async script can run before Vue has mounted the div. Loading it in onMounted makes sure the div exists first.
Does the Nuxt version work with server rendering?
Yes. The div is in the server HTML and useHead places the script before the closing body tag, so both are present when the page loads.
Can I show two widgets on one route?
Yes. Render the component twice with different ids. One embed.js fills both.
Does this work with Vue 2?
The code uses Vue 3’s script setup syntax. A Vue 2 app would need the same idea written with the mounted hook.
Still have a question? Ask the StoryProof team or book a demo.
Ship real testimonials in your Vue app
Collect testimonials with one link and render them with a small component you own.
Sources
- compiler-dom errors (X_IGNORED_SIDE_EFFECT_TAG), Vue.js core repository. Checked 2026-11-13.
- Built-in Directives: v-html, Vue.js. Checked 2026-11-13.
- Element: innerHTML property, MDN Web Docs. Checked 2026-11-13.
- SEO and Meta, Nuxt. Checked 2026-11-13.
- useHead, Nuxt. Checked 2026-11-13.
Last updated 2026-11-13.