Как перейти с jQuery на Next.js
Опубликовано: 2022-03-10Эта статья была любезно поддержана нашими дорогими друзьями из Netlify, которые представляют собой разнообразную группу невероятных талантов со всего мира и предлагают платформу для веб-разработчиков, которая увеличивает производительность. Спасибо!
Когда в 2006 году появился jQuery, многие разработчики и организации начали использовать его в своих проектах. Возможность расширения и манипулирования DOM , которую предлагает библиотека, великолепна, и у нас также есть много плагинов для добавления поведения к нашим страницам на случай, если нам нужно выполнять задачи, которые не поддерживаются основной библиотекой jQuery. Это упростило большую часть работы для разработчиков и в тот момент сделало JavaScript мощным языком для создания веб-приложений или одностраничных приложений.
Результат популярности jQuery можно измерить и сегодня: почти 80% самых популярных веб-сайтов мира все еще используют его. Вот некоторые из причин, по которым jQuery так популярен:
- Он поддерживает манипуляции с DOM.
- Он обеспечивает манипуляции с CSS.
- Работает одинаково во всех веб-браузерах.
- Он оборачивает методы событий HTML.
- Легко создавать вызовы AJAX.
- Простые в использовании эффекты и анимация.
За прошедшие годы JavaScript сильно изменился и добавил несколько функций, которых у нас не было в прошлом. С переопределением и развитием ECMAScript некоторые из функций, предоставляемых jQuery, были добавлены к стандартным функциям JavaScript и поддерживаются всеми веб-браузерами. Когда это произошло, часть поведения, предлагаемого jQuery , больше не нужна , поскольку мы можем делать то же самое с помощью простого JavaScript.
С другой стороны, начал появляться новый способ мышления и проектирования пользовательских интерфейсов. Такие фреймворки, как React, Angular или Vue, позволяют разработчикам создавать веб-приложения на основе многократно используемых функциональных компонентов. React, т.е., работает с «виртуальным DOM», который представляет собой DOM-представление в памяти, тогда как jQuery напрямую взаимодействует с DOM менее производительным способом. Кроме того, React предлагает интересные функции, облегчающие разработку определенных функций, таких как управление состоянием. Благодаря этому новому подходу и популярности, которую начали набирать одностраничные приложения, многие разработчики начали использовать React для своих проектов веб-приложений.
И фронтенд-разработка развивалась еще больше, когда фреймворки создавались поверх других фреймворков. Так обстоит дело, например, с Next.js. Как вы, наверное, знаете, это платформа React с открытым исходным кодом, которая предлагает функции для создания статических страниц, создания отображаемых на стороне сервера страниц и объединения обоих типов в одном приложении. Это также позволяет создавать бессерверные API внутри того же приложения.
Существует любопытный сценарий: несмотря на то, что эти интерфейсные фреймворки с годами становятся все более и более популярными, jQuery по-прежнему используется подавляющим большинством веб-страниц. Одна из причин, почему это происходит, заключается в том, что процент сайтов, использующих WordPress, действительно высок, а jQuery включен в состав CMS . Другая причина заключается в том, что некоторые библиотеки, такие как Bootstrap, зависят от jQuery, и есть несколько готовых к использованию шаблонов, использующих его и его плагины.
Но еще одна причина такого количества веб-сайтов, использующих jQuery, — это стоимость переноса всего веб-приложения на новую платформу. Это непросто, это недешево и требует много времени. Но, в конце концов, работа с новыми инструментами и технологиями приносит много преимуществ: более широкую поддержку, помощь сообщества, лучший опыт разработчиков и простоту привлечения людей к работе над проектом.
Есть много сценариев, когда нам не нужно (или мы не хотим) следовать архитектуре, которую навязывают нам такие фреймворки, как React или Next.js, и это нормально. Однако jQuery — это библиотека, содержащая много кода и функций, которые больше не нужны. Многие функции, предлагаемые jQuery, могут быть реализованы с использованием современных встроенных функций JavaScript и, вероятно, более производительным способом.
Давайте обсудим, как мы могли бы отказаться от использования jQuery и перенести наш веб-сайт в веб-приложение React или Next.js.
Определите стратегию миграции
Нужна ли нам библиотека?
В зависимости от особенностей нашего веб-приложения у нас может даже быть случай, когда фреймворк на самом деле не нужен. Как упоминалось ранее, несколько функций jQuery были включены (или, по крайней мере, очень похожи) в последние версии веб-стандарта. Итак, учитывая, что:
- Шаблон
$(selector)
из jQuery можно заменить наquerySelectorAll()
.
Вместо того, чтобы делать:
$("#someId");
Мы можем сделать:
document.querySelectorAll("#someId");
- Теперь у нас есть свойство
Element.classList
, если мы хотим манипулировать классами CSS.
Вместо того, чтобы делать:
$(selector).addClass(className);
Мы можем сделать:
element.classList.add(className);
- Многие анимации можно сделать напрямую с помощью CSS, а не с помощью JavaScript.
Вместо того, чтобы делать:
$(selector).fadeIn();
Мы можем сделать:
element.classList.add('show'); element.classList.remove('hide');
И примените некоторые стили CSS:
.show { transition: opacity 400ms; } .hide { opacity: 0; }
- Теперь у нас есть функция addEventListener, если мы хотим обрабатывать события.
Вместо того, чтобы делать:
$(selector).on(eventName, eventHandler);
Мы можем сделать:
element.addEventListener(eventName, eventHandler);
- Вместо использования jQuery Ajax мы можем использовать
XMLHttpRequest
.
Вместо того, чтобы делать:
$.ajax({ type: 'POST', url: '/the-url', data: data });
Мы можем сделать:
var request = new XMLHttpRequest(); request.open('POST', '/the-url', true); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); request.send(data);
Для получения более подробной информации вы можете взглянуть на эти фрагменты кода Vanilla JavaScript.
Идентифицировать компоненты
Если мы используем jQuery в нашем приложении, у нас должен быть некоторый HTML-контент, сгенерированный на веб-сервере, и код JavaScript, добавляющий интерактивности странице. Вероятно, мы добавляем обработчики событий при загрузке страницы, которые будут манипулировать DOM, когда происходят события, возможно, обновляя CSS или стиль элементов. Мы также можем вызывать серверные службы для выполнения действий, которые могут повлиять на DOM страницы или даже перезагрузить ее.
Идея заключалась бы в том, чтобы реорганизовать код JavaScript , живущий на страницах, и создать компоненты React. Это поможет нам объединить связанный код и составить элементы, которые станут частью более крупной композиции. Сделав это, мы также сможем лучше обрабатывать состояние нашего приложения. Анализируя интерфейс нашего приложения, мы должны разделить его на части, посвященные определенной задаче, чтобы мы могли создавать компоненты на основе этого.
Если у нас есть кнопка:
<button>Click</button>
Со следующей логикой:
var $btnAction = $("#btn-action"); $btnAction.on("click", function() { alert("Button was clicked"); });
Мы можем перенести его в компонент React:
import React from 'react'; function ButtonComponent() { let handleButtonClick = () => { alert('Button clicked!') } return <button onClick={handleButtonClick}>Click</button> }
Но мы также должны оценить, как будет осуществляться процесс миграции, так как наше приложение работает и используется, и мы не хотим на него влиять (или, по крайней мере, влиять на него как можно меньше).
Хорошая миграция
Хорошая миграция — это та, при которой все части приложения полностью переносятся на новую структуру или технологию. Это был бы идеальный сценарий для нашего приложения, поскольку мы будем синхронизировать все части и будем использовать унифицированный инструмент и версию с уникальными ссылками.
Хорошая и полная миграция обычно включает в себя полное переписывание кода нашего приложения, и в этом есть смысл. Если мы создаем приложение с нуля, у нас есть возможность решить, в каком направлении двигаться с новым кодом. Мы могли бы по-новому взглянуть на наши существующие системы и рабочий процесс и создать совершенно новое приложение с теми знаниями, которые у нас есть на данный момент, более полными, чем те, которые у нас были, когда мы впервые создавали наше веб-приложение.
Но полная переработка имеет некоторые проблемы. Во-первых, это требует много времени. Чем больше приложение, тем больше времени нам потребуется на его переписывание. Другая проблема заключается в объеме работы и количестве разработчиков, которые для этого требуются. И, если мы не будем делать прогрессивную миграцию, нам придется подумать о том, сколько времени наше приложение будет недоступно.
Обычно полная переработка может быть выполнена с небольшими проектами, проектами, которые не меняются часто, или приложениями, которые не так критичны для нашего бизнеса.
Быстрая миграция
Другой подход — разделение приложения на части или куски. Мы переносим приложение по частям и выпускаем эти части по мере их готовности. Итак, мы перенесли части нашего приложения, доступные для пользователей и сосуществующие с нашим существующим производственным приложением.
Благодаря этой постепенной миграции мы быстрее доставляем отдельные функции нашего проекта пользователям, поскольку нам не нужно ждать, пока все приложение будет переписано. Мы также получаем более быструю обратную связь от пользователей, что позволяет нам обнаруживать ошибки или проблемы раньше.
Но постепенная миграция заставляет нас использовать другие инструменты, библиотеки, зависимости и фреймворки. Или нам даже придется поддерживать разные версии одного и того же инструмента. Эта расширенная поддержка может вызвать конфликты в нашем приложении.
У нас даже могут возникнуть проблемы, если мы применяем политики в глобальном масштабе, поскольку каждая из перенесенных частей может работать по-разному, но на нее влияет код, устанавливающий глобальные параметры для нашей системы. Примером этого является использование каскадной логики для стилей CSS.
Представьте, что мы работаем с разными версиями jQuery в нашем веб-приложении, потому что мы добавили функции из более новых версий в модули, которые были созданы позже. Насколько сложно было бы перенести все наше приложение на более новую версию jQuery? Теперь представьте тот же сценарий, но с переходом на совершенно другую среду, такую как Next.js. Это может быть сложно.
Миграция Франкенштейна
Денис Мишунов написал статью в Smashing Magazine, в которой представил альтернативу этим двум идеям миграции, пытаясь получить лучшее из двух предыдущих подходов: миграция Франкенштейна. Он основывает процесс миграции на двух основных компонентах: микросервисах и веб-компонентах.
Процесс миграции состоит из списка шагов, которые необходимо выполнить:
1. Определите микросервисы
Основываясь на коде нашего приложения, мы должны разделить его на независимые части, посвященные одной небольшой работе. Если мы думаем об использовании React или Next.js, мы могли бы связать концепцию микросервисов с различными имеющимися у нас компонентами.
В качестве примера рассмотрим приложение со списком продуктов. У нас есть список вещей, которые нужно купить, и вход, чтобы добавить больше вещей в список. Итак, если мы хотим разделить наше приложение на небольшие части, мы могли бы подумать о компоненте «список элементов» и «добавить элемент». Делая это, мы можем разделить функциональность и разметку, связанные с каждой из этих частей, на разные компоненты React.
Чтобы подтвердить, что компоненты независимы, мы должны иметь возможность удалить один из них из приложения, и это не должно повлиять на другие. Если мы получаем ошибку при удалении разметки и функциональности из сервиса, значит, мы неправильно идентифицируем компоненты или нам нужно реорганизовать работу нашего кода.
2. Разрешить доступ хоста к пришельцу
«Хост» — это наше существующее приложение. «Чужого» мы начнем создавать с помощью нового фреймворка. Оба должны работать независимо, но мы должны предоставить доступ от Host к Alien. Мы должны быть в состоянии развернуть любое из двух приложений, не нарушая работу другого, но сохраняя связь между ними.
3. Напишите чужой компонент
Перепишите службу из нашего хост-приложения в наше приложение Alien, используя новую структуру. Компонент должен следовать тому же принципу независимости, который мы упоминали ранее.
Вернемся к примеру со списком продуктов. Мы определили компонент «добавить элемент». С jQuery разметка компонента будет выглядеть примерно так:
<input class="new-item" />
И код JavaScript/jQuery для добавления элементов в список будет примерно таким:
var ENTER_KEY = 13; $('.new-item').on('keyup', function (e) { var $input = $(e.target); var val = $input.val().trim(); if (e.which !== ENTER_KEY || !val) { return; } // code to add the item to the list $input.val(''); });
Вместо этого мы можем создать компонент AddItem
React:
import React from 'react' function AddItemInput({ defaultText }) { let [text, setText] = useState(defaultText) let handleSubmit = e => { e.preventDefault() if (e.which === 13) { setText(e.target.value.trim()) } } return <input type="text" value={text} onChange={(e) => setText(e.target.value)} onKeyDown={handleSubmit} /> }
4. Напишите оболочку веб-компонента вокруг чужой службы
Создайте компонент-оболочку, который импортирует нашу только что созданную службу Alien и отображает ее. Идея состоит в том, чтобы создать мост между приложением Host и приложением Alien. Имейте в виду, что нам может понадобиться сборщик пакетов для генерации кода JavaScript, который работает в нашем текущем приложении, поскольку нам нужно будет скопировать наши новые компоненты React и заставить их работать.
Следуя примеру со списком продуктов, мы можем создать файл AddItem-wrapper.js
в проекте Host. Этот файл будет содержать код, который оборачивает наш уже созданный компонент AddItem
и создает с его помощью пользовательский элемент:
import React from "../alien/node_modules/react"; import ReactDOM from "../alien/node_modules/react-dom"; import AddItem from "../alien/src/components/AddItem"; class FrankensteinWrapper extends HTMLElement { connectedCallback() { const appWrapper = document.createElement("div"); appWrapper.classList.add("grocerylistapp"); ... ReactDOM.render( <HeaderApp />, appWrapper ); … } } customElements.define("frankenstein-add-item-wrapper", FrankensteinWrapper);
Мы должны принести необходимые нод-модули и компоненты из папок приложения Alien, так как нам нужно их импортировать, чтобы компонент заработал.
5. Замените службу хоста веб-компонентом
Этот компонент-оболочка заменит компонент в хост-приложении, и мы начнем его использовать. Таким образом, приложение в рабочей среде будет представлять собой смесь компонентов Host и компонентов, обернутых Alien.
В нашем примере хост-приложения мы должны заменить:
<input class="new-item" />
С участием
<frankenstein-add-item-wrapper></frankenstein-add-item-wrapper> ... <script type="module" src="js/AddItem-wrapper.js"></script>
6. Промыть и повторить
Выполните шаги 3, 4 и 5 для каждой из идентифицированных микрослужб.
7. Переключиться на пришельца
Host теперь представляет собой набор компонентов-оболочек, которые включают в себя все веб-компоненты, созданные нами в приложении Alien. Поскольку мы преобразовали все идентифицированные микросервисы, можно сказать, что приложение Alien готово и все сервисы перенесены. Теперь нам просто нужно указать нашим пользователям приложение Alien.
Метод миграции Франкенштейна работает как комбинация хорошего и быстрого подходов. Мы полностью переносим приложение, но выпускаем различные компоненты, когда они будут готовы. Таким образом, они доступны для более раннего использования и оценки пользователями в производственной среде.
Однако мы должны учитывать, что при таком подходе мы делаем чрезмерную работу. Если мы хотим использовать компоненты, которые мы создаем для нашего приложения Alien, мы должны создать компонент-оболочку для включения в приложение Host. Это заставляет нас тратить время на разработку кода для этих элементов-оболочек. Кроме того, используя их в нашем хост-приложении, мы дублируем включение кода и зависимостей и добавляем код, который повлияет на производительность нашего приложения.
Применение душителя
Другой подход, который мы можем использовать, — это удушение устаревшего приложения. Мы определяем границы нашего существующего веб-приложения, и всякий раз, когда нам нужно добавить функциональные возможности в наше приложение, мы делаем это, используя более новую структуру, пока старая система не будет «задушена». Этот подход помогает нам снизить потенциальный риск, с которым мы можем экспериментировать при переносе приложения.
Чтобы следовать этому подходу, нам нужно идентифицировать различные компоненты, как мы это делаем в Frankenstein Migration. Как только мы разделим наше приложение на разные части связанного императивного кода, мы оборачиваем их в новые компоненты React. Мы не добавляем никакого дополнительного поведения, мы просто создаем компоненты React, которые отображают наш существующий контент.
Давайте посмотрим на пример для большего пояснения. Предположим, у нас есть этот HTML-код в нашем приложении:
<div class="accordion"> <div class="accordion-panel"> <h3 class="accordion-header">Item 1</h3> <div class="accordion-body">Text 1</div> </div> <div class="accordion-panel"> <h3 class="accordion-header">Item 2</h3> <div class="accordion-body">Text 2</div> </div> <div class="accordion-panel"> <h3 class="accordion-header">Item 3</h3> <div class="accordion-body">Text 3</div> </div>> </div>
И этот код JavaScript (мы уже заменили функции jQuery новыми стандартными функциями JavaScript).
const accordions = document.querySelectorAll(".accordion"); for (const accordion of accordions) { const panels = accordion.querySelectorAll(".accordion-panel"); for (const panel of panels) { const head = panel.querySelector(".accordion-header"); head.addEventListener('click', () => { for (const otherPanel of panels) { if (otherPanel !== panel) { otherPanel.classList.remove('accordion-expanded'); } } panel.classList.toggle('accordion-expanded'); }); } }
Это обычная реализация компонента accordion
для JavaScript. Поскольку мы хотим представить здесь React, нам нужно обернуть наш существующий код новым компонентом React:
function Accordions() { useEffect(() => { const accordions = document.querySelectorAll(".accordion") for (const accordion of accordions) { const panels = accordion.querySelectorAll(".accordion-panel") for (const panel of panels) { const head = panel.querySelector(".accordion-header") head.addEventListener("click", () => { for (const otherPanel of panels) { if (otherPanel !== panel) { otherPanel.classList.remove("accordion-expanded") } } panel.classList.toggle("accordion-expanded") }); } } }, []) return null } ReactDOM.render(<Accordions />, document.createElement("div"))
Компонент не добавляет никакого нового поведения или функции. Мы используем useEffect
потому что компонент был смонтирован в документе. Вот почему функция возвращает null, потому что хуку не нужно возвращать компонент.
Итак, мы не добавили никаких новых функций в наше существующее приложение, но внедрили React, не изменив его поведения. Отныне всякий раз, когда мы добавляем новые функции или изменения в наш код, мы будем делать это, используя более новый выбранный фреймворк.
Рендеринг на стороне клиента, рендеринг на стороне сервера или статическая генерация?
Next.js дает нам возможность выбирать, как мы хотим отображать каждую страницу нашего веб-приложения. Мы можем использовать рендеринг на стороне клиента, который React уже предлагает нам, для создания контента непосредственно в браузере пользователя. Или мы можем визуализировать содержимое нашей страницы на сервере, используя рендеринг на стороне сервера. Наконец, мы можем создавать содержимое нашей страницы во время сборки, используя статическую генерацию.
В нашем приложении мы должны загружать и отображать код при загрузке страницы, прежде чем мы начнем взаимодействовать с любой библиотекой или фреймворком JavaScript. Мы можем использовать язык программирования или технологию рендеринга на стороне сервера, например ASP.NET, PHP или Node.js. Мы можем воспользоваться преимуществами возможностей Next.js и заменить наш текущий метод рендеринга серверным методом рендеринга Next.js. Делая это, мы сохраняем все поведение внутри одного и того же проекта, который работает под эгидой выбранного нами фреймворка. Кроме того, мы сохраняем логику нашей главной страницы и компонентов React в одном и том же коде, который генерирует весь необходимый контент для нашей страницы.
Давайте подумаем о странице панели инструментов в качестве примера. Мы можем сгенерировать всю начальную разметку страницы во время загрузки на сервере вместо того, чтобы создавать ее с помощью React в веб-браузере пользователя.
const DashboardPage = ({ user }) => { return ( <div> <h2>{user.name}</h2> // User data </div> ) } export const getServerSideProps = async ({ req, res, params }) => { return { props: { user: getUser(), }, } }, }) export default DashboardPage
Если разметка, которую мы отображаем при загрузке страницы, предсказуема и основана на данных, которые мы можем получить во время сборки, статическая генерация будет хорошим выбором. Генерация статических ресурсов во время сборки сделает наше приложение более быстрым, безопасным, масштабируемым и простым в обслуживании. И, если нам нужно создать динамический контент на страницах нашего приложения, мы можем использовать рендеринг на стороне клиента React для получения информации из служб или источников данных.
Представьте, что у нас есть блог-сайт со множеством сообщений в блогах. Если мы используем статическую генерацию, мы можем создать общий файл [blog-slug].js
в нашем приложении Next.js, и добавив следующий код, мы сгенерируем все статические страницы для наших сообщений в блоге во время сборки.
export const getStaticPaths = async () => { const blogPosts = await getBlogPosts() const paths = blogPosts.map(({ slug }) => ({ params: { slug, }, })) return { paths, fallback: false, } } export const getStaticProps = async ({ params }) => { const { slug } = params const blogPost = await getBlogPostBySlug(slug) return { props: { data: JSON.parse(JSON.stringify(blogPost)), }, } }
Создайте API, используя маршруты API
Одна из замечательных функций, которые предлагает Next.js, — это возможность создавать маршруты API. С их помощью мы можем создавать собственные бессерверные функции, используя Node.js. Мы также можем установить пакеты NPM для расширения функциональности. Самое интересное в этом то, что наш API останется в том же проекте/приложении, что и наш интерфейс, поэтому у нас не будет проблем с CORS.
Если мы поддерживаем API, который вызывается из нашего веб-приложения с использованием функций jQuery AJAX, мы могли бы заменить их с помощью API-маршрутов . При этом мы будем хранить всю кодовую базу нашего приложения в одном репозитории и упростим развертывание нашего приложения. Если мы используем сторонний сервис, мы можем использовать маршруты API для «маскировки» внешних URL-адресов.
У нас может быть API-маршрут /pages/api/get/[id].js
, который возвращает данные, которые мы используем на нашей странице.
export default async (req, res) => { const { id } = req.query try { const data = getData(id) res.status(200).json(data) } catch (e) { res.status(500).json({ error: e.message }) } }
И вызвать его из кода нашей страницы.
const res = await fetch(`/api/get/${id}`, { method: 'GET', }) if (res.status === 200) { // Do something } else { console.error(await res.text()) }
Развернуть в Netlify
Netlify — это полная платформа, которую можно использовать для автоматизации, управления, создания, тестирования, развертывания и размещения веб-приложений. Он имеет множество функций, которые упрощают и ускоряют разработку современных веб-приложений. Некоторые основные моменты Netlify:
- Глобальная хостинговая платформа CDN,
- Поддержка бессерверных функций,
- Развертывание предварительных версий на основе запросов на вытягивание Github,
- вебхуки,
- Мгновенные откаты,
- Управление доступом на основе ролей.
Netlify — отличная платформа для управления и размещения наших приложений Next.js, и с ее помощью довольно просто развернуть веб-приложение.
Прежде всего, нам нужно отслеживать код нашего приложения Next.js в репозитории Git. Netlify подключается к GitHub (или к платформе Git, которую мы предпочитаем), и всякий раз, когда в ветку вносится изменение (фиксация или запрос на включение), будет запускаться автоматическая задача «сборка и развертывание».
Когда у нас есть репозиторий Git с кодом нашего приложения, нам нужно создать для него «Netlify Site». Для этого у нас есть два варианта:
- Использование интерфейса командной строки Netlify
После того, как мы установим CLI (npm install -g netlify-cli
) и войдем в нашу учетную запись Netlify (ntl login
), мы можем перейти в корневой каталог нашего приложения, запуститьntl init
и следовать инструкциям. - Использование веб-приложения Netlify
Мы должны перейти на https://app.netlify.com/start. Подключитесь к нашему провайдеру Git, выберите репозиторий нашего приложения из списка, настройте некоторые параметры сборки и выполните развертывание.
Для обоих методов мы должны учитывать, что наша команда сборки будет next build
, а наш каталог для развертывания out
.
Наконец, плагин Essential Next.js устанавливается автоматически, что позволит нам развертывать и использовать маршруты API, динамические маршруты и режим предварительного просмотра. Вот и все, наше приложение Next.js запущено и работает на быстром и стабильном хостинге CDN.
Заключение
В этой статье мы оценивали веб-сайты с использованием библиотеки jQuery и сравнивали их с новыми интерфейсными фреймворками, такими как React и Next.js. Мы определили, как мы можем начать миграцию, если это принесет нам пользу, на более новый инструмент. Мы оценили различные стратегии миграции и увидели несколько примеров сценариев, которые мы могли бы перенести в проекты веб-приложений Next.js. Наконец, мы увидели, как развернуть наше приложение Next.js в Netlify и запустить его.
Дополнительная литература и ресурсы
- Миграция Франкенштейна: независимый от фреймворка подход
- Удаление jQuery из внешнего интерфейса GitHub.com
- Начало работы с Next.js
- Как развернуть сайты Next.js для Netlify
- Статьи Next.js в блоге Netlify