
Наконец, настал этот момент, и я решился написать статью. Давно хотел, но как-то не хватало мотивации. А ведь, знаете, как говорят: «гнев — лучший мотиватор». Есть же такое выражение?
Предыстория
Я приглашаю вас в путешествие, но сначала нужно расставить декорации. Представьте, что вы работаете в некой компании X, и один из ваших сервисов на Next.js крякнулся. Ну и поскольку это Next.js, то мы понятия не имеем, что конкретно произошло, так как логирование процессов по умолчанию включено только при разработке.
И теперь перед нами квест — найти и настроить механизм логирования для продакшена. Будет нелегко, но нам как бы не привыкать.
Промежуточный слой
Первым на своём пути мы встречаем промежуточное ПО. В документации даже сказано:
«Промежуточное ПО выполняется до разрешения маршрутов, и особенно полезно для реализации кастомной серверной логики вроде аутентификации, логирования или обработки перенаправлений».
Хорошо, вроде ничего сложного. Пора выбирать библиотеку логирования. Я обратился к pino, так как уже с ней знаком. Хотя любое решение будет лучше, чем console.log. Думаю, разберёмся с этим до обеда.
Начнём с настройки основного промежуточного ПО:
// middleware.ts
import { NextResponse, NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
return new NextResponse.next({
request: request,
headers: request.headers,
// status: 200,
// statusText: 'OK'
});
}
export const config = {
matcher: "/:path*",
};
Думаю, что у нас уже возникла проблема. Из своего промежуточного ПО мы можем передать не более 4 параметров. Единственное, что реально влияет на задействованный маршрут, это headers. Давайте не будем упускать тот факт, что нельзя использовать несколько промежуточных программ или связывать их в цепочку. Как же можно было так налажать? Мы используем программные прослойки с начала 2010-х, когда только появился Express.
Как бы то ни было, мы достаточно умны, и можем воспользоваться изящными решениями, которые предлагает нам современный Node.js. Обратимся к AsyncLocalStorage.
// app/logger.ts
import { AsyncLocalStorage } from "async_hooks";
import { Logger, pino } from "pino";
const loggerInstance = pino({
// Необходимая конфигурация.
level: process.env.LOG_LEVEL ?? "trace",
});
export const LoggerStorage = new AsyncLocalStorage<Logger>();
export function logger(): Logger | null {
return LoggerStorage.getStore() ?? null;
}
export function requestLogger(): Logger {
return loggerInstance.child({ requestId: crypto.randomUUID() });
}
// middleware.ts
export async function middleware(request: NextRequest) {
LoggerStorage.enterWith(requestLogger());
logger()?.debug({ url: request.url }, "Started processing request!");
return NextResponse.next();
}
Уфф…самое тяжёлое позади. Теперь протестируем всё это. Переходим на localhost:3000 и видим следующее:
{ requestId: 'ec7718fa-b1a2-473e-b2e2-8f51188efa8f' } { url: 'http://localhost:3000/' } 'Started processing request!'
GET / 200 in 71ms
{ requestId: '09b526b1-68f4-4e90-971f-b0bc52ad167c' } { url: 'http://localhost:3000/next.svg' } 'Started processing request!'
{ requestId: '481dd2ff-e900-4985-ae15-0b0a1eb5923f' } { url: 'http://localhost:3000/vercel.svg' } 'Started processing request!'
{ requestId: 'e7b29301-171c-4c91-af25-771471502ee4' } { url: 'http://localhost:3000/file.svg' } 'Started processing request!'
{ requestId: '13766de3-dd00-42ce-808a-ac072dcfd4c6' } { url: 'http://localhost:3000/window.svg' } 'Started processing request!'
{ requestId: '317e054c-1a9a-4dd8-ba21-4c0201fbeada' } { url: 'http://localhost:3000/globe.svg' } 'Started processing request!'
Не знаю, использовали ли вы pino ранее, но так быть не должно. А можете понять, почему?
Я не Next.js и томить вас ожиданиями не стану. Это вывод браузера. Почему? Ну, потому что по умолчанию средой выполнения промежуточного ПО в Next.js является edje. Да, мы можем переключиться на среду nodejs, которая должна нормально заработать. Вот только на деле это может оказаться не так.
Я пробовал такой подход в свеженьком проекте Next.js, и у меня получилось. Но вот повторить это в реальном проекте мне не удалось. Не подумайте, я не сумасшедший. Ну да ладно, основная проблема всё равно не в этом. Мы постепенно к ней приближаемся.
Перелистывая местные хроники безумств
Логировать промежуточное ПО круто и всё такое, но главная магия происходит не здесь. Для её раскрытия нужно логировать страницы и макеты. Попробуем.
// app/page.tsx
export default function Home() {
logger()?.info("Logging from the page!");
return <div>Real simple website!</div>
}
Теперь обновляем страницу и получаем:
✓ Compiled / in 16ms
GET / 200 in 142ms
И всё? И всё. Ничего. Совсем.
Для сохранения исторической ясности покажу, как этот вывод должен выглядеть:
✓ Compiled / in 2.2s
[11:38:59.259] INFO (12599): Logging from the page!
requestId: "2ddef9cf-6fee-4d1d-8b1e-6bb16a3e636b"
GET / 200 in 2520ms
Ладно, что-то я затянул, пора переходить к сути. Функция logger возвращает null. Почему? Не уверен, но мне кажется, что рендеринг выполняется не в том же асинхронном контексте, что и промежуточное ПО.
И что с этим делать? Вы не поверите. Помните, что из промежуточной программы можно передать лишь одно значение — headers? Да, именно это нам и нужно.
Следующий код не для слабонервных:
// app/log/serverLogger.ts
import { pino } from "pino";
export const loggerInstance = pino({
// Необходимая конфигурация.
level: process.env.LOG_LEVEL ?? "info",
});
// app/log/middleware.ts
// Да, нужно разделить логгеры ...
// Здесь почти всё то же самое.
import { loggerInstance } from "./serverLogger";
export function requestLogger(requestId: string): Logger {
return loggerInstance.child({ requestId });
}
// app/log/server.ts
import { headers } from "next/headers";
import { loggerInstance } from "./serverLogger";
import { Logger } from "pino";
import { NextRequest } from "next/server";
const REQUEST_ID_HEADER = "dominik-request-id";
export function requestHeaders(
request: NextRequest,
requestId: string,
): Headers {
const head = new Headers(request.headers);
head.set(REQUEST_ID_HEADER, requestId);
return head;
}
// Да, эта функция должна быть асинхронной ...
export async function logger(): Promise<Logger> {
const hdrs = await headers();
const requestId = hdrs.get(REQUEST_ID_HEADER);
return loggerInstance.child({ requestId });
}
// middleware.ts
import { logger, LoggerStorage, requestLogger } from "./app/log/middleware";
import { requestHeaders } from "./app/log/server";
export async function middleware(request: NextRequest) {
const requestId = crypto.randomUUID();
LoggerStorage.enterWith(requestLogger(requestId));
logger()?.debug({ url: request.url }, "Started processing request!");
return NextResponse.next({ headers: requestHeaders(request, requestId) });
}
// app/page.tsx
export default async function Home() {
(await logger())?.info("Logging from the page!");
// ...
}
Разве не прекрасно? Мне особенно нравится, что теперь можно импортировать код логирования промежуточного слоя с сервера. Естественно, работать он не будет. Или, наоборот, импортировать код логирования сервера из промежуточного слоя. Который тоже работать не будет. Здесь важно ничего не напутать. И это мы ещё не говорили о логировании в клиентских компонентах, которые, вопреки своему названию, тоже выполняются на сервере. Да, это уже третье разделение.
Вас принимают за детей
Мне следует извиниться за то, что завёл вас в эту ловушку. Просто я сам уже несколько раз в неё попадал. Система промежуточного ПО может быть очень полезна при правильном дизайне, и я хотел показать вам, как бывает в противном случае. По факту это и стало основной причиной для написания статьи.
Думаю, что каждый из нас достигал в своей жизни некой точки, когда чувствовал, что с него хватит. Для меня эта точка возникла здесь. К чёрту! Давайте использовать кастомный сервер.
Эта возможность Next.js позволяет программно запускать сервер с нестандартной конфигурацией. Чаще всего вам это не потребуется, но в исключительных случаях может оказаться полезным.
Взглянем на пример из документации:
import { createServer } from 'http'
import { parse } from 'url'
import next from 'next'
const port = parseInt(process.env.PORT || '3000', 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url!, true)
handle(req, res, parsedUrl)
}).listen(port)
console.log(
`> Server listening at http://localhost:${port} as ${
dev ? 'development' : process.env.NODE_ENV
}`
)
})
Обратите внимание, что здесь снова handle не получает никакие параметры — только URL запроса, сам сырой запрос и ответ.
Как бы то ни было, у нас есть AsyncLocalStorage, так что волноваться не стоит. Давайте слегка перепишем этот пример.
// app/logger.ts
// Возвращаемся к нашей вариации с AsyncLocalStorage.
import { pino, Logger } from "pino";
import { AsyncLocalStorage } from "async_hooks";
const loggerInstance = pino({
// Вся необходимая конфигурация.
level: process.env.LOG_LEVEL ?? "info",
});
export const LoggerStorage = new AsyncLocalStorage<Logger>();
export function logger(): Logger | null {
return LoggerStorage.getStore() ?? null;
}
export function requestLogger(): Logger {
return loggerInstance.child({ requestId: crypto.randomUUID() });
}
// server.ts
import { logger, LoggerStorage, requestLogger } from "./app/logger";
app.prepare().then(() => {
createServer(async (req, res) => {
// Новый код.
LoggerStorage.enterWith(requestLogger());
logger()?.info({}, "Logging from server!");
const parsedUrl = parse(req.url!, true);
await handle(req, res, parsedUrl);
}).listen(port);
});
// middleware.ts
import { logger } from "./app/logger";
export async function middleware(request: NextRequest) {
logger()?.info({}, "Logging from middleware!");
return NextResponse.next();
}
// app/page.tsx
import { logger } from "./logger";
export default async function Home() {
logger()?.info("Logging from the page!");
// ...
}
Хорошо, теперь протестируем наше решение. Обновляем браузер, и …
> Server listening at http://localhost:3000 as development
[12:29:52.183] INFO (19938): Logging from server!
requestId: "2ffab9a2-7e15-4188-8959-a7822592108f"
✓ Compiled /middleware in 388ms (151 modules)
○ Compiling / ...
✓ Compiled / in 676ms (769 modules)
И всё. Да они издеваются. Какого хрена?
Тут вы можете подумать, что AsyncLocalStorage работает не так. И вполне можете оказаться правы, но я напомню, что headers() и cookies() используют AsyncLocalStorage. Это то преимущество разработчиков Next.js, которого у нас нет.
Насколько я знаю, есть лишь два способа передать информацию из промежуточного слоя на страницу.
Заголовки
NextResponse.redirect/NextResponse.rewriteдля перенаправления ответа с дополнительными параметрами (например,/[requestId]/page.tsx)
Как вы могли заметить, радужным ни один из них в нашем случае не выглядит. К вам просто относятся как к детям. Разработчики Next.js имеют чёткое представление о том, как всё должно работать, и вы либо ему подчиняетесь, либо проходите мимо. Обратите внимание: если бы это касалось только промежуточного ПО, то я бы не стал тратить свои выходные на всю эту критику фреймворка React. У меня есть дела поважнее. Но это постоянная боль, с которой при работе с Next.js вы встречаетесь ежедневно.
Vercel может лучше
Бесит же в этом примере то, что Vercel может справиться с подобными задачами намного лучше. Я не хочу излишне хвалить Svelte(Kit), так как их последние решения вызывают у меня опасения, но этот фреймворк намного лучше Next.js. Давайте заглянем в их документацию по промежуточному ПО:
handle — эта функция выполняется при каждом получении запроса сервером SvelteKit [...] Она позволяет изменять заголовки или тело ответа, либо полностью обходить SvelteKit (для программной реализации маршрутов, например).
Пока звучит неплохо.
locals — чтобы добавить собственные данные в запрос, который передаётся обработчикам в +server.js и серверным функциям load, заполните объект event.locals как показано ниже.
На моих глазах от радости навернулись слёзы. Туда также можно передавать реальные объекты и классы — например, логгер.
Вы можете определить несколько функций обработки и выполнять их последовательно.
Вот так выглядит реальный инжиниринг. SvelteKit — это продукт Vercel. Но как так получается, что флагманский проект уступает побочному по возможностям? Что за чертовщина?
Учёные открыли сверхмассивную чёрную дыру в https://github.com/vercel/next.js/issues
Мне больше нечего особо добавить, но раз уж мы тут все собрались, то будет уместным упомянуть про баг-трекер на GitHub. Это, пожалуй, вершина всей той мусорной кучи недоразумений, которые есть в Next.js. Это то место, куда все надежды и мольбы приходят умирать. Среднее время ответа на баг-репорт здесь — никогда. Я из спортивного интереса решил поискать истории отправки отчётов о багах и их обсуждения касательно тех проблем, с которыми сталкивался сам. В итоге я даже готов принимать ставки на то, сколько лет уйдёт, чтобы получить ответ от команды Next.js.
Думаете, я шучу? Здесь годами лежат сотни запросов с кучей эмодзи ? в ожидании официального ответа. И когда этот ответ, наконец, приходит, в нём говорится, что вы действуете неправильно, и решение для ваших реальных проблем уже в разработке. После этого упомянутое «решение» ещё несколько лет томится в канареечной версии.
Я сам лично отправлял два баг-репорта год назад. Имейте в виду, что для создания валидного баг-репорта вам нужно воспроизвести проблему.
И что же ты получаешь за время, потраченное на минимальное воспроизведение бага? Всё верно. Полное молчание.
Я бы сообщил ещё о десятке проблем, которые встречал, но после такого уже не стал.
Честно говоря, даже не знаю, существуют ли ещё те баги.
Какие здесь можно сделать выводы?
Не знаю. Лично я больше не хочу использовать Next.js. Вы можете решить, что это всего-навсего одна проблема, которую я преувеличил. Но в этом фреймворке на каждом углу можно встретить баги и пограничные случаи. Как они вообще умудрились сделать так, что TypeScript компилируется медленнее Rust? Зачем проводить различие между кодом, выполняющемся на клиенте и на сервере, не предоставляя никаких инструментов для использования этого факта? Зачем? Почему? И так далее. Не думаю, что у меня хватит ресурса, чтобы вытащить всех нас из этого болота Next.js. Но я обязательно озвучу своё мнение, если в итоге мы напишем другое приложение. Посмотрим, вдруг трава в нём окажется зеленее.
Комментарии (51)

