# Add a feedback widget to a Hugo site

> Put the widget script in a partial and include it from layouts/_default/baseof.html with defer. Store the public product key in site params, with a separate value for production builds in config/production. Hugo static pages have no sign-in, so the widget records each page URL automatically, and a docs-only button opens the form with data-escuta-open.

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

## Where does the widget go in Hugo's base template?

Hugo builds every page from templates in `layouts/`. The file that wraps every page is `layouts/_default/baseof.html`, which defines the `head` and `body` and lets each page fill a `main` block. Put the widget in a partial, a small template you include from anywhere, so the script and its settings live in one place.

Create `layouts/partials/escuta-widget.html`:

```html
{{- with .Site.Params.escuta }}
  {{- if .key }}
  <script src="https://escutaproduto.com/widget.js"
          data-key="{{ .key }}"
          data-trigger="{{ if eq $.Section "docs" }}none{{ else }}button{{ end }}"
          defer></script>
  {{- end }}
{{- end }}
```

The `with` block switches the context to the `escuta` params, and `if .key` skips the script when no key is set. The `$.Section` reference points at the page being built, even inside the `with` block. On documentation pages the floating button is hidden with `data-trigger="none"`, so the only entry point there is the one you add in the next section.

Include the partial in the `head` of `baseof.html`:

```html
<!DOCTYPE html>
<html lang="{{ .Site.Language.LanguageCode }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
  {{ partial "escuta-widget.html" . }}
</head>
<body>
  {{ block "main" . }}{{ end }}
  {{ partial "escuta-feedback-link.html" . }}
</body>
</html>
```

Every page that uses `baseof.html` now gets the widget. Hugo's template engine escapes the `data-key` value in the attribute, so a stray character in the key cannot break the markup.

## How do I keep the product key in site params?

Site params are values you define in your Hugo configuration and read with `.Site.Params`. Put the public product key in `config/_default/hugo.toml`, under a table named `params.escuta`:

```toml
baseURL = "https://docs.example.com/"
title = "Example docs"

[params.escuta]
key = "pk_your_dev_product_key"
```

Hugo lowercases parameter names, so `.Site.Params.escuta.key` is the right path in the template. The product key is public, since it ships in the HTML by design, and it can only create feedback. Keep it in the configuration rather than in the template, so a change happens in one file.

## How do I use different keys for preview and production builds?

Hugo reads configuration from a folder per environment. Running `hugo` for a build uses the `production` environment by default, and `hugo server` uses `development`. Keep the development key in `config/_default/hugo.toml`, and override it for production in `config/production/hugo.toml`:

```toml
[params.escuta]
key = "pk_your_production_product_key"
```

Hugo merges the production file over the defaults, so only the key changes. Create one Escuta Produto product for each key, and give each product its own allowed origins. Add your local address, which `hugo server` prints as `http://localhost:1313/` by default, to the development product. Requests from any origin that is not in the list receive a 403, so a copied key cannot send feedback from another site.

## How do I show the widget only in the docs section?

Documentation pages often need a clear prompt more than a floating button. Create `layouts/partials/escuta-feedback-link.html`, which renders a button only on pages in the `docs` section:

```html
{{- with .Site.Params.escuta }}
{{- if and .key (eq $.Section "docs") }}
<aside class="feedback-prompt">
  <p>Is this page missing something?</p>
  <button type="button" data-escuta-open="idea">Suggest a change</button>
  <button type="button" data-escuta-open="bug">Report a problem</button>
</aside>
{{- end }}
{{- end }}
```

The `data-escuta-open` value preselects the type, so the first button opens the form with the idea type and the second with the bug type. Valid values are `bug`, `idea`, `praise` and `other`. The [widget reference](/docs/widget) explains the attribute and the JavaScript calls that open the form.

Because the partial checks the section, the same base template serves a blog or a landing page without a prompt. You can add a `{{ if eq .Section "blog" }}` branch later for a different question on another section.

## Does a static Hugo page know who is reading it?

No, not by default. A Hugo site is static HTML, so there is no server session to read the signed-in user from. The widget still works for anonymous visitors, and it records the address of the page each time someone sends a message. That is usually enough for documentation, where the page address shows which article prompted the feedback.

If your docs sit behind a separate sign-in, call identify from a small script after the visitor signs in, using the queue form the widget reads when it starts. The queue works before or after the script loads. Keep that script in `static/js/`, and load it with `defer` after the widget tag, so the order stays predictable.

## What about the Content Security Policy on a static host?

Hugo does not send headers, so the policy belongs to your host. On Cloudflare Pages, Netlify and similar hosts, a `_headers` file in the `static/` folder sets them. Hugo copies it to the output root unchanged:

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

The policy needs the widget host in `script-src` and `connect-src`. Hugo themes sometimes write small inline scripts or styles, so check the browser console after a build. If a theme script is blocked, prefer moving it to a file in `static/` over loosening the policy. The [CSP guide for widgets](/resources/content-security-policy-widgets) explains how to read a blocked request.

## How do I check the Hugo build works?

Run `hugo server` and open a docs page. View the page source and confirm that the `escutaproduto.com/widget.js` script tag is present with your development key. Click the prompt button and send a test idea. Then run `hugo` for a production build, and confirm that the output carries the production key.

If the script tag is missing, the `with` block found no key, so check the file under `config/` and that the table name is `params.escuta`. If the prompt does not appear, check that the page is in the `docs` section, which Hugo takes from the folder name under `content/`.

## Finishing the Hugo setup with Escuta Produto

Once the partial and the prompt are in place, send a few test items from different docs pages. Each item records the page address it came from, so you can see which articles generate questions. The [best practices guide](/resources/feedback-widget-best-practices) covers how to place a prompt without distracting readers, and the [Eleventy guide](/resources/feedback-widget-eleventy) shows the same layout pattern for another static generator.

In Escuta Produto, the items appear in the inbox of the product that owns the key, with a filter by type and status. Set the status of an answered suggestion to done when you ship it, so the same question does not come back as a new item.

## Frequently asked questions

### Where should the feedback widget script go in a Hugo site?

In a partial such as layouts/partials/escuta-widget.html, included from the head of layouts/_default/baseof.html. Every page built from the base template then loads the widget with the defer attribute.

### How do I use a different feedback key for production in Hugo?

Keep the development key in config/_default and add a production override under config/production. Hugo uses the production environment for a normal build, so the production key appears only in the published site.

### Can a Hugo docs page show a feedback button only in the docs section?

Yes. Check the page section in a partial with the eq function and render a button with the data-escuta-open attribute only when the section is docs. Set data-trigger to none on those pages to hide the floating button.

### Does the feedback widget record the page URL on a static Hugo site?

Yes. The widget reads the page address when a visitor sends a message, so each item records the article it came from. Static pages have no signed-in user, so items from documentation readers are anonymous by default.
