rebrand: кэш-бастинг логотипа (старое лого залипало в SW/кэше)
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions

Причина: SW веб-админки прекэшировал /logo.svg и отдавал stale, CACHE_VERSION не
менялся при ребрендинге; публичный сайт отдавал лого/favicon без Cache-Control.

- sw.js: CACHE_VERSION fm-v2 → am-v3 (+ASSET_VERSION am3); activate чистит старые
  кэши, skipWaiting+clients.claim; прекэш бренд-ассетов версионирован; бренд в
  комментариях → ALL MARKET.
- ?v=am3 на нехэшированных бренд-ассетах: index.html, manifest, Logo.tsx,
  публичный Logo.astro + BaseLayout favicon.
- nginx web: index.html + бренд-ассеты + manifest → no-cache; /assets/* → immutable.
- nginx public: html + лого/favicon → no-cache; /_astro/* → immutable.

Проверено Playwright: старый клиент (SW fm-v2 + лого Food Market) → после выкладки
+ 1 перезагрузки кэш am-v3, fm-v2 удалён, лого ALL MARKET. Заметка на будущее про
очистку API_CACHE при logout — в прогресс-доке.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 19:47:41 +05:00
parent e5bbd4a047
commit 466e546ae2
9 changed files with 80 additions and 17 deletions

View file

@ -134,15 +134,31 @@ server {
} }
location = /manifest.webmanifest { location = /manifest.webmanifest {
types { } default_type application/manifest+json; types { } default_type application/manifest+json;
add_header Cache-Control "public, max-age=3600"; # no-cache: при ребрендинге manifest (name/иконки) должен обновляться сразу.
add_header Cache-Control "no-cache, must-revalidate";
try_files /manifest.webmanifest =404; try_files /manifest.webmanifest =404;
} }
location = /offline.html { location = /offline.html {
try_files /offline.html =404; try_files /offline.html =404;
} }
# SPA fallback — all other routes return index.html # Хэшированные ассеты Vite (имя содержит контент-хэш) — иммутабельно на год.
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# Нехэшированные бренд-ассеты (лого/иконки/favicon) — no-cache + ETag, чтобы
# смена логотипа не упиралась в кэш браузера (ETag даёт 304, если не менялось).
location ~* ^/(logo|logo-light|logo-bg|logo-fg|favicon|favicon-32|apple-touch-icon|icon-192|icon-512)\.(svg|png)$ {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
# index.html (SPA-оболочка) — no-cache: всегда свежая, подтягивает новые
# хэшированные бандлы и версии бренд-ассетов. SPA fallback для всех маршрутов.
location / { location / {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
} }

View file

@ -88,3 +88,18 @@ API и web (0 ошибок); лого вернулось на Food Market (dist/
- 08.10 — ревью Mac-Claude: безопасность входа (префилл/подсказка/сидер/ротация пароля стенда), - 08.10 — ревью Mac-Claude: безопасность входа (префилл/подсказка/сидер/ротация пароля стенда),
тост 404 (StageBanner/QuickLoginBlock → fetch), касса (лого крупнее + разделитель, бейдж внутри). тост 404 (StageBanner/QuickLoginBlock → fetch), касса (лого крупнее + разделитель, бейдж внутри).
- 08.10 — deploy-stage OK; menu-walk 3/3; full-chain 16/16 ЗЕЛЁНЫЙ; откат проверен в worktree. - 08.10 — deploy-stage OK; menu-walk 3/3; full-chain 16/16 ЗЕЛЁНЫЙ; откат проверен в worktree.
- 08.10 — **кэш-бастинг лого** (у владельца в браузере оставалось старое лого из SW/кэша):
- `sw.js`: `CACHE_VERSION fm-v2 → am-v3` + `ASSET_VERSION=am3`; activate удаляет старые кэши,
skipWaiting+clients.claim есть; прекэш бренд-ассетов версионирован; бренд в комментариях → ALL MARKET.
- `?v=am3` на нехэшированных бренд-ассетах во всех ссылках: `index.html`, `manifest.webmanifest`,
`Logo.tsx`, публичный `Logo.astro` + `BaseLayout.astro` favicon.
- nginx web (`deploy/nginx.conf`): `index.html` + бренд-ассеты + `manifest` → `no-cache`; `/assets/*` → immutable.
- nginx public (`src/food-market.public/deploy/nginx.conf`): html + лого/favicon → `no-cache`; `/_astro/*` → immutable.
- Проверка Playwright: старый клиент (SW fm-v2 + лого Food Market) → после выкладки + 1 перезагрузки
кэш `am-v3`, `fm-v2` удалён, лого ALL MARKET. PASS.
- При будущей смене логотипа: bump `CACHE_VERSION` (sw.js) + `?v=` в ссылках (index.html, manifest, Logo.tsx/.astro).
### Заметка на будущее (НЕ в этой задаче)
SW кэширует ответы `GET /api/*` (`API_CACHE`). На ОБЩЕМ компьютере после выхода из аккаунта данные
прошлого пользователя могут остаться в кэше. Предложение: при logout чистить `API_CACHE`
(`caches.delete('am-v3-api')` или через `postMessage` к SW) — отдельной задачей.

View file

@ -23,18 +23,34 @@ server {
gzip_types text/css application/javascript application/json image/svg+xml text/plain; gzip_types text/css application/javascript application/json image/svg+xml text/plain;
gzip_min_length 1000; gzip_min_length 1000;
# Хэшированные ассеты Astro (/_astro/*) — иммутабельно на год.
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Легаси-путь на случай, если что-то лежит в /assets/ с хэшем.
location ~* ^/assets/.*\.(js|css|woff2?|svg|png|jpg|webp)$ { location ~* ^/assets/.*\.(js|css|woff2?|svg|png|jpg|webp)$ {
expires 1y; expires 1y;
add_header Cache-Control "public, immutable"; add_header Cache-Control "public, immutable";
try_files $uri =404; try_files $uri =404;
} }
# Нехэшированные бренд-ассеты (лого/иконки/favicon) — no-cache + ETag, чтобы
# смена логотипа не упиралась в эвристический кэш браузера по Last-Modified.
location ~* ^/(logo|logo-light|favicon)\.(svg|png)$ {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
# Старый URL — постоянный редирект на новую страницу импорта. # Старый URL — постоянный редирект на новую страницу импорта.
location = /migration-from-other-system { return 301 /import/; } location = /migration-from-other-system { return 301 /import/; }
location = /migration-from-other-system/ { return 301 /import/; } location = /migration-from-other-system/ { return 301 /import/; }
# Pretty URLs: /pricing → /pricing/index.html или /pricing.html # Pretty URLs: /pricing → /pricing/index.html или /pricing.html.
# HTML — no-cache: страницы со ссылками на лого/ассеты должны быть свежими.
location / { location / {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri $uri/ $uri.html =404; try_files $uri $uri/ $uri.html =404;
} }
} }

View file

@ -1,6 +1,7 @@
--- ---
interface Props { variant?: 'light' | 'dark'; class?: string } interface Props { variant?: 'light' | 'dark'; class?: string }
const { variant = 'light', class: className = '' } = Astro.props const { variant = 'light', class: className = '' } = Astro.props
const src = variant === 'dark' ? '/logo-light.svg' : '/logo.svg' // ?v=am3 — версия ассета для обхода кэша при ребрендинге.
const src = variant === 'dark' ? '/logo-light.svg?v=am3' : '/logo.svg?v=am3'
--- ---
<img src={src} alt="ALL MARKET" class={`h-8 w-auto select-none ${className}`} draggable="false" /> <img src={src} alt="ALL MARKET" class={`h-8 w-auto select-none ${className}`} draggable="false" />

View file

@ -27,7 +27,7 @@ const ymId = import.meta.env.PUBLIC_YM_ID ?? 'REPLACE_ME'
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=am3" />
<title>{title} · ALL MARKET</title> <title>{title} · ALL MARKET</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="canonical" href={canonical} /> <link rel="canonical" href={canonical} />

View file

@ -2,17 +2,19 @@
<html lang="ru-KZ"> <html lang="ru-KZ">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <!-- ?v=am3 — версия бренд-ассетов для обхода кэша (см. sw.js ASSET_VERSION).
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" /> При смене логотипа bump ?v здесь, в manifest, в Logo.tsx и CACHE_VERSION в sw.js. -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=am3" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png?v=am3" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>ALL MARKET</title> <title>ALL MARKET</title>
<meta name="theme-color" content="#00B207" /> <meta name="theme-color" content="#00B207" />
<!-- PWA: manifest + apple-touch-meta. SW регистрируется в main.tsx. --> <!-- PWA: manifest + apple-touch-meta. SW регистрируется в main.tsx. -->
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest?v=am3" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="ALL MARKET" /> <meta name="apple-mobile-web-app-title" content="ALL MARKET" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png?v=am3" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -11,9 +11,9 @@
"theme_color": "#00B207", "theme_color": "#00B207",
"lang": "ru-KZ", "lang": "ru-KZ",
"icons": [ "icons": [
{ "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, { "src": "/favicon.svg?v=am3", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { "src": "/icon-192.png?v=am3", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } { "src": "/icon-512.png?v=am3", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
], ],
"shortcuts": [ "shortcuts": [
{ "name": "Дашборд", "url": "/dashboard" }, { "name": "Дашборд", "url": "/dashboard" },

View file

@ -1,4 +1,4 @@
/* Food Market — service worker (read-only owner PWA). /* ALL MARKET — service worker (read-only owner PWA).
* *
* Стратегии: * Стратегии:
* - Навигация (mode=navigate): network-first с offline-fallback на /offline.html. * - Навигация (mode=navigate): network-first с offline-fallback на /offline.html.
@ -9,8 +9,13 @@
* (через api interceptor). * (через api interceptor).
* *
* Версия кэша инкрементируется при изменении SW — старые удаляются на activate. * Версия кэша инкрементируется при изменении SW — старые удаляются на activate.
* ВАЖНО при смене логотипа/иконок: bump CACHE_VERSION И ASSET_VERSION (ниже),
* и ?v=<ASSET_VERSION> в ссылках (index.html, manifest, Logo.tsx). Иначе у тех,
* кто уже открывал админку, останется старое лого из прекэша SW.
* 2026-10: ребрендинг Food Market → ALL MARKET — fm-v2 → am-v3.
*/ */
const CACHE_VERSION = 'fm-v2'; const CACHE_VERSION = 'am-v3';
const ASSET_VERSION = 'am3'; // ?v=am3 на нехэшированных бренд-ассетах
const STATIC_CACHE = `${CACHE_VERSION}-static`; const STATIC_CACHE = `${CACHE_VERSION}-static`;
const API_CACHE = `${CACHE_VERSION}-api`; const API_CACHE = `${CACHE_VERSION}-api`;
const OFFLINE_URL = '/offline.html'; const OFFLINE_URL = '/offline.html';
@ -18,8 +23,14 @@ const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
event.waitUntil( event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => caches.open(STATIC_CACHE).then((cache) =>
// Прекеш offline-page чтобы fallback работал даже после reboot'a. // Прекеш offline-page + бренд-ассеты (версионированные — совпадают со
cache.addAll(['/offline.html', '/favicon.svg', '/logo.svg', '/manifest.webmanifest']) // ссылками на странице), чтобы fallback и лого работали даже офлайн.
cache.addAll([
'/offline.html',
`/favicon.svg?v=${ASSET_VERSION}`,
`/logo.svg?v=${ASSET_VERSION}`,
`/manifest.webmanifest?v=${ASSET_VERSION}`,
])
).then(() => self.skipWaiting()) ).then(() => self.skipWaiting())
); );
}); });

View file

@ -9,8 +9,10 @@ interface Props { className?: string; variant?: 'light' | 'dark' }
* *
* Готовые файлы бренда из public/ (logo.svg — color, logo-light.svg — white). * Готовые файлы бренда из public/ (logo.svg — color, logo-light.svg — white).
* Не перерисовывать и не перекрашивать — только менять размер. */ * Не перерисовывать и не перекрашивать — только менять размер. */
// ?v=am3 — версия ассета для обхода кэша при ребрендинге (см. sw.js ASSET_VERSION).
const V = 'am3'
export function Logo({ className, variant = 'light' }: Props) { export function Logo({ className, variant = 'light' }: Props) {
const src = variant === 'dark' ? '/logo-light.svg' : '/logo.svg' const src = variant === 'dark' ? `/logo-light.svg?v=${V}` : `/logo.svg?v=${V}`
return ( return (
<img <img
src={src} src={src}