FAQs Management
TinyKit Pro includes a comprehensive Frequently Asked Questions (FAQ) management system with admin controls for creating, updating, ordering, and publishing ...
TinyKit Pro includes a comprehensive Frequently Asked Questions (FAQ) management system with admin controls for creating, updating, ordering, and publishing FAQs on your public pages.
🎯 Features
Public FAQ Display
- Active FAQs Only: Only published FAQs visible to public users
- Custom Ordering: Display FAQs in administrator-defined order
- Clean Interface: Professional accordion-style FAQ display
- SEO Friendly: Properly structured content for search engines
Admin Management
- Full CRUD Operations: Create, read, update, and delete FAQ entries
- Display Order Control: Drag-and-drop or manual ordering
- Active/Inactive Toggle: Quickly publish or hide FAQs
- Auto-Normalization: Automatic gap elimination in display order
- Timestamp Tracking: Created and updated timestamps for all entries
🏗️ Architecture
Database Schema
faqs: defineTable({
question: v.string(),
answer: v.string(),
displayOrder: v.number(), // Lower number = higher priority
isActive: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_active", ["isActive"])
.index("by_display_order", ["displayOrder"])
.index("by_created_at", ["createdAt"]);Key Design Decisions
- Display Order: Lower numbers appear first (1, 2, 3, ...)
- Active Status: Inactive FAQs hidden from public but retained in database
- Auto-Normalization: Gaps in display order automatically filled when FAQs are deleted
- Simple Structure: No categories or tags for maximum flexibility
🚀 Usage
Display FAQs on Public Pages
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
function FAQSection() {
const faqs = useQuery(api.faqs.public.queries.listActive);
if (!faqs || faqs.length === 0) {
return null; // No FAQs to display
}
return (
<section className="container mx-auto py-20">
<h2 className="text-3xl font-bold text-center mb-12">
Frequently Asked Questions
</h2>
<Accordion type="single" collapsible className="max-w-3xl mx-auto">
{faqs.map((faq, index) => (
<AccordionItem key={faq._id} value={`item-${index}`}>
<AccordionTrigger className="text-left">
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
{faq.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</section>
);
}Admin FAQ Management
import { useMutation, useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { toast } from "sonner";
function FAQAdmin() {
const faqs = useQuery(api.faqs.private.queries.listAll);
const createFAQ = useMutation(api.faqs.private.mutations.create);
const updateFAQ = useMutation(api.faqs.private.mutations.update);
const removeFAQ = useMutation(api.faqs.private.mutations.remove);
const toggleActive = useMutation(api.faqs.private.mutations.toggleActive);
const handleCreate = async (question: string, answer: string) => {
const promise = createFAQ({ question, answer });
toast.promise(promise, {
loading: "Creating FAQ...",
success: "FAQ created successfully!",
error: "Failed to create FAQ",
});
};
const handleToggle = async (id: Id<"faqs">) => {
const promise = toggleActive({ id });
toast.promise(promise, {
loading: "Updating status...",
success: "FAQ status updated!",
error: "Failed to update status",
});
};
const handleDelete = async (id: Id<"faqs">) => {
if (!confirm("Are you sure you want to delete this FAQ?")) return;
const promise = removeFAQ({ id });
toast.promise(promise, {
loading: "Deleting FAQ...",
success: "FAQ deleted successfully!",
error: "Failed to delete FAQ",
});
};
// ... render UI
}⚙️ Admin Functions
Create FAQ
const createFAQ = useMutation(api.faqs.private.mutations.create);
await createFAQ({
question: "What is your refund policy?",
answer: "We offer a 30-day money-back guarantee...",
isActive: true, // Optional, defaults to true
});Update FAQ
const updateFAQ = useMutation(api.faqs.private.mutations.update);
await updateFAQ({
id: faqId,
question: "Updated question text",
answer: "Updated answer text",
isActive: true, // Optional
});Delete FAQ
const removeFAQ = useMutation(api.faqs.private.mutations.remove);
await removeFAQ({ id: faqId });
// Automatically normalizes display order of remaining FAQsToggle Active Status
const toggleActive = useMutation(api.faqs.private.mutations.toggleActive);
await toggleActive({ id: faqId });
// Flips isActive from true to false or vice versaReorder FAQs
const reorderFAQ = useMutation(api.faqs.private.mutations.reorder);
await reorderFAQ({
id: faqId,
newDisplayOrder: 2, // Move to position 2
});
// Automatically adjusts surrounding FAQs📊 Display Order Management
How Display Order Works
- Lower Numbers First: FAQs with
displayOrder: 1appear beforedisplayOrder: 2 - Auto-Assignment: New FAQs get the next available order (highest + 1)
- Auto-Normalization: Deleting FAQ #2 renumbers others to eliminate gaps
- Reordering Logic: Moving FAQs automatically shifts surrounding items
Display Order Examples
// Initial state
FAQ 1: displayOrder = 1
FAQ 2: displayOrder = 2
FAQ 3: displayOrder = 3
// Delete FAQ 2
FAQ 1: displayOrder = 1
FAQ 3: displayOrder = 2 // Automatically renumbered
// Move FAQ 3 to position 1
FAQ 3: displayOrder = 1 // Moved up
FAQ 1: displayOrder = 2 // Shifted down🎨 Implementation Patterns
Landing Page FAQ Section
function LandingPageFAQs() {
const faqs = useQuery(api.faqs.public.queries.listActive);
if (!faqs || faqs.length === 0) {
return null;
}
return (
<section className="bg-muted/50 py-20">
<div className="container mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4">Common Questions</h2>
<p className="text-muted-foreground text-lg">
Everything you need to know about our product
</p>
</div>
<Accordion type="single" collapsible className="max-w-3xl mx-auto">
{faqs.map((faq, index) => (
<AccordionItem
key={faq._id}
value={`faq-${index}`}
className="bg-background border rounded-lg mb-4 px-6"
>
<AccordionTrigger className="text-left hover:no-underline">
<span className="font-semibold">{faq.question}</span>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
<div className="pt-2 pb-4">{faq.answer}</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
);
}Admin FAQ Management Interface
function FAQManagement() {
const faqs = useQuery(api.faqs.private.queries.listAll);
const createFAQ = useMutation(api.faqs.private.mutations.create);
const [isCreating, setIsCreating] = useState(false);
if (!faqs) {
return <LoadingSpinner />;
}
return (
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">FAQ Management</h1>
<Button onClick={() => setIsCreating(true)}>
<Plus className="h-4 w-4 mr-2" />
Add FAQ
</Button>
</div>
<div className="space-y-4">
{faqs.map((faq, index) => (
<Card key={faq._id}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-muted-foreground">
#{faq.displayOrder}
</span>
<Badge variant={faq.isActive ? "default" : "secondary"}>
{faq.isActive ? "Active" : "Inactive"}
</Badge>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleToggle(faq._id)}
>
{faq.isActive ? "Hide" : "Show"}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(faq)}
>
Edit
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(faq._id)}
>
Delete
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<h3 className="font-semibold mb-2">{faq.question}</h3>
<p className="text-muted-foreground">{faq.answer}</p>
</CardContent>
</Card>
))}
</div>
{isCreating && (
<CreateFAQDialog
onClose={() => setIsCreating(false)}
onCreate={createFAQ}
/>
)}
</div>
);
}🔒 Access Control
All admin FAQ functions require proper authorization:
// Only admin can manage FAQs
await requireAccess(ctx, {
userRole: ["admin"],
});Required Roles:
- Admin: Full FAQ management access
- User: Read-only access to active FAQs (public)
📚 API Reference
Public Queries
listActive
// Get all active FAQs sorted by display order
const faqs = await ctx.runQuery(api.faqs.public.queries.listActive);
// Returns: Array<{
// _id: Id<"faqs">;
// question: string;
// answer: string;
// displayOrder: number;
// isActive: boolean;
// createdAt: number;
// updatedAt: number;
// }>Private Mutations
create
await ctx.runMutation(api.faqs.private.mutations.create, {
question: "Your question here?",
answer: "Your answer here.",
isActive: true, // Optional, defaults to true
});update
await ctx.runMutation(api.faqs.private.mutations.update, {
id: faqId,
question: "Updated question",
answer: "Updated answer",
isActive: true,
});remove
await ctx.runMutation(api.faqs.private.mutations.remove, {
id: faqId,
});
// Auto-normalizes display ordertoggleActive
await ctx.runMutation(api.faqs.private.mutations.toggleActive, {
id: faqId,
});reorder
await ctx.runMutation(api.faqs.private.mutations.reorder, {
id: faqId,
newDisplayOrder: 3, // Move to position 3
});🧪 Testing
Manual Testing Checklist
- Create FAQ works and assigns correct displayOrder
- Update FAQ preserves displayOrder
- Delete FAQ normalizes remaining displayOrders
- Toggle active status works correctly
- Reorder FAQ updates surrounding items correctly
- Public query only returns active FAQs
- FAQs display in correct order
- Admin requires proper permissions
Edge Cases to Test
// Empty FAQ list
faqs = [];
// Single FAQ
faqs = [{ question: "Q1", displayOrder: 1 }];
// Reorder to same position (no-op)
reorder({ id, newDisplayOrder: currentOrder });
// Delete last FAQ
remove({ id: lastFaqId });
// Toggle all FAQs inactive (public should show none)
toggleActive for all FAQs;🎯 Best Practices
Content Guidelines
- Clear Questions: Write questions from user perspective
- Concise Answers: Keep answers brief but complete
- Common First: Most frequently asked questions at top
- Update Regularly: Keep FAQs current with product changes
Display Order Strategy
- Start Simple: Use increments of 1 (1, 2, 3, ...)
- Let System Normalize: Rely on auto-normalization after deletions
- Logical Grouping: Order related questions together
- Most Important First: Critical information at the top
Admin Workflow
- Draft Mode: Create FAQs as inactive first
- Review Content: Ensure accuracy before activating
- Gradual Rollout: Activate FAQs one at a time
- Monitor Feedback: Update based on user questions
📚 Related Documentation
- Site Branding - Customize FAQ section appearance
- Admin Dashboard - Admin interface overview
- Authorization System - Role-based access control
Mailing List System
TinyKit Pro includes a unified mailing list system with waitlist collection, newsletter subscriptions, exit-intent popups, and Resend Audiences integration.
Testimonials Management
TinyKit Pro includes a flexible testimonials system that supports both Twitter/X tweet embeds and custom testimonials, perfect for showcasing social proof on...