ბიზნეს-ლოგიკა
კონცეფცია «Midnight Cellar», პალიტრა და შრიფტები, 16 ანიმაციური ფენა სუფთა CSS-ზე, GEO-ოპტიმიზაცია LLM-ისთვის, კონტენტ-პაიპლაინი 200+ ფოტოსთვის, სტეკი.
TL;DR
BRUNO Tbilisi — modern European fusion ვაკეში. საიტი შექმნილია როგორც editorial-ჟურნალი: ღრმა ink-შავი ფონი («Midnight Cellar»), კრემისფერი ქაღალდის ტექსტი, fire-orange სანთლის აქცენტი, Fraunces Variable display italic-ჩანართებით, IBM Plex Serif body-სთვის. 16 ანიმაციური ფენა რეალიზებულია სუფთა CSS + IntersectionObserver-ზე — framer-motion-ის გარეშე, ანიმაციური ბიბლიოთეკების გარეშე. Multi-page არქიტექტურა: მთავარი გვერდის 9 სექცია + 8 routes. Built for GEO: JSON-LD Restaurant + Menu + FAQPage, llms.txt answer-first summary-ით, robots.txt გამოკვეთილი Allow-ით GPTBot, ClaudeBot, PerplexityBot, Google-Extended-ისთვის. კონტენტ-პაიპლაინი აგროვებს 200+ ფოტოს 5 წყაროდან (Instagram, Yandex Maps, Restaurant Guru, TripAdvisor, Google Business), prepare-ი sharp + blurhash + pHash-დედუპლიკაციის გავლით. დეპლოი — VPS + nginx + Cloudflare, დომენი brunotbilisi.com.
კონტექსტი და აუდიტორია
BRUNO — რესტორანი ვაკეში, თბილისის პრემიუმ-უბანში. სამიზნე სტუმარი: საერთაშორისო ტურისტი 30–45 წლის (ვიზიტი 2–4 დღით) ან ექსპატი, რომელიც ეძებს კარგ ადგილს biodynamic wine-ით და წესიერი brunch-ით. კონკურენცია ვაკეში მკვრივია: ათეულობით ადგილი 500 მ რადიუსში, თითოეული თავისი ინსტაგრამითა და Resos-გვერდით.
დამკვეთს უნდოდა საიტ-მაღაზია, არა საიტ-ვიზიტბარათი. მიზანი — ორგანული ძიების გარდაქმნა ჯავშნებად და Wolt-მიწოდების შეკვეთებად. ეს ნიშნავს:
- აღმოჩენადობა LLM-ასისტენტებში — როცა ტურისტი ეკითხება ChatGPT-ს «where to get good brunch in Tbilisi», BRUNO უნდა მოხვდეს ტოპ-3-ში.
- Editorial-სიმკვრივე — ცოტა ტექსტი, ბევრი ფოტო, ყველაფერი რიტმულად. არა «ჩვენ შესახებ / მენიუ / კონტაქტები», არამედ სცენა-განწყობა, როგორც ღვინის ჟურნალში.
- სტუმრების სამივე ტიპის ადაპტურობა — დაკავებული ბიზნეს-ლანჩი (საჭიროა ფასი + დრო + ტელეფონი), დაგეგმილი ვახშამი (საჭიროა მენიუ მთლიანად + ბიოდინამიკური ღვინოები), ბრანჩ-შაბათ-კვირა (საჭიროა კერძების ფოტოები და სამუშაო საათები).
ვიდეო-მიმოხილვა ზემოთ აჩვენებს გვერდის რეალურ რიტმს: პაუზა hero-ზე (სანთელი ციმციმებს მარჯვენა ზედა კუთხეში), signature-კერძების კარტოჩკები 3D tilt-ით, /reels ჩაშენებული ვიდეო-მარყუჟებით, masonry-გალერეა blurhash placeholder-ით, გრძელი ჟურნალური ციტატები reviews-ში.
კონცეფცია: Midnight Cellar
ღვინის რესტორანში საღამოს შესვლა — ეს არის ნახევრად-ბნელ დარბაზში შესვლა, სადაც სანთლები მაგიდებზე, აგურის თაღები, ბოთლები მინის უკან და მიჩუმებული jazz. მე ეს გადავიტანე digital-ში — ფაქტიურად. პალიტრა საპირისპიროა სტანდარტული «რესტორნის» საიტისა (სადაც ჩვეულებრივ თეთრი ფონი პასტელისფერი აქცენტებით): აქ ღრმა ink-შავი თბილი კრემისფერი ქაღალდის ტექსტით. სანთლის ცეცხლი — ერთადერთი, რაც ანათებს.
კონცეფციის სამი გასაღები:
- არა სკევომორფიზმი, არამედ ალუზია. მე არ ვხატავ სანთელს — ვაყენებ რადიალურ გრადიენტს
radial-gradient(ellipse 55% 42% at 18% 15%, rgba(201,101,31,0.28) 0%, transparent 55%)ციმციმის keyframe-ანიმაციით (6s ease-in-out infinite). ეფექტი — თითქოს მარჯვნივ ზევით ანთია თბილი სინათლის წყარო, და ის ცოტათი პულსირებს. ეს საკმარისია — ტვინი თვითონ ასრულებს სანთელს. - ჟურნალური რიტმი, არა ლენდინგისეული. სექციები ფართოა, hairline-გამყოფები 1px გრადიენტი გამჭვირვალობისკენ (
linear-gradient(to right, transparent, rgba(245,238,224,0.16), transparent)), ბევრი ჰაერი. სათაურები მსხვილია, body 16-17px, line-height 1.7. არა «გასაყიდი ლენდინგი», არამედ გრძელი გადაშლა ჟურნალში. - Editorial > marketing. არანაირი «დაჯავშნე მაგიდა ახლავე!» huge-buttons-ში. ჯავშნის ღილაკი მშვიდია, mini-mono caps. მთავარი — ფოტოები და ტექსტები.
პალიტრა
app/globals.css :root-დან — ტოკენები:
| ტოკენი | HEX | როლი |
|---|---|---|
--ink-base | #0D0A07 | ღრმა ink-შავი, body-ის ფონი — შავსა და მუქ-ყავისფერს შორის |
--ink-raised | #15110B | წამოწეული ფენა — კარტოჩკები, hover-state |
--ink-inset | #080605 | base-ზე ღრმა — ფოკუსური სექციები (Hero, Reviews) |
--ink-line | rgba(245,238,224,0.08) | თხელი შტრიხები — გამყოფები, borders |
--ink-line-strong | rgba(245,238,224,0.16) | hairline divider გრადიენტის ცენტრი |
--cream | #F5EEE0 | ძირითადი ტექსტი — თბილი ქაღალდი, არა სუფთა თეთრი |
--cream-muted | #9A8E78 | მეორადი ტექსტი — caption, meta |
--fire | #C9651F | ცეცხლისფერი ნარინჯისფერი — სანთელი, აქცენტი, single CTA |
--fire-glow | rgba(201,101,31,0.12) | რბილი განათება candle-glow უტილიტებისთვის |
--fire-glow-strong | rgba(201,101,31,0.22) | გაძლიერებული განათება hero-სთვის |
--wine | #6B1E2A | იშვიათი ემოციური აქცენტი (brunch, რევიუები) |
--wine-muted | #3A1419 | ღვინო მუქი, ღრმა სექციებისთვის |
მხოლოდ ორი აქცენტ-ფერი — fire და wine. მწვანე არ არის, ლურჯი არ არის, იისფერი არ არის. ეს იძლევა ვიზუალურ მონოლითურობას — ლოგოს გარეშეც კი საიტი ცნობადად «BRUNO»-ა.
ტიპოგრაფიკა
სამი შრიფტი, ყველა next/font/google-ის გავლით:
const fraunces = Fraunces({
subsets: ['latin'],
variable: '--font-fraunces',
axes: ['SOFT', 'WONK', 'opsz'],
display: 'swap',
});
const plexSerif = IBM_Plex_Serif({ ..., weight: ['400'] });
const plexMono = IBM_Plex_Mono({ ..., weight: ['400', '500'] });
- Fraunces Variable display-სათაურებისთვის. აქტიურია სამი ღერძი:
SOFT(სერიფების სირბილე 0–100),WONK(დამახასიათებელი «უცნაური» ფორმები 0–1),opsz(ოპტიკური ზომა 9–144). Italic-ვარიანტიWONK 1-ით დაSOFT 100-ით გამოიყურება როგორც ხელნაკეთი გრავიურა — არა როგორც ჩვეული დახრა. ეს იძლევა იშვიათ ხასიათს, რომლის გამეორება რთულია. - IBM Plex Serif body-სთვის — ნეიტრალური თანამედროვე serif, weight 400. კარგად იკითხება მუქ ფონზე გრძელ მონაკვეთებზე.
- IBM Plex Mono meta-სთვის — caption, ფასები, სამუშაო საათები, ტელეფონი. Mono editorial-ში — ეს არის მინიშნება «ეტიკეტზე», ხელნაკეთობაზე.
ძირითადი უტილიტები:
.display {
font-family: var(--font-fraunces);
font-variation-settings: 'SOFT' 30, 'WONK' 0, 'opsz' 144;
letter-spacing: -0.035em;
line-height: 0.92;
}
.display-italic {
font-style: italic;
font-variation-settings: 'SOFT' 100, 'WONK' 1, 'opsz' 144;
}
Letter-spacing -0.035em და line-height 0.92 display-ზე — სათაურები ძალიან მკვრივია, ეს არის «მარცვალი». body-ზე — ჩვეულებრივი 1.7 line-height, კითხვა კომფორტულია გრძელ მონაკვეთებზე.
16 ანიმაციური ფენა
მთავარი განსხვავება პორტფოლიოს სხვა ქეისებისგან — აქ ანიმაციები სუფთა CSS + IntersectionObserver-ზე, framer-motion-ის გარეშე, ანიმაციური ბიბლიოთეკების გარეშე. ეს ნიშნავს:
- Bundle ~80 KB gzipped-ით ნაკლები
- SSR იდეალურად მუშაობს (არაფერი «ხტუნავს» ჰიდრაციისას)
- Performance — ყველა ანიმაცია GPU compositor-ზე, არ გადაითვლება ყოველ rAF-ზე
ზემოთ ვიდეოში ჩანს 9 16-დან. სრული სია:
გლობალური ფენები
- Film grain
body::before-ის გავლით. SVG-noisefeTurbulence-ის გავლით, opacity 0.04,mix-blend-mode: screen. მუდმივი ფირის ფაქტურა — მყისიერი ფოტო-ბეჭდვის შეგრძნება, არა «ვერსტკის». - Scroll progress bar — fixed top, fire-orange ზოლი, სიგანე =
scrollY / scrollHeight. რეალიზებულიაElement.animate({ width: '...%' })-ის გავლით rAF-throttle-ით. - Scroll velocity blur — სწრაფ scroll-ზე body იღებს
filter: blur(0..1.8px)--scroll-blurCSS-ცვლადის გავლით. Lerp-დაგლუვება (current += (target - current) * 0.3) აშორებს კანკალს. ნელ სქროლზე blur = 0.
Hero
- Candle flicker — radial-gradient მარჯვენა ზედა კუთხეში keyframe
candleFlicker6sease-in-out infinite-ით, ცვლის opacity-ს 0.85 → 1.0 → 0.92 → 1.0 არარეგულარულ ინტერვალებში. ჰგავს ნამდვილ სანთლის ცეცხლს. - Hero parallax JS-გადაფარვის
HeroParallax-ის გავლით: ფონი იწევა 0.3× scrollY-დან CSS-ცვლადი--parallax-y-ის ხარჯზე. - Gyro-drift mobile-ზე: მოწყობილობის დახრის დროს ფონი იწევა ±2-3°-ით კუთხის მიხედვით.
DeviceOrientationEventAPI-ის გავლით — იქ, სადაც iOS იძლევა permission-ს. თბილისის ერთ-ერთი ცოტა საიტი, რომელიც იყენებს gyroscope-ს.
სექციები
- Scroll-reveal
[data-reveal]-attribute-ის დაIntersectionObserver-ის გავლით. ელემენტს აქვსopacity: 0; transform: translateY(24px), viewport-ში მოხვედრისას იღებსdata-reveal-in="true"→ opacity და transform ნულისკენ, transition 600ms cubic-bezier(0.22, 1, 0.36, 1). Stagger--reveal-delayCSS-ცვლადის გავლით. - Menu category stagger + 3D tilt. გვერდზე /menu და Signatures-ში მთავარზე — კარტოჩკები რეაგირებენ mouse-over-ზე:
transform: perspective(1000px) rotateX(...) rotateY(...)კურსორის პოზიციის მიხედვით. Subtle, მაგრამ კარტოჩკებს «ცოცხალს» ხდის. Stagger გამოჩენისას — 80ms კარტოჩკებს შორის. - Gallery wave — masonry-ბლოკები იღებენ reveal-delay-ს დიაგონალური ტალღით:
delay = (col * 50 + row * 80)ms. ქმნის «გადმომავალი ტალღის» ეფექტს ერთდროული მოსაწყენი გამოჩენის ნაცვლად. - Amber panel breathe — სექცია Brunch & Bar (ცალკე გვერდზე /brunch) სუნთქავს: თბილი ქარვისფერი ნათება პულსირებს 8s alternate. შეუძლებელია ნახვა მთავარის მიმოხილვაში — საჭიროა შესვლა /brunch-ზე.
- Sunrise sweep — one-shot ანიმაცია /brunch პანელზე. viewport-ში პირველად მოხვედრისას (sessionStorage flag) პანელი იღებს class
sunrise-sweep-ს, და სინათლის გრადიენტი «გადაივლის» მარცხნიდან მარჯვნივ 1.5s-ში. «ამოსვლის» ეფექტი.
Touch / Mobile
- Touch ripple — კასტომი feedback შეხებაზე:
touchstart-ისას ჩნდება წრე opacity-fade-out-ით 600ms-ში. უკეთესია, ვიდრე default tap-highlight. - Swipe hints — mobile-კარუსელ Signatures-ზე: თხელი ისარი «გადახვევით ჩნდება» სექციის პირველი მონახულებისას, მიანიშნებს swipe-ზე.
- Music note ping — «cocktails»-სექციაზე hover-ისას დესკტოპზე პატარა music-note იკონა pings-ს tiny scale 1.0 → 1.15 → 1.0-ით.
Reels
- Autoplay-on-scroll videos გვერდზე /reels: 8 ვიდეო-მარყუჟი ინტერიერით და კერძებით. გამოიყენება
IntersectionObserverthreshold 0.5-ით — თუ ვიდეო viewport-ის ცენტრშია, ის უკრავს, წინააღმდეგ შემთხვევაში პაუზდება. ხმის გარეშე. ოპტიმიზაცია — preload="metadata" ყველაზე, ვიდეო ჩამოიტვირთება მხოლოდ სქროლის დროს.
CountUp
- CountUp About-სექციაზე: რეიტინგები 4.9★, 4.8★, 4.6★, 4.7★ ოთხი წყაროდან (Google/Yandex/TripAdvisor/Restaurant Guru) — ანიმირდება 0-დან მიზნობრივ მნიშვნელობამდე 1.2s-ში viewport-ში მოხვედრისას. Subtle, მაგრამ ციფრებს «ცოცხალ» შეფერილობას მატებს.
ყველა 16 ფენა პატივს სცემს prefers-reduced-motion: reduce-ს. CSS-ში:
@media (prefers-reduced-motion: reduce) {
.candle-glow::before { animation: none; }
[data-reveal] { transition: opacity 200ms; transform: none !important; }
/* ... */
}
JS-ში — window.matchMedia('(prefers-reduced-motion: reduce)').matches შემოწმება ყოველი კომპონენტ-პროვაიდერის სტარტზე, fallback no-op-ისკენ.
გვერდის არქიტექტურა
მთავარი (app/page.tsx) — 9 სექცია:
- Hero (
#top) — full-bleed დარბაზის ფოტო + display-სათაური «Bruno · Vake», ქვესათაური «Modern European fusion», hours + tel mini-mono caps - HomeQuoteStrip — მოკლე ციტატა gourmet-მიმოხილვიდან, fire-orange wonk-italic
- HomeTriad — სამი vertical-ბლოკი: «Eat», «Drink», «Visit» ფოტოებით
- Signatures (
#signatures) — 9 signature კერძი preview-ით + 3D tilt grid - Reels (
#reels) — 4 ჩაშენებული ვიდეო-მარყუჟი autoplay-on-scroll-ით - Gallery (
#gallery) — 57 ფოტო masonry-ში, blurhash placeholder - Reviews (
#reviews) — გრძელი ჟურნალური ციტატები 4 წყაროდან (Google/Yandex/TripAdvisor/Guru) - HomeVisitCompact — რუკა + მისამართი + საათები + ტელეფონი + booking link
- Footer — სოცქსელები, იურ.ინფორმაცია (BBB Restogroup LLC, 402360404)
Sticky Nav ზემოთ — fade-in პირველი გადახვევის შემდეგ, blur backdrop, semi-transparent ink. ღრმა დონის გვერდებზე (მაგალითად /menu/pasta) ემატება Breadcrumb. დამალული ScrollProgress — fire-orange ზოლი ზედაპირზე.
შიდა გვერდები
8 routes მთავარის გარდა:
/menu+/menu/{category}— 11 კატეგორია (starters, salads, pasta, pizza, mains, desserts, brunch, brunch-extras, kids, drinks, wine). თითოეულ ქვეკატეგორიას — საკუთარი გვერდი full-bleed ფოტოთი და კერძების stagger-კარტოჩკებით./brunch— ცალკე სადესანტო გვერდი სამიზნე აუდიტორიისთვის «brunch 16:00-მდე ყოველდღე». მასზე ცხოვრობს sunrise sweep — საიტის ყველაზე ნელი და ემოციური ანიმაცია./cocktails— ცალკე გვერდი CocktailsPanel-ით + მუსიკალური ping-ეფექტი იკონაზე./reels— autoplay-on-scroll რეჟიმი: 8 ვიდეო-მარყუჟი, სქროლავ და ხედავ ყველაფერს, რაც დარბაზში ხდება./reviews— სრული 127 რევიუ, 4 ფილტრი წყაროს მიხედვით./visit— რუკა + სამი მისვლის გზა + პარკინგი + dress code./about— გრძელი ტექსტი რესტორნის ფილოსოფიაზე + რეიტინგები CountUp-ით.
GEO: built for LLM-discoverability
საიტი კეთდებოდა ძიების მეორე კონტურისთვის — LLM-ასისტენტებისთვის (ChatGPT, Claude, Perplexity). შეკრების მომენტში თბილისში ამით არავინ იყო დაკავებული. რა გაკეთდა:
public/llms.txt
Answer-first summary plain-text-ში: 600 სიტყვა პასუხებით 8 მთავარ კითხვაზე («where is BRUNO», «what's the cuisine», «opening hours», «brunch menu», «cocktail program», «vegan options», «booking», «address»). სტრუქტურირებულია როგორც Q&A. LLM-ბოტები ინდექსაციისას იღებენ კონცენტრატს — არ არის საჭირო HTML-ის პარსინგი.
public/robots.txt
გამოკვეთილი Allow: ამისთვის:
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Applebot-Extended
Allow: /
User-agent: Bytespider
Allow: /
თბილისის ბევრი რესტორნის საიტი ბლოკავს ამ ბოტებს default-ით (CMS-ის default robots.txt-ის გავლით). ჩვენ — პირიქით.
JSON-LD
app/layout.tsx-ში ჩაკერებულია სრული schema:
- Restaurant — name, address, geo, phone, hours, priceRange, servesCuisine, acceptsReservations
- Menu — ყველა 11 კატეგორია MenuSection-ით და MenuItem-ით (სახელი, აღწერა, ფასი, allergen, image)
- FAQPage — 8 კითხვა answer-first პასუხებით
Semantic HTML
<article>, <section>, <dl>/<dt>/<dd>, <figure>/<figcaption> — ყველგან, სადაც ეს მართებულია. LLM-ბოტები უკეთესად იღებენ ერთეულებს სწორად დანიშნული კონტენტიდან.
Answer-first prose About / Visit / FAQ-ში
არა «BRUNO — ეს არის ადგილი, სადაც...», არამედ «BRUNO is a modern European fusion restaurant in Vake, Tbilisi. Address: Zakaria Paliashvili 11a. Phone: +995 551 07 78 49. Hours: Sun–Thu 11:00–23:00, Fri–Sat 11:00–02:00.» — შიშველი პასუხი პირველ წინადადებაში, შემდეგ დეტალები.
შედეგი
პუბლიკაციიდან 2 თვის შემდეგ BRUNO ხვდება ChatGPT-სა და Perplexity-ში მოთხოვნებზე «brunch in Tbilisi», «european fusion Vake», «truffle pasta Tbilisi». ეს არის ორგანული არხი, რომელიც არ იყიდება და არ არის დამოკიდებული ფასიან არხებზე.
კონტენტ-პაიპლაინი 200+ ფოტოსთვის
რესტორნის-საიტის მთავარი ტკივილი — ფოტოები. დამკვეთმა მისცა Instagram-ის წვდომა და Yandex Maps-ის საერთო საქაღალდე. მე ავაწყე პაიპლაინი 5 წყაროზე:
| წყარო | ინსტრუმენტი | ფოტო |
|---|---|---|
| Instagram @brunotbilisi | instaloader (login-ის გარეშე, საჯარო პროფილი) | 242 პოსტი |
| Yandex Maps | Playwright headless chromium → /gallery/ → orig | 65+ |
| Google Business Profile | Playwright g.page/r/...-ის გავლით | ~30 |
| TripAdvisor | Playwright review URL-ის გავლით | ~40 |
| Restaurant Guru | Playwright profile-ის გავლით | 45 |
შეკრების შემდეგ — ერთიანი pipeline scripts/process-gallery.mjs:
- Resize
sharp-ის გავლით: long edge max 1600px (მეტი არ არის საჭირო ვებისთვის). - Convert ორ ფორმატში: WebP q=82 + AVIF q=70. ფრონტზე —
<picture><source type="image/avif">+ WebP fallback-ით. AVIF უფრო მძიმეა CPU-ზე, მაგრამ ზომა 20-30%-ით ნაკლებია. - Blurhash ყოველი ფოტოსთვის — 32-character placeholder, რენდერდება მაშინვე
<canvas>-ში, სანამ იტვირთება საბოლოო გამოსახულება. არანაირი layout-ის «ხტუნვა», არანაირი grey rectangles. - pHash დედუპლიკაცია — იგივე ფოტოები არის IG-ზე და Yandex-ზე (რესტორნის ფოტო-ფოლდერი). pHash distance < 5 — ითვლება დუბლიკატად, რჩება მხოლოდ საუკეთესო რეზოლუციის მიხედვით.
- Manifest —
public/gallery/manifest.jsonმეტამონაცემებით:[{ "src": "/gallery/2025-03-15_001.webp", "srcAvif": "/gallery/2025-03-15_001.avif", "blurhash": "L9AS}j%M00%M~q%M00%M", "width": 1600, "height": 2000, "caption": "...", "date": "2025-03-15", "source": "instagram", "tags": ["food", "interior", "drinks"] }] - Curation
manifest.curated.json-ში — hero-კანდიდატების შერჩევა (ფოტოები luminosity ~0.48-ით — არც ძალიან მუქი, არც გადანათებული), და 3 sub-სექცია main gallery-სთვის: «interior», «food», «drinks».
Final gallery — 57 ფოტო ~250 შეკრებიდან, ორგანიზებულია სამ მთლიან განყოფილებაში.
სტეკი და რატომ ასე
| ტექნოლოგია | რატომ |
|---|---|
| Next.js 14 | App Router. SSR-ready (დინამიური Open Graph-ისთვის), მაგრამ სტატიკური გვერდები ქეშირდება. Image optimization next/image-ის გავლით |
| TypeScript | strict-mode, types restaurant data-სთვის, manifest, gallery items |
| Tailwind CSS | დიზაინ-ტოკენები :root CSS-ცვლადების გავლით, უტილიტარული კლასები. Bundle ~12 KB purge-ის შემდეგ |
| next/font/google | Fraunces Variable (axes: SOFT, WONK, opsz) + IBM Plex Serif + IBM Plex Mono — ყველა იყოფა სუბსეტებად, არანაირი FOUT |
| CSS animations + IntersectionObserver | ყველა 16 ფენა სუფთა CSS-ზე, framer-motion-ის გარეშე. Bundle ~80 KB gzipped-ით ნაკლები, performance — ყველა ანიმაცია GPU compositor-ზე |
| sharp | image processing, ~5x უფრო სწრაფი, ვიდრე ImageMagick |
| blurhash | 32-char placeholder layout shift-ის გარეშე |
| VPS + nginx + Cloudflare | self-hosted, არანაირი Vercel-tier შეზღუდვა. Cloudflare Polish + WebP/AVIF auto-conversion |
რა იყო რთული
1. ScrollVelocityBlur jank-ების გარეშე
პირდაპირი გადაწყვეტა — ყოველ scroll-event-ზე --scroll-blur-ის განახლება. ეს ქმნის jank-ს სწრაფი სქროლისას — frame გამოტოვდება. გადაწყვეტა:
useScrollEmitterhook —requestAnimationFramethunk, შეიძლება unsubscribe-ი- Lerp-დაგლუვება:
current += (target - current) * 0.3— თუნდაც scroll-event აყეფდეს, blur გლუვად მოძრაობს - Threshold:
currentBlur < 0.04 → 0— წყვეტს წვრილმანებს
DevTools FPS-ში არასოდეს არ ვარდება 58-ქვემოთ.
2. Sunrise sweep ზუსტად ერთხელ
დამკვეთი: «sunrise sweep — ეს არის ემოცია, მეორედ ნანახი ის მობეზრებთ». გადაწყვეტა:
- viewport-ში პირველი ნახვისას — IntersectionObserver triggers class
sunrise-sweep sessionStorage.setItem('bruno.sunrise.seen', '1')— სესიის დროშა- /brunch-ზე ერთსა და იმავე სესიაში ხელახლა შესვლისას — ანიმაცია არ ეშვება
- სესიებს შორის (ახალი ჩანართი, ახალი დღე) — ისევ მუშაობს
3. Gyroscope iOS-ზე — permission flow
DeviceOrientationEvent.requestPermission() ხელმისაწვდომია მხოლოდ user-gesture-ის შემდეგ (კნოპმაკი click). iOS pre-13-ზე permission არ არის საჭირო, iOS 13+-ზე საჭიროა გამოკვეთილი მოთხოვნა. რეალიზებულია onClick-ის გავლით mini-floating იკონაზე «პარალაქსის ჩართვა» კუთხეში — click-ისას იძახება requestPermission(), approve-ის შემდეგ gyro-drift აქტიურდება. Android-ზე — ყოველგვარი permission-ის გარეშე, gyro მუშაობს მაშინვე.
4. Image pipeline deduplication-ით
pHash ბიბლიოთეკა მუშაობს მხოლოდ browser-ზე (Canvas API). გადაწყვეტა — node-port pHash-image + sharp წინასწარი დამუშავებისთვის. დრო — ~20s 250 ფოტოზე VPS-ზე. Cache .scrape-cache/phash/-ში — განმეორებითი გაშვებები ამუშავებს მხოლოდ ახალ ფოტოებს.
5. JSON-LD Menu schema 11 კატეგორიის × N კერძის ზომით
Restaurant Schema მოითხოვს Menu ქვესქემას MenuSection-ით და MenuItem-ით. BRUNO-ს — 11 კატეგორია, 80+ კერძი. მე ვაგენერირებ schema-ს data/restaurant.ts-დან runtime app/layout.tsx-ში. JSON-LD-ის ზომა — ~30 KB unminified. მინიფიკაცია JSON.stringify-ით spaces-ის გარეშე — 18 KB. ჩაიდება <script type="application/ld+json" dangerouslySetInnerHTML>-ის გავლით.
6. Editorial design mobile-ზე
Editorial-საიტი მობილურზე კარგავს ემოციის 80%-ს — მასშტაბი სხვაა, რიტმი ინგრევა. გადაწყვეტები:
- Display-სათაურები fluid
clamp(2.5rem, 8vw, 6rem)-ის გავლით — iPhone 13-ზე Hero h1 — 64px, iPad-ზე — 96px, 4K-ზე — 128px - Hairline-გამყოფები შეცვლილია
1px solid rgba(245,238,224,0.10)-ით — მობილურზე subtler - Padding y-direction შემცირებულია
py-32-დანpy-16-მდე - 3D tilt იხურება mobile-ზე —
@media (hover: none) { .tilt-card:hover { transform: none; } }— რადგან კურსორი არ არის
დეპლოი
VPS, nginx, Let's Encrypt, Cloudflare — same setup, რაც პორტფოლიოს სხვა პროექტებს. nginx კონფიგი სტანდარტულია, ერთი დეტალის გარდა:
# Long-term cache for hashed assets
location ~* \.(webp|avif|woff2|js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Short cache for HTML (revalidate often)
location / {
expires 5m;
add_header Cache-Control "public, must-revalidate";
}
Cloudflare Polish — auto-WebP / AVIF ნებისმიერ ატვირთულ გამოსახულებაზე, Brotli, DDoS protection. ჰოსტინგის ღირებულება — $0/თვეში (free Cloudflare + იაფი VPS).
შედეგი
საიტი გამოშვებულია და მუშაობს brunotbilisi.com-ზე. გაშვებიდან პირველ ორ თვეში:
- Lighthouse mobile: Performance 96, Accessibility 100, Best Practices 100, SEO 100
- ChatGPT და Perplexity ანჯღრევენ BRUNO-ს ტოპ-3-ში ძირითად ინგლისურ მოთხოვნებზე brunch-ისა და fine dining-ის შესახებ თბილისში
- ჯავშნის Conversion საიტის გავლით — ერთადერთი მეტრიკა, რომელსაც დამკვეთი იყოფა — გაიზარდა ~40%-ით წინა საიტთან შედარებით (Squarespace template)
სტეკი არჩეულია მაქსიმალური ხანგრძლივობისა და კონტროლისთვის: Next.js 14 — სტაბილური, არა bleeding-edge; სუფთა CSS-ანიმაციები — იმუშავებს 5 წლის შემდეგაც, თუნდაც framer-motion და Tailwind გადადოს; კონტენტი data/restaurant.ts-ში — ტელეფონის ან კერძის ფასის შესწორება იღებს 30 წამს.