Add a feedback widget to an Astro site
Add the widget script to your base layout with is:inline and defer. Astro keeps the script as written, and ClientRouter keeps the widget working across view-transition navigations. Logged-in pages can identify the user from Astro.locals, while static pages work anonymously through data-escuta-open buttons.
By Rafael Thayto · Last updated
Add the widget to your base layout
Most Astro sites share one layout file that wraps every page. Add the widget script to its head in that file, and mark it with is:inline:
---
// src/layouts/Base.astro
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script is:inline src="https://escutaproduto.com/widget.js" data-key="pk_your_product_key" defer></script>
</head>
<body>
<slot />
</body>
</html>
Astro processes the script tags it finds in components, bundling local modules and rewriting some attributes. The is:inline directive tells Astro to leave the tag exactly as written. Without it, the remote script and its data-key could be changed or moved. The defer attribute keeps the widget from blocking the page while the HTML renders.
Every page that uses the layout gets the widget, so you do not add it per page. Keep it in one place so a future redesign cannot drop it from some pages by accident.
Astro builds static pages at build time and server-rendered pages at request time, and the widget script behaves the same in both. It runs in the browser after the HTML arrives, so the only part that differs is identify, which needs a user that exists on the server. After you add the layout, view the page source and confirm the script tag sits in the head. If it appears anywhere else, look first for a missing is:inline directive.
Keep the widget across view transitions
Astro's view transitions swap the page body on navigation, which is faster than a full reload. Enable the router by importing ClientRouter from astro:transitions and placing it in the head:
---
import { ClientRouter } from "astro:transitions";
---
<head>
<ClientRouter />
</head>
ClientRouter is the current name for the component. Older tutorials show ViewTransitions, the earlier name, and those examples still describe the same behavior. With the router enabled, the script in the layout head stays loaded between pages, so the floating button and the data-escuta-open buttons keep working.
If you add an inline script that must run again on every navigation, give it the data-astro-rerun attribute. Astro runs such scripts again after each page swap. The identify call in the next section uses it for that reason.
Identify logged-in users on server-rendered pages
An Astro site with server rendering can read the session in middleware and pass the user to the layout. Set Astro.locals.user in src/middleware.ts, and declare its type in src/env.d.ts. Then render the identify call only when a user exists:
---
// src/layouts/Base.astro (inside the head, after the widget script)
const user = Astro.locals.user;
---
{user && (
<script is:inline define:vars={{ user }} data-astro-rerun>
window.EscutaProduto = window.EscutaProduto || { q: [] };
(window.EscutaProduto.q ||= []).push([
"identify",
[{ id: user.id, email: user.email, name: user.name }],
]);
</script>
)}
The define:vars attribute passes the object into the script. Astro serializes the values for you, so a name with quotes or a closing script tag stays inside a string. Do not build the script text by concatenating values yourself. Send only the fields you would show the person, because the script runs in the browser and anyone can read it.
Static sites without a login
A fully static site has no user at build time, and no identify is needed. Skip the conditional block above and let the widget run anonymously. Visitors can still send feedback with the floating button, and each item records the page URL and browser.
Use the attribute buttons where a page has a clear reason for feedback, such as a docs page or a pricing comparison:
<button type="button" data-escuta-open="idea">Suggest a feature</button>
Static pages also suit the hosted feedback page. Link to it from a footer when you want a page that is not tied to the widget at all.
Content Security Policy for an Astro site
Pages built as static HTML use inline scripts only where you write them. A policy that allows the widget host works for the external script. The define:vars block above is an inline script whose content changes per user, so a static hash cannot cover it. For a strict policy, move the values into a data attribute and read them from an external file:
<div id="escuta-user" data-user={JSON.stringify(user)} hidden></div>
<script is:inline src="/escuta-identify.js" data-astro-rerun></script>
// public/escuta-identify.js
const el = document.getElementById("escuta-user");
const user = el && el.dataset.user ? JSON.parse(el.dataset.user) : null;
if (user) {
window.EscutaProduto = window.EscutaProduto || { q: [] };
(window.EscutaProduto.q ||= []).push(["identify", [user]]);
}
Astro escapes attribute values, so the JSON stays intact. Set the policy in your host's headers. On Cloudflare Pages, for example, a _headers file can set it for every path:
/*
Content-Security-Policy: script-src 'self' https://escutaproduto.com; connect-src 'self' https://escutaproduto.com
For the directives and how to debug a blocked request, see the CSP guide for widgets.
Check the Astro install in your Escuta Produto inbox
- Add
http://localhost:4321(the default Astro dev URL) to the allowed origins of your product. - Open a page, click the Feedback button and send a test idea.
- In the inbox, confirm the item shows the page URL and browser. If you added identify, it should also show the name and email.
- Navigate to a second page with a normal link. Open the widget there too, and confirm the page URL changed.
If the button disappears after navigation, check that ClientRouter sits in the head and that the widget script uses is:inline. A script Astro bundled can behave differently after a swap.
For the full option list, read the widget reference. The Nuxt guide covers the server-rendered Vue equivalent, and the widget performance notes explain why defer matters on content sites.
Frequently asked questions
Why does the Astro widget script need is:inline?
Astro bundles the script tags it finds in your components. The is:inline directive tells Astro to output the tag exactly as written, so the widget loads from escutaproduto.com with its data-key intact.
Does the feedback widget work with Astro view transitions?
Yes. Add ClientRouter from astro:transitions to the head of your layout. Navigations swap the page body and keep the script loaded, so the Feedback button keeps working on each page.
Can I use the feedback widget on a static Astro site without login?
Yes. Static pages have no signed-in user, so skip identify. The floating button and any data-escuta-open buttons work for anonymous visitors. Each feedback item still records the page URL and browser.
How do I pass a logged-in user to identify in Astro?
Set Astro.locals.user in middleware for server-rendered pages, then render an identify call in the layout. For a strict CSP, put the user in a data attribute and read it from an external script.
Related
- Add a feedback widget to a React appInstall the Escuta Produto feedback widget in a Vite React app: the script in index.html, identify from your auth context, and how routes behave.
- Add a feedback widget to a Vue appAdd the Escuta Produto feedback widget to a Vue 3 app with Vite: script in index.html, identify in a composable after login, and Vue Router notes.
- Add a feedback widget to a Nuxt appAdd the Escuta Produto feedback widget to a Nuxt app: app.head script config, a client-only plugin for identify, and server rendering caveats.
- Add a feedback widget to a SvelteKit appAdd the Escuta Produto feedback widget to a SvelteKit app: the app.html script, identify from layout data, and Content Security Policy in svelte.config.js.