При переносе моего блога с Heroku на Netlify было несколько динамических аспектов, которые мне пришлось пересмотреть при переходе на полностью статическую платформу хостинга.

Две функции, которые в моем случае тесно связаны, — это поиск и динамические ссылки быстрого доступа. Эти ярлыки означали, что я мог посетить remysharp.com/twitter и перенаправить на последний пост, где слаг содержит «twitter».

Возможно, вы уже догадались, как это работает, поэтому вам можно пропустить то, как я сокращаю поисковый контент, а затем расставляю приоритеты результатов. После этого раздела я объясню, как работают ссылки быстрого доступа.

Как это работало

Раньше мой блог размещался на Heroku. Контент (сообщения в блогах, страницы и т. д.) был полностью статичным. Однако у сервера был динамический аспект, что означало, что я мог выполнять некоторую логику на стороне сервера.

/* match slug partial and redirect to post */
route.get('/:slug', (req, res, next) => {
  // if our slug matches *anywhere* in the slug of a post
  // find will return the *first* match, which is the
  // latest post. If there's no match, then do a normal
  // request (which might lead to a 404)
  var slug = slugs.find(
    slug => slug.includes(req.params.slug)
  );

  if (slug) {
    // blogs is a global object with all the post data
    var url = fullUrlForPost(blogs[slug]);
    return redirect(res, url);
  }

  next();
});

Приведенный выше код упрощен по сравнению с моим исходным server.js.

В поиске на стороне сервера использовался эластичный поиск (из бесплатного дополнения Бонсай через Heroku). На самом деле, после публикации нового поста в блоге также потребовалось бы обновить эластичный поисковый индекс (что было частью моего рабочего процесса, но еще одним шагом в цепочке).

Итак, если я перехожу на полностью статическое решение, как мне решить эти две функции, которые я хотел сохранить?

Решение поиска в первую очередь

Рефакторинг моего блога в значительной степени вдохновлен тем, как работает 11ty (он не использовал его напрямую из-за стержней, которые я сделал для своей спины, используя ранее Pug и Harp — но это нормально). Таким образом, я наткнулся на публикацию Фила Хоксворта о поиске на стороне клиента и черпал вдохновение в его коде, внося свои собственные улучшения.

Одно важное замечание (как указывает Фил в своем посте): форма для поиска должна поддерживаться Google или DuckDuckGo (или другим).

В настоящее время я указываю на https://www.google.co.uk/search со скрытым полем q=site:https://remysharp.com, а фактическое поле поиска называется q, и Google объединит запросы вместе. К сожалению, на момент написания DuckDuckGo не присоединяется к полям поиска (поэтому он перенаправляет на поиск всех на remysharp.com).

Части для поиска

Следуя примеру Фила, мне нужны три части:

  1. HTML-код, отображающий мою форму поиска
  2. Большой двоичный объект JSON с доступным для поиска контентом из моих сообщений в блоге.
  3. JavaScript, который будет выполнять поиск на стороне клиента (или, точнее, фильтрацию)

HTML

Мой HTML выглядит так — я не думаю, что это требует объяснений, за исключением тега script в конце:

<form id="search" action="https://www.google.co.uk/search">
  <label>Search for:
    <input id="for" autofocus="autofocus" name="q" placeholder="fragment of post..." type="text"/>
    <input type="hidden" name="q" id="q" value="site:https://remysharp.com"/>
  </label>
</form>

<ul id="search-results">
<!-- placeholder for results -->
</ul>

<script id="result-template" type="template">
  <li>
   <a href="{{url}}">{{title}}</a>
  </li>
</script>

Я использую тег script с атрибутом invliad type. Это означает, что он игнорируется браузером, поскольку я собираюсь использовать это в своем JavaScript.

Данные поиска/JSON

В моем конкретном случае я использую Pug, но цель та же: создать статический файл, содержащий (возможно, большой) дамп моих сообщений в формате JSON:

---
layout: false
permalink: /js/search-data.js
---
- var format = ({ url, output, data }) => ({
-   url, title: data.title, text: squash(_.output)
- });
- var data = JSON.stringify(collections.blog.map(format));

| var searchData = !{ data };

Результатом является файл в /js/search-data.js, содержащий:

var searchData = [{"url":"/2019/04/24/all-your-envs-in-a-row","text":"all your envs row ve used zeit s now platform ll know get environment values readable by code have jump few hoops there are solutions place can put m able keep where d expect them caveats this technique works most common cases ll proba…" /* snipped */ }]

Помните, что это не 11ty — хотя есть объект collections. Важно знать, что collections.blog — это массив сообщений в блоге, которые я написал с данными вступительной части в .data, а output в моем случае — это отрендеренный пост (а не источник, который я объясню чуть позже). ).

План состоит в том, чтобы загрузить это с помощью клиентского JavaScript и использовать данные для предоставления результатов поиска.

Сжатие текста

Важно отметить, что функция squash удаляет лишние символы и «низкокачественный» текст (такие слова, как «и», «или» и т. д.). Это тесно связано с работой Фила.

