ZapiszPrzepis: zapisuje przepis, nie link

· 10 min czytania

Aplikacja PWA, która z udostępnionego posta, rolki albo bloga robi trwałą, uporządkowaną kopię przepisu po polsku, a z kopii publiczną bazę przepisów. Next.js, Supabase, Inngest, Cloudflare. Przetestowana na telefonach z Androidem.

Aplikacja PWA, która z udostępnionego posta, rolki albo bloga robi trwałą, uporządkowaną kopię przepisu po polsku, a z kopii publiczną bazę przepisów. Next.js, Supabase, Inngest, Cloudflare. Przetestowana na telefonach z Androidem.

Przepisy krążą jako posty, rolki i filmy. Odruch to zakładka albo przycisk „Zapisz” na platformie, ale oba przechowują wskaźnik, nie treść. Po kilku miesiącach część zapisanych linków przestaje działać: autor kasuje rolkę, grupa znika, blog przestaje istnieć, post staje się prywatny. Kolekcja zamienia się w listę błędów 404, a człowiek dowiaduje się o tym wtedy, gdy chce z niej skorzystać.

ZapiszPrzepis działa odwrotnie, w modelu archive-first. Link zamienia się w trwałą kopię przepisu w bazie, a adres źródła zostaje obok, żeby oryginał był o jedno kliknięcie dalej, dopóki istnieje.

Projektowałem to dla osoby nietechnicznej, która nie wpisuje składników ręcznie, nie wypełnia formularzy i nie układa folderów ani tagów. Ten warunek wyciął większość „oczywistych” funkcji i okazał się najlepszym filtrem w całym projekcie.

To ciąg dalszy wpisu o tym, jak pracuję z AI na prawdziwym projekcie. Tamten tekst opisuje metodę: specyfikacje przed kodem, bramki, testy. Ten opisuje produkt: co zbudowałem, jak działa i co zmieniło się od sierpnia, gdy zaczęto z niego naprawdę korzystać.

Z prywatnej kolekcji do publicznej bazy

Pierwsza wersja była prywatną kolekcją jednego użytkownika. Największa zmiana od sierpnia to przebudowa na publiczną bazę przepisów.

  • Jeden link, jedna strona. Każdy przepis ma publiczny adres /przepis/{slug}. Dodanie linku, który już jest w bazie, nie tworzy duplikatu, tylko dodaje istniejący przepis do ulubionych. Scalone duplikaty przekierowują (301) na przepis, który został.
  • Goście czytają bez konta. Strona główna, przepisy, kategorie i wyszukiwarka są otwarte. Dostęp do danych dla gości idzie przez widoki, które domyślnie niczego nie ujawniają, więc pomyłka w uprawnieniach kończy się brakiem danych, a nie wyciekiem.
  • Konto daje własną kolekcję. Ulubione, lista „Moje przepisy” i publiczny profil pod /u/{nazwa}. Dodawanie przepisów z linków jest za kodem dostępu.
  • Wiele wersji tego samego dania. Przepisy na to samo danie są grupowane („Mamy jeszcze 2 inne przepisy na krokiety”), a różnice w składnikach oznaczone kolorem. Podobne przepisy nie podsuwają dań mięsnych osobie, która ogląda bezmięsne.
Strona przepisu na komputerze: zdjęcie z plakietką źródła, tytuł, autor dodania, czasy, ulubione i tryb gotowania, składniki z polami wyboru i kroki w bezokoliczniku

Jak to działa

Wejście to jeden gest. Aplikacja rejestruje się jako Web Share Target, więc pojawia się w systemowym menu „Udostępnij” obok Messengera i WhatsAppa. Użytkownik wysyła link i na tym kończy.

Co dzieje się po udostępnieniu linku
  1. Krok 1

    Rozpoznanie źródła

    Post na Facebooku, rolka, film z YouTube i blog to cztery różne przypadki, każdy z własnym sposobem pobrania treści.

  2. Krok 2

    Sprawdzenie duplikatu

    Ten sam przepis opublikowany dwa razy nie trafia do bazy drugi raz, nawet pod innym adresem.

  3. Krok 3

    Pobranie treści

    Wiele dróg i zapasowe plany, gdy pierwsza zawiedzie.

  4. Krok 4

    Redakcja

    Model wyciąga przepis i porządkuje go według stałych reguł.

  5. Krok 5

    Kontrola

    Bez składników albo bez kroków nic nie trafia do bazy, a w powiadomieniach pojawia się informacja, że na stronie nie ma przepisu.

  6. Krok 6

    Etykiety

    Drugi, mocniejszy przebieg nadaje tytułowi właściwą formę, dodaje tagi i opis.

  7. Krok 7

    Zapis

    Przepis trafia do bazy, a zdjęcie do własnego magazynu.

