nuxt-reviews
v0.1.3
Published
A Nuxt module to fetch and aggregate reviews from multiple tourism platforms (Google, Trustpilot, SerpAPI, Outscraper, Booking.com)
Maintainers
Readme
A Nuxt module to fetch and aggregate reviews from multiple tourism & review platforms with a single composable.
Features
- 🔌 Multi-provider — Google Places, Trustpilot, SerpAPI, Outscraper, Booking.com (beta)
- 🎭 Mock provider — Fake multilingual reviews for development (no API keys needed)
- 🔄 Normalized data — All reviews mapped to a single Schema.org-based interface
- 📊 Aggregate ratings — Automatic average, total, and star distribution
- 🛡️ Content moderation — Toxicity filtering via Google Perspective API or OpenAI
- ⚡ Server-side caching — Built-in Nitro cached handlers
- 🧩 Auto-imported —
useReviews()anduseReviewSchema()composables work out of the box - 🎨 Component kit —
<ReviewStars>,<ReviewCard>,<ReviewList>,<ReviewSummary>ready to use - 🔍 Schema.org JSON-LD — Auto-inject structured data for Google rich snippets
- 🎯 Type-safe — Full TypeScript support with provider-specific configs
Quick Setup
Install the module to your Nuxt application with one command:
npx nuxi module add nuxt-reviewsAdd to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-reviews'],
reviews: {
providers: {
google: {
apiKey: process.env.GOOGLE_API_KEY,
placeId: 'ChIJ...',
},
trustpilot: {
apiKey: process.env.TRUSTPILOT_API_KEY,
businessUnitId: '...',
},
// Optional paid providers
serpapi: {
apiKey: process.env.SERPAPI_API_KEY,
placeId: 'ChIJ...',
},
outscraper: {
apiKey: process.env.OUTSCRAPER_API_KEY,
placeId: 'ChIJ...',
},
// Booking.com (beta) — requires Connectivity Partner credentials
bookingcom: {
username: process.env.BOOKINGCOM_USERNAME,
password: process.env.BOOKINGCOM_PASSWORD,
propertyId: '1234567890',
},
},
cache: true, // default: true
cacheTTL: 3600, // default: 1 hour
maxReviews: 50, // default: 50
// Content moderation (optional)
moderation: {
enabled: true,
provider: 'perspective', // 'perspective' or 'openai'
apiKey: process.env.PERSPECTIVE_API_KEY,
toxicityThreshold: 0.7, // 0.0 - 1.0 (default: 0.7)
action: 'filter', // 'filter' or 'flag'
},
},
})How It Works
The module provides two modes of fetching reviews:
| Mode | Endpoint | Composable | Description |
|---|---|---|---|
| All (aggregated) | GET /api/_reviews | useReviews() | Fetches from all configured providers in parallel, merges into a single list sorted by date |
| Single provider | GET /api/_reviews/google | useReviews({ provider: 'google' }) | Fetches from only the specified provider |
Aggregated Mode (All Providers)
All configured providers are called simultaneously via Promise.allSettled. If one provider fails (e.g. expired API key), the others still return successfully — failed providers appear in the errors array.
<script setup>
const { reviews, aggregate, sources, pending, error } = useReviews()
// or explicitly:
const { reviews } = useReviews({ provider: 'all' })
</script>GET /api/_reviews
GET /api/_reviews?limit=20&language=tr{
"reviews": [
{
"id": "trustpilot_67abc123",
"provider": "trustpilot",
"rating": 5,
"text": "Harika bir otel deneyimiydi, personel cok ilgiliydi.",
"title": "Mukemmel tatil",
"author": {
"name": "Ahmet Y.",
"avatar": null,
"url": null
},
"publishedAt": "2025-03-20T14:30:00Z",
"language": "tr",
"isVerified": true,
"likes": 3,
"businessResponse": {
"text": "Degerli misafirimiz, guzel yorumunuz icin tesekkur ederiz!",
"publishedAt": "2025-03-21T09:00:00Z"
}
},
{
"id": "google_places_reviews_0",
"provider": "google",
"rating": 4,
"text": "Great location, clean rooms. Breakfast could be better.",
"title": null,
"author": {
"name": "Sarah M.",
"avatar": "https://lh3.googleusercontent.com/a/photo.jpg",
"url": "https://www.google.com/maps/contrib/12345"
},
"publishedAt": "2025-03-18T10:15:00Z",
"language": "en",
"isVerified": false,
"likes": null,
"businessResponse": null
},
{
"id": "serpapi_1711234567_0",
"provider": "serpapi",
"rating": 5,
"text": "Manzara muhtesem, havuz alani cok guzeldi.",
"title": null,
"author": {
"name": "Mehmet K.",
"avatar": "https://lh3.googleusercontent.com/a/photo2.jpg",
"url": "https://www.google.com/maps/contrib/67890",
"reviewCount": 42
},
"publishedAt": "2025-03-15T08:20:00Z",
"language": "tr",
"likes": 7,
"images": [
"https://lh5.googleusercontent.com/p/review-photo.jpg"
],
"businessResponse": {
"text": "Tekrar bekleriz!",
"publishedAt": "2025-03-16T11:00:00Z"
}
}
],
"aggregate": {
"average": 4.7,
"total": 3,
"distribution": {
"1": 0,
"2": 0,
"3": 0,
"4": 1,
"5": 2
}
},
"sources": [
{ "provider": "trustpilot", "count": 1, "average": 5.0 },
{ "provider": "google", "count": 1, "average": 4.0 },
{ "provider": "serpapi", "count": 1, "average": 5.0 }
],
"fetchedAt": "2025-03-22T12:00:00.000Z",
"errors": null
}Single Provider Mode
<script setup>
const { reviews, aggregate, pending } = useReviews({ provider: 'trustpilot' })
</script>GET /api/_reviews/trustpilot
GET /api/_reviews/google?language=en&limit=5{
"reviews": [
{
"id": "trustpilot_67abc123",
"provider": "trustpilot",
"rating": 5,
"text": "Harika bir otel deneyimiydi, personel cok ilgiliydi.",
"title": "Mukemmel tatil",
"author": {
"name": "Ahmet Y."
},
"publishedAt": "2025-03-20T14:30:00Z",
"language": "tr",
"isVerified": true,
"likes": 3,
"businessResponse": {
"text": "Degerli misafirimiz, guzel yorumunuz icin tesekkur ederiz!",
"publishedAt": "2025-03-21T09:00:00Z"
}
},
{
"id": "trustpilot_67abc456",
"provider": "trustpilot",
"rating": 4,
"text": "Overall good experience but check-in was slow.",
"title": "Good but room for improvement",
"author": {
"name": "John D."
},
"publishedAt": "2025-03-19T16:45:00Z",
"language": "en",
"isVerified": true,
"likes": 1,
"businessResponse": null
}
],
"aggregate": {
"average": 4.5,
"total": 2,
"distribution": {
"1": 0,
"2": 0,
"3": 0,
"4": 1,
"5": 1
}
},
"sources": [
{ "provider": "trustpilot", "count": 2, "average": 4.5 }
],
"fetchedAt": "2025-03-22T12:00:00.000Z",
"totalAvailable": 486,
"nextPageToken": null
}Error Handling (Partial Failure)
When using aggregated mode, if one provider fails the others still return. Failed providers are listed in errors:
{
"reviews": [
{
"id": "trustpilot_67abc123",
"provider": "trustpilot",
"rating": 5,
"text": "Amazing hotel!",
"author": { "name": "Jane D." },
"publishedAt": "2025-03-20T14:30:00Z"
}
],
"aggregate": {
"average": 5.0,
"total": 1,
"distribution": { "1": 0, "2": 0, "3": 0, "4": 0, "5": 1 }
},
"sources": [
{ "provider": "trustpilot", "count": 1, "average": 5.0 }
],
"fetchedAt": "2025-03-22T12:00:00.000Z",
"errors": [
{
"provider": "google",
"error": "Request failed with status 403: API key expired"
}
]
}Composable Options
<script setup>
// All providers, default options
const { reviews, aggregate, sources, pending, error, refresh } = useReviews()
// Single provider
const { reviews: googleOnly } = useReviews({ provider: 'google' })
// With filters
const { reviews: filtered } = useReviews({
provider: 'trustpilot',
limit: 10,
language: 'tr',
sort: 'createdat.desc',
})
// Pagination (single-provider only)
const { reviews, nextPageToken, totalAvailable } = useReviews({ provider: 'serpapi', limit: 10 })
// Pass nextPageToken.value to fetch the next page:
// useReviews({ provider: 'serpapi', limit: 10, pageToken: nextPageToken.value })
// Lazy load (don't fetch immediately)
const { reviews: lazy, refresh: loadReviews } = useReviews({
provider: 'all',
immediate: false,
})
// Call loadReviews() when needed (e.g. button click, intersection observer)
</script>Direct API Access
GET /api/_reviews → All providers (aggregated, parallel fetch)
GET /api/_reviews/google → Google Places only
GET /api/_reviews/trustpilot → Trustpilot only
GET /api/_reviews/serpapi → SerpAPI only
GET /api/_reviews/outscraper → Outscraper only
GET /api/_reviews/bookingcom → Booking.com only (beta)
Query params:
?limit=20 → max reviews per provider
&language=tr → filter by language
&sort=newest → sort order (provider-specific)
&pageToken=abc → pagination (single provider only)Content Moderation
Review platforms do basic moderation before publishing, but mild profanity, passive-aggressive insults, and non-English abuse (especially Turkish, Russian) can still slip through. The module provides server-side content moderation that analyzes every review before it reaches your frontend.
Supported Moderation Providers
| Provider | Languages | Free Tier | Best For | |---|---|---|---| | Google Perspective API | 40+ (TR, EN, DE, RU included) | Generous | Multi-language projects | | OpenAI Moderation API | Multi-language | Free with API key | Projects already using OpenAI |
Configuration
// nuxt.config.ts
reviews: {
moderation: {
enabled: true,
provider: 'perspective', // or 'openai'
apiKey: process.env.PERSPECTIVE_API_KEY,
toxicityThreshold: 0.7, // 0.0 - 1.0 (default: 0.7)
attributes: [ // Perspective API only (all enabled by default)
'TOXICITY',
'SEVERE_TOXICITY',
'INSULT',
'PROFANITY',
'THREAT',
'IDENTITY_ATTACK',
],
action: 'filter', // 'filter' or 'flag' (default: 'filter')
},
}Actions: Filter vs Flag
| Action | Behavior | Use Case |
|---|---|---|
| filter | Toxic reviews are removed from the response entirely | Public-facing websites — visitors never see abusive content |
| flag | All reviews are returned, toxic ones have moderation data attached | Admin dashboards — you decide what to show/hide |
How It Works
- Reviews are fetched from providers (Google, Trustpilot, etc.)
minRatingfilter is applied (if configured)- Each review text is sent to the moderation API for toxicity analysis
- Based on
action:filter: Reviews withtoxicity >= thresholdare removedflag: All reviews kept, flagged ones getmoderationfield attached
- Aggregate ratings are computed from the final filtered list
Example: Flagged Review (action: 'flag')
When using action: 'flag', toxic reviews include moderation scores:
{
"id": "trustpilot_99xyz",
"provider": "trustpilot",
"rating": 1,
"text": "Worst hotel ever, staff was incredibly rude and [offensive content]...",
"author": { "name": "Angry Guest" },
"publishedAt": "2025-03-19T08:00:00Z",
"moderation": {
"flagged": true,
"scores": {
"toxicity": 0.89,
"severeToxicity": 0.45,
"insult": 0.92,
"profanity": 0.87,
"threat": 0.12,
"identityAttack": 0.05
},
"provider": "perspective"
}
}Example: Clean Review (action: 'flag')
Non-toxic reviews also get scores (useful for analytics):
{
"id": "google_places_reviews_0",
"provider": "google",
"rating": 4,
"text": "Great location, clean rooms. Breakfast could be better.",
"author": { "name": "Sarah M." },
"publishedAt": "2025-03-18T10:15:00Z",
"moderation": {
"flagged": false,
"scores": {
"toxicity": 0.08,
"severeToxicity": 0.01,
"insult": 0.05,
"profanity": 0.02,
"threat": 0.00,
"identityAttack": 0.00
},
"provider": "perspective"
}
}Threshold Guide
| Threshold | Sensitivity | Filters |
|---|---|---|
| 0.5 | High — aggressive filtering | Mild negativity, sarcasm, borderline content |
| 0.7 | Recommended — balanced | Clear insults, profanity, harassment |
| 0.9 | Low — only extreme cases | Severe hate speech, explicit threats |
Environment Variables
# Google Perspective API (recommended)
NUXT_REVIEWS_MODERATION_API_KEY=your_perspective_api_key
# Or OpenAI
NUXT_REVIEWS_MODERATION_API_KEY=your_openai_api_keyGetting a Perspective API Key
- Go to Google Cloud Console
- Enable the Perspective Comment Analyzer API
- Create an API key
- No billing required for moderate usage
Mock Provider
For development and testing without API keys, use the built-in mock provider:
// nuxt.config.ts
reviews: {
providers: {
mock: {}, // no configuration needed
},
}Returns 12 realistic hotel reviews in 7 languages (TR, EN, FR, DE, ES, IT, RU, JA) with ratings, author info, business responses, and verified flags. Supports all standard filters (language, limit, sort, minRating).
<script setup>
const { reviews, aggregate } = useReviews({ provider: 'mock' })
// or with filters
const { reviews: trOnly } = useReviews({ provider: 'mock', language: 'tr' })
</script>Component Kit
Four zero-dependency components are auto-registered and ready to use:
<ReviewStars>
<ReviewStars :rating="4.5" size="md" />
<!-- size: 'sm' | 'md' | 'lg' --><ReviewCard>
<ReviewCard :review="review" :show-provider="true" :show-response="true" :truncate="200" /><ReviewList>
<ReviewList :reviews="reviews" :loading="pending" :columns="2" show-provider>
<!-- Optional custom card slot -->
<template #review="{ review }">
<MyCustomCard :review="review" />
</template>
</ReviewList><ReviewSummary>
<ReviewSummary :aggregate="aggregate" title="Customer Reviews" :show-distribution="true" />Full example
<script setup>
const { reviews, aggregate, pending } = useReviews()
useReviewSchema(reviews, aggregate, {
name: 'Grand Hotel Istanbul',
url: 'https://example.com',
})
</script>
<template>
<ReviewSummary :aggregate="aggregate" title="Guest Reviews" />
<ReviewList :reviews="reviews" :loading="pending" :columns="2" show-provider />
</template>Schema.org JSON-LD
useReviewSchema injects structured data automatically for Google rich snippets (star ratings in search results):
<script setup>
const { reviews, aggregate } = useReviews()
useReviewSchema(reviews, aggregate, {
name: 'Grand Hotel Istanbul',
description: 'Luxury hotel in the heart of Istanbul',
url: 'https://example.com/hotel',
image: 'https://example.com/hotel.jpg',
type: 'Hotel', // defaults to 'LocalBusiness'
})
</script>Generates a <script type="application/ld+json"> block with AggregateRating and up to 20 individual Review entries — the minimum Google needs to show stars in search results.
Providers
| Provider | Free | Pagination | Notes | |---|---|---|---| | Mock | Yes | No | 12 multilingual reviews, no API key needed | | Google Places | $200/mo credit | No (5 max) | Official API, limited reviews | | Trustpilot | Yes | Yes | Best free option with full pagination | | SerpAPI | No (~$50/mo) | Yes | Google reviews without the 5-review limit | | Outscraper | Free tier | Yes | Pay-per-use, good for bulk | | Booking.com (beta) | Partner only | Yes | Requires Connectivity Partner credentials |
Environment Variables
API keys are injected via runtime config and support NUXT_ prefix override:
NUXT_REVIEWS_PROVIDERS_GOOGLE_API_KEY=...
NUXT_REVIEWS_PROVIDERS_TRUSTPILOT_API_KEY=...
NUXT_REVIEWS_PROVIDERS_BOOKINGCOM_USERNAME=...
NUXT_REVIEWS_PROVIDERS_BOOKINGCOM_PASSWORD=...Contribution
# Install dependencies
pnpm install
# Generate type stubs
pnpm run dev:prepare
# Develop with the playground
pnpm run dev
# Build the playground
pnpm run dev:build
# Run ESLint
pnpm run lint
# Run Vitest
pnpm run test
pnpm run test:watch
# Release new version
pnpm run release