Component: landing
Who is this for
Section titled “Who is this for”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,/uzor/en.src/proxy.tsredirects any other path to a locale chosen from a cookie or theAccept-Languageheader. - The form posts to one server action,
submitRequest. It filters bots (honeypot, minimum fill time), validates with a sharedzodschema, rate-limits per IP in memory and then callsdeliverRequest. deliverRequesttries the leads server first (POST /leads, 5 s timeout). On a timeout, network error, 5xx, 401 or 404 it falls back to a direct TelegramsendMessagemarked⚠️ Запасной канал(“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_.
Component diagram
Section titled “Component diagram”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
Modules
Section titled “Modules”| 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 |
Delivery routes
Section titled “Delivery routes”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.
The POST /leads body
Section titled “The POST /leads body”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).
Environment
Section titled “Environment”| 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.
Where to go next
Section titled “Where to go next”- Flow: lead submission — the full path from the form to the card in the group.
- Component: sticker-api — the receiving side of
POST /leads. - Local setup: landing — running the site on your machine.
- Runbook: leads missing or fallback — what to do when a backup-channel message appears.
- Internationalisation — locale rules in detail.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.