Skip to content

Local setup — landing page

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, then npm run dev, then open http://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 /ru or /en. /uz is reached only through the language switch or a direct link.
  • Run npx playwright install chromium once before npm run shots, npm run audit, npm run film, npm run js-weight or npm run lighthouse.
  • On Windows a killed next build leaves worker processes that hold .next/lock, and a killed next start keeps port 3000. Find the root process and kill the whole tree with taskkill /PID <pid> /T /F.
  • Production runs on Vercel. See Deploy and rollback.

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 “Окружение”
  1. Windows 10 or 11 with PowerShell.
  2. Node 22 or newer on PATH. Check with node --version.
  3. Git.
  4. Network access for the one-time Playwright Chromium download (needed only for the quality scripts).
Terminal window
git clone https://github.com/<owner>/ai-stickers-landing.git
git -C ai-stickers-landing config user.name "<your-name>"
git -C ai-stickers-landing config user.email "<your-email>"
cd ai-stickers-landing
npm install
npm run dev

Open http://localhost:3000. The dev server uses port 3000 by default; every quality script assumes that address unless you set SHOTS_BASE.

Copy the example file. Do this even if you leave every value empty, so the file exists when you need it later.

Terminal window
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.

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”
  1. A submit faster than 3 seconds is silently dropped. The action treats it as a bot, returns success and sends nothing (MIN_FILL_MS = 3000 in src/app/actions/submitRequest.ts). Wait three seconds after the page loads.
  2. 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 key local. Restart the dev server to reset it.
  3. After one successful submit the form is replaced by the “sent” screen for 24 hours. The flag is the localStorage key ai-sticker-pack.request-sent. Remove it in DevTools (Application → Local Storage) to get the form back.

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:

Terminal window
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.

Run each command on its own, not as one chain. A hung command is then easy to spot.

Terminal window
npx tsc --noEmit
npm run lint
npm test
npm run build

npm 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.

npm run lighthouse and npm run js-weight need a production server, not npm run dev.

Terminal window
npm run build
npm run start

NEXT_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.

Terminal window
npx playwright install chromium

This 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
Terminal window
Get-NetTCPConnection -LocalPort 3000 -State Listen | Select-Object -ExpandProperty OwningProcess

Take the printed process id and kill its whole tree:

Terminal window
taskkill /PID <pid> /T /F

/T kills child processes too. Without it you are back where you started.

Terminal window
Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" |
Where-Object { $_.CommandLine -match 'next' } |
Select-Object ProcessId, ParentProcessId, CommandLine

Kill 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.

The full procedure, including rollback, is on Deploy and rollback. In short:

  1. The repository is imported into Vercel with the Next.js preset, default build command and Node.js 22.x.
  2. The five variables above are set in the Vercel project. LEADS_ENDPOINT and LEADS_SECRET are set for Production only; Preview deployments without them send straight to Telegram, or to the function log if no bot variables are set either.
  3. The public domain is pack.eventtalks.uz. It is configured in Vercel only; in the repository the address comes exclusively from NEXT_PUBLIC_SITE_URL.
  4. Changing NEXT_PUBLIC_SITE_URL needs a redeploy, because the value is compiled into canonical links, hreflang, the sitemap and Open Graph image URLs.
  5. Commits must be authored with the identity in the repository’s local git config. Never pass -c user.email or -c user.name to git: a commit with a different e-mail makes the Vercel deployment fail with “commit email could not be matched”.
  6. Rollback of the leads integration: remove LEADS_SECRET in Vercel and redeploy. The form then sends straight to Telegram again.

After a deploy, verify:

  • / redirects to /ru or /en;
  • /robots.txt and /sitemap.xml contain the real domain, not localhost;
  • /ru/opengraph-image returns the share image;
  • a test request reaches the requests group.

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