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.

By 路 Published 路 Last updated

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:

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

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:

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

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

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

{% 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 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 covers the same pattern for another static generator, and the Astro guide shows a static site built with a component framework. For the React-based alternative, see the React guide.

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.