Skip to content

When Ratings Are Off, Hiding the Badge Is Not Enough

Adityo Guni Waluyo

Switching the rating flag off is not enough while card badges and aggregateRating markup still ship. My notes on sweeping every surface clean.

TL;DR

Switching off ratings hid the detail page section but cards and JSON-LD still showed the old score from cached property data. That mismatch between what users see and what crawlers read risks spam penalties. Now the rating_enabled flag gates both the UI badge and the structured data so everything disappears together.

I had just switched the rating feature off in the WisataKota project admin dashboard. The attraction detail page went clean instantly, the whole review section gone without a trace. But when I opened the homepage, something odd caught my eye. The attraction cards were still showing off a 4.5 star badge.

My first thought was browser cache. I hard-refreshed, even tried an incognito window. Same result. The stars were still there on the listing cards. Worse, when I right-clicked and hit View Page Source, the aggregateRating markup was still printed neatly inside the JSON-LD block.

My first guess was wrong. I assumed the earlier change that cut the review fetch on the detail page was enough to kill the feature globally. Turns out the gating logic had never reached the card, list, and map components, and most dangerously if missed, the structured data generator.

Leftover Traces in Cards and JSON-LD

A review snippet is basically an average of the combined rating scores from many reviewers [1]. To show a valid rich snippet, the system must supply that average rating [1]. The problem appears when the feature is switched off at the business level but the presentation and data layers fall out of sync.

In the WisataKota project, the previous change had already stopped the review fetch on the detail page. The backend request was indeed gone. But the card components on other pages were still reading leftover rating data from the main property object, not from the separate review endpoint.

So even though no new reviews could load, the attraction cards kept showing the old rating. Worse, the function generating the SEO schema markup kept running undisturbed. It kept injecting AggregateRating data into the HTML even though the feature was visually gone. That type itself is defined as an average rating from multiple ratings or reviews [2].

Search engines do not read your monitor. They read the HTML and JSON-LD markup you serve. When the interface shows no rating at all but the structured markup still claims a high rating exists, that creates a discrepancy.

Search spam policies explicitly cover misleading or hidden structured data [3]. Engines treat that as an attempt to deceive users or manipulate rankings. I have no exact number for the ranking impact, and there is no public number for a case like this, but the risk of a manual penalty or a drop in organic visibility is very real.

Consistency between what humans see and what crawlers read is non-negotiable.

Flag Off Means Every Surface Off

The fix is not adding display: none in CSS. When a flag kills a feature, every surface advertising that feature must die with it. That includes the visual badges and the invisible JSON-LD data. In React, this kind of conditional rendering is a built-in idiom using if, &&, or ternary syntax [4].

I updated the conditional logic in the card component. Instead of only checking whether rating data exists, the component must check the rating_enabled flag first.

// /src/components/ListingCard.tsx
interface CardProps {
  place: PlaceData;
  isRatingEnabled: boolean;
}

export function ListingCard({ place, isRatingEnabled }: CardProps) {
  return (
    <div className="card">
      <h3>{place.name}</h3>
      {isRatingEnabled && place.rating && (
        <div className="rating-badge">
          &#9733; {place.rating.average}
        </div>
      )}
    </div>
  );
}

The same change belongs in the structured data generator. It must no longer assume that rating data sitting in the object may be published to the schema.

// /src/lib/seo/generateStructuredData.ts
export function generatePlaceSchema(place: PlaceData, isRatingEnabled: boolean) {
  const baseSchema = {
    "@context": "https://schema.org",
    "@type": "TouristAttraction",
    "name": place.name,
  };

  if (isRatingEnabled && place.rating) {
    return {
      ...baseSchema,
      "aggregateRating": {
        "@type": "AggregateRating",
        "ratingValue": place.rating.average,
        "reviewCount": place.rating.count
      }
    };
  }

  return baseSchema;
}

With this approach, the isRatingEnabled flag becomes the single gate controlling both the visual output and the structured data output.

The verification is not guesswork either. I open the page that should be clean, view source, and search for the string aggregateRating. If it still shows up, some generator is not gated yet. Then I check the homepage, the category list, and the map one by one. No card may display any rating element.

The rule I hold now: every time I switch a feature off, its traces go away as one complete package. Badges, fetch, and JSON-LD die together. Never let the remains of a dead feature keep haunting production code as a ghost.

Sources

1. Google Search Central, review snippet guidelines, review-snippet docs
2. Schema.org, AggregateRating type, AggregateRating
3. Google Search Central, spam policies, spam policies
4. React docs, conditional rendering, conditional rendering
5. Internal article, review-mati-tetap-404

Related articles