Google Reviews Extra

Can anyone recommend an extra / snippet / plugin / method to include google reviews from a certain google business profile on a MODX3 website?

I’m not aware of one. It might be possible to develop your own using cURL. You could turn on Dev. Tools in Chrome (Ctrl-shift-i) and load a page that has reviews on it to see what calls Google is making to get the reviews, then call them yourself with cURL.

Since I’ve never seen any reviews outside of Google, I suspect that they require a secret token for the calls or make them internally. In that case, you might be able to use cURL to get the Google search result page and extract the reviews from the source.

Thanks Bob. I’ve tried various methods that have been described on Stack Overflow but they all seem to lead to dead ends.

There’s this extra for MODX but I don’t think it works with MODX3… Elfsight Google Reviews 1.0.0-pl | MODX

…and the problem with this type of extra is, they tend to be pretty intrusive in terms of the info you have to ‘give’ away to use it.

My technical knowledge isn’t good enough to build anything myself unfortunately.

I once wrote a small Google Reviews Snippet for https://architex.ch/. I have no idea how to make a Modx Extra out of it, but I can put the code in here if that’s enough for you. But it only shows the overall rating, not the Review-Comments. I already had the API call so far that the comments are also loaded, but then did not pursue it any further, as it was not needed by the client.
If the functionality is enough for you, let me know and I’ll translate the comments into English and post it here.

Bildschirmfoto 2024-02-27 um 14.32.06

2 Likes

I thought that Google might consider the reviews to be their intellectual property, but apparently not since you can get them with the Google API.

Hi Desper - If you have a working API call it would be good to see, provided it doesn’t take up too much of your time. Tahnk you.

Yes they do make them available but I have no idea how to use the API call.

I will also add the other parts beside the Snippet with the API Call. Maybe it is easier to get the hole picture then.

I use a Setting (ClientConfig) for the Google Place ID. If it’s not empty the following code will be used:
HTML:

[[++googlePlaceId:notempty=`
    <div id="google-bewertung-container" class="google-bewertung-container" target="blank">
            <a href="[[!getGooglePlaceDetails? &link=`1`]]" target="black" class="google-bewertung-wrapper">
                <div class="google-bewertung">   
                    <div class="google-logo">
                        <img src="/template/images/Google__G__Logo.png" alt="Google Bewertungen">
                    </div>
                    <div class="google-bewertung--content">          
                        <div class="google-bewertung--content--head">Google Bewertung</div>
                        <div class="google-bewertung--content--rating">
                            <div class="google-bewertung--content--rating-">
                                [[!getGooglePlaceDetails? &rating=`1`]]&nbsp;
                            </div>
                            <div class="google-bewertung--content--rating--text">
                                [[!getGooglePlaceDetails? &stars=`1`]]
                            </div>
                        </div>
                        <div class="google-bewertung--content--total">
                            Basierend auf [[!getGooglePlaceDetails? &totalRatings=`1`]] Rezensionen<br>
                        </div>
                    </div>
                </div>
                <div class="extrernalGoogle">Auf Google Maps ansehen</div>
            </a>
        <a id="google-close" class="google-close">
            <i class="fa-solid fa-xmark"></i>
        </a>
    </div>
    `]]

Script at body end (this is just for the behaviour of the module, so it does not overlay the Footer Links and stays closed for 24 hours after closing, by setting a cookie)

