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
- Wrap the layer's component under its own name. Nuxt resolves your file
first, so
app/components/DuxtPageFeedback.vuein your project replaces the layer's — and the@duxtalias 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> - 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$fetchin the wrapper with whatever your provider's SDK exposes. - 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.
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).
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