DarthVictor
07.09.2025 09:58Next.js уже во времена нового роутера и папочки
phpapp для серверных компонентов шагнул не туда.

gun_dose
07.09.2025 09:58Главный недостаток next.js - он дико тормознутый. Когда они добавили SSG, это означало "ребята, мы не знаем, как ускорить это г-но, поэтому вот вам генератор статики". Проблема изначально была в том, что никому не нужен реактивный фронтенд на бэке. На бэке на странице нет и не может быть никаких событий, поэтому абсолютное большинство фреймворков, независимо от языка, используют строковые шаблонизаторы.

devmargooo
07.09.2025 09:58Next.js не дает вам возможность чейнить мидлвары, потому что это все-таки технология для разработки интерфейсов, а не логики веб-приложения. Не надо писать огромную логику в миддлварах на next.js, прикрутите себе бек и там чейните мидлвары сколько хотите :)
Лог "Logging from the page!" не отобразился в браузерной консоли потому, что Next.js - это технология серверного рендеринга. Рендеринг был выполнен на сервере, там и нужно искать лог :) (в данном случае лог будет в билдтайме). В целом по рассказу такое ощущение, что автор ищет логи не там, где их возможно увидеть :)
Клиентские компоненты называются таковыми, потому что там доступно клиентское апи (хуки реакта, обработчики событий)

