Предисловие

Представьте себе следующую ситуацию: у вас на руках есть SPA с рендерингом полностью на клиенте, и вам необходимо сделать так, чтобы в зависимости от URL было разное содержимое у тега <head>.

Например, ваш шеф просит вас сделать так, чтобы при вставке в Телеграм ссылки на французскую версию сайта с query параметром ?hl=fr появлялась превью с французским заголовком и описанием сайта.

Как раз в такой позиции я оказался некоторое время назад, и мне на растерзание попался сайт на чистом, старом-добром, клиентском Vue.

У любого разработчика, знающего отличия SSR и CSR, первым инстинктом будет сказать, что это просто невозможно, а если и возможно, то надо переписывать сайт на Nuxt или хотя-бы на Vite SSR, на что уйдет парочка спринтов.

Но если вы не боитесь замарать руки о костыли, то есть и другой способ! Идея очень проста – пишем сервер на Node, который будет отдавать наш сбилженный сайт, но подменять в зависимости от запроса часть index.html.

Туториал

В первую очередь установим зависимости:
yarn add koa koa-static

Создаем файл entryServer.js. Из него мы будем отдавать наш сайт. В моем случае содержимое папки dist.

// entryServer.js

const Koa = require('koa');
const fs = require('fs');

const app = new Koa();

app.use(async (ctx, next) => {
  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  ctx.body = distHtml;
});

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);

Если запустить node entryServer.js, то на https://localhost:3000 будет пустая страница. Это потому что все ассеты и скрипты недоступны, и сервер отдает вместо них index.html. Чтобы это исправить используем koa-static.

// entryServer.js

const Koa = require('koa');
const fs = require('fs');
const serve = require('koa-static');

const app = new Koa();

app.use(async (ctx, next) => {
  // Если запрашивается файл, 
  // то переходим к мидлваре, 
  // которая отдает статические 
  // ассеты
  if (ctx.request.url.includes('.')) {
    await next();
    return;
  }

  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  ctx.body = distHtml;
});

// Отдаем содержимое каталога dist
app.use(serve('dist'));

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);

Теперь наш сайт работает, как должен, но все еще только на клиенте. Давайте добавим логику на стороне сервера! Создадим несколько файлов в которых содержатся вариации SEO тегов для разных языков, например:

<!-- head/en.html -->

<title>My cool website</title>
<meta name="description" content="With a cool description">
// entryServer.js

const Koa = require('koa');
const fs = require('fs');
const serve = require('koa-static');

// Файлы, которые содержат только
// теги, которые мы собираемся локализовать.
// <title>, <meta> ...
const heads = {
  en: fs.readFileSync('./head/en.html').toString(),
  de: fs.readFileSync('./head/de.html').toString(),
  fr: fs.readFileSync('./head/fr.html').toString()
};

const app = new Koa();

app.use(async (ctx, next) => {
  // Если запрашивается файл, 
  // то переходим к мидлваре, 
  // которая отдает статические 
  // ассеты
  if (ctx.request.url.includes('.')) {
    await next();
    return;
  }

  // Здесь мы смотрим на параметр hl
  // и выбираем необходимый набор тегов.
  // Английский выбирается по умолчанию.
  const lang = ctx.request.query.hl;
  let head = heads.en;
  if (Object.keys(heads).includes(lang)) {
    head = heads[lang];
  }

  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  const body = distHtml.replace('</head>', `${head}\n</head>`);
  ctx.body = body;
});

// Отдаем содержимое каталога dist
app.use(serve('dist'));

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);

Вот наш законченный сервер! Если пользователь вставит my-spa.com?hl=fr в сайт или приложение, которое парсит Open Graph, то превью будет на французском. И нам даже не пришлось тратить сотни часов на миграцию на SSR фреймворк!

Комментарии (1)


  1. MountainGoat
    28.08.2023 08:06

    А почему нельзя было сделать index-fr.html, index-en.html, index-sanskrit.html и просто index.html, который перекидывал бы куда надо по параметрам?