Wyciąganie przepisu trwa kilkanaście sekund i bywa kapryśne, więc nie dzieje się w trakcie żądania. Aplikacja wysyła zdarzenie do Inngest, który ustawia zadanie w kolejce i ponawia je przy błędzie. Użytkownik dostaje odpowiedź od razu, a przepis pojawia się chwilę później. Jeśli import się nie uda, powód trafia do powiadomień i można go ponowić stamtąd. Import zawieszony w stanie „oczekuje” jest traktowany jako nieudany, żeby nic nie wisiało w nieskończoność.

Redakcja: z posta do przepisu

Samo zapisanie treści nie wystarcza. Post z Facebooka to zwykle narracja („pokroiłam, zeszkliłam, dodałam”), pełna wtrętów i zachęt. Przepis w aplikacji ma wyglądać tak samo niezależnie od źródła, więc reguły redakcji są częścią produktu.

  • Kroki to krótkie polecenia w bezokoliczniku: „Pokroić mięso w kostkę.”, „Zeszklić cebulę.”. Narracja i tryb rozkazujący są przepisywane na tę formę, znikają wtręty typu „u mnie” i słowa „następnie”, „potem”, „na koniec”, bo kroki są numerowane. Słowa mówiące o czasie („gdy woda się zagotuje”) zostają. Przepisy zapisane wcześniej poprawia osobny skrypt (w wydaniu 0.2.0 objął 609 z nich), a nowe dostają tę formę od razu.
  • Kolejność poprawiana tylko tam, gdzie trzeba. Składnik, który źródło przygotowuje po kroku, który go używa (lukier, kruszonka, sos), przesuwam przed ten krok. Końcowe „Smacznego!” wypada, a porady o przechowywaniu i wariantach trafiają do jednego kroku „Opcjonalnie:”.
  • Nic nie jest dopisywane. Gdy post podaje składniki, a metodę zostawia filmowi, kroki zostają puste. Czasu, którego źródło nie podaje, model może się domyślić, ale wtedy jest on oznaczony jako szacunkowy. Liczby porcji nie szacuję wcale.
  • Źródła obcojęzyczne. Przepis z angielskiej strony dostaje polskie nazwy produktów (makarony jako „literki”, „kółka”, „małe rurki”) i polskie sformułowania bez kalk, a amerykańskie oznaczenia w rodzaju „94% chude” są upraszczane do „chude”. Miary amerykańskie są przeliczane na metryczne.
  • Miary domowe. Gdy źródło podaje obok wagi miarę z kuchni („250 g mąki – ok. 1,5 szklanki”), pokazuję ją jako drugą, jaśniejszą linię pod ilością. Model przepisuje ją dosłownie ze źródła i nigdy jej nie szacuje. Dla podstawowych produktów sypkich miarę domową wylicza osobno sama aplikacja, z tabeli gęstości, tylko przy wyświetlaniu.
  • Pozostałe porządki: tytuł jak w menu (bez „Jak zrobić…”, bez „najlepszy”, bez pierwszej osoby), sekcje składników (Ciasto, Krem, Masa), jednostki amerykańskie przeliczone na metryczne, kategoria według reguł (ciasteczka, ciasta, pieczywo, syropy i kolejne).

Dwa przebiegi to świadomy podział kosztów. Tańszy model wyciąga treść, a mocniejszy, który dostaje jeden gotowy przepis, nadaje etykiety i tagi. Z tańszym tagi wychodziły źle: oznaczał bułki i pizzę jako „kruche”. Model do wyciągania treści wybieram na własnych testach, nie z rankingu: po teście na przepisach przeszedłem na gpt-5.4-mini, a szerszy test 21 modeli na polskich i angielskich przepisach potwierdził ten wybór (czołówka to statystyczny remis).

Jak wybrałem model: test 21 modeli AI na 50 polskich i angielskich przepisach, z wzorcem sprawdzonym ręcznie, metodą i wynikami.

Źródła i zapasowe plany

Pierwsza wersja czytała blogi i YouTube. Od tamtej pory największa część pracy poszła w to, żeby link „po prostu działał”, bo w praktyce źródła zawodzą na różne sposoby.

