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.

By 路 Published 路 Last updated

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:

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

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

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:

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

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

/*
  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 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 covers how to place a prompt without distracting readers, and the Eleventy guide 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.