Design: Fix Missing AD Restaurants on Group Landing Page
Date: 2026-06-29
Status: Approved
Scope: Option A — surgical fix (G1 + G2 + G3)
Problem
AD (sponsored/advertisement) restaurants are returned by the GroupLandingRestaurants GraphQL API but silently dropped before they reach the UI. The bug affects any group landing page that has sponsored restaurants in its list — observed at /en/famous-and-special-restaurants?city_id=1.
Root Cause
The backend deduplication contract: when multiple entries share the same branchId, the compact-restaurant REST API returns isMerged: false on entries that must not be merged — they should appear as independent cards (e.g. an organic card AND a sponsored card for the same branch). hh-pegasus respects this flag; the monorepo port missed it entirely.
Call chain where the bug lives:
useGroupLandingRestaurants
→ getRestaurantList (shared-service)
→ getGroupLandingRestaurant (shared-api, GraphQL) ← returns both entries
→ mergeSearchResult (shared-api/common) ← BUG: drops the AD entry here
→ getCompactRestaurants (shared-api) ← compact has isMerged: false
Inside mergeSearchResult, when a restaurant has a compact version:
- hh-pegasus: checks
compact.attributes.isMerged === false→ pushes the original entry unchanged and skips merging - Monorepo: no check → falls into normal merge path → second occurrence (
existingIndex !== undefined) hitscontinue→ the AD entry is silently dropped
The isMerged field is also completely absent from the monorepo’s shared-type package, so even if the guard were written, compact.attributes.isMerged would be undefined at runtime (TypeScript casts it away). Both the type and the guard must be fixed together.
Solution
G1 + G2: Restore the isMerged guard (shared-type + shared-api)
File 1: packages/shared-type/src/Restaurant.ts
Add isMerged to the compact/featured restaurant attributes Valibot schema. This is the field the backend compact-restaurant API sets to signal merge intent.
// In the FeaturedRestaurantAttributes object schema, alongside isAds:
isMerged: optional(boolean()),
The inferred TypeScript type picks this up automatically — no manual interface edit needed.
File 2: packages/shared-api/src/common/mergeSearchResult.ts
Restore the guard block immediately after if (compact) {:
if (compact) {
// Restore from hh-pegasus: backend marks isMerged: false to prevent
// deduplication (e.g. organic + sponsored entries for the same branch).
if (compact.attributes.isMerged === false) {
mergedRestaurants.push(restaurant);
continue;
}
// ... existing merge logic unchanged
}
G3: Restore rankingId propagation (same file)
On first-seen merge, copy the original entry’s ranking ID to the compact version so impression tracking stays intact:
// After: compact.attributes.branchRestaurantIds = [String(restaurant.id)]
compact.rankingId = restaurant.rankingId; // was requestId in pegasus; renamed in monorepo
Testing
File: packages/shared-api/src/common/__tests__/mergeSearchResult.test.ts
Add one new test case:
“keeps both entries when compact has isMerged: false”
Arrange: two restaurants with the samebranchId; first is organic, second hasisAds: true; compact API returnsisMerged: false.
Assert: result array has 2 entries — both the organic and the AD entry — in their original form (not replaced by compact).
No changes to existing test cases.
Files Changed
| File | Change |
|---|---|
packages/shared-type/src/Restaurant.ts | Add isMerged: optional(boolean()) to FeaturedRestaurantAttributes Valibot schema |
packages/shared-api/src/common/mergeSearchResult.ts | Add isMerged === false guard + compact.rankingId = restaurant.rankingId |
packages/shared-api/src/common/__tests__/mergeSearchResult.test.ts | Add 1 test case for isMerged: false |
Out of Scope
- G4 (
sortBystore-fallback drift ingetRestaurantList) — not causing visible bugs; fix separately if needed. - G5–G8 — confirmed no-ops or already handled.
- UI changes — AD label rendering is already parity-complete in both pegasus and monorepo.
- Dynamic group landing worker setup — not relevant for static slugs like
famous-and-special-restaurants.
Verification
After the fix, start the dev server at port 5184 and visit /en/famous-and-special-restaurants?city_id=1 on iPhone 14 viewport. Confirm:
- AD restaurants appear in the list (small “Ad” label in upper-right of card image).
- Total restaurant count matches production (
https://web.hungryhub.com/en/famous-and-special-restaurants?city_id=1). - No regressions on non-AD entries (existing cards unchanged).
- The new test passes:
pnpm exec turbo run test --concurrency 2 --filter=@hh-pegasus/shared-api.