Skip to content

Commit 606c21a

Browse files
fix(og): map 'foreign beer' category to international-beer API endpoint (#290)
* fix(og): map 'foreign beer' category to international-beer API endpoint The OG preview handler derives the API endpoint name directly from the URL's category param. For foreign beer drinks the category field value is 'foreign beer' (with a space) but the API endpoint is 'international-beer', so the handler fetched a non-existent URL and silently fell back to the plain SPA — no OG metadata was injected. Add a CATEGORY_TO_ENDPOINT lookup in fetchDrinkData so 'foreign beer' resolves to 'international-beer.json', matching what the Flutter app uses when loading these drinks. * chore: update mise.lock node 21.7.3 → 22.22.3 * fix(og): use Object.hasOwn to guard category-to-endpoint lookup Using bracket notation on a plain object lets inherited property names like 'constructor' or '__proto__' resolve to non-string values and produce a malformed upstream URL. Object.hasOwn checks own properties only, so unknown categories fall through to the original value safely. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec87295 commit 606c21a

3 files changed

Lines changed: 44 additions & 14 deletions

File tree

functions/_lib/drink-preview.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const CRAWLER_UA_PATTERNS = [
1212
const DATA_BASE_URL = 'https://data.cambeerfestival.app';
1313
const OG_IMAGE_URL = 'https://cambeerfestival.app/icons/Icon-512.png';
1414

15+
// Product category field values don't always match their API endpoint names.
16+
const CATEGORY_TO_ENDPOINT = {
17+
'foreign beer': 'international-beer',
18+
};
19+
1520
export function isCrawler(userAgent) {
1621
if (!userAgent) return false;
1722
const ua = userAgent.toLowerCase();
@@ -59,7 +64,8 @@ export function buildOgTags(product, producer, canonicalUrl) {
5964
}
6065

6166
export async function fetchDrinkData(festivalId, category) {
62-
const url = `${DATA_BASE_URL}/${encodeURIComponent(festivalId)}/${encodeURIComponent(category)}.json`;
67+
const endpoint = Object.hasOwn(CATEGORY_TO_ENDPOINT, category) ? CATEGORY_TO_ENDPOINT[category] : category;
68+
const url = `${DATA_BASE_URL}/${encodeURIComponent(festivalId)}/${encodeURIComponent(endpoint)}.json`;
6369
const response = await fetch(url);
6470
if (!response.ok) return null;
6571
const data = await response.json();

functions/test/drink-preview.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,5 +263,29 @@ describe('fetchDrinkData', () => {
263263
'https://data.cambeerfestival.app/cbf2025/beer.json',
264264
);
265265
});
266+
267+
it('maps "foreign beer" category to "international-beer" endpoint', async () => {
268+
const mockFetch = vi.fn().mockResolvedValue({
269+
ok: true,
270+
json: () => Promise.resolve({ producers: [] }),
271+
});
272+
vi.stubGlobal('fetch', mockFetch);
273+
await fetchDrinkData('cbf2026', 'foreign beer');
274+
expect(mockFetch).toHaveBeenCalledWith(
275+
'https://data.cambeerfestival.app/cbf2026/international-beer.json',
276+
);
277+
});
278+
279+
it('does not treat inherited property names as endpoint remaps', async () => {
280+
const mockFetch = vi.fn().mockResolvedValue({
281+
ok: true,
282+
json: () => Promise.resolve({ producers: [] }),
283+
});
284+
vi.stubGlobal('fetch', mockFetch);
285+
await fetchDrinkData('cbf2026', 'constructor');
286+
expect(mockFetch).toHaveBeenCalledWith(
287+
'https://data.cambeerfestival.app/cbf2026/constructor.json',
288+
);
289+
});
266290
});
267291

mise.lock

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)