Краулінг мета-тегів для React SPA за допомогою Express та Playwright: рішення на стороні сервера

pic

Зображення згенероване за допомогою Dall-E 3

Ви використовуєте React для створення вашого односторінкового застосунку і успішно створили блог, опублікувавши його на вибраному сервері. Все здається правильним, але ось ви створюєте свою статтю і публікуєте її на сайті, а потім вирішуєте поділитися статтею в соціальних мережах — таких як Facebook, Twitter, Instagram тощо — для підвищення залученості і ділитесь нею. Ви думаєте, що все йде добре, правда? Але після того, як ви поділилися статтею, виявляєте, що вона не відображає відповідну соціальну картку при публікації, а лише показує мета-заголовок, опис та зображення з файлу index.html, хоча насправді вона мала б показувати заголовок статті, опис та обкладинку статті. Ви шукаєте в Google своє питання і знаходите посилання для перевірки попереднього перегляду соціального контенту, де ви бачите саме те, що отримуєте при публікації в соціальних мережах. І ви знайшли рішення: використовувати react-helmet-async для динамічних мета-тегів HTML на різних сторінках. Врешті ви додаєте код Helmet на сторінку з деталями статті, як показано нижче:

const export YourArticleDetailPage = () => {  
 return (  
 <>  





 // решта вашого JSX коду статті  

 )  
}

І після цієї зміни ви перевіряєте в браузері і радієте, бо мета-теги динамічно завантажуються у вашому HTML head. Час відправити нові зміни на сервер і сподіватися, що соціальний веб-краулер знайде ваші динамічні теги та відобразить соціальну картку як очікується. І ось ви знову ділитеся статтею в соціальних мережах і виявляєте, що це не працює так, як мало б. Ви зробили все правильно, правда? Це мало б працювати, не так ли? Що пішло не так?

Насправді, коли ви тестували локально, код виконувався у вашому браузері, і браузер завантажував усі необхідні JS та CSS для вашої сторінки та виконувся код react-helmet-async, який змінював мета-теги в head. Тобто react-helmet-async працює правильно. Але проблема полягає в тому, що коли соціальний веб-краулер сканує вашу React-сторінку, він спочатку отримує файл index.html з його вмістом, і краулер не може виконати JavaScript код, саме тому динамічні мета-теги react-helmet-async не завантажуються під час візиту краулера.

pic

Фото Володимира Грищенка на Unsplash

Врешті-решт ви розумієте, що сталося, і ось рішення. Ви не можете використовувати будь-які JavaScript-пакети для зміни виводу вашого HTML в React і зміни мета-тегів head — для цього ви вже використовували react-helmet-async, і жоден інший пакет не дасть іншого результату. Отже, рішення цієї проблеми є такими:

  1. Створіть сервер на express js і надайте контент, що рендериться на сервері, для ботів, павуків, краулерів тощо. Це можна налаштувати швидко і як тимчасове рішення.
  2. Ви можете перейти на бібліотеку для рендерингу на сервері, таку як remix, nextjs або gatsby, якщо у вас є час і ви хочете змінити підхід для більш стабільного і потужного розроблення продукту.
  3. Ви можете використовувати онлайн-сервіси для індексації React-сторінок, такі як prerender.io, якщо ви готові заплатити.

У цій статті ми розглянемо перше рішення для швидкого результату, але я б рекомендував рішення 2 для кращого та більш стабільного підходу. Щоб створити сервер на Express, вам не потрібно бути експертом в Express.js — ми просто створимо сервер і будемо подавати наш вже готовий файл через сервер Express.
Отже, давайте вирішимо нашу проблему.

Необхідні умови

Що нам потрібно для створення фальшивого SSR за допомогою express js:

  • express для створення HTTP сервера для обслуговування контенту.
  • playwright або puppeteer, в залежності від вашого вибору, ви також можете використовувати selenium або будь-який інший пакет, який може завантажити веб-браузер на сервері. Це основна частина, яка необхідна для рендерингу контенту на сервері і повернення його назад.

