diff --git a/apps/landing/src/components/Label.tsx b/apps/landing/src/components/Label.tsx
index e4ad3d6..67fcc1f 100644
--- a/apps/landing/src/components/Label.tsx
+++ b/apps/landing/src/components/Label.tsx
@@ -52,7 +52,7 @@ export function Label({ item, roastDate, wide = false }: { item: Item; roastDate
{item.notes.length > 0 && (
- {item.notes.map(keep).join(' •\u00A0')}
+ {item.notes.map(keep).join('\u00A0• ')}
)}
{item.spec && (
diff --git a/apps/landing/src/components/WeekStrip.tsx b/apps/landing/src/components/WeekStrip.tsx
index 0b47bf1..39de7c9 100644
--- a/apps/landing/src/components/WeekStrip.tsx
+++ b/apps/landing/src/components/WeekStrip.tsx
@@ -38,7 +38,7 @@ export function WeekStrip({ week }: { week: RoastWeek }) {
- {cupNote.day} · {cupNote.label}
+ {cupNote.day}{'\u00A0·\u00A0'}{cupNote.label}
{typo(cupNote.caption)}
diff --git a/apps/landing/src/lib/typo.test.ts b/apps/landing/src/lib/typo.test.ts
index 6cb773a..04bc209 100644
--- a/apps/landing/src/lib/typo.test.ts
+++ b/apps/landing/src/lib/typo.test.ts
@@ -17,7 +17,8 @@ describe('typo', () => {
expect(typo('Иргачеффе вкусный')).toBe('Иргачеффе вкусный');
});
it('неразрывный дефис в фамилии', () => {
- expect(typo('Scott Soltys-Curry')).toBe('Scott Soltys‑Curry');
+ expect(typo('Scott Soltys-Curry')).toBe('Scott\u00A0Soltys\u2011Curry');
+ expect(typo('Фото: Mona Jain, Tina Guina')).toBe('Фото: Mona\u00A0Jain, Tina\u00A0Guina');
});
it('слеш не уходит на новую строку', () => {
expect(typo('Curry / Unsplash')).toBe(`Curry${N}/ Unsplash`);
diff --git a/apps/landing/src/lib/typo.ts b/apps/landing/src/lib/typo.ts
index 81c099f..7bcbdee 100644
--- a/apps/landing/src/lib/typo.ts
+++ b/apps/landing/src/lib/typo.ts
@@ -5,6 +5,8 @@ const SHORT = 'а|в|во|и|к|ко|на|не|но|о|об|от|до|по|с|с
const SHORT_RE = new RegExp(`(^|[\\s(«—–-])(${SHORT}) `, 'giu');
const UNIT_RE = /(\d) (кг|г|шт|BYN|мин|дн\.?|день|дня)(?=$|[\s.,:;!?)»])/gu;
const NAME_HYPHEN_RE = /([A-Z][a-z]+)-([A-Z][a-z]+)/g;
+// имя и фамилия латиницей не разрываются: «Scott Soltys-Curry»
+const NAME_PAIR_RE = /\b([A-Z][a-z]+) (?=[A-Z][a-z]+)/g;
export function typo(text: string): string {
// дважды: «и в Минске» — после замены первого слова второе стоит уже после NBSP
@@ -13,6 +15,7 @@ export function typo(text: string): string {
return out
.replace(UNIT_RE, `$1${NBSP}$2`)
.replace(NAME_HYPHEN_RE, '$1\u2011$2')
+ .replace(NAME_PAIR_RE, `$1${NBSP}`)
.replace(/ \//g, `${NBSP}/`);
}
diff --git a/deploy/Dockerfile.landing b/deploy/Dockerfile.landing
index 699dc03..363d68b 100644
--- a/deploy/Dockerfile.landing
+++ b/deploy/Dockerfile.landing
@@ -23,7 +23,8 @@ RUN pnpm --filter @kvarta/landing build \
&& mkdir -p apps/landing/public \
&& rm -rf apps/landing/.next/standalone/node_modules/.pnpm/@img+sharp-*
-# sharp для next/image под целевую платформу; npm — чистый JS, под эмуляцией работает
+# sharp для next/image под целевую платформу; npm — чистый JS, под эмуляцией работает.
+# Next 16 грузит его обычным require("sharp") (NEXT_SHARP_PATH больше не читает) — находит через NODE_PATH.
FROM node:22-alpine AS sharp
WORKDIR /sharp
RUN npm install --omit=dev --no-audit --no-fund sharp@0.35.5 >/dev/null
@@ -43,7 +44,7 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0 \
- NEXT_SHARP_PATH=/sharp/node_modules/sharp
+ NODE_PATH=/sharp/node_modules
WORKDIR /app
# outputFileTracingRoot = корень репо, поэтому server.js лежит в apps/landing/ внутри standalone
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/.next/standalone ./
diff --git a/docs/ai-log.md b/docs/ai-log.md
index 242b4aa..fbb651e 100644
--- a/docs/ai-log.md
+++ b/docs/ai-log.md
@@ -26,3 +26,4 @@
| 20 | Оркестрация | Оркестратор закоммитил ретранслятор через `git add -A` в том же чекауте, где дизайнер вёл незакоммиченную вёрстку шага 12 — почти весь шаг 12 уехал в коммит `20db25b` «ретранслятор Bot API», а на прод выкатилась половина вёрстки | Отчёт дизайнера | История переписана с согласия автора: `20db25b` разделён на «ретранслятор» и «секции», `push --force-with-lease`; впредь — `git add` по путям |
| 21 | Шаги 12–13, дизайн | Самопроверка дизайнера по своим же скриншотам пропустила: полупрозрачную подложку штампа (просвечивало фото), фильтр «краски», съевший половину даты, ряд hero-метрик, запрещённый его же DESIGN.md, и повтор «ЧТ» лентой и штампом на каждой этикетке | Независимый критик по скриншотам, без доступа к коду | Итерация критика 1 |
| 22 | Шаг 12, дизайн | Секция доставки свёрстана во всю ширину без привязки к контейнеру — «Самовывоз» на 1440 вылез за правый край на 24px | Критик по скриншоту 1440 | Колонки 7–12 контейнера 1280 |
+| 23 | Шаг 5, образы | Оркестратор подключил sharp через `NEXT_SHARP_PATH` по памяти о старых версиях Next; Next 16 эту переменную не читает и при отсутствии sharp молча отдаёт оригинал — `w=750` весил 838 КБ JPEG | Lighthouse mobile на проде: «image delivery, savings 777 KiB», затем `curl` по `/_next/image` | `NODE_PATH=/sharp/node_modules`; проверено: тот же запрос — 151 КБ webp |