гора.
AI-translation · draft (awaiting native review)
BRUNO Tbilisi

ბიზნეს-ლოგიკა

კონცეფცია «Midnight Cellar», პალიტრა და შრიფტები, 16 ანიმაციური ფენა სუფთა CSS-ზე, GEO-ოპტიმიზაცია LLM-ისთვის, კონტენტ-პაიპლაინი 200+ ფოტოსთვის, სტეკი.

~13 წუთი წასაკითხი · 2684 სიტყვა
სრული მიმოხილვა: მთავარი (Hero → About → Signatures → Reels → Gallery → Reviews → Visit → Footer) + გვერდი /reels autoplay-ვიდეოებით. დესკტოპის და მობილურის მიმოხილვები — ბრაუზერი ირჩევს წყაროს viewport-ის სიგანის მიხედვით.

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-მიწოდების შეკვეთებად. ეს ნიშნავს:

  1. აღმოჩენადობა LLM-ასისტენტებში — როცა ტურისტი ეკითხება ChatGPT-ს «where to get good brunch in Tbilisi», BRUNO უნდა მოხვდეს ტოპ-3-ში.
  2. Editorial-სიმკვრივე — ცოტა ტექსტი, ბევრი ფოტო, ყველაფერი რიტმულად. არა «ჩვენ შესახებ / მენიუ / კონტაქტები», არამედ სცენა-განწყობა, როგორც ღვინის ჟურნალში.
  3. სტუმრების სამივე ტიპის ადაპტურობა — დაკავებული ბიზნეს-ლანჩი (საჭიროა ფასი + დრო + ტელეფონი), დაგეგმილი ვახშამი (საჭიროა მენიუ მთლიანად + ბიოდინამიკური ღვინოები), ბრანჩ-შაბათ-კვირა (საჭიროა კერძების ფოტოები და სამუშაო საათები).

ვიდეო-მიმოხილვა ზემოთ აჩვენებს გვერდის რეალურ რიტმს: პაუზა hero-ზე (სანთელი ციმციმებს მარჯვენა ზედა კუთხეში), signature-კერძების კარტოჩკები 3D tilt-ით, /reels ჩაშენებული ვიდეო-მარყუჟებით, masonry-გალერეა blurhash placeholder-ით, გრძელი ჟურნალური ციტატები reviews-ში.

კონცეფცია: Midnight Cellar

ღვინის რესტორანში საღამოს შესვლა — ეს არის ნახევრად-ბნელ დარბაზში შესვლა, სადაც სანთლები მაგიდებზე, აგურის თაღები, ბოთლები მინის უკან და მიჩუმებული jazz. მე ეს გადავიტანე digital-ში — ფაქტიურად. პალიტრა საპირისპიროა სტანდარტული «რესტორნის» საიტისა (სადაც ჩვეულებრივ თეთრი ფონი პასტელისფერი აქცენტებით): აქ ღრმა ink-შავი თბილი კრემისფერი ქაღალდის ტექსტით. სანთლის ცეცხლი — ერთადერთი, რაც ანათებს.

კონცეფციის სამი გასაღები:

  1. არა სკევომორფიზმი, არამედ ალუზია. მე არ ვხატავ სანთელს — ვაყენებ რადიალურ გრადიენტს radial-gradient(ellipse 55% 42% at 18% 15%, rgba(201,101,31,0.28) 0%, transparent 55%) ციმციმის keyframe-ანიმაციით (6s ease-in-out infinite). ეფექტი — თითქოს მარჯვნივ ზევით ანთია თბილი სინათლის წყარო, და ის ცოტათი პულსირებს. ეს საკმარისია — ტვინი თვითონ ასრულებს სანთელს.
  2. ჟურნალური რიტმი, არა ლენდინგისეული. სექციები ფართოა, hairline-გამყოფები 1px გრადიენტი გამჭვირვალობისკენ (linear-gradient(to right, transparent, rgba(245,238,224,0.16), transparent)), ბევრი ჰაერი. სათაურები მსხვილია, body 16-17px, line-height 1.7. არა «გასაყიდი ლენდინგი», არამედ გრძელი გადაშლა ჟურნალში.
  3. Editorial > marketing. არანაირი «დაჯავშნე მაგიდა ახლავე!» huge-buttons-ში. ჯავშნის ღილაკი მშვიდია, mini-mono caps. მთავარი — ფოტოები და ტექსტები.

პალიტრა

app/globals.css :root-დან — ტოკენები:

