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_at set on insert
  • updated_at refreshed 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

FieldTypeRequiredDescription
authorNamestringYesName of the testimonial author (max 100 chars)
authorTitlestringNoAuthor's job title (max 100 chars)
authorCompanystringNoAuthor's company name (max 100 chars)
testimonialTextstringYesThe testimonial content (max 1000 chars)
ratingnumberNoStar rating from 1 to 5
isPublishedbooleanNoPublish state, defaults to true
sortOrdernumberNoDisplay 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:

ParameterTypeDescription
published"true" or "false"Filter by publish status
minRatingnumber (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:

StatusConditionBody
400Validation failed{ "success": false, "error": "Validation failed", "details": [...] }
404Not found{ "error": "Testimonial not found" }
500Server error{ "success": false, "error": "Failed to ..." }

Permissions

The Testimonials plugin uses role-based access for admin operations.

Admin Access

RolePermissions
adminFull access to all testimonial operations
editorCreate, 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:

ColumnTypeDescription
idINTEGER (PK)Auto-incrementing primary key
author_nameTEXTTestimonial author name (NOT NULL)
author_titleTEXTAuthor's job title
author_companyTEXTAuthor's company
testimonial_textTEXTThe testimonial content (NOT NULL)
ratingINTEGERStar rating, 1-5 (CHECK constraint)
isPublishedINTEGER1 = published, 0 = draft (NOT NULL)
sortOrderINTEGERDisplay order (NOT NULL, default 0)
created_atINTEGERUnix timestamp, set on insert
updated_atINTEGERUnix timestamp, auto-updated via trigger

Indexes

IndexColumn
idx_testimonials_publishedisPublished
idx_testimonials_sort_ordersortOrder
idx_testimonials_ratingrating

Next Steps

Was this page helpful?