Article Helpfulness Rating Widget
"Was this article helpful?" — the simplest feedback tool for knowledge bases, blogs, and documentation. Helps identify poorly-written content without manual analytics analysis.
Backend
Schema::create('article_ratings', function (Blueprint $table) {
$table->id();
$table->foreignId('article_id')->constrained()->cascadeOnDelete();
$table->boolean('helpful');
$table->text('comment')->nullable();
$table->string('session_id');
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->timestamps();
$table->unique(['article_id', 'session_id']); // One vote per session
});
// ArticleRatingController
public function store(Request $request, Article $article): JsonResponse
{
$request->validate(['helpful' => 'required|boolean', 'comment' => 'nullable|string|max:500']);
ArticleRating::updateOrCreate(
['article_id' => $article->id, 'session_id' => session()->getId()],
['helpful' => $request->helpful, 'comment' => $request->comment, 'user_id' => auth()->id()]
);
return response()->json(['success' => true]);
}
// Aggregate for displaying stats
public function stats(Article $article): JsonResponse
{
return response()->json([
'helpful' => $article->ratings()->where('helpful', true)->count(),
'not_helpful' => $article->ratings()->where('helpful', false)->count(),
]);
}
Frontend
export function ArticleRating({ articleId }: { articleId: number }) {
const [voted, setVoted] = useState<boolean | null>(null);
const [comment, setComment] = useState('');
const [showBox, setShowBox] = useState(false);
const vote = async (helpful: boolean) => {
setVoted(helpful);
if (!helpful) setShowBox(true); // Show comment field for negative votes
await fetch(`/api/articles/${articleId}/rating`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ helpful }),
});
};
const submitComment = async () => {
await fetch(`/api/articles/${articleId}/rating`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ helpful: false, comment }),
});
setShowBox(false);
};
if (voted === true) return <p className="text-sm text-green-600">Glad we could help!</p>;
return (
<div className="border-t pt-6 mt-8">
{voted === null ? (
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">Was this article helpful?</span>
<button onClick={() => vote(true)} className="text-sm px-3 py-1 rounded border hover:bg-green-50">👍 Yes</button>
<button onClick={() => vote(false)} className="text-sm px-3 py-1 rounded border hover:bg-red-50">👎 No</button>
</div>
) : showBox ? (
<div>
<p className="text-sm mb-2">What could be improved?</p>
<textarea value={comment} onChange={e => setComment(e.target.value)}
className="w-full border rounded p-2 text-sm h-24 resize-none" placeholder="Optional..." />
<button onClick={submitComment} className="mt-2 text-sm bg-gray-800 text-white px-4 py-1.5 rounded">
Submit
</button>
</div>
) : null}
</div>
);
}
Timeline
Helpfulness widget with voting and optional comment: 1 working day.







