Динамические метатеги для ботов и сканеров с помощью Firebase и Cloudflare Workers

100-4327010

От создателя: существует огромное количество методов предоставления динамических метатегов ботам и поисковым сканерам, большая часть из которых соединено с управлением вашими своими серверами, будь то SSR, подготовительный рендеринг либо создание статических веб-сайтов. Но что, если вы не желаете управлять сервером либо подключаться к платформе SSR, есть ли иной вариант?

Это ситуация, в какой я не так давно оказался.

Подготовительный рендеринг был бы неплохим решением; маршрутизировать входящие запросы на базе пользовательского агента, юзеры получают веб-сайт из CDN, а боты могут быть перенаправлены на службу подготовительного рендеринга. Сначала пасмурная функция либо лямбда кажется неплохим методом реализации этого, мы можем обработать пользовательские входящие запросы и подходящим образом навести их. Но есть соглашение с бессерверным режимом, и в этом случае это прохладный пуск. Если вы новичок в бессерверном режиме, прохладный пуск — это когда платформа замедляет ваш код, когда он не употребляется; это произойдет, если ваша рабочая перегрузка непостоянна. Неувязка возникает, когда поступает новейший запрос, и платформе нужно опять загрузить и инициализировать код, что происходит медлительно. При прохладном запуске юзеры могут ожидать несколько секунд (5 либо наиболее), до этого чем сервер будет готов обработать входящий запрос, что в этом случае неприемлемо.

AWS дает так именуемый «Provisioned Concurrency» для Lambda Functions, чтоб уменьшить прохладный пуск. На самом деле, вы сможете заплатить за то, чтоб некие функции оставались «теплыми» 24 часа в день, 7 дней в недельку, но для меня это лишает смысла бессерверность, не так ли? Преимущество оплаты лишь за то, что вы используете, и возможность моментального масштабирования в согласовании со спросом, сейчас пропали.

Ни о каком прохладном старте не быть может и речи, поэтому что он очень неспешный. Перебегайте на Cloudflare Workers. Cloudflare Workers различаются от бессерверных предложений GCP и AWS, но у их также есть и для иная цель. Они не запускают Node и не раскручивают виртуальную машинку, потому это дозволяет Cloudflare иметь объявленное время прохладного пуска 0 мс при развертывании в 155 местах в Cloudflare CDN. Это смягчает две препядствия, которые у меня появляются при использовании пасмурных функций / лямбда для таковых целей, как маршрутизация входящих запросов.

У воркеров есть некие ограничения, к примеру, на уровне бесплатного использования вы получаете лишь 10 мсек за один вызов. Но можно просто применять Worker в качестве оборотного прокси, чтоб проверить, как пользовательский агент идентифицирует себя и на базе этого будет показывать правильные представления. Итак, давайте создадим это.

Новая стратегия популяризации поискового продвижения от Google

Заместо того, чтоб добавлять домен в Firebase Hosting, я добавил его в Cloudflare. Я использую Cloudflare в качестве DNS и перенаправляю запросы через Worker, который действует как оборотный прокси.

JavaScript const userAgents = [ “googlebot”, “Yahoo! Slurp”, “bingbot”, “yandex”, “baiduspider”, “facebookexternalhit”, “twitterbot”, “rogerbot”, “linkedinbot”, “embedly”, “quora link preview”, “showyoubot”, “outbrain”, “pinterest/0.”, “developers.google.com/+/web/snippet”, “slackbot”, “vkShare”, “W3C_Validator”, “redditbot”, “Applebot”, “WhatsApp”, “flipboard”, “tumblr”, “bitlybot”, “SkypeUriPreview”, “nuzzel”, “Discordbot”, “Google Page Speed”, “Qwantify”, “pinterestbot”, “Bitrix link preview”, “XING-contenttabreceiver”, “Chrome-Lighthouse”,

];

/** * Detect whether the user agent string matches that of a known bot * @param {string} userAgent */ export default (userAgent) => { return userAgents.some( (crawlerUserAgent) => userAgent.toLowerCase().indexOf(crawlerUserAgent.toLowerCase()) !== -1 );

};

