cursor

Como integrar Google Reviews en Nuxt 3

Cómo mostrar las reseñas de Google en tu sitio web desarrollado con Nuxt 3 y mejorar la confianza de tus visitantes.

Por hdezzz
Imagen sin descripción

Si tienes un proyecto de desarrollo web personalizado en Nuxt 3 y quieres dar un plus de confianza a tus visitantes, integrar reseñas de Google (Google Reviews) es una excelente opción. No solo sirve para mostrar la opinión de tus clientes satisfechos, sino que también ayuda a mejorar la credibilidad de tu marca y tu presencia online.

A continuación, te explico paso a paso cómo hacerlo.

¿Por qué Integrar Google Reviews en tu página web?

  • Mayor confianza: Las reseñas positivas generan cercanía con los usuarios, animándolos a interactuar y contratar servicios de desarrollo web o diseño.
  • Mejora de la reputación: Google valora mucho las interacciones reales, lo que puede favorecer tu posicionamiento en búsquedas locales o de tu sector.
  • Aporte de valor: Mostrar testimonios de clientes reales ayuda a diferenciarte de la competencia y refuerza tu estrategia de marketing digital.
site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full

Configura tu proyecto en Nuxt 3

  • Crea o abre tu proyecto Nuxt 3.
  • Asegúrate de tener instalados los paquetes necesarios (Node.js, npm o Yarn).
  • Ejecuta npm install (o yarn install) para verificar que todos tus módulos estén al día.
site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
npx nuxi init mi-proyecto
cd mi-proyecto
npm install
npm i tailwindcss
npm i nuxt-swiper
modules: [
    '@nuxtjs/tailwindcss',
    'nuxt-swiper'
],

Paso 2: Obtén la Clave de API de Google Places

Para acceder a las reseñas de Google, necesitas una clave de API:

  • Ve a la Google Cloud Platform.
  • Crea un nuevo proyecto o selecciona uno existente.
  • Navega a APIs y Servicios > Habilitar APIs y Servicios.
  • Busca y habilita la Google Places API.
  • En Credenciales, crea una clave de API.
  • Restricción de API: Por seguridad, restringe el uso de la clave a la API de Places.

También necesitarás el id de lugar, que es el identificador que recibe las reseñas. Puedes buscar tu id en Places API.

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full

Asegura Tus Claves de API

Para mantener la seguridad, es recomendable utilizar variables de entorno. Crea, si no lo tienes ya, un archivo .env en la raíz de tu proyecto:

Nota: Asegúrate de reemplazar 'NUXT_PUBLIC_GOOGLE_MAP_API_KEY' con tu clave de API real. Además, es recomendable almacenar las claves de API en variables de entorno para mayor seguridad.

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
NUXT_PUBLIC_GOOGLE_MAP_API_KEY=TU_CLAVE_API

Y añádelas a tu archivo nuxt.config para poder acceder a ellas desde cualquier componente:

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
runtimeConfig: {
    public: {
        googleMapsApiKey: process.env.NUXT_PUBLIC_GOOGLE_MAP_API_KEY,
    },
},

Crea un composable para obtener las reseñas

Para mantener tu proyecto web personalizado ordenado, lo ideal es centralizar las peticiones a Google en un endpoint dentro de tu proyecto Nuxt:

  • En la carpeta server/api/, crea un archivo (por ejemplo, reviews.js o reviews.ts).
  • Importa las dependencias necesarias para realizar solicitudes HTTP.
  • Haz una petición GET a la API de Google Places usando tu clave (API Key).
  • Devuelve los datos que necesites (nombre del usuario, calificación, texto de la reseña, etc.).
site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
// server/api/get-google-reviews.js
import { getQuery } from 'h3';