ტოკენიHEXროლი
--ink-base#0D0A07ღრმა ink-შავი, body-ის ფონი — შავსა და მუქ-ყავისფერს შორის
--ink-raised#15110Bწამოწეული ფენა — კარტოჩკები, hover-state
--ink-inset#080605base-ზე ღრმა — ფოკუსური სექციები (Hero, Reviews)
--ink-linergba(245,238,224,0.08)თხელი შტრიხები — გამყოფები, borders
--ink-line-strongrgba(245,238,224,0.16)hairline divider გრადიენტის ცენტრი
--cream#F5EEE0ძირითადი ტექსტი — თბილი ქაღალდი, არა სუფთა თეთრი
--cream-muted#9A8E78მეორადი ტექსტი — caption, meta
--fire#C9651Fცეცხლისფერი ნარინჯისფერი — სანთელი, აქცენტი, single CTA
--fire-glowrgba(201,101,31,0.12)რბილი განათება candle-glow უტილიტებისთვის
--fire-glow-strongrgba(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-დან. სრული სია:

გლობალური ფენები

  1. Film grain body::before-ის გავლით. SVG-noise feTurbulence-ის გავლით, opacity 0.04, mix-blend-mode: screen. მუდმივი ფირის ფაქტურა — მყისიერი ფოტო-ბეჭდვის შეგრძნება, არა «ვერსტკის».
  2. Scroll progress bar — fixed top, fire-orange ზოლი, სიგანე = scrollY / scrollHeight. რეალიზებულია Element.animate({ width: '...%' })-ის გავლით rAF-throttle-ით.
  3. Scroll velocity blur — სწრაფ scroll-ზე body იღებს filter: blur(0..1.8px) --scroll-blur CSS-ცვლადის გავლით. Lerp-დაგლუვება (current += (target - current) * 0.3) აშორებს კანკალს. ნელ სქროლზე blur = 0.

Hero

  1. Candle flicker — radial-gradient მარჯვენა ზედა კუთხეში keyframe candleFlicker 6s ease-in-out infinite-ით, ცვლის opacity-ს 0.85 → 1.0 → 0.92 → 1.0 არარეგულარულ ინტერვალებში. ჰგავს ნამდვილ სანთლის ცეცხლს.
  2. Hero parallax JS-გადაფარვის HeroParallax-ის გავლით: ფონი იწევა 0.3× scrollY-დან CSS-ცვლადი --parallax-y-ის ხარჯზე.
  3. Gyro-drift mobile-ზე: მოწყობილობის დახრის დროს ფონი იწევა ±2-3°-ით კუთხის მიხედვით. DeviceOrientationEvent API-ის გავლით — იქ, სადაც iOS იძლევა permission-ს. თბილისის ერთ-ერთი ცოტა საიტი, რომელიც იყენებს gyroscope-ს.

სექციები

  1. 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-delay CSS-ცვლადის გავლით.
  2. Menu category stagger + 3D tilt. გვერდზე /menu და Signatures-ში მთავარზე — კარტოჩკები რეაგირებენ mouse-over-ზე: transform: perspective(1000px) rotateX(...) rotateY(...) კურსორის პოზიციის მიხედვით. Subtle, მაგრამ კარტოჩკებს «ცოცხალს» ხდის. Stagger გამოჩენისას — 80ms კარტოჩკებს შორის.
  3. Gallery wave — masonry-ბლოკები იღებენ reveal-delay-ს დიაგონალური ტალღით: delay = (col * 50 + row * 80)ms. ქმნის «გადმომავალი ტალღის» ეფექტს ერთდროული მოსაწყენი გამოჩენის ნაცვლად.
  4. Amber panel breathe — სექცია Brunch & Bar (ცალკე გვერდზე /brunch) სუნთქავს: თბილი ქარვისფერი ნათება პულსირებს 8s alternate. შეუძლებელია ნახვა მთავარის მიმოხილვაში — საჭიროა შესვლა /brunch-ზე.
  5. Sunrise sweep — one-shot ანიმაცია /brunch პანელზე. viewport-ში პირველად მოხვედრისას (sessionStorage flag) პანელი იღებს class sunrise-sweep-ს, და სინათლის გრადიენტი «გადაივლის» მარცხნიდან მარჯვნივ 1.5s-ში. «ამოსვლის» ეფექტი.

Touch / Mobile

  1. Touch ripple — კასტომი feedback შეხებაზე: touchstart-ისას ჩნდება წრე opacity-fade-out-ით 600ms-ში. უკეთესია, ვიდრე default tap-highlight.
  2. Swipe hints — mobile-კარუსელ Signatures-ზე: თხელი ისარი «გადახვევით ჩნდება» სექციის პირველი მონახულებისას, მიანიშნებს swipe-ზე.
  3. Music note ping — «cocktails»-სექციაზე hover-ისას დესკტოპზე პატარა music-note იკონა pings-ს tiny scale 1.0 → 1.15 → 1.0-ით.

Reels

  1. Autoplay-on-scroll videos გვერდზე /reels: 8 ვიდეო-მარყუჟი ინტერიერით და კერძებით. გამოიყენება IntersectionObserver threshold 0.5-ით — თუ ვიდეო viewport-ის ცენტრშია, ის უკრავს, წინააღმდეგ შემთხვევაში პაუზდება. ხმის გარეშე. ოპტიმიზაცია — preload="metadata" ყველაზე, ვიდეო ჩამოიტვირთება მხოლოდ სქროლის დროს.

CountUp

  1. 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 სექცია:

  1. Hero (#top) — full-bleed დარბაზის ფოტო + display-სათაური «Bruno · Vake», ქვესათაური «Modern European fusion», hours + tel mini-mono caps
  2. HomeQuoteStrip — მოკლე ციტატა gourmet-მიმოხილვიდან, fire-orange wonk-italic
  3. HomeTriad — სამი vertical-ბლოკი: «Eat», «Drink», «Visit» ფოტოებით
  4. Signatures (#signatures) — 9 signature კერძი preview-ით + 3D tilt grid
  5. Reels (#reels) — 4 ჩაშენებული ვიდეო-მარყუჟი autoplay-on-scroll-ით
  6. Gallery (#gallery) — 57 ფოტო masonry-ში, blurhash placeholder
  7. Reviews (#reviews) — გრძელი ჟურნალური ციტატები 4 წყაროდან (Google/Yandex/TripAdvisor/Guru)
  8. HomeVisitCompact — რუკა + მისამართი + საათები + ტელეფონი + booking link
  9. 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 @brunotbilisiinstaloader (login-ის გარეშე, საჯარო პროფილი)242 პოსტი
Yandex MapsPlaywright headless chromium → /gallery/ → orig65+
Google Business ProfilePlaywright g.page/r/...-ის გავლით~30
TripAdvisorPlaywright review URL-ის გავლით~40
Restaurant GuruPlaywright profile-ის გავლით45

შეკრების შემდეგ — ერთიანი pipeline scripts/process-gallery.mjs:

  1. Resize sharp-ის გავლით: long edge max 1600px (მეტი არ არის საჭირო ვებისთვის).
  2. Convert ორ ფორმატში: WebP q=82 + AVIF q=70. ფრონტზე — <picture> <source type="image/avif"> + WebP fallback-ით. AVIF უფრო მძიმეა CPU-ზე, მაგრამ ზომა 20-30%-ით ნაკლებია.
  3. Blurhash ყოველი ფოტოსთვის — 32-character placeholder, რენდერდება მაშინვე <canvas>-ში, სანამ იტვირთება საბოლოო გამოსახულება. არანაირი layout-ის «ხტუნვა», არანაირი grey rectangles.
  4. pHash დედუპლიკაცია — იგივე ფოტოები არის IG-ზე და Yandex-ზე (რესტორნის ფოტო-ფოლდერი). pHash distance < 5 — ითვლება დუბლიკატად, რჩება მხოლოდ საუკეთესო რეზოლუციის მიხედვით.
  5. Manifestpublic/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"]
    }]
    
  6. Curation manifest.curated.json-ში — hero-კანდიდატების შერჩევა (ფოტოები luminosity ~0.48-ით — არც ძალიან მუქი, არც გადანათებული), და 3 sub-სექცია main gallery-სთვის: «interior», «food», «drinks».

Final gallery — 57 ფოტო ~250 შეკრებიდან, ორგანიზებულია სამ მთლიან განყოფილებაში.

სტეკი და რატომ ასე

ტექნოლოგიარატომ
Next.js 14App Router. SSR-ready (დინამიური Open Graph-ისთვის), მაგრამ სტატიკური გვერდები ქეშირდება. Image optimization next/image-ის გავლით
TypeScriptstrict-mode, types restaurant data-სთვის, manifest, gallery items
Tailwind CSSდიზაინ-ტოკენები :root CSS-ცვლადების გავლით, უტილიტარული კლასები. Bundle ~12 KB purge-ის შემდეგ
next/font/googleFraunces 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-ზე
sharpimage processing, ~5x უფრო სწრაფი, ვიდრე ImageMagick
blurhash32-char placeholder layout shift-ის გარეშე
VPS + nginx + Cloudflareself-hosted, არანაირი Vercel-tier შეზღუდვა. Cloudflare Polish + WebP/AVIF auto-conversion

რა იყო რთული

1. ScrollVelocityBlur jank-ების გარეშე

პირდაპირი გადაწყვეტა — ყოველ scroll-event-ზე --scroll-blur-ის განახლება. ეს ქმნის jank-ს სწრაფი სქროლისას — frame გამოტოვდება. გადაწყვეტა:

  • useScrollEmitter hook — requestAnimationFrame thunk, შეიძლება 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 წამს.

ბიზნესი და დიზაინი · hiregora.com