Local setup — landing page
Who is this for
Section titled “Who is this for”A developer who has just cloned the landing repository (ai-stickers-landing) and wants it running locally on Windows, with a working request form and the screenshot tooling.
- Node 22 or newer, npm, no Docker.
npm install, thennpm run dev, then openhttp://localhost:3000. - No environment variables are needed. With none set, a submitted request is printed to the server console as
[request]and the visitor still sees the “request sent” screen. /always redirects to/ruor/en./uzis reached only through the language switch or a direct link.- Run
npx playwright install chromiumonce beforenpm run shots,npm run audit,npm run film,npm run js-weightornpm run lighthouse. - On Windows a killed
next buildleaves worker processes that hold.next/lock, and a killednext startkeeps port 3000. Find the root process and kill the whole tree withtaskkill /PID <pid> /T /F. - Production runs on Vercel. See Deploy and rollback.
What you are setting up
Section titled “What you are setting up”The landing page is a separate Next.js 16 (App Router) site: TypeScript in strict mode, Tailwind CSS v4, deployed on Vercel. It shares no code, database or keys with the product repository. Every product screen on the page is a mock-up drawn in code.
| Fact | Value | Source |
|---|---|---|
| Runtime | Node >=22 |
engines in package.json |
| Framework | next 16.2.12, react 19.2.4 |
package.json |
| Runtime dependencies | next, react, react-dom, zod — nothing else |
package.json |
| Dev tooling | playwright, sharp, tsx, eslint, tailwindcss |
package.json |
| Containers | None. The repository rules forbid Docker for this project | CLAUDE.md, section “Окружение” |
Prerequisites
Section titled “Prerequisites”- Windows 10 or 11 with PowerShell.
- Node 22 or newer on
PATH. Check withnode --version. - Git.
- Network access for the one-time Playwright Chromium download (needed only for the quality scripts).
Install and run
Section titled “Install and run”git clone https://github.com/<owner>/ai-stickers-landing.gitgit -C ai-stickers-landing config user.name "<your-name>"git -C ai-stickers-landing config user.email "<your-email>"cd ai-stickers-landingnpm installnpm run devOpen http://localhost:3000. The dev server uses port 3000 by default; every quality script assumes that address unless you set SHOTS_BASE.
Environment variables
Section titled “Environment variables”Copy the example file. Do this even if you leave every value empty, so the file exists when you need it later.
Copy-Item .env.example .env.local.env* is git-ignored except .env.example. Never commit .env.local.
| Variable | Scope | Purpose | Local value |
|---|---|---|---|
LEADS_ENDPOINT |
server only | URL of the leads server, https://stickers.eventtalks.uz/leads in production |
leave empty |
LEADS_SECRET |
server only | Shared secret sent in the X-Stickers-Leads-Secret header |
leave empty |
TELEGRAM_BOT_TOKEN |
server only | Bot that posts a request straight into the requests group: the fallback channel, and the only route when LEADS_* is empty |
leave empty |
TELEGRAM_CHAT_ID |
server only | The requests group, <LEADS_CHAT_ID> |
leave empty |
NEXT_PUBLIC_SITE_URL |
server and client | Absolute site URL for canonical links, hreflang, sitemap and Open Graph images | http://localhost:3000 |
None of the first four may ever get a NEXT_PUBLIC_ prefix. A test in src/lib/deliverRequest.test.ts fails if a client file reads them.
What the form does with each combination
Section titled “What the form does with each combination”The routing lives in src/lib/deliverRequest.ts. The server action logs the chosen route as [request] <request_id> → <route>.
LEADS_* |
TELEGRAM_* |
Route name | What happens | Visitor sees |
|---|---|---|---|---|
| empty | empty | console |
The request text is printed with console.info, first line [request] |
Success |
| empty | set | direct |
sendMessage straight to the group |
Success, or an error if Telegram refuses |
| set | set | server |
POST to the leads server, 5 s timeout |
Success on 201 or 200 |
| set | set | fallback |
Leads server timed out, was unreachable, or answered 5xx, 401 or 404; the request goes to the group with a first line starting ⚠️ Запасной канал (“fallback channel”) |
Success |
| set | set | server-throttled |
Leads server answered 429; no fallback |
“Too many requests” |
| set | set | server-rejected |
Leads server answered 422; logged, no fallback |
Error, form stays |
| set | empty | lost |
Leads server failed and there is no bot to fall back to | Error, form stays |
For local work, the first row is what you want: nothing leaves your machine.
Three things that surprise people when testing the form locally
Section titled “Three things that surprise people when testing the form locally”- A submit faster than 3 seconds is silently dropped. The action treats it as a bot, returns success and sends nothing (
MIN_FILL_MS = 3000insrc/app/actions/submitRequest.ts). Wait three seconds after the page loads. - After five submits in ten minutes you get the throttled message. The limit is per IP and kept in memory (
src/lib/rateLimit.ts). Locally every request counts under the keylocal. Restart the dev server to reset it. - After one successful submit the form is replaced by the “sent” screen for 24 hours. The flag is the
localStoragekeyai-sticker-pack.request-sent. Remove it in DevTools (Application → Local Storage) to get the form back.
Locale redirect behaviour
Section titled “Locale redirect behaviour”Every page lives under /ru, /uz or /en. There is no unprefixed page. src/proxy.ts redirects any other path.
Request to / |
Lands on |
|---|---|
Cookie NEXT_LOCALE holds a published locale |
that locale, including /uz |
Accept-Language names a CIS language first (ru, uz, uk, be, kk, ky, tg, tk, az, hy) |
/ru |
Accept-Language names English first |
/en |
Accept-Language names only other languages |
/en |
No Accept-Language, an empty one, or * |
/ru |
A browser set to Uzbek gets /ru, never /uz. Quality values are honoured, so en;q=0.3, kk;q=0.9 resolves to /ru.
Check it without a browser:
curl.exe -s -o NUL -w "%{redirect_url}`n" -H "Accept-Language: de-DE" http://localhost:3000/curl.exe -s -o NUL -w "%{redirect_url}`n" -H "Accept-Language: uz-UZ" http://localhost:3000/curl.exe -s -o NUL -w "%{redirect_url}`n" -H "Cookie: NEXT_LOCALE=uz" http://localhost:3000/Expected: /en, /ru, /uz. The full rules are on i18n and copy.
Checks before a commit
Section titled “Checks before a commit”Run each command on its own, not as one chain. A hung command is then easy to spot.
npx tsc --noEmitnpm run lintnpm testnpm run buildnpm test runs six test files with Node’s built-in runner through tsx, each test limited to 60 seconds (--test-timeout=60000): dictionaries, showcase packs, the form schema, request delivery, the “sent” flag and the hero scene steps.
Production build locally
Section titled “Production build locally”npm run lighthouse and npm run js-weight need a production server, not npm run dev.
npm run buildnpm run startNEXT_PUBLIC_SITE_URL is baked into the build. If you build with a different value than the address you serve from, canonical and hreflang links point elsewhere and Lighthouse reports it under SEO.
One-time Playwright browser install
Section titled “One-time Playwright browser install”npx playwright install chromiumThis downloads the Chromium build that all quality scripts drive. npm run lighthouse also uses that same Chromium and fetches lighthouse@12 through npx on first use, so that first run needs network access. The scripts are described on Landing quality tooling.
Windows pitfall: orphaned build workers and port 3000
Section titled “Windows pitfall: orphaned build workers and port 3000”On Windows, stopping a command with a timeout or with a task runner’s “stop” kills only the top process. Its children survive. The repository rules record this in CLAUDE.md, section “Проверка вёрстки”.
| Symptom | Cause |
|---|---|
Another next build process is already running |
Worker processes of a killed next build still hold .next/lock |
| Port 3000 is already taken when you start the server again | A previous next start is still listening |
| A quality script hangs on its first navigation | SHOTS_BASE points at a port where nothing, or an old build, answers |
Find and kill the owner of port 3000
Section titled “Find and kill the owner of port 3000”Get-NetTCPConnection -LocalPort 3000 -State Listen | Select-Object -ExpandProperty OwningProcessTake the printed process id and kill its whole tree:
taskkill /PID <pid> /T /F/T kills child processes too. Without it you are back where you started.
Find leftover next build workers
Section titled “Find leftover next build workers”Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" | Where-Object { $_.CommandLine -match 'next' } | Select-Object ProcessId, ParentProcessId, CommandLineKill the root one (the process whose parent is not another node.exe from the list) with the same taskkill /PID <pid> /T /F. Then build again. Do not delete .next/lock while the workers are alive.
The rule in the repository is to give every long command its own time limit and to treat a hang as a failure within a minute. The watchdog that enforces this in the scripts is described on Landing quality tooling.
Deploying: summary
Section titled “Deploying: summary”The full procedure, including rollback, is on Deploy and rollback. In short:
- The repository is imported into Vercel with the Next.js preset, default build command and Node.js 22.x.
- The five variables above are set in the Vercel project.
LEADS_ENDPOINTandLEADS_SECRETare set for Production only; Preview deployments without them send straight to Telegram, or to the function log if no bot variables are set either. - The public domain is
pack.eventtalks.uz. It is configured in Vercel only; in the repository the address comes exclusively fromNEXT_PUBLIC_SITE_URL. - Changing
NEXT_PUBLIC_SITE_URLneeds a redeploy, because the value is compiled into canonical links, hreflang, the sitemap and Open Graph image URLs. - Commits must be authored with the identity in the repository’s local git config. Never pass
-c user.emailor-c user.nameto git: a commit with a different e-mail makes the Vercel deployment fail with “commit email could not be matched”. - Rollback of the leads integration: remove
LEADS_SECRETin Vercel and redeploy. The form then sends straight to Telegram again.
After a deploy, verify:
/redirects to/ruor/en;/robots.txtand/sitemap.xmlcontain the real domain, notlocalhost;/ru/opengraph-imagereturns the share image;- a test request reaches the requests group.
Where to go next
Section titled “Where to go next”- Landing quality tooling — what
audit,shots,film,lighthouse,js-weightandcontrastcheck and how to read their output. - Environment variables — the complete list for both repositories.
- i18n and copy — changing texts and locale routing in detail.
- Deploy and rollback — the production procedure.
- Lead submission flow — what happens to a request after the form.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.