Этап 1b: сайт

- Страницы обоих брендов из блоков Payload (D-029): главная, цены,
  локации, поддержка, 4 юридические, 404 и 500. Коллекции pages
  (черновики и версии) и faq с местами показа, 12 типов блоков; поля
  sites — почта и Telegram поддержки, часы работы, SEO по умолчанию.
- Карта и глобус рисуются на сервере (d3-geo), контуры во внешнем
  SVG через <use>; выбор локации синхронизирован между картой и сеткой.
- Тексты сайта и юридические черновики на 5 языках: de, fr, es —
  перевод с глоссарием; seed создаёт страницы и FAQ, только если их
  нет, SEED_CONTENT=reset пересоздаёт.
- SEO: title и описания, canonical, hreflang с x-default,
  sitemap.xml и robots.txt на каждый домен, картинки для соцсетей.
- Статическая сборка и пересборка через revalidatePath из хуков
  Payload (D-030); /svc/internal/revalidate для проверки.
- 404 — статичная страница со статусом 404 через proxy; на сайте
  только опубликованные страницы.
- Производительность: Client Hints темы Payload только в админке,
  предзагрузка 2 файлов шрифтов вместо 6; Lighthouse mobile 97–99.
- Контраст --ink-3 по WCAG AA, радиокнопки вместо div role=button.
- Документация: итоги 1b, глоссарий de/fr/es, трактовки юридических
  черновиков подтверждены, переносимые вопросы — в этапах 3e–3h.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreypr207andClaude Opus 5.5 committed 2026-10-22 04:53:15 +02:00
1 parent 515acfb69d
commit 20491d1ba4
134 files changed
+11232 -212

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import { revalidatePath } from 'next/cache'
import type { PayloadRequest } from 'payload'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { revalidateAfterChange, revalidateAfterDelete, SKIP_REVALIDATE } from './revalidateSite'
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
const req = { payload: { logger: { debug: vi.fn() } } } as unknown as PayloadRequest
// хукам нужны только doc, previousDoc, req и context
const change = (doc: object, previousDoc: object = {}, context: object = {}) =>
revalidateAfterChange({ doc, previousDoc, req, context } as never)
describe('пересборка маркетинговых страниц', () => {
beforeEach(() => vi.mocked(revalidatePath).mockReset())
it('правка без черновиков пересобирает layout всех маркетинговых страниц', () => {
change({ id: '1' })
expect(revalidatePath).toHaveBeenCalledWith('/(front)/m/[host]/[locale]/[currency]', 'layout')
})
it('черновик страницы не пересобирает, публикация и снятие с публикации — да', () => {
change({ _status: 'draft' }, { _status: 'draft' })
expect(revalidatePath).not.toHaveBeenCalled()
change({ _status: 'published' }, { _status: 'draft' })
change({ _status: 'draft' }, { _status: 'published' })
expect(revalidatePath).toHaveBeenCalledTimes(2)
})
it('seed и пакетные правки пропускают пересборку', () => {
change({ id: '1' }, {}, { [SKIP_REVALIDATE]: true })
revalidateAfterDelete({ doc: { id: '1' }, req, context: { [SKIP_REVALIDATE]: true } } as never)
expect(revalidatePath).not.toHaveBeenCalled()
})
it('вне запроса Next ошибка пересборки не мешает сохранению', () => {
vi.mocked(revalidatePath).mockImplementationOnce(() => {
throw new Error('static generation store missing')
})
expect(() => change({ id: '1' })).not.toThrow()
expect(req.payload.logger.debug).toHaveBeenCalled()
})
})
+46
View File
@@ -0,0 +1,46 @@
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'
import { revalidateMarketingPages } from '@/core/content/revalidate'
/** Флаг в `context` операции: seed и пакетные правки пересборку не запускают */
export const SKIP_REVALIDATE = 'skipRevalidate'
const revalidate = (req: PayloadRequest, context: Record<string, unknown>) => {
if (context[SKIP_REVALIDATE]) return
try {
revalidateMarketingPages()
} catch (error) {
// вне запроса Next (скрипты Payload) пересобирать нечего: кэш страниц живёт в процессе Next
req.payload.logger.debug({ msg: 'revalidatePath пропущен', err: error })
}
}
/** У коллекций с черновиками пересобираем только на публикацию и снятие с публикации */
const affectsPublished = (doc: unknown, previousDoc: unknown): boolean => {
const status = (value: unknown) =>
typeof value === 'object' && value !== null && '_status' in value ? value._status : undefined
const current = status(doc)
if (current === undefined) return true
return current === 'published' || status(previousDoc) === 'published'
}
export const revalidateAfterChange: CollectionAfterChangeHook = ({
doc,
previousDoc,
req,
context,
}) => {
if (affectsPublished(doc, previousDoc)) revalidate(req, context)
return doc
}
export const revalidateAfterDelete: CollectionAfterDeleteHook = ({ doc, req, context }) => {
revalidate(req, context)
return doc
}
/** Хуки для коллекций, чьи данные показывают маркетинговые страницы */
export const revalidateSiteHooks = {
afterChange: [revalidateAfterChange],
afterDelete: [revalidateAfterDelete],
}