From 5feadd4c7d71aa6a7304894a2161e8c04856ce88 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:01:55 +0200 Subject: [PATCH 1/3] Add documentation feedback widget --- bin/check-gt-navigation.ts | 1 + i18n/en/ui.json | 16 ++ src/components/Feedback.astro | 287 ++++++++++++++++++++++++++++++ src/components/feedback.client.ts | 115 ++++++++++++ src/layouts/DocsLayout.astro | 8 +- src/lib/ui-strings.server.ts | 16 ++ 6 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 src/components/Feedback.astro create mode 100644 src/components/feedback.client.ts diff --git a/bin/check-gt-navigation.ts b/bin/check-gt-navigation.ts index 02ae330a5..250ad0c18 100644 --- a/bin/check-gt-navigation.ts +++ b/bin/check-gt-navigation.ts @@ -119,6 +119,7 @@ function assertUiStrings(): void { for (const section of [ "account", "actions", + "feedback", "navigation", "search", "status", diff --git a/i18n/en/ui.json b/i18n/en/ui.json index dee4a6990..3d42a3835 100644 --- a/i18n/en/ui.json +++ b/i18n/en/ui.json @@ -16,6 +16,22 @@ "view": "View", "viewMarkdown": "View as Markdown" }, + "feedback": { + "additionalFeedback": "Additional feedback (optional)", + "close": "Close feedback form", + "confusing": "Hard to follow or confusing", + "error": "Feedback could not be sent. Please try again.", + "inaccurate": "Inaccurate, out of date, or doesn't work", + "missingInformation": "Missing information", + "negativeLabel": "This page could be improved", + "negativeQuestion": "Why was it not helpful?", + "other": "Something else", + "positiveLabel": "This page was helpful", + "question": "Was this page helpful?", + "submit": "Submit", + "submitting": "Sending…", + "thankYou": "Thanks for your feedback!" + }, "navigation": { "breadcrumb": "Breadcrumb", "chooseLanguage": "Choose documentation language", diff --git a/src/components/Feedback.astro b/src/components/Feedback.astro new file mode 100644 index 000000000..7c8188fe4 --- /dev/null +++ b/src/components/Feedback.astro @@ -0,0 +1,287 @@ +--- +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { uiStrings } from "@/lib/ui-strings.server"; + +interface Props { + currentLocale?: string; +} + +const { currentLocale } = Astro.props; +const messages = uiStrings(currentLocale).feedback; +--- + +
+

+ {messages.question} +

+ +
+ + + + + + + + +
+
+

{messages.negativeQuestion}

+ +
+ +
+ {messages.negativeQuestion} + + + + +
+ + + + + +

+
+
+
+
+
+ + + + diff --git a/src/components/feedback.client.ts b/src/components/feedback.client.ts new file mode 100644 index 000000000..3d5a4bc6d --- /dev/null +++ b/src/components/feedback.client.ts @@ -0,0 +1,115 @@ +import { mount } from "@cloudflare/nimbus-docs/client"; + +const FEEDBACK_ENDPOINT = "https://sql-clickhouse.clickhouse.com"; +const INSERT_QUERY = "INSERT INTO docs_feedback.feedback FORMAT JSONEachRow"; + +type Sentiment = "Positive" | "Negative"; + +function googleAnalyticsId(): string { + const cookie = document.cookie + .split(";") + .map((part) => part.trim()) + .find((part) => part.startsWith("_ga=")) + ?.slice(4); + const parts = cookie?.split(".").slice(-2); + return parts?.length === 2 ? parts.join("-") : "anonymous"; +} + +function feedbackRow(sentiment: Sentiment, reason = "", comment = "") { + return { + page_url: window.location.href, + date: new Date().toISOString().replace("T", " ").slice(0, 19), + sentiment, + reason, + google_id: googleAnalyticsId(), + comment, + }; +} + +async function sendFeedback(sentiment: Sentiment, reason = "", comment = "") { + const url = new URL(FEEDBACK_ENDPOINT); + url.searchParams.set("query", INSERT_QUERY); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-ndjson", + "x-clickhouse-user": "docs_feedback", + "x-clickhouse-key": "", + }, + body: `${JSON.stringify(feedbackRow(sentiment, reason, comment))}\n`, + }); + + if (!response.ok) { + throw new Error(`Feedback submission failed with status ${response.status}`); + } +} + +mount("[data-ch-feedback]", (root) => { + const positive = root.querySelector("[data-ch-feedback-positive]"); + const negative = root.querySelector("[data-ch-feedback-negative]"); + const form = root.querySelector("[data-ch-feedback-form]"); + const panel = root.querySelector("[data-popover-content]"); + const close = root.querySelector("[data-ch-feedback-close]"); + const question = root.querySelector("[data-ch-feedback-question]"); + const status = root.querySelector("[data-ch-feedback-status]"); + const submit = form?.querySelector("[type=submit]"); + if (!positive || !negative || !form || !panel || !close || !question || !status || !submit) { + return () => {}; + } + + const controller = new AbortController(); + const { signal } = controller; + let submitted = false; + + const select = (button: HTMLButtonElement) => { + for (const candidate of [positive, negative]) { + const selected = candidate === button; + candidate.toggleAttribute("data-selected", selected); + candidate.setAttribute("aria-pressed", String(selected)); + } + }; + + const finish = () => { + submitted = true; + positive.disabled = true; + negative.disabled = true; + question.textContent = root.dataset.thankYouLabel ?? "Thanks for your feedback!"; + if (panel.matches(":popover-open")) panel.hidePopover(); + }; + + const reportError = () => { + status.textContent = root.dataset.errorLabel ?? "Feedback could not be sent. Please try again."; + positive.disabled = false; + negative.disabled = false; + submit.disabled = false; + }; + + positive.addEventListener("click", () => { + if (submitted) return; + select(positive); + positive.disabled = true; + negative.disabled = true; + void sendFeedback("Positive").then(finish, reportError); + }, { signal }); + + negative.addEventListener("click", () => { + if (!submitted) select(negative); + }, { signal }); + + close.addEventListener("click", () => panel.hidePopover(), { signal }); + + form.addEventListener("submit", (event) => { + event.preventDefault(); + if (submitted) return; + const data = new FormData(form); + submit.disabled = true; + status.textContent = root.dataset.submittingLabel ?? "Sending…"; + void sendFeedback( + "Negative", + String(data.get("reason") ?? ""), + String(data.get("comment") ?? ""), + ).then(finish, reportError); + }, { signal }); + + return () => controller.abort(); +}); diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro index 7bfb27fb0..eab6718a4 100644 --- a/src/layouts/DocsLayout.astro +++ b/src/layouts/DocsLayout.astro @@ -18,6 +18,7 @@ import { Pagination } from "@/components/ui/pagination"; import { PageActions } from "@/components/ui/page-actions"; import { Badge } from "@/components/ui/badge"; import ViewSwitcher from "@/components/compat/ViewSwitcher.astro"; +import Feedback from "@/components/Feedback.astro"; import type { DocsPageProps } from "@cloudflare/nimbus-docs/types"; import { getVersionStatus, getVersionAlternates } from "@cloudflare/nimbus-docs"; import { withBase } from "@/lib/base"; @@ -299,16 +300,17 @@ const messages = uiStrings(currentLocale); )} - {!isWide && headings !== false && (hasToc || headings.length > 0) && ( + {!isWide && ( )} diff --git a/src/lib/ui-strings.server.ts b/src/lib/ui-strings.server.ts index a041d1b4f..f3ee16aeb 100644 --- a/src/lib/ui-strings.server.ts +++ b/src/lib/ui-strings.server.ts @@ -26,6 +26,22 @@ export type UiStrings = { view: string; viewMarkdown: string; }; + feedback: { + additionalFeedback: string; + close: string; + confusing: string; + error: string; + inaccurate: string; + missingInformation: string; + negativeLabel: string; + negativeQuestion: string; + other: string; + positiveLabel: string; + question: string; + submit: string; + submitting: string; + thankYou: string; + }; navigation: { breadcrumb: string; chooseLanguage: string; From 226e17b47e31dd17d4d679d56a5c254f015e7d21 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:02:14 +0200 Subject: [PATCH 2/3] Fix feedback submission CORS --- src/components/feedback.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/feedback.client.ts b/src/components/feedback.client.ts index 3d5a4bc6d..f5403ad78 100644 --- a/src/components/feedback.client.ts +++ b/src/components/feedback.client.ts @@ -32,7 +32,7 @@ async function sendFeedback(sentiment: Sentiment, reason = "", comment = "") { const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/x-ndjson", + "Content-Type": "text/plain", "x-clickhouse-user": "docs_feedback", "x-clickhouse-key": "", }, From 2c789f4a2e13f672c6aae6fe1ae4e8a50748d1e1 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:46:12 +0200 Subject: [PATCH 3/3] Address feedback submission review --- src/components/Feedback.astro | 5 ++-- src/components/feedback.client.ts | 42 ++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/components/Feedback.astro b/src/components/Feedback.astro index 7c8188fe4..1f8dc4e4d 100644 --- a/src/components/Feedback.astro +++ b/src/components/Feedback.astro @@ -82,11 +82,12 @@ const messages = uiStrings(currentLocale).feedback; > -

+ +