文档是 Nuxt 开发者体验的核心。为了持续改进它,我们需要一种简单有效的方式,直接在每个页面上收集用户反馈。以下是我们如何设计和实现我们的反馈小部件,灵感来源于 Plausible 的隐私优先方法。
目前,用户可以通过创建 GitHub issues 或直接联系我们来提供对我们文档的反馈。虽然这些渠道很有价值并且仍然重要,但它们要求用户离开当前上下文并采取几个步骤来分享他们的想法。
我们想要一些不同的东西
我们的解决方案由三个主要组件组成
该界面结合了 Vue 3 的 Composition API 和Motion for Vue来创建引人入胜的用户体验。该小部件使用布局动画实现平滑的状态过渡,并使用弹簧物理效果实现自然的反馈。useFeedback 可组合项处理所有状态管理,并在用户在页面之间导航时自动重置。
例如,这是成功状态动画
<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>
您可以在这里找到反馈小部件的源代码此处.
挑战在于在保护隐私的同时检测重复(用户改变主意)。我们从Plausible的方法中汲取了灵感在不使用 Cookie 的情况下统计独立访客.
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('')
}
此方法通过组合以下内容生成每日唯一标识符
为什么这很安全?
首先,我们定义反馈表的模式,并在 path 和 fingerprint 列上添加唯一约束。
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)])
然后,在服务器端,我们使用Drizzle采用 UPSERT 策略
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()
}
})
此方法允许用户在一天内改变主意时进行更新,为新反馈创建记录,并自动实现每页和每个用户的重复数据删除。
您可以在这里找到服务器端源代码此处.
我们使用 Zod 进行运行时验证和类型生成
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>
这种方法确保了前端、API 和数据库之间的一致性。
该小部件现已在所有文档页面上线。我们的下一步是在 nuxt.com 中构建一个管理界面,以分析反馈模式并识别需要改进的页面。这将帮助我们根据真实的用户反馈持续提高文档质量。
完整的源代码可在GitHub上找到,以供参考和贡献!