Привет! Меня зовут Анна, я JS-разработчик в компании SimbirSoft. Сегодня расскажу об интересном кейсе на одном из наших проектов, а именно, речь пойдет об отображении favicon сторонних сайтов в нашем приложении.
Это не крипта! favicon (от favourite icon) — маленькая картинка, символ страницы в сети, который обычно присутствует на заголовке вкладки браузера.
Зачем ее добывать? Мы будем говорить о ситуации, когда нам надо отображать ссылки, которые приложил пользователь нашего продукта. Например, когда у пользователя есть возможность прикладывать ссылки к сообщению или к профилю, эти ссылки надо как-то прилично отобразить.
Какие у нас варианты?
Отображать ссылку “as is” — много-много букв, зато сразу видно, куда пойдем (подходит для тех, кто любит буквы рассматривать).
Маскируем ссылку коротким описанием — распространенный подход, который используется в текстовых редакторах в том числе.
Маскируем ссылку картинкой — я отделила этот подход от предыдущего пункта, так как для единообразия нужно определиться, как будут выглядеть у нас ссылки - как тексты или как картинки.
Выводим набор: текст + картинка (favicon) — вариант, который улучшает UX. В этом случае пользователю легче воспринимать информацию, если есть графический элемент, и понятнее, если есть текст.
Отображаем превью ссылки — как в крупных социальных сетях. Иногда мы видим буквально скрин страницы, на которую ведет ссылка.
В статье мы подробно поговорим о предпоследнем пункте как наиболее сбалансированном варианте по критерию «цена-качество». В конце будет ссылка на плейграунд, в котором вы можете затестить готовое решение.
Представим ситуацию: вы владелец какого-либо продукта и хотите реализовать в нем подобное отображение ссылок. Ну или вы фронтендер, которому бизнес поставил задачу отображать ссылки как-то так:
В таком контексте добыча favicon нужна, чтобы:
украсить UI (а настоящая красота функциональна - пользователи получают представление о содержании ссылки и возможность воспринять ее через релевантное графическое изображение);
улучшить UX (а хороший пользовательский опыт - это любовь к продукту);
сберечь ресурсы (тут нечего уточнять:)).
В поисках решения у меня сразу возникла мысль: надо отправлять запрос по адресу ссылки и получать метаданные — те данные, которые заполняют в теге <head> для сайтов-поисков. Погуглив, наткнулась на статью, где описывается кейс, как человек решал вопрос отображения превью ссылок. В материале говорится о том, что в википедии есть собственный API для получения превью, а также, что существует специальный протокол, созданный для отображения превью ссылок. Автор статьи попробовал с фронта получить данные для отображения ссылки (fetch) и уперся в CORS — большинство сайтов не отдадут данные из-за политики безопасности.
Изучив вопрос с npm-пакетами, я подумала, что решать эту задачу на фронте дороговато. Предложила команде другой вариант: отправляем ссылку на сервер → бэк ее обрабатывает, после чего присылает нам name и thumbnail → мы фронте их красиво отображаем. Но из-за высокой загрузки бэка мы решили, что вместо метаданных нам будет достаточно получить только favicon. Название при этом пользователь пишет сам. На этом решении и остановились.
Почему получение favicon дешевле получения метаданных? Потому что favicon принято размещать по тому же URL, где лежит index.html. Поэтому ссылкой на эту картинку будет url ссылки (его доменная часть) с добавлением ‘/favicon.ico’. То есть задача сводится к получению строки адреса картинки, а не к отправке запроса и получению данных.
Итак, надо извлечь из строки часть, которая содержит протокол и доменное имя.
Если загуглить этот вопрос, перед нами откроются необозримые горизонты обсуждения темы «распарсить URL». Я думала только о regexp и new URL, но можно, оказывается, использовать createElement (не буду здесь освещать этот способ, но он похож на использование new URL, в этом видео все об этих трех способах).
В сети можно найти такие примеры регулярного выражения:
const re = new RegExp('(https|http)://.*(com|ru|org|en)/');
if (re.test(value)) {
setFavicon(`${re.exec(value)[0] ?? ''}favicon.ico`);
}
Именно с этого варианта я начала писать функцию получения favicon. Он вполне рабочий, но понятно, например, что вот эта часть регулярки: (com|ru|org|en) исключает прохождение URL с другими доменами (dev, uk и т.п.).
Тогда я обратилась к конструктору new URL и написала свою функцию таким образом:
const url = new URL(value);
if (url.origin) {
const favicon = `${url.origin}/favicon.ico`;
setFavicon(favicon);
}
Несмотря на то, что в сети есть статьи о неидеальности работы конструктора new URL, мне его функциональность показалась достаточной. Я стала тестировать свою функцию на разных строках, и тут меня ждал один сюрприз: если строка не распарсится в URL случится ошибка, и все упадет ай-яй-яй!
С regexp такой неприятности не произойдет, но решить проблему нетрудно, оборачиваем блок добычи favicon в try-catch:
try {
const url = new URL(value);
if (url.origin) {
const favicon = `${url.origin}/favicon.ico`;
setFavicon(favicon);
}
} catch {
console.log(`Ошибка при получении иконки адреса ${value}`);
}
Я думала на этом всё, а оказалось — нет. Строка может распарситься в URL, мы получим ссылку на favicon, но картинки там не будет. Не положили!
Что тогда будет отображено на фронте? Вот такая кракозябра:
В связи с этим вспоминаем HTML, атрибуты тегов: у тега img есть возможность обрабатывать ошибки полученных изображений, функция onerror. Я в ней скидываю favicon до пустой строки и, таким образом, выводится заглушка.
const preview = favicon ? (
<img
src={favicon}
alt="link logo"
onError={() => {
setFavicon('');
}}
/>
) : (
<FontAwesomeIcon icon="link" />
);
Подводя итог, хочу сказать, что описанный способ отображения приложенных ссылок серьезно улучшает UX:
ссылки имеют уникальный графический элемент,
информация выделяется,
пользователю «красиво и приятно»,
он получает позитивный эмоциональный отклик.
А для нас — разработчиков — это практически ничего не стоит, потому что наш проект не увеличивает кодовую базу и затрачиваемые ресурсы. Одни плюсы и никаких минусов. Пишите в комментарии свое мнение и делитесь опытом:)
Спасибо за внимание!
Больше авторских материалов для frontend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.
Комментарии (9)
delphinpro
04.09.2024 14:12Как насчет того, чтобы парсить код страницы по адресу, тащить оттуда link rel=icon?
Тут уже можно учитывать атрибуты size, если есть в нескольких размерах.Конечно, есть минусы. Такой вариант лучше переложить на плечи бэка. И тут опять возникают варианты - добывать иконку один раз, при добавлении ссылки, кэшировать у себя. Но тогда она не изменится, если целевой сайт ее поменяет (хотя это редко бывает). Добывать при каждом запросе - накладно. Пробовать обновлять по расписанию. Ну тоже лишние расходы.
Ваш вариант хорош тем, что он прост как валенок =) и не создает лишней нагрузки. Но и работает не в 100% случаев (когда приходится отображать дефолтную иконку). Еще размер картинки - обычно в корень кладут мелкую иконку 16*16, и если нужно отображать крупнее, будет не очень.
SSul
04.09.2024 14:12Спасибо, что так погрузились! Про «парсить код страницы» — это возможно после того, как ты этот код получишь, а как его получить? Опять запрос, от них отказались. Насчет кэшировать — пока считается, что пользователи пользуются нашим приложением не для хранения ссылок, так что по идее нагрузки большой не должно быть, если появится необходимость, будем думать. Маленький размер картинки тоже пока устраивает :)
valery1707
04.09.2024 14:12Потому что favicon принято размещать по тому же URL, где лежит index.html. Поэтому ссылкой на эту картинку будет url ссылки (его доменная часть) с добавлением ‘/favicon.ico’.
В целом может конечно и принято, но полагаться на это всё равно нельзя - в итоге иконка может лежать как и вовсе совсем не там, так и как бы там, но с указанием размеров в имени файла для поддержки браузеров, умеющих в красивые иконки и вы её по простому имени всё равно не обнаружите.
SSul
04.09.2024 14:12Спасибо за комментарий! Действительно, часто иконку не находим, но для наших нужд этого пока хватает. А вот искать иконку с указанием размеров — это идея! Уже думаю :)
Hidadmin
04.09.2024 14:12+1А с чего вы взяли, что все сайты используют только /favicon.ico + при этом размещают его в корне сайта?
favicon.ico - устаревший формат указания иконки сайта.
Сейчас последние версии браузеров поддерживают вывод фавикон в форматах png, gif, jpeg, svg и располагаться они могут где угодно, по любому пути.
Пример:
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="icon" sizes="192x192" href="/i/android-touch-icon-192x192.png">
<link rel="icon" sizes="128x128" href="/i/android-touch-icon-128x128.png">По моему мнению проще отдавать иконку из базы того же Яндекса - с большой долей вероятности, если сайт в индексе, то его иконка также будет проиндексирована, в каком бы формате она не была бы указана на сайте. И при этом не нужно ничего парсить.
Пример:
<span style="background-image: url('https://favicon.yandex.net/favicon/site-analyzer.ru');width: 16px;height: 16px;padding-right: 16px;">
SSul
04.09.2024 14:12Спасибо, работает! Пока работала над этой задачей, тестировала на собственных ссылках, и всегда открытая фигма как раз иконку не отдает :) У них иконки лежат по другому урлу —static.figma.com.
Вашим способом фигмовскую иконку я добыла :) Правда, страшненькая, маленькая, но все-таки! Это хороший повод поискать еще несколько вариантов добычи иконок и написать вторую часть статьи :)
krasov_rf
При таком подходе, например если ссылка с фавиконом придет в личные сообщения, запрос ведь улетит на фавиконку, вместе с IP адресом просмотревшего пользователя? Соответственно заиграет фактор "я тебя по ип пробью".
SSul
В нашем проекте нет такой функции, чтобы обмениваться личными сообщениями. Если появится такая тема, я обязательно учту этот момент, спасибо за то, что обратили внимание :)