י״ט בטבת תשפ״ה

יוג‘ין pen טיטרמן

פרשת וארא

Building Mapa Especial: a guide to specialty coffee in São Paulo

Published: 19 January 2025
פורסם בי״ט בטבת תשפ״ה

My latest pet project is Mapa Especial, a map of specialty coffee shops in São Paulo. The city's specialty coffee scene is very decentralized and hard to navigate, so I thought I'd solve this problem myself.

The website is free and open-source, built upon the foundation of Leaflet, Next.js, and React. It is completely static — you can host it for free on GitHub pages.

landing page

This article explains the major design decisions I made when building Mapa Especial — from branding to application architecture.

The idea

In much of Europe, manual coffee brewing is the primary distinguishing factor for specialty coffee shops. If a place serves Hario V60, they likely brew specialty beans. That is not the case in Brazil, where filter coffee is the nationwide standard. Even diners that use pre-ground supermarket-grade beans have a brewing cone behind the counter. Looks can be deceiving, too: businesses that advertise themselves as 'coffee shops' are often just brunch restaurants.

São Paulo has a rich specialty coffee scene, with new spots opening up every couple months. But the aforementioned cultural differences make Google Maps next to useless when you look for good coffee places. That's why I decided to launch Mapa Especial — for people like me, who want to tell 'specialty' coffee shops from the rest.

The branding

Like São Paulo itself, the color scheme had to be both urban and tropical.

  • The grey background represents the everpresent concrete.
  • The dark green is the colour of the palm leaves.
  • The orange is the colour of the sabiá-laranjeira, the official bird of the São Paulo state.

colours

Once I decided on the color scheme, I became interested in further exploring the avian theme. Sabiás are very cute, and can make for a good mascot.

early logo sketches

It soon became clear that the bird's shape vaguely resembles the famous piso paulista — the trademark pavement pattern of the São Paulo city.

piso paulista and later logo sketches

All that's left is to give one of these birds a coffee cup — and we have the branding!

The logo can shrink and grow:

logo comparison

It neatly fits on business cards and stickers alike:

business card and sticker

The mapping engine

The Leaflet mapping engine was the obvious choice: few libraries beat it in terms of functionality, and none in terms of the available help content. The companion react-leaflet library, however, changes the API enough to warrant a comprehensive documentation of its own — which it currently lacks. Many questions I had could only be answered by looking at other open-source projects or StackOverflow.

The compatibility between the recent versions of nextjs and react-leaflet is not perfect, either. To reliably program buttons that overlay the map, I had to define a GenericLeafletControl that references native leaflet methods.

limited active area

The leaflet-active-area plugin helps me shrink and expand the active area of the map. Since there are two modal windows that overlay the map, sometimes it is necessary to adjust the active area to account for the space that these windows take up.

Data storage

Leaflet natively supports the GeoJSON data format for marker placement. The react-leaflet compatibility layer even offers a <GeoJSON /> component to streamline the process. That said, I simply couldn't get it to work. I suspect this is another nextjs compatibility issue.

For the sake of simplicity, I kept the GeoJSON data format. It seems to be more or less common, and I didn't want to reinvent the wheel. I did, however, implement custom logic that parses this data and generates markers as necessary.

item details vs JSON code

Marker data is spread across two files: cafes.json and shops.json. The LeafletLayer component creates a Leaflet map layer for each. Since there are many roasters that sell beans to multiple coffee shops, roaster data is stored separately (roasters.json). A future update will, hopefully, include a separate page with the list of confirmed specialty roasters in the area.

State management and filtering

The application's main UI includes two modal windows: the filter window and the active item window. When you first open the map, the application loads their default states from JSON files (defaultFilterState.json and defaultDetailState.json). In this configuration, the filter form is empty and the active item window is hidden.

Filtering is simple. When the filter changes, React re-renders the marker layers. As these layers initialize, they only load data for establishments that match the filter query. Since all the data is stored on the client, the changes are instant and do not slow the app down.

filter comparison

Fun fact: The application refreshes the map the moment you change filter settings. However, this isn't apparent to smartphone users: in the mobile version, the filter window obscures the map. To improve the website's UX I added an 'apply filter' button to the mobile interface. The button does nothing but has a positive psychological effect.

The state of the active item window is identical in structure to a data entry from cafes.json or shops.json. When you click the "view more" button on a marker, the application populates the window with that marker's data. When you close the detail window, its state is reset.

Other fun discoveries

⦿ The react-select library does not do SSR and it took me an embarrassingly long time to figure out why it keeps throwing errors when I run the dev server.

⦿ Want to display the rating of a place on Google Maps but don't want to pay for the place card API? Just crop the iframe:

iFrame trap

What's Next?

The website is functional, but there's always room for improvement. If you'd like to contribute to the project, visit the GitHub page and open a new PR or issue.