От автора: существует множество способов предоставления динамических метатегов ботам и поисковым сканерам, большинство из которых связано с управлением вашими собственными серверами, будь то 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 в качестве обратного прокси, чтобы проверить, как пользовательский агент идентифицирует себя и на основе этого будет отображать правильные представления. Итак, давайте сделаем это.
Вместо того, чтобы добавлять домен в Firebase Hosting, я добавил его в Cloudflare. Я использую Cloudflare в качестве DNS и перенаправляю запросы через Worker, который действует как обратный прокси.
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 ); };
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 / Google CDN.
Вторая часть — настроить пред-рендерер. Мы могли бы отправить его в службу вроде prerender.io, но облачные функции поддерживают Puppeteer прямо из коробки, и это еще не конец света, если бот должен ждать холодного запуска, пока он не истечет. Puppeteer может предварительно отрисовать нашу страницу и вернуть строку HTML.
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<string> ): Promise<void> => { 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 <= cacheDurationMilliSeconds) { // Send cached HTML back res.status(200).send(html); const browser = await browserPromise; await browser.close(); return; } // Else remove the page from cache and continue execution const { id } = prerenderCachePathQuerySnapshot.docs[0]; prerenderCacheReference.doc(id).delete(); } // Prerender the page const browser = await browserPromise; const page = await browser.newPage(); await page.goto(`https://${upstreamDomain}${path}`, { waitUntil: "networkidle2", }); const html = await page.content(); // serialize HTML const pageClosePromise = page.close(); // Add the new page to the cache const pageCachePromise = prerenderCacheReference.add({ html, path, date: Date.now(), }); await Promise.all([pageClosePromise, pageCachePromise]); res.status(200).send(html); return; }; export const prerender = functions .runWith({ memory: "2GB" }) .https.onRequest(async (req, res) => { res.set("Access-Control-Allow-Origin", "*"); await prerenderFunction(req, res); });
Функция открывает новую вкладку в Chrome без заголовка, делает запрос к Firebase Hosting, отображает полученный HTML и закрывает вкладку. Чтобы быть немного хитрее, я кэширую каждый запрос в Firestore, чтобы ускорить ответ, когда что-то уже было предварительно обработано. Срок действия кеша в настоящее время установлен на 1 день, вы также можете программно очищать маршруты в кеше при обновлении содержимого, независимо от варианта использования.
Как это работает? Что ж, на самом деле это работает очень хорошо, горячие запросы к функции занимают всего 1,5 секунды, когда страница не кешируется, что довольно разумно. В худших случаях я видел, что запросы занимают до 8 секунд, что не очень хорошо, если они кешируются, но в моих ненаучных тестах не истекло время ожидания ни одного из ботов. Чтобы еще больше улучшить время отклика, я вызываю функцию, когда кто-то нажимает кнопку «Поделиться» на моем сайте, это подогревает функцию, а также кэширует эту страницу до того, как она будет просканирована, поэтому эти запросы довольно эффективны.
В конечном счете, я думаю, что это решение работает очень хорошо для моего варианта использования, если вы не можете смириться с холодным запуском для ботов, возможно, вы захотите направить свои запросы на службу типа prerender.io. Или, возможно, вам действительно нужно управлять собственным сервером.
Автор: Richard Cooke
Источник: richard-0094.medium.com
Редакция: Команда webformyself.