SSCF - Единый стандарт для контактных форм

SSCF - Единый стандарт для контактных форм
Стандарты

38

14 мин.

Определения

Стандарт SSCF определяет требования к семантике, доступности и логике работы контактных форм.

Структура и семантика

Элемент формы и группировка полей

  • Для сбора и отправки связанных данных используется нативный элемент <form>. Он предоставляет браузеру семантику формы, поведение клавиатуры и механизм отправки.
  • Связанные поля группируются с помощью <fieldset> и <legend>. <fieldset> применяется только там, где поля образуют понятную логическую группу; оборачивать им каждое поле по отдельности не требуется.
  • <legend> задаёт название группы и располагается первым дочерним элементом <fieldset>.
  • Для отправки данных методом POST у <form> явно задаются method="post" и адрес обработчика через action. Для форм с файлами дополнительно задаётся enctype="multipart/form-data".
  • Секреты интеграций и ключи внешних сервисов не размещаются в клиентском коде. Данные передаются на собственный защищённый серверный обработчик, который выполняет проверку и взаимодействует с внешними системами.

Названия и атрибуты элементов управления

  • Каждое отправляемое поле получает уникальный id и осмысленный name. id связывает поле с подписью, а name задаёт имя поля в отправляемом наборе данных.
  • Для <input> указывается подходящий type: например, text, email, tel, checkbox, file, number или range. У <select> и <textarea> атрибута type нет; тип взаимодействия определяется самим элементом.
  • Атрибут required добавляется только к полям, обязательность которых действительно обусловлена сценарием. Для необязательных полей он не используется.
  • autocomplete задаётся подходящими токенами, например name, email, tel, given-name или family-name. Это помогает браузеру подобрать сохранённые данные и указывает назначение поля.
  • placeholder необязателен. Он может показывать короткий пример формата, но не заменяет постоянную подпись <label> и не используется как единственное объяснение поля.
  • Видимый <label> связывается с полем через одинаковые значения for и id либо оборачивает поле. Нажатие на подпись переводит фокус на соответствующее поле.

Пример семантической структуры формы:

<form action="/ru/contact" method="post" enctype="multipart/form-data" data-contact-form>
  <fieldset>
    <legend>Контактные данные</legend>

    <div class="form-field">
      <label for="name">Имя</label>
      <input
        id="name"
        name="name"
        type="text"
        autocomplete="name"
        placeholder="Например, Анна"
        required
      />
    </div>

    <div class="form-field">
      <label for="email">Электронная почта</label>
      <input
        id="email"
        name="email"
        type="email"
        autocomplete="email"
        placeholder="name@example.com"
        required
      />
    </div>

    <div class="form-field">
      <label for="message">Сообщение</label>
      <textarea
        id="message"
        name="message"
        placeholder="Кратко опишите вопрос"
        required
      ></textarea>
    </div>
  </fieldset>

  <div class="form-consent">
    <label class="form-consent__label">
      <input id="data-consent" name="dataConsent" type="checkbox" required />
      <span>Согласие на обработку данных</span>
    </label>
    <a href="/ru/policy" target="_blank" rel="noopener noreferrer">
      Условия обработки данных (откроются в новой вкладке)
    </a>
  </div>

  <button class="form-submit" type="submit">Отправить</button>
  <p role="status" aria-live="polite" data-form-status></p>
</form>

Чекбокс и его подпись объединены внутри <label>, поэтому нажатие на текст также переключает поле. Элемент <a> намеренно расположен вне <label>: ссылка остаётся отдельным интерактивным элементом. Открытие внутренней страницы в новой вкладке применяется только при обоснованной необходимости; в таком случае поведение заранее обозначается. Для target="_blank" задаётся rel="noopener noreferrer".

Состояния и валидация полей

Визуальные состояния

  • Состояние по умолчанию показывает обычное поле ввода без ошибочной или активной индикации.
  • Состояние фокуса заметно отличается от обычного и сохраняет видимый индикатор для клавиатурной навигации.
  • Состояние ошибки обозначается не только цветом: поле получает текстовое сообщение, а при собственной программной валидации — aria-invalid="true".
  • Ошибка связывается с соответствующим полем через aria-describedby. В сообщении указываются проблема и способ её исправления, например требуемый формат email.
  • aria-invalid="true" не выставляется до того, как ошибка действительно обнаружена. После исправления значения состояние обновляется или снимается.
  • Успешное состояние не обязательно показывать у каждого поля. Если оно отображается, его смысл также передаётся текстом или дополнительным знаком, а не только зелёной рамкой.

