Skip to content

Component: landing

A developer who changes the landing page or debugs “the organiser sent the form, where did the request go?”.

  • The landing is a separate repository (ai-stickers-landing): Next.js 16 App Router on Vercel, three locales (ru, uz, en), no database.
  • Every page lives under /ru, /uz or /en. src/proxy.ts redirects any other path to a locale chosen from a cookie or the Accept-Language header.
  • The form posts to one server action, submitRequest. It filters bots (honeypot, minimum fill time), validates with a shared zod schema, rate-limits per IP in memory and then calls deliverRequest.
  • deliverRequest tries the leads server first (POST /leads, 5 s timeout). On a timeout, network error, 5xx, 401 or 404 it falls back to a direct Telegram sendMessage marked ⚠️ Запасной канал (“backup channel”). A 429 or 422 is not retried through the fallback.
  • All four delivery variables are server-only. Nothing with a secret is prefixed NEXT_PUBLIC_.
flowchart TB
    browser["Visitor browser"]

    subgraph next["Landing on Vercel"]
        proxy["proxy.ts - locale redirect"]
        layout["locale layout and page - sections"]
        dict["i18n - config and dictionaries"]
        form["RequestForm - client component"]
        action["submitRequest - server action"]
        schema["requestSchema - zod rules"]
        limit["rateLimit - 5 per IP per 10 min"]
        deliver["deliverRequest - routing and fallback"]
        msg["requestMessage - Telegram text"]
        sent["requestSent - 24 h browser flag"]
    end

    leads["sticker-api POST /leads"]
    tg["Telegram Bot API sendMessage"]

    browser --> proxy
    proxy --> layout
    layout --> dict
    layout --> form
    form -- "form post" --> action
    form --> sent
    action --> schema
    action --> limit
    action --> deliver
    deliver -- "1. primary" --> leads
    deliver -- "2. fallback" --> msg
    msg --> tg
Module Responsibility Source
Locale proxy Redirects any path without a locale prefix to /<locale>…. The NEXT_LOCALE cookie (set by the language switch) wins over Accept-Language, but only for a published locale. Static files, /api and /_next are skipped src/proxy.ts
Locale config The list ru, uz, en; default ru; localeFromAcceptLanguage: a CIS language (Uzbek included) gives ru, English gives en, anything else en, no header ru. The Uzbek page is never chosen automatically — only through the switch or a direct link. hreflang alternates and Open Graph locales src/i18n/config.ts
Dictionaries One TypeScript object per locale; the English one defines the shape the others must satisfy src/i18n/index.ts, src/i18n/dictionaries/en.ts
Site switches Values that are not words: credits, year, and locales.uzPublished (whether the Uzbek page is in the switch, hreflang and sitemap) src/content/site.ts
Root layout <html lang>, fonts, metadata, header and footer, inline gates that run before first paint (theme, reveal) src/app/[locale]/layout.tsx
Page Composes the sections in a fixed order: hero, showcase, sticker ribbon, how it works, why, organiser, data and consent, setup, FAQ, request form. Client sections receive only their slice of the dictionary src/app/[locale]/page.tsx
Request form Client component. Generates a requestId (UUID) when it opens, records startedAt, collects utm_* query parameters, contains the hidden honeypot field company. Loads the validator only on submit. After success it sets the 24-hour “sent” flag src/components/sections/RequestForm.tsx
Server action The only server entry point for the form. Order of checks: honeypot → minimum fill time (3000 ms) → schema → per-IP rate limit → delivery. A honeypot or too-fast post gets the same “success” answer and nothing is sent anywhere src/app/actions/submitRequest.ts
Request schema One schema for client and server, built with zod/mini. Returns error codes, never sentences. Normalises Telegram to @name and the phone to +digits src/lib/requestSchema.ts, src/lib/requestFields.ts
Rate limit Five submissions per IP per ten minutes, counted in memory of the serverless instance src/lib/rateLimit.ts
Delivery Chooses the route (see below), builds the POST /leads body, performs the fallback src/lib/deliverRequest.ts
Telegram message Plain-text message (always in Russian) with the username made a link through an explicit entity, so nothing a visitor typed can break markup src/lib/requestMessage.ts
“Request sent” flag localStorage key ai-sticker-pack.request-sent holding the send time. For 24 hours the visitor sees the “sent” screen instead of an empty form. A convenience, not a guard src/lib/requestSent.ts
Submit state The result shape shared by the action and the form: idle, success, invalid, failed, throttled src/lib/submitState.ts
SEO routes robots.txt and a sitemap with one entry per published locale src/app/robots.ts, src/app/sitemap.ts
Image settings AVIF then WebP, a short list of image widths, one-year cache next.config.ts

deliverRequest returns a result for the visitor and a route for the log line [request] <requestId> → <route>.

Situation Route What the visitor sees Is the request in the database?
Leads server answers 201 (stored) or 200 (same request_id already stored, or a repeat contact) server success Yes
Leads server answers 429 server-throttled throttled No — and no fallback, otherwise the fallback would be a way around the server’s limit
Leads server answers 422 server-rejected failed No — the page and the server disagree on the rules; this is a bug to fix, logged without field values
Timeout (5 s), network error, 5xx, 401 or 404, and the Telegram fallback succeeds fallback success No — the group gets a plain message whose first line is ⚠️ Запасной канал: сервер заявок не ответил — заявки нет в базе (request_id …) (“Backup channel: the leads server did not answer — the request is not in the database”). An admin handles it by hand
Same, and the fallback fails or no bot is configured lost failed No
LEADS_ENDPOINT / LEADS_SECRET not set, bot configured direct success, or failed if Telegram rejects No
Nothing configured (local development) console success No — the message is printed to the server console

Why a fallback at all: the form is the sales entry point, and a short outage of the leads server must not lose an organiser’s request. Why it is marked: a fallback message has no status buttons and no database row, so the group must be able to tell it from a normal card.

Built by leadsBody in deliverRequest.ts: request_id, name, telegram, phone, optional date, guests, comment, locale, optional utm, optional client_ip (first address of x-forwarded-for) and submitted_at. The header X-Stickers-Leads-Secret carries the shared secret. The server validates the same rules again with the same error codes (bot/services/leads.py).

Variable Scope Purpose
LEADS_ENDPOINT server only Full URL of the leads endpoint (https://stickers.eventtalks.uz/leads)
LEADS_SECRET server only Must equal LEADS_SECRET in the server’s bot/.env
TELEGRAM_BOT_TOKEN server only Bot used for the fallback and the direct route
TELEGRAM_CHAT_ID server only Group that receives fallback and direct messages
NEXT_PUBLIC_SITE_URL public Absolute site URL for canonical links, hreflang, sitemap and Open Graph images. The production value (https://pack.eventtalks.uz) is set only in Vercel

Source: .env.example.


Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.