Почему я решил создать собственный шаблон Laravel + React + Inertia с нуля ? Знаешь, как обычно бывает? Нужно сделать новый проект. Ты открываешь документацию Laravel, видишь готовые стартовые пакеты. Думаешь: «О, круто, сейчас быстренько установлю и поехало». И действительно, за час уже есть работающий прототип с авторизацией, профилем, сбросом пароля.
Но потом начинается самое интересное. Ты пытаешься что-то изменить под свои нужды. И тут выясняется, что готовый пакет — это чёрный ящик. Он сам подтягивает контроллеры, маршруты, middleware, о которых ты даже не знал. Ты меняешь одну строчку — а оно ломается в трёх местах. Ты думаешь, что контролируешь проект, а на самом деле просто пользуешься чужой магией.
И тогда я решил: хватит. Соберу свой шаблон. С нуля. Под Laravel 13, React 19, Inertia и Tailwind 4. Без Fortify, без готовых решений, без скрытых зависимостей. Только то, что я сам настроил своими руками.
Что я получил в итоге?
Я наконец-то разобрался, как Inertia связывает фронт и бэк, как работает маршрутизация, зачем нужен middleware, как передавать данные из контроллера в React-компонент. Да, это заняло пару вечеров. Но знаешь, что круто? Теперь каждый новый проект я стартую за пять минут. Просто копирую свой шаблон — и уже пишу бизнес-логику. Без гаданий «а почему оно так работает», без страха что-то сломать.
Главный урок, который я вынес
Когда ты сам собираешь фундамент, ты перестаёшь бояться. Потому что ты каждую строчку написал своими руками. Ты знаешь, где что лежит, зачем нужен каждый файл, как всё связано. И это, брат, даёт уверенность, которой не даст ни один готовый пакет.
Поэтому в этой статье я покажу, как собрать такой шаблон с нуля. Не потому что «так правильно» или «так делают в FAANG». А потому что это реально удобно, когда ты понимаешь каждый слой системы. Чистый Laravel → сам подключаешь Inertia → настраиваешь Vite + React. Никакой магии, никаких скрытых контроллеров. Только ты и код.
В этой статье: • Шаг 1-2: Установка Laravel и Inertia • Шаг 3-7: Настройка фронта (React, Vite, Blade) • Шаг 8: Установка shadcn/ui • Шаг 9-10: Лейаут, страницы, маршруты • Шаг 11-12: Dev-режим и запуск
Делаем строго по шагам. Только Laravel + Inertia + React. Без Breeze, Fortify, и прочего. Только отправная точка для разработки, где ты сам полностью все контролируешь, без «черных ящиков».
Первым делом нужно удостовериться что установлены актуальные версии node.js, npm, composer и php не ниже версии 8.4. и они доступны глобально.
node -v # должно быть 20+
npm -v # 10+
php -v # 8.4+
composer -V
Шаг 1. Создаём чистый Laravel 13
Прежде чем что-то накручивать, нам нужен свежий, девственно чистый Laravel. Без него, сам понимаешь, никак.
Я обычно создаю папку для проекта прямо на рабочем столе — так проще потом найти, да и глаза всегда перед собой. Назовём её, скажем, laravel-inertia-start. Но если хочешь, можешь назвать my-app или как душе угодно. Дальше открываю эту папку в VS Code. Самый быстрый способ — перетащить папку прямо в окно редактора, но можно и через «Открыть с помощью Code».
Теперь самое главное — терминал. В VS Code это удобно: жмёшь Ctrl+` (это та же клавиша, что и буква «ё», только с Ctrl), и терминал выезжает снизу. Красота.
И вот тут внимание, брат. Мы сейчас находимся внутри пустой папки. Чтобы создать свежий Laravel, прямо в терминале вводим команду:
composer create-project laravel/laravel . --prefer-dist
Точка в конце не случайна — она говорит Composer’у: «поставь Laravel прямо сюда, в текущую папку, а не создавай новую вложенную». Если забудешь точку, создастся папка laravel внутри твоей папки , и потом придётся всё перекладывать — не повторяй моих ошибок.laravel-inertia-start
После этой команды Composer начнёт колдовать. Он подтянет сам Laravel 13 (последнюю стабильную версию, если ты, конечно, смотрел на дату статьи), все зависимости, создаст структуру папок. Займёт это секунд двадцать-тридцать, зависит от интернета.
Когда всё закончится, ты увидишь в папке кучу файлов: artisan, composer.json, папки app, config, routes и так далее. Это и есть наш будущий шедевр.
Можешь сразу проверить, что всё работает, запустив сервер:
php artisan serve
Шаг 2. Устанавливаем Inertia (бэкенд)
Ладно, давай теперь к делу. Первым делом нам нужно поставить Inertia на бэкенд. Но сначала скажу пару слов, зачем всё это нужно.
Знаешь, раньше я мучился с классической связкой API + React. То есть на Laravel делаешь эндпоинты, на фронте — отдельный SPA, они общаются через JSON, ты вечно возишься с загрузчиками, состояниями загрузки, обрабатываешь ошибки на фронте, дублируешь логику… Короче, геморрой тот ещё. А если нужен SSR — вообще песня.
Inertia решает эту проблему так, что я до сих пор улыбаюсь, когда об этом думаю. Он позволяет писать полноценный React-фронт, но при этом ты работаешь с Laravel так, будто это классический монолит. То есть ты возвращаешь из контроллера не JSON, а просто вызываешь inertia()->render('Users/Index', $users) — и всё. Данные сами уедут на фронт, Inertia автоматически отрендерит нужный React-компонент, обновит URL в браузере, поменяет заголовок страницы. И всё это без единого fetch или axios. Как будто волшебство, но нет — просто очень умный пакет.
По сути, Inertia — это тонкий слой, который ворует лучшие стороны SPA (быстрые переходы между страницами без перезагрузки) и лучшие стороны классического монолита (один источник правды, никакой возни с апишками). Для меня это стал настоящий джедайский приём за последние пару лет.
Установка Inertia на Laravel
Так что давай ставить. Заходим в корень Laravel и пишем:
composer require inertiajs/inertia-laravel
Сразу скажу про PowerShell. Если ты на Windows, терминал по умолчанию может быть именно PowerShell — он работает, никаких проблем. Но иногда бывает, что при установке вылезают ошибки про выполнение сценариев. Если такое случилось, просто запусти терминал как «Command Prompt» (cmd) или PowerShell от администратора — один раз помогает.
Пакет ставится быстро. Composer подтянет inertia-laravel, а вместе с ним — возможно, ещё пару зависимостей. Всё легально, без паники. Теперь у нас есть основа для Laravel Inertia React шаблона.
Inertia на стороне Laravel установлен.
Публикация middleware
После установки я обычно публикую специальный middleware, который будет пробрасывать на фронт общие данные (например, пользователя или flash-сообщения):
php artisan inertia:middleware
Эта команда создаст файл App\Http\Middleware\HandleInertiaRequests.php.
Если вдруг после установки IDE ругается, что не видит класс Inertia — просто перезапусти терминал или выполни composer dump-autoload. Старая болячка, но лечится легко.
Вот и всё, бэкенд готов. Теперь в любом контроллере вместо view('welcome') ты можешь писать inertia('Dashboard', ['user' => $user]) — и Laravel поймёт, что нужно отрендерить React-компонент Dashboard, передав туда данные. Пока он, конечно, этого не умеет — потому что фронтовую часть мы ещё не ставили. Но мы дойдём.
ШАГ 3. Редактируем bootstrap/app.php. Добавляем наш middleware.
Laravel 13 немного поменял структуру — раньше всё настраивалось в app/Http/Kernel.php, а теперь отвечает за middleware файл bootstrap/app.php. И нам нужно сказать Laravel’у: «Слушай, когда ты обрабатываешь веб-запросы, не забудь пропустить их через Inertia».
Поэтому открываю bootstrap/app.php и полностью заменяю его содержимое на вот этот код:
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
// Обработчик ошибок по умолчанию
})
->create();
Критично: Метод ->withExceptions() должен присутствовать, иначе будет ошибка BindingResolutionException.
ШАГ.4 Настраиваем фронтенд для Laravel Inertia React (package.json)
Ладно, бэкенд мы подготовили, Inertia на стороне Laravel уже стоит и middleware подвязали. Теперь самое мясо — фронт. Нам нужно поставить React, сам Inertia для React и нормальный сборщик, чтобы это всё летало.
Открываю терминал (всё там же, в корне проекта) и начинаю колдовать с npm.
Первым делом ставлю основные пакеты — то, без чего вообще ничего не заведётся:
npm i @inertiajs/react react react-dom
Что тут происходит? @inertiajs/react — это адаптер, который подруживает Inertia с React. Он даёт хуки, компоненты типа Head, Link, и главное — тот самый <Inertia />, который будет рендерить наши страницы. Без него Inertia просто не поймёт, как общаться с React. react и react-dom — сами понимаете, куда без них.
Дальше нам нужен плагин для Vite, который научит его понимать JSX и быстро пересобирать React-компоненты. Это уже в разработку, так что с флагом -D (или --save-dev):
npm i @vitejs/plugin-react -D
@vitejs/plugin-react включает поддержку React-компонентов, быстрого обновления (Fast Refresh) и всякие вкусности. Без него Vite будет смотреть на твой JSX как баран на новые ворота.
После всех установок у тебя обновится package.json — там появятся новые зависимости.
Приблизительно вот так на данном этапе выглядит рабочая конфигурация (май 2026 год):
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-react": "^6.0.2",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.1",
"tailwindcss": "^4.0.0",
"vite": "^8.0.0"
},
"dependencies": {
"@inertiajs/react": "^3.2.0",
"react": "^19.2.6",
"react-dom": "^19.2.6"
}
}
React 19 принёс много улучшений. Читай о нововведениях React 19 в официальном блоге
ШАГ.5 Vite для Laravel Inertia React: полная настройка
Vite — это наш сборщик, который будет компилировать React, Tailwind и вообще всё, что мы напишем. Laravel 13 уже дружит с Vite «из коробки», но нам нужно его немного допилить под наш стек.
Открываю корневой файл vite.config.js и немного корректируем его:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.jsx'],
refresh: true,
fonts: [
bunny('Instrument Sans', {
weights: [400, 500, 600],
}),
],
}),
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'resources/js'),
},
},
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
});
Эта конфигурация vite.config.js для Laravel Inertia React включает всё необходимое для быстрой разработки. Теперь давай пробежимся по настройкам, чтобы ты понимал, что тут к чему, когда через месяц вернёшься в этот файл.
Плагин laravel() — база, без него Vite не поймёт, что он работает внутри Laravel. В input я указываю две точки входа: app.css (туда импортируется Tailwind) и app.jsx (главный файл React). refresh: true — приятная штука: когда я меняю Blade-шаблоны, страница сама перезагружается. А fonts с bunny подтягивает шрифт Instrument Sans с Bunny CDN — быстро и без лишних телодвижений.
react() — включает поддержку React и Fast Refresh. Без него твои компоненты даже не зарендерятся.
tailwindcss() — плагин для Tailwind CSS 4. Обрати внимание, я импортирую его из @tailwindcss/vite, а не из старого пакета. В четвёртой версии всё по-новому, работает быстрее.
resolve.alias — здесь я настраиваю алиас @, который будет указывать на папку resources/js. Теперь в компонентах я смогу писать import Button from '@/Components/Button вместо этого бесконечного ../../../Components/Button. Поверь, спасение для глаз.
server.watch.ignored — говорю Vite’у: «не следи за папкой storage/framework/views«. В неё Laravel кеширует Blade, и если Vite за ней наблюдает, бывают странные баги. Просто игнорируем — и живём спокойно.
Создаем в корне jsconfig.json — это важно для корректной работы alias.
{
"compilerOptions": {
"baseUrl": ".",
"ignoreDeprecations": "6.0",
"paths": {
"@/*": ["./resources/js/*"]
}
},
"include": ["resources/js/**/*.js", "resources/js/**/*.jsx"]
}
Vite 8 работает на новом движке Rolldown. Документация Vite объясняет все настройки подробно.
ШАГ.6 Создаём Blade-обёртку
Итак, у нас есть React, есть Inertia, Vite уже дышит в затылок. Но остаётся один вопрос: как Laravel поймёт, что нужно отдавать не просто HTML-страницу, а именно ту, которая загрузит всё React-хозяйство?
Ответ простой: нам нужен один-единственный Blade-шаблон. Он будет как входная дверь — встречает все запросы и запускает Inertia.
Создаю файл resources/views/app.blade.php и пишу туда вот такой минималистичный код:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>Мой Inertia App</title>
@viteReactRefresh
@vite(['resources/js/app.jsx', 'resources/css/app.css'])
</head>
<body>
@inertia
</body>
</html>
Смотри, тут всё гениально просто. Атрибут inertia у тега <title> — не опечатка. Он нужен, чтобы Inertia мог динамически менять заголовок страницы, когда ты переходишь между разделами. Удобно, да?
@viteReactRefresh — эта директива нужна для горячей перезагрузки React-компонентов. Без неё Fast Refresh не заработает, и при каждом изменении страница будет перезагружаться целиком. Не наш метод.
@vite(['resources/js/app.jsx', 'resources/css/app.css']) — здесь Vite подставляет ссылки на собранные файлы. В режиме разработки это будут ссылки на дев-сервер, в продакшене — на скомпилированные ассеты. Главное, что мы один раз написали, и оно само разруливает.
И самая важная строчка — @inertia. Эта директива выводит корневой контейнер React-приложения, куда Inertia будет рендерить текущий компонент. Можно сказать, что это магическая дыра, через которую React проникает в наш Laravel-мир.
И заметь, брат, в этом шаблоне нет ничего про React напрямую. Никаких div id="app", никаких скриптов вручную. Всё сделает Inertia и Vite за нас. Красота.
После этого шага все запросы, которые Inertia перенаправляет на рендер компонента, попадут именно в этот шаблон, загрузят наш будущий app.jsx и покажут нужную страницу.
Теперь осталось написать сам app.jsx — точку входа React-приложения.
ШАГ.7 Точка входа React + Inertia в Laravel
Ну что, дошли до самого сердца нашего приложения. У нас есть Blade-обёртка, есть настроенный Vite, Inertia на бэкенде ждёт не дождётся. Осталось последнее — сказать React’у: «просыпайся, брат, пора работать».
Создаю файл resources/js/app.jsx и пишу туда вот такой код:
import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true });
return pages[`./Pages/${name}.jsx`];
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
});
Точка входа React для Laravel Inertia готова. Теперь давай разберём, что здесь происходит, потому что это не магия, а просто умный код.
createInertiaApp — это главная функция, которая связывает всё воедино. Она принимает конфиг с двумя ключевыми вещами: как находить компоненты и как их рендерить.
resolve — здесь я говорю Inertia: «Слушай, когда Laravel попросит отрендерить компонент Users/Index, ты должен найти файл ./Pages/Users/Index.jsx и отдать его». Я использую import.meta.glob с eager: true, чтобы все компоненты подгрузились сразу. Для небольших и средних проектов это норм. Если проект огромный, можно потом переделать на ленивую загрузку, но для стартового шаблона — самое то.
Обрати внимание на структуру: все страницы у меня лежат в папке resources/js/Pages/. Каждый файл — это React-компонент, который соответствует определённому маршруту. Если в Laravel вызываешь inertia('Dashboard'), то Inertia ищет ./Pages/Dashboard.jsx. Логично и просто.
setup — здесь происходит сам рендеринг. Я получаю DOM-элемент (тот самый, который создал @inertia в Blade-шаблоне), React-компонент App (внутренняя обёртка Inertia) и все пропсы, которые пришли с бэкенда. Дальше createRoot(el).render(...) — стандартный способ запустить React 19, без устаревшего ReactDOM.render.
И знаешь что? После этого всего наш фронт готов. Inertia теперь знает, где искать компоненты, React знает, куда рендерить, Vite следит за изменениями. Но это еще не все. Для того чтобы все заработало нужно еще установить shadcn/ui, создать layout, пару страниц и прописать маршрут в routes/web.php.
ШАГ.8 Добавляем shadcn/ui в Laravel Inertia React шаблон — рабочий способ без головной боли
Ты спросишь что такое shadcn/ui ? И зачем ? Ответ — это современный инструмент для быстрого прототипирования ui компонентов. Это крутая вещь и смысл ты сейчас поймешь ! Компоненты shadcn/ui отлично дополняют шаблон Laravel React Inertia. Ты получаешь готовые UI-элементы с полным контролем.
Знаешь, с shadcn была у меня одна засада. Пытаюсь запустить стандартное npx shadcn@latest init, а он либо качает CLI во временную папку, либо ругается, что не видит зависимости, либо версию не ту подтягивает. Особенно часто эта беда вылезает с zod — пакетом для валидации схем, который shadcn использует внутри.
Я перепробовал несколько способов и нашёл один, который работает стабильно. Делюсь.
Ставим shadcn и zod прямо в проект
Вместо того чтобы каждый раз качать CLI через npx, я просто устанавливаю shadcn как обычную зависимость разработки. Прямо в node_modules моего проекта. Команда простая:
npm install -D shadcn zod
Что тут происходит? shadcn — это сам CLI для добавления компонентов. zod — это библиотека для валидации, которую shadcn использует под капотом. Устанавливаю их оба с флагом -D (devDependencies), потому что в продакшене они не нужны.
Теперь shadcn лежит локально, видит все зависимости твоего проекта, не путается в версиях и не пытается каждый раз что-то скачивать.
Инициализация через npm exec
Дальше самое важное. Запускаю инициализацию:
npm exec shadcn init
Нажимаем два раза Enter (Radix->Nova)
Обрати внимание: именно npm exec, а не npx. Почему? npm exec (или сокращённо npm x) заставляет npm использовать локально установленную версию из папки node_modules/.bin/. А npx по умолчанию сначала проверит кэш, может скачать свежую версию, проигнорировать локальную и в итоге не увидеть твой zod.
Я один раз наступил на эти грабли — часа два сидел, гадал, почему команда падает с ошибкой «Cannot find module ‘zod'». А всё потому, что npx использовал свой временный CLI, а не тот, что лежал рядом с zod в проекте.
Добавляем компоненты
Когда инициализация прошла успешно, добавлять компоненты — одно удовольствие:
npm exec shadcn add button card input
После этого shadcn создаст папку resources/js/components/ui и положит туда файлы button.jsx, card.jsx, input.jsx. Это не готовые бинарные зависимости, а просто исходники React-компонентов на Tailwind. Ты можешь их редактировать, подгонять под себя, менять стили — полный контроль.
И самое приятное: теперь в твоих страницах можно писать:
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
В следующем шаге мы их используем.
ШАГ.9 Делаем глобальный лейаут + страницы Welcome и About
Вот мы и добрались до самого вкусного — до живых страниц. Сейчас наша точка входа app.jsx есть, Inertia настроена, но внутри — пустота. Давай это исправим.
Я хочу, чтобы на всех страницах была общая шапка с меню и футер. Не копировать же одно и то же в каждую страницу. Поэтому первым делом я создаю глобальный лейаут — такую обёртку, в которую будут заворачиваться все остальные страницы.
Структуру я организую так, чтобы всё лежало на своих местах:
resources/js/
├── app.jsx # Точка входа (уже есть)
├── Layouts/
│ └── AppLayout.jsx # ← Новый: глобальный лейаут с меню
├── Pages/
│ ├── Home.jsx # ← Новая: главная страница
│ ├── About.jsx # ← Новая: страница "О нас"
Давай сначала создадим сам лейаут.
9.1 Создаем лейаут для наших страниц.
Глобальный лейаут — основа любого Laravel Inertia React проекта. Он обеспечивает единую структуру всех страниц.
Создаем файл resources/js/Layouts/AppLayout.jsx
import { Link, usePage } from '@inertiajs/react';
import { Button } from '@/components/ui/button';
export default function AppLayout({ children, title = 'Мой сайт' }) {
const { url } = usePage();
// Хелпер для активной ссылки
const isActive = (path) => url === path ? 'text-primary font-medium' : 'text-muted-foreground hover:text-foreground';
return (
<div className="min-h-screen flex flex-col bg-background">
{/* Шапка с навигацией */}
<header className="border-b bg-card">
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
<Link href="/" className="text-xl font-bold text-foreground">
MyApp
</Link>
<nav className="flex items-center gap-6">
<Link href="/" className={isActive('/')}>
Главная
</Link>
<Link href="/about" className={isActive('/about')}>
О нас
</Link>
</nav>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" asChild>
<Link href="/login">Войти</Link>
</Button>
<Button size="sm" asChild>
<Link href="/register">Регистрация</Link>
</Button>
</div>
</div>
</header>
{/* Основной контент */}
<main className="flex-1 container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">{title}</h1>
{children}
</main>
{/* Футер */}
<footer className="border-t py-6 text-center text-sm text-muted-foreground">
<p>© {new Date().getFullYear()} MyApp. Все права защищены.</p>
</footer>
</div>
);
}
Что тут важно. usePage() даёт мне доступ к текущему URL. С помощью простой функции isActive я делаю активную ссылку подсвеченной — это удобно и пользователю понятно. Link из Inertia вместо обычного a — потому что он делает переходы без перезагрузки страницы, как в SPA. Всё остальное — просто вёрстка на Tailwind: шапка, контейнер, футер. А children — это то место, куда будет подставляться содержимое конкретной страницы.
9.2 Создаем главную страницу.
Теперь саму главную страницу. Создаю resources/js/Pages/Home.jsx
import AppLayout from '@/Layouts/AppLayout';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export default function Home() {
return (
<AppLayout title="Добро пожаловать">
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Быстрый старт</CardTitle>
<CardDescription>Чистый стек без лишней магии</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<p>• Laravel 13 + Inertia 3 + React 19</p>
<p>• Vite 8 для быстрой сборки</p>
<p>• Полный контроль над кодом</p>
<p>• Готов к деплою на shared-хостинг</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>🛠 Что дальше?</CardTitle>
<CardDescription>Идеи для развития</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<p>• Добавить ручную авторизацию</p>
<p>• Подключить Tailwind CSS</p>
<p>• Создать админ-панель</p>
<p>• Настроить деплой-скрипт</p>
</CardContent>
</Card>
</div>
<div className="mt-8 flex gap-4">
<Button asChild>
<a href="/about">Узнать больше</a>
</Button>
<Button variant="outline" asChild>
<a href="https://laravel.com" target="_blank" rel="noopener noreferrer">
Документация Laravel
</a>
</Button>
</div>
</AppLayout>
);
}
Видишь, как красиво? Я просто беру лейаут, оборачиваю им свой контент и передаю заголовок через пропс title. Вся шапка, меню, футер — уже есть. На странице — только то, что уникально для неё. Чистота и порядок.
9.3 Создаем страницу «О нас».
По такому же принципу в resources/js/Pages/About.jsx
import AppLayout from '@/Layouts/AppLayout';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export default function About() {
return (
<AppLayout title="О проекте">
<div className="prose prose-neutral max-w-none">
<Card>
<CardHeader>
<CardTitle>Привет!</CardTitle>
<CardDescription>Немного о том, как сделан этот сайт</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p>
Этот проект создан с нуля на стеке <strong>Laravel 13 + Inertia + React</strong>.
Никаких готовых стартер-китов — только чистый код и полное понимание каждого слоя.
</p>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Бэкенд</CardTitle>
</CardHeader>
<CardContent>
<ul className="text-sm space-y-1">
<li>• Laravel 13</li>
<li>• Inertia.js</li>
<li>• PHP 8.2+</li>
<li>• SQLite/MySQL</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Фронтенд</CardTitle>
</CardHeader>
<CardContent>
<ul className="text-sm space-y-1">
<li>• React 19</li>
<li>• Vite 8</li>
<li>• shadcn/ui</li>
<li>• Tailwind CSS</li>
</ul>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-lg">Деплой</CardTitle>
</CardHeader>
<CardContent>
<ul className="text-sm space-y-1">
<li>• Shared-хостинг</li>
<li>• cPanel/Plesk</li>
<li>• FTP/SFTP</li>
<li>• Без Node.js на сервере</li>
</ul>
</CardContent>
</Card>
</div>
<p className="text-sm text-muted-foreground">
Совет: этот шаблон можно использовать как основу для любого проекта.
Просто скопируй структуру и добавь свою бизнес-логику.
</p>
</CardContent>
</Card>
</div>
</AppLayout>
);
}
Теперь осталось добавить маршруты в routes/web.php
ШАГ.10 Настраиваем маршруты для Laravel Inertia React
Ну вот, страницы мы создали, лейаут написали, а Laravel пока даже не знает, что по адресу / или /about нужно что-то показывать. Давай это исправим.
Открываю файл routes/web.php. В Laravel это главный файл для всех веб-маршрутов. По умолчанию там уже есть какой-то код, но я полностью заменяю его на свой:
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
// Главная страница
Route::get('/', function () {
return Inertia::render('Home');
})->name('home');
// Страница "О нас"
Route::get('/about', function () {
return Inertia::render('About');
})->name('about');
Смотри, как тут всё прозрачно. Я использую фасад Inertia и вызываю метод render(). Первым параметром передаю название компонента — 'Home'. Inertia пойдёт искать файл resources/js/Pages/Home.jsx, загрузит его и отрендерит внутри нашей Blade-обёртки. Вторым параметром можно было бы передать данные (например, список пользователей), но пока нам не нужно.
Метод ->name('home') — это я просто даю маршруту имя. Зачем? Чтобы потом в коде писать route('home') вместо захардкоженного '/'. Если вдруг я решу, что главная страница будет по адресу /main, мне достаточно поменять это в одном месте — в web.php, а все ссылки в проекте сами подхватятся. Мелочь, а приятно.
Что со старой домашней страницей?
В стандартном Laravel после установки уже есть маршрут для '/', который показывает либо Blade-шаблон welcome.blade.php, либо что-то ещё. Я его просто удаляю, он нам больше не нужен.
Как работает навигация (без перезагрузки)
Знаешь, одно из самых крутых ощущений, когда впервые работаешь с Inertia, — это переходы между страницами. Кликаешь по ссылке, а страница не моргает, не белеет, не перезагружается. Контент просто плавно меняется, но при этом URL в адресной строке обновляется, стрелка «назад» работает, и всё выглядит как настоящий многостраничный сайт.
Спрашивается: как это работает? Давай разберу по шагам, потому что когда понимаешь механику, магия перестаёт быть магией и становится инструментом.
Шаг первый. Inertia перехватывает клик
Когда я использую компонент <Link> из Inertia вместо обычной <a>, происходит хитрость. Inertia вешает обработчик на клик и не даёт браузеру уйти по ссылке как обычно. Вместо этого он говорит: «Спокойно, парень, я сам всё сделаю». Обычные ссылки без Link будут работать как обычно — с полноценной перезагрузкой.
Шаг второй. Умный запрос на сервер
Inertia отправляет на сервер запрос к нужному адресу, например /about. Но не простой, а со специальными заголовками. Самые важные из них — X-Inertia: true и X-Requested-With: XMLHttpRequest. По этим заголовкам Laravel понимает: «Ага, ко мне пришёл не обычный пользователь, а Inertia на фронте. Значит, мне не нужно отдавать полную HTML-страницу».
Шаг третий. Laravel отдаёт JSON
И вот тут начинается настоящее волшебство. Laravel, увидев эти заголовки, не возвращает полный HTML с шапкой, футером и скриптами. Вместо этого он возвращает компактный JSON-ответ. Выглядит он примерно так:
{
"component": "About",
"props": {
"user": null,
"title": "О проекте"
},
"url": "/about",
"version": null
}
Это идеально. Никакого лишнего HTML, только название нужного компонента и данные, которые ему нужны. Сервер отдал килобайты вместо мегабайтов — быстро и экономно.
Шаг четвёртый. Inertia обновляет только необходимое
На клиенте Inertia получает этот JSON и делает несколько вещей практически мгновенно. Он находит React-компонент About в папке resources/js/Pages/About.jsx, рендерит его с переданными пропсами и подменяет только содержимое внутри лейаута. Шапка, футер, боковая панель — всё, что не изменилось, остаётся на месте. Браузер не перерисовывает страницу целиком, не перезапускает скрипты, не теряет состояние.
При этом Inertia обновляет URL в адресной строке через history.pushState — тот же механизм, который используют настоящие SPA. И кнопки «назад» и «вперёд» продолжают работать, потому что Inertia слушает событие popstate.
В результате пользователь получает мгновенные переходы без мерцания, но при этом разработчик пишет код почти так же, как в классическом Laravel. Никаких отдельного API, никакого fetch, никакой ручной работы с состоянием загрузки. Inertia сделал всю грязную работу за меня.
Для меня это и есть главная причина использовать Inertia вместо классического API + React. Скорость разработки как в монолите, скорость работы как в SPA.
ШАГ.12 Пишем скрипт для синхронного запуска браузера и корректируем composer.json
Ты знаешь, какая у меня была боль при первых попытках сделать автозапуск приложения во вкладке браузера? Когда я запускал composer run dev, браузер открывался не вовремя и в итоге я видел ошибку, приходилось обновлять страницу вручную через пару секунд. Мелочь, а раздражало.
Я нашёл решение: браузер должен открываться только после того, как сервер реально запустился и начал отвечать на запросы. Для этого я написал небольшой скрипт, который проверяет доступность http://localhost:8000 и только потом открывает браузер.
Создаю папку .npm в корне проекта (если её нет) и внутри файл open-browser.js:
import { exec } from 'child_process';
import http from 'http';
import { promisify } from 'util';
import { setTimeout as sleep } from 'timers/promises';
const execAsync = promisify(exec);
const URL = 'http://localhost:8000';
const MAX_ATTEMPTS = 30;
const DELAY_MS = 500;
const openBrowser = async (url) => {
const commands = {
win32: `start ${url}`,
darwin: `open ${url}`,
linux: `xdg-open ${url}`
};
const command = commands[process.platform] || commands.linux;
await execAsync(command);
};
const checkServer = () => {
return new Promise((resolve) => {
const request = http.get(URL, () => resolve(true));
request.on('error', () => resolve(false));
request.setTimeout(1000, () => {
request.destroy();
resolve(false);
});
});
};
const waitForServer = async () => {
console.log('Waiting for server...');
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
if (await checkServer()) {
console.log('Server ready! Opening browser...');
await openBrowser(URL);
return;
}
console.log(`Attempt ${attempt}/${MAX_ATTEMPTS} - Server not ready yet...`);
await sleep(DELAY_MS);
}
console.error('Timeout waiting for server');
process.exit(1);
};
waitForServer();
Теперь разберу, что тут происходит, потому что скрипт простой, но хитрый.
openBrowser(url) — определяет операционную систему (Windows, macOS, Linux) и запускает нужную команду для открытия браузера. На Windows это start, на Mac — open, на Linux — xdg-open. Кроссплатформенно, красиво.
checkServer() — отправляет HTTP-запрос на http://localhost:8000 и проверяет, отвечает ли сервер. Если отвечает — возвращает true, если нет или таймаут — false. Никакой магии, просто честная проверка.
waitForServer() — главная функция. Она раз за разом (до 30 попыток) проверяет сервер каждые полсекунды. Как только сервер ответил — тут же открывает браузер. Если после 30 попыток сервер так и не запустился — выводит ошибку и завершается.
MAX_ATTEMPTS = 30 и DELAY_MS = 500 — максимум 30 попыток с интервалом полсекунды. Итого скрипт ждёт сервер максимум 15 секунд. Этого с запасом хватает даже на медленных машинах.
Корректируем composer.json — умный запуск браузера с использованием скрипта open-browser.js
Теперь обновляю скрипт dev в composer.json:
"scripts": {
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74,#fdbbb4\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"npm run dev\" \"node .npm/open-browser.js\" --names=server,queue,vite,open --kill-others-on-fail"
]
},
Что изменилось? Теперь вызывается node .npm/open-browser.js. То есть запускается наш умный скрипт, который сам дождётся сервера и сам откроет браузер в нужный момент.
Разбор остальных настроек
Composer\Config::disableProcessTimeout — отключает таймаут Composer. По умолчанию Composer ждёт, пока команда завершится, но наши процессы (сервер, очередь, Vite) работают постоянно. Без этой строки Composer через 300 секунд убьёт всё хозяйство. С ней — живём спокойно.
Убрали php artisan pail — в прошлой версии у меня был запуск php artisan pail для красивых логов с цветами. Но она требует дополнительной установки и не всем нужна. Логи можно смотреть через стандартный storage/logs/laravel.log.
--tries=1 без --timeout=0 — раньше было --tries=1 --timeout=0. Я убрал --timeout=0, потому что бесконечное ожидание выполнения задачи редко нужно на старте проекта. Оставил только одну попытку на выполнение — для разработки этого достаточно.
Немного про цвета
Строка -c "#93c5fd,#c4b5fd,#fdba74,#fdbbb4" — это цвета для каждого процесса в concurrently:
#93c5fd(голубой) — сервер Laravel#c4b5fd(фиолетовый) — очередь#fdba74(оранжевый) — Vite#fdbbb4(нежно-розовый) — скрипт открытия браузера
Визуально приятно и понятно — сразу видно, какой процесс что пишет в терминал.
Почему это лучше, чем было раньше?
Раньше: запустил composer run dev → браузер открылся сразу → сервер ещё не готов → ошибка → ручное обновление через 5 секунд.
Теперь: запустил composer run dev → браузер терпеливо ждёт → сервер запустился → скрипт открыл браузер → сразу видишь работающий сайт. Без лишних телодвижений.
Мелочь, а приятно, брат. Особенно когда ты перезапускаешь проект по 20 раз за день.
ШАГ.12 Запуск проекта Laravel Inertia React с нуля
Теперь все готово и всё, что нам нужно сделать, — открыть терминал в корне проекта и написать одну единственную команду:
composer run dev
Через пару секунд браузер сам откроется на http://localhost:8000, и ты увидишь свою главную страницу. Шапка, меню, футер, карточки — всё на месте. И главное, кликаешь по ссылке «О нас» — и страница меняется без перезагрузки. Быстро, плавно, красиво.
Брат, мы с тобой проделали огромную работу. С нуля, без готовых стартер-китов и генераторов, мы собрали свой собственный шаблон на современном стеке:
- Laravel 13 — мощный бэкенд, который мы контролируем целиком
- React 19 — быстрый фронт с компонентным подходом
- Inertia — связка, которая даёт SPA без геморроя с отдельным API
- Tailwind 4 — утилитарный CSS с новым движком на Rust
- shadcn/ui — готовые компоненты, которые мы сами держим под рукой
- Vite — молниеносная сборка и hot reload
Но главное даже не в этом. Главное — что теперь у тебя есть собственный стартовый шаблон Laravel 13 React 19 Inertia. Ты знаешь каждую строчку кода, понимаешь, как всё связано, и не боишься ничего сломать. Потому что ты это сам собрал.
И теперь, когда тебе понадобится начать новый проект, ты просто копируешь этот шаблон и за 5 минут уже пишешь бизнес-логику. Никаких «а почему Breeze/Fortify/Jetstream сделал именно так?», никаких скрытых зависимостей, никакой магии.
P.S. О кнопках «Логин» и «Регистрация»
Ты наверняка заметил, что кнопки «Войти» и «Регистрация» в шапке пока не работают. При клике на них появляется модальное окно с ошибкой. Это не баг, а особенность поведения Inertia, о которой полезно знать.
Вот как это работает. Когда ты кликаешь на <Link href="/login">, Inertia отправляет запрос на сервер по этому адресу. Но в нашем routes/web.php нет маршрута для /login. Laravel не знает, что делать с этим запросом, и возвращает ошибку 404. Inertia на клиенте получает эту ошибку и показывает её в модальном окне — так сделано, чтобы страница не падала целиком, а пользователь видел понятное сообщение.
Почему не открывается отдельная страница с ошибкой? Потому что Inertia по умолчанию перехватывает ошибки и показывает их в виде модального окна прямо поверх текущей страницы. Это сделано специально — так ты не теряешь контекст и можешь продолжать работу.
Что с этим делать?
Два пути. Первый — просто оставить как есть, если ты пока не планируешь добавлять авторизацию. Кнопки не работают, но и не мешают.
Второй — временно убрать эти кнопки из лейаута до тех пор, пока ты не реализуешь аутентификацию. Для этого закомментируй или удали в AppLayout.jsx блок с ними.
Но если хочешь сделать всё красиво, тема аутентификации — это следующий логический шаг после нашего шаблона. Там нужно будет создать контроллеры, маршруты для логина/регистрации, страницы входа и регистрации, настроить сессии и защищённые маршруты. Это тема для отдельной статьи, и она уже в планах.
А пока просто запомни: кнопки не работают, потому что нет маршрутов. Добавишь маршруты — заработают. Всё честно, без скрытой магии.