Пример поля с сообщением об ошибке:

<div class="form-field form-field--error">
  <label for="email">Электронная почта</label>
  <input
    id="email"
    name="email"
    type="email"
    autocomplete="email"
    aria-invalid="true"
    aria-describedby="email-error"
    required
  />
  <p id="email-error">Укажите адрес электронной почты в корректном формате.</p>
</div>

Момент проверки и блокировка отправки

  • Нативные ограничения HTML, такие как required, type="email", min, max и pattern, применяются там, где подходят сценарию. Пользовательская проверка дополняет их только необходимыми правилами.
  • Проверка на клиенте нужна для оперативной обратной связи, но все значимые ограничения повторно проверяются на сервере: клиентские данные можно изменить или отправить в обход интерфейса.
  • Ошибку не следует показывать до того, как пользователь взаимодействовал с полем, покинул его или попытался отправить форму. Это предотвращает появление сообщения об ошибке у ещё не заполненного поля.
  • Простую локальную проверку обычно выполняют при вводе, потере фокуса или отправке без дополнительной задержки. Debounce около 300 ms допустим для дорогой или сетевой проверки после паузы ввода; результат такой проверки не заменяет немедленную проверку при blur и перед отправкой.
  • По умолчанию кнопка отправки остаётся доступной, чтобы пользователь мог запустить валидацию и получить объяснения ошибок. Отключение кнопки при каждом невалидном значении может скрыть причину недоступности и затруднить исправление формы.
  • Если в интерфейсе принято отключать кнопку до выполнения условий, рядом явно описываются эти условия, а поля и ошибки остаются доступными для исправления. disabled не заменяет сообщения об ошибках.
  • При попытке отправки формы с несколькими ошибками выводится понятное резюме или сообщения у полей; при необходимости фокус переводится к первому невалидному полю.

Safari и автозаполнение

  • В Safari на iPhone размер текста непосредственно в сфокусированном поле устанавливается не менее 16px, чтобы избежать автоматического масштабирования страницы при фокусе.
  • Масштабирование страницы не отключается через ограничения viewport. Пользователь должен сохранять возможность увеличивать страницу средствами браузера.
  • Автозаполнение не блокируется без конкретной причины. Для персональных данных указываются точные значения autocomplete, например name, email и tel.
  • Если системный стиль автозаполнения конфликтует с оформлением, для :autofill и :-webkit-autofill задаются читаемые цвета текста и фона с достаточным контрастом. Системный стиль не скрывается ценой неразличимого содержимого или фокуса.

Базовые стили полей и состояния автозаполнения:

.form-field input,
.form-field select,
.form-field textarea,
.phone-field input {
  font-size: 16px;
}

.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible,
.phone-field input:focus-visible {
  outline: 2px solid #2457d6;
  outline-offset: 2px;
}

.form-field [aria-invalid="true"] {
  border-color: #b42318;
}

.form-field input:autofill,
.form-field input:-webkit-autofill,
.phone-field input:autofill,
.phone-field input:-webkit-autofill {
  -webkit-text-fill-color: #161616;
  box-shadow: 0 0 0 1000px #fff inset;
}

Цвета в примере заменяются токенами интерфейса с проверенным контрастом. Размер 16px важен для текста самого поля; размер подписи или placeholder не меняет размер текста ввода.

Типы полей и мобильный ввод

