Nuxt

Install the package and start it from a client-only plugin, with keys in runtime config.

Installed with the @userlily/js npm package · Written for Nuxt 3

What works

ProductStatusNote
Analytics & custom eventsSupported
Feedback widgetSupported
Review badgeSupported
Web pushSupported
Roadmap embedSupported
Feedback, survey & roadmap linksSupported

1. Install the package

Terminal

npm install @userlily/js

2. Add your keys

Nuxt maps NUXT_PUBLIC_* variables onto runtimeConfig.public. A server-rendered app reads them when it starts; with nuxt generate they are fixed at build time.

.env

NUXT_PUBLIC_USERLILY_HOST=https://dev.userlily.com
NUXT_PUBLIC_USERLILY_TRACKING_ID=YOUR_TRACKING_ID
NUXT_PUBLIC_USERLILY_CAMPAIGN=YOUR_CAMPAIGN_KEY
NUXT_PUBLIC_USERLILY_BROADCAST_KEY=YOUR_PROJECT_KEY

nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      userlilyHost: '',
      userlilyTrackingId: '',
      userlilyCampaign: '',
      userlilyBroadcastKey: '',
    },
  },
})

3. Create a client-only plugin

The .client suffix keeps it out of server rendering. Nuxt loads it automatically, and records route changes on its own.

plugins/userlily.client.js

import { init } from '@userlily/js'

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig().public
  init({
    host: config.userlilyHost,
    analytics: { trackingId: config.userlilyTrackingId },
    widget: { campaign: config.userlilyCampaign },
    broadcast: { project: config.userlilyBroadcastKey },
  })
})

4. Send custom events

In any component

<script setup>
import { track } from '@userlily/js'
</script>

<template>
  <button @click="track('signup_completed')">Sign up</button>
</template>

5. Show the review badge

Place <ReviewBadge /> where it should appear.

components/ReviewBadge.vue

<!-- components/ReviewBadge.vue -->
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { mountSyndication } from '@userlily/js'

const host = useRuntimeConfig().public.userlilyHost
const target = ref(null)
let unmount = () => {}

onMounted(() => {
  unmount = mountSyndication({
    host,
    campaign: 'YOUR_CAMPAIGN_KEY',
    target: target.value,
  })
})
onUnmounted(() => unmount())
</script>

<template>
  <div ref="target" />
</template>

6. Host the push worker

The worker must be served from your own domain at /userlily-sw.js, as JavaScript, with no redirect and over HTTPS. Each of these fails silently. Nuxt serves public/ at the site root.

Terminal - re-run after updating the package

npx --no userlily copy-sw public

7. Ask for push permission from your own button

Optional. Works when “When to ask” is set to “Only when your own code asks”.

In any component

<script setup>
import { broadcast } from '@userlily/js'
</script>

<template>
  <button @click="broadcast.prompt()">Get notified</button>
</template>

8. Embed the public roadmap

Turn on “Enable public roadmap” in Roadmap → Settings first.

components/RoadmapEmbed.vue

<!-- components/RoadmapEmbed.vue -->
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'

const ORIGIN = 'https://dev.userlily.com'
const frame = ref(null)

function onMessage(event) {
  if (event.origin !== ORIGIN) return
  const data = event.data
  if (data?.type !== 'userlily:roadmap:height') return
  if (frame.value && data.height > 0) {
    frame.value.style.height = data.height + 'px'
  }
}

onMounted(() => window.addEventListener('message', onMessage))
onUnmounted(() => window.removeEventListener('message', onMessage))
</script>

<template>
  <iframe
    ref="frame"
    :src="ORIGIN + '/r/YOUR_ROADMAP_KEY?embed=1'"
    title="Product roadmap"
    loading="lazy"
    style="width: 100%; height: 720px; border: 0"
  />
</template>

Every option, limit and troubleshooting step is in the main docs. Logos are trademarks of their owners and are shown only to indicate compatibility.