npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nuxt-reviews

v0.1.3

Published

A Nuxt module to fetch and aggregate reviews from multiple tourism platforms (Google, Trustpilot, SerpAPI, Outscraper, Booking.com)

Readme

npm version npm downloads License Nuxt

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-importeduseReviews() and useReviewSchema() 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-reviews

Add 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

  1. Reviews are fetched from providers (Google, Trustpilot, etc.)
  2. minRating filter is applied (if configured)
  3. Each review text is sent to the moderation API for toxicity analysis
  4. Based on action:
    • filter: Reviews with toxicity >= threshold are removed
    • flag: All reviews kept, flagged ones get moderation field attached
  5. 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_key

Getting a Perspective API Key

  1. Go to Google Cloud Console
  2. Enable the Perspective Comment Analyzer API
  3. Create an API key
  4. 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

License

MIT - Eralp Ozcan