12345678910111213141516171819202122232425262728293031323334353637383940414243444546 const userAgents = [  “googlebot”,  “Yahoo! Slurp”,  “bingbot”,  “yandex”,  “baiduspider”,  “facebookexternalhit”,  “twitterbot”,  “rogerbot”,  “linkedinbot”,  “embedly”,  “quora link preview”,  “showyoubot”,  “outbrain”,  “pinterest/0.”,  “developers.google.com/+/web/snippet”,  “slackbot”,  “vkShare”,  “W3C_Validator”,  “redditbot”,  “Applebot”,  “WhatsApp”,  “flipboard”,  “tumblr”,  “bitlybot”,  “SkypeUriPreview”,  “nuzzel”,  “Discordbot”,  “Google Page Speed”,  “Qwantify”,  “pinterestbot”,  “Bitrix link preview”,  “XING-contenttabreceiver”,  “Chrome-Lighthouse”,]; /** * Detect whether the user agent string matches that of a known bot * @param {string} userAgent */export default (userAgent) => {  return userAgents.some(    (crawlerUserAgent) =>      userAgent.toLowerCase().indexOf(crawlerUserAgent.toLowerCase()) !== -1  );};

JavaScript import isBot from “./isBot”;

const publicDomain = “your-domain.com”; const upstreamDomain = “your-project.web.app”; const prerenderEndpoint =

“https://us-central1-your-project.cloudfunctions.net/prerender”;

/** * Handles the incoming request * @param {Request} request */ async function handleRequest(request) { const { method, headers, url } = request;

const userAgent = request.headers.get(“user-agent”);

let fetchUrl = “”;

// Set the fetch URL based on the result of isBot if (isBot(userAgent)) { // Extract the path segment of the domain and append it as a query param to // the upstream prerender endpoint const path = url.split(publicDomain).pop(); fetchUrl = `${prerenderEndpoint}?path=${encodeURI(path)}`; } else { // replace the publicDomain with the upstreamDomain fetchUrl = url.replace(publicDomain, upstreamDomain);

}

return fetch(fetchUrl, { method, headers, });

}

addEventListener(“fetch”, (event) => { event.respondWith(handleRequest(event.request));

});

12345678910111213141516171819202122232425262728293031323334353637 import isBot from “./isBot”; const publicDomain = “your-domain.com”;const upstreamDomain = “your-project.web.app”;const prerenderEndpoint =  “https://us-central1-your-project.cloudfunctions.net/prerender”; /** * Handles the incoming request * @param {Request} request */async function handleRequest(request) {  const { method, headers, url } = request;  const userAgent = request.headers.get(“user-agent”);   let fetchUrl = “”;   // Set the fetch URL based on the result of isBot  if (isBot(userAgent)) {    // Extract the path segment of the domain and append it as a query param to    // the upstream prerender endpoint    const path = url.split(publicDomain).pop();    fetchUrl = `${prerenderEndpoint}?path=${encodeURI(path)}`;  } else {    // replace the publicDomain with the upstreamDomain    fetchUrl = url.replace(publicDomain, upstreamDomain);  }   return fetch(fetchUrl, {    method,    headers,  });} addEventListener(“fetch”, (event) => {  event.respondWith(handleRequest(event.request));});

Массив строк пользовательского агента и условное выражение взяты из пакета prerender-node, который является промежным программным обеспечением экспресс-обработки с целью проверки, является ли пользовательский агент ботом для маршрутизации входящих запросов в службу подготовительной визуализации.

Это работает весьма отлично, входящие запросы от юзеров весьма стремительно обрабатываются из Cloudflare / Гугл CDN.

2-ая часть — настроить пред-рендерер. Мы могли бы выслать его в службу вроде prerender.io, но пасмурные функции поддерживают Puppeteer прямо из коробки, и это еще не конец света, если бот должен ожидать прохладного пуска, пока он не истечет. Puppeteer может за ранее отрисовать нашу страничку и возвратить строчку HTML.

