OLaNouKay: what a local guide to the French Caribbean demands from the code
TL;DR
OLaNouKay answers three questions in Guadeloupe and Martinique: where to eat a bokit, which beach, which river. The site brings together place pages, a map, rankings by commune and a merchant dashboard.
Ordering and payment are not open. This piece describes what the product demanded from the code.
What the product does
The product splits into two surfaces: a web app to discover places and a dashboard for merchants. On the discovery side:
- Place pages: photos, description, opening hours and the daily menu when the merchant publishes one.
- Rankings: lists by commune and category, such as
/gp/baie-mahault/bokit, sorted by view count and then by name. - The map: pins colored by category, with filters.
- Lokal Now: the
/lokal-nowpage lists places open at this moment, from the closest to closing time to the furthest.
The dashboard covers the daily menu, stock, promotions, events and statistics.
In June 2026, I removed the map from the navigation bar. The Bokit tab replaced it: the product launches on this one dish, and the map stays reachable from the home page.
Place data that must not be invented
A local guide is worth what its place pages are worth, and mine go through separate tools depending on which data is missing.
The import script takes a list of names and communes, then queries Google Places for coordinates, address, opening hours, phone number and photos. It works in two stages:
planreads without write access and sorts each line: to import, already present, not found, closed, outside the archipelago or outside the catalog.applycreates the places from the reviewed plan and skips those that exist. Rerunning the script after an interruption creates no duplicates.
Classification does not rely on Google's types alone, because they know neither the bokit nor the food truck. The label in my list wins when it is more precise. No description is copied: imported places arrive with empty text.
Mapbox geocoding needs a different guard. Without a bounding area, a street called "La Savane" could resolve near Bayonne, in mainland France. Each request therefore carries the territory's bounding box, and the script fills in missing coordinates only.
The worst defect came from the display. A place without hours showed "11h · 22h", and a place without declared amenities advertised a terrace, delivery, card payment and wifi. Beaches and rivers included. Missing hours now read "Non renseignés" (not provided) in French, and the amenities block disappears.
A map that loads only in the browser
The map relies on mapbox-gl, a library that needs the browser. The component therefore loads without server rendering, and its code imports mapbox-gl at initialization. Place pages, on the other hand, render on the server and appear in the sitemap.
The pins are hand-built HTML elements. One bug forced a structural rule: Mapbox writes its own transform on the outer element to position the pin. A hover zoom set in the same place overwrote it and sent pins to the top left of the map. The zoom now lives on an inner element.
Local time behind "open now"
Every opening calculation goes through a fixed four-hour offset, because Guadeloupe stays at UTC-4 all year. The calculation subtracts that offset from the current instant, then reads the time in UTC:
const d = new Date(epochMs - GUADELOUPE_UTC_OFFSET_MS)
return d.getUTCHours() * 60 + d.getUTCMinutes()
The first version of the scheduled function updateIsOpen read getHours(). The server runs on UTC, and the schedule's timeZone setting only controls the trigger. A place open from 11:00 to 22:00 would have been open from 07:00 to 18:00 on site. No place had hours yet: I fixed the defect in October 2026 before it had any effect.
The second trap comes from night services. An entry of 18:00 → 02:00 opens one day and closes the next, so openStateAt checks two things: today's service and the tail of yesterday's. Before this change, the scheduled function marked a 18:00 → 00:00 service as permanently closed. Eleven test cases cover this calculation.
Two other choices follow from this:
- Two reads:
updateIsOpenwrites theisOpenfield every 15 minutes, while/lokal-nowrecalculates the state on each request, with no static cache. - An accepted copy: Cloud Functions cannot resolve the shared package at runtime. The calculation exists there twice, with a comment requiring both versions to stay aligned.
The function walks through every active place with no territory filter: the same offset applies to places in Martinique.
Two interface languages and one canonical per version
The interface exists in French and English with next-intl, with no Spanish version. French stays at the root and English lives under /en, which kept the existing French addresses.
This structure produced an SEO bug. Pages hard-coded their canonical to the French path: the English version declared itself a copy of the other one, with no language tag. The observed result: /en/gp/bokit came up on French searches. The alternatesFor function now gives each version its own canonical and announces the other through hreflang.
What is not open
The repository holds more code than the site puts into service, and the gap deserves to be written down:
- Ordering and payment: the Stripe functions and the payment screen exist, but card payment has never been tested end to end. Nothing is presented as delivered until that test passes.
- Votes: the campaign code exists, the campaigns have not launched. A script can still vote in a loop with made-up addresses, and that has to be stopped first.
- Martinique: the reference data covers its 34 communes and the starting catalog holds 11 bokits, but pages by commune exist only under
/gp, and the Google import filters on Guadeloupe's bounding area.
As of November 1, 2026, the repository is at version 0.3.8.