Šta za ručak?
Java · Spring Boot · Next.js · Postgres
A Serbian recipe site. You can search in Latin or Cyrillic, with or without diacritics, and filter recipes for Orthodox fasting days.
Why I built it
When I lived at home, my mom asked me "Šta za ručak?" every day. It means "What's for lunch?". That's where the name comes from.
I also wanted a real site to work on, with the parts most backends have: accounts, sessions, search, photo uploads and moderation.
What it runs on
Caddy is the reverse proxy. It sends /api to the API and everything else to Next.js, which renders the pages.
The API is Java 25 and Spring Boot. It talks to Postgres with plain SQL through JdbcClient, without an ORM, because the SQL does most of the work. Saving a recipe is a single statement, and a trigger works out its fasting level from the ingredients.
Passwords are hashed with Argon2id. A session is a row in Postgres and the cookie only holds its id, so logging out deletes the session. Rate limits are counted in Postgres too, so a restart doesn't reset them. Uploaded photos are checked to be JPEGs and their EXIF data is removed, because phone photos carry GPS coordinates.
Search and fasting
People here write Serbian in Latin and in Cyrillic, and a lot of them skip the diacritics. Search folds all of it to one form before comparing, so cevapi, ćevapi and ћевапи find the same recipe. The search runs in Postgres, and trigram matching catches typos and partial words.
On Orthodox fasting days a lot of people skip meat, dairy and eggs, and on some days fish or oil too. Each recipe's fasting level is worked out from its ingredients, so nobody has to label it. A recipe with an ingredient line the parser couldn't read is left out of the fasting filter, so a recipe with meat never shows up as a fasting recipe.
The ingredient parser
Recipes are pasted in as plain text. The parser turns a line like 2 kašike ulja into a quantity, a unit and an ingredient. It understands 1/2, 1,5, ½, pola and ranges like 2-3.
Quantities are stored as fractions, so changing the number of servings keeps ½ as ½. Units carry all three Serbian plural forms, so 2 kašike becomes 5 kašika when you scale up.
A line the parser can't read keeps its text and goes on a list for me to look at. When I add the missing word, every recipe that used it is parsed again, including old ones.