Źródło Jak odczytuję przepis
Facebook: rolki i filmy
Opis z wbudowanego odtwarzacza, bez sesji Facebooka. Gdy post odsyła do bloga, idę za linkiem. Gdy Facebook odmawia, aplikacja mówi, co naprawdę zobaczyła, zamiast zgadywać.
Facebook: posty i grupy
Osobne ścieżki dla stron i grup, bo ich posty udostępniają się inaczej. Czytam też pierwszy komentarz autora, a zdjęcia z albumów i wielu zdjęć w poście grupy trafiają do galerii. Link udostępniony z aplikacji i ten sam wklejony z przeglądarki mają ten sam klucz.
YouTube
Opis filmu, a gdy nie ma w nim przepisu, transkrypcja. Film jest osadzany tylko wtedy, gdy da się go osadzić.
Blogi
Treść główna strony, a dla Bloggera (także na własnych domenach) kanał JSON. Przepis rozbity na kilka stron WordPressa jest składany w całość, a galerie Wix czytane w pełnym rozmiarze.
Gdy coś zawodzi
Zapasowe czytanie strony przez Jina Reader (z odzyskiwaniem okładki i oczyszczaniem HTML), a powód porażki zapisuje się przy imporcie.

Do tego dochodzi deduplikacja w kilku warstwach. Ten sam link znany pod kilkoma adresami (udostępnienie z aplikacji Facebooka, stały link do posta, nazwa grupy w innej formie) dostaje jeden klucz. Powtórka tego samego autora jest wyłapywana po odcisku jego tekstu, jeszcze zanim zostanie wywołany model, a ta, która różni się tekstem, po prawie identycznym zestawie składników (także wtedy, gdy dwa zapisy trafiają niemal jednocześnie).

Zdjęcia i źródła na kartach

Zdjęcie przepisu to osobny problem. Pierwszy obrazek na stronie bywa logo, reklamą albo miniaturą innego przepisu.

  • Okładka i galeria wybierane z treści. Odrzucam zdjęcia z sekcji reklam, powiązanych wpisów i komentarzy, przyciski, logotypy sklepów, miniatury filmów i obrazki, które są linkiem do innej strony. Dla blogów ze złym og:image wybieram właściwe zdjęcie sam.
  • Kopia zdjęć u siebie. Okładka i galeria są archiwizowane w magazynie aplikacji, bo adresy z Facebooka wygasają w ciągu dni. Okładki z hostów z błędem SSL albo odrzucających ruch z Cloudflare są odzyskiwane osobną drogą.
  • Zdjęcia kroków. Gdy źródło ma zdjęcie przy konkretnym kroku, jest ono pokazywane przy tym kroku (dla zalogowanych).
  • Skąd jest przepis. Plakietka źródła pokazuje logo Facebooka i autora albo domenę bloga. Osoba prywatna występuje tylko jako imię i inicjał nazwiska, bo pełnego nazwiska nigdy nie zapisuję.

Wygoda na co dzień

Strona główna: pytanie „Czego dziś szukasz?”, pole wyszukiwania, przycisk głosowy i podpowiedzi
Strona główna z wyszukiwaniem głosem
Strona przepisu na telefonie: plakietka źródła, tytuł, ulubione, tryb gotowania i składniki z zaznaczonymi pozycjami
Przepis i lista składników z polami wyboru
Tryb gotowania: krok 5 z 9 dużą czcionką, pasek postępu i przyciski Wstecz oraz Dalej
Tryb gotowania: jeden krok na ekranie
Kategoria Zupy z wybranym tagiem „warzywne”, licznikiem 20 przepisów i przyciskiem Wyczyść
Kategoria z filtrem po tagach
  • Tryb gotowania. Pokazuje jeden krok na pełnym ekranie, dużą czcionką, z paskiem postępu i przyciskami „Wstecz” i „Dalej”, a ekran telefonu nie gaśnie w trakcie (Wake Lock), bo przy mące na rękach nikt nie chce go odblokowywać. Na samej stronie przepisu składniki mają pola wyboru: zaznaczony składnik się przekreśla, a „Wyczyść zaznaczenia” zeruje listę.
  • Wyszukiwanie. Jedna wyszukiwarka z zakresem „wszystkie / moje / ulubione”, podpowiedziami podczas pisania, ostatnimi wyszukiwaniami i głosem (przycisk „Powiedz” i ekran „Słucham…”). Osobno wyszukiwarka przepisów w internecie, która korzysta tylko z zaufanych stron i ma limit zapytań na minutę.
  • Kategorie i tagi. Kategorie z licznikami, a w kategorii wybór kilku tagów naraz, które zawężają listę (przepis musi mieć wszystkie wybrane).
  • Ulubione. Serce z podpisem na kartach i na zdjęciu, osobna lista. Kliknięcie przez osobę niezalogowaną zapamiętuje zamiar i dokańcza go po zalogowaniu.
  • Drukowanie. Przycisk „Drukuj” i osobny arkusz stylów do druku: składniki i kroki w dwóch kolumnach, małe zdjęcie przy nagłówku, bez linii źródła i bez zdjęć kroków.
  • Przepis dnia na stronie głównej, który zmienia się o polskiej północy, a nie o północy UTC.
  • Ustawienia wyglądu. Wielkość tekstu skaluje całą aplikację, razem z opisem przepisu. Ta strona jest dostępna także bez logowania, bo osoba, której drobny druk przeszkadza, powinna to zmienić, zanim się zaloguje. Do tego wybór koloru akcentu.