Телефон

  • Для телефонного номера используется поле type="tel" с autocomplete="tel". inputmode="tel" может дополнительно подсказать мобильному устройству подходящую экранную клавиатуру.
  • Международная форма не имеет единой фиксированной длины в цифрах. Проверка только по правилу «не менее 11 цифр» или «ровно 11 цифр» некорректна для разных стран и форматов.
  • Номер проверяется с учётом выбранной страны и её плана нумерации, а для передачи нормализуется в согласованный формат, например E.164. Одна лишь маска проверяет отображение, а не существование или доступность номера.
  • Повторяющиеся цифры сами по себе не являются надёжным признаком невалидного номера. Такие эвристики не применяются как универсальный запрет ввода; защита от спама выполняется отдельно на сервере.
  • При выборе страны одновременно обновляются код и формат отображения номера. Ввод номера с международным префиксом и вставка скопированного номера обрабатываются без потери цифр и без дублирования кода страны.
  • Символ + и цифры кода страны не блокируются как универсально запрещённый ручной ввод: международный номер можно набрать или вставить. Если поле закрепляет код выбранной страны, повторный код нормализуется, а не добавляется к номеру второй раз.
  • Если код выбранной страны нельзя редактировать, начальная позиция каретки при фокусе располагается после кода; при этом сохраняется возможность выделять и заменять номер, вставлять международный номер и менять страну. Речь идёт о позиции каретки при фокусе, а не об автоматической установке autofocus при загрузке формы. Для России национальный префикс 8 и международный +7 не считаются двумя одинаковыми масками: ввод нормализуется к выбранному формату.
  • Начальная страна может определяться настройкой пользователя, локалью сайта или его явным выбором. Определение по IP требует отдельного сервиса, может ошибаться и не должно блокировать ручной выбор страны или ввод номера.
  • Курсор сохраняет ожидаемую позицию при наборе, вставке, выделении и удалении в середине номера. Не следует вручную перемещать каретку после каждого события ввода, если библиотека уже управляет форматированием.
  • Номер, введённый в поле, не подтверждает владение им. Для проверки доступности или владения требуется отдельный процесс подтверждения, например кодом.

React-реализация проекта

В React-проекте вместо @react-input/mask для международного телефонного поля используется react-international-phone. Переданный пример useMask({ mask, replacement }) относится к @react-input/mask и не является API телефонного пакета. Здесь используется PhoneInput, который форматирует номер, поддерживает выбор страны и обработку позиции курсора. Определение страны по IP не является встроенной функцией пакета, проверкой номера или заменой пользовательского выбора.

import { useState } from 'react';
import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';

export const PhoneField = () => {
  const [phone, setPhone] = useState('');

  return (
    <div className="phone-field">
      <label htmlFor="phone">Телефон</label>
      <PhoneInput
        defaultCountry="ru"
        forceDialCode
        value={phone}
        onChange={(phone) => setPhone(phone)}
        inputProps={{
          id: 'phone',
          name: 'phone',
          type: 'tel',
          autoComplete: 'tel',
          inputMode: 'tel',
          required: true,
        }}
      />
    </div>
  );
};

react-international-phone отвечает за ввод и форматирование, но актуальная документация пакета больше не рассматривает его как библиотеку валидации номера. Для проверки по планам нумерации применяется специализированная библиотека, например google-libphonenumber, а также серверная проверка.

import { PhoneNumberUtil } from 'google-libphonenumber';

const phoneUtil = PhoneNumberUtil.getInstance();

const isValidPhone = (phone) => {
  try {
    return phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(phone));
  } catch (error) {
    return false;
  }
};

Проверка формата и плана нумерации не подтверждает, что номер активен или принадлежит отправителю.

Select, textarea и загрузка файлов

  • Для списка вариантов по умолчанию используется нативный <select>. Он предоставляет встроенную клавиатурную и платформенную поддержку.
  • Пользовательский список применяется только при необходимости, которую нельзя закрыть нативным <select>. Для него реализуются доступное имя, состояние раскрытия и выбора, управление клавиатурой и предсказуемое управление фокусом по подходящему ARIA-паттерну.
  • У <textarea> сохраняется возможность изменения размера, если она не нарушает макет. Если вертикальное изменение размера требуется ограничить, используется resize: vertical или иной обоснованный предел; безусловный resize: none может мешать вводу длинного текста.
  • У загрузки файлов задаются допустимые типы, максимальный размер и число файлов. Атрибут accept только подсказывает варианты файлового диалога и не является проверкой безопасности.
  • Сервер повторно проверяет тип и размер файлов, не доверяет одному MIME-заголовку или имени файла, ограничивает допустимые расширения и безопасно обрабатывает имя и хранение.
  • После выбора отображается список вложений с именем и размером. При необходимости показываются прогресс, ошибка и управление удалением каждого файла.

