Back to Tips & Tricks

JetEngine Review System With a CCT: Star Ratings, Average and Count

BoostMyCroco TeamPublished on
Five teal and gold stars above stacked review cards linked to a post, illustrating a JetEngine CCT review system

You can build a JetEngine review system with a CCT and no separate reviews plugin. Each review goes into a Custom Content Type with a 1–5 rating field and the ID of the reviewed post. A JetFormBuilder form saves new reviews as Draft for moderation. The single post then shows its published reviews in a Listing Grid, plus an average rating and a review count from one SQL/AI Query.

You need JetEngine with the Custom Content Type and Dynamic Visibility modules turned on, plus JetFormBuilder.

CPT, CCT or JetReviews?

By default, CCT items have no single pages, and they live in their own database table instead of the posts table. The Has Single Page option can link a CCT to a post type, but reviews do not need one (see how to create a CCT).

  • Use a CCT when reviews only appear under their post. This guide uses one.
  • Use a CPT when each review needs its own URL and archive.
  • Use JetReviews when a ready-made reviews plugin is enough. Crocoblock documents how to show the JetReviews average rating and count.

Step 1: Create the reviews CCT

  1. Go to WordPress Dashboard > JetEngine > Custom Content Types and click Add New. Name it Reviews with the slug reviews.
  2. In the Field tab, add these fields:
    • review_title: Text
    • review_text: Textarea or WYSIWYG
    • rating: Number
    • reviewed_post: Number, for the ID of the reviewed post
  3. Save with Add/Update Content Type. The items are stored in the table jet_cct_reviews with your site's table prefix.
  4. Add one sample review with a real post ID and set it to Publish. You need it to preview the queries.
  • Option A, a post ID field. The form writes the current post ID into reviewed_post. It is the simplest option, and the SQL average only needs a plain Where clause. This guide uses option A.
  • Option B, a JetEngine Relation. In JetEngine > Relations > Add New, set your post type as the Parent object, the Reviews CCT as the Child object, and the Relation type to One to many, as in Crocoblock's commenting system tutorial. Keep reviewed_post anyway, because the average in Step 6 reads that field.

Step 3: Build the review form

Create a new JetFormBuilder form with these fields:

  1. A Text Field named review_title.
  2. A Textarea or WYSIWYG Field named review_text.
  3. A required Radio Field named rating with the manual options 1, 2, 3, 4 and 5.
  4. A Hidden Field named post_id. Set its value option to Current Post ID.

Then add these post-submit actions:

  • Insert/Update Custom Content Type Item: set CONTENT TYPE to Reviews and ITEM STATUS to Draft. In FIELDS MAP, map each form field to the CCT field with the same name, and map post_id to reviewed_post. A test submission should appear as Draft in the Reviews CCT list.
  • Send Email to the site admin, so you know when a review is waiting.
  • Option B only: Connect Relation Items. Set RELATION to your relation, PARENT ITEM ID to post_id and CHILD ITEM ID to inserted_cct_reviews. Set UPDATE CONTEXT to "We updating children items for the parent object" and HOW TO STORE NEW ITEMS to "Append new items to already existing related items".

Set SUBMIT TYPE to Page Reload in two places: JetForm tab > Form Settings, and the JetForm widget or block. For a clearer "awaiting approval" message, see our JetFormBuilder success message popup tip.

Step 4: Show the form to logged-in users only

Place the form on your single post template. In the JetForm widget, go to Advanced > Dynamic Visibility, click Enable, choose "Show element if condition met" and add the condition User logged in. In Gutenberg, use the eye icon in the block toolbar. A login also ties each review to a user, which Step 5 uses for the author. Dynamic Visibility only hides the form and does not block submissions on the server, so every review still starts as Draft.

Step 5: List the post's reviews

Listing Item

  1. Go to JetEngine > Listings/Components > Add New Item and choose the Reviews CCT as the source.
  2. Add Dynamic Fields with Source "Post/Term/User/Object Data" and pick review_title, review_text and rating in Object Field.
  3. For the author, add a Dynamic Field, pick a user field such as Nickname, and set Context to Current CCT item author. Add the avatar with a Dynamic Image widget or block using the same context.

