Мультиязычная страница ошибки и 404 для лендинга на 25 языков
Мультиязычная страница ошибки для лендинга: повторная заявка и 404 на 25 языках, выбор языка, RTL для арабского, настройка nginx, Apache и Keitaro…

Мультиязычная страница ошибки нужна лендингу в двух случаях: клиент оставил повторную заявку или открыл адрес, которого нет. Ниже один HTML-файл на 25 языков без библиотек. Он сам выбирает язык, пишет понятный текст для каждого случая, а в режим страницы 404 переключается одним атрибутом.
Это обновлённая версия статьи 2024 года. Первая страница умела только сообщение о повторной заявке, брала язык лишь из браузера и показывала арабский текст слева направо. Теперь есть 404, язык лендинга, письмо справа налево и запрет индексации.
- Когда лендингу нужна страница ошибки
- Как подключить страницу ошибки
- Полный код error.html
- Как работает страница
- Выбор языка
- Сценарий: дубль или 404
- Что ещё в шаблоне
- Страница 404 для лендинга
- nginx
- Apache
- Keitaro
- Что изменилось по сравнению с версией 2024 года
- Частые вопросы
- Чем страница ошибки отличается от страницы 404?
- Как сделать страницу 404 мультиязычной?
- Можно ли назвать файл error.php?
- Нужен ли пиксель на странице ошибки?
- Как добавить новый язык?
Когда лендингу нужна страница ошибки
| Ситуация | Что показать | Как клиент туда попадает |
|---|---|---|
| Повторная заявка с тем же номером | Заявка уже принята, звонок будет, отправлять снова не нужно | Редирект из api.php или из скрипта на лендинге |
| Адрес, которого нет на домене | Страница не найдена, ссылка на главную | Веб-сервер отдаёт 404.html с кодом 404 |
| Партнёрка вернула ошибку API | Обычную страницу «Спасибо» | Лид сохраняете у себя и отправляете повторно |
Последний вариант подходит, только если заявка надёжно сохранена у вас и предусмотрена повторная отправка. Обычная запись ошибки в лог этого не обеспечивает. Не сообщайте о принятом заказе, если данные потеряны. После сохранения заявки и постановки на повторную отправку клиенту можно показать мультиязычную страницу «Спасибо за заказ».