[[++googlePlaceId:notempty=`
<script src="../template/js/js.cookie.min.js"></script>
<script>
    // Google Box nach oben schieben wenn ende der Seite ereicht ist damit datenschutz link im Footer nicht überdeckt wird. 
    document.addEventListener("scroll", function() {
        var footer = document.querySelector('footer');
        var box = document.querySelector('.google-bewertung-container');
        var footerRect = footer.getBoundingClientRect();
        var viewportHeight = window.innerHeight;
    
        if (footerRect.top < viewportHeight) {
            // Footer ist sichtbar
            box.classList.add('google-bewertung--raised');
        } else {
            // Footer ist nicht sichtbar
            box.classList.remove('google-bewertung--raised');
        }
    });

    //cookie setzen und box schließen
    const googleBewertungClose = document.getElementById('google-close');
    const googleBewertung = document.getElementById('google-bewertung-container');
    const googleBewertungRemember = Cookies.get('googleBewertungStayClosed');
    
    googleBewertungClose.addEventListener('click', (event) => { 
        Cookies.set('googleBewertungStayClosed', '1' , { expires: 1 });
        googleBewertung.style.display = "none";  
    });
    
    if (googleBewertungRemember == null){
        googleBewertung.style.display = "block";  
    }

</script>
`]]

SCSS: It’s almost just CSS in this part

.google-bewertung-container {
    position: fixed;
    bottom: 10px;
    right: 10px;   
    transition: all .1s ease-in-out;
    transform: translateY(0px);
    display: none;
    z-index: 5;
}

.google-bewertung-wrapper{
    text-decoration: none;
    color: inherit;
}

.google-bewertung {
    width: auto;
    height: auto;
    background-color: #ffffff;
    border-top: 6px solid #4fce6a;
    box-shadow: 0px 0px 8px #0000005c;
    font-family: arial;
    font-size: 14px;
    display: flex;
    padding: 10px;
    align-items: center;
    border-radius: 3px;
    color: inherit;
    text-decoration: none;
    position: relative;
    z-index: 2;
}

.google-bewertung--raised {
   transform: translateY(-80px);
   transition: all .1s ease-in-out;
}

.google-bewertung--content--rating{
    display: flex;
}

.google-logo{
    display: flex;
    align-items: center;
}

.google-logo img{
    width: 40px;
    height: 40px;
    margin: 10px;
    margin-left: 0;
    line-height: 1.5em;
}

.google-bewertung--content--rating{
    color: #e7711b;
    font-weight: bold;
    font-size: 17px;
    margin-top: 3px;
}

.google-bewertung--content--total {
    opacity: .8;
}

.star-rating {
    position: relative;
    unicode-bidi: bidi-override;
    color: #c5c5c5;
    font-size: 17px;
    line-height: 1;
    width: 100px;
    height: 17px;
    overflow: hidden;
    letter-spacing: 4px;
}

.star-rating-top {
    position: absolute;
    z-index: 1;
    overflow: hidden;
    color: #e7711b;
    white-space: nowrap;
}

.star-rating-bottom {
    padding: 0;
    display: block;
    z-index: 0;
}

.review-item {
    margin-top: 10px;
}

.review-rating {
    font-weight: bold;
}

.review-text {
    font-style: italic;
}


.google-close{
    position: absolute;
    right: 8px;
    top: 10px;
    padding: 5px;
    font-size: 20px;
    color: #000000;
    opacity: .5;
    transition: all .1s ease-in-out;
    font-family: Arial, Helvetica, sans-serif;
    z-index: 3;
    &:hover{
        opacity: 1;
        cursor: pointer;
    }
}


.google-bewertung-wrapper:hover .google-bewertung{
    border-top-left-radius: 0px;
    border-top-right-radius: 0px;
    transition: all .3s ease-in-out;
}

.extrernalGoogle {
    position: absolute;
    top: 0;
    z-index: 1;
    width: 100%;
    background: #3f86f4;
    transform: translateY(4px);
    font-size: .7em;
    padding: 0px 6px 3px;
    box-sizing: border-box;
    box-shadow: 0 0 0px rgb(0 0 0 / 28%);
    color: #ffffffcc;
    font-family: arial;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    transition: all .3s ease-in-out;
}

.google-bewertung-wrapper:hover .extrernalGoogle{
    transform: translateY(-86%);
    transition: all .3s ease-in-out;
    box-shadow: 0 0 5px rgb(0 0 0 / 28%);
}

@media(max-width:1000px){
    .google-bewertung--raised{
        transform: translateY(-120px);
    }
}

@media(max-width:450px){
    .google-bewertung--raised{
        transform: translateY(-175px);
    }
}

Snippet with API Call getGooglePlaceDetails

<?php
// Google API Key and Place ID
$apiKey = 'Your API Key';
$placeId = $modx->getOption('googlePlaceId'); //google place id
/*google place id finder https://geo-devrel-javascript-samples.web.app/samples/places-placeid-finder/app/dist/*/

// Check whether a place ID is available
if (!$placeId) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'Place ID ist nicht gesetzt.');
    return 'Place ID ist nicht gesetzt.';
}

// Set language (e.g. 'de' for German)
$language = 'de';

// Determine which information is to be queried
$includeRating = $modx->getOption('rating', $scriptProperties);
$includeStars = $modx->getOption('stars', $scriptProperties);
$includeTotalRatings = $modx->getOption('totalRatings', $scriptProperties);
$includeReviews = $modx->getOption('reviews', $scriptProperties);
$includeLink = $modx->getOption('link', $scriptProperties);

$fields = [];
if ($includeRating || $includeStars) {
    $fields[] = 'rating';
}
if ($includeTotalRatings) {
    $fields[] = 'user_ratings_total';
}
if ($includeReviews) {
    $fields[] = 'reviews';
}
$fieldsString = implode(',', $fields);

$url = 'https://maps.googleapis.com/maps/api/place/details/json?placeid=' . $placeId . '&fields=' . $fieldsString . '&language=' . $language . '&key=' . $apiKey;

// cURL-Anfrage
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($curl);
$curlError = curl_error($curl);
curl_close($curl);

// Check for cURL errors
if ($response === false) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'cURL-Fehler bei Google Places API-Anfrage: ' . $curlError);
    return 'Ein Fehler ist aufgetreten.';
}

// Convert answer into an array
$data = json_decode($response, true);

// Check for errors in the API response
if (isset($data['error_message'])) {
    $modx->log(modX::LOG_LEVEL_ERROR, 'Google Places API-Fehler: ' . $data['error_message']);
    return 'Ein Fehler ist aufgetreten.';
}

// Extract data from the response
$output = '';
if (isset($data['result'])) {
    if ($includeRating) {
        $output .= "<span class='rating-value'>" . $data['result']['rating'] . "</span>";
    }
    if ($includeStars) {
        $rating = $data['result']['rating'];
        $widthPercentage = ($rating / 5) * 100; // Convert width to per cent
        $output .= "<div class='star-rating'>";
        $output .= "<div class='star-rating-top' style='width: {$widthPercentage}%;'>";
        $output .= str_repeat('★', 5);
        $output .= "</div>";
        $output .= "<div class='star-rating-bottom'>";
        $output .= str_repeat('★', 5);
        $output .= "</div>";
        $output .= "</div>";
    }
    if ($includeTotalRatings && isset($data['result']['user_ratings_total'])) {
        $output .= "<span class='total-ratings'>" . $data['result']['user_ratings_total'] . " </span>";
    }
    if ($includeReviews && isset($data['result']['reviews'])) {
        $reviews = $data['result']['reviews'];
        foreach ($reviews as $review) {
            if (isset($review['rating']) && isset($review['text'])) {
                $output .= "<div class='review-item'><span class='review-rating'>" . $review['rating'] . "/5</span>, <span class='review-text'>" . $review['text'] . "</span></div>";
            }
        }
    }
    // Add link to Google Maps
    if ($includeLink) {
        $googleMapsUrl = "https://www.google.com/maps/place/?q=place_id:" . $placeId;
        $output .= $googleMapsUrl;
    }
}

return $output;

I translated the comments but there is still some german. I hope it is still understandable

3 Likes

That’s great thank you Desper. I will see if I can make it work.