export default defineEventHandler(async (event) => {
    const { placeId } = getQuery(event);

    if (!placeId) {
        return { error: 'El placeId es requerido.' };
    }

    const config = useRuntimeConfig();
    const apiKey = config.public.googleMapsApiKey;
    const apiUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=rating,user_ratings_total,reviews&language=es&key=${apiKey}`;

    try {
        const response = await fetch(apiUrl);
        const data = await response.json();

        if (data?.result?.reviews && data.result.reviews.length > 0) {
            // Puedes filtrar las reseñas para mostrar, por ejemplo, sólo las de 4 y 5 estrellas: review.rating >= 4
            // para mostrarlas todas review.rating >= 1
            const filteredReviews = data.result.reviews
                .filter((review) => review.rating >= 1)
                .slice(-100);

            return {
                averageRating: data.result.rating,
                totalReviews: data.result.user_ratings_total,
                reviews: filteredReviews,
            };
        } else {
            return { error: 'No se encontraron reseñas.' };
        }
    } catch (err) {
        console.error('Error al llamar a la API de Google Places:', err);
        return { error: 'Error al obtener las reseñas' };
    }
});

Crea un componente para mostrar las reseñas

Crea un componente llamado GoogleReviews.vue dentro de la carpeta components. He añadido un svg para mostrar las 5 estrellas y un script que calcula el ancho que debe rellenarse.

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
<template>
    <div>
        <!-- <h2 class="text-2xl font-bold">Reseñas de Google</h2> -->
        <div class="flex flex-col lg:flex-row">
            <div v-if="averageRating !== null && totalReviews !== null"
                class="my-4 w-full lg:w-1/6 flex flex-col items-center">
                <div class="rating-text font-bold text-clamp-2xl">
                    <strong class=""> Excelente </strong>
                </div>
                <div class="stars pb-4 w-44" :data-stars="averageRating">
                    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
                        viewBox="0 0 172.4 33.6">
                        <g>
                            <defs>
                                <path id="SVGID_1" d="M17.8,1.1L23,11.5l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4L7.5,32.7l2-11.4l-8.4-8.1l11.6-1.7L17.8,1.1z
                                M52,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4L52,27.3l-10.3,5.4l2-11.4l-8.4-8.1l11.6-1.7L52,1.1z M86.2,1.1l5.2,10.4l11.6,1.7
                                l-8.4,8.1l2,11.4l-10.3-5.4l-10.3,5.4l2-11.4l-8.4-8.1L81,11.5L86.2,1.1z M120.4,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4
                                L110,32.7l2-11.4l-8.4-8.1l11.6-1.7L120.4,1.1z M154.6,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4l-10.3,5.4l2-11.4
                                l-8.4-8.1l11.6-1.7L154.6,1.1z" stroke="#F6BB09" />
                            </defs>
                            <clipPath id="SVGID_2">
                                <use xlink:href="#SVGID_1" style="overflow:visible;" />
                            </clipPath>
                            <rect class="bg-stars [clip-path:url(#SVGID_2)] fill-gold-1" x="0.7" y="0.1"
                                :width="calculateWidth(averageRating)" height="33.5" fill="#F6BB09" />
                            <use xlink:href="#SVGID_1"
                                style="overflow:visible;fill:none;stroke:#F6BB09;stroke-miterlimit:10;" />
                        </g>
                    </svg>
                    <p class="text-xs font-light mb-0 text-center">Valoración media: {{ averageRating }} / 5</p>
                </div>
                <span class="nowrap">En base a <strong>{{ totalReviews }} reseñas</strong></span>
                <img loading="lazy" src="~/assets/images/icons/google-logo.svg" alt="Logo Google" class="w-24" />
            </div>

            <!-- Mostrar reseñas filtradas -->
            <div class="relative w-5/6">
                <button class="swiper-button-prev absolute left-0 top-1/2 -translate-y-1/2 z-10">
                    ←
                </button>
                <button class="swiper-button-next absolute right-0 top-1/2 -translate-y-1/2 z-10">
                    →
                </button>
                <ClientOnly>
                    <swiper-container
                        class="mySwiper px-6 max-w-full list-none lg:px-6 min-h-96 flex flex-col lg:flex-row gap-6 w-full lg:w-5/6"
                        v-if="reviews.length" ref="reviewsSlider" :init="false">
                        <swiper-slide v-for="(review, index) in reviews" :key="index"
                            class="p-6 mt-2 mb-12 bg-white border border-gray-300 rounded-md w-full">
                            <div class="flex items-center gap-4 mb-4">
                                <img loading="lazy" :src="review.profile_photo_url" :alt="review.author_name" width="40"
                                    height="40" class="size-10 rounded-full">
                                <div class="w-full">
                                    <p class="font-semibold mb-0">{{ review.author_name }}</p>
                                    <p class="text-xs font-light mb-0">{{ review.relative_time_description }}</p>
                                </div>
                                <img src="~/assets/images/icons/google-color-icon.svg" alt="Google" width="24" height="24"
                                    class="size-6" />
                            </div>
                            <!-- <p class="text-sm">Calificación: {{ review.rating }} estrellas</p> -->
                            <div class="stars pb-4 w-24" :data-stars="review.rating">
                                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
                                    y="0px" viewBox="0 0 172.4 33.6">
                                    <g>
                                        <defs>
                                            <path id="SVGID_1" d="M17.8,1.1L23,11.5l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4L7.5,32.7l2-11.4l-8.4-8.1l11.6-1.7L17.8,1.1z
                                         M52,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4L52,27.3l-10.3,5.4l2-11.4l-8.4-8.1l11.6-1.7L52,1.1z M86.2,1.1l5.2,10.4l11.6,1.7
                                         l-8.4,8.1l2,11.4l-10.3-5.4l-10.3,5.4l2-11.4l-8.4-8.1L81,11.5L86.2,1.1z M120.4,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4
                                         L110,32.7l2-11.4l-8.4-8.1l11.6-1.7L120.4,1.1z M154.6,1.1l5.2,10.4l11.6,1.7l-8.4,8.1l2,11.4l-10.3-5.4l-10.3,5.4l2-11.4
                                         l-8.4-8.1l11.6-1.7L154.6,1.1z" stroke="#F6BB09" />
                                        </defs>
                                        <clipPath id="SVGID_2">
                                            <use xlink:href="#SVGID_1" style="overflow:visible;" />
                                        </clipPath>
                                        <rect class="bg-stars [clip-path:url(#SVGID_2)] fill-gold-1" x="0.7" y="0.1"
                                            :width="calculateWidth(review.rating)" height="33.5" fill="#F6BB09" />
                                        <use xlink:href="#SVGID_1"
                                            style="overflow:visible;fill:none;stroke:#F6BB09;stroke-miterlimit:10;" />
                                    </g>
                                </svg>
                            </div>
                            <div class="text-wrapper">
                                <p class="italic text-sm">"{{ review.text }}"</p>
                            </div>
                        </swiper-slide>
                    </swiper-container>
                </ClientOnly>
            </div>
        </div>
    </div>
</template>
<script setup>
    import { ref, onMounted } from 'vue';

    const placeId = 'TU_PLACE_ID_AQUÍ';
    // Cargamos las reseñas y calificaciones en el servidor usando useAsyncData
    const { data, error } = await useAsyncData(async () => {
        return await $fetch(`/api/get-google-reviews?placeId=${placeId}`);
    })

    const reviewsSlider = ref(null)
    const reviews = ref(data.value?.reviews || [])
    const averageRating = ref(data.value?.averageRating || null)
    const totalReviews = ref(data.value?.totalReviews || null)

    // Configuración de swiper slider
    useSwiper(reviewsSlider, {
        loop: true,
        autoHeight: true,
        slidesPerView: 1,
        spaceBetween: 30,
        navigation: {
            nextEl: '.swiper-button-next',
            prevEl: '.swiper-button-prev',
        },
        pagination: {
            clickable: true,
        },
        autoplay: {
            delay: 5000,
        },
        breakpoints: {
            '768': {
                slidesPerView: 2,
                spaceBetween: 40,
            },
            '1280': {
                slidesPerView: 3,
                spaceBetween: 40,
            },
        },
    });

    // Función para calcular el ancho basado en la valoración
    function calculateWidth(rating) {
        return `${(rating * 20) * 171.7 / 100}`;
    }
</script>
<style scoped>
    .fill-gold-1 {
        @apply fill-[#F6BB09]
    }

    .swiper-button-prev,
    .swiper-button-next {
        @apply bg-white text-black shadow-md rounded-full p-2 w-10 h-10 flex items-center justify-center hover:bg-gray-100 transition;
    }
</style>

Muestra las reseñas en tu frontend

  • En el componente de tu página (por ejemplo, pages/index.vue), haz una llamada al endpoint que creaste.
  • Renderiza la lista de reseñas en un layout atractivo.
  • Personaliza estilos y formato para que se adapte a tu diseño (colores, tipografía y estructura de tu sitio web personalizado).
site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full
<template>
    <div class="reviews-title">
        Reseñas de nuestros clientes
    </div>
    <GoogleReviews />
</template>

<script setup>
    import GoogleReviews from '~/components/GoogleReviews.vue'
</script>

<style scoped>
    .reviews-title {
        font-family: Arial, Helvetica, sans-serif;
        font-size: 3rem;
        padding: 2rem;
    }
</style>

Ajusta el diseño y la experiencia de usuario

Para que la sección de reseñas luzca profesional y cohesionada, cuida aspectos como:

  • Tipografías y colores: que respeten la identidad visual de tu marca.
  • Distribución en pantalla: un layout tipo grid o carrusel puede hacer que las reseñas sean más atractivas.
  • Llamadas a la acción: aprovecha para guiar a los usuarios hacia un formulario de contacto o hacia otros servicios que ofrezcas.

He subido el proyecto a github para que lo descargues si lo necesitas: Nuxt Google Reviews

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full

Conclusión

Integrar Google Reviews en Nuxt 3 es un paso sencillo y muy poderoso para fortalecer la confianza en tus servicios de desarrollo web personalizado. Siguiendo estos pasos, conseguirás destacar tus reseñas de forma elegante y ordenada, potenciando tanto tu reputación online como la experiencia de tus usuarios.

¿Listo para dar el siguiente paso?
Aprovecha al máximo la credibilidad que aportan las reseñas de Google y haz que tu proyecto Nuxt 3 brille con luz propia. ¡Si tienes dudas o necesitas asesoría para tu próximo sitio web, no dudes en ponerte en contacto con nosotros!

site-width grid grid-cols-12 gap-4 py-20 xl:py-20 separator-bottom col-span-full

Preguntas frecuentes

¿Por qué es importante integrar las reseñas de Google en mi sitio web?

¿Qué beneficios SEO obtengo al mostrar reseñas de Google en mi sitio?

¿Puede afectar la velocidad de carga de mi web la integración de Google Reviews?

Reimagina tu marca. Reimagina tu web

ESTUDIO DEDISEÑO CREATIVO

08035 BARCELONA