Ползунок диапазона

  • Для выбора числового значения в пределах диапазона используется нативный <input type="range">, если его поведение соответствует задаче. Он уже поддерживает семантику, фокус, клавиатуру и изменение значения указателем.
  • Для значения, которое важно вводить точно, рядом может быть связанное поле type="number". Диапазон, шаг и текущее значение синхронизируются из одного источника состояния, чтобы контролы не расходились.
  • Для пары «ползунок + числовое поле» в отправляемые данные включается одно каноническое значение. Дублирование одного и того же параметра в теле формы не создаётся случайно.
  • У ползунка задаются доступная подпись, а также осмысленные min, max, step и начальное value. Текущее значение показывается текстом, если без него положение ползунка недостаточно понятно.
  • Нативный ползунок поддерживает изменение значения клавиатурой и указателем. Если создаётся собственный контрол, отдельно реализуются указатель, касание, клавиши, фокус, доступное имя и передача значения вспомогательным технологиям.
  • cursor: pointer может указывать на интерактивность ползунка. cursor: grab и cursor: grabbing применяются только когда контрол действительно реализует перетаскивание; курсор не заменяет поддержку касания и клавиатуры.
  • При щелчке по дорожке нативный контрол обрабатывает указатель согласно реализации браузера. Для собственного ползунка переход к указанной точке должен работать как мышью, так и касанием, а изменение не должно ломать прокрутку страницы.

Пример связанного числового поля и ползунка:

<div class="form-field">
  <label for="budget-range">Бюджет</label>
  <input
    id="budget-range"
    type="range"
    min="0"
    max="100000"
    step="5000"
    value="50000"
    aria-describedby="budget-current-value"
  />
  <output id="budget-current-value" for="budget-range">50 000 ₽</output>

  <label for="budget-value">Точное значение, ₽</label>
  <input
    id="budget-value"
    name="budget"
    type="number"
    min="0"
    max="100000"
    step="5000"
    value="50000"
    inputmode="numeric"
    required
  />
</div>

Оба контрола обновляют одно значение в состоянии интерфейса. В примере только числовое поле имеет name, поэтому значение отправляется один раз.

Пример синхронизации на нативных полях:

const rangeInput = document.querySelector('#budget-range');
const numberInput = document.querySelector('#budget-value');
const budgetOutput = document.querySelector('#budget-current-value');

const formatBudget = (value) => `${new Intl.NumberFormat('ru-RU').format(Number(value))} ₽`;

const updateBudgetOutput = () => {
  budgetOutput.value = formatBudget(rangeInput.value);
};

rangeInput.addEventListener('input', () => {
  numberInput.value = rangeInput.value;
  updateBudgetOutput();
});

numberInput.addEventListener('change', () => {
  if (!numberInput.validity.valid) return;

  rangeInput.value = numberInput.value;
  numberInput.value = rangeInput.value;
  updateBudgetOutput();
});

Ползунок обновляет числовое поле во время перемещения. Введённое число синхронизируется с ползунком после завершения ввода и только при прохождении ограничений min, max и step.

input[type="range"] {
  cursor: pointer;
}

input[type="range"]:active {
  cursor: grabbing;
}

Доступность и управление с клавиатуры

  • Все элементы формы доступны в предсказуемом порядке с помощью Tab и Shift+Tab; видимый фокус не удаляется.
  • Нативный <select> управляется клавиатурой по правилам операционной системы и браузера. Пользовательский список должен самостоятельно реализовывать согласованное поведение клавиш, включая перемещение между вариантами и выбор.
  • Чекбокс переключается клавишей Space, когда он в фокусе. Нативный элемент сохраняет правильное состояние и доступен без добавления дублирующих ARIA-атрибутов.
  • У каждого поля есть доступное имя, обычно предоставляемое <label>. aria-label или aria-labelledby используются только тогда, когда видимой подписи нет или её недостаточно.
  • ARIA добавляется для описания недостающих имени, состояния или связи, а не по правилу «каждому input и button нужен aria-атрибут». Нативная семантика предпочтительнее повторного объявления роли.
  • Состояния ошибки различимы не только по цвету. Сообщение доступно визуально и программно связано с полем; изменение общей формы при необходимости объявляется без неожиданного перемещения фокуса.
  • Текст кнопки достаточен для её доступного имени. Для кнопок только с иконкой задаётся понятное имя через aria-label или aria-labelledby.
  • Для чекбокса поле можно обернуть вместе с текстом подписи в <label>, чтобы вся подпись переключала состояние. Ссылка на политику размещается отдельно от <label>, чтобы не вкладывать интерактивную ссылку в подпись.
  • Условие обязательности чекбокса определяется сценарием формы и требованиями продукта; наличие ссылки на политику само по себе не означает, что любое поле согласия следует делать обязательным.
  • Сообщения об ошибке и успехе короткие, понятные и доступны без необходимости искать их по всей странице. Для динамического статуса подходит отдельная область role="status" с aria-live="polite".