Query

  1. Go to JetEngine > Query Builder > Add New and set Query Type to Custom Content Type Query.
  2. In From Content Type, pick Reviews, then set Status to Publish.
  3. Add a clause with Field reviewed_post, compare set to equal, and the current post ID macro as the Value.
  4. Set Order By to Created Date with type Date, newest first.

Listing Grid

On the single post template, add a Listing Grid with the review Listing Item and Columns Number set to 1. Under Custom Query, turn on Use Custom Query and pick the query. "No data was found" is normal until the post has a published review, and you can change that text in the grid's General tab.

Step 6: Average rating and review count

Go to WordPress Dashboard > JetEngine > JetEngine > Query Builder, add a new query and set Query Type to SQL/AI Query (see the SQL/AI Query documentation). Start with Advanced/AI mode off, so JetEngine writes the table, column and macro names for you.

  1. From Table: pick the Reviews CCT table (jet_cct_reviews with your prefix).
  2. Where, first clause: column reviewed_post, Compare Equal, Type Numeric. Click the database icon in the Value field and pick the current post ID macro. Leave "Exclude this clause from the query if dynamic value is empty" off, or an empty ID would average every review on the site.
  3. Turn on Preview results. Note the names of the status column and the item ID column, and the value stored for a published item. Expect publish, but confirm it, because a wrong value returns no reviews and no error.
  4. Where, second clause: the status column, Compare Equal, and the published value you confirmed. This keeps Draft reviews out of the numbers.
  5. Switch Advanced/AI mode on. With an empty SQL textarea, JetEngine fills it with the SQL built from these settings. From now on the visual settings no longer apply, so edit only the SQL.
  6. Replace the SELECT part with the two aggregates below. Keep the FROM and WHERE lines exactly as JetEngine wrote them, macro included.
  7. Check Preview results for one row with avg_rating and review_count, then save with Add/Update Query.

The finished query has this shape:

SELECT COALESCE(ROUND(AVG(rating), 1), 0) AS avg_rating,
  COUNT(<id column>) AS review_count
FROM {prefix}jet_cct_reviews
WHERE reviewed_post = <current post ID macro, as generated>
AND <status column> = 'publish'

ROUND gives one decimal, and COALESCE returns 0 instead of an empty value when a post has no published reviews. COUNT also returns 0, so the query always gives one row. Keep {prefix} in front of the table name, and never use an existing column name as an alias, because the alias replaces that column's value.

Do not use Include Calculated Columns in visual mode for this. An empty Include columns returns all columns, so AVG and COUNT would sit next to plain columns with no GROUP BY. That can raise an SQL error or fill the row with values from an arbitrary review.

To display the result, create a second Listing Item with Query Builder as its source and this query selected. Add Dynamic Fields and pick avg_rating and review_count in Object Field if your version lists them; the aliases in Preview results are the names to look for. Then add a Listing Grid to the single post template with Columns Number 1, turn on Use Custom Query, and pick the SQL query.

Step 7: Approve reviews and draw the stars

To approve a review, open the Reviews CCT item list and edit the Draft review. The Hidden Field and the rating can be changed in the browser, so check that reviewed_post points to the right post and that the rating is a whole number from 1 to 5. Then set the status to Publish and save.

To show the average as stars, add the CSS below to your child theme's stylesheet, which works with any theme. On a classic theme you can also use Appearance > Customize > Additional CSS; on a block theme, use Appearance > Editor > Styles > Additional CSS.

Code

/* BoostMyCroco: star rating for a JetEngine review average */
.review-stars {
  --star-size: 22px;
  --star-empty: #cfd8dc;
  --star-fill: #f4b400;
  position: relative;
  display: inline-block;
  font-size: var(--star-size);
  line-height: 1;
  white-space: nowrap;
  vertical-align: middle;
}

/* Five grey stars as the base layer */
.review-stars::before {
  content: "★★★★★";
  color: var(--star-empty);
}