Jhayphal
07.09.2025 09:58Интересно, чем автор оригинала заслужил такое уважение, чтобы переводить его труды.
Периодически при поиске нужной информации сталкиваюсь с англоязычными публикациями, в которых написана откровенная чушь. Когда видно, что автор публикации не владеет темой достаточно глубоко.
В то же время у некоторых людей есть стойкое ощущение, что если статья написана на английском, значит она заслуживает особого внимания.

Spaceguest
07.09.2025 09:58А я на этом дерьме написал проект на 100К строк. Миддлвару использовал только для CSP. Она там вообще чисто чтоб было, а проку ноль. Rest маршруты тупо декорировал функциями с логированием, авторизацией и прочими делами. А вот с тем, как код пересекает границу сервер-клиент я дофига проблем словил. Так что второй раз пожалуй ни для чего серьезного я nextjs не возьму

DmitryOlkhovoi
07.09.2025 09:58У нас тоже дикие траблы с Next.js. Корни всех проблем, в том, что React, по сути заутсорсил развития фреймворка и новых фичей таким вот инструментам. Они за него имплементят SSR и прочее. И все оно полусырое. Не говоря уже в целом о том, что React'у пора на покой
Dhwtj
Пишите веб бэк на Go, Rust, C#, Java
Но не на JS, PHP (если только самый простой)
Vadiok
Почему вы считаете, что на Go и Rust лучше писать сложный бэк?
Dhwtj
Go = PHP (или node.js/next.js) + компиляция (скорость и статическая типизация) + отличный сетевой рантайм
Rust = Go + более выразительные типы - рантайм - GC (но часто это и плюс)
C# / Java где-то посередине: богатые библиотеки, GC есть, но предсказуемый, типы богаче Go, но без Rust'овской борьбы с borrow checker
Rust, конечно, молодой и не про веб больше. Но я накатал приложение на 5000 строк, мне понравилось
Vadiok
Те преимущества, что вы описали - это про скорость. Микросервисы на Go - милое дело, но вот сложные штуки, где важна скорость разработки, но не в ущерб архитектуре, и есть множество готовых решений для ее ускорения, Rust и Go по-моему в пролете. А PHP с Node.js - вполне себе оправданные решения.
Про Java и C# не сильно в курсе, но мой вопрос и не про них.
Dhwtj
Видел очень плохие приложения на PHP причем именно из-за языковых особенностей. Травмирующий опыт и всё такое.
Представьте себе единый бизнес процесс (визард), описанный в нескольких файлах на 4-5000 строк без типов, только словари и понять логику что это за данные и каким правилам они отвечают невозможно не прочитав весь код.
А теперь представьте ещё всё приложение целиком на и150-200.000 строк, где система согласования на 20 состояний, 50 переходов и десяток ролей равномерно размазана по жирным контроллерам-рендерам (ну, как PHP это любит). Да, можно было написать лучше. Но динамическая типизация провоцирует такое
Жирные контроллеры-рендеры-запросы к БД в одной функции видел только в пхп
В других языках такое физически неудобно писать.
PHP :Барьер входа в говнокод нулевой
HTML, JS, SQL, логика — всё тут. SQL injection в комплекте
В Rust я бы на автомате написал так
Dhwtj
Да, кода больше. Но читать просто, достаточно прочитать сигнатуры. В Rust они не врут в отличие от PHP, JS, python
Rive
Это вы лукавите, сравнивая лапшичный код на PHP уровня сайта школы 2001 года и упорядоченный код на Rust, который использует фреймворк Axum и шаблонизатор Askama (им обоим не больше 5 лет).
На Rust так же можно написать грязный код уровня
Скрытый текст
Но в 2025 году код из вашего примера для PHP и моего примера для Rust будут одинаково ругать коллеги.
Dhwtj
Лукавство, конечно. Вы меня поймали.
Тем не менее, PHP провоцирует на говнокод. На Rust я бы просто не додумался до такого.
tempick
Тейки из 2010 до сих пор живы хех. Если что, в PHP есть такая вещь как фреймворки (laravel, symfony и иногда yii2 на легаси), которые являются просто базой. Вы выше просто навалили какой-то отрывок кода. Да, так можно написать, теоретически. Но так никто и не делает. Я бы мог в пример навалить такой же гавнокод на джаве или плюсах из своих первых задач в шараге. Но это не имеет смысла
NN1
С вашего позволения беру ваш прекрасный код, чтобы пугать коллег ;)
Vedomir
>Видел очень плохие приложения на PHP причем именно из-за языковых особенностей.
Откровенный обман, так как языковые способности PHP никак не заставляют писать говнокод.
Говнокод можно написать на любом языке.
YegorP
Это как?
Dhwtj
Go
Rust это красиво!
JS не сильно отличается от Go
DarthVictor
На JS никто уже не пишет ни бэк, ни фронт, а система типов TS побогаче и Go, и Java, и пожалуй даже C#. Хоть и не без проблем, вроде отсутствия нормальной ковариантности и контрвариантности. Правда к TS есть вопросы к серверному рантайму (вроде реализации многопоточности) и экосистеме бэкенда.
Wowfirst
Почему вы считаете, что на JS не стоит писать бек?
David_Osipov
Потому что выстрелить себе в колено, открыв кучи уязвимостей, на JS в разы легче, чем на Rust (который за такое сильно бьёт по рукам).
ionicman
Вот не надо пинять на язык, ладно)
Преимуществ, когда один стэк и на фронте и на бэке - куча, нода уже взрослая и отлично показала себя в проде, библиотек на все случаи жизни, а если что-то не так - всегда можно залезть и поправить.
Все проблемы у людей от головыи рук, не надо инструменты винить.
При безграмотной арзитектуре, погоне за хайпом и борще в голове вас нечего не спасёт. Выбирать надо под задачу, под вектор развития и под приемственность (сходите ваканчии под go/rust помониторьте).
Epsiloncool
Тоже раньше считал js дырявым, пока не открыл для себя typescript. Эта штука с линтером точно не даст вам накостылить ошибок и заодно сильно поможет при разработке (подсказками и предупреждениями).
David_Osipov
Имхо, в среднем, все мы люди и все мы ошибаемся, поэтому инструменты должны защищать от ошибок, как это сделано в Rust или, как ниже заметили, в TS. Всё же JS - это язык с громадным легаси, от которого невозможно отказаться, иначе всё рухнет, но и которое иногда очень неясно работает и, чтобы написать безопасный и быстрый код, нужно все эти особенности знать, а на это убить надо от 5 лет безостановочного кодинга, т.е. тупо стать сеньором.
Согласен, что сеньор сможет написать относительно безопасный и быстрый код (ну тут скорее просто быстро написать код, чем быстрый код), на фронте и на бэке, но то же самое сможет сделать на расте миддл.
Dhwtj
Путём надра.. гриндинга
DmitryOlkhovoi
Я думал в раст комьюнити больше принято класть руку на коленку товарища
gluck59
Как вам удалось в одной фразе смешать чисто серверный PHP и выполняющийся в браузере (по крайней мере придуманный для браузера) JS?
Dhwtj
Догадайся
DmitryOlkhovoi
Какая разница для чего он придуман, по сути оба интерпретируемые, оба через апи работают I/O. Ну а так очевидно шутливый либо тупой наброс)
khmm12
Иногда неплохо бы загуглить незнакомый фреймворк / библиотеку, чтобы не раздавать советы писать SPA с SSR на языках, на которых слегка проблематично реализовать SPA, да ещё с SSR (кроме C# с Blazor, но удачи набрать специалистов).
DmitryOlkhovoi
А вы воспользуйтесь своим советом. Вдруг окажется SSR был изначально без этих инструментов))) Понимате мы "натягивали" верстку на пхп, которая где рендерилась?)
khmm12
В моих словах где-то есть отрицание того факта, что SSR возможен без Next.js? Если есть, приведите пожалуйста. Я вполне успешно реализовывал SSR с React до того как появился Next.js ещё в далеком 2016 году.
Пост о проблемах конкретного фрейморка Next.js, автор комментария выше предлагает писать backend на Go, Rust, C#, Java, из чего можно предположить, что он не понимает, что такое Next.js и для чего он нужен.
Я даже подчеркну, что особой любви не питал и не питаю к Next.js. Весь фреймворк сделан так, что шаг влево, шаг вправо, и он начинает вставлять в палки в колеса. Так было в 2018 году, когда взять react + react-router + loadable-components + webpack было безопаснее с точки жизни продукта. Так и осталось в настоящее время, если не стало хуже. Но я не буду отрицать, что Next.js имеет преимущества, особенно в скорости разработки на начальных этапах, когда продукт должен был выйти ещё вчера.
khmm12
Как и в ASP.NET, Django, Ruby on Rails, и даже, прости Господи, Nitrogen. Только какое это имеет отношение к SPA?
DmitryOlkhovoi
Партишины так же по апи возвращались, как например это делает сейчас htmx, там даже есть атрибут на всю сраницу для aync навигации. Вполне себе SPA. А еще помните был hashtag для роутинга до history api
Так же у edge темплейтов сейчас есть асинхронный функционал, вполне себе можно сходить за частями в момент регдера темплейта и сделать себе микрофронты даже. И все на базе технологий 20 летней давности)
Собственно, что даёт nextjs чего не может дать другой стек? Изоморфность? А она действительно нужна?
khmm12
Было дело, в RoR даже Stimulus на замену Turbolinks завезли. Но бизнес проголосовал деньгами: UX стал сложнее, микросервисы, backend'еры в своей массе заниматься frontend не хотят, а всё чаще и чаще не умеют, и уж тем более сложным UX, frontend'еры backend трогать не хотят и не умеют (дайте мне React/Vue/Angular/Svelte/Solidjs). Поэтому я бы всё таки констатировал смерть классического подхода, где backend рендерит свои шаблоны, а рядом прикрученный JavaScript добавляет UX логику. "Миром" сейчас правят react, vue, angular. И уже даже новостные сайты это приложения на React / Vue.
Я не буду вступать в полемику хорошо это и плохо. Боюсь, скоро в Web ещё и Flutter войдёт (уже есть попытки), где для выделения текста вставляются элементы поверх Canvas. Просто констатирую, что SPA на сегодня это React / Vue / Angular / e.t.c.
DmitryOlkhovoi
Ну просто у нас например есть проект на стеке Adonis, Alpine, Edge. И там шаблоны. Это супер, думаю статейку как то напишу. Убрали кучу астракций, а по возможностям все тоже самое