Reviews
Reviews are a first-class Craft element with star ratings, pros/cons, admin responses, moderation, and schema.org output. This page covers the frontend workflow and the craft.reviews Twig API.
Frontend Form
Post reviews to the stars/reviews/save action. The hidden timestamp and honeypot fields power the built-in spam protection — keep them in place.
<form method="post">
{{ csrfInput() }}
{{ actionInput('stars/reviews/save') }}
{{ redirectInput('/thank-you') }}
<input type="hidden" name="entryId" value="{{ entry.id }}">
{# Spam protection: timestamp + honeypot (hidden from users, caught by bots) #}
<input type="hidden" name="__stars_ts" value="{{ now|date('U') }}">
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input type="text" name="starsHoneypot" tabindex="-1" autocomplete="off">
</div>
<label for="reviewerName">Your Name</label>
<input type="text" id="reviewerName" name="reviewerName" required>
<label for="reviewerEmail">Email</label>
<input type="email" id="reviewerEmail" name="reviewerEmail">
<label for="rating">Rating</label>
<select id="rating" name="rating">
{% for i in 1..5 %}
<option value="{{ i }}">{{ '★'|repeat(i) }}{{ '☆'|repeat(5 - i) }}</option>
{% endfor %}
</select>
<label for="reviewText">Review</label>
<textarea id="reviewText" name="reviewText"></textarea>
<button type="submit">Submit Review</button>
</form>
With Pros & Cons
Add repeatable pros[] and cons[] inputs to collect structured pro/con lists (when enabled in settings):
<label>Pros</label>
<input type="text" name="pros[]" placeholder="Pro 1">
<input type="text" name="pros[]" placeholder="Pro 2">
<label>Cons</label>
<input type="text" name="cons[]" placeholder="Con 1">
<input type="text" name="cons[]" placeholder="Con 2">
AJAX Submission
Send the form with an Accept: application/json header to receive a JSON response instead of a redirect:
const form = document.querySelector('#review-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const res = await fetch('/', {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: new FormData(form),
});
const data = await res.json();
if (data.success) {
// Review submitted
} else {
// Handle data.error or data.errors
}
});
Displaying Reviews
Query approved reviews for an entry and render ratings, pros/cons, and admin responses:
{% set reviews = craft.reviews.forEntry(entry).all() %}
{% set avg = craft.reviews.averageRating(entry) %}
{% set count = craft.reviews.count(entry) %}
{% if count > 0 %}
<p>{{ avg|number_format(1) }} out of 5 ({{ count }} {{ count == 1 ? 'review' : 'reviews' }})</p>
{% for review in reviews %}
<article class="review">
<strong>{{ review.reviewerName }}</strong>
<span>{{ '★'|repeat(review.rating) }}{{ '☆'|repeat(5 - review.rating) }}</span>
<time datetime="{{ review.dateCreated|date('Y-m-d') }}">{{ review.dateCreated|date('M j, Y') }}</time>
{% if review.reviewText %}
<p>{{ review.reviewText }}</p>
{% endif %}
{% set pros = review.prosArray %}
{% if pros|length %}
<ul class="pros">
{% for pro in pros %}<li>{{ pro }}</li>{% endfor %}
</ul>
{% endif %}
{% set cons = review.consArray %}
{% if cons|length %}
<ul class="cons">
{% for con in cons %}<li>{{ con }}</li>{% endfor %}
</ul>
{% endif %}
{% if review.adminResponse %}
<blockquote>
<strong>Response:</strong> {{ review.adminResponse }}
</blockquote>
{% endif %}
</article>
{% endfor %}
{% endif %}
Rating Distribution
Build a star histogram from the rating distribution:
{% set dist = craft.reviews.distribution(entry) %}
{% for stars, count in dist|reverse %}
<div>{{ stars }} stars: {{ count }}</div>
{% endfor %}
Schema.org Markup
Output valid Review + AggregateRating JSON-LD for Google Rich Results. Place it in your <head>:
{{ craft.reviews.schemaOrg(entry)|raw }}
Captcha
If you enable a captcha provider, craft.reviews.captcha() returns the provider name and site key (or null when captcha is off) so you can render the widget and include its token with the form.
craft.reviews API Reference
| Method | Returns | Description |
|---|---|---|
forEntry(entry) | ReviewQuery | Approved reviews for an entry, newest first |
averageRating(entry) | float | Average rating (approved only) |
count(entry) | int | Count of approved reviews |
distribution(entry) | array | {1: n, 2: n, ...} rating histogram |
schemaOrg(entry) | string | JSON-LD <script> tag |
captcha() | array|null | Active captcha provider and site key |
All entry methods accept an Entry object or an entry ID integer.