Отправка и состояния результата

Состояния отправки

  • До отправки кнопка содержит ясное действие, например «Отправить».
  • Во время сетевого запроса предотвращается повторная отправка. Кнопка может временно получать disabled, а состояние формы — aria-busy="true".
  • Текст статуса операции выводится отдельно от кнопки, например в области role="status". Изменения текста внутри заблокированной кнопки не всегда дают надёжное уведомление вспомогательным технологиям.
  • После подтверждённого успеха показывается сообщение «Форма отправлена» или эквивалент. Форма очищается только после подтверждения сервером; при ошибке введённые данные сохраняются, если пользователь не запросил их очистку.
  • Сообщение об успехе не скрывается автоматически через произвольные пять секунд. Если интерфейс возвращает кнопку в исходное состояние по таймеру, сообщение всё равно остаётся доступным достаточно долго и не теряется до того, как его можно прочитать.
  • Автоматическое скрытие статуса не является защитой от спама. Ограничение частоты, защита от повторных запросов и проверка достоверности выполняются сервером.

Если кнопка действительно отключена, её визуальное состояние должно отличаться от доступного. Курсор и прозрачность — только визуальные подсказки; причина недоступности остаётся текстово понятной.

.form-submit:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

Серверная обработка и интеграции

  • Перед отправкой проверяются все поля на клиенте, а те же критичные правила повторяются на сервере. Сервер не доверяет флагам валидности, значениям disabled или ограничениям из интерфейса.
  • Данные отправляются методом POST по HTTPS. Применимость CSRF-защиты, ограничения частоты, идемпотентности и других мер определяется моделью угроз и способом аутентификации обработчика.
  • Если данные требуется передать в несколько систем, браузер отправляет их в собственный серверный обработчик. Ключи Bitrix, Directus, Telegram и других интеграций не включаются в JavaScript-код страницы.
  • Promise.allSettled() сообщает результат нескольких независимых обещаний, но не делает их атомарной транзакцией и не отменяет уже выполненные действия при частичном сбое. Сервер определяет, какой результат считается принятым, как повторяются неудачные доставки и как предотвращаются дубликаты.
  • Пользователю сообщается результат основного действия. Успех отображается только тогда, когда сервер подтвердил определённый системой критерий приёма, например сохранение заявки или её надёжную постановку в очередь.
  • При неуспешной отправке сообщение описывает следующий шаг, не стирая введённые значения. Ошибки отдельных полей, полученные от сервера, связываются с соответствующими полями.

Пример отправки формы на серверный обработчик:

const form = document.querySelector('[data-contact-form]');
const submitButton = form.querySelector('[type="submit"]');
const formStatus = form.querySelector('[data-form-status]');
let isSubmitting = false;

form.addEventListener('submit', (event) => {
  event.preventDefault();

  if (isSubmitting || !form.reportValidity()) return;

  isSubmitting = true;
  submitButton.disabled = true;
  form.setAttribute('aria-busy', 'true');
  formStatus.textContent = 'Отправка…';

  fetch(form.action, {
    method: 'POST',
    body: new FormData(form),
    headers: { Accept: 'application/json' },
  })
    .then((response) => {
      if (!response.ok) throw new Error('Form submission failed');

      form.reset();
      formStatus.textContent = 'Форма успешно отправлена';
    })
    .catch(() => {
      formStatus.textContent = 'Не удалось отправить форму. Проверьте соединение и повторите попытку.';
    })
    .then(() => {
      isSubmitting = false;
      submitButton.disabled = false;
      form.removeAttribute('aria-busy');
    });
});

Пример предполагает наличие data-contact-form, кнопки отправки и области статуса в разметке. В реальном обработчике отдельно разбираются серверные ошибки, тайм-ауты и повторные запросы; клиентский код не обращается напрямую к внешним интеграциям.

