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

MethodReturnsDescription
forEntry(entry)ReviewQueryApproved reviews for an entry, newest first
averageRating(entry)floatAverage rating (approved only)
count(entry)intCount of approved reviews
distribution(entry)array{1: n, 2: n, ...} rating histogram
schemaOrg(entry)stringJSON-LD <script> tag
captcha()array|nullActive captcha provider and site key

All entry methods accept an Entry object or an entry ID integer.