JavaScript import { db, functions } from “@/admin.config”;

const upstreamDomain = “your-project.web.app”;

%MINIFYHTML57b7cef9817bfd29c06ff9db4056115d20% %MINIFYHTML57b7cef9817bfd29c06ff9db4056115d21%

const cacheDurationSeconds = 86400;
const cacheDurationMilliSeconds = cacheDurationSeconds * 1000;

const prerenderFunction = async ( req: functions.https.Request, res: functions.Response ): Promise => {

const chromium = (await import(“chrome-aws-lambda”)).default;

const browserPromise = chromium.puppeteer.launch({ args: chromium.args, defaultViewport: chromium.defaultViewport, executablePath: await chromium.executablePath, headless: chromium.headless, ignoreHTTPSErrors: true,

});

const prerenderCacheReference = db.collection(“prerenderCache”);

// Decode the path from the query const encodedPath = req.query.path as string;

const path = decodeURI(encodedPath);

// Check if the requested page exists in the cache const prerenderCachePathQuerySnapshot = await prerenderCacheReference .where(“path”, “==”, path)

.get();

// If the page is in the cache if (!prerenderCachePathQuerySnapshot.empty) {

const { html, date } = prerenderCachePathQuerySnapshot.docs[0].data();

// Check cache age
const cacheAge = Date.now() – date;

if (cacheAge { res.set(“Access-Control-Allow-Origin”, “*”); await prerenderFunction(req, res);

});

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 import { db, functions } from “@/admin.config”; const upstreamDomain = “your-project.web.app”; const cacheDurationSeconds = 86400;const cacheDurationMilliSeconds = cacheDurationSeconds * 1000; const prerenderFunction = async (  req: functions.https.Request,  res: functions.Response): Promise => {  const chromium = (await import(“chrome-aws-lambda”)).default;   const browserPromise = chromium.puppeteer.launch({    args: chromium.args,    defaultViewport: chromium.defaultViewport,    executablePath: await chromium.executablePath,    headless: chromium.headless,    ignoreHTTPSErrors: true,  });   const prerenderCacheReference = db.collection(“prerenderCache”);   // Decode the path from the query  const encodedPath = req.query.path as string;  const path = decodeURI(encodedPath);   // Check if the requested page exists in the cache  const prerenderCachePathQuerySnapshot = await prerenderCacheReference    .where(“path”, “==”, path)    .get();   // If the page is in the cache  if (!prerenderCachePathQuerySnapshot.empty) {    const { html, date } = prerenderCachePathQuerySnapshot.docs[0].data();     // Check cache age    const cacheAge = Date.now() – date;     if (cacheAge {    res.set(“Access-Control-Allow-Origin”, “*”);    await prerenderFunction(req, res);  });

Функция открывает новейшую вкладку в Chrome без заголовка, делает запрос к Firebase Hosting, показывает приобретенный HTML и закрывает вкладку. Чтоб быть незначительно хитрее, я кэширую любой запрос в Firestore, чтоб убыстрить ответ, когда что-то уже было за ранее обработано. Срок деяния кеша в истинное время установлен на 1 денек, вы также сможете программно очищать маршруты в кеше при обновлении содержимого, независимо от варианта использования.

Как это работает? Что ж, по сути это работает весьма отлично, жаркие запросы к функции занимают всего 1,5 секунды, когда страничка не кешируется, что достаточно уместно. В худших вариантах я лицезрел, что запросы занимают до 8 секунд, что не весьма отлично, если они кешируются, но в моих ненаучных тестах не истекло время ожидания ни 1-го из ботов. Чтоб еще более сделать лучше время отклика, я вызываю функцию, когда кто-то надавливает клавишу «Поделиться» на моем веб-сайте, это подогревает функцию, также кэширует эту страничку до того, как она будет просканирована, потому эти запросы достаточно эффективны.

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

Создатель: Richard Cooke

Редакция: Команда webformyself.

Источник