Тестирование

  • Форма имеет корректную структуру <form>, группы <fieldset> и <legend> используются только для связанных полей.
  • Каждое поле имеет постоянную подпись, уникальный id, подходящий type там, где это применимо, и name, если значение должно быть отправлено.
  • Проверены нативные required, форматы полей, поведение autocomplete и различие между подписью и необязательным placeholder.
  • Состояния по умолчанию, фокуса и ошибки различимы; ошибка не передаётся только цветом и связана с полем через текстовое описание.
  • Ошибки не показываются до взаимодействия без причины; проверка срабатывает при нужных событиях и повторяется при отправке.
  • Пользователь может понять, почему отправка не состоялась. Отключённая кнопка не становится единственным индикатором ошибки или обязательного действия.
  • Отправка работает мышью, касанием и клавиатурой; проверяются Tab, Shift+Tab, Space для чекбокса и управление нативным либо пользовательским select.
  • На iPhone проверяется отсутствие автоматического масштабирования поля, а ввод на сенсорных устройствах использует подходящую клавиатуру.
  • Автозаполненные значения сохраняют читаемый текст, контраст, фокус и визуальное состояние поля.
  • Телефонные номера проверяются для выбранной страны и международного формата; отдельно проверяются вставка, удаление, перемещение каретки, смена страны и номер с повторяющимися цифрами.
  • Страна по IP, если применяется, проверяется как приблизительное начальное значение с возможностью ручного выбора и рабочим запасным сценарием.
  • Пользовательский select, телефонный селектор страны и собственный ползунок проверяются с клавиатурой и вспомогательными технологиями.
  • Для файлов проверяются допустимые типы, размер, количество, список, удаление, серверная проверка и сценарии ошибки загрузки.
  • Числовое поле и ползунок остаются синхронизированы при вводе, перетаскивании, достижении границ и изменении шага; значение отправляется в согласованном виде.
  • При отправке проверяются повторное нажатие, сетевой сбой, серверная ошибка, сохранение значений при ошибке, очистка после успеха и доступность объявления статуса.
  • Если отправка вызывает несколько интеграций, тестируется частичный отказ и отсутствие дубликатов после повторной попытки.

Итоговый чек-лист SSCF

КатегорияОсновные требованияНазначение
Семантика<form>, <fieldset>, <legend>, label, подходящий тип контролаПонятная структура и нативное поведение
ДанныеУникальные id, name для отправляемых значений, required по сценариюКорректная передача полей
АвтозаполнениеТочные токены autocomplete; placeholder только как подсказкаБыстрое и понятное заполнение
ВалидацияКлиентская обратная связь и повторная серверная проверкаОбнаружение ошибок без доверия клиенту
СостоянияОбычное, фокус, ошибка, загрузка и результат; текст не только цветПонимание текущего состояния формы
ТелефонСтрановой формат, нормализация, отдельная проверка валидностиВвод национальных и международных номеров
Специальные поляНативные select и range, доступный textarea, серверная проверка файловПредсказуемое управление данными
ДоступностьПодписи, связи ошибок, клавиатура, корректная семантика ARIAУправление и понимание формы разными способами
ОтправкаPOST на сервер, защищённые интеграции, обработка частичного сбояНадёжная передача и понятный результат

Похожие статьи

  • Анатомия React компонента

    Моё видение того, как должен выглядеть React компонент

    421

    7 мин.

  • SST-A - Единый стандарт для табов

    Стандарт SST-A определяет требования к семантике, доступности и логике работы табов

    62

    6 мин.

  • SSF-U - Единый стандарт для фуллскрина

    Стандарт SSF-U определяет требования к семантике, доступности и логике работы фуллскрина

    248

    2 мин.

  • SSA - Единый стандарт для аккордеона

    Стандарт SSA определяет требования к семантике, доступности и логике работы аккордеона

    258

    2 мин.

  • Bad Practices для сайтов

    Разбор критических ошибок веб-дизайна. Почему слайдеры, автоплей и тяжёлые страницы снижают конверсию и позиции в Google и Yandex

    232

    1 мин.

  • SSP - Единый стандарт для пагинации

    Стандарт SSP определяет требования к семантике, доступности и логике работы пагинации

    240

    1 мин.

  • Все статьи

Свяжитесь со мной