Предисловие
Представьте себе следующую ситуацию: у вас на руках есть 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 фреймворк!
MountainGoat
А почему нельзя было сделать index-fr.html, index-en.html, index-sanskrit.html и просто index.html, который перекидывал бы куда надо по параметрам?