Testimonials Plugin
Customer testimonials and reviews management with star ratings, publish controls, and a filterable REST API.
Overview
The Testimonials plugin gives SonicJS sites a turnkey system for collecting and displaying customer testimonials. Each testimonial captures the author's name, title, company, review text, and an optional 1-5 star rating. A full CRUD API lets you query by publish status and minimum rating.
Star Ratings
Optional 1-5 star rating with database-level validation
Author Profiles
Capture author name, job title, and company for each testimonial
REST API
Full CRUD API with filtering by publish status and minimum rating
Publish Control
Draft and published states with sort ordering for display
Features
Testimonial Management
- Store testimonials with author name, title, company, and review text
- Optional 1-5 star rating (enforced at both schema and database level)
- Toggle between draft and published states
- Custom sort ordering for curated display
Filtering API
- Filter by published status
- Filter by minimum star rating
- Results ordered by sort order then creation date (newest first)
- Zod-validated request bodies on create and update
Automatic Timestamps
created_atset on insertupdated_atrefreshed automatically via database trigger
Data Schema
The plugin validates all data using Zod:
Testimonial Schema
import { z } from 'zod'
const testimonialSchema = z.object({
id: z.number().optional(),
authorName: z.string().min(1, 'Author name is required').max(100),
authorTitle: z.string().max(100).optional(),
authorCompany: z.string().max(100).optional(),
testimonialText: z.string().min(1, 'Testimonial text is required').max(1000),
rating: z.number().min(1).max(5).optional(),
isPublished: z.boolean().default(true),
sortOrder: z.number().default(0),
createdAt: z.number().optional(),
updatedAt: z.number().optional(),
})
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
authorName | string | Yes | Name of the testimonial author (max 100 chars) |
authorTitle | string | No | Author's job title (max 100 chars) |
authorCompany | string | No | Author's company name (max 100 chars) |
testimonialText | string | Yes | The testimonial content (max 1000 chars) |
rating | number | No | Star rating from 1 to 5 |
isPublished | boolean | No | Publish state, defaults to true |
sortOrder | number | No | Display order, defaults to 0 |
API Endpoints
All endpoints are mounted at /api/testimonials.
List Testimonials
GET /api/testimonials
# Get all testimonials
curl https://your-site.com/api/testimonials
# Filter by published status
curl https://your-site.com/api/testimonials?published=true
# Filter by minimum rating
curl https://your-site.com/api/testimonials?minRating=4
# Combine filters
curl "https://your-site.com/api/testimonials?published=true&minRating=4"
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
published | "true" or "false" | Filter by publish status |
minRating | number (1-5) | Return only testimonials with rating >= value |
Response:
List Response
{
"success": true,
"data": [
{
"id": 1,
"author_name": "Jane Smith",
"author_title": "CTO",
"author_company": "Acme Corp",
"testimonial_text": "SonicJS transformed our content workflow...",
"rating": 5,
"isPublished": 1,
"sortOrder": 0,
"created_at": 1712678400,
"updated_at": 1712678400
}
]
}
Get Single Testimonial
GET /api/testimonials/:id
curl https://your-site.com/api/testimonials/1
Response:
Single Response
{
"success": true,
"data": {
"id": 1,
"author_name": "Jane Smith",
"author_title": "CTO",
"author_company": "Acme Corp",
"testimonial_text": "SonicJS transformed our content workflow...",
"rating": 5,
"isPublished": 1,
"sortOrder": 0,
"created_at": 1712678400,
"updated_at": 1712678400
}
}
Create Testimonial
POST /api/testimonials
curl -X POST https://your-site.com/api/testimonials \
-H "Content-Type: application/json" \
-d '{
"authorName": "John Doe",
"authorTitle": "Lead Developer",
"authorCompany": "Tech Inc",
"testimonialText": "The plugin system is incredibly flexible.",
"rating": 5,
"isPublished": true,
"sortOrder": 1
}'
Response (201):
Create Response
{
"success": true,
"data": { "id": 2, "author_name": "John Doe", "..." : "..." },
"message": "Testimonial created successfully"
}
Update Testimonial
Supports partial updates -- only include the fields you want to change.
PUT /api/testimonials/:id
curl -X PUT https://your-site.com/api/testimonials/2 \
-H "Content-Type: application/json" \
-d '{
"rating": 4,
"isPublished": false
}'
Response:
Update Response
{
"success": true,
"data": { "id": 2, "author_name": "John Doe", "rating": 4, "isPublished": 0, "..." : "..." },
"message": "Testimonial updated successfully"
}
Delete Testimonial
DELETE /api/testimonials/:id
curl -X DELETE https://your-site.com/api/testimonials/2
Response:
Delete Response
{
"success": true,
"message": "Testimonial deleted successfully"
}
Error Responses
All endpoints return consistent error shapes:
| Status | Condition | Body |
|---|---|---|
| 400 | Validation failed | { "success": false, "error": "Validation failed", "details": [...] } |
| 404 | Not found | { "error": "Testimonial not found" } |
| 500 | Server error | { "success": false, "error": "Failed to ..." } |
Permissions
The Testimonials plugin uses role-based access for admin operations.
Admin Access
| Role | Permissions |
|---|---|
admin | Full access to all testimonial operations |
editor | Create, edit, and delete testimonials |
API Access
The REST API routes do not require authentication by default. To restrict API access, wrap the routes with your own auth middleware.
Restrict API Access
import { Hono } from 'hono'
import { authMiddleware } from './middleware/auth'
const app = new Hono()
// Protect all testimonial API routes
app.use('/api/testimonials/*', authMiddleware())
Admin Interface
The plugin registers three admin pages and a sidebar menu item.
Testimonials List
Path: /admin/testimonials
Browse and manage all testimonials:
- View testimonials in a sortable list
- See author info, rating, and publish status at a glance
- Quick publish/unpublish toggles
Create Testimonial
Path: /admin/testimonials/new
Add a new testimonial with the full form:
- Author name, title, and company fields
- Testimonial text area
- Star rating selector (1-5)
- Publish toggle and sort order
Edit Testimonial
Path: /admin/testimonials/:id
Edit an existing testimonial with the same form layout.
Menu Item
The plugin adds a Testimonials item to the admin sidebar with a star icon at position 60.
Integration
Plugin Registration
Register the Plugin
import { createTestimonialPlugin } from '@sonicjs-cms/core/plugins'
// Using the factory function
const testimonialsPlugin = createTestimonialPlugin()
// Or import the pre-built instance
import { testimonialsPlugin } from '@sonicjs-cms/core/plugins'
Lifecycle Hooks
The plugin implements install, uninstall, activate, and deactivate lifecycle hooks:
Lifecycle Hooks
// Install: creates the testimonials table and indexes
// Uninstall: drops the testimonials table
// Activate / Deactivate: logging only
builder.lifecycle({
install: async (context) => {
await context.db.prepare(testimonialMigration).run()
},
uninstall: async (context) => {
await context.db.prepare('DROP TABLE IF EXISTS testimonials').run()
},
activate: async () => { /* logs activation */ },
deactivate: async () => { /* logs deactivation */ },
})
Querying from Custom Code
Custom Query - Top Rated
import { Hono } from 'hono'
const app = new Hono()
// Get top-rated published testimonials for a widget
app.get('/api/top-testimonials', async (c) => {
const db = (c as any).env?.DB
const { results } = await db
.prepare(
'SELECT * FROM testimonials WHERE isPublished = 1 AND rating >= 4 ORDER BY rating DESC, sortOrder ASC LIMIT 5'
)
.all()
return c.json({ data: results })
})
Custom Query - By Company
app.get('/api/testimonials-by-company/:company', async (c) => {
const company = c.req.param('company')
const db = (c as any).env?.DB
const { results } = await db
.prepare(
'SELECT * FROM testimonials WHERE author_company = ? AND isPublished = 1 ORDER BY sortOrder ASC'
)
.bind(company)
.all()
return c.json({ data: results })
})
Database Schema
The plugin creates a single table with supporting indexes and an update trigger:
| Column | Type | Description |
|---|---|---|
id | INTEGER (PK) | Auto-incrementing primary key |
author_name | TEXT | Testimonial author name (NOT NULL) |
author_title | TEXT | Author's job title |
author_company | TEXT | Author's company |
testimonial_text | TEXT | The testimonial content (NOT NULL) |
rating | INTEGER | Star rating, 1-5 (CHECK constraint) |
isPublished | INTEGER | 1 = published, 0 = draft (NOT NULL) |
sortOrder | INTEGER | Display order (NOT NULL, default 0) |
created_at | INTEGER | Unix timestamp, set on insert |
updated_at | INTEGER | Unix timestamp, auto-updated via trigger |
Indexes
| Index | Column |
|---|---|
idx_testimonials_published | isPublished |
idx_testimonials_sort_order | sortOrder |
idx_testimonials_rating | rating |