Playwright і puppeteer насправді є фреймворками для тестування веб-додатків і автоматизації (як це описано в документації playwright). Але для наших цілей вони є ідеальними бібліотеками, щоб імітувати наш SSR і передавати контент, який рендериться на сервері, ботам і краулерам.

Налаштування

Спочатку встановіть необхідні пакети:

npm install express playwright // якщо ви використовуєте playwright  

npm install express puppeteer // якщо ви використовуєте puppeteer

Після того, як пакети будуть успішно встановлені, створіть файл server.js у корені вашої проектної директорії і додайте наступний код:

import express, { static as staticpath } from "express";  
import { chromium } from"playwright";  
import { join } from "path";  
import { fileURLToPath } from "url";  
import path from "path";  

const app = express();  
const PORT = 3000;  

const __filename = fileURLToPath(import.meta.url);  
const __dirname = path.dirname(__filename);  

// Обслуговуємо ваші файли збірки   
const BUILD_PATH = join(__dirname, "build"); // Налаштуйте шлях, якщо потрібно  
app.use(staticpath(BUILD_PATH));  

// Проміжне ПЗ для попереднього рендерингу  
app.use(async (req, res, next) => {  
 const userAgent = req.headers["user-agent"];  

 // Перевіряємо, чи запит від бота  
 const isBot = /bot|crawler|spider|crawling|facebookexternalhit\/*|facebot|Twitterbot\/*|LinkedInBot\/*|GuzzleHttp\/7|WhatsApp\/*/i.test(userAgent);  
 if (isBot) {  
 try {  
 const browser = await chromium.launch({ headless: true });  
 const context = await browser.newContext();  
 const page = await context.newPage();  
 const url = `http://localhost:${PORT}${req.originalUrl}`;  
 await page.goto(url, { waitUntil: "networkidle0" });  

 const html = await page.content();  
 await browser.close();  

 res.send(html);  
 } catch (err) {  
 console.error("Помилка Puppeteer:", err);  
 res.status(500).send("Помилка при попередньому рендерингу сторінки");  
 }  
 } else {  
 next(); // Пропускаємо інші запити  
 }  
});  

// Якщо запит не від бота, обслуговуємо React додаток  
app.use((req, res) => {  
 res.sendFile(join(BUILD_PATH, "index.html"));  
});  

// Запускаємо сервер  
app.listen(PORT, () => {  
 console.log(`Сервер попереднього рендерингу працює на http://localhost:${PORT}`);  
});

Давайте розглянемо код покроково.

Крок 1:

Створюємо сервер на express і константу PORT, на якому працюватиме наш сервер, а також створюємо константу __dirname, щоб мати доступ до шляху до папки збірки, де знаходиться код вашого проєкту. Ви можете налаштувати її відповідно до вашого шляху до збірки.

const app = express();  
const PORT = 3000;  

const __filename = fileURLToPath(import.meta.url);  
const __dirname = path.dirname(__filename);  

// Обслуговуємо ваші файли збірки   
const BUILD_PATH = join(__dirname, "build"); // Налаштуйте шлях, якщо потрібно  
app.use(staticpath(BUILD_PATH));

Крок 2:

Створюємо проміжне ПЗ для попереднього рендерингу HTML контенту, де перевіряємо, чи запит надійшов від бота чи ні. Ми перевіряємо запит, аналізуючи user agent — ви можете дізнатися більше про user agent тут. В основному, різні соціальні медіа-краулери використовують різні user agent, і ми повинні перевіряти, чи міститься він у заголовках запиту чи ні. Наприклад, user agent для Facebook — це facebookexternalhit/1.0 або facebookexternalhit/1.1, для Twitter — Twitterbot/1.1 і так далі. Я додав user agent для Facebook, Twitter, LinkedIn та WhatsApp.
Ви можете додати більше умов для тих, для кого потрібно, щоб соціальна картка з’являлася.

