Skip to content
duxt

Collect page feedback

Wrap DuxtPageFeedback, keep the answer where your analytics already live.

Every documentation page ends with "Was this page helpful?", and the layer does nothing with the answer: it holds it in the component's own state for the length of the visit, emits an event, and stores nothing. Wrapping the component is how the answer reaches you.

Steps

  1. Wrap the layer's component under its own name. Nuxt resolves your file first, so app/components/DuxtPageFeedback.vue in your project replaces the layer's — and the @duxt alias still reaches the original by path, so the wrapper keeps the layer's markup, translations and reset-on-navigation:
    <script setup lang="ts">
    import DuxtPageFeedbackBase from '@duxt/components/DuxtPageFeedback.vue';
    
    const route = useRoute();
    
    // The event carries the answer and nothing else — the page it belongs to is
    // yours to read, and `fullPath` is the one that keeps the locale, repository
    // and version prefixes the answer was actually given under.
    async function onFeedback(helpful: boolean) {
      await $fetch('/api/feedback', {
        method: 'POST',
        body: { path: route.fullPath, helpful }
      });
    }
    </script>
    
    <template>
      <DuxtPageFeedbackBase @feedback="onFeedback" />
    </template>
    
  2. Give the answer somewhere to land. A route of your own, so the reader's browser never talks to a third party you did not choose:
    export default defineEventHandler(async (event) => {
      const { path, helpful } = await readBody(event);
    
      // Your storage, your analytics, your queue.
      await recordFeedback({ path, helpful });
    
      return { ok: true };
    });
    

    A client-side analytics call works the same way — replace the $fetch in the wrapper with whatever your provider's SDK exposes.
  3. Rebuild and answer the question once, then check that the answer arrived with the path you expected — under a version prefix, not just at the bare URL.

Rendering your own control

The event keeps the layer's buttons. To replace them, take the default slot instead: it exposes the same state the component renders from, so the reset on navigation and the "thanks" state stay the layer's job.

<script setup lang="ts">
import DuxtPageFeedbackBase from '@duxt/components/DuxtPageFeedback.vue';
</script>

<template>
  <DuxtPageFeedbackBase v-slot="{ answered, answer }">
    <form v-if="answered === undefined" @submit.prevent="answer(true)">
      <label>
        Tell us what is missing
        <textarea name="comment" />
      </label>
      <button type="submit">Send</button>
    </form>
    <p v-else>Thank you.</p>
  </DuxtPageFeedbackBase>
</template>

answer(helpful) sets the state and emits the event, so a slot and a listener compose: hang your form on the slot, and keep the @feedback handler from step 1 to deliver what it collected.

Why the layer stores nothing

A documentation theme that phoned home by default would have to pick a destination for someone else's readers, and there is no defensible one: the layer knows neither where your analytics live nor what your privacy policy promises. Shipping the question with an event and no backend keeps the decision where it belongs — the same reason the footer's legal row and the repository links ship empty (ADR 0005).

Checklist

  • The wrapper renders the layer's component rather than reimplementing it
  • The answer is sent with the path it was given on, prefixes included
  • The destination is yours — a route, a queue or an SDK you chose
  • Nothing in the payload identifies the reader
Was this page helpful?