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

::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:

   ```vue [app/components/DuxtPageFeedback.vue]
   <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:

   ```ts [server/api/feedback.post.ts]
   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.

::

::callout{type="tip" title="One destination for everything?"}
The same answer is also a `feedback` analytics event. If search, copies and
try-it requests should land where the feedback lands, write one callback instead
of a wrapper — see [Send analytics events](/guides/send-analytics-events). The
two are independent: this component's event and slot work whether or not
`duxt.analytics.track` is set.
::

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

```vue [app/components/DuxtPageFeedback.vue]
<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.

::callout{type="warning" title="Two answers are not two readers"}
Nothing in the component identifies a visitor, and it deliberately stays that
way. If you count answers per page, count them as answers — a browser that
reloads and answers again is a second answer, not a second reader, and turning
one into the other is a consent question your privacy policy has to answer
first.
::

## 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](/adr/0005-ship-the-layer-without-owner-specific-links)).

## 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