Dostępność i prawdziwy telefon

Aplikacja ma być czytelna dla osoby, która nie lubi małego druku, więc przebudowa publiczna szła pod WCAG 2.2 AA: duże cele dotykowe (48 px), brak przewijania w poziomie przy 320 px szerokości i przy powiększonej systemowej czcionce, przycisk „Wróć” zamiast gubienia się w nawigacji. Nazwy kategorii nie łamią się w środku słowa nawet przy dużej czcionce, a spójniki jednoliterowe („i”, „w”, „z”) nie zostają na końcu wiersza.

Testuję na dwóch telefonach z Androidem, Pixel 11 Pro i Pixel 9, jako zainstalowaną PWA, a nie tylko w oknie przeglądarki. To znalazło błędy, których emulator nie pokazuje: kolor paska systemowego i dolnej nawigacji na Androidzie, przez który przyciski mogły zniknąć na białym tle. Naprawiłem to ciemnym tłem w manifeście i dynamicznym theme-color.

Widoczność w wyszukiwarkach i dla modeli AI

  • Dane strukturalne. Strony przepisów mają schema.org/Recipe w JSON-LD, dzięki czemu Google może pokazać przepis jako rozszerzony wynik.
  • Opisy. Opis do wyników wyszukiwania powstaje przy zapisie przepisu, a kategorie mają własne tytuły, opisy i karty do udostępniania.
  • Mapa strony i robots.txt. Search Console odczytał mapę dwa dni po wysłaniu (status Success) i znalazł w niej 824 adresy. Strony szybko pojawiły się w indeksie Google.
  • Dla modeli językowych. Każdy przepis i każda kategoria ma wersję w Markdownie, a w katalogu głównym leży llms.txt z listą stron, więc asystenci AI mogą cytować przepis z jego źródła, a nie ze zgadywania.

Logowanie i dostęp

Podstawą jest logowanie linkiem w mailu, bez hasła, który działa na dowolnym urządzeniu: link otwarty w innej przeglądarce niż ta, w której zaczęło się logowanie, nie kończy się już błędem. Doszło logowanie przez Google i przez Facebooka. Przycisk Facebooka czekał, aż aplikacja Meta przejdzie na tryb Live, i od 24 października jest włączony. Po pierwszym logowaniu aplikacja pyta raz o formę zwrotu, żeby powitać po polsku w wołaczu i napisać „Dodała” albo „Dodał” w publicznych tekstach, a nie „Witaj, Użytkowniku”.

Wylogowanie z jednego urządzenia nie wylogowuje już ze wszystkich, a chwilowy błąd sprawdzenia sesji nie wyrzuca użytkownika z aplikacji.

