Repository: redpanda-data/docs-ui
Pull request #285: Switch to native Netlify form submission to enable reCAPTCHA
Author: login: JakeSCahill
Created: 2025-05-09T12:20:41Z | Merged: 2025-05-10T06:07:53Z

This PR updates our feedback form to use native HTML form submission instead of JavaScript `fetch()` to ensure Netlify's built-in reCAPTCHA and spam protection features are actually enforced.

Previously, we submitted the form using `fetch()` to avoid a page reload and keep the modal experience smooth. However, Netlify only applies reCAPTCHA validation and spam filtering to native form POST requests. As a result, our previous approach completely bypassed these protections and allowed spam through, despite reCAPTCHA being enabled in our dashboard.


Diff:
diff --git a/src/css/base.css b/src/css/base.css
index f42e433b..cb795781 100644
--- a/src/css/base.css
+++ b/src/css/base.css
@@ -114,6 +114,23 @@ object[type="image/svg+xml"]:not([width]) {
   opacity: 0.5;
 }
 
+.hint {
+  margin: 0.5em 0;
+  color: #555;
+  font-size: small;
+  text-align: center;
+}
+
+button[disabled] {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+button[disabled]:hover {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
 @media (pointer: fine) {
   @supports (scrollbar-width: thin) {
     html {
diff --git a/src/css/feedback-form.css b/src/css/feedback-form.css
index 37da5242..a2cc7825 100644
--- a/src/css/feedback-form.css
+++ b/src/css/feedback-form.css
@@ -22,6 +22,22 @@
   max-height: 80vh;
 }
 
+.feedback-toast {
+  margin-top: 1rem;
+  margin-bottom: 1em;
+  padding: 0.75rem 1rem;
+  background: var(--tip-background);
+  color: var(--tip-on-color);
+  border-left: 4px solid #27ae60;
+  font-size: 0.9rem;
+  border-radius: 4px;
+  max-width: 400px;
+}
+
+.feedback-toast.hidden {
+  display: none;
+}
+
 .feedback-modal textarea {
   font-family: inherit;
   font-size: 0.9rem;
@@ -45,6 +61,7 @@
 .feedback-modal .radio-buttons.form-field {
   display: flex;
   flex-direction: column;
+  gap: 15px;
 }
 
 .feedback-modal .form-field {
diff --git a/src/partials/announcement-email.hbs b/src/partials/announcement-email.hbs
index 24a9c466..05bc74a5 100644
--- a/src/partials/announcement-email.hbs
+++ b/src/partials/announcement-email.hbs
@@ -1,6 +1,6 @@
 <div class="announcement-email" role="banner" id="announcement-email">
   <div class="row">
-    <form data-netlify="true" data-netlify-recaptcha="true" name="announcementEmail" method="POST" netlify-honeypot="bot-field" onSubmit="handleSubmitEmail(event)">
+    <form data-netlify="true" name="announcementEmail" method="POST" netlify-honeypot="bot-field" onSubmit="handleSubmitEmail(event)">
       <input type="hidden" name="form-name" value="announcementEmail">
       <p class="hidden">
         <label class="hidden">Beep-Boop. Bot-field <input name="bot-field"></label>
@@ -10,7 +10,6 @@
         <div>
           <input type="email" name="email" id="email" placeholder="Email" class="email-input" required><input type="submit" class="submit-announcement-button-email"></input>
         </div>
-        <div data-netlify-recaptcha="true"></div>
         <button id="close-announcement-email-footer" type="button" aria-label="close" class="close-announcement-email-footer">
           <svg viewBox="0 0 15 15" width="14" height="14">
             <g stroke="currentColor" stroke-width="2.4">
diff --git a/src/partials/article.hbs b/src/partials/article.hbs
index 7449ea63..0e333810 100644
--- a/src/partials/article.hbs
+++ b/src/partials/article.hbs
@@ -1,4 +1,5 @@
 <article class="doc {{#if page.attributes.no-toc}}no-toc{{/if}}">
+  {{> feedback-success}}
 {{#if (ne page.attributes.role 'home')}}
 {{> breadcrumbs}}
 {{/if}}
diff --git a/src/partials/bloblang-signup-form.hbs b/src/partials/bloblang-signup-form.hbs
index 49d252b9..a218e4a3 100644
--- a/src/partials/bloblang-signup-form.hbs
+++ b/src/partials/bloblang-signup-form.hbs
@@ -1,10 +1,9 @@
 <div class="banner-container" id="bloblang-banner">
   <button class="close" id="dismiss-banner" aria-label="Dismiss">&times;</button>
   <p>Sign up for updates about new features in the playground:</p>
-  <form  data-netlify="true" data-netlify-recaptcha="true" name="bloblang-emails" method="POST" netlify-honeypot="bot-field" id="bloblang-form" class="banner-form">
+  <form  data-netlify="true" name="bloblang-emails" method="POST" netlify-honeypot="bot-field" id="bloblang-form" class="banner-form">
     <input type="email" name="email" id="bloblang-email" placeholder="Enter your email..." required />
     <button class="button" type="submit">Sign up</button>
-    <div data-netlify-recaptcha="true"></div>
   </form>
 </div>
 <script>
diff --git a/src/partials/feedback-form-script.hbs b/src/partials/feedback-form-script.hbs
index c5f9752a..d5235a67 100644
--- a/src/partials/feedback-form-script.hbs
+++ b/src/partials/feedback-form-script.hbs
@@ -1,77 +1,120 @@
 <script>
-const thumbs = document.querySelectorAll('.thumb');
-const positiveModal = document.getElementById('positive');
-const negativeModal = document.getElementById('negative');
-var currentForm;
-var positive;
+;
+(() => {
+  const SELECTORS = {
+    form: '#feedbackForm',
+    thumbs: '.thumb',
+    feedbackDetails: '#feedback-details',
+    feedbackOptions: '#feedback-options',
+    feedbackPrompt: '#feedback-prompt',
+    submitButton: '#submitButton',
+    captchaHint: '#captchaHint',
+    captchaField: 'textarea[name="g-recaptcha-response"]',
+    toast: '#feedback-toast',
+    modalOverlay: '#modal-overlay',
+    thumbsToc: '#thumbs-toc'
+  };
 
-thumbs.forEach(function(thumb) {
-  thumb.addEventListener('click', function() {
-    if (thumb.id.indexOf('up') !== -1) {
-      positiveModal.classList.toggle("hidden");
-      currentForm = positiveModal
-      positive = true;
-    } else if (thumb.id.indexOf('down') !== -1) {
-      negativeModal.classList.toggle("hidden");
-      currentForm = negativeModal
-      positive = false;
-    }
-  });
-});
+  const POLL_INTERVAL = 300;            // ms between captcha checks
+  const POLL_TIMEOUT = 5 * 60 * 1000;   // stop polling after 5 minutes
+  const SCROLL_HIDE_OFFSET = 700;       // px before bottom to hide thumbs
 
-const handleSubmit = function (event) {
-  event.preventDefault();
-  const currentUrl = '{{{ or site.url siteRootPath }}}{{{ page.url }}}'
-  const beta = {{ is-prerelease page }}
-  var version = '';
-  {{#if page.attributes.version }}
-  version = {{{ page.attributes.version }}}
-  {{/if}}
+  let positive = null;
 
-  const form = event.target;
-  var formData = new FormData(form);
+  document.addEventListener('DOMContentLoaded', () => {
+    const form = document.querySelector(SELECTORS.form);
+    if (!form) return;
+    const thumbs = document.querySelectorAll(SELECTORS.thumbs);
+    const details = document.querySelector(SELECTORS.feedbackDetails);
+    const options = document.querySelector(SELECTORS.feedbackOptions);
+    const prompt = document.querySelector(SELECTORS.feedbackPrompt);
+    const submitBtn = document.querySelector(SELECTORS.submitButton);
+    const hint = document.querySelector(SELECTORS.captchaHint);
+    const toast = document.querySelector(SELECTORS.toast);
 
-  formData.set('version', version)
-  formData.set('url', currentUrl)
-  formData.set('positiveFeedback', positive);
-  formData.set('beta', beta)
+    // Hide thumbs when scrolling to the bottom
+    document.addEventListener(
+      'scroll',
+      event => requestAnimationFrame(() => {
+        const toc = document.querySelector(SELECTORS.thumbsToc);
+        if (!toc) return;
+        const docH = document.body.scrollHeight;
+        const scrollPos = window.scrollY + window.innerHeight;
+        toc.style.display = (scrollPos + SCROLL_HIDE_OFFSET > docH) ? 'none' : 'block';
+      }),
+      { passive: true }
+    );
 
-  const formFields = event.target.closest('form')
-  const feedback = formFields.querySelector('input[name="feedback"]:checked').value
+    thumbs.forEach(thumb => {
+      thumb.addEventListener('click', () => {
+        positive = thumb.id.includes('up');
+        form.elements['positiveFeedback'].value = positive;
 
-  formData.set('feedback', feedback)
-  formData.set('otherText', (form.querySelector('textarea[name="otherText"]').value))
+        // Safely build radio options
+        const items = positive
+          ? [ 'Solved my problem', 'Easy to understand', 'Other' ]
+          : [ 'Not helpful', 'Too complex', 'Other' ];
+        options.innerHTML = items.map((txt, i) =>
+          `<label><input type="radio" name="feedback" value="${txt}"${i===0? ' checked':''}> ${txt}</label>`
+        ).join('');
 
-  formData.set('email', (form.querySelector('input[name="email"]').value))
+        prompt.textContent = positive
+          ? 'Let us know what we do well:'
+          : 'Let us know what could be improved:';
 
-  formData.set('date', new Date())
-  const nav = window.navigator
-  const navigatorString =
-      nav.userAgent + ', ' + nav.language
+        details.classList.remove('hidden');
+        form.classList.remove('hidden');
+      });
+    });
 
-  formData.set('navigator', navigatorString)
+    form.addEventListener('submit', () => {
+      const version = form.dataset.version || '';
+      const beta = form.dataset.beta === 'true';
+      form.elements['url'].value = window.location.href;
+      form.elements['positiveFeedback'].value = positive;
+      form.elements['version'].value = version;
+      form.elements['beta'].value = beta;
+      form.elements['date'].value = new Date().toISOString();
+      form.elements['navigator'].value = `${navigator.userAgent}, ${navigator.language}`;
+      // Store feedback type
+      sessionStorage.setItem('feedbackType', positive ? 'positive' : 'negative');
+    });
 
-  fetch("{{page.url}}", {
-    method: "POST",
-    headers: { "Content-Type": "application/x-www-form-urlencoded" },
-    body: new URLSearchParams(formData).toString(),
-  }).then(() => {
-      console.log('Form submission success')
-      clearFeedbackModal()
-    })
-    .catch(error => {
-      console.error('Form submission error:', error)
-    })
-}
-function clearFeedbackModal() {
-  var modalBody = currentForm.querySelector('.feedback-modal-body');
-  modalBody.innerHTML = `
-  {{> feedback-success-modal}}
-  `;
-}
+    window.closeForm = event => {
+      event.preventDefault();
+      form.classList.add('hidden');
+    };
+
+    if (submitBtn && hint) {
+      submitBtn.disabled = true;
+      hint.style.display = 'block';
+      const start = Date.now();
+      // Enable the submit button when the reCAPTCHA token is available
+      // https://answers.netlify.com/t/recaptcha-integration-in-forms-configuration/6181/3
+      const intervalId = setInterval(() => {
+        const captcha = document.querySelector(SELECTORS.captchaField);
+        if (captcha && captcha.value.trim() !== '') {
+          submitBtn.disabled = false;
+          hint.style.display = 'none';
+          clearInterval(intervalId);
+        }
+        if (Date.now() - start > POLL_TIMEOUT) {
+          clearInterval(intervalId);
+          console.warn('reCAPTCHA token polling timed out');
+        }
+      }, POLL_INTERVAL);
+    }
 
-function closeForm(event) {
-  event.preventDefault()
-  currentForm.classList.toggle("hidden");
-}
-</script>
\ No newline at end of file
+    if (toast && window.location.search.includes('success')) {
+      const modal = document.querySelector(SELECTORS.form);
+      modal.classList.add('hidden');
+      toast.classList.remove('hidden');
+      setTimeout(() => toast.classList.add('hidden'), 5000);
+
+      const url = new URL(window.location.href);
+      url.searchParams.delete('success');
+      window.history.replaceState({}, document.title, url);
+    }
+  });
+})();
+</script>
diff --git a/src/partials/feedback-forms.hbs b/src/partials/feedback-forms.hbs
index 5857e59b..f6252677 100644
--- a/src/partials/feedback-forms.hbs
+++ b/src/partials/feedback-forms.hbs
@@ -1,94 +1,46 @@
-<form id="positive" class="hidden" data-netlify-recaptcha="true" data-netlify="true" name="feedbackForm" method="POST" netlify-honeypot="bot-field" onSubmit="handleSubmit(event)">
+<form id="feedbackForm" class="hidden" name="feedbackForm" method="POST" netlify-honeypot="bot-field" data-netlify="true" data-netlify-recaptcha="true" action="?success=true">
+
+  <!-- Honeypot -->
+  <p class="hidden">
+    <label>Beep-Boop. Bot-field: <input name="bot-field"></label>
+  </p>
+
+  <!-- Hidden metadata -->
+  <input type="hidden" name="version">
+  <input type="hidden" name="url">
+  <input type="hidden" name="positiveFeedback">
+  <input type="hidden" name="beta">
+  <input type="hidden" name="date">
+  <input type="hidden" name="navigator">
+
   <div class="feedback-modal">
     <div class="feedback-modal-body">
-      <h4>What do you like about this doc?</h4>
-      <div>
-        <div class="radio-buttons form-field">
-          <input type="hidden" name="form-name" value="feedbackForm">
-          <p class="hidden">
-            <label class="hidden">Beep-Boop. Bot-field <input name="bot-field"></label>
-          </p>
-          <input class="hidden" name="version">
-          <input class="hidden" name="url">
-          <input class="hidden" name="positiveFeedback">
-          <input class="hidden" name="beta">
-          <input class="hidden" name="date">
-          <input class="hidden" name="navigator">
-          <input class="hidden" name="feedback">
-          <label>
-            <input type="radio" name="feedback" id="solvedProblem" value="Solved my problem" checked="">
-            <span class="radio-label">Solved my problem</span>
-          </label>
-          <label>
-            <input type="radio" name="feedback" id="easyToUnderstand" value="Easy to understand">
-            <span class="radio-label">Easy to understand</span>
-          </label>
-          <label>
-            <input type="radio" name="feedback" id="other" value="other">
-            <span class="radio-label">Other</span>
-          </label>
-        </div>
+      <h4>What do you think of this page?</h4>
+
+      <div id="feedback-details" class="hidden">
+        <div class="radio-buttons form-field" id="feedback-options"></div>
+
         <div class="form-field">
-          <h4>Let us know what we do well:</h4>
-          <textarea id="otherText" name="otherText" rows="4" cols="50" placeholder="Please share details or suggestions for this topic."></textarea>
+          <h4><span id="feedback-prompt">Let us know more:</span></h4>
+          <textarea name="otherText" rows="4" cols="50" placeholder="Please share details or suggestions."></textarea>
         </div>
+
         <div class="form-field">
           <h4>Let us contact you about your feedback:</h4>
-          <input type="text" name="email" id="email" placeholder="email@example.com">
+          <input type="text" name="email" placeholder="email@example.com">
         </div>
+
         <div data-netlify-recaptcha="true"></div>
-      </div>
-      <div class="form-field wrapper">
-        <button type="submit" class="submitButton">Submit</button>
-      </div>
-    </div>
-  </div>
-  <div class="modal-overlay" id="modal-overlay">
-  </div>
-</form>
-<form id="negative" class="hidden" data-netlify="true" name="feedbackForm" method="POST" netlify-honeypot="bot-field" onSubmit="handleSubmit(event)">
-  <div class="feedback-modal">
-    <div class="feedback-modal-body">
-      <h4>What did you not like about this doc?</h4>
-      <div>
-        <div class="radio-buttons form-field">
-          <input type="hidden" name="form-name" value="feedbackForm">
-          <p class="hidden">
-            <label class="hidden">Beep-Boop. Bot-field <input name="bot-field"></label>
-          </p>
-          <input class="hidden" name="version">
-          <input class="hidden" name="url">
-          <input class="hidden" name="feedback">
-          <input class="hidden" name="beta">
-          <input class="hidden" name="date">
-          <input class="hidden" name="navigator">
-          <label>
-            <input type="radio" name="feedback" id="solvedProblem" value="Did not solve my problem" checked="">
-            <span class="radio-label">Did not solve my problem</span>
-          </label>
-          <label>
-            <input type="radio" name="feedback" id="easyToUnderstand" value="Hard to understand">
-            <span class="radio-label">Hard to understand</span>
-          </label>
-          <label>
-            <input type="radio" name="feedback" id="other" value="other">
-            <span class="radio-label">Other</span>
-          </label>
-        </div>
-        <div class="form-field">
-          <h4>Let us know what we can improve:</h4>
-          <textarea id="otherText" name="otherText" rows="4" cols="50" placeholder="Please share details or suggestions for this topic."></textarea>
-        </div>
-        <div class="form-field">
-          <h4>Let us contact you about your feedback:</h4>
-          <input type="text" name="email" id="email" placeholder="email@example.com">
+
+        <div class="form-field wrapper">
+          <button type="submit" id="submitButton" class="submitButton">Submit</button>
         </div>
-      </div>
-      <div class="form-field wrapper">
-        <button type="submit" class="submitButton">Submit</button>
+        <p id="captchaHint" class="hint" style="display:none;" aria-describedby="captchaHint" aria-live="polite">
+          Please complete the “I'm not a robot” checkbox to submit.
+        </p>
       </div>
     </div>
   </div>
-  <div class="modal-overlay" id="modal-overlay">
-  </div>
+
+  <div class="modal-overlay" id="modal-overlay"></div>
 </form>
diff --git a/src/partials/feedback-success-modal.hbs b/src/partials/feedback-success-modal.hbs
deleted file mode 100644
index b9d5aeea..00000000
--- a/src/partials/feedback-success-modal.hbs
+++ /dev/null
@@ -1,8 +0,0 @@
-<div class="form-field wrapper">
-  <span class="material-icons MuiIcon-root" aria-hidden="true" style="color: green;overflow: hidden;width: 1em;height: 1em;font-size: 4em;">checkmark</span>
-  <h4>Thank you for your feedback!</h4>
-  <p>We appreciate your input and will use it to improve our documentation.</p>
-  <div class="form-field wrapper">
-    <button onclick="closeForm(event)" class="button close">Close</button>
-  </div>
-</div>
diff --git a/src/partials/feedback-success.hbs b/src/partials/feedback-success.hbs
new file mode 100644
index 00000000..49a5e65e
--- /dev/null
+++ b/src/partials/feedback-success.hbs
@@ -0,0 +1,3 @@
+<div id="feedback-toast" class="feedback-toast hidden" role="status" aria-live="polite">
+  🎉 Thanks for your feedback!
+</div>
\ No newline at end of file
diff --git a/src/partials/thumbs.hbs b/src/partials/thumbs.hbs
index b2e6905f..9da82a20 100644
--- a/src/partials/thumbs.hbs
+++ b/src/partials/thumbs.hbs
@@ -9,6 +9,7 @@
     </button>
   </div>
 </div>
+
 <script>
 document.addEventListener('scroll', function(e){
   let documentHeight = document.body.scrollHeight;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/partials/feedback-form-script.hbs (3)</summary><blockquote>

`22-33`: **Deriving sentiment from the element’s id is brittle**

`positive = thumb.id.includes('up');` assumes the id naming scheme never changes and that no other “up” string sneaks in. A `data-*` attribute or a dedicated class is less error-prone and self-documenting.

```diff
-    positive = thumb.id.includes('up');
+    // <button class="thumb" data-positive="true" …>
+    positive = thumb.dataset.positive === 'true';
```

This also avoids confusing string searches on ids and makes unit-testing simpler.  
 <!-- review_comment_end -->

---

`27-37`: **Large template literals hinder maintainability & localisation**

Embedding full HTML fragments directly in JS makes future edits and translations harder. Consider:

1. Moving the fragments to separate `<template>` tags in the markup and cloning them.
2. Or defining the option lists in a JSON structure and generating the radios programmatically.

Both approaches isolate content from logic and play nicer with i18n tooling.  
 <!-- review_comment_end -->

---

`66-69`: **`closeForm` fails when called programmatically**

`closeForm` expects an `event` argument and immediately dereferences it.  
If you invoke it from code (`closeForm()`) you’ll hit `Cannot read property 'preventDefault' of undefined`.

```diff
-function closeForm(event) {
-  event.preventDefault();
+function closeForm(event) {
+  if (event) event.preventDefault();
```

Minor, but strengthens the API.  
 <!-- review_comment_end -->

<!-- file_end -->

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between aaf4ab962b3e22a90dc0539f86ea24963e325acc and 8ecfe48d2ccbfbf0d8e16e0de2d7103fe387bf72.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/css/feedback-form.css` (2 hunks)
* `src/partials/article.hbs` (1 hunks)
* `src/partials/feedback-form-script.hbs` (1 hunks)
* `src/partials/feedback-forms.hbs` (1 hunks)
* `src/partials/thumbs.hbs` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>src/partials/thumbs.hbs (1)</summary>

`12-12`: **Good whitespace improvement.**

Adding a blank line between the HTML markup and script improves code readability.

</details>
<details>
<summary>src/partials/article.hbs (1)</summary>

`2-4`: **Well-implemented toast notification with good accessibility.**

The toast notification is correctly implemented with:
- Appropriate aria attributes (`role="status"` and `aria-live="polite"`)
- Hidden state by default (via the "hidden" class)
- Clear success message

This ensures screen readers will announce the feedback confirmation when it appears.

</details>
<details>
<summary>src/css/feedback-form.css (2)</summary>

`25-39`: **Well-structured toast notification styling.**

The toast styling follows good practices with:
- Clear visual design (border, colors, spacing)
- Use of CSS variables for theming consistency
- Appropriate maximum width
- Proper hidden state implementation

This creates a clean, accessible notification for users.

---

`64-64`: **Good spacing improvement for radio buttons.**

Adding vertical spacing between radio buttons improves usability and readability.

</details>
<details>
<summary>src/partials/feedback-forms.hbs (6)</summary>

`1-2`: **Correctly implemented native Netlify form submission.**

The form properly includes:
- Method="POST" for native form submission
- data-netlify="true" and data-netlify-recaptcha="true" attributes
- Action with success parameter for handling the post-submission state
- Hidden form-name input required by Netlify

This implementation successfully enables Netlify's built-in reCAPTCHA and spam protection.

---

`4-7`: **Proper honeypot implementation for bot detection.**

The honeypot field is correctly implemented with:
- Hidden class to make it invisible to users
- Clear naming ("bot-field") as required by Netlify
- Proper label for accessibility

This helps prevent spam submissions without affecting real users.

---

`9-15`: **Well-structured metadata collection.**

The hidden fields for collecting metadata are organized at the top level of the form, making the structure cleaner and easier to maintain. This data will provide valuable context for the feedback.

---

`19-27`: **Good dynamic content structure.**

The form content has been refactored to:
- Use a generic heading that works for both positive and negative feedback
- Include an empty container for dynamically populated options
- Have a generalized prompt for additional feedback

This supports the consolidated feedback approach while maintaining flexibility.

---

`36-38`: **Proper button placement and styling.**

The submit button is correctly wrapped in a container with appropriate styling classes, maintaining consistency with the design system.

---

`43-43`: **Simplified modal overlay.**

The modal overlay has been simplified to a single empty div with an ID, which is sufficient for its purpose of creating a backdrop for the modal dialog.

</details>
<details>
<summary>src/partials/feedback-form-script.hbs (1)</summary>

`48-61`: **Re-evaluate sending full navigator string – possible PII**

`navigator.userAgent` leaks detailed browser & device info that isn’t strictly required for documentation feedback and may be considered PII under some regulations.  
If you only need coarse metrics (browser family / major version, locale), sanitise or hash the UA before submitting.

```diff
-  const nav = navigator.userAgent + ', ' + navigator.language;
+  // Example: keep only the first product token and locale
+  const nav = `${navigator.userAgent.split(' ')[0]}, ${navigator.language}`;
```

Confirm with your privacy policy before merging.  
 <!-- review_comment_end -->

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T12:28:26Z
user: login: Feediver1
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:22:41Z

Review comments:
diff_hunk: @@ -1,77 +1,103 @@
 <script>
+const form = document.getElementById('feedbackForm');
 const thumbs = document.querySelectorAll('.thumb');
-const positiveModal = document.getElementById('positive');
-const negativeModal = document.getElementById('negative');
-var currentForm;
-var positive;
-
-thumbs.forEach(function(thumb) {
-  thumb.addEventListener('click', function() {
-    if (thumb.id.indexOf('up') !== -1) {
-      positiveModal.classList.toggle("hidden");
-      currentForm = positiveModal
-      positive = true;
-    } else if (thumb.id.indexOf('down') !== -1) {
-      negativeModal.classList.toggle("hidden");
-      currentForm = negativeModal
-      positive = false;
-    }
+const feedbackDetails = document.getElementById('feedback-details');
+const feedbackOptions = document.getElementById('feedback-options');
+const feedbackPrompt = document.getElementById('feedback-prompt');
+let positive = null;
+
+// Scroll visibility toggle for thumbs
+document.addEventListener('scroll', function() {
+  const feedback = document.getElementById('thumbs-toc');
+  if (!feedback) return;
+
+  const modifier = 700;
+  const documentHeight = document.body.scrollHeight;
+  const currentScroll = window.scrollY + window.innerHeight;
+
+  feedback.style.display = (currentScroll + modifier > documentHeight) ? 'none' : 'block';
+});
+
path: src/partials/feedback-form-script.hbs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Throttle / make scroll listener passive to avoid jank**

The listener fires on every scroll tick and may trigger layout/re-calculation because it touches `document.body.scrollHeight` and mutates style.  
On long articles this can add noticeable jank.

```diff
-document.addEventListener('scroll', function() {
+// `passive` signals we never call preventDefault, letting the browser optimise.
+// `requestAnimationFrame` throttles DOM writes to one per frame.
+document.addEventListener(
+  'scroll',
+  () => requestAnimationFrame(() => {
     const feedback = document.getElementById('thumbs-toc');
     if (!feedback) return;
@@
-  feedback.style.display = (currentScroll + modifier > documentHeight) ? 'none' : 'block';
-});
+    feedback.style.display =
+      currentScroll + modifier > documentHeight ? 'none' : 'block';
+  }),
+  { passive: true }
+);
```

If you want a cleaner solution, an `IntersectionObserver` watching a sentinel at the page bottom would remove manual maths altogether.  
 <!-- review_comment_end -->



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// Scroll visibility toggle for thumbs
// `passive` signals we never call preventDefault, letting the browser optimise.
// `requestAnimationFrame` throttles DOM writes to one per frame.
document.addEventListener(
  'scroll',
  () => requestAnimationFrame(() => {
    const feedback = document.getElementById('thumbs-toc');
    if (!feedback) return;

    const modifier = 700;
    const documentHeight = document.body.scrollHeight;
    const currentScroll = window.scrollY + window.innerHeight;

    feedback.style.display =
      currentScroll + modifier > documentHeight ? 'none' : 'block';
  }),
  { passive: true }
);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T12:28:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,77 +1,103 @@
 <script>
+const form = document.getElementById('feedbackForm');
 const thumbs = document.querySelectorAll('.thumb');
-const positiveModal = document.getElementById('positive');
-const negativeModal = document.getElementById('negative');
-var currentForm;
-var positive;
-
-thumbs.forEach(function(thumb) {
-  thumb.addEventListener('click', function() {
-    if (thumb.id.indexOf('up') !== -1) {
-      positiveModal.classList.toggle("hidden");
-      currentForm = positiveModal
-      positive = true;
-    } else if (thumb.id.indexOf('down') !== -1) {
-      negativeModal.classList.toggle("hidden");
-      currentForm = negativeModal
-      positive = false;
-    }
+const feedbackDetails = document.getElementById('feedback-details');
+const feedbackOptions = document.getElementById('feedback-options');
+const feedbackPrompt = document.getElementById('feedback-prompt');
+let positive = null;
+
+// Scroll visibility toggle for thumbs
+document.addEventListener('scroll', function() {
+  const feedback = document.getElementById('thumbs-toc');
+  if (!feedback) return;
+
+  const modifier = 700;
+  const documentHeight = document.body.scrollHeight;
+  const currentScroll = window.scrollY + window.innerHeight;
+
+  feedback.style.display = (currentScroll + modifier > documentHeight) ? 'none' : 'block';
+});
+
+// Handle thumbs click
+thumbs.forEach(thumb => {
+  thumb.addEventListener('click', () => {
+    positive = thumb.id.includes('up');
+    form.querySelector('input[name="positiveFeedback"]').value = positive;
+
+    feedbackOptions.innerHTML = positive
+      ? `
+        <label><input type="radio" name="feedback" value="Solved my problem" checked> Solved my problem</label>
+        <label><input type="radio" name="feedback" value="Easy to understand"> Easy to understand</label>
+        <label><input type="radio" name="feedback" value="other"> Other</label>
+      `
+      : `
+        <label><input type="radio" name="feedback" value="Not helpful" checked> Not helpful</label>
+        <label><input type="radio" name="feedback" value="Too complex"> Too complex</label>
+        <label><input type="radio" name="feedback" value="other"> Other</label>
+      `;
+
+    feedbackPrompt.textContent = positive
+      ? "Let us know what we do well:"
+      : "Let us know what could be improved:";
+
+    feedbackDetails.classList.remove('hidden');
+    form.classList.remove('hidden');
   });
 });
 
-const handleSubmit = function (event) {
+// Fill hidden metadata
+form.addEventListener('submit', () => {
+  const version = "{{{ page.attributes.version }}}" || "";
+  const beta = {{ is-prerelease page }};
+  const url = window.location.href;
path: src/partials/feedback-form-script.hbs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Guard against undefined `is-prerelease` value**

If `{{ is-prerelease page }}` ever renders blank, the line becomes `const beta = ;` and throws a SyntaxError halting the entire script.

```diff
-  const beta = {{ is-prerelease page }};
+  const beta = Boolean({{ is-prerelease page }} || false);
```

A simple Boolean cast guarantees valid JS even when the template helper returns nothing.  
 <!-- review_comment_end -->



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T12:28:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-05-09T12:20:46Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *docs-ui* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | eeb7d6ac6bd862a8182e603c43e5150cf8e595ff |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-ui/deploys/681e34334ce8110008e57f24 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-285--docs-ui.netlify.app](https://deploy-preview-285--docs-ui.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTI4NS0tZG9jcy11aS5uZXRsaWZ5LmFwcCJ9.Zldfj91WWq_04q9AyjfixuoMkOes_bVfTG_oStBZyqI)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [docs-ui Preview](https://deploy-preview-285--docs-ui.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/docs-ui/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-09T12:20:51Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto incremental reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

The changes consolidate the feedback mechanism on the page by merging separate positive and negative feedback forms into a single, dynamically populated form. The form's content and options are now updated in real-time based on user interaction (thumbs up or down). Additional hidden fields are introduced for metadata collection, and feedback type is tracked using sessionStorage. The submission flow is simplified, removing manual fetch logic and relying on standard form submission with success handling via a toast notification. CSS styles are added for the toast notification and improved spacing in the feedback modal. Accessibility enhancements are included for the toast element.

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant ThumbsNav
    participant FeedbackForm
    participant SessionStorage
    participant Toast

    User->>ThumbsNav: Clicks thumbs up/down
    ThumbsNav->>FeedbackForm: Show form with dynamic options
    User->>FeedbackForm: Fills form and submits
    FeedbackForm->>SessionStorage: Store feedback type
    FeedbackForm->>FeedbackForm: Populate hidden metadata fields
    FeedbackForm->>FeedbackForm: Submit form (standard POST)
    FeedbackForm-->>User: Modal closes
    FeedbackForm->>Toast: Show "Thanks for your feedback!" toast
    Toast-->>User: Toast auto-hides after delay
```

## Suggested reviewers

- Feediver1

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=redpanda-data/docs-ui&utm_content=285):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR