feat(landing): VK entry logo + restore the Telegram channel build-arg

Add the VK Mini App logo next to the Telegram one on the landing hero,
linked via the new VITE_VK_APP_LINK build-arg (full URL, wired through
compose, CI and prod-deploy from TEST_/PROD_VITE_VK_APP_LINK).

Also restore the landing's Telegram link itself: commit 57c778f
collapsed the per-language vars to VITE_TELEGRAM_GAME_CHANNEL_NAME in
compose/CI but left gateway/Dockerfile with the stale _EN/_RU ARGs and
without the plain one, so the built bundle saw the var as undefined and
dead-code-eliminated the whole channel-link branch — deployed landings
(prod included) have shown no Telegram logo since.
This commit is contained in:
Ilia Denisov
2026-07-02 00:10:02 +02:00
parent db17287113
commit 5f574a765d
14 changed files with 80 additions and 26 deletions
+1
View File
@@ -335,6 +335,7 @@ jobs:
VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }} VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }}
VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }} VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME }} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME }}
VITE_VK_APP_LINK: ${{ vars.TEST_VITE_VK_APP_LINK }}
VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }} VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }}
# Unset vars render empty -> the compose ":-" defaults apply. # Unset vars render empty -> the compose ":-" defaults apply.
POSTGRES_DB: ${{ vars.TEST_POSTGRES_DB }} POSTGRES_DB: ${{ vars.TEST_POSTGRES_DB }}
+1
View File
@@ -40,6 +40,7 @@ jobs:
VITE_TELEGRAM_BOT_ID: ${{ vars.PROD_VITE_TELEGRAM_BOT_ID }} VITE_TELEGRAM_BOT_ID: ${{ vars.PROD_VITE_TELEGRAM_BOT_ID }}
VITE_TELEGRAM_LINK: ${{ vars.PROD_VITE_TELEGRAM_LINK }} VITE_TELEGRAM_LINK: ${{ vars.PROD_VITE_TELEGRAM_LINK }}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.PROD_VITE_TELEGRAM_GAME_CHANNEL_NAME }} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.PROD_VITE_TELEGRAM_GAME_CHANNEL_NAME }}
VITE_VK_APP_LINK: ${{ vars.PROD_VITE_VK_APP_LINK }}
VITE_GATEWAY_URL: ${{ vars.PROD_VITE_GATEWAY_URL }} VITE_GATEWAY_URL: ${{ vars.PROD_VITE_GATEWAY_URL }}
POSTGRES_PASSWORD: ${{ secrets.PROD_POSTGRES_PASSWORD }} POSTGRES_PASSWORD: ${{ secrets.PROD_POSTGRES_PASSWORD }}
GM_BASICAUTH_HASH: ${{ secrets.PROD_GM_BASICAUTH_HASH }} GM_BASICAUTH_HASH: ${{ secrets.PROD_GM_BASICAUTH_HASH }}
+1
View File
@@ -32,6 +32,7 @@ GM_BASICAUTH_HASH= # required; `caddy hash-password` bcrypt
VITE_TELEGRAM_BOT_ID= VITE_TELEGRAM_BOT_ID=
VITE_TELEGRAM_LINK= # friend-invite Mini App link (full URL, e.g. https://t.me/<bot>/<app>) VITE_TELEGRAM_LINK= # friend-invite Mini App link (full URL, e.g. https://t.me/<bot>/<app>)
VITE_TELEGRAM_GAME_CHANNEL_NAME= # landing "Play in Telegram" link, the bot's game channel VITE_TELEGRAM_GAME_CHANNEL_NAME= # landing "Play in Telegram" link, the bot's game channel
VITE_VK_APP_LINK= # landing "Play on VK" link (full URL, https://vk.com/app<id>)
VITE_GATEWAY_URL= VITE_GATEWAY_URL=
# --- Grafana ---------------------------------------------------------------- # --- Grafana ----------------------------------------------------------------
+3 -2
View File
@@ -92,9 +92,10 @@ without it Docker's resolver handles `otelcol`, `gateway` and `api.telegram.org`
| `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. | | `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. |
| `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: friend-invite Mini App link (full URL, `https://t.me/<bot>/<app>``<app>` is the Mini App short name from BotFather). | | `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: friend-invite Mini App link (full URL, `https://t.me/<bot>/<app>``<app>` is the Mini App short name from BotFather). |
| `VITE_TELEGRAM_GAME_CHANNEL_NAME` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link, the bot's game channel (e.g. `https://t.me/Erudit_Game`). | | `VITE_TELEGRAM_GAME_CHANNEL_NAME` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link, the bot's game channel (e.g. `https://t.me/Erudit_Game`). |
| `VITE_VK_APP_LINK` | variable | _(empty)_ | UI build-arg: the landing "Play on VK" link, the VK Mini App (full URL, `https://vk.com/app<id>`). |
| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). | | `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). |
The five `VITE_*` are **build-args** baked into the gateway and landing images at The six `VITE_*` are **build-args** baked into the gateway and landing images at
build time (both targets share one UI build stage — keep the args identical so it is build time (both targets share one UI build stage — keep the args identical so it is
built once), so changing them requires a rebuild (`--build`), not just a restart. built once), so changing them requires a rebuild (`--build`), not just a restart.
@@ -198,7 +199,7 @@ BOTLINK_GATEWAY_CERT, BOTLINK_GATEWAY_KEY, BOTLINK_BOT_CERT, BOTLINK_BOT_KEY}`;
`PROD_{REGISTRY_USER, MAIN_HOST, TG_HOST, CADDY_SITE_ADDRESS, GM_BASICAUTH_USER, `PROD_{REGISTRY_USER, MAIN_HOST, TG_HOST, CADDY_SITE_ADDRESS, GM_BASICAUTH_USER,
GRAFANA_ROOT_URL, LOG_LEVEL, DICT_VERSION, TELEGRAM_MINIAPP_URL, TELEGRAM_GAME_CHANNEL_ID, GRAFANA_ROOT_URL, LOG_LEVEL, DICT_VERSION, TELEGRAM_MINIAPP_URL, TELEGRAM_GAME_CHANNEL_ID,
TELEGRAM_CHAT_ID, TELEGRAM_BOT_USERNAME, VITE_TELEGRAM_BOT_ID, VITE_TELEGRAM_LINK, TELEGRAM_CHAT_ID, TELEGRAM_BOT_USERNAME, VITE_TELEGRAM_BOT_ID, VITE_TELEGRAM_LINK,
VITE_TELEGRAM_GAME_CHANNEL_NAME}`. VITE_TELEGRAM_GAME_CHANNEL_NAME, VITE_VK_APP_LINK}`.
## Host-side setup (outside this repo) ## Host-side setup (outside this repo)
+2
View File
@@ -134,6 +134,7 @@ services:
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-} VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-} VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-}
VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev} VITE_APP_VERSION: ${APP_VERSION:-dev}
# Go binary version (the SPA's VITE_APP_VERSION is the same git tag). # Go binary version (the SPA's VITE_APP_VERSION is the same git tag).
@@ -207,6 +208,7 @@ services:
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-} VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-} VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-}
VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev} VITE_APP_VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -21,7 +21,7 @@ Settings also pick the board's bonus-label style (beginner / classic / none). A
costs nothing when the rack has no legal move. The word-check accepts only the costs nothing when the rack has no legal move. The word-check accepts only the
variant's alphabet, remembers answers within the session and rate-limits repeats. variant's alphabet, remembers answers within the session and rate-limits repeats.
A public **landing page** at the site root introduces the game, switches language and A public **landing page** at the site root introduces the game, switches language and
theme, and links to the matching per-language Telegram channel; the game itself runs at theme, and links to the Telegram game channel and the VK Mini App; the game itself runs at
`/app/` (web), `/telegram/` (the Telegram Mini App) and `/vk/` (the VK Mini App). The landing's theme is ephemeral `/app/` (web), `/telegram/` (the Telegram Mini App) and `/vk/` (the VK Mini App). The landing's theme is ephemeral
(it follows the system scheme, not the saved preference); its language choice is saved. (it follows the system scheme, not the saved preference); its language choice is saved.
+1 -1
View File
@@ -22,7 +22,7 @@ top-1 подсказку, безлимитную проверку слова с
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
и ограничивает частоту повторов. и ограничивает частоту повторов.
Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и
тему и ведёт в соответствующий по-язычный Telegram-канал; сама игра живёт по адресам тему и ведёт в Telegram-канал игры и в VK Mini App; сама игра живёт по адресам
`/app/` (веб), `/telegram/` (Telegram Mini App) и `/vk/` (VK Mini App). Тема на странице эфемерна (берётся из `/app/` (веб), `/telegram/` (Telegram Mini App) и `/vk/` (VK Mini App). Тема на странице эфемерна (берётся из
системной настройки, а не из сохранённой), выбор языка сохраняется. системной настройки, а не из сохранённой), выбор языка сохраняется.
+4 -8
View File
@@ -23,18 +23,14 @@ RUN corepack enable && corepack prepare pnpm@11.0.9 --activate
# VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev"). # VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev").
ARG VITE_TELEGRAM_BOT_ID= ARG VITE_TELEGRAM_BOT_ID=
ARG VITE_TELEGRAM_LINK= ARG VITE_TELEGRAM_LINK=
ARG VITE_TELEGRAM_LINK_EN= ARG VITE_TELEGRAM_GAME_CHANNEL_NAME=
ARG VITE_TELEGRAM_LINK_RU= ARG VITE_VK_APP_LINK=
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=
ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=
ARG VITE_GATEWAY_URL= ARG VITE_GATEWAY_URL=
ARG VITE_APP_VERSION= ARG VITE_APP_VERSION=
ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \ ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \
VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \ VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \
VITE_TELEGRAM_LINK_EN=$VITE_TELEGRAM_LINK_EN \ VITE_TELEGRAM_GAME_CHANNEL_NAME=$VITE_TELEGRAM_GAME_CHANNEL_NAME \
VITE_TELEGRAM_LINK_RU=$VITE_TELEGRAM_LINK_RU \ VITE_VK_APP_LINK=$VITE_VK_APP_LINK \
VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=$VITE_TELEGRAM_GAME_CHANNEL_NAME_EN \
VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=$VITE_TELEGRAM_GAME_CHANNEL_NAME_RU \
VITE_GATEWAY_URL=$VITE_GATEWAY_URL \ VITE_GATEWAY_URL=$VITE_GATEWAY_URL \
VITE_APP_VERSION=$VITE_APP_VERSION VITE_APP_VERSION=$VITE_APP_VERSION
+11
View File
@@ -0,0 +1,11 @@
<svg width="101" height="100" viewBox="0 0 101 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2_40)">
<path d="M0.5 48C0.5 25.3726 0.5 14.0589 7.52944 7.02944C14.5589 0 25.8726 0 48.5 0H52.5C75.1274 0 86.4411 0 93.4706 7.02944C100.5 14.0589 100.5 25.3726 100.5 48V52C100.5 74.6274 100.5 85.9411 93.4706 92.9706C86.4411 100 75.1274 100 52.5 100H48.5C25.8726 100 14.5589 100 7.52944 92.9706C0.5 85.9411 0.5 74.6274 0.5 52V48Z" fill="#0077FF"/>
<path d="M53.7085 72.042C30.9168 72.042 17.9169 56.417 17.3752 30.417H28.7919C29.1669 49.5003 37.5834 57.5836 44.25 59.2503V30.417H55.0004V46.8752C61.5837 46.1669 68.4995 38.667 70.8329 30.417H81.5832C79.7915 40.5837 72.2915 48.0836 66.9582 51.1669C72.2915 53.6669 80.8336 60.2086 84.0836 72.042H72.2499C69.7082 64.1253 63.3754 58.0003 55.0004 57.1669V72.042H53.7085Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2_40">
<rect width="100" height="100" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 996 B

+22 -9
View File
@@ -4,7 +4,7 @@
import { i18n, localeFrom, setLocale, t, type Locale } from './lib/i18n/index.svelte'; import { i18n, localeFrom, setLocale, t, type Locale } from './lib/i18n/index.svelte';
import { loadPrefs, savePrefs, type Prefs } from './lib/session'; import { loadPrefs, savePrefs, type Prefs } from './lib/session';
import { aboutContent } from './lib/aboutContent'; import { aboutContent } from './lib/aboutContent';
import { telegramChannelLink } from './lib/landing'; import { telegramChannelLink, vkAppLink } from './lib/landing';
// Standalone landing page, the public entry at "/" (the game SPA lives at /app/ and // Standalone landing page, the public entry at "/" (the game SPA lives at /app/ and
// /telegram/). It reuses the app's theme/i18n/prefs leaf modules — not the app store — so it // /telegram/). It reuses the app's theme/i18n/prefs leaf modules — not the app store — so it
@@ -17,6 +17,7 @@
const about = $derived(aboutContent(i18n.locale, 24)); // 24h = the auto-match move clock const about = $derived(aboutContent(i18n.locale, 24)); // 24h = the auto-match move clock
const tgLink = $derived(telegramChannelLink()); const tgLink = $derived(telegramChannelLink());
const vkLink = $derived(vkAppLink());
const locales: { code: Locale; label: string }[] = [ const locales: { code: Locale; label: string }[] = [
{ code: 'en', label: '🇬🇧 English' }, { code: 'en', label: '🇬🇧 English' },
{ code: 'ru', label: '🇷🇺 Русский' }, { code: 'ru', label: '🇷🇺 Русский' },
@@ -72,11 +73,20 @@
<section class="hero"> <section class="hero">
<h1>{about.title}</h1> <h1>{about.title}</h1>
<p class="tagline">{t('landing.tagline')}</p> <p class="tagline">{t('landing.tagline')}</p>
{#if tgLink || vkLink}
<div class="entries">
{#if tgLink} {#if tgLink}
<a class="tg" href={tgLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playTelegram')}> <a class="entry" href={tgLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playTelegram')}>
<img src="telegram-logo.svg" alt="" width="64" height="64" /> <img src="telegram-logo.svg" alt="" width="64" height="64" />
</a> </a>
{/if} {/if}
{#if vkLink}
<a class="entry" href={vkLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playVK')}>
<img src="vk-logo.svg" alt="" width="64" height="64" />
</a>
{/if}
</div>
{/if}
</section> </section>
<section class="info"> <section class="info">
@@ -190,19 +200,22 @@
color: var(--text-muted); color: var(--text-muted);
font-size: 1.05rem; font-size: 1.05rem;
} }
/* The Telegram entry is just the bigger logo (no button chrome, no caption); the link /* The platform entries are just the bigger logos in a row (no button chrome, no captions);
keeps an aria-label for assistive tech. */ each link keeps an aria-label for assistive tech. */
.tg { .entries {
align-self: center; align-self: center;
display: inline-flex; display: flex;
gap: 20px;
margin-top: 8px; margin-top: 8px;
border-radius: 50%;
} }
.tg img { .entry {
display: inline-flex;
}
.entry img {
display: block; display: block;
transition: transform 0.12s ease; transition: transform 0.12s ease;
} }
.tg:hover img { .entry:hover img {
transform: scale(1.06); transform: scale(1.06);
} }
.info { .info {
+1
View File
@@ -205,6 +205,7 @@ export const en = {
'landing.tagline': 'Play Scrabble with friends or a smart robot — in your browser or on Telegram.', 'landing.tagline': 'Play Scrabble with friends or a smart robot — in your browser or on Telegram.',
'landing.playTelegram': 'Play in Telegram', 'landing.playTelegram': 'Play in Telegram',
'landing.playVK': 'Play on VK',
'lang.en': 'English', 'lang.en': 'English',
'lang.ru': 'Русский', 'lang.ru': 'Русский',
+1
View File
@@ -205,6 +205,7 @@ export const ru: Record<MessageKey, string> = {
'landing.tagline': 'Играй в Скрэббл с друзьями или умным роботом — в браузере или в Telegram.', 'landing.tagline': 'Играй в Скрэббл с друзьями или умным роботом — в браузере или в Telegram.',
'landing.playTelegram': 'Играть в Telegram', 'landing.playTelegram': 'Играть в Telegram',
'landing.playVK': 'Играть во ВКонтакте',
'lang.en': 'English', 'lang.en': 'English',
'lang.ru': 'Русский', 'lang.ru': 'Русский',
+15 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { telegramChannelLink } from './landing'; import { telegramChannelLink, vkAppLink } from './landing';
describe('telegramChannelLink', () => { describe('telegramChannelLink', () => {
afterEach(() => vi.unstubAllEnvs()); afterEach(() => vi.unstubAllEnvs());
@@ -14,3 +14,17 @@ describe('telegramChannelLink', () => {
expect(telegramChannelLink()).toBeNull(); expect(telegramChannelLink()).toBeNull();
}); });
}); });
describe('vkAppLink', () => {
afterEach(() => vi.unstubAllEnvs());
it('returns the configured VK app link verbatim', () => {
vi.stubEnv('VITE_VK_APP_LINK', 'https://vk.com/app54655464');
expect(vkAppLink()).toBe('https://vk.com/app54655464');
});
it('returns null when the link is unset or blank', () => {
vi.stubEnv('VITE_VK_APP_LINK', ' ');
expect(vkAppLink()).toBeNull();
});
});
+13 -1
View File
@@ -1,5 +1,5 @@
// Pure helpers for the public landing page, kept out of the Svelte component so the // Pure helpers for the public landing page, kept out of the Svelte component so the
// Telegram-channel link selection is unit-testable. // platform-entry link selection is unit-testable.
/** /**
* telegramChannelLink returns the t.me link for the single bot's game channel, or null * telegramChannelLink returns the t.me link for the single bot's game channel, or null
@@ -13,3 +13,15 @@ export function telegramChannelLink(): string | null {
const name = (raw as string | undefined)?.trim().replace(/^@/, ''); const name = (raw as string | undefined)?.trim().replace(/^@/, '');
return name ? `https://t.me/${name}` : null; return name ? `https://t.me/${name}` : null;
} }
/**
* vkAppLink returns the VK Mini App link for the landing "Play on VK" entry, or null when it is
* not configured. The link is a build-time var (VITE_VK_APP_LINK, the full URL
* `https://vk.com/app<id>`) because the test and prod contours run the same VK app but the var
* mirrors the per-contour Telegram entries.
*/
export function vkAppLink(): string | null {
const raw = import.meta.env.VITE_VK_APP_LINK;
const link = (raw as string | undefined)?.trim();
return link ? link : null;
}