# Add a feedback widget to an Eleventy site

> Add the widget script to the head of your base layout with defer. Read the public product key from a global data file, _data/escuta.js, which reads the ESCUTA_KEY build variable. Eleventy pages are static HTML with no sign-in, so the widget records each page URL on its own and anonymous visitors can still send feedback.

Source: https://escutaproduto.com/resources/feedback-widget-eleventy
Last updated: 2026-10-11

## Where does the script go in the Eleventy base layout?

Eleventy sites usually share one layout that every page wraps, such as `_includes/base.njk` in a Nunjucks project. Put the widget script in its `head` with `defer`. Each page built from the layout then loads the widget, and the Feedback button appears site-wide:

```njk
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ title }} · My docs</title>
  {% if escuta.key %}
  <script src="https://escutaproduto.com/widget.js" data-key="{{ escuta.key }}" defer></script>
  {% endif %}
</head>
<body>
  {{ content | safe }}
  {% include "partials/feedback-prompt.njk" %}
</body>
</html>
```

The `{% if escuta.key %}` guard skips the widget when the build has no key, so a local build without the variable does not show a button that cannot send. The `escuta` object comes from global data, which the next section sets up. The `{{ content | safe }}` line outputs the page body that Eleventy has already rendered from Markdown, Nunjucks or another template language. The `{% include %}` line adds an optional prompt, which you can remove if you only need the floating button.

Nunjucks escapes `{{ escuta.key }}` by default, so the key cannot break out of the `data-key` value.

## How do I read the key from global data?

Global data files in the `_data` folder become variables for every template. A JavaScript file lets you read environment variables at build time. Create `_data/escuta.js`:

```javascript
module.exports = {
  key: process.env.ESCUTA_KEY || "",
};
```

Eleventy uses the file name as the variable name, so the layout reads `escuta.key`. Because the file runs during the build, the value comes from the environment of the build process, not from the browser. Set `ESCUTA_KEY` in your shell or in the build settings of your host before you run `npx @11ty/eleventy`. Eleventy does not load a `.env` file on its own, so export the variable or pass it inline when you test locally.

The product key is public and ships in the HTML by design. Still, keep the development and production keys apart. Create one Escuta Produto product for each, and give each product its own allowed origins. Add the local address that Eleventy's dev server prints, usually `http://localhost:8080`, to the development product. A request from an origin that is not on the list receives a 403, so a copied key cannot send feedback from another site.

## Does a static Eleventy page need identify?

Usually not. An Eleventy site is generated HTML, so there is no server session and no signed-in user in the page. Anonymous feedback is the normal case for documentation, changelogs and marketing pages, and the widget handles it with no extra code.

Identify is only worth adding when the site has a sign-in of its own, for example a members area built with a separate client-side script. In that case, call identify from that script once the visitor signs in, using the queue form, which works before or after the widget loads:

```javascript
window.EscutaProduto = window.EscutaProduto || { q: [] };
(window.EscutaProduto.q ||= []).push(["identify", [{ id: "user_123", email: "reader@example.com", name: "Reader" }]]);
```

Replace the sample values with the data your sign-in returns, and never write them into the template at build time. A page built once would show the same person to every visitor.

## How does the widget know which page it is on?

The widget reads the browser's current address when a visitor sends a message, and it stores that address with the item. On an Eleventy site, each generated page is a real HTML file with its own address, such as `/guides/install/`, so every item records the exact page that prompted it. You do not need a template variable for the page address.

This matters for documentation, where the same question often comes from one article. In the inbox you can read the page address next to the message, which tells you which guide needs a clearer step. The [widget reference](/docs/widget) lists the fields the widget stores automatically.

## Which headers stop a strict policy from blocking the widget?

Eleventy builds files and does not send headers, so the policy belongs to your host. Many hosts read a `_headers` file from the output folder, which Eleventy copies from the `input` folder with a passthrough copy. Netlify and Cloudflare Pages both read this format:

```text
/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://escutaproduto.com; connect-src 'self' https://escutaproduto.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:
```

Add the copy rule to `.eleventy.js` so the file reaches the output folder:

```javascript
module.exports = function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy({ "src/_headers": "_headers" });
};
```

The policy needs the widget host in `script-src` and `connect-src`. If a plugin or a theme writes inline scripts, move them into a file under the input folder, which keeps the policy strict. The [CSP guide for widgets](/resources/content-security-policy-widgets) explains how to find a blocked request in the browser console.

## How do I open the form from a prompt partial?

The layout includes `partials/feedback-prompt.njk`, which holds buttons that open the form. Create that file in your includes folder, `_includes/partials/feedback-prompt.njk`:

```njk
{% if escuta.key %}
<aside class="feedback-prompt">
  <p>Was this page useful?</p>
  <button type="button" data-escuta-open="praise">Yes</button>
  <button type="button" data-escuta-open="idea">Not yet</button>
</aside>
{% endif %}
```

Each button carries `data-escuta-open` with a type, which preselects the form. If you want only this prompt and no floating button, add `data-trigger="none"` to the script tag in the layout. The [widget reference](/docs/widget) lists the complete set of attributes.

## What should I check after the first build?

Run `ESCUTA_KEY=pk_your_dev_product_key npx @11ty/eleventy --serve` and open any page. View the source and confirm that the `widget.js` tag carries your key. Click the Feedback button and send a test idea. Then run a production build and check that the key in the output matches the production product.

If the script tag is missing, the variable was empty when Eleventy ran, which is the most common cause. If the widget loads but sending fails, the origin of the page is missing from the allowed origins of that product, so add the exact address with its port.

## Setting up the Eleventy build in your Escuta Produto inbox

After the build works, send a few items from different pages and check how they appear in the inbox. Each one should carry the page address, and the type filter helps you separate bugs in the docs from ideas for new guides. Escuta Produto keeps those items in the product you created for the site, next to any other product you run.

The [Hugo guide](/resources/feedback-widget-hugo) covers the same pattern for another static generator, and the [Astro guide](/resources/feedback-widget-astro) shows a static site built with a component framework. For the React-based alternative, see the [React guide](/resources/feedback-widget-react).

## Frequently asked questions

### Where should the feedback widget script go in an Eleventy site?

In the head of your base layout, such as _includes/base.njk, with the defer attribute. Every page that uses the layout gets the script, so the Feedback button appears site-wide.

### How do I pass the feedback key to an Eleventy template?

Create a global data file in the _data folder that exports the key from process.env.ESCUTA_KEY. Templates then read the value as a variable, and the build fills it in from your environment.

### Does the feedback widget know which page a visitor is reading on a static site?

Yes. The widget reads the browser address when a message is sent, so each item records the exact generated page, such as a guide URL. You do not need to pass the page address from a template.

### Do I need identify on an Eleventy site without a login?

No. Static pages have no signed-in user, so anonymous feedback is the normal case. Add identify only when the site has its own client-side sign-in, and call it after the visitor signs in.