Как подключить страницу ошибки
- Сохраните код из следующего раздела в файл
error.htmlи положите его в папку лендинга рядом сindex.html. - В
api.php, где проверяется дубль, сделайте редирект наerror.html. Пример ниже. - Для страницы 404 скопируйте файл в
404.html, поменяйте один атрибут и настройте веб-сервер. Это описано в отдельном разделе. - Проверьте результат: откройте
error.html?lang=deиerror.html?reason=notfound.
Редирект при дубле по номеру, если вы используете функцию is_duplicate_lead() из статьи про отсечение дублей лидов:
<?php
// api.php: функция is_duplicate_lead() из статьи про дубли лидов
if (is_duplicate_lead($_POST['phone'] ?? '')) {
header('Location: error.html');
exit;
}Если редирект в ваших скриптах уже ведёт на error.php, просто назовите файл error.php. PHP-кода в нём нет, и сервер отдаст HTML как есть.
Дубли лучше ловить по номеру в одном формате. Маска телефона с кодом страны отправляет номер как +79161234567, как бы клиент его ни ввёл, и проверка по логу перестаёт пропускать один номер, записанный по-разному.
Полный код error.html
<!DOCTYPE html>
<html lang="en" data-reason="duplicate">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Error</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: #f4f4f4;
font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
color: #222;
}
.container {
width: 100%;
max-width: 520px;
padding: 32px 24px;
background: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, .1);
text-align: center;
}
h1 { margin: 0 0 16px; font-size: 26px; }
p { margin: 0; font-size: 17px; line-height: 1.5; }
.button {
display: inline-block;
margin-top: 24px;
padding: 12px 24px;
background: #007bff;
color: #fff;
border-radius: 5px;
text-decoration: none;
}
.button:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<h1 id="title">Your request has already been received</h1>
<p id="message">You have already submitted a request with these details. Our manager will call you soon, there is no need to submit it again.</p>
<a id="home" class="button" href="./">Go to the home page</a>
</div>
<script>
var I18N = {
en: { home: 'Go to the home page',
duplicate: ['Your request has already been received', 'You have already submitted a request with these details. Our manager will call you soon, there is no need to submit it again.'],
notfound: ['Page not found', 'The page may have been moved or deleted. Check the address or go to the home page.'] },
ru: { home: 'На главную',
duplicate: ['Заявка уже принята', 'Вы уже оставили заявку с этими данными. Наш менеджер скоро позвонит, отправлять её повторно не нужно.'],
notfound: ['Страница не найдена', 'Возможно, страница переехала или была удалена. Проверьте адрес или перейдите на главную.'] },
es: { home: 'Ir a la página principal',
duplicate: ['Su solicitud ya fue recibida', 'Ya envió una solicitud con estos datos. Nuestro asesor le llamará pronto, no es necesario enviarla de nuevo.'],
notfound: ['Página no encontrada', 'Es posible que la página se haya movido o eliminado. Revise la dirección o vaya a la página principal.'] },
pt: { home: 'Ir para a página inicial',
duplicate: ['Sua solicitação já foi recebida', 'Você já enviou uma solicitação com estes dados. Em breve entraremos em contato por telefone, não é preciso enviar de novo.'],
notfound: ['Página não encontrada', 'A página pode ter sido movida ou removida. Verifique o endereço ou vá para a página inicial.'] },
fr: { home: "Retour à l'accueil",
duplicate: ['Votre demande a déjà été reçue', 'Vous avez déjà envoyé une demande avec ces coordonnées. Notre conseiller vous appellera bientôt, inutile de la renvoyer.'],
notfound: ['Page introuvable', "La page a peut-être été déplacée ou supprimée. Vérifiez l'adresse ou revenez à l'accueil."] },
de: { home: 'Zur Startseite',
duplicate: ['Ihre Anfrage ist bereits eingegangen', 'Sie haben bereits eine Anfrage mit diesen Daten gesendet. Unser Berater ruft Sie in Kürze an, ein erneutes Senden ist nicht nötig.'],
notfound: ['Seite nicht gefunden', 'Die Seite wurde möglicherweise verschoben oder gelöscht. Prüfen Sie die Adresse oder gehen Sie zur Startseite.'] },
it: { home: 'Torna alla home page',
duplicate: ['La tua richiesta è già stata ricevuta', 'Hai già inviato una richiesta con questi dati. Un nostro operatore ti chiamerà a breve, non serve inviarla di nuovo.'],
notfound: ['Pagina non trovata', "La pagina potrebbe essere stata spostata o eliminata. Controlla l'indirizzo o torna alla home page."] },
pl: { home: 'Przejdź na stronę główną',
duplicate: ['Twoje zgłoszenie zostało już przyjęte', 'Zgłoszenie z tymi danymi zostało już wysłane. Nasz konsultant wkrótce zadzwoni, nie trzeba wysyłać go ponownie.'],
notfound: ['Nie znaleziono strony', 'Strona mogła zostać przeniesiona lub usunięta. Sprawdź adres lub przejdź na stronę główną.'] },
ro: { home: 'Mergeți la pagina principală',
duplicate: ['Cererea dumneavoastră a fost deja primită', 'Ați trimis deja o cerere cu aceste date. Un consultant vă va suna în curând, nu este nevoie să o trimiteți din nou.'],
notfound: ['Pagina nu a fost găsită', 'Este posibil ca pagina să fi fost mutată sau ștearsă. Verificați adresa sau mergeți la pagina principală.'] },
cs: { home: 'Na úvodní stránku',
duplicate: ['Vaši žádost jsme už přijali', 'Žádost s těmito údaji už byla odeslána. Náš operátor vám brzy zavolá, není nutné ji posílat znovu.'],
notfound: ['Stránka nenalezena', 'Stránka mohla být přesunuta nebo smazána. Zkontrolujte adresu nebo přejděte na úvodní stránku.'] },
hu: { home: 'Vissza a kezdőlapra',
duplicate: ['Kérését már megkaptuk', 'Ezekkel az adatokkal már küldött kérést. Munkatársunk hamarosan felhívja, nem kell újra elküldenie.'],
notfound: ['Az oldal nem található', 'Lehet, hogy az oldalt áthelyezték vagy törölték. Ellenőrizze a címet, vagy lépjen a kezdőlapra.'] },
bg: { home: 'Към началната страница',
duplicate: ['Заявката ви вече е получена', 'Вече изпратихте заявка с тези данни. Наш консултант ще ви се обади скоро, не е нужно да я изпращате отново.'],
notfound: ['Страницата не е намерена', 'Възможно е страницата да е преместена или изтрита. Проверете адреса или отидете на началната страница.'] },
el: { home: 'Στην αρχική σελίδα',
duplicate: ['Το αίτημά σας έχει ήδη ληφθεί', 'Έχετε ήδη στείλει αίτημα με αυτά τα στοιχεία. Ένας σύμβουλός μας θα σας καλέσει σύντομα, δεν χρειάζεται να το στείλετε ξανά.'],
notfound: ['Η σελίδα δεν βρέθηκε', 'Η σελίδα ίσως μετακινήθηκε ή διαγράφηκε. Ελέγξτε τη διεύθυνση ή μεταβείτε στην αρχική σελίδα.'] },
et: { home: 'Avalehele',
duplicate: ['Teie taotlus on juba vastu võetud', 'Olete nende andmetega juba taotluse saatnud. Meie konsultant helistab teile peagi, uuesti saata pole vaja.'],
notfound: ['Lehte ei leitud', 'Leht võib olla teisaldatud või kustutatud. Kontrollige aadressi või minge avalehele.'] },
lv: { home: 'Uz sākumlapu',
duplicate: ['Jūsu pieteikums jau ir saņemts', 'Pieteikums ar šiem datiem jau ir nosūtīts. Mūsu konsultants drīz jums piezvanīs, sūtīt to atkārtoti nav nepieciešams.'],
notfound: ['Lapa nav atrasta', 'Iespējams, lapa ir pārvietota vai dzēsta. Pārbaudiet adresi vai dodieties uz sākumlapu.'] },
lt: { home: 'Į pagrindinį puslapį',
duplicate: ['Jūsų paraiška jau gauta', 'Paraiška su šiais duomenimis jau išsiųsta. Mūsų konsultantas netrukus jums paskambins, siųsti jos dar kartą nereikia.'],
notfound: ['Puslapis nerastas', 'Puslapis galėjo būti perkeltas arba ištrintas. Patikrinkite adresą arba eikite į pagrindinį puslapį.'] },
tr: { home: 'Ana sayfaya git',
duplicate: ['Talebiniz zaten alındı', 'Bu bilgilerle zaten bir talep gönderdiniz. Temsilcimiz kısa süre içinde sizi arayacak, tekrar göndermenize gerek yok.'],
notfound: ['Sayfa bulunamadı', 'Sayfa taşınmış veya silinmiş olabilir. Adresi kontrol edin ya da ana sayfaya gidin.'] },
ar: { home: 'الانتقال إلى الصفحة الرئيسية',
duplicate: ['تم استلام طلبك بالفعل', 'لقد أرسلت طلبًا بهذه البيانات من قبل. سيتصل بك أحد ممثلينا قريبًا، ولا داعي لإرساله مرة أخرى.'],
notfound: ['الصفحة غير موجودة', 'ربما تم نقل الصفحة أو حذفها. تحقق من العنوان أو انتقل إلى الصفحة الرئيسية.'] },
hi: { home: 'होम पेज पर जाएँ',
duplicate: ['आपका अनुरोध पहले ही मिल चुका है', 'आप इन विवरणों के साथ पहले ही अनुरोध भेज चुके हैं। हमारा प्रतिनिधि जल्द ही आपको कॉल करेगा, इसे दोबारा भेजने की ज़रूरत नहीं है।'],
notfound: ['पेज नहीं मिला', 'हो सकता है कि पेज हटा दिया गया हो या उसका पता बदल गया हो। पता जाँचें या होम पेज पर जाएँ।'] },
id: { home: 'Ke halaman utama',
duplicate: ['Permintaan Anda sudah kami terima', 'Anda sudah mengirim permintaan dengan data ini. Petugas kami akan segera menelepon, tidak perlu mengirimnya lagi.'],
notfound: ['Halaman tidak ditemukan', 'Halaman mungkin sudah dipindahkan atau dihapus. Periksa alamatnya atau buka halaman utama.'] },
vi: { home: 'Về trang chủ',
duplicate: ['Yêu cầu của bạn đã được tiếp nhận', 'Bạn đã gửi yêu cầu với thông tin này. Nhân viên của chúng tôi sẽ sớm gọi cho bạn, không cần gửi lại.'],
notfound: ['Không tìm thấy trang', 'Trang có thể đã được di chuyển hoặc bị xóa. Hãy kiểm tra lại địa chỉ hoặc về trang chủ.'] },
kk: { home: 'Басты бетке өту',
duplicate: ['Өтініміңіз қабылданып қойған', 'Сіз осы деректермен өтінім жіберіп қойдыңыз. Менеджеріміз жақын арада хабарласады, қайта жіберудің қажеті жоқ.'],
notfound: ['Бет табылмады', 'Бет басқа мекенжайға көшірілген немесе жойылған болуы мүмкін. Мекенжайды тексеріңіз немесе басты бетке өтіңіз.'] },
uz: { home: "Bosh sahifaga o'tish",
duplicate: ['Arizangiz allaqachon qabul qilingan', "Siz bu ma'lumotlar bilan ariza yuborgansiz. Menejerimiz tez orada qo'ng'iroq qiladi, qayta yuborish shart emas."],
notfound: ['Sahifa topilmadi', "Sahifa ko'chirilgan yoki o'chirilgan bo'lishi mumkin. Manzilni tekshiring yoki bosh sahifaga o'ting."] },
az: { home: 'Əsas səhifəyə keç',
duplicate: ['Müraciətiniz artıq qəbul edilib', 'Bu məlumatlarla artıq müraciət göndərmisiniz. Menecerimiz tezliklə sizə zəng edəcək, yenidən göndərməyə ehtiyac yoxdur.'],
notfound: ['Səhifə tapılmadı', 'Səhifə köçürülmüş və ya silinmiş ola bilər. Ünvanı yoxlayın və ya əsas səhifəyə keçin.'] },
ka: { home: 'მთავარ გვერდზე გადასვლა',
duplicate: ['თქვენი განაცხადი უკვე მიღებულია', 'ამ მონაცემებით განაცხადი უკვე გამოგზავნილია. ჩვენი მენეჯერი მალე დაგირეკავთ, ხელახლა გაგზავნა საჭირო არ არის.'],
notfound: ['გვერდი ვერ მოიძებნა', 'შესაძლოა, გვერდი გადატანილია ან წაშლილია. შეამოწმეთ მისამართი ან გადადით მთავარ გვერდზე.'] }
};
var DEFAULT_LANG = 'en';
var RTL = ['ar', 'he', 'fa', 'ur'];
function store(key) {
try { return sessionStorage.getItem(key) || ''; } catch (e) { return ''; }
}
function pickLang(params) {
var list = [params.get('lang'), store('lead_lang')].concat(navigator.languages || [navigator.language]);
for (var i = 0; i < list.length; i++) {
var code = String(list[i] || '').slice(0, 2).toLowerCase();
if (I18N) return code;
}
return DEFAULT_LANG;
}
var params = new URLSearchParams(location.search);
var lang = pickLang(params);
var reason = params.get('reason') || document.documentElement.getAttribute('data-reason');
if (reason !== 'notfound') reason = 'duplicate';
var text = I18N[lang][reason];
document.documentElement.lang = lang;
document.documentElement.dir = RTL.indexOf(lang) > -1 ? 'rtl' : 'ltr';
document.title = text[0];
document.getElementById('title').textContent = text[0];
document.getElementById('message').textContent = text[1];
var home = document.getElementById('home');
home.textContent = I18N[lang].home;
// для 404 ведём в корень сайта: относительная ссылка сломается на чужом адресе
if (reason === 'notfound') home.href = '/';
</script>
</body>
</html>Как работает страница
Выбор языка
Скрипт идёт по списку и берёт первый язык, для которого есть перевод:
- параметр
langв адресе, напримерerror.html?lang=es; - язык лендинга, который сохраняет скрипт со страницы «Спасибо» (ключ
lead_langвsessionStorage); - языки браузера из navigator.languages в порядке предпочтения;
- английский по умолчанию (
DEFAULT_LANG).
Из тега языка берутся две первые буквы, поэтому es-MX и es-ES получат один испанский текст. Для арабского выставляется dir="rtl": атрибут dir задаёт направление текста, и без него арабская фраза выводится слева направо, а знаки препинания встают не на своё место. В старой версии атрибута не было.
Сценарий: дубль или 404
Текст выбирается по параметру reason в адресе, а если его нет, по атрибуту data-reason у тега <html>. Любое другое значение считается повторной заявкой. Для 404 кнопка ведёт в корень сайта /, для дубля на главную лендинга ./, то есть в папку, где лежит страница.
Что ещё в шаблоне
- Тег
<meta name="robots" content="noindex, nofollow">. По справке Google такой тег в<head>закрывает страницу от индексации во всех поисковиках, которые поддерживают это правило. - Тексты выводятся через
textContent, HTML из переводов не выполняется. - Пикселя на странице нет. Повторная заявка не должна попасть в рекламный кабинет как ещё один Lead.
- Внешних скриптов и шрифтов нет, страница грузится одним запросом.
Страница 404 для лендинга
Веб-сервер показывает страницу 404 через внутренний редирект: в адресной строке остаётся тот адрес, который открыл посетитель. Параметр ?reason туда не попадёт, поэтому для 404 нужна копия файла с другим атрибутом. Скопируйте error.html в 404.html и замените вторую строку:
<html lang="en" data-reason="notfound">Положите 404.html в корень сайта и подключите её в настройках сервера.
nginx
Директива error_page в блоке server:
server {
# ... ваши настройки сайта ...
error_page 404 /404.html;
}По документации nginx делает внутренний редирект на указанный адрес. Код ответа меняется, только если явно указать его через =, например error_page 404 =200 /empty.gif;. Для страницы 404 так делать не нужно: поисковики и боты должны видеть, что страницы нет. После правки проверьте конфиг командой nginx -t и примените его через nginx -s reload, оба ключа описаны в документации nginx.
Apache
Директива ErrorDocument работает в конфиге сайта и в .htaccess (нужно право AllowOverride FileInfo):
ErrorDocument 404 /404.htmlМы проверили эту строку на Apache 2.4.58: запрос несуществующего адреса вернул код 404, адрес в браузере не изменился, а браузер с польским языком получил «Nie znaleziono strony» и кнопку на главную.
Keitaro
Если лендинг открывается через трекер, 404 можно не делать. В настройках домена Keitaro есть опция «Перехватывать 404»: запросы на несуществующие адреса уходят на главную домена. Она доступна, когда для домена выбрана кампания в качестве Index страницы.
Что изменилось по сравнению с версией 2024 года
| Что | Версия 2024 | Версия 2026 |
|---|---|---|
| Сценарии | только повторная заявка | повторная заявка и 404 |
| Откуда язык | только язык браузера | ?lang, язык лендинга, язык браузера |
| Языки | 31, тексты длинные | 25, тексты короче и есть для обоих сценариев |
| Арабский | слева направо | dir="rtl" |
| Индексация | без noindex | noindex, nofollow |
| Кнопка | javascript:history.back() | обычная ссылка на главную |
В новый список вошли английский, русский, испанский, португальский, французский, немецкий, итальянский, польский, румынский, чешский, венгерский, болгарский, греческий, эстонский, латышский, литовский, турецкий, арабский, хинди, индонезийский, вьетнамский, казахский, узбекский, азербайджанский и грузинский. Если нужен язык из старой версии, например китайский, японский или бенгальский, добавьте блок в объект I18N по образцу. Тексты мы переводили сами, перед запуском на новое ГЕО покажите их носителю языка. Как перевести сам лендинг, разобрано в статье про локализацию лендингов под ГЕО.
Страницу проверили в headless Chromium: семь локалей браузера, приоритет ?lang, ?reason=notfound, письмо справа налево для арабского и ответ сервера с кодом 404 без смены адреса. Старая версия на арабской локали выводила текст слева направо.
Частые вопросы
Чем страница ошибки отличается от страницы 404?
Страница ошибки объясняет, почему заявку не приняли повторно, и открывается редиректом из вашего скрипта. Страница 404 сообщает, что адреса нет, и её отдаёт веб-сервер с кодом 404. В шаблоне это один файл с двумя наборами текстов.
Как сделать страницу 404 мультиязычной?
Возьмите этот шаблон, поставьте data-reason="notfound", сохраните как 404.html и подключите через error_page в nginx или ErrorDocument в Apache. Язык страница выберет по браузеру посетителя.
Можно ли назвать файл error.php?
Да. В файле только HTML и JavaScript, PHP отдаст его без изменений. Это удобно, если редирект на error.php уже прописан в старых лендингах.
Нужен ли пиксель на странице ошибки?
Нет. Повторная заявка не новый лид, и событие Lead с этой страницы испортит статистику в рекламном кабинете.
Как добавить новый язык?
Скопируйте любой блок в объекте I18N, замените ключ на двухбуквенный код языка и переведите пять строк. Для языков с письмом справа налево добавьте код в массив RTL, там уже есть арабский, иврит, фарси и урду.