Bezpieczeństwo i niezawodność

  • Odczyty gości fail-closed. Publiczne strony czytają dane przez widoki z listą dozwolonych pól, a granica tej listy ma własne testy.
  • Zdjęcia przez listę dozwolonych. Optymalizator obrazów przepuszcza tylko zarchiwizowane zdjęcia przepisów, a adresy z sieci prywatnych są odrzucane przy pobieraniu.
  • Zapisy tylko przez serwer. Zapis do tabeli przepisów i importów jest odebrany sesji użytkownika i zostaje przy roli serwisowej, więc zablokowanie dotyczy bazy, a nie tylko kodu aplikacji (po audycie kodu). Przekierowanie po logowaniu nie pozwala już wyskoczyć poza serwis.
  • Nagłówki bezpieczeństwa ustawiane w warstwie pośredniej na każdej odpowiedzi.
  • Service worker bez stron z konta. Tryb offline pokazuje zapisane publiczne strony albo komunikat „Offline”, ale strony zalogowanego użytkownika nie trafiają do pamięci podręcznej, a po wylogowaniu pamięć jest czyszczona. Wcześniej zapasowa ścieżka nie działała (zawsze brała pierwszą gałąź), więc to też była realna poprawka.
  • Kopia zapasowa i czuwanie. Tabele z przepisami są zrzucane do R2 codziennie, a projekt w Supabase dostaje codzienne zapytanie, żeby nie zasypiał.
  • Sprawdzenie produkcji. Po wdrożeniu skrypt sprawdza, czy pliki statyczne naprawdę się ładują, i zgłasza sporadyczne błędy stron.

Technologie

Warstwa Wybór i powód
Framework
Next.js 16 (App Router): Server Actions, formularze bez osobnego API
Baza i logowanie
Supabase: Postgres z RLS, link w mailu, Google i Facebook
Zadania w tle
Inngest: kolejka i ponawianie bez własnej infrastruktury
Pobieranie i redakcja
Firecrawl, Jina Reader, OpenAI (dwa przebiegi: wyciąganie i etykiety)
Hosting
Cloudflare Workers (OpenNext), kopia zapasowa do R2
Klient
PWA + Web Share Target: jedyny sposób na jednoruchowe wejście

Hosting na Cloudflare Workers oznacza brak API Node.js w czasie działania, więc każda integracja musi działać na zwykłym fetch. To wyeliminowało kilka bibliotek jeszcze przed pierwszą linijką kodu.

Wnioski

  • Archiwum kopii wygrywa z zakładką, bo przepis nie zależy od życia źródła.
  • Reguły redakcji są produktem, nie dodatkiem. To one sprawiają, że przepisy z różnych źródeł czyta się tak samo.
  • Źródła zawodzą po cichu, więc pomagają zapasowe plany, uczciwe komunikaty i powiadomienia z możliwością ponowienia.
  • Jeden link, jedna strona pozwoliła zamienić prywatną listę w bazę, którą da się czytać, linkować i znaleźć w Google.
  • Prawdziwy telefon znajduje to, czego nie widać na komputerze.

Jak do tego doszedłem, czyli specyfikacje przed kodem, potok agentów, audyt kodu i testy, opisuję we wpisie o pracy z AI.

Następny krok

Masz projekt, który po prostu ma działać?

Buduję strony i aplikacje webowe od zera: plan przed kodem, testy tam, gdzie logika bywa podstępna, i repozytorium, które zostaje Twoje. Napisz, czego potrzebujesz, a powiem, czy i jak da się to zrobić.

Porozmawiajmy o Twoim projekcie Zobacz, czym się zajmuję

Najczęściej zadawane pytania

— 01
Czym ZapiszPrzepis różni się od zakładki w przeglądarce?
Zakładka przechowuje wskaźnik, który działa tylko dopóki istnieje źródło. ZapiszPrzepis zapisuje kopię treści: tytuł, składniki, kroki i zdjęcie trafiają do własnej bazy. Adres źródła zostaje obok, ale przepis przetrwa, gdy autor skasuje post.
— 02
Czy trzeba mieć konto, żeby czytać przepisy?
Nie. Strony przepisów, kategorie, wyszukiwarka i ustawienia wyglądu są dostępne dla gości. Konto (link w mailu, Google albo Facebook) jest potrzebne do ulubionych i własnej kolekcji, a dodawanie przepisów z linków chroni kod dostępu.
— 03
Czy model AI zmyśla składniki albo kroki?
Prompt każe zapisać tylko to, co podaje źródło. Gdy post ma składniki, a metodę zostawia filmowi, lista kroków zostaje pusta. Czas przygotowania, którego źródło nie podaje, jest oznaczony jako szacunkowy, a liczby porcji nie szacuję w ogóle. Strona, która przepisu nie zawiera (zapowiedź kursu, wpis z samym linkiem do artykułu gdzie indziej), kończy się komunikatem, a nie zmyślonym przepisem.
— 04
Co się dzieje, gdy ten sam przepis zapisze dwoje ludzi?
Jeden link ma jedną stronę. Dodanie linku, który już jest w bazie, nie tworzy kopii, tylko dodaje ten przepis do ulubionych, a scalone duplikaty przekierowują na przepis, który został.
Powrót do portfolio