module.exports = function squash(text = '') {
  // ensure the text is
  const content = text.toLowerCase();

  // remove all html elements and new lines
  // this also ensures code blocks are removed
  // from the search results - and they make up
  // a large part of my posts
  const re = /(&lt;.*?&gt;)/gi;
  const plain = unescape(
    content
      .replace(re, '') // strip escaped code and the contents
      .replace(/<code.*<\/code>/gms, '') // strip entire code blocks
      .replace(/<\/?[^>]+(>|$)/g, '') // remove tags from around text
  );

  // remove duplicated words and duplicated spaces
  // new Set ensures unique elements in the collection,
  // then the `...` spread operator converts the set
  // to an array so it can be joined back up.
  const string = [...new Set(plain.split(/\s+/))].join(' ');

  // remove short and less meaningful words
  let result = string.replace(
    /\b(the|a|an|and|am|you|I|to|if|of|off|me|this|that|with|have|from|like|when|just|your|some|also|know|there|because|actually|recently|something)\b/gi,
    ''
  )
  .replace(/[^\w\s]/gm, ' ') // fail safe: remove non-chars & non-white space
  .replace(/\b\w{1,2}\b/gm, '') // remove any "words" of 1 or 2 characters
  .replace(/\s{2,}/gm, ' ') // compress whitespace to a single space

  // trim for good measure!
  return result.trim();
};

В приведенном выше коде есть некоторое дублирование внутри него (в регулярных выражениях), но он хорошо справляется с тем, что оставляет большую часть того, что важно.

Совет по повышению эффективности. Если вы создаете собственный файл данных поиска, потратьте немного времени, чтобы найти малозначительные слова, которые вы часто используете, и удалить их из результатов. Не копируйте мой пример выше, это всего лишь пример того, что я удаляю.

Я написал jq-запрос, который вы можете использовать, чтобы получить хорошее представление о частоте слов. Замените исходный JSON из моего примера своим собственным и настройте числа (в длине слова), чтобы понять, какие слова можно удалить. Использование этого метода позволило мне уменьшить файл данных на 70 КБ.

Поиск и приоритизация результатов

Прежде всего, сгенерированный search-data.js включается в мою страницу поиска HTML. В моем случае для (на момент написания) 485 сообщений в блоге это означает 580 КБ JavaScript (219 КБ сжато для моего посетителя).

Когда посетитель выполняет поиск, мой код проверяет его запрос на соответствие следующим критериям и дает ему «хит-пойнты»:

  • URL — 100 хитов за матч.
  • Титул — 100 хитов за матч.
  • Основной текст — 1 балл за слова длиной менее 5 символов, в противном случае хитпойнты = длина слова.

Наконец, если есть какое-либо обращение, очки за последнее время публикации добавляются. 100 баллов, разделенные на количество лет публикации (где публикации этого года — «1 год»). Я потратил некоторое время на настройку этого алгоритма, и это то, что хорошо сработало для меня.

В комментариях на моей собственной странице результатов поиска скрыты комментарии с «весом» счетчика посещений (который я показал во время тестирования), который дает вам представление о том, как это работает:

Очки жизни определяют порядок результатов. Вместо того, чтобы вываливать много JavaScript в этот пост, вы можете просмотреть мой поисковый JavaScript здесь — в частности, функция find — это место, где происходит весь подсчет хитпойнтов.

После того, как кандидаты собраны, результаты интерполируются в шаблон (тег скрипта с type="template" из предыдущего). Опять же, это живет в моем поисковом JavaScript, и, конечно же, вы можете/должны использовать свою собственную версию шаблонов.

Так это поиск.

Динамические ссылки быстрого доступа распространяются на данные поиска, и мне это нравится.

Динамические ссылки быстрого доступа

Учитывая, что я размещаю свой блог на Netlify, я могу определить свои собственные перенаправления. Если вы перейдете по несуществующему URL-адресу, вы нажмете моя пользовательская страница 404.

Это источник моей страницы 404:

script.
  const data = !{ JSON.stringify(collections.blog.map(_ => ({ slug: _.slug, url: _.url }))) }
  const pathname = window.location.pathname.split('/').pop().toLowerCase();
  const match = data.find(_ => _.slug.includes(pathname));
  if (match) window.location = match.url;

h1 Redirecting...

script.
  if (!match) document.getElementsByTagName('h1')[0].innerText = 'Four oh four...'

// rest of my "normal" 404 page here

Что это делает? Когда страница загружается, JavaScript немедленно срабатывает и проверяет, является ли URL-адрес (только путь) частичным совпадением любого слага для моих сообщений в блоге. Помните, что .find для массивов JavaScript возвращает первый результат (и мой collections.blog упорядочивается самым последним первым).

Если есть совпадение, JavaScript немедленно перенаправляет — и, поскольку JavaScript блокирует, он предотвращает появление остальной части страницы 404 (которая показывает список моих последних сообщений).

Если совпадений нет, заголовок h1 меняется на мой заголовок Четыре о четыре… (да, неудачная шутка!). Если вы проверите исходный код страницы 404, вы увидите, что он переполнен JSON. Он занимает 13 КБ со всем этим JSON, что не так уж ужасно и сравнимо с любым изображением в моем блоге.

Перенаправление Netlify, которое я использую, также относительно прямолинейно (и, вероятно, рекомендуется):

/* /404.html 404

Ну это все. Теперь вы можете перейти к последнему сообщению в случае сбоя, введя «remysharp.com слэш fail» в URL-адрес браузера, и он будет перенаправлен на правильный пост.

Все статично, с помощью удивительной функции перенаправления Netlify.

Первоначально опубликовано в журнале Remy Sharp’s b:log