Version 5.0.2
May 27, 2026
Added
- Points plugin integration. Vouch registers a "Review approved" trigger with Points when installed, along with three bundled conditions for narrowing rules: Is from source, Rating is at least, and Review length is at least. Points are only awarded for reviews tied to a Craft user.
Version 5.0.1
May 27, 2026
Added
- Trustpilot Business Unit finder on the Trustpilot source edit page. Search by company name or domain and click a result to fill in the Business Unit ID.
- Sources dashboard widget - lists each pull-based source with its last-synced timestamp and a one-click Sync button. Manual sources are excluded.
sourceIdsetting lets you scope the widget to a single source. - Honeypot field on the front-end submission controller. Include a
vouchHoneypotinput in your form; any non-empty value is silently discarded. - Per-IP submission rate limiting. New
submissionRateLimit(default 5) andsubmissionRateWindow(default 60s) settings. craft.vouch.settingsTwig variable - direct access to the plugin's settings model (e.g.craft.vouch.settings.headlineMaxLength).
Changed
- Renamed
Reviews::averageRatingForElement→Reviews::rating(and the matchingcraft.vouch.ratingForElement→craft.vouch.rating). - Renamed
Reviews::ratingBreakdownForElement→Reviews::ratingsBySource(and the matchingcraft.vouch.ratingBreakdownForElement→craft.vouch.ratingsBySource). - Cron-driven sync now runs inline. The
--syncflag onvouch/sync/*commands has been dropped (it's the default and only mode).
Removed
- Queue-backed sync path:
Sync::queue(),Sync::queueAll(), andSyncSourceJobare gone. The CLI runs syncs inline; the queue runner is no longer involved. - Third-party provider extension API:
EVENT_REGISTER_PROVIDERS,RegisterProvidersEvent, andProviderRegistry's registration event. Built-in providers are now a fixed list. craft.vouch.pluginName()- usecraft.vouch.settings.pluginNameinstead.
Version 5.0.0
May 26, 2026
Added
Core data model & plugin scaffold
- Plugin skeleton with
Sourcemodel,Reviewelement type,Settingsmodel. - Install migration for
{{%vouch_sources}}and{{%vouch_reviews}}tables. Reviewelement type with element index, status filtering (Live / Pending approval), per-source sub-sources, and standardcraft.vouch.reviews()queries.
Source UX
- "Find a Place ID" search on the Google Reviews source edit page. Proxies the Places Text Search endpoint via
SourcesController::actionFindGooglePlaceusing the API key from the form (works on new, unsaved sources too) and lists matching places with click-to-fill behaviour. Saves admins from hand-pasting opaqueChIJ…IDs. - Google Business Profile API mode on the Google source. The source edit page has a Mode dropdown: "Places API" (existing key-based behaviour, 5-review cap) or "Business Profile API" (OAuth, full review history). Business Profile mode wires:
- Per-source OAuth 2.0 client (Client ID + Client Secret stored encrypted on the source)
- "Connect Google account" button → standard
accounts.google.comconsent flow → callback exchanges the auth code for a refresh token, stored encrypted alongside the client credentials GoogleConnector::fetchReviews()branches on mode; the Business Profile path paginatesmybusiness.googleapis.com/v4/.../reviews, normalises into the existingFetchedReviewDTO, and respects the globalbackfillDayscursor.- Note: Google's reviews endpoint is gated behind partner approval as of 2023 - the feature exists in Vouch but won't return data until the operator's Google Cloud project is approved by Google.
Provider connectors
ConnectorInterface+BaseConnector+FetchedReviewDTO + event-drivenProviderRegistry- third-party plugins can add their own connectors viaEVENT_REGISTER_PROVIDERS.- Google Reviews connector (Places API New):
apiKey+placeId, documents the 5-review cap upstream. - Trustpilot connector (public Business Units API):
apiKey+businessUnitId, paginated newest-first with cursor early-exit. - Feefo connector (Reviews API v20):
merchantIdentifier+ optionalapiKey, service-review extraction. - Reviews.io connector (Merchant Reviews API):
storeId+apiKey, reviewer email passed through where present. - Manual connector: no API behind it, used as the source type for CP-authored and front-end-submitted reviews.
- Brand-accurate SVG logos stored in
src/resources/icons/and loaded viaBaseConnector::loadIcon()- adding a new connector is just dropping its SVG into that folder.
Sync orchestration
Syncservice withrun()(synchronous),queue()(enqueuesSyncSourceJob), andqueueAll()(enqueues every enabled source).SyncSourceJobqueue job - failures rethrow so Craft's retry policy kicks in. The source row'slastSyncErrorpersists the human-readable message either way.- Cron-driven cadence (no per-source schedule field - the cron entry is the schedule):
craft vouch/sync/all- enqueue every enabled source.craft vouch/sync/source <id|handle>- enqueue or run a single source.- Both accept
--syncto bypass the queue.
- Connector "Test connection" auto-fires on the source edit page; renders as a status dot + label (Akeneo-style).
- Per-row "Sync" button on the sources index runs synchronously via the controller.
- Backfill window for first sync is a global setting (
backfillDays),0= pull all history.
Manual reviews
- "New review" button on the reviews index opens a full element-edit form (rating, headline, review, reviewer, related element, business reply, approved toggle).
- Front-end submission via anonymous
vouch/reviews/submitcontroller action - hard-rejects non-manual sources so customer submissions can't bypass Trustpilot/Feefo moderation. - API-sourced reviews show a "will be overwritten on next sync" warning when edited in the CP.
Reviewer emailfield is read-only for existing reviews (editable on create).- Front-end submit attaches per-field validation errors to the review model + sets a flash error +
requiresLoginroute param when applicable, so re-rendered forms can show inline errors and a "Log in" link. - Configurable
headlineMaxLength(default 120) andreviewMaxLength(default 2000) length caps - applied as validation rules and surfaced asmaxlengthattributes in the documented Twig form example.
Moderation
- Per-source
Require manual approvaltoggle on the source edit page. When on, reviews below the globalautoApproveThreshold(default 5) land as Pending until approved by an admin. applyModeration()now runs on every save path (front-end submit, CP author, API sync) - the threshold is consistent across all sources, not just provider syncs.- Bulk Approve element-index action on the reviews index. Skips already-approved rows, batches the work, fires
EVENT_AFTER_APPROVE_REVIEWidentically to single-row approvals. Visible to users withvouch-editReviews.
Spam / attribution controls
- Front-end submissions only link
reviewerUserIdwhen the submitter is authenticated AND their account email matches the submitted email - blocks the forge-by-email attribution attack. requireLoginForKnownEmailssetting (defaulttrue): rejects any submission whose email matches an existing Craft user when the submitter isn't logged in as them. Returns a 403 with{ ok: false, requiresLogin: true, loginUrl, message, errors }for JSON requests and attaches the rejection as areviewerEmailvalidation error for HTML submits.
Dashboard widgets
- Reviews Pending Approval - lists pending reviews with title (truncated with ellipsis), rating, reviewer + relative date. Footer links to the pending source on the reviews index.
- Latest Reviews - most recent approved reviews; optional per-source filter. Reviewer name links to the matched Craft user when known.
- Top Reviewed Elements - ranks elements by review count or average rating. Configurable element type (Entries / Assets / Categories / Users / Commerce Products); when "Entries" is picked an additional section filter appears. Column header reflects the chosen element type's display name.
- All three widgets' display names use the configured
pluginNameso a CP rename flows through.
Permissions
vouch-approveReviews- top-level permission (sibling of "View reviews") that gates both the single-row Approve button and the bulk Approve element-index action. Lets a moderator-only role approve without granting full edit/delete.vouch-manageSourcessplit into three:vouch-createSources,vouch-editSources,vouch-deleteSources.actionSavedistinguishes new vs edit bysourceIdand gates accordingly;actionDeleterequiresvouch-deleteSources; the "+ New source" tiles on the sources index now checkvouch-createSources; the "Find a Place ID" helper accepts eithervouch-createSourcesorvouch-editSources.vouch-viewWidgets- new top-level permission. All three dashboard widgets'isSelectable()now checks this instead ofvouch-viewReviews, so admins can hide widgets independently of review access.- Permissions heading uses the configured
pluginNameso a CP rename flows through to the user-group settings UI.
Users integration
- Opt-in "Reviews" column on the Users element index showing how many approved reviews each user has authored (matched via
reviewerUserId). - "Reviews" screen on the user edit page (and
/myaccount) - mirrors the pattern Commerce uses for its "Commerce" tab. Embeds the reviews element index pre-filtered to that user. Sidebar label respects the configuredpluginName.
CP UX
- Sources index uses Craft's
VueAdminTablewith built-in search, delete, and a per-row Sync button. - Inline provider quick-add tiles above the table - one click to start a new source for any provider. The standalone
/admin/vouch/sources/newpicker page is gone. - "Connection" column on the sources table shows live API health for each pull-capable source (async; em-dash for Manual / disabled rows).
- Status dot before the source name reflects enabled/disabled.
- Native Save dropdown ("Save and continue editing") via
formActionson the source edit page. - Auto-generated handle from name via
Craft.HandleGenerator. autoSuggestFieldwithsuggestEnvVars: trueon all credential and connection fields - supports$ENV_VARreferences that resolve viaApp::parseEnv()at use-time.
Rating roll-up (entries, products, front-end)
Reviews::averageRatingForElement(int $elementId)- singleAVG()aggregate, no N+1.Reviews::ratingBreakdownForElement(int $elementId)- per-source average + count.- Rating column on Entries and (when Commerce is installed) Products element indexes. Opt-in via column settings, shows "4.2 ★" or em-dash.
- Sidebar summary on Entry and Product edit pages (via
EVENT_DEFINE_SIDEBAR_HTML) - overall average with star + per-source breakdown. - Twig:
craft.vouch.ratingForElement(entry.id)andcraft.vouch.ratingBreakdownForElement(entry.id)mirror these for front-end use.
Events (public API for downstream integrations)
Reviews::EVENT_AFTER_SYNC_REVIEW- fires on every successful upsert, withisNewflag.Reviews::EVENT_AFTER_APPROVE_REVIEW- fires exactly once per review when it transitions to approved (auto or manual);autoflag distinguishes the two paths.Sync::EVENT_BEFORE_SOURCE_SYNC- cancellable; set$event->cancelled = trueto skip the run.Sync::EVENT_AFTER_SOURCE_SYNC- carries the populatedSyncResult.
Front-end surface
- Twig
craft.vouch.*:reviews()(chainable element query),sources(),source(handle),providers(),averageRating(sourceId?),ratingForElement(elementId),ratingBreakdownForElement(elementId),pluginName(). craft.vouch.reviews()defaults toapproved(true)so pending reviews never leak to the front-end. Pass.approved(null)to include both,.approved(false)for pending only.- Convenience getters on the
Reviewelement:review.sourceName,review.sourceHandle,review.providerHandle,review.getReviewerUser(). - GraphQL type
VouchReview+ two queries:vouchReviews- filterable bysourceId,rating,minRating,approved,reviewerUserId,relatedElementId,limit,offset. Defaults toapproved: trueon the public surface.vouchReview(id)- single review by id.
PII & GDPR
reviewerEmailHash(SHA-256 lowercase) stored alongsidereviewerEmailso user-matching survivesemailRetentionDayspurge.- Credentials encrypted at rest via Craft's security key, base64-wrapped for UTF-8-safe column storage.
Settings
- Plugin settings stored in Project Config (so they sync via
project.yaml). config/vouch.phpoverlays Project Config values for per-environment overrides.- Settings page accessible via Settings → Plugins → Vouch (not surfaced in Vouch's own sidebar).
- Available settings:
pluginName,matchAuthorsToUsers,emailRetentionDays,backfillDays,autoApproveThreshold,requireLoginForKnownEmails,headlineMaxLength,reviewMaxLength.
Changed
- Field rename + DB migration (schema 1.0.1):
title→headline,body→review,authorName→reviewerName,authorEmail→reviewerEmail,authorEmailHash→reviewerEmailHash,authorUserId→reviewerUserId,response→businessReply. Property, DB column, GraphQL type, Twig accessor, controller form-field, and condition rule names all updated. Migrationm260526_120000_rename_review_fieldsrenames the columns in place; FKs/indices are dropped and re-created against the new names. Anyone consuming the GraphQL surface,craft.vouch.reviews()filters, or the old property names will need to update their references.
Fixed
- Reviews index now extends
_layouts/elementindexso the source-list sidebar renders to the left of the table. Sources::saveSource()coerces ActiveRecord date columns toDateTimebefore writing to the model's typed properties.- Encrypted credentials are base64-encoded for UTF-8-safe storage in
TEXTcolumns. HandleValidator+ unique-handle check onSourceso duplicate handles surface as form validation rather than a SQL integrity exception.- Source dropdown on new-review form uses array-of-objects options to dodge Twig's integer-key reindexing in
merge. reviewedAtstored viaDb::prepareDateForDb()so the timestamp survives PHP/DB timezone mismatches (previously a tz-less literal was written and re-interpreted as UTC, shifting dates across midnight boundaries).relatedElementIdno longer required on manual submissions (was rejecting otherwise-valid customer reviews).- Section field in the Top Reviewed Elements widget settings now toggles live on element-type change (was waiting until Save before appearing).
- Element-index empty placeholders use em dashes (
—) consistently. - Google source setup: README now recommends
backfillDays=0for Google sources (Places-New caps at 5 reviews per call anyway, so the upstream cost is already bounded). With the default 90-day filter, places whose most recent reviews were all older than 90 days returned "0 new, 0 updated" on first sync, which read like a credentials failure.