Предисловие
Представьте себе следующую ситуацию: у вас на руках есть 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 фреймворк!
