댓글 검색 목록

[Nodejs] Firebase 및 Cloudflare 작업자를 사용하는 봇 및 크롤러 용 동적 메타 태그

페이지 정보

작성자 운영자 작성일 21-03-11 17:40 조회 1,058 댓글 0

봇과 크롤러에 동적 메타 태그를 제공하는 방법에는 여러 가지가 있으며, 대부분은 SSR, 사전 렌더링 또는 정적 사이트 생성 경로에 상관없이 자체 서버를 관리하는 것과 관련됩니다. 하지만 서버를 관리하거나 SSR 프레임 워크에 고정하지 않으려는 경우 다른 옵션이 있습니까?


이것이 제가 최근에 Firebase를 사용하여 프로젝트 작업을 할 때 발견 한 직책입니다. Firebase 호스팅에 클라이언트 측 렌더링 앱이 있습니다. 훌륭하고 빠르고 안정적입니다. 문제는 사용자가 사이트 크롤러에서 페이지를 공유 할 때 메타 태그를 긁어 낼 수 없다는 것입니다. 그래서 저는 이러한 메타 태그를 제공 할 방법을 찾아야 했고 이상적으로는 SSR에 의존하거나 서버를 관리하지 않고 이를 수행하고 싶었습니다.


사전 렌더링이 좋은 해결책이 될 것입니다. 사용자 에이전트를 기반으로 들어오는 요청을 라우팅하고 사용자는 CDN에서 사이트를 가져오고 봇은 사전 렌더링 서비스로 라우팅 될 수 있습니다. 처음에는 Cloud Function 또는 Lambda가 이에 접근하는 좋은 방법 인 것처럼 보이므로 들어오는 요청의 사용자 에이전트를 확인하고 그에 따라 라우팅 할 수 있습니다. 그러나 서버리스와의 상충 관계가 있으며 이 경우 콜드 스타트입니다. 서버리스를 처음 사용하는 경우 콜드 스타트는 플랫폼이 사용되지 않을 때 코드를 스핀 다운하는 것입니다. 워크로드가 일관성이 없으면 이런 일이 발생합니다. 문제는 새로운 요청이 들어오고 플랫폼이 코드를 다시 로드하고 초기화해야 할 때 발생합니다. 콜드 스타트는 서버가 수신 요청을 처리 할 준비가 되기 전에 사용자가 몇 초 (5 이상)를 기다리게 할 수 있으며, 이는 이 사용 사례에서는 허용되지 않습니다.


AWS는 콜드 스타트를 완화하기 위해 Lambda 함수에 대해 "프로비저닝 된 동시성"이라는 기능을 제공합니다. 기본적으로 여러 기능을 연중 무휴로 "따뜻하게"유지하기 위해 비용을 지불 할 수 있지만, 서버리스의 전체 요점을 무너 뜨리는 저에게는 맞습니까? 사용한 만큼만 지불하는 이점과 수요에 맞춰 즉시 확장 할 수 있는 기능이 사라졌습니다.

콜드 스타트가 있는 것은 너무 느리기 때문에 문제가 되지 않습니다. Cloudflare 작업자를 입력하십시오. Cloudflare 작업자는 GCP 및 AWS 서버리스 제품과 다르지만 용도도 다릅니다. Node를 실행하거나 VM을 회전하지 않기 때문에 Cloudflare는 0ms 콜드 시작 시간을 광고하면서 Cloudflare CDN의 155 개 위치에 배포 할 수 있습니다. 이렇게 하면 들어오는 요청을 라우팅 하는 것과 같은 목적으로 Cloud Functions / Lambda를 사용할 때 발생하는 두 가지 고정 지점이 완화됩니다.


작업자에는 몇 가지 제한이 있습니다. 예를 들어 프리 티어에서는 호출 당 10ms 만 실행됩니다. 그러나 작업자를 역방향 프록시로 쉽게 사용하여 사용자 에이전트가 자신을 식별하고 이를 기반으로 올바른 자산을 제공하는 방법을 확인할 수 있습니다. 그러니 해봅시다.


Firebase 호스팅에 도메인을 추가하는 대신 Cloudflare에 추가했습니다. Cloudflare를 DNS로 사용하고 있으며 역방향 프록시 역할을 하는 작업자를 통해 요청을 라우팅했습니다.


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와 같은 서비스로 보낼 수 있지만 Cloud Functions는 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
): 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 <= 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 호스팅에 요청하고 결과 HTML을 렌더링하고 탭을 닫습니다. 좀 더 똑똑해 지려면 Firestore의 모든 요청을 캐싱하여 미리 렌더링 된 항목이 있을 때 응답 속도를 높일 수 있습니다. 내 캐시 기간은 현재 1 일로 설정되어 있으며 사용 사례에 맞는 콘텐츠가 업데이트 되면 프로그래밍 방식으로 캐시의 경로를 플러시 할 수도 있습니다.


어떻게 수행합니까? 글쎄, 그것은 실제로 꽤 잘 작동하고, 페이지가 캐시 되지 않았을 때 함수에 대한 따뜻한 요청은 1.5 초만큼 낮습니다. 최악의 경우에는 요청이 최대 8 초까지 걸리는 것을 보았습니다. 캐시 된 경우 몇 초가 걸리지 만 비과학적인 테스트에서 봇의 시간이 초과되지 않았으므로 함께 살 수 있습니다. 그것. 응답 시간을 더 개선하기 위해 누군가가 내 사이트에서 공유 버튼을 클릭 할 때 함수를 호출하면 함수가 워밍업 되고 크롤링 되기 전에 해당 페이지를 캐시하므로 이러한 요청이 꽤 성능이 좋습니다.


궁극적으로 저는 이 솔루션이 제 사용 사례에 매우 잘 작동한다고 생각합니다. 봇의 콜드 스타트가 당신이 살 수 없는 것이 있다면 아마도 prerender.io 유형의 서비스가 당신의 요청을 가리키고 싶을 수도 있습니다. 또는 자신의 서버를 관리해야 할 수도 있습니다.


https://richard-0094.medium.com/dynamic-meta-tags-for-bots-and-crawlers-using-firebase-and-cloudflare-workers-b351cd7045db


댓글목록 0

등록된 댓글이 없습니다.

웹학교 로고

온라인 코딩학교

코리아뉴스 2001 - , All right reserved.