惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
O
OpenAI News
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
云风的 BLOG
云风的 BLOG
罗磊的独立博客
S
SegmentFault 最新的问题
The Register - Security
The Register - Security
Hugging Face - Blog
Hugging Face - Blog
D
DataBreaches.Net
U
Unit 42
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
雷峰网
雷峰网
V
Visual Studio Blog
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
博客园 - 【当耐特】
G
Google Developers Blog
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
I
InfoQ
Martin Fowler
Martin Fowler
F
Fortinet All Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
AI
AI
Google Online Security Blog
Google Online Security Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
爱范儿
爱范儿
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org

The Nuxt Blog

Meet Nuxi · Nuxt Blog Introducing the Nuxt Agent · Nuxt Blog Nuxt 4.4 · Nuxt Blog Nuxt 4.3 · Nuxt Blog Building an MCP Server for Nuxt · Nuxt Blog Nuxt Image v2 · Nuxt Blog Nuxt 4.2 · Nuxt Blog Nuxt UI v4 · Nuxt Blog Nuxt 4.1 · Nuxt Blog Nuxt 3.18 · Nuxt Blog Announcing Nuxt 4.0 · Nuxt Blog Roadmap to v4 · Nuxt Blog Nuxt 3.17 · Nuxt Blog Nuxt UI v3 · Nuxt Blog Nuxt 3.16 · Nuxt Blog Nuxt 3.15 · Nuxt Blog Introducing Nuxt Icon v1 · Nuxt Blog Nuxt 3.14 · Nuxt Blog Nuxt 3.13 · Nuxt Blog Introducing Nuxt Scripts · Nuxt Blog Nuxt 2 End-of-Life (EOL) · Nuxt Blog Nuxt 3.12 · Nuxt Blog Refreshed Nuxt ESLint Integrations · Nuxt Blog Nuxt: Looking forward · Nuxt Blog Nuxt 3.11 · Nuxt Blog The Evolution of Shiki v1.0 · Nuxt Blog Nuxt 3.10 · Nuxt Blog Nuxt 3.9 · Nuxt Blog Nuxt DevTools v1.0 · Nuxt Blog Nuxt 3.8 · Nuxt Blog A New Website · Nuxt Blog Nuxt 3.7 · Nuxt Blog Nuxt on the Edge · Nuxt Blog Nuxt 3.6 · Nuxt Blog Nuxt 3.5 · Nuxt Blog Nuxt 3.4 · Nuxt Blog Introducing Nuxt DevTools · Nuxt Blog Nuxt 3.3 · Nuxt Blog Nuxt: A vision for 2023 · Nuxt Blog Announcing 3.0 · Nuxt Blog Announcing Nuxt 3 Release Candidate · Nuxt Blog Introducing Nuxt 3 Beta · Nuxt Blog Nuxt 2 Static Improvements · Nuxt Blog Going Full Static · Nuxt Blog Understanding how fetch works in Nuxt 2.12 · Nuxt Blog Nuxt 2: From Terminal to Browser · Nuxt Blog Introducing Smart Prefetching · Nuxt Blog Navigation · Nuxt Blog
Building a Privacy-First Feedback Widget · Nuxt Blog
2025-06-13 · via The Nuxt Blog

Documentation is at the heart of the Nuxt developer experience. To continuously improve it, we needed a simple and effective way to collect user feedback directly on each page. Here's how we designed and implemented our feedback widget, drawing inspiration from Plausible's privacy-first approach.

Currently, users can provide feedback on our documentation by creating GitHub issues or contacting us directly. While these channels are valuable and remain important, they require users to leave their current context and take several steps to share their thoughts.

We wanted something different:

  • Contextual: Directly integrated into each documentation page
  • Frictionless: Maximum 2 clicks to provide feedback
  • Privacy-respecting: No personal tracking, GDPR compliant by design

Technical architecture

Our solution consists of three main components:

1. Frontend with Motion animations

The interface combines Vue 3's Composition API with Motion for Vue to create an engaging user experience. The widget uses layout animations for smooth state transitions and spring physics for natural feedback. The useFeedback composable handles all state management and automatically resets when users navigate between pages.

Here's the success state animation, for example:

<template>
  <!-- ... -->
  <motion.div
    v-if="isSubmitted"
    key="success"
    :initial="{ opacity: 0, scale: 0.95 }"
    :animate="{ opacity: 1, scale: 1 }"
    :transition="{ duration: 0.3 }"
    class="flex items-center gap-3 py-2"
    role="status"
    aria-live="polite"
    aria-label="Feedback submitted successfully"
  >
    <motion.div
      :initial="{ scale: 0 }"
      :animate="{ scale: 1 }"
      :transition="{ delay: 0.1, type: 'spring', visualDuration: 0.4 }"
      class="text-xl"
      aria-hidden="true"
    >
    </motion.div>
    <motion.div
      :initial="{ opacity: 0, x: 10 }"
      :animate="{ opacity: 1, x: 0 }"
      :transition="{ delay: 0.2, duration: 0.3 }"
    >
      <div class="text-sm font-medium text-highlighted">
        Thank you for your feedback!
      </div>
      <div class="text-xs text-muted mt-1">
        Your input helps us improve the documentation.
      </div>
    </motion.div>
  </motion.div>
  <!-- ... -->
</template>

You can find the source code of the feedback widget here.

2. Plausible-inspired anonymization

The challenge was detecting duplicates (a user changing their mind) while preserving privacy. We took inspiration from Plausible's approach to counting unique visitors without cookies.

export async function generateHash(
  today: string,
  ip: string,
  domain: string,
  userAgent: string
): Promise<string> {
  const data = `${today}+${domain}+${ip}+${userAgent}`

  const buffer = await crypto.subtle.digest(
    'SHA-1',
    new TextEncoder().encode(data)
  )

  return [...new Uint8Array(buffer)]
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

This method generates a unique daily identifier by combining:

  • IP + User-Agent: Naturally sent with every HTTP request
  • Domain: Enables environment isolation
  • Current date: Forces daily rotation of identifiers

Why is this secure?

  • IP and User-Agent are never stored in the database
  • The hash changes daily, preventing long-term tracking
  • Very difficult to reverse engineer original data from the hash
  • GDPR compliant by design (no persistent personal data)

3. Database persistence with conflict handling

First, we define the schema for the feedback table and add a unique constraint on the path and fingerprint columns.

export const feedback = sqliteTable('feedback', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  rating: text('rating').notNull(),
  feedback: text('feedback'),
  path: text('path').notNull(),
  title: text('title').notNull(),
  stem: text('stem').notNull(),
  country: text('country').notNull(),
  fingerprint: text('fingerprint').notNull(),
  createdAt: integer({ mode: 'timestamp' }).notNull(),
  updatedAt: integer({ mode: 'timestamp' }).notNull()
}, table => [uniqueIndex('path_fingerprint_idx').on(table.path, table.fingerprint)])

Then, in the server, we use Drizzle with an UPSERT strategy:

await drizzle.insert(tables.feedback).values({
  rating: data.rating,
  feedback: data.feedback || null,
  path: data.path,
  title: data.title,
  stem: data.stem,
  country: event.context.cf?.country || 'unknown',
  fingerprint,
  createdAt: new Date(),
  updatedAt: new Date()
}).onConflictDoUpdate({
  target: [tables.feedback.path, tables.feedback.fingerprint],
  set: {
    rating: data.rating,
    feedback: data.feedback || null,
    country,
    updatedAt: new Date()
  }
})

This approach enables updates if the user changes their mind within the day, creation for new feedback, and automatic deduplication per page and user.

You can find the source code of the server side here.

We use Zod for runtime validation and type generation:

export const FEEDBACK_RATINGS = [
  'very-helpful',
  'helpful', 
  'not-helpful',
  'confusing'
] as const

export const feedbackSchema = z.object({
  rating: z.enum(FEEDBACK_RATINGS),
  feedback: z.string().optional(),
  path: z.string(),
  title: z.string(),
  stem: z.string()
})

export type FeedbackInput = z.infer<typeof feedbackSchema>

This approach ensures consistency across frontend, API, and database.

What's next

The widget is now live across all documentation pages. Our next step is building an admin interface within nuxt.com to analyze feedback patterns and identify pages that need improvement. This will help us continuously enhance the documentation quality based on real user feedback.

The complete source code is available on GitHub for inspiration and contributions!