/* Five gold stars on top, clipped to the rating (clamped to 0-5) */
.review-stars::after {
  content: "★★★★★";
  position: absolute;
  top: 0;
  left: 0;
  width: calc(min(max(var(--rating, 0), 0), 5) / 5 * 100%);
  overflow: hidden;
  color: var(--star-fill);
}

How the stars work

The ::before layer draws five grey stars, and the ::after layer draws five gold stars on top, clipped to the rating divided by 5. A --rating of 4.3 shows 86% of the gold.

To pass the average to --rating, use the Dynamic Field that shows avg_rating and turn on its Customize field output option (confirm the label in your Dynamic Field widget or block). Use this format, where %s is the value:

<span class="review-stars" style="--rating: %s" aria-hidden="true"></span>

View the page source and check that the span keeps style="--rating: …" with a number, in Bricks too. If your builder strips the attribute, show the number as text only.

The span is hidden from screen readers, so keep a second Dynamic Field that shows the number as text, for example "4.3 out of 5", next to review_count. The same format on the rating field in the reviews Listing Item gives each review its own stars.

To test, log in, submit a 5-star review, approve it and reload the post. The review should appear, the count should go up by one, and the stars should change. A post with no published reviews shows 0, 0 and empty stars.

Troubleshooting

  • The review is saved but not tied to the post. Check that the Hidden Field's value is Current Post ID, the form sits on the single post template, and post_id maps to reviewed_post in FIELDS MAP. With option B, also check PARENT ITEM ID, CHILD ITEM ID (inserted_cct_reviews) and "Append new items".
  • The average does not change after submitting. Make sure the review is published and Page Reload is set in both the form settings and the widget or block. If results still look stale, turn off Cache Query on the SQL query; this mostly matters with an external object cache such as Redis or Memcached.
  • The average or count is wrong or blank. Check that the SELECT holds only the two aggregates, that neither alias matches an existing column, and that the status value matches Preview results. Our JetEngine Query Builder mistakes guide covers more query fixes.
  • Guests can see the form. Recheck the Dynamic Visibility condition "User logged in" on the form widget or block.

You now have moderated reviews with a live average and count, all inside JetEngine and JetFormBuilder. If you would rather have it built and styled for you, post a request on BoostMyCroco.

Frequently Asked Questions

Can JetEngine show a star rating without JetReviews?
Yes. Store a numeric rating in a CCT, calculate the average with an SQL/AI Query that uses AVG and COUNT, and draw the stars with CSS. JetReviews is worth adding only if you prefer a ready-made reviews plugin.
Why does my JetEngine average rating include reviews I have not approved?
The SQL query is missing a condition on the CCT's status column, so Draft items are averaged too. Add a Where condition with the value your published items store, and confirm that value in Preview results.
Should I store reviews in a CCT or a custom post type?
A CCT suits reviews that only appear under their post, since by default its items have no single pages and sit in their own table. Pick a CPT if each review should have its own URL. With a CPT the rating lives in post meta, so the SQL query here would need a different setup.
Does this review setup work with Gutenberg or Bricks instead of Elementor?
The CCT, the JetFormBuilder form, the Query Builder queries and the Listing Grid work with Elementor, Gutenberg and Bricks. In Gutenberg you set Dynamic Visibility with the eye icon in the block toolbar. Put the star CSS in your child theme's stylesheet, and check in Bricks that the custom field output keeps the style attribute.

This guide answers a question from our community: Custom review system with CPT/CCT

Sources

  1. How to Create Custom Content Type (CCT) (crocoblock.com)
  2. How to Display Average Rating and Reviews Count — JetReviews (crocoblock.com)
  3. How to Create Commenting System — JetEngine (crocoblock.com)
  4. SQL/AI Query Type of Query Builder — JetEngine (crocoblock.com)
  5. Setting Calculated Columns and Grouping Results — JetEngine | Crocoblock (crocoblock.com)
  6. JetEngine Dynamic Function Feature for WordPress (crocoblock.com)

Related tips