Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2635,17 +2635,18 @@ async function findItemBySlugs(type, poiSlug, titleSlug) {
`, [poi.id]);
rows = q.rows;
} else {
const poiIds = await getRollupPoiIds(pool, poi.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
}

const q = await pool.query(`
SELECT n.id, n.title, n.summary, n.source_url, n.source_name, n.news_type,
n.publication_date, n.collection_date, n.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_news n
JOIN pois p ON n.poi_id = p.id
LEFT JOIN poi_news_urls u ON u.news_id = n.id
WHERE n.poi_id = $1 AND n.moderation_status IN ('published', 'auto_approved')
WHERE n.poi_id = ANY($1) AND n.moderation_status IN ('published', 'auto_approved')
GROUP BY n.id, p.name, p.id
ORDER BY COALESCE(n.publication_date, n.collection_date) DESC
`, [poi.id]);
`, [poiIds]);
rows = q.rows;
}

Expand Down Expand Up @@ -2803,7 +2804,7 @@ app.use(async (req, res, next) => {
const safeDesc = escapeHtml(description.length > 200 ? description.substring(0, 197) + '...' : description);
// 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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:

  1. Use the specific child POI's image (item.poi_id) if it has one.
  2. If the child POI has no image (returns the brand fallback), fall back to the parent POI's image (item._poi.id).
  3. Finally, fall back to the brand image.
Suggested change
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);


const indexPath = path.join(staticPath, 'index.html');
let html = await fs.readFile(indexPath, 'utf-8');
Expand Down
Loading