Красивая транслитерация для URL с помощью AWS Lambda

Занимаясь SEO, столкнулся с тем, что во многих CMS URLы для страниц генерируются так: название прогоняется через транслитератор, потом небуквенные символы заменяются на _, и на этом все. С учетом того, что при наполнении сайта данные часто вставляются через буфер обмена с лишними пробелами, получаем ужасные адреса вида http://example.com/-ochen-horoshiy--tovar_-_kupite-оbyazatelno_. Глядя на это, я решил, что нужно создать транслитератор, который будет делать красивые URL. Такой транслитератор должен уметь следующее:
  • делать регистр букв всегда нижним
  • удалять все посторонние символы, кроме букв, цифр, тире и прочерков, а пробелы заменять на «-» (в соответствии с рекомендациями Google);
  • обрезать пробелы по краям, а также несколько пробелов, идущих подряд;
  • уметь обрезать URL по первой запятой (это полезно для многих магазинов, где после запятой часто идут второстепенные параметры товара, которые не требуется выносить в URL);
  • уметь обрезать URL по границе слова так, чтобы не превышать указанную длину.
Встроить такое в какую-либо конкретную CMS несложно. Но хотелось сделать какое-то более универсальное решение — API, к которой можно было бы обращаться из любой CMS. И вот недавно я узнал о бессерверных вычислениях и платформе Lambda для Amazon Web Services. Я счел, что она для таких задач подходит идеально и решил попробовать её в деле.

Создание Lambda функции на AWS

Для этого я отправился на https://aws.amazon.com/ru/lambda/ и зарегистрировался. При регистрации потребовался номер банковской карты и телефон. Как выяснилось, вполне можно использовать виртуальную карту Яндекс.Денег, но при этом с нее в момент регистрации списывается 1 доллар. Также требуют адрес, но вроде бы не проверяют его на подлинность. После того, как мы зарегистрировались и зашли в консоль Amazon Web Services, нужно выбрать регион, где будет размещаться наша функция. К сожалению, в России у AWS серверов нет, поэтому нужно Франкфурт как самый географически близкий пункт. Теперь в списке сервисов ищем Lambda, заходим туда и нажимаем большую оранжевую кнопку «Create function».
Далее нужно выбрать «Author from scratch», ввести имя функци (я назвал её URL-builder) и выбрать среду выполнения. PHP там не поддерживается, поэтому я выбрал Python 3.8.
На следующем шаге появится IDE для редактирования кода функции, но сначала нужно настроить событие, по которому она будет вызываться. В нашем случае это HTTP-запрос. Для этого нажимаем «Add trigger», на появившемся экране выставляем «Create new API» и выбираем там «API gateway» и указываем, что это должна быть HTTP API. Также в Additional Settings нужно поставить галочку CORS, чтобы разрешить кроссдоменные AJAX-запросы (это пригодится при интеграции). Остальные параметры можно оставить по умолчанию. На следующем шаге мы снова окажемся на экране редактирования нашей функции, но там уже будет видна строка API gateway. Если щелкнуть по ней, увидим API endpoint — адрес, по которому нужно обращаться для вызова функции. В моем случае это https://qjq6fageic.execute-api.eu-central-1.amazonaws.com/default/URL-builder.

Пишем код бессерверной функци

