fix(landing): sharp в образе через NODE_PATH; итерация критика 3 — переносы
Next 16 грузит sharp обычным require и NEXT_SHARP_PATH не читает: без sharp /_next/image отдавал исходный JPEG. Критик 3 (9/10): точка-разделитель нот и «·» в скобке недели не висят в начале/конце строки, имя и фамилия латиницей не разрываются.
This commit is contained in:
@@ -52,7 +52,7 @@ export function Label({ item, roastDate, wide = false }: { item: Item; roastDate
|
|||||||
<div className={wide ? 'lg:px-8' : 'flex flex-col'}>
|
<div className={wide ? 'lg:px-8' : 'flex flex-col'}>
|
||||||
{item.notes.length > 0 && (
|
{item.notes.length > 0 && (
|
||||||
<Row label={label.notes} wide={wide}>
|
<Row label={label.notes} wide={wide}>
|
||||||
<span className="tag text-ink">{item.notes.map(keep).join(' •\u00A0')}</span>
|
<span className="tag text-ink">{item.notes.map(keep).join('\u00A0• ')}</span>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{item.spec && (
|
{item.spec && (
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function WeekStrip({ week }: { week: RoastWeek }) {
|
|||||||
<span className="week-bracket-line" aria-hidden="true" />
|
<span className="week-bracket-line" aria-hidden="true" />
|
||||||
<div className="week-bracket-note">
|
<div className="week-bracket-note">
|
||||||
<p className="tag text-cherry">
|
<p className="tag text-cherry">
|
||||||
{cupNote.day} · {cupNote.label}
|
{cupNote.day}{'\u00A0·\u00A0'}{cupNote.label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-small text-ink">{typo(cupNote.caption)}</p>
|
<p className="text-small text-ink">{typo(cupNote.caption)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ describe('typo', () => {
|
|||||||
expect(typo('Иргачеффе вкусный')).toBe('Иргачеффе вкусный');
|
expect(typo('Иргачеффе вкусный')).toBe('Иргачеффе вкусный');
|
||||||
});
|
});
|
||||||
it('неразрывный дефис в фамилии', () => {
|
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('слеш не уходит на новую строку', () => {
|
it('слеш не уходит на новую строку', () => {
|
||||||
expect(typo('Curry / Unsplash')).toBe(`Curry${N}/ Unsplash`);
|
expect(typo('Curry / Unsplash')).toBe(`Curry${N}/ Unsplash`);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const SHORT = 'а|в|во|и|к|ко|на|не|но|о|об|от|до|по|с|с
|
|||||||
const SHORT_RE = new RegExp(`(^|[\\s(«—–-])(${SHORT}) `, 'giu');
|
const SHORT_RE = new RegExp(`(^|[\\s(«—–-])(${SHORT}) `, 'giu');
|
||||||
const UNIT_RE = /(\d) (кг|г|шт|BYN|мин|дн\.?|день|дня)(?=$|[\s.,:;!?)»])/gu;
|
const UNIT_RE = /(\d) (кг|г|шт|BYN|мин|дн\.?|день|дня)(?=$|[\s.,:;!?)»])/gu;
|
||||||
const NAME_HYPHEN_RE = /([A-Z][a-z]+)-([A-Z][a-z]+)/g;
|
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 {
|
export function typo(text: string): string {
|
||||||
// дважды: «и в Минске» — после замены первого слова второе стоит уже после NBSP
|
// дважды: «и в Минске» — после замены первого слова второе стоит уже после NBSP
|
||||||
@@ -13,6 +15,7 @@ export function typo(text: string): string {
|
|||||||
return out
|
return out
|
||||||
.replace(UNIT_RE, `$1${NBSP}$2`)
|
.replace(UNIT_RE, `$1${NBSP}$2`)
|
||||||
.replace(NAME_HYPHEN_RE, '$1\u2011$2')
|
.replace(NAME_HYPHEN_RE, '$1\u2011$2')
|
||||||
|
.replace(NAME_PAIR_RE, `$1${NBSP}`)
|
||||||
.replace(/ \//g, `${NBSP}/`);
|
.replace(/ \//g, `${NBSP}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ RUN pnpm --filter @kvarta/landing build \
|
|||||||
&& mkdir -p apps/landing/public \
|
&& mkdir -p apps/landing/public \
|
||||||
&& rm -rf apps/landing/.next/standalone/node_modules/.pnpm/@img+sharp-*
|
&& 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
|
FROM node:22-alpine AS sharp
|
||||||
WORKDIR /sharp
|
WORKDIR /sharp
|
||||||
RUN npm install --omit=dev --no-audit --no-fund sharp@0.35.5 >/dev/null
|
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 \
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
PORT=3000 \
|
PORT=3000 \
|
||||||
HOSTNAME=0.0.0.0 \
|
HOSTNAME=0.0.0.0 \
|
||||||
NEXT_SHARP_PATH=/sharp/node_modules/sharp
|
NODE_PATH=/sharp/node_modules
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# outputFileTracingRoot = корень репо, поэтому server.js лежит в apps/landing/ внутри standalone
|
# outputFileTracingRoot = корень репо, поэтому server.js лежит в apps/landing/ внутри standalone
|
||||||
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/.next/standalone ./
|
COPY --from=build --chown=kvarta:kvarta /repo/apps/landing/.next/standalone ./
|
||||||
|
|||||||
@@ -26,3 +26,4 @@
|
|||||||
| 20 | Оркестрация | Оркестратор закоммитил ретранслятор через `git add -A` в том же чекауте, где дизайнер вёл незакоммиченную вёрстку шага 12 — почти весь шаг 12 уехал в коммит `20db25b` «ретранслятор Bot API», а на прод выкатилась половина вёрстки | Отчёт дизайнера | История переписана с согласия автора: `20db25b` разделён на «ретранслятор» и «секции», `push --force-with-lease`; впредь — `git add` по путям |
|
| 20 | Оркестрация | Оркестратор закоммитил ретранслятор через `git add -A` в том же чекауте, где дизайнер вёл незакоммиченную вёрстку шага 12 — почти весь шаг 12 уехал в коммит `20db25b` «ретранслятор Bot API», а на прод выкатилась половина вёрстки | Отчёт дизайнера | История переписана с согласия автора: `20db25b` разделён на «ретранслятор» и «секции», `push --force-with-lease`; впредь — `git add` по путям |
|
||||||
| 21 | Шаги 12–13, дизайн | Самопроверка дизайнера по своим же скриншотам пропустила: полупрозрачную подложку штампа (просвечивало фото), фильтр «краски», съевший половину даты, ряд hero-метрик, запрещённый его же DESIGN.md, и повтор «ЧТ» лентой и штампом на каждой этикетке | Независимый критик по скриншотам, без доступа к коду | Итерация критика 1 |
|
| 21 | Шаги 12–13, дизайн | Самопроверка дизайнера по своим же скриншотам пропустила: полупрозрачную подложку штампа (просвечивало фото), фильтр «краски», съевший половину даты, ряд hero-метрик, запрещённый его же DESIGN.md, и повтор «ЧТ» лентой и штампом на каждой этикетке | Независимый критик по скриншотам, без доступа к коду | Итерация критика 1 |
|
||||||
| 22 | Шаг 12, дизайн | Секция доставки свёрстана во всю ширину без привязки к контейнеру — «Самовывоз» на 1440 вылез за правый край на 24px | Критик по скриншоту 1440 | Колонки 7–12 контейнера 1280 |
|
| 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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user