Якщо запит надійшов від бота, то:

  • Запустіть браузер на сервері.
  • Створіть контекст браузера для завантаження сторінки.
  • Створіть нову сторінку.
  • Перейдіть за URL, який запитав бот.
  • Почекайте, поки контент завантажиться.
  • Закрийте екземпляр браузера.
  • І нарешті, надішліть HTML контент, запитаний ботом. Це буде фактичний контент сторінки, який був відрендерений на сервері і надісланий запитувачу.
  • Якщо під час цього процесу виникає помилка, поверніть помилку сервера 500.
  • А якщо запит не від бота, продовжуйте обробляти сторінку.
 // Проміжне ПЗ для попереднього рендерингу  
app.use(async (req, res, next) => {  
 const userAgent = req.headers["user-agent"];  

 // Перевіряємо, чи запит від бота  
 const isBot = /bot|crawler|spider|crawling|facebookexternalhit\/*|facebot|Twitterbot\/*|LinkedInBot\/*|GuzzleHttp\/7|WhatsApp\/*/i.test(userAgent);  
 if (isBot) {  
 try {  
 const browser = await chromium.launch({ headless: true });  
 const context = await browser.newContext();  
 const page = await context.newPage();  
 const url = `http://localhost:${PORT}${req.originalUrl}`;  
 await page.goto(url, { waitUntil: "networkidle0" });  

 const html = await page.content();  
 await browser.close();  

 res.send(html);  
 } catch (err) {  
 console.error("Помилка Playwright:", err);  
 res.status(500).send("Помилка при попередньому рендерингу сторінки");  
 }  
 } else {  
 next(); // Пропускаємо інші запити  
 }  
});

Крок 3:

Створюємо fallback для обслуговування React додатку для запитів не від ботів за допомогою файлу index.html з нашої директорії збірки. Цей fallback важливий, тому що коли бот запитує URL, React має можливість вказати, де шукати інші URL, окрім шляху до index, і наш файл index.html знає все про маршрутизацію.

Нарешті, запускаємо сервер express на localhost з портом, який ми вказали вище.

 // Fallback для обслуговування React додатку для запитів не від ботів  
app.use((req, res) => {  
 res.sendFile(join(BUILD_PATH, "index.html"));  
});  

// Запускаємо сервер  
app.listen(PORT, () => {  
 console.log(`Сервер попереднього рендерингу працює на http://localhost:${PORT}`);  
});

Крок 4:

Нарешті, запустіть файл server.js локально та перевірте, чи працює він належним чином.

node server.js

Він повинен запуститися без помилок і вивести в консолі наступне:

Сервер попереднього рендерингу працює на http://localhost:3000

Тепер перейдіть до браузера і перевірте, чи працює ваш додаток як очікується. Якщо все завантажується коректно, значить, все працює без помилок, і ви можете продовжувати. Наприкінці, давайте перевіримо, чи бот crawls нашу сторінку: якщо це так, сторінка повинна відповісти з динамічно завантаженим мета-тегом з сервера. Щоб перевірити це, використайте команду curl в терміналі. Якщо у вас ще не встановлено curl, будь ласка, спершу встановіть його. Щоб дізнатися більше про curl, перейдіть за посиланням тут.

curl -H "User-Agent: bot" localhost:3000/path/to/your/article

Коли ви запустите цю команду, ви повинні отримати відповідь в терміналі з HTML контентом, у якому правильно завантажено мета-теги за допомогою react-helmet-async — для цього нам і потрібен react-helmet-async, щоб динамічно завантажувати мета-теги. Playwright і puppeteer відповідають лише за запуск браузера на сервері і отримання контенту на сервері, замість того, щоб отримувати його на клієнтській стороні.

Крок 5:

Вперше вам потрібно запустити нижче вказану команду для того, щоб playwright працював належним чином. Це встановить необхідний браузер і відсутні пакети на вашому сервері.

npx playwright install

Це забезпечить правильне налаштування всього, під час виконання цієї команди не повинно бути помилок. Якщо будуть помилки, команда підкаже, які пакети потрібно встановити.

Нарешті, ви можете відправити свій код на сервер і налаштувати сервер для express разом з nginx або іншим проксі-сервером, який ви використовуєте. Ця частина залежить від того, як налаштований ваш сервер.
Ви повинні мати можливість виконувати команду node server.js з директорії проєкту та проксувати цей URL localhost:3000 на ваш проксі-сервер. Також не забувайте вперше запустити команду npx playwright install на сервері для правильного налаштування всього.

Запуск з Puppeteer замість Playwright

Якщо ви не хочете використовувати Playwright і хочете використовувати Puppeteer, вам потрібно відповідним чином оновити ваш файл server.js.

import express, { static as staticpath } from "express";  
import { launch } from "puppeteer"; // ======> імпортуємо цей пакет  
import { join } from "path";  
import { fileURLToPath } from "url";  
import path from "path";  


const app = express();  
const PORT = 3000;  

const __filename = fileURLToPath(import.meta.url);  
const __dirname = path.dirname(__filename);  

// Обслуговування ваших Vite файлів збірки  
const BUILD_PATH = join(__dirname, "build"); // Налаштуйте шлях, якщо потрібно  
app.use(staticpath(BUILD_PATH));  

// Проміжне ПЗ для попереднього рендерингу з Puppeteer  
app.use(async (req, res, next) => {  
 const userAgent = req.headers["user-agent"];  

 // Перевірка, чи запит від бота  
 const isBot = /bot|crawler|spider|crawling|facebookexternalhit\/*|facebot|Twitterbot\/*|/*/i.test(userAgent);  
 if (isBot) {  
 try {  
 // Оновлюємо пакет puppeteer для запуску браузера на сервері  
 const browser = await launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });  
 const page = await browser.newPage();  
 const url = `http://localhost:${PORT}${req.originalUrl}`;  
 await page.goto(url, { waitUntil: "networkidle0" });  

 const html = await page.content();  
 await browser.close();  

 res.send(html);  
 } catch (err) {  
 console.error("Помилка Puppeteer:", err);  
 res.status(500).send("Помилка при попередньому рендерингу сторінки");  
 }  
 } else {  
 next(); // Пропускаємо інші запити  
 }  
});  

// Fallback для обслуговування React додатку для запитів не від ботів  
app.use((req, res) => {  
 res.sendFile(join(BUILD_PATH, "index.html"));  
});  

// Запуск сервера  
app.listen(PORT, () => {  
 console.log(`Сервер попереднього рендерингу працює на http://localhost:${PORT}`);  
});

Після налаштування Puppeteer у файлі server.js, вам потрібно встановити браузер chromium для коректної роботи. Puppeteer має деякі проблеми з роботою на Ubuntu 23.03+ через особливості безпеки цієї операційної системи, але ви можете спробувати самостійно, все залежить від вас. У вищенаведеному коді всі інші частини залишаються такими ж, за винятком запуску браузера за допомогою Puppeteer замість Playwright.

Висновок

Хоча це й дещо «костильне» рішення для налаштування сервера та передачі контенту, відрендереного в браузері на сервері, я не впевнений, чи варто використовувати такий підхід. Якщо хтось може пояснити більш надійний спосіб вирішення цієї проблеми, я буду радий навчитися і відкритий до інших методів для цього рішення.

Я настійно рекомендую вибрати рішення 2 для цієї проблеми, яке я згадував на початку статті, і використовувати SSR фреймворки.

Перекладено з: Meta Tag Crawling for React SPAs with Express and Playwright: A Server-Side Solution

Leave a Reply

Your email address will not be published. Required fields are marked *