Тепреь осталось написать код, который и будет выполнять транслитерацию и прочую обработку URL. Код оформляется в виде самой обычной глобальной python-функции, определенной через def. На вход она принимает два параметра типа dict — event и context. Из event для транслитератора потребуется только элемент queryStringParameters лежит еще один dict, в котором находятся параметры HTTP-запроса. Главная трудность оказалась в том, что эта функция должна возвращать. В документации и Интернете много устаревших примеров, где результаты работы функции можно выводить обычным print. Когда я попытался это использовать, стал получать ошибку вида {"message":"Internal Server Error"}. После недолгих поисков выяснилось, что здесь вам не PHP и на выходе функция должна вернуть dict с ключами statusCode (код ответа сервера) и body с текстом ответа. Причем body уже должен быть приведет к строке, автоматически AWS Lambda этого не делает. Кроме того, можно указать ключ headers с HTTP-заголовками.
Вставляем в IDE код самой функции и сохраняем его как index.py. Он в итоге получился следующим:
def url_builder(event, context):     if event['queryStringParameters']==None or 'tx' not in event['queryStringParameters']: # в параметре tx передается текст, на основе которого нужно сгенерировать URL, если его нет -- отдаем 404 и ничего не делаем         return { 'statusCode': 404 }     text = event['queryStringParameters']['tx'].lower()     if "stopcomma" in event['queryStringParameters'] and "," in text: # если пришел параметр stopcomma и запятая есть в тексте, обрезаем его до первой запятой         pos = text.find(",")         text = text[:pos]     lower_case_letters = { # таблица транслитерации         u'а': u'a',         u'б': u'b',         u'в': u'v',         u'г': u'g',         u'д': u'd',         u'е': u'e',         u'ё': u'e',         u'ж': u'zh',         u'з': u'z',         u'и': u'i',         u'й': u'y',         u'к': u'k',         u'л': u'l',         u'м': u'm',         u'н': u'n',         u'о': u'o',         u'п': u'p',         u'р': u'r',         u'с': u's',         u'т': u't',         u'у': u'u',         u'ф': u'f',         u'х': u'h',         u'ц': u'ts',         u'ч': u'ch',         u'ш': u'sh',         u'щ': u'sch',         u'ъ': u'',         u'ы': u'y',         u'ь': u'',         u'э': u'e',         u'ю': u'yu',         u'я': u'ya'     }     answer = ""     for c in text:         if c.isalpha(): #           answer+=c if not c in lower_case_letters else lower_case_letters[c] # русские буквы транслитерируем по таблице, остальные оставляем как есть         elif c.isdigit() or c=='_' :           answer+=c         elif c=='-' or c==' ':           answer+=" "     while "__" in answer: # избавляемся от нескольких прочерков (_) подряд         answer=answer.replace("__","_")     answer=answer.strip().replace("—","-").replace(" ","-").replace("_-","-").replace("-_","-") # заменяем пробелы на - и избавляемся от сочетаний _- и -_     while "--" in answer: # избавляемся от нескольких дефисов (-) подряд (это нужно делать после всех остальных преобразований)         answer=answer.replace("--","-")     if 'maxlen' in event['queryStringParameters']:         maxlen=int(event['queryStringParameters']['maxlen'])         while len(answer)>maxlen:             pos = answer.rfind("-")             if pos!=-1:                 answer=answer[:pos]             else:                 answer=answer[:maxlen]                        return {     'statusCode': 200,     'headers': {'Content-Type': 'text/plain'}, # чтобы не тратить ресурсы на стороне клиента на разбор JSON, вернем сгенерированный URL как обычный текст     'body': answer     }

Теперь нужно заполнить поле handler. В нем указывается точка входа в таком формате: сначала имя файла, потом, через точку, имя функции, которая должна быть вызвана. Файл я назвал index.py, поэтому handler будет такой: index.url_builder.
Важный момент: изменения в коде применяются только после того, как нажата оранжевая кнопка Save наверху, если делать Save в самой IDE, изменения сохраняются, но продолжает выполняться старая версия кода!
Пытаемся запустить: https://qjq6fageic.execute-api.eu-central-1.amazonaws.com/default/URL-builder?tx=Красивая%20транслитерация%20для%20URL%20с%20помощью%20AWS%20Lambda. И получаем ответ — транслитерированный URL: krasivaya-transliteratsiya-dlya-url-s-pomoschyu-aws-lambda. Оно работает!

API транслитератора и пример интеграции в CMS

URL вызова: https://qjq6fageic.execute-api.eu-central-1.amazonaws.com/default/URL-builder
Поддерживаемые методы: GET, POST
Параметры:
  • tx — исходная строка, которую требуется транслитерировать в URL. Обязательный параметр, его отсутствие вызывает ошибку 404.
  • maxlen — максимальная длина URL. Обрезка делается с учетом границ слов, но если первое слово длиннее указанного количества символов, оно будет обрезано до указанной длины. Необязательный параметр.
  • stopcomma — если этот параметр присутствует (с любым, даже нулевым значением), для генерации URL будет использована только часть строки до первой запятой. Необязательный параметр.
Рассмотрим то, как можно интегрировать транслитератор в CMS на примере моего форумного движка Intellect Board. Там в форме создания новой темы есть два поля: topic[title] для названия темы и topic[hurl] для её URL. Вставим в код страницы небольшой JavaScript, в котором повесим на поле с названием обработчик события onchange, который будет запрашивать URL при каждом изменении названия, и прописывать его в соответствующее поле:
<script> var title=document.querySelector('input[name="topic[title]"]'); var hurl=document.querySelector('input[name="topic[hurl]"]'); title.addEventListener("change",function(e) {   var xhr = new XMLHttpRequest();   xhr.open('GET', 'https://qjq6fageic.execute-api.eu-central-1.amazonaws.com/default/URL-builder?maxlen=30&tx='+title.value);   xhr.onreadystatechange = function() {     if (xhr.readyState !== 4) return;     if (xhr.status === 200) {       hurl.value=xhr.responseText;     }   };   xhr.send(); }); </script>

Замените имена полей на те, которые используются в форме вашей CMS и получите возможность создавать красивые URL!