При переносе моего блога с 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).
Части для поиска
Следуя примеру Фила, мне нужны три части:
- HTML-код, отображающий мою форму поиска
- Большой двоичный объект JSON с доступным для поиска контентом из моих сообщений в блоге.
- 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 = /(<.*?>)/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