fix(og): news permalink resolves share image via rollup parent POI (#475)#479
fix(og): news permalink resolves share image via rollup parent POI (#475)#479fatherlinux wants to merge 1 commit into
Conversation
) findItemBySlugs filtered news by exact poi_id, but the news API expands boundary/org parents via getRollupPoiIds. After the 2026-06-09 seed refresh the first POI with media+news is a boundary/org POI whose news exists only via child rollup, so the lookup returned null, the middleware fell through, and Express served the static index.html brand-fallback og:image. Expand the news query to poi_id = ANY(getRollupPoiIds(...)) and resolve og:image against the slug-resolved parent POI (item._poi.id).
There was a problem hiding this comment.
Code Review
This pull request updates the findItemBySlugs function in backend/server.js to support rolled-up POIs for news items by fetching rollup POI IDs and querying with ANY. It also changes the Open Graph image resolution to use the parent POI ID (item._poi.id) instead of the child POI ID (item.poi_id). The reviewer suggests extending the rollup POI support to the event branch of findItemBySlugs to prevent similar failures. Additionally, they recommend a tiered fallback strategy for resolving the Open Graph image to prioritize the child POI's image before falling back to the parent POI's image.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| `, [poi.id]); | ||
| rows = q.rows; | ||
| } else { | ||
| const poiIds = await getRollupPoiIds(pool, poi.id); |
There was a problem hiding this comment.
The event branch of findItemBySlugs (lines 2623–2636) suffers from the exact same issue as the news branch. If an event is rolled up to a parent POI (boundary/org), trying to resolve its permalink via the parent POI's slug will fail because the query strictly filters on WHERE e.poi_id = $1.
To match the events API and support rolled-up events, the event branch should also be updated to use getRollupPoiIds and check (e.poi_id = ANY($1) OR e.venue_poi_id = ANY($1)).
Here is how the event branch should be refactored:
if (type === 'event') {
const poiIds = await getRollupPoiIds(pool, poi.id);
const q = await pool.query(`
SELECT e.id, e.title, e.description, e.start_date, e.end_date, e.event_type,
e.location_details, e.source_url, e.publication_date, e.collection_date, e.image_url,
p.name AS poi_name, p.id AS poi_id,
COALESCE(json_agg(json_build_object('url', u.url, 'source_name', u.source_name)) FILTER (WHERE u.id IS NOT NULL), '[]'::json) AS additional_urls
FROM poi_events e
JOIN pois p ON e.poi_id = p.id
LEFT JOIN poi_event_urls u ON u.event_id = e.id
WHERE (e.poi_id = ANY($1) OR e.venue_poi_id = ANY($1)) AND e.moderation_status IN ('published', 'auto_approved')
GROUP BY e.id, p.name, p.id
ORDER BY e.start_date DESC
`, [poiIds]);
rows = q.rows;
}| // Image priority: source article image, then POI primary photo, then brand. | ||
| const sourceImage = isUsableSourceImage(item.image_url) ? item.image_url : null; | ||
| const ogImage = escapeHtml(sourceImage || await resolvePoiOgImage(item.poi_id, baseUrl)); | ||
| const ogImage = escapeHtml(sourceImage || await resolvePoiOgImage(item._poi.id, baseUrl)); |
There was a problem hiding this comment.
While falling back to the parent POI's image (item._poi.id) ensures that we don't get a generic brand fallback when the parent has media, it completely bypasses the child POI's specific image (item.poi_id) if one exists. News and events are often highly specific to the child POI (e.g., a specific trailhead or waterfall), so using the child POI's image is much more relevant for social sharing.
We can implement a tiered fallback strategy:
- Use the specific child POI's image (
item.poi_id) if it has one. - If the child POI has no image (returns the brand fallback), fall back to the parent POI's image (
item._poi.id). - Finally, fall back to the brand image.
| const ogImage = escapeHtml(sourceImage || await resolvePoiOgImage(item._poi.id, baseUrl)); | |
| let resolvedImage = sourceImage; | |
| if (!resolvedImage) { | |
| const childImage = await resolvePoiOgImage(item.poi_id, baseUrl); | |
| if (childImage && !childImage.endsWith(OG_FALLBACK_IMAGE)) { | |
| resolvedImage = childImage; | |
| } else { | |
| resolvedImage = await resolvePoiOgImage(item._poi.id, baseUrl); | |
| } | |
| } | |
| const ogImage = escapeHtml(resolvedImage); |
Fixes #475.
Root cause
findItemBySlugsfiltered news with a directWHERE n.poi_id = $1, while/api/pois/:id/newsexpands boundary/org parents viagetRollupPoiIds. After the 2026-06-09 seed refresh, the first POI in the test loop with both media and news is a boundary/org POI whose news exists only via rollup from child POIs. SofindItemBySlugsreturnednull, the news-permalink middleware fell through tonext(), Express served the staticindex.html(absolute brand-fallback og:image), andogImages[0].endsWith(FALLBACK_IMAGE_PATH)wastrue— failing the assertion at line 125.Changes (
backend/server.js, 4 lines)findItemBySlugs(news branch): expand togetRollupPoiIds(pool, poi.id)andWHERE n.poi_id = ANY($1), matching the news API.item._poi.id(slug-resolved parent POI) instead ofitem.poi_id.Provenance
🤖 Maiden autonomous run of Ashigaru — Kagetora dispatched Claude (Sonnet) in an unprivileged sandbox; diagnosed by code-reading only (no DB). Not verified locally — relying on this PR's CI to run
ogShareImages. Draft, pending CI + human review.