Skip to main content

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.

Shown in StoryProof when you copy a widget’s embed code.

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.

  • 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.

  • 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.

  • 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.

  • Problem 5

    Our users would rather talk

    Product users rarely write long reviews, yet a short recording says more about the experience.

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.

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

  1. compiler-dom errors (X_IGNORED_SIDE_EFFECT_TAG), Vue.js core repository. Checked 2026-11-13.
  2. Built-in Directives: v-html, Vue.js. Checked 2026-11-13.
  3. Element: innerHTML property, MDN Web Docs. Checked 2026-11-13.
  4. SEO and Meta, Nuxt. Checked 2026-11-13.
  5. useHead, Nuxt. Checked 2026-11-13.

Last updated 2026-11-13.