Restaurant review platforms have a trust problem that star ratings cannot fully solve. User reviews are subjective, gameable, and filtered through individual taste preferences. A restaurant with 4.2 stars might have 200 ecstatic reviews for its food and three alarming mentions of stomach issues buried on page six. A health inspection score is different: it's objective, government-sourced, and specifically designed to measure food safety - exactly what a percentage of your users care about most when deciding where to eat.
Adding health inspection data to a review platform is not a big engineering lift, but doing it well requires decisions about UI placement, information density, graceful degradation, legal framing, and A/B testing strategy. This guide walks through each of these for teams building Yelp-style, TripAdvisor-style, or OpenTable-style experiences.
Where to Place the Health Score in Your UI
The placement question depends on your platform's conversion model and your user's decision context. There are three primary surfaces where health data adds value:
1. Restaurant Detail Page - Full Treatment
The detail page is where users are already committed to evaluating a specific restaurant. They've clicked; they want information. This is the right place for the full health treatment: grade badge, score out of 100, inspection date, and a summary of violation highlights (e.g., "No critical violations in most recent inspection").
Placement on the detail page: immediately below or adjacent to the restaurant name and aggregate star rating, in the trust signal cluster that typically also contains price range, cuisine type, and accreditations. Health grade is a peer of star rating in terms of user decision weight - it belongs in that same visual tier, not buried in a "more info" accordion.
2. Search Results List - Compact Badge Only
In search result cards, information density is constrained. A compact health badge showing grade letter (A/B/C/F) or a small color-coded indicator is appropriate. Full score and violation details are too much for a card context. The goal is to allow grade-filtering behavior (users who want A grades only can visually scan) without cluttering the primary decision signals (name, photos, star rating, cuisine, distance).
Placement in search cards: secondary line with price range, cuisine, and distance. A single badge requires only 40-60px of width and adds a meaningful scannable signal without dominating the card.
3. Map View - Color-Coded Markers
Map-based restaurant discovery benefits from color-coded health grade markers. Green (A), blue (B), yellow (C), red (F), gray (no data). Users scanning a map can immediately identify safe areas for dining with strong compliance records. This is especially useful for tourists unfamiliar with specific restaurants and for users with health or dietary concerns that make food safety a higher priority.
The geo search endpoint makes this efficient: a single API call with latitude, longitude, and radius returns all covered establishments with their current health scores, avoiding N+1 lookups as the user pans the map.
What Information to Show
The right level of detail varies by surface and user intent. Here's a tiered information hierarchy:
| Surface | Show | Don't Show |
|---|---|---|
| Search list card | Grade letter (A/B/C/F), color indicator | Numeric score, violation details, inspection date |
| Map marker | Color-coded pin, grade letter on hover | Any text in the marker itself |
| Detail page - header | Grade badge with numeric score, inspection date | Raw violation codes |
| Detail page - expanded | Violation summary, critical count, trend over last 3 inspections | Internal API fields, raw government codes |
The grade badge is the atomic unit. Every surface that shows health data should show at minimum a grade letter (A/B/C/F) with a color indicator. Everything else is additive detail for users who want it.
React Component: Health Score Badge
Here is a production-ready React component implementing the grade badge with all display states, including the no-data fallback:
import React from 'react';
const GRADE_CONFIG = {
A: { color: '#22c55e', bg: 'rgba(34,197,94,0.12)', border: 'rgba(34,197,94,0.3)' },
B: { color: '#60a5fa', bg: 'rgba(96,165,250,0.12)', border: 'rgba(96,165,250,0.3)' },
C: { color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', border: 'rgba(245,158,11,0.3)' },
F: { color: '#ef4444', bg: 'rgba(239,68,68,0.12)', border: 'rgba(239,68,68,0.3)' },
};
function formatInspectionDate(dateStr) {
if (!dateStr) return null;
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
}
function getMonthsAgo(dateStr) {
if (!dateStr) return Infinity;
const ms = Date.now() - new Date(dateStr).getTime();
return Math.floor(ms / (1000 * 60 * 60 * 24 * 30));
}
export function HealthScoreBadge({
score,
grade,
inspectionDate,
compact = false,
showDate = true,
onClick
}) {
// No data state
if (!score && !grade) {
return (
<span
className="health-badge health-badge--none"
title="Health inspection data not available for this location"
style={{
display: 'inline-flex', alignItems: 'center', gap: '0.4rem',
padding: compact ? '0.2rem 0.5rem' : '0.35rem 0.75rem',
background: 'rgba(148,163,184,0.08)',
border: '1px solid rgba(148,163,184,0.2)',
borderRadius: '6px', fontSize: compact ? '0.75rem' : '0.8rem',
color: '#8494a7', fontWeight: 600, cursor: onClick ? 'pointer' : 'default'
}}
onClick={onClick}
role={onClick ? 'button' : undefined}
>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#8494a7', display: 'inline-block' }} />
{compact ? 'N/A' : 'Score N/A'}
</span>
);
}
const config = GRADE_CONFIG[grade] || GRADE_CONFIG['F'];
const monthsAgo = getMonthsAgo(inspectionDate);
const isStale = monthsAgo > 12;
const formattedDate = formatInspectionDate(inspectionDate);
return (
<div
style={{ display: 'inline-flex', flexDirection: 'column', gap: '0.2rem' }}
onClick={onClick}
role={onClick ? 'button' : undefined}
style={{ cursor: onClick ? 'pointer' : 'default' }}
>
<span
className={`health-badge health-badge--${grade.toLowerCase()}`}
title={`Health Grade ${grade} (${score}/100) - Inspected ${formattedDate}`}
style={{
display: 'inline-flex', alignItems: 'center', gap: '0.4rem',
padding: compact ? '0.2rem 0.5rem' : '0.35rem 0.75rem',
background: config.bg,
border: `1px solid ${config.border}`,
borderRadius: '6px',
fontSize: compact ? '0.75rem' : '0.8rem',
color: config.color,
fontWeight: 700
}}
>
<span style={{
width: 8, height: 8, borderRadius: '50%',
background: config.color, display: 'inline-block'
}} />
{compact ? `${grade}` : `Grade ${grade}`}
{!compact && score && (
<span style={{ fontWeight: 400, fontSize: '0.75em', opacity: 0.8 }}>
• {score}
</span>
)}
</span>
{showDate && !compact && formattedDate && (
<span style={{
fontSize: '0.7rem', color: isStale ? '#f59e0b' : '#8494a7'
}}>
{isStale ? `Outdated - ` : ''}Inspected {formattedDate}
</span>
)}
</div>
);
}
// Usage:
// <HealthScoreBadge score={94} grade="A" inspectionDate="2026-01-15" />
// <HealthScoreBadge score={78} grade="B" inspectionDate="2025-09-03" compact />
// <HealthScoreBadge /> {/* no data state */}
Handling the "No Data Available" State
A significant percentage of your restaurant catalog will not have health inspection data - either because their jurisdiction is not covered, they are a food truck or pop-up with a different permit type, the establishment is too new to have an inspection record, or the name/address matching failed. Handling this gracefully is as important as handling the data-present states.
The cardinal rule: never show an empty widget or a broken element. The "no data" state should be a designed, intentional UI state that communicates absence without implying a problem.
- Search list cards: Omit the health badge entirely for no-data restaurants, or show a subtle "Score N/A" indicator. Do not show a gray square or broken element.
- Detail pages: Show a clearly-labeled "Health inspection score not available for this location" section where the badge would appear. Include a brief explanation: "We don't currently cover [City/County] health inspections."
- Map markers: Use a gray pin color for no-data establishments. Include a tooltip explaining the absence when the user hovers or taps.
- Filter UI: If you offer a "Grade A only" filter, the results should explicitly note that restaurants without data are excluded from the filter results, not that they failed the filter.
A/B Test Hypotheses for Health Score Integration
If you're introducing health scores to an existing review platform, these are the test hypotheses worth validating:
Test 1: Grade badge placement on detail page
Hypothesis: Showing the health grade badge adjacent to the star rating (above the fold) increases reservation/order conversion for Grade A restaurants by 8-15%.
Test 2: Grade filter in search
Hypothesis: Adding a health grade filter (Grade A or B only) to search results increases sessions-per-user by 12%+ as users find a reason to come back for safety-conscious browsing.
Test 3: Compact badge in search list cards
Hypothesis: Adding a compact grade badge to search result cards does not significantly reduce click-through rate for C/F-grade restaurants (users already know what they're choosing).
Test 4: Health score in push notification for reservation reminders
Hypothesis: Including "Grade A restaurant - verified by government inspection" in pre-visit reminder push notifications reduces same-day cancellation rate by 5%+.
Legal and Liability Considerations
Health inspection records are government documents and public records in all US jurisdictions. Displaying them is protected activity. However, how you frame and present this data has practical implications:
What's Safe
- Displaying scores, grades, violation counts, and inspection dates exactly as returned by the API (which normalizes from government source data)
- Linking to the original government inspection portal for users who want the full report
- Showing factual statements: "Grade A as of January 2026" or "1 critical violation noted in most recent inspection"
- Filtering or sorting by grade, which is a presentation decision, not an editorial one
What to Avoid
- Implied current danger: "This restaurant may currently be unsafe" based on historical data. Use past tense ("violations were found") not present tense ("this restaurant has unsafe conditions").
- Fabricated severity language: Describing a minor structural violation as a "food safety hazard" when the source data classifies it as non-critical.
- Score display without date: Showing a score from 18 months ago without indicating its age implies currency you don't have.
- Defamation risk from user-generated amplification: If your platform allows users to comment on health scores ("this is why they gave people food poisoning"), moderate that content. The government data itself is protected; user commentary adding allegations is not.
Recommended Disclaimer Language
Health scores are sourced from government health department inspection records and reflect the establishment's status at the time of inspection. Scores do not represent current conditions. Data is updated regularly but may not reflect inspections conducted after [last_refresh_date]. For the most current inspection information, visit your local health department's website.
Place this disclaimer on any page that features health score data - typically as footer text on the detail page, not inline with the badge itself.
Data Integration Pattern with Caching
Review platforms have two distinct health data access patterns: real-time lookups for individual restaurant pages and batch enrichment for search index updates. The architecture should separate these concerns:
// API route: GET /api/restaurant/:id/health-score
// Returns cached data; triggers async refresh if TTL expired
export async function getRestaurantHealthScore(restaurantId) {
const cacheKey = `health:${restaurantId}`;
// Check application cache first (Redis/Memcached)
const cached = await cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Check database for last persisted score
const stored = await db.restaurantHealth.findOne({ restaurantId });
if (stored) {
const ageHours = (Date.now() - stored.fetchedAt) / 3600000;
if (ageHours < 48) {
// Serve from DB, repopulate fast cache
await cache.setex(cacheKey, 3600, JSON.stringify(stored));
return stored;
}
// Data in DB is stale - serve it but queue async refresh
queueHealthRefresh(restaurantId);
return stored;
}
// No data at all - fetch synchronously (first load)
const restaurant = await db.restaurants.findOne({ id: restaurantId });
const healthData = await foodSafeApi.lookup({
name: restaurant.name,
address: restaurant.address,
city: restaurant.city,
state: restaurant.state
});
// Persist and cache
await db.restaurantHealth.upsert({ restaurantId, ...healthData, fetchedAt: new Date() });
await cache.setex(cacheKey, 3600, JSON.stringify(healthData));
return healthData;
}
// Background job: nightly batch refresh
// Runs for all restaurants where last fetch > 24 hours ago
async function batchRefreshHealthScores() {
const stale = await db.restaurantHealth.findAll({
where: { fetchedAt: { lt: new Date(Date.now() - 86400000) } },
limit: 500 // process in batches
});
for (const record of stale) {
await queueHealthRefresh(record.restaurantId);
await sleep(100); // rate limit courtesy delay
}
}
The Competitive Advantage of Health Transparency
In a market where Yelp, Google Maps, and TripAdvisor all show the same user reviews and star ratings, health inspection data is a genuine differentiator - one that competitors cannot replicate without building the same data pipeline. It's also the kind of feature that generates press coverage ("The review platform that finally shows you if a restaurant is safe"), which drives organic user acquisition.
More importantly, it creates a virtuous cycle. Restaurants with A grades have an incentive to maintain them and will proactively publicize their grade to drive traffic through your platform. This turns restaurant operators into advocates for your platform's health transparency feature - a remarkably cheap marketing channel.
For platforms considering the build vs buy decision for the underlying data pipeline, see our post on build vs buy for restaurant inspection data pipelines. For the complete data quality and caching strategy guide, see health inspection data quality best practices.
Before shipping health scores to production: (1) Implement all display states including no-data and stale-data. (2) Add inspection date to every score display surface. (3) Place legal disclaimer on detail pages. (4) Set up nightly batch refresh job with rate limiting. (5) Instrument click events on health score badges to measure engagement. (6) Define A/B test control and variant, set sample size for statistical significance. (7) Confirm no-data state is visually indistinguishable from a passing grade absence - it should read as "unknown," not "failed."