diff --git a/docs.json b/docs.json
index e8a4a2c..f73d6a6 100644
--- a/docs.json
+++ b/docs.json
@@ -11,6 +11,93 @@
"navigation": {
"global": {},
"versions": [
+ {
+ "version": "3.x",
+ "groups": [
+ {
+ "group": "Getting Started",
+ "pages": [
+ "v3/getting-started/index",
+ "v3/getting-started/demo-application",
+ "v3/getting-started/upgrade-guide"
+ ]
+ },
+ {
+ "group": "Installation",
+ "pages": [
+ "v3/installation/server-side-setup",
+ "v3/installation/client-side-setup"
+ ]
+ },
+ {
+ "group": "Core Concepts",
+ "pages": [
+ "v3/core-concepts/who-is-it-for",
+ "v3/core-concepts/how-it-works",
+ "v3/core-concepts/the-protocol"
+ ]
+ },
+ {
+ "group": "The Basics",
+ "pages": [
+ "v3/the-basics/pages",
+ "v3/the-basics/responses",
+ "v3/the-basics/redirects",
+ "v3/the-basics/routing",
+ "v3/the-basics/title-and-meta",
+ "v3/the-basics/links",
+ "v3/the-basics/manual-visits",
+ "v3/the-basics/instant-visits",
+ "v3/the-basics/forms",
+ "v3/the-basics/http-requests",
+ "v3/the-basics/optimistic-updates",
+ "v3/the-basics/file-uploads",
+ "v3/the-basics/validation",
+ "v3/the-basics/layouts",
+ "v3/the-basics/view-transitions"
+ ]
+ },
+ {
+ "group": "Data & Props",
+ "pages": [
+ "v3/data-props/shared-data",
+ "v3/data-props/flash-data",
+ "v3/data-props/partial-reloads",
+ "v3/data-props/deferred-props",
+ "v3/data-props/merging-props",
+ "v3/data-props/once-props",
+ "v3/data-props/polling",
+ "v3/data-props/prefetching",
+ "v3/data-props/load-when-visible",
+ "v3/data-props/infinite-scroll",
+ "v3/data-props/remembering-state"
+ ]
+ },
+ {
+ "group": "Security",
+ "pages": [
+ "v3/security/authentication",
+ "v3/security/authorization",
+ "v3/security/csrf-protection",
+ "v3/security/history-encryption"
+ ]
+ },
+ {
+ "group": "Advanced",
+ "pages": [
+ "v3/advanced/asset-versioning",
+ "v3/advanced/code-splitting",
+ "v3/advanced/error-handling",
+ "v3/advanced/events",
+ "v3/advanced/progress-indicators",
+ "v3/advanced/scroll-management",
+ "v3/advanced/server-side-rendering",
+ "v3/advanced/testing",
+ "v3/advanced/typescript"
+ ]
+ }
+ ]
+ },
{
"version": "2.x",
"groups": [
@@ -181,165 +268,189 @@
}
},
"redirects": [
+ {
+ "source": "/index",
+ "destination": "/v3/getting-started/index"
+ },
{
"source": "/demo-application",
- "destination": "/v2/getting-started/demo-application"
+ "destination": "/v3/getting-started/demo-application"
},
{
"source": "/upgrade-guide",
- "destination": "/v2/getting-started/upgrade-guide"
+ "destination": "/v3/getting-started/upgrade-guide"
},
{
"source": "/server-side-setup",
- "destination": "/v2/installation/server-side-setup"
+ "destination": "/v3/installation/server-side-setup"
},
{
"source": "/client-side-setup",
- "destination": "/v2/installation/client-side-setup"
+ "destination": "/v3/installation/client-side-setup"
},
{
"source": "/who-is-it-for",
- "destination": "/v2/core-concepts/who-is-it-for"
+ "destination": "/v3/core-concepts/who-is-it-for"
},
{
"source": "/how-it-works",
- "destination": "/v2/core-concepts/how-it-works"
+ "destination": "/v3/core-concepts/how-it-works"
},
{
"source": "/the-protocol",
- "destination": "/v2/core-concepts/the-protocol"
+ "destination": "/v3/core-concepts/the-protocol"
},
{
"source": "/pages",
- "destination": "/v2/the-basics/pages"
+ "destination": "/v3/the-basics/pages"
},
{
"source": "/responses",
- "destination": "/v2/the-basics/responses"
+ "destination": "/v3/the-basics/responses"
},
{
"source": "/redirects",
- "destination": "/v2/the-basics/redirects"
+ "destination": "/v3/the-basics/redirects"
},
{
"source": "/routing",
- "destination": "/v2/the-basics/routing"
+ "destination": "/v3/the-basics/routing"
},
{
"source": "/title-and-meta",
- "destination": "/v2/the-basics/title-and-meta"
+ "destination": "/v3/the-basics/title-and-meta"
},
{
"source": "/links",
- "destination": "/v2/the-basics/links"
+ "destination": "/v3/the-basics/links"
},
{
"source": "/manual-visits",
- "destination": "/v2/the-basics/manual-visits"
+ "destination": "/v3/the-basics/manual-visits"
},
{
"source": "/forms",
- "destination": "/v2/the-basics/forms"
+ "destination": "/v3/the-basics/forms"
},
{
"source": "/file-uploads",
- "destination": "/v2/the-basics/file-uploads"
+ "destination": "/v3/the-basics/file-uploads"
},
{
"source": "/validation",
- "destination": "/v2/the-basics/validation"
+ "destination": "/v3/the-basics/validation"
},
{
"source": "/view-transitions",
- "destination": "/v2/the-basics/view-transitions"
+ "destination": "/v3/the-basics/view-transitions"
+ },
+ {
+ "source": "/instant-visits",
+ "destination": "/v3/the-basics/instant-visits"
+ },
+ {
+ "source": "/http-requests",
+ "destination": "/v3/the-basics/http-requests"
+ },
+ {
+ "source": "/optimistic-updates",
+ "destination": "/v3/the-basics/optimistic-updates"
+ },
+ {
+ "source": "/layout-props",
+ "destination": "/v3/the-basics/layouts"
+ },
+ {
+ "source": "/v3/the-basics/layout-props",
+ "destination": "/v3/the-basics/layouts"
},
{
"source": "/shared-data",
- "destination": "/v2/data-props/shared-data"
+ "destination": "/v3/data-props/shared-data"
},
{
"source": "/partial-reloads",
- "destination": "/v2/data-props/partial-reloads"
+ "destination": "/v3/data-props/partial-reloads"
},
{
"source": "/deferred-props",
- "destination": "/v2/data-props/deferred-props"
+ "destination": "/v3/data-props/deferred-props"
},
{
"source": "/merging-props",
- "destination": "/v2/data-props/merging-props"
+ "destination": "/v3/data-props/merging-props"
},
{
"source": "/polling",
- "destination": "/v2/data-props/polling"
+ "destination": "/v3/data-props/polling"
},
{
"source": "/prefetching",
- "destination": "/v2/data-props/prefetching"
+ "destination": "/v3/data-props/prefetching"
},
{
"source": "/load-when-visible",
- "destination": "/v2/data-props/load-when-visible"
+ "destination": "/v3/data-props/load-when-visible"
},
{
"source": "/infinite-scroll",
- "destination": "/v2/data-props/infinite-scroll"
+ "destination": "/v3/data-props/infinite-scroll"
},
{
"source": "/remembering-state",
- "destination": "/v2/data-props/remembering-state"
+ "destination": "/v3/data-props/remembering-state"
},
{
"source": "/authentication",
- "destination": "/v2/security/authentication"
+ "destination": "/v3/security/authentication"
},
{
"source": "/authorization",
- "destination": "/v2/security/authorization"
+ "destination": "/v3/security/authorization"
},
{
"source": "/csrf-protection",
- "destination": "/v2/security/csrf-protection"
+ "destination": "/v3/security/csrf-protection"
},
{
"source": "/history-encryption",
- "destination": "/v2/security/history-encryption"
+ "destination": "/v3/security/history-encryption"
},
{
"source": "/asset-versioning",
- "destination": "/v2/advanced/asset-versioning"
+ "destination": "/v3/advanced/asset-versioning"
},
{
"source": "/code-splitting",
- "destination": "/v2/advanced/code-splitting"
+ "destination": "/v3/advanced/code-splitting"
},
{
"source": "/error-handling",
- "destination": "/v2/advanced/error-handling"
+ "destination": "/v3/advanced/error-handling"
},
{
"source": "/events",
- "destination": "/v2/advanced/events"
+ "destination": "/v3/advanced/events"
},
{
"source": "/progress-indicators",
- "destination": "/v2/advanced/progress-indicators"
+ "destination": "/v3/advanced/progress-indicators"
},
{
"source": "/scroll-management",
- "destination": "/v2/advanced/scroll-management"
+ "destination": "/v3/advanced/scroll-management"
},
{
"source": "/server-side-rendering",
- "destination": "/v2/advanced/server-side-rendering"
+ "destination": "/v3/advanced/server-side-rendering"
},
{
"source": "/testing",
- "destination": "/v2/advanced/testing"
+ "destination": "/v3/advanced/testing"
},
{
"source": "/typescript",
- "destination": "/v2/advanced/typescript"
+ "destination": "/v3/advanced/typescript"
}
]
-}
+}
\ No newline at end of file
diff --git a/v2/advanced/events.mdx b/v2/advanced/events.mdx
index 4ded28c..d386525 100644
--- a/v2/advanced/events.mdx
+++ b/v2/advanced/events.mdx
@@ -486,24 +486,24 @@ The `error` event fires when validation errors are present on "successful" page
```js Vue icon="vuejs"
import { router } from '@inertiajs/vue3'
-router.on('error', (errors) => {
- console.log(errors)
+router.on('error', (event) => {
+ console.log(event.detail.errors)
})
```
```jsx React icon="react"
import { router } from '@inertiajs/react'
-router.on('error', (errors) => {
- console.log(errors)
+router.on('error', (event) => {
+ console.log(event.detail.errors)
})
```
```js Svelte icon="s"
import { router } from '@inertiajs/svelte'
-router.on('error', (errors) => {
- console.log(errors)
+router.on('error', (event) => {
+ console.log(event.detail.errors)
})
```
@@ -732,7 +732,7 @@ The `prefetching` event fires when the router starts prefetching a page.
import { router } from '@inertiajs/vue3'
router.on('prefetching', (event) => {
- console.log(`Prefetching ${event.detail.page.url}`)
+ console.log(`Prefetching ${event.detail.visit.url}`)
})
```
@@ -740,7 +740,7 @@ router.on('prefetching', (event) => {
import { router } from '@inertiajs/react'
router.on('prefetching', (event) => {
- console.log(`Prefetching ${event.detail.page.url}`)
+ console.log(`Prefetching ${event.detail.visit.url}`)
})
```
@@ -748,7 +748,7 @@ router.on('prefetching', (event) => {
import { router } from '@inertiajs/svelte'
router.on('prefetching', (event) => {
- console.log(`Prefetching ${event.detail.page.url}`)
+ console.log(`Prefetching ${event.detail.visit.url}`)
})
```
@@ -766,7 +766,7 @@ The `prefetched` event fires when the router has successfully prefetched a page.
import { router } from '@inertiajs/vue3'
router.on('prefetched', (event) => {
- console.log(`Prefetched ${event.detail.page.url}`)
+ console.log(`Prefetched ${event.detail.visit.url}`)
})
```
@@ -774,7 +774,7 @@ router.on('prefetched', (event) => {
import { router } from '@inertiajs/react'
router.on('prefetched', (event) => {
- console.log(`Prefetched ${event.detail.page.url}`)
+ console.log(`Prefetched ${event.detail.visit.url}`)
})
```
@@ -782,7 +782,7 @@ router.on('prefetched', (event) => {
import { router } from '@inertiajs/svelte'
router.on('prefetched', (event) => {
- console.log(`Prefetched ${event.detail.page.url}`)
+ console.log(`Prefetched ${event.detail.visit.url}`)
})
```
diff --git a/v3/advanced/asset-versioning.mdx b/v3/advanced/asset-versioning.mdx
new file mode 100644
index 0000000..685a50b
--- /dev/null
+++ b/v3/advanced/asset-versioning.mdx
@@ -0,0 +1,61 @@
+---
+title: Asset Versioning
+---
+
+One common challenge when building single-page apps is refreshing site assets when they've been changed. Thankfully, Inertia makes this easy by optionally tracking the current version of your site assets. When an asset changes, Inertia will automatically make a full page visit instead of a XHR visit on the next request.
+
+## Configuration
+
+To enable automatic asset refreshing, you need to tell Inertia the current version of your assets. This can be any arbitrary string (letters, numbers, or a file hash), as long as it changes when your assets have been updated.
+
+Typically, your application's asset version can be specified within the `version` method of the Inertia `HandleInertiaRequests` middleware.
+
+```php
+class HandleInertiaRequests extends Middleware
+{
+ public function version(Request $request)
+ {
+ return parent::version($request);
+ }
+}
+```
+
+Alternatively, the asset version can be provided manually using the `Inertia::version()` method.
+
+```php
+use Inertia\Inertia;
+
+Inertia::version($version);
+Inertia::version(fn () => $version); // Lazily...
+```
+
+## Cache Busting
+
+Asset refreshing in Inertia works on the assumption that a hard page visit will trigger your assets to reload. However, Inertia doesn't actually do anything to force this. Typically this is done with some form of cache busting. For example, appending a version query parameter to the end of your asset URLs.
+
+With Laravel's Vite integration, asset versioning is done automatically. If you're using Laravel Mix, you can do this automatically by enabling [versioning](https://laravel.com/docs/mix#versioning-and-cache-busting) in your `webpack.mix.js` file.
+
+## Manual Refreshing
+
+If you want to take asset refreshing into your control, you can return a fixed value from the `version` method in the `HandleInertiaRequests` middleware. This disables Inertia's automatic asset versioning.
+
+For example, if you want to notify users when a new version of your frontend is available, you can still expose the actual asset version to the frontend by including it as [shared data](/v3/data-props/shared-data).
+
+```php
+class HandleInertiaRequests extends Middleware
+{
+ public function version(Request $request)
+ {
+ return null;
+ }
+
+ public function share(Request $request)
+ {
+ return array_merge(parent::share($request), [
+ 'version' => parent::version($request),
+ ]);
+ }
+}
+```
+
+On the frontend, you can watch the `version` property and show a notification when a new version is detected.
diff --git a/v3/advanced/code-splitting.mdx b/v3/advanced/code-splitting.mdx
new file mode 100644
index 0000000..f5df7dc
--- /dev/null
+++ b/v3/advanced/code-splitting.mdx
@@ -0,0 +1,102 @@
+---
+title: Code Splitting
+---
+
+By default, Inertia lazy-loads page components, splitting each page into its own bundle that is loaded on demand. This reduces the initial JavaScript bundle size but requires additional requests when visiting new pages.
+
+You may disable lazy loading to eagerly bundle all pages into a single file. Eager loading eliminates per-page requests but increases the initial bundle size.
+
+## Vite Plugin
+
+The `lazy` option in the `pages` shorthand controls how page components are loaded. It defaults to `true`.
+
+```js
+createInertiaApp({
+ pages: {
+ lazy: false, // Bundle all pages into a single file
+ },
+ // ...
+})
+```
+
+## Manual Vite
+
+You may configure code splitting manually using Vite's `import.meta.glob()` function when not using the Inertia Vite plugin. Pass `{ eager: true }` to bundle all pages, or omit it to lazy-load them.
+
+
+
+```js Vue icon="vuejs"
+resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.vue') // [!code --:2]
+ return pages[`./Pages/${name}.vue`]()
+ const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) // [!code ++:2]
+ return pages[`./Pages/${name}.vue`]
+},
+```
+
+```js React icon="react"
+resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.jsx') // [!code --:2]
+ return pages[`./Pages/${name}.jsx`]()
+ const pages = import.meta.glob('./Pages/**/*.jsx', { eager: true }) // [!code ++:2]
+ return pages[`./Pages/${name}.jsx`]
+},
+```
+
+```js Svelte icon="s"
+resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.svelte') // [!code --:2]
+ return pages[`./Pages/${name}.svelte`]()
+ const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) // [!code ++:2]
+ return pages[`./Pages/${name}.svelte`]
+},
+```
+
+
+
+## Webpack
+
+To use code splitting with Webpack, you will first need to enable [dynamic imports](https://github.com/tc39/proposal-dynamic-import) via a Babel plugin. Let's install it now.
+
+```bash
+npm install @babel/plugin-syntax-dynamic-import
+```
+
+Next, create a `.babelrc` file in your project with the following configuration:
+
+```json
+{
+ "plugins": ["@babel/plugin-syntax-dynamic-import"]
+}
+```
+
+If you're using Laravel Mix, the dynamic imports Babel plugin is already installed and configured, and you can skip these steps. We recommend using Laravel Mix 6 or above, as there are known issues with older versions.
+
+Finally, update the `resolve` callback in your app's initialization code to use `import` instead of `require`.
+
+
+
+```js Vue icon="vuejs"
+resolve: name => require(`./Pages/${name}`), // [!code --]
+resolve: name => import(`./Pages/${name}`), // [!code ++]
+```
+
+```js React icon="react"
+resolve: name => require(`./Pages/${name}`), // [!code --]
+resolve: name => import(`./Pages/${name}`), // [!code ++]
+```
+
+```js Svelte icon="s"
+resolve: name => require(`./Pages/${name}.svelte`), // [!code --]
+resolve: name => import(`./Pages/${name}.svelte`), // [!code ++]
+```
+
+
+
+You should also consider using cache busting to force browsers to load the latest version of your assets. To accomplish this, add the following configuration to your webpack configuration file.
+
+```js
+output: {
+ chunkFilename: 'js/[name].js?id=[chunkhash]',
+}
+```
diff --git a/v3/advanced/error-handling.mdx b/v3/advanced/error-handling.mdx
new file mode 100644
index 0000000..095dfe8
--- /dev/null
+++ b/v3/advanced/error-handling.mdx
@@ -0,0 +1,157 @@
+---
+title: Error Handling
+---
+
+## Development
+
+One of the advantages to working with a robust server-side framework is the built-in exception handling you get for free. For example, Laravel ships with a beautiful error reporting tool which displays a nicely formatted stack trace in local development.
+
+The challenge is, if you're making an XHR request (which Inertia does) and you hit a server-side error, you're typically left digging through the network tab in your browser's devtools to diagnose the problem.
+
+Inertia solves this issue by showing all non-Inertia responses in a modal. This means you get the same beautiful error-reporting you're accustomed to, even though you've made that request over XHR.
+
+## Production
+
+In production you will want to return a proper Inertia error response instead of relying on the modal-driven error reporting that is present during development. To accomplish this, you may use the `Inertia::handleExceptionsUsing()` method in your application's service provider.
+
+```php
+// app/Providers/AppServiceProvider.php
+use Inertia\Inertia;
+use Inertia\ExceptionResponse;
+
+public function boot(): void
+{
+ Inertia::handleExceptionsUsing(function (ExceptionResponse $response) {
+ if (in_array($response->statusCode(), [403, 404, 500, 503])) {
+ return $response->render('ErrorPage', [
+ 'status' => $response->statusCode(),
+ ])->withSharedData();
+ }
+ });
+}
+```
+
+Since exceptions like 404s occur outside of the Inertia middleware (the request never reaches your routes), the error response won't have access to shared data or the root view by default. Calling `withSharedData()` explicitly resolves the Inertia middleware and includes your shared props in the error page.
+
+The `ExceptionResponse` instance provides the `exception`, `request`, and `response` as public readonly properties, along with the following methods:
+
+- `render($component, $props)` - Render an Inertia page component with the given props
+- `withSharedData()` - Include shared data from the Inertia middleware
+- `usingMiddleware($class)` - Specify which Inertia middleware to use for shared data and root view resolution
+- `rootView($view)` - Set a custom root view for the error response
+- `statusCode()` - Get the HTTP status code of the original response
+
+The Inertia middleware is auto-resolved from the matched route or the kernel's middleware groups, so `withSharedData()` typically works without specifying a middleware class. Returning `null` from the callback falls through to Laravel's default exception rendering.
+
+### Error Page Example
+
+You'll need to create the error page components referenced above. Here's an example you may use as a starting point.
+
+
+
+```vue Vue icon="vuejs"
+
+
+
+
+
{{ title }}
+
{{ description }}
+
+
+```
+
+```jsx React icon="react"
+export default function ErrorPage({ status }) {
+ const title = {
+ 503: "503: Service Unavailable",
+ 500: "500: Server Error",
+ 404: "404: Page Not Found",
+ 403: "403: Forbidden",
+ }[status];
+
+ const description = {
+ 503: "Sorry, we are doing some maintenance. Please check back soon.",
+ 500: "Whoops, something went wrong on our servers.",
+ 404: "Sorry, the page you are looking for could not be found.",
+ 403: "Sorry, you are forbidden from accessing this page.",
+ }[status];
+
+ return (
+
+
{title}
+
{description}
+
+ );
+}
+```
+
+```svelte Svelte icon="s"
+
+
+
+
{title[status]}
+
{description[status]}
+
+```
+
+
+
+### Manual Exception Handling
+
+Under the hood, `handleExceptionsUsing()` registers a `$exceptions->respond()` callback in your application's `bootstrap/app.php` file. You may register this callback manually if you prefer.
+
+```php
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Inertia\Inertia;
+
+->withExceptions(function (Exceptions $exceptions) {
+ $exceptions->respond(function (Response $response, Throwable $exception, Request $request) {
+ if (! app()->environment(['local', 'testing']) && in_array($response->getStatusCode(), [500, 503, 404, 403])) {
+ return Inertia::render('ErrorPage', ['status' => $response->getStatusCode()])
+ ->toResponse($request)
+ ->setStatusCode($response->getStatusCode());
+ }
+
+ return $response;
+ });
+})
+```
diff --git a/v3/advanced/events.mdx b/v3/advanced/events.mdx
new file mode 100644
index 0000000..382ea46
--- /dev/null
+++ b/v3/advanced/events.mdx
@@ -0,0 +1,784 @@
+---
+title: Events
+---
+
+Inertia provides an event system that allows you to "hook into" the various lifecycle events of the library.
+
+## Registering Listeners
+
+To register an event listener, use the `router.on()` method.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+
+
+Under the hood, Inertia uses native browser events, so you can also interact with Inertia events using the typical event methods you may already be familiar with - just be sure to prepend `inertia:` to the event name.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+document.addEventListener("inertia:start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+document.addEventListener("inertia:start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+document.addEventListener("inertia:start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+
+
+## Removing Listeners
+
+When you register an event listener, Inertia automatically returns a callback that can be invoked to remove the event listener.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+let removeStartEventListener = router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+
+// Remove the listener...
+removeStartEventListener();
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+let removeStartEventListener = router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+
+// Remove the listener...
+removeStartEventListener();
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+let removeStartEventListener = router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+
+// Remove the listener...
+removeStartEventListener();
+```
+
+
+
+Combined with hooks, you can automatically remove the event listener when components unmount.
+
+
+
+```vue Vue icon="vuejs"
+
+```
+
+```jsx React icon="react"
+import { useEffect } from "react";
+import { router } from "@inertiajs/react";
+
+useEffect(() => {
+ return router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+ });
+}, []);
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+$effect(() => {
+ return router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+ });
+});
+```
+
+
+
+Alternatively, if you're using native browser events, you can remove the event listener using `removeEventListener()`.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+let startEventListener = (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+};
+
+document.addEventListener("inertia:start", startEventListener);
+
+// Remove the listener...
+document.removeEventListener("inertia:start", startEventListener);
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+let startEventListener = (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+};
+
+document.addEventListener("inertia:start", startEventListener);
+
+// Remove the listener...
+document.removeEventListener("inertia:start", startEventListener);
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+let startEventListener = (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+};
+
+document.addEventListener("inertia:start", startEventListener);
+
+// Remove the listener...
+document.removeEventListener("inertia:start", startEventListener);
+```
+
+
+
+## Cancelling Events
+
+Some events, such as `before`, `networkError`, and `httpException`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("before", (event) => {
+ if (!confirm("Are you sure you want to navigate away?")) {
+ event.preventDefault();
+ }
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("before", (event) => {
+ if (!confirm("Are you sure you want to navigate away?")) {
+ event.preventDefault();
+ }
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("before", (event) => {
+ if (!confirm("Are you sure you want to navigate away?")) {
+ event.preventDefault();
+ }
+});
+```
+
+
+
+For convenience, if you register your event listener using `router.on()`, you can cancel the event by returning `false` from the listener.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+
+
+Note, browsers do not allow cancelling the native `popstate` event, so preventing forward and back history visits while using Inertia.js is not possible.
+
+## Before
+
+The `before` event fires when a request is about to be made to the server. This is useful for intercepting visits.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("before", (event) => {
+ console.log(`About to make a visit to ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("before", (event) => {
+ console.log(`About to make a visit to ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("before", (event) => {
+ console.log(`About to make a visit to ${event.detail.visit.url}`);
+});
+```
+
+
+
+The primary purpose of this event is to allow you to prevent a visit from happening.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("before", (event) => {
+ return confirm("Are you sure you want to navigate away?");
+});
+```
+
+
+
+## Start
+
+The `start` event fires when a request to the server has started. This is useful for displaying loading indicators.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("start", (event) => {
+ console.log(`Starting a visit to ${event.detail.visit.url}`);
+});
+```
+
+
+
+The `start` event is not cancelable.
+
+## Progress
+
+The `progress` event fires as progress increments during file uploads.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("progress", (event) => {
+ this.form.progress = event.detail.progress.percentage;
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("progress", (event) => {
+ this.form.progress = event.detail.progress.percentage;
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("progress", (event) => {
+ this.form.progress = event.detail.progress.percentage;
+});
+```
+
+
+
+The `progress` event is not cancelable.
+
+## Success
+
+The `success` event fires on successful page visits, unless validation errors are present. However, this does _not_ include history visits.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("success", (event) => {
+ console.log(`Successfully made a visit to ${event.detail.page.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("success", (event) => {
+ console.log(`Successfully made a visit to ${event.detail.page.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("success", (event) => {
+ console.log(`Successfully made a visit to ${event.detail.page.url}`);
+});
+```
+
+
+
+The `success` event is not cancelable.
+
+## Flash
+
+The `flash` event fires when [flash data](/v3/data-props/flash-data) is received from the server. This is useful for displaying toast notifications or handling temporary data in a central location.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("flash", (event) => {
+ if (event.detail.flash.toast) {
+ showToast(event.detail.flash.toast);
+ }
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("flash", (event) => {
+ if (event.detail.flash.toast) {
+ showToast(event.detail.flash.toast);
+ }
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("flash", (event) => {
+ if (event.detail.flash.toast) {
+ showToast(event.detail.flash.toast);
+ }
+});
+```
+
+
+
+The `flash` event is not cancelable. [Partial reloads](/v3/data-props/partial-reloads) will only trigger the event if the flash data has changed.
+
+## Error
+
+The `error` event fires when validation errors are present on "successful" page visits.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("error", (event) => {
+ console.log(event.detail.errors);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("error", (event) => {
+ console.log(event.detail.errors);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("error", (event) => {
+ console.log(event.detail.errors);
+});
+```
+
+
+
+The `error` event is not cancelable.
+
+## HTTP Exception
+
+The `httpException` event fires when a non-Inertia response is received from the server, such as an HTML or vanilla JSON response. A valid Inertia response is a response that has the `X-Inertia` header set to `true` with a `json` payload containing [the page object](/v3/core-concepts/the-protocol#the-page-object).
+
+This event is fired for all response types, including `200`, `400`, and `500` response codes.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("httpException", (event) => {
+ console.log(`An invalid Inertia response was received.`);
+ console.log(event.detail.response);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("httpException", (event) => {
+ console.log(`An invalid Inertia response was received.`);
+ console.log(event.detail.response);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("httpException", (event) => {
+ console.log(`An invalid Inertia response was received.`);
+ console.log(event.detail.response);
+});
+```
+
+
+
+You may cancel the `httpException` event to prevent Inertia from showing the non-Inertia response modal.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("httpException", (event) => {
+ event.preventDefault();
+
+ // Handle the invalid response yourself...
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("httpException", (event) => {
+ event.preventDefault();
+
+ // Handle the invalid response yourself...
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("httpException", (event) => {
+ event.preventDefault();
+
+ // Handle the invalid response yourself...
+});
+```
+
+
+
+## Network Error
+
+The `networkError` event fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("networkError", (event) => {
+ console.log(`An unexpected error occurred during an Inertia visit.`);
+ console.log(event.detail.exception);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("networkError", (event) => {
+ console.log(`An unexpected error occurred during an Inertia visit.`);
+ console.log(event.detail.exception);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("networkError", (event) => {
+ console.log(`An unexpected error occurred during an Inertia visit.`);
+ console.log(event.detail.exception);
+});
+```
+
+
+
+You may cancel the `networkError` event to prevent the error from being thrown.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("networkError", (event) => {
+ event.preventDefault();
+ // Handle the error yourself
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("networkError", (event) => {
+ event.preventDefault();
+ // Handle the error yourself
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("networkError", (event) => {
+ event.preventDefault();
+ // Handle the error yourself
+});
+```
+
+
+
+This event will _not_ fire for XHR requests that receive `400` and `500` level responses or for non-Inertia responses, as these situations are handled in other ways by Inertia. Please consult the [error handling documentation](/v3/advanced/error-handling) for more information.
+
+## Finish
+
+The `finish` event fires after an XHR request has completed for both "successful" and "unsuccessful" responses. This event is useful for hiding loading indicators.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("finish", (event) => {
+ NProgress.done();
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("finish", (event) => {
+ NProgress.done();
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("finish", (event) => {
+ NProgress.done();
+});
+```
+
+
+
+The `finish` event is not cancelable.
+
+## Navigate
+
+The `navigate` event fires on successful page visits, as well as when navigating through history.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("navigate", (event) => {
+ console.log(`Navigated to ${event.detail.page.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("navigate", (event) => {
+ console.log(`Navigated to ${event.detail.page.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("navigate", (event) => {
+ console.log(`Navigated to ${event.detail.page.url}`);
+});
+```
+
+
+
+The `navigate` event is not cancelable.
+
+## Prefetching
+
+The `prefetching` event fires when the router starts prefetching a page.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("prefetching", (event) => {
+ console.log(`Prefetching ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("prefetching", (event) => {
+ console.log(`Prefetching ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("prefetching", (event) => {
+ console.log(`Prefetching ${event.detail.visit.url}`);
+});
+```
+
+
+
+The `prefetching` event is not cancelable.
+
+## Prefetched
+
+The `prefetched` event fires when the router has successfully prefetched a page.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.on("prefetched", (event) => {
+ console.log(`Prefetched ${event.detail.visit.url}`);
+});
+```
+
+```jsx React icon="react"
+import { router } from "@inertiajs/react";
+
+router.on("prefetched", (event) => {
+ console.log(`Prefetched ${event.detail.visit.url}`);
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.on("prefetched", (event) => {
+ console.log(`Prefetched ${event.detail.visit.url}`);
+});
+```
+
+
+
+The `prefetched` event is not cancelable.
+
+## Event Callbacks
+
+In addition to the global events described throughout this page, Inertia also provides a number of [event callbacks](/v3/the-basics/manual-visits#event-callbacks) that fire when manually making Inertia visits.
diff --git a/v3/advanced/progress-indicators.mdx b/v3/advanced/progress-indicators.mdx
new file mode 100644
index 0000000..6827af9
--- /dev/null
+++ b/v3/advanced/progress-indicators.mdx
@@ -0,0 +1,387 @@
+---
+title: Progress Indicators
+---
+
+Since Inertia requests are made via XHR, there would typically not be a browser loading indicator when navigating from one page to another. To solve this, Inertia displays a progress indicator at the top of the page whenever you make an Inertia visit. However, [asynchronous requests](#visit-options) do not show the progress indicator unless explicitly configured.
+
+Of course, if you prefer, you can disable Inertia's default loading indicator and provide your own custom implementation. We'll discuss both approaches below.
+
+## Default
+
+Inertia's default progress indicator is a light-weight wrapper around the [NProgress](https://ricostacruz.com/nprogress/) library. You can customize it via the `progress` property of the `createInertiaApp()` function.
+
+```js
+createInertiaApp({
+ progress: {
+ // The delay after which the progress bar will appear, in milliseconds...
+ delay: 250,
+ // The color of the progress bar...
+ color: "#29d",
+ // Whether to include the default NProgress styles...
+ includeCSS: true,
+ // Whether the NProgress spinner will be shown...
+ showSpinner: false,
+ },
+ // ...
+});
+```
+
+You can disable Inertia's default loading indicator by setting the `progress` property to `false`.
+
+```js
+createInertiaApp({
+ progress: false,
+ // ...
+});
+```
+
+## Programmatic Access
+
+When you need to control the progress indicator outside of Inertia requests, for example, when making requests with Axios or other libraries, you can use Inertia's progress methods directly.
+
+
+
+```js Vue icon="vuejs"
+import { progress } from "@inertiajs/vue3";
+
+progress.start(); // Begin progress animation
+progress.set(0.25); // Set to 25% complete
+progress.finish(); // Complete and fade out
+progress.reset(); // Reset to start
+progress.remove(); // Complete and remove from DOM
+progress.hide(); // Hide progress bar
+progress.reveal(); // Show progress bar
+
+progress.isStarted(); // Returns boolean
+progress.getStatus(); // Returns current percentage or null
+```
+
+```js React icon="react"
+import { progress } from "@inertiajs/react";
+
+progress.start(); // Begin progress animation
+progress.set(0.25); // Set to 25% complete
+progress.finish(); // Complete and fade out
+progress.reset(); // Reset to start
+progress.remove(); // Complete and remove from DOM
+progress.hide(); // Hide progress bar
+progress.reveal(); // Show progress bar
+
+progress.isStarted(); // Returns boolean
+progress.getStatus(); // Returns current percentage or null
+```
+
+```js Svelte icon="s"
+import { progress } from "@inertiajs/svelte";
+
+progress.start(); // Begin progress animation
+progress.set(0.25); // Set to 25% complete
+progress.finish(); // Complete and fade out
+progress.reset(); // Reset to start
+progress.remove(); // Complete and remove from DOM
+progress.hide(); // Hide progress bar
+progress.reveal(); // Show progress bar
+
+progress.isStarted(); // Returns boolean
+progress.getStatus(); // Returns current percentage or null
+```
+
+
+
+The `hide()` and `reveal()` methods work together to prevent conflicts when separate parts of your code need to control progress visibility. Each `hide()` call increments an internal counter, while `reveal()` decrements it. The progress bar only appears when this counter reaches zero.
+
+However, `reveal()` accepts an optional `force` parameter that bypasses this counter. Inertia uses this pattern internally to hide progress during prefetching while ensuring it appears for actual navigation.
+
+```js
+progress.hide(); // Counter = 1, bar hidden
+progress.hide(); // Counter = 2, bar still hidden
+progress.reveal(); // Counter = 1, bar still hidden
+progress.reveal(); // Counter = 0, bar now visible
+
+// Force reveal bypasses the counter
+progress.reveal(true);
+```
+
+If you've disabled the progress indicator with `progress: false` in `createInertiaApp()`, these programmatic methods will not work.
+
+## Custom
+
+It's also possible to setup your own custom page loading indicators using Inertia [events](/v3/advanced/events). Let's explore how to do this using the [NProgress](https://ricostacruz.com/nprogress/) library as an example.
+
+First, disable Inertia's default loading indicator.
+
+```js
+createInertiaApp({
+ progress: false,
+ // ...
+});
+```
+
+Next, install the NProgress library.
+
+```bash
+npm install nprogress
+```
+
+After installation, you'll need to add the NProgress [styles](https://github.com/rstacruz/nprogress/blob/master/nprogress.css) to your project. You can do this using a CDN hosted copy of the styles.
+
+```html
+
+```
+
+Then, import both `NProgress` and the Inertia `router` into your application.
+
+
+
+```js Vue icon="vuejs"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/vue3";
+```
+
+```js React icon="react"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/react";
+```
+
+```js Svelte icon="s"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/svelte";
+```
+
+
+
+Next, add a `start` event listener. We'll use this listener to show the progress bar when a new Inertia visit begins.
+
+```js
+router.on("start", () => NProgress.start());
+```
+
+Finally, add a `finish` event listener to hide the progress bar when the page visit finishes.
+
+```js
+router.on("finish", () => NProgress.done());
+```
+
+That's it! Now, as you navigate from one page to another, the progress bar will be added and removed from the page.
+
+### Handling Cancelled Visits
+
+While this custom progress implementation works great for page visits that finish properly, it would be nice to handle cancelled visits as well. First, for interrupted visits (those that get cancelled as a result of a new visit), the progress bar should simply be reset back to the start position. Second, for manually cancelled visits, the progress bar should be immediately removed from the page.
+
+We can accomplish this by inspecting the `event.detail.visit` object that's provided to the finish event.
+
+```js
+router.on("finish", (event) => {
+ if (event.detail.visit.completed) {
+ NProgress.done();
+ } else if (event.detail.visit.interrupted) {
+ NProgress.set(0);
+ } else if (event.detail.visit.cancelled) {
+ NProgress.done();
+ NProgress.remove();
+ }
+});
+```
+
+### File Upload Progress
+
+Let's take this a step further. When files are being uploaded, it would be great to update the loading indicator to reflect the upload progress. This can be done using the `progress` event.
+
+```js
+router.on("progress", (event) => {
+ if (event.detail.progress.percentage) {
+ NProgress.set((event.detail.progress.percentage / 100) * 0.9);
+ }
+});
+```
+
+Now, instead of the progress bar "trickling" while the files are being uploaded, it will actually update it's position based on the progress of the request. We limit the progress here to 90%, since we still need to wait for a response from the server.
+
+### Loading Indicator Delay
+
+The last thing we're going to implement is a loading indicator delay. It's often preferable to delay showing the loading indicator until a request has taken longer than 250-500 milliseconds. This prevents the loading indicator from appearing constantly on quick page visits, which can be visually distracting.
+
+To implement the delay behavior, we'll use the `setTimeout` and `clearTimeout` functions. Let's start by defining a variable to keep track of the timeout.
+
+```js
+let timeout = null;
+```
+
+Next, let's update the `start` event listener to start a new timeout that will show the progress bar after 250 milliseconds.
+
+```js
+router.on("start", () => {
+ timeout = setTimeout(() => NProgress.start(), 250);
+});
+```
+
+Next, we'll update the `finish` event listener to clear any existing timeouts in the event that the page visit finishes before the timeout does.
+
+```js
+router.on("finish", (event) => {
+ clearTimeout(timeout);
+ // ...
+});
+```
+
+In the `finish` event listener, we need to determine if the progress bar has actually started displaying progress, otherwise we'll inadvertently cause it to show before the timeout has finished.
+
+```js
+router.on("finish", (event) => {
+ clearTimeout(timeout);
+
+ if (!NProgress.isStarted()) {
+ return;
+ }
+ // ...
+});
+```
+
+And, finally, we need to do the same check in the `progress` event listener.
+
+```js
+router.on("progress", (event) => {
+ if (!NProgress.isStarted()) {
+ return;
+ }
+ // ...
+});
+```
+
+That's it, you now have a beautiful custom page loading indicator!
+
+### Complete Example
+
+For convenience, here is the full source code of the final version of our custom loading indicator.
+
+
+
+```js Vue icon="vuejs"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/vue3";
+
+let timeout = null;
+
+router.on("start", () => {
+ timeout = setTimeout(() => NProgress.start(), 250);
+});
+
+router.on("progress", (event) => {
+ if (NProgress.isStarted() && event.detail.progress.percentage) {
+ NProgress.set((event.detail.progress.percentage / 100) * 0.9);
+ }
+});
+
+router.on("finish", (event) => {
+ clearTimeout(timeout);
+
+ if (!NProgress.isStarted()) {
+ return;
+ }
+
+ if (event.detail.visit.completed) {
+ NProgress.done();
+ } else if (event.detail.visit.interrupted) {
+ NProgress.set(0);
+ } else if (event.detail.visit.cancelled) {
+ NProgress.done();
+ NProgress.remove();
+ }
+});
+```
+
+```js React icon="react"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/react";
+
+let timeout = null;
+
+router.on("start", () => {
+ timeout = setTimeout(() => NProgress.start(), 250);
+});
+
+router.on("progress", (event) => {
+ if (NProgress.isStarted() && event.detail.progress.percentage) {
+ NProgress.set((event.detail.progress.percentage / 100) * 0.9);
+ }
+});
+
+router.on("finish", (event) => {
+ clearTimeout(timeout);
+
+ if (!NProgress.isStarted()) {
+ return;
+ }
+
+ if (event.detail.visit.completed) {
+ NProgress.done();
+ } else if (event.detail.visit.interrupted) {
+ NProgress.set(0);
+ } else if (event.detail.visit.cancelled) {
+ NProgress.done();
+ NProgress.remove();
+ }
+});
+```
+
+```js Svelte icon="s"
+import NProgress from "nprogress";
+import { router } from "@inertiajs/svelte";
+
+let timeout = null;
+
+router.on("start", () => {
+ timeout = setTimeout(() => NProgress.start(), 250);
+});
+
+router.on("progress", (event) => {
+ if (NProgress.isStarted() && event.detail.progress.percentage) {
+ NProgress.set((event.detail.progress.percentage / 100) * 0.9);
+ }
+});
+
+router.on("finish", (event) => {
+ clearTimeout(timeout);
+
+ if (!NProgress.isStarted()) {
+ return;
+ }
+
+ if (event.detail.visit.completed) {
+ NProgress.done();
+ } else if (event.detail.visit.interrupted) {
+ NProgress.set(0);
+ } else if (event.detail.visit.cancelled) {
+ NProgress.done();
+ NProgress.remove();
+ }
+});
+```
+
+
+
+## Visit Options
+
+In addition to these configurations, Inertia.js provides two visit options to control the loading indicator on a per-request basis: `showProgress` and `async`. These options offer greater control over how Inertia.js handles asynchronous requests and manages progress indicators.
+
+### Showprogress
+
+The `showProgress` option provides fine-grained control over the visibility of the loading indicator during requests.
+
+```js
+router.get("/settings", {}, { showProgress: false });
+```
+
+### Async
+
+The `async` option allows you to perform asynchronous requests without displaying the default progress indicator. It can be used in combination with the `showProgress` option.
+
+```js
+// Disable the progress indicator
+router.get("/settings", {}, { async: true });
+// Enable the progress indicator with async requests
+router.get("/settings", {}, { async: true, showProgress: true });
+```
diff --git a/v3/advanced/scroll-management.mdx b/v3/advanced/scroll-management.mdx
new file mode 100644
index 0000000..56bc844
--- /dev/null
+++ b/v3/advanced/scroll-management.mdx
@@ -0,0 +1,135 @@
+---
+title: Scroll Management
+---
+
+## Scroll Resetting
+
+When navigating between pages, Inertia mimics default browser behavior by automatically resetting the scroll position of the document body (as well as any [scroll regions](#scroll-regions) you've defined) back to the top.
+
+In addition, Inertia keeps track of the scroll position of each page and automatically restores that scroll position as you navigate forward and back in history.
+
+## Scroll Preservation
+
+Sometimes it's desirable to prevent the default scroll resetting when making visits. You can disable this behavior by setting the `preserveScroll` option to `true`.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.visit(url, { preserveScroll: true });
+```
+
+```js React icon="react"
+import { router } from "@inertiajs/react";
+
+router.visit(url, { preserveScroll: true });
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.visit(url, { preserveScroll: true });
+```
+
+
+
+If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to "errors".
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.visit(url, { preserveScroll: "errors" });
+```
+
+```js React icon="react"
+import { router } from "@inertiajs/react";
+
+router.visit(url, { preserveScroll: "errors" });
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.visit(url, { preserveScroll: "errors" });
+```
+
+
+
+You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback.
+
+
+
+```js Vue icon="vuejs"
+import { router } from "@inertiajs/vue3";
+
+router.post("/users", data, {
+ preserveScroll: (page) => page.props.someProp === "value",
+});
+```
+
+```js React icon="react"
+import { router } from "@inertiajs/react";
+
+router.post("/users", data, {
+ preserveScroll: (page) => page.props.someProp === "value",
+});
+```
+
+```js Svelte icon="s"
+import { router } from "@inertiajs/svelte";
+
+router.post("/users", data, {
+ preserveScroll: (page) => page.props.someProp === "value",
+});
+```
+
+
+
+When using an [Inertia link](/v3/the-basics/links), you can preserve the scroll position using the `preserveScroll` prop.
+
+
+
+```vue Vue icon="vuejs"
+import { Link } from '@inertiajs/vue3'
+
+Home
+```
+
+```jsx React icon="react"
+import { Link } from "@inertiajs/react";
+
+
+ Home
+;
+```
+
+```svelte Svelte icon="s"
+import { inertia, Link } from '@inertiajs/svelte'
+
+Home
+
+Home
+```
+
+
+
+## Scroll Regions
+
+If your app doesn't use document body scrolling, but instead has scrollable elements (using the `overflow` CSS property), scroll resetting will not work.
+
+In these situations, you must tell Inertia which scrollable elements to manage by adding the `scroll-region` attribute to the element.
+
+```html
+
+
+
+```
+
+## Text Fragments
+
+[Text fragments](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments) allow you to link directly to specific text on a page using a special URL syntax like `#:~:text=term`. However, the browser removes the fragment directive before any JavaScript runs, so text fragments only work if the targeted text is present in the initial HTML response.
+
+To use text fragments with your Inertia pages, enable [server-side rendering](/v3/advanced/server-side-rendering).
diff --git a/v3/advanced/server-side-rendering.mdx b/v3/advanced/server-side-rendering.mdx
new file mode 100644
index 0000000..8f868a2
--- /dev/null
+++ b/v3/advanced/server-side-rendering.mdx
@@ -0,0 +1,564 @@
+---
+title: Server-Side Rendering (SSR)
+---
+
+import { ClientSpecific } from "/snippets/client-specific.jsx"
+import { ReactSpecific } from "/snippets/react-specific.jsx"
+import { SvelteSpecific } from "/snippets/svelte-specific.jsx"
+import { VueSpecific } from "/snippets/vue-specific.jsx"
+
+Server-side rendering pre-renders your JavaScript pages on the server, allowing your visitors to receive fully rendered HTML when they visit your application. Since fully rendered HTML is served by your application, it's also easier for search engines to index your site.
+
+Server-side rendering uses Node.js to render your pages in a background process; therefore, Node must be available on your server for server-side rendering to function properly.
+
+## Laravel Starter Kits
+
+If you are using [Laravel Starter Kits](https://laravel.com/docs/starter-kits), Inertia SSR is [supported](https://laravel.com/docs/starter-kits#inertia-ssr) through a build command:
+
+```bash
+npm run build:ssr
+```
+
+## Vite Plugin Setup
+
+The recommended way to configure SSR is with the `@inertiajs/vite` plugin. This approach handles SSR configuration automatically, including development mode SSR without a separate Node.js server.
+
+
+
+ ```bash
+ npm install @inertiajs/vite
+ ```
+
+
+
+ Add the Inertia plugin to your `vite.config.js` file. The plugin will automatically detect your SSR entry point.
+
+ ```js vite.config.js
+ import inertia from '@inertiajs/vite'
+ import laravel from 'laravel-vite-plugin'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ plugins: [
+ laravel({
+ input: ['resources/css/app.css', 'resources/js/app.js'],
+ refresh: true,
+ }),
+ inertia(),
+ ],
+ })
+ ```
+
+ You may also configure SSR options explicitly.
+
+ ```js vite.config.js
+ inertia({
+ ssr: {
+ entry: 'resources/js/ssr.js',
+ port: 13714,
+ cluster: true,
+ },
+ })
+ ```
+
+ You may pass `false` to opt out of the plugin's automatic SSR handling, for example if you prefer to [configure SSR manually](#manual-setup) but still want to use the other features of the Vite plugin.
+
+ ```js vite.config.js
+ inertia({
+ ssr: false,
+ })
+ ```
+
+
+
+ Update the `build` script in your `package.json` to build both bundles.
+
+ ```json package.json
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build" // [!code --]
+ "build": "vite build && vite build --ssr" // [!code ++]
+ },
+ ```
+
+
+
+### Development Mode
+
+The Vite plugin handles SSR automatically during development. There is no need to build your SSR bundle or start a separate Node.js server. Simply run your Vite dev server as usual:
+
+```bash
+npm run dev
+```
+
+The Vite plugin exposes a server endpoint that Laravel uses for rendering, complete with HMR support.
+
+The `vite build --ssr` and `php artisan inertia:start-ssr` commands are for [production](#production) only. You should not run them during development.
+
+### Production
+
+For production, build both bundles and start the SSR server.
+
+```bash
+npm run build
+php artisan inertia:start-ssr
+```
+
+### Custom SSR Entry Point
+
+The Vite plugin reuses your `app.js` entry point for both browser and SSR rendering by default, so no separate file is needed. The plugin detects the `data-server-rendered` attribute to decide whether to hydrate or mount, and the `setup` and `resolve` callbacks are optional.
+
+If you need custom SSR logic (such as Vue plugins that should only run on the server), you may create a separate `resources/js/ssr.js` file.
+
+
+
+```js Vue icon="vuejs"
+import { createInertiaApp } from '@inertiajs/vue3'
+import createServer from '@inertiajs/vue3/server'
+import { createSSRApp, h } from 'vue'
+import { renderToString } from 'vue/server-renderer'
+
+createServer(page =>
+ createInertiaApp({
+ page,
+ render: renderToString,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.vue')
+ return pages[`./Pages/${name}.vue`]()
+ },
+ setup({ App, props, plugin }) {
+ return createSSRApp({
+ render: () => h(App, props),
+ }).use(plugin)
+ },
+ }),
+)
+```
+
+```jsx React icon="react"
+import { createInertiaApp } from '@inertiajs/react'
+import createServer from '@inertiajs/react/server'
+import ReactDOMServer from 'react-dom/server'
+
+createServer(page =>
+ createInertiaApp({
+ page,
+ render: ReactDOMServer.renderToString,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.jsx')
+ return pages[`./Pages/${name}.jsx`]()
+ },
+ setup: ({ App, props }) => ,
+ }),
+)
+```
+
+```js Svelte icon="s"
+import { createInertiaApp } from '@inertiajs/svelte'
+import createServer from '@inertiajs/svelte/server'
+import { render } from 'svelte/server'
+
+createServer(page =>
+ createInertiaApp({
+ page,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.svelte')
+ return pages[`./Pages/${name}.svelte`]()
+ },
+ setup({ App, props }) {
+ return render(App, { props })
+ },
+ }),
+)
+```
+
+
+
+Be sure to add anything that's missing from your `app.js` file that makes sense to run in SSR mode, such as plugins or custom mixins.
+
+## Manual Setup
+
+If you prefer not to use the Vite plugin, you may configure SSR manually.
+
+
+
+ Create an SSR entry point file within your Laravel project.
+
+
+ ```bash Vue icon="vuejs"
+ touch resources/js/ssr.js
+ ```
+
+ ```bash React icon="react"
+ touch resources/js/ssr.jsx
+ ```
+
+ ```bash Svelte icon="s"
+ touch resources/js/ssr.js
+ ```
+
+
+ This file will look similar to your app entry point, but it runs in Node.js instead of the browser. Here's a complete example.
+
+
+
+ ```js Vue icon="vuejs"
+ import { createInertiaApp } from '@inertiajs/vue3'
+ import createServer from '@inertiajs/vue3/server'
+ import { renderToString } from 'vue/server-renderer'
+ import { createSSRApp, h } from 'vue'
+
+ createServer(page =>
+ createInertiaApp({
+ page,
+ render: renderToString,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.vue')
+ return pages[`./Pages/${name}.vue`]()
+ },
+ setup({ App, props, plugin }) {
+ return createSSRApp({
+ render: () => h(App, props),
+ }).use(plugin)
+ },
+ }),
+ )
+ ```
+
+ ```jsx React icon="react"
+ import { createInertiaApp } from '@inertiajs/react'
+ import createServer from '@inertiajs/react/server'
+ import ReactDOMServer from 'react-dom/server'
+
+ createServer(page =>
+ createInertiaApp({
+ page,
+ render: ReactDOMServer.renderToString,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.jsx')
+ return pages[`./Pages/${name}.jsx`]()
+ },
+ setup: ({ App, props }) => ,
+ }),
+ )
+ ```
+
+ ```js Svelte icon="s"
+ import { createInertiaApp } from '@inertiajs/svelte'
+ import createServer from '@inertiajs/svelte/server'
+ import { render } from 'svelte/server'
+
+ createServer(page =>
+ createInertiaApp({
+ page,
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.svelte')
+ return pages[`./Pages/${name}.svelte`]()
+ },
+ setup({ App, props }) {
+ return render(App, { props })
+ },
+ }),
+ )
+ ```
+
+
+
+
+
+ Add the `ssr` property to the Laravel Vite plugin configuration.
+
+
+
+ ```js Vue icon="vuejs" vite.config.js
+ export default defineConfig({
+ plugins: [
+ laravel({
+ input: ['resources/js/app.js'],
+ ssr: 'resources/js/ssr.js', // [!code ++]
+ refresh: true,
+ }),
+ // ...
+ ],
+ })
+ ```
+
+ ```js React icon="react" vite.config.js
+ export default defineConfig({
+ plugins: [
+ laravel({
+ input: ['resources/js/app.jsx'],
+ ssr: 'resources/js/ssr.jsx', // [!code ++]
+ refresh: true,
+ }),
+ // ...
+ ],
+ })
+ ```
+
+ ```js Svelte icon="s" vite.config.js
+ export default defineConfig({
+ plugins: [
+ laravel({
+ input: ['resources/js/app.js'],
+ ssr: 'resources/js/ssr.js', // [!code ++]
+ refresh: true,
+ }),
+ // ...
+ ],
+ })
+ ```
+
+
+
+
+
+ Update the `build` script in your `package.json` to build both bundles.
+
+ ```json package.json
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build" // [!code --]
+ "build": "vite build && vite build --ssr" // [!code ++]
+ },
+ ```
+
+
+
+### Clustering
+
+By default, the SSR server runs on a single thread. You may enable clustering to start multiple Node servers on the same port, with requests handled by each thread in a round-robin fashion.
+
+```js vite.config.js
+inertia({
+ ssr: {
+ cluster: true,
+ },
+})
+```
+
+When using a [custom SSR entry point](#custom-ssr-entry-point) or [manual setup](#manual-setup), you may pass the `cluster` option to `createServer` instead.
+
+
+
+```js Vue icon="vuejs"
+createServer(page =>
+ createInertiaApp({
+ // ...
+ }),
+ { cluster: true },
+)
+```
+
+```jsx React icon="react"
+createServer(page =>
+ createInertiaApp({
+ // ...
+ }),
+ { cluster: true },
+)
+```
+
+```js Svelte icon="s"
+createServer(page =>
+ createInertiaApp({
+ // ...
+ }),
+ { cluster: true },
+)
+```
+
+
+
+## Running the SSR Server
+
+The SSR server is only required in production. During development, the [Vite plugin](#development-mode) handles SSR automatically.
+
+Once you have built both your client-side and server-side bundles, you may start the SSR server using the following Artisan command.
+
+```bash
+php artisan inertia:start-ssr
+```
+
+You may use the `--runtime` option to specify which runtime you want to use. This allows you to switch from the default Node.js runtime to Bun.
+
+```bash
+php artisan inertia:start-ssr --runtime=bun
+```
+
+With the server running, you should be able to access your app within the browser with server-side rendering enabled. In fact, you should be able to disable JavaScript entirely and still navigate around your application.
+
+## Client-Side Hydration
+
+
+You should also update your `app.js` file to use hydration instead of normal rendering. This allows VueReactSvelte to pick up the server-rendered HTML and make it interactive without re-rendering it.
+
+
+
+
+```js Vue icon="vuejs"
+import { createApp, h } from 'vue' // [!code --]
+import { createSSRApp, h } from 'vue' // [!code ++]
+import { createInertiaApp } from '@inertiajs/vue3'
+
+createInertiaApp({
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.vue')
+ return pages[`./Pages/${name}.vue`]()
+ },
+ setup({ el, App, props, plugin }) {
+ createApp({ render: () => h(App, props) }) // [!code --]
+ createSSRApp({ render: () => h(App, props) }) // [!code ++]
+ .use(plugin)
+ .mount(el)
+ },
+})
+```
+
+```js React icon="react"
+import { createInertiaApp } from '@inertiajs/react'
+import { createRoot } from 'react-dom/client' // [!code --]
+import { hydrateRoot } from 'react-dom/client' // [!code ++]
+
+createInertiaApp({
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.jsx')
+ return pages[`./Pages/${name}.jsx`]()
+ },
+ setup({ el, App, props }) {
+ createRoot(el).render() // [!code --]
+ hydrateRoot(el, ) // [!code ++]
+ },
+})
+```
+
+```js Svelte icon="s"
+import { createInertiaApp } from '@inertiajs/svelte'
+import { mount } from 'svelte' // [!code --]
+import { hydrate, mount } from 'svelte' // [!code ++]
+
+createInertiaApp({
+ resolve: name => {
+ const pages = import.meta.glob('./Pages/**/*.svelte')
+ return pages[`./Pages/${name}.svelte`]()
+ },
+ setup({ el, App, props }) {
+ mount(App, { target: el, props }) // [!code --]
+ if (el.dataset.serverRendered === 'true') { // [!code ++:5]
+ hydrate(App, { target: el, props })
+ } else {
+ mount(App, { target: el, props })
+ }
+ },
+})
+```
+
+
+
+## Error Handling
+
+When SSR rendering fails, Inertia gracefully falls back to client-side rendering. The Vite plugin logs detailed error information to the console, including the component name, request URL, source location, and a tailored hint to help you resolve the issue.
+
+Common SSR errors are automatically classified. Browser API errors (such as referencing `window` or `document` in server-rendered code) include guidance on moving the code to a lifecycle hook. Component resolution errors suggest checking file paths and casing.
+
+Inertia also dispatches an `SsrRenderFailed` event on the server. You may listen for this event to log failures or send them to an error tracking service.
+
+```php
+use Illuminate\Support\Facades\Log;
+use Inertia\Ssr\SsrRenderFailed;
+
+Event::listen(SsrRenderFailed::class, function (SsrRenderFailed $event) {
+ Log::warning('SSR failed', $event->toArray());
+});
+```
+
+### Throwing on Error
+
+For CI or E2E testing, you may prefer SSR failures to throw an exception instead of falling back silently. Set the `throw_on_error` option in your `config/inertia.php` file.
+
+```php
+'ssr' => [
+ 'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
+],
+```
+
+You may set the environment variable in your `phpunit.xml` to catch SSR errors during testing.
+
+```xml
+
+```
+
+## Disabling SSR
+
+Sometimes you may wish to disable server-side rendering for certain pages or routes in your application.
+
+### Per-Route via Middleware
+
+You may use the `$withoutSsr` property on your Inertia middleware to disable SSR for specific route patterns.
+
+```php
+use Inertia\Middleware;
+
+class HandleInertiaRequests extends Middleware
+{
+ /**
+ * Defines the routes that should not use SSR.
+ *
+ * @var array
+ */
+ protected $withoutSsr = [
+ 'admin/*',
+ 'dashboard',
+ ];
+}
+```
+
+### Via Facade
+
+You may also disable SSR for specific routes using the `Inertia::withoutSsr()` method, typically called from a service provider.
+
+```php
+use Inertia\Inertia;
+
+Inertia::withoutSsr(['admin/*', 'dashboard']);
+```
+
+### Per-Request
+
+You may disable SSR for the current request by setting the `inertia.ssr.enabled` configuration value to `false`.
+
+```php
+if (request()->is('admin/*')) {
+ config(['inertia.ssr.enabled' => false]);
+}
+```
+
+## Deployment
+
+When deploying your SSR enabled app to production, you'll need to build both the client-side (`app.js`) and server-side bundles (`ssr.js`), and then run the SSR server as a background process, typically using a process monitoring tool such as Supervisor.
+
+```bash
+php artisan inertia:start-ssr
+```
+
+To stop the SSR server, for instance when you deploy a new version of your website, you may utilize the `inertia:stop-ssr` Artisan command. Your process monitor (such as Supervisor) should be responsible for automatically restarting the SSR server after it has stopped.
+
+```bash
+php artisan inertia:stop-ssr
+```
+
+You may use the `inertia:check-ssr` Artisan command to verify that the SSR server is running. This can be helpful after deployment and works well as a Docker health check to ensure the server is responding as expected.
+
+```bash
+php artisan inertia:check-ssr
+```
+
+By default, a check is performed to ensure the server-side bundle exists before dispatching a request to the SSR server. In some cases, such as when your app runs on multiple servers or is containerized, the web server may not have access to the SSR bundle. To disable this check, you may set the `inertia.ssr.ensure_bundle_exists` configuration value to `false`.
+
+### Laravel Cloud
+
+To run the SSR server on Laravel Cloud, you may use Cloud's [native support for Inertia SSR](https://cloud.laravel.com/docs/compute#inertia-ssr).
+
+### Laravel Forge
+
+To run the SSR server on Forge, you may enable it via the [Inertia SSR toggle](https://forge.laravel.com/docs/sites/laravel#inertia-server-side-rendering-ssr) in your site's application panel. Forge will create the required daemon and, optionally, update your deploy script to restart the SSR server on each deployment.
diff --git a/v3/advanced/testing.mdx b/v3/advanced/testing.mdx
new file mode 100644
index 0000000..a365c57
--- /dev/null
+++ b/v3/advanced/testing.mdx
@@ -0,0 +1,260 @@
+---
+title: Testing
+---
+
+There are many different ways to test an Inertia application. This page provides a quick overview of the tools available.
+
+## End-to-end Tests
+
+One popular approach to testing your JavaScript page components is to use an end-to-end testing tool like [Cypress](https://www.cypress.io/) or [Pest](https://pestphp.com). These are browser automation tools that allow you to run real simulations of your app in the browser. These tests are known to be slower; however, since they test your application at the same layer as your end users, they can provide a lot of confidence that your app is working correctly. And, since these tests are run in the browser, your JavaScript code is actually executed and tested as well.
+
+## Client-Side Unit Tests
+
+Another approach to testing your page components is using a client-side unit testing framework, such as [Vitest](https://vitest.dev/), [Jest](https://jestjs.io/), or [Mocha](https://mochajs.org/). This approach allows you to test your JavaScript page components in isolation using Node.js.
+
+## Endpoint Tests
+
+In addition to testing your JavaScript page components, you will likely want to also test the Inertia responses that are returned by your server-side framework. A popular approach to doing this is using endpoint tests, where you make requests to your application and examine the responses. Laravel [provides tooling](https://laravel.com/docs/http-tests) for executing these types of tests.
+
+However, to make this process even easier, Inertia's Laravel adapter provides additional HTTP testing tools. Let's take a look at an example.
+
+```php
+use Inertia\Testing\AssertableInertia as Assert;
+
+class PodcastsControllerTest extends TestCase
+{
+ public function test_can_view_podcast()
+ {
+ $this->get('/podcasts/41')
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('Podcasts/Show')
+ ->has('podcast', fn (Assert $page) => $page
+ ->where('id', $podcast->id)
+ ->where('subject', 'The Laravel Podcast')
+ ->where('description', 'The Laravel Podcast brings you Laravel and PHP development news and discussion.')
+ ->has('seasons', 4)
+ ->has('seasons.4.episodes', 21)
+ ->has('host', fn (Assert $page) => $page
+ ->where('id', 1)
+ ->where('name', 'Matt Stauffer')
+ )
+ ->has('subscribers', 7, fn (Assert $page) => $page
+ ->where('id', 2)
+ ->where('name', 'Claudio Dekker')
+ ->where('platform', 'Apple Podcasts')
+ ->etc()
+ ->missing('email')
+ ->missing('password')
+ )
+ )
+ );
+ }
+}
+```
+
+As you can see in the example above, you may use these assertion methods to assert against the content of the data provided to the Inertia response. In addition, you may assert that array data has a given length as well as scope your assertions.
+
+You may use the `inertiaProps` method to retrieve the props returned in the response. You can pass a key to retrieve a specific property, and nested properties are supported using "dot" notation.
+
+```php
+$response = $this->get('/podcasts/41');
+
+// Returns all props...
+$response->inertiaProps();
+
+// Returns a specific prop...
+$response->inertiaProps('podcast');
+
+// Returns a nested prop using "dot" notation...
+$response->inertiaProps('podcast.id');
+```
+
+Let's dig into the `assertInertia` method and the available assertions in detail. First, to assert that the Inertia response has a property, you may use the `has` method. You can think of this method as being similar to PHP's `isset` function.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ // Checking if a root-level property has 7 items...
+ ->has('podcasts', 7)
+
+ // Checking nested properties using "dot" notation...
+ ->has('podcast.subscribers', 7)
+);
+```
+
+To assert that an Inertia property has a specified amount of items, you may provide the expected size as the second argument to the `has` method.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ // Checking if a root-level property has 7 items...
+ ->has('podcasts', 7)
+
+ // Checking nested properties using "dot" notation...
+ ->has('podcast.subscribers', 7)
+);
+```
+
+The `has` method may also be used to scope properties in order to lessen repetition when asserting against nested properties.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ // Creating a single-level property scope...
+ ->has('message', fn (Assert $page) => $page
+ // We can now continue chaining methods...
+ ->has('subject')
+ ->has('comments', 5)
+
+ // And can even create a deeper scope using "dot" notation...
+ ->has('comments.0', fn (Assert $page) => $page
+ ->has('body')
+ ->has('files', 1)
+ ->has('files.0', fn (Assert $page) => $page
+ ->has('url')
+ )
+ )
+ )
+);
+```
+
+When scoping into Inertia properties that are arrays or collections, you may also assert that a specified number of items are present in addition to scoping into the first item.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ // Assert that there are 5 comments and automatically scope into the first comment...
+ ->has('comments', 5, fn (Assert $page) => $page
+ ->has('body')
+ // ...
+ )
+);
+```
+
+To assert that an Inertia property has an expected value, you may use the `where` assertion.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('message', fn (Assert $page) => $page
+ // Assert that the subject prop matches the given message...
+ ->where('subject', 'This is an example message')
+
+ // Or, assert against deeply nested values...
+ ->where('comments.0.files.0.name', 'example-attachment.pdf')
+ )
+);
+```
+
+Inertia's testing methods will automatically fail when you haven't interacted with at least one of the props in a scope. While this is generally useful, you might run into situations where you're working with unreliable data (such as from an external feed), or with data that you really don't want interact with in order to keep your test simple. For these situations, the `etc` method exists.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('message', fn (Assert $page) => $page
+ ->has('subject')
+ ->has('comments')
+ ->etc()
+ )
+);
+```
+
+The `missing` method is the exact opposite of the `has` method, ensuring that the property does not exist. This method makes a great companion to the `etc` method.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('message', fn (Assert $page) => $page
+ ->has('subject')
+ ->missing('published_at')
+ ->etc()
+ )
+);
+```
+
+### Testing Partial Reloads
+
+You may use the `reloadOnly` and `reloadExcept` methods to test how your application responds to [partial reloads](/v3/data-props/partial-reloads). These methods perform a follow-up request and allow you to make assertions against the response.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('orders')
+ ->missing('statuses')
+ ->reloadOnly('statuses', fn (Assert $reload) => $reload
+ ->missing('orders')
+ ->has('statuses', 5)
+ )
+);
+```
+
+Instead of passing a single prop as a string, you may also pass an array of props to `reloadOnly` or `reloadExcept`.
+
+### Testing Deferred Props
+
+You may use the `loadDeferredProps` method to test how your application responds to [deferred props](/v3/data-props/deferred-props). This method performs a follow-up request to load the deferred properties and allows you to make assertions against the response.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('users')
+ ->has('roles')
+ ->missing('permissions') // Deferred prop not in initial response
+ ->loadDeferredProps(fn (Assert $reload) => $reload
+ ->has('permissions')
+ ->where('permissions.0.name', 'edit users')
+ )
+);
+```
+
+You may also load specific deferred prop groups by passing the group name as the first argument to the `loadDeferredProps` method.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->has('users')
+ ->missing('teams')
+ ->missing('projects')
+ ->loadDeferredProps('attributes', fn (Assert $reload) => $reload
+ ->has('teams', 5)
+ ->has('projects')
+ ->missing('permissions') // Different group
+ )
+);
+```
+
+Instead of passing a single group as a string, you may also pass an array of groups to `loadDeferredProps`.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ ->loadDeferredProps(['default', 'attributes'], fn (Assert $reload) => $reload
+ ->has('permissions')
+ ->has('teams')
+ ->has('projects')
+ )
+);
+```
+
+### Testing Flash Data
+
+You may use the `hasFlash` and `missingFlash` methods to test [flash data](/v3/data-props/flash-data) in your Inertia responses.
+
+```php
+$response->assertInertia(fn (Assert $page) => $page
+ // Assert flash data exists...
+ ->hasFlash('message')
+
+ // Assert flash data has a specific value...
+ ->hasFlash('message', 'Item saved!')
+
+ // Nested values are supported using "dot" notation...
+ ->hasFlash('notification.type', 'success')
+
+ // Assert flash data does not exist...
+ ->missingFlash('error')
+);
+```
+
+#### Redirect Responses
+
+The `hasFlash` and `missingFlash` methods above only work on rendered Inertia page responses. For redirect responses, you may use the `assertInertiaFlash` and `assertInertiaFlashMissing` methods directly on the test response to assert against the session's flash data.
+
+```php
+$response = $this->post('/users');
+
+$response->assertRedirect('/dashboard')
+ ->assertInertiaFlash('message')
+ ->assertInertiaFlash('message', 'User created!')
+ ->assertInertiaFlash('notification.type', 'success')
+ ->assertInertiaFlashMissing('error');
+```
diff --git a/v3/advanced/typescript.mdx b/v3/advanced/typescript.mdx
new file mode 100644
index 0000000..a8e88b6
--- /dev/null
+++ b/v3/advanced/typescript.mdx
@@ -0,0 +1,479 @@
+---
+title: TypeScript
+---
+
+Inertia provides first-class TypeScript support. You may configure global types using declaration merging, and pass generics to hooks and router methods for type-safe props, forms, and state management.
+
+## Using pnpm
+
+Due to pnpm's strict dependency isolation, `@inertiajs/core` is not accessible at `node_modules/@inertiajs/core`. Instead, it's nested inside `.pnpm/`, which prevents TypeScript module augmentation from resolving the module.
+
+You may fix this by configuring pnpm to [hoist the package](https://pnpm.io/settings#publichoistpattern). Add the following to your `.npmrc` file and run `pnpm install`.
+
+```ini
+public-hoist-pattern[]=@inertiajs/core
+```
+
+Alternatively, you may add `@inertiajs/core` as a direct dependency in your project.
+
+```bash
+pnpm add @inertiajs/core
+```
+
+## Global Configuration
+
+You may configure Inertia's types globally by augmenting the `InertiaConfig` interface in the `@inertiajs/core` module. This is typically done in a `global.d.ts` file in your project's root or `types` directory.
+
+```ts
+// global.d.ts
+declare module "@inertiajs/core" {
+ export interface InertiaConfig {
+ sharedPageProps: {
+ auth: { user: { id: number; name: string } | null };
+ appName: string;
+ };
+ flashDataType: {
+ toast?: { type: "success" | "error"; message: string };
+ };
+ errorValueType: string[];
+ }
+}
+```
+
+
+ For module augmentation to work, your `tsconfig.json` needs to include `.d.ts`
+ files. Make sure a pattern like `"resources/js/**/*.d.ts"` is present in the
+ `include` array, adjusted to match your project's directory structure.
+
+
+### Shared Page Props
+
+The `sharedPageProps` option defines the type of data that is [shared](/v3/data-props/shared-data) with every page in your application. With this configuration, `page.props.auth` and `page.props.appName` will be properly typed everywhere.
+
+```ts
+sharedPageProps: {
+ auth: { user: { id: number; name: string } | null }
+ appName: string
+}
+```
+
+### Flash Data
+
+The `flashDataType` option defines the type of [flash data](/v3/data-props/flash-data) in your application.
+
+```ts
+flashDataType: {
+ toast?: { type: 'success' | 'error'; message: string }
+}
+```
+
+### Error Values
+
+By default, validation error values are typed as `string`. You may configure TypeScript to expect arrays instead for [multiple errors per field](/v3/the-basics/validation#multiple-errors-per-field).
+
+```ts
+errorValueType: string[]
+```
+
+
+ The next version of [Laravel
+ Wayfinder](https://github.com/laravel/wayfinder/tree/next) may automatically
+ generate these types for you by analyzing your Laravel application. It
+ generates TypeScript types for shared props, page props, form requests, and
+ Eloquent models. This version is currently in beta.
+
+
+## Page Components
+
+You may type the `import.meta.glob` result for better type safety when resolving page components.
+
+
+
+```ts Vue icon="vuejs"
+import { createInertiaApp } from "@inertiajs/vue3";
+import type { DefineComponent } from "vue";
+
+createInertiaApp({
+ resolve: (name) => {
+ const pages = import.meta.glob("./Pages/**/*.vue");
+ return pages[`./Pages/${name}.vue`]();
+ },
+ // ...
+});
+```
+
+```tsx React icon="react"
+import { createInertiaApp, type ResolvedComponent } from "@inertiajs/react";
+
+createInertiaApp({
+ resolve: (name) => {
+ const pages = import.meta.glob("./Pages/**/*.tsx");
+ return pages[`./Pages/${name}.tsx`]();
+ },
+ // ...
+});
+```
+
+```ts Svelte icon="s"
+import { createInertiaApp, type ResolvedComponent } from "@inertiajs/svelte";
+
+createInertiaApp({
+ resolve: (name) => {
+ const pages = import.meta.glob("./Pages/**/*.svelte");
+ return pages[`./Pages/${name}.svelte`]();
+ },
+ // ...
+});
+```
+
+
+
+## Page Props
+
+You may type page-specific props by passing a generic to `usePage()`. These are merged with your global `sharedPageProps`, giving you autocomplete and type checking for both shared and page-specific data.
+
+
+
+```vue Vue icon="vuejs"
+
+```
+
+```tsx React icon="react"
+import { usePage } from "@inertiajs/react";
+
+export default function Posts() {
+ const page = usePage<{
+ posts: { id: number; title: string }[];
+ }>();
+
+ return (
+
+ {page.props.posts.map((post) => (
+
{post.title}
+ ))}
+
+ );
+}
+```
+
+```svelte Svelte icon="s"
+
+```
+
+
+
+## Form Helper
+
+The [form helper](/v3/the-basics/forms#form-helper) accepts a generic type parameter for type-safe form data and error handling. This provides autocomplete for form fields and errors, and prevents typos in field names.
+
+
+
+```vue Vue icon="vuejs"
+
+```
+
+```tsx React icon="react"
+import { useForm } from "@inertiajs/react";
+
+export default function CreateUser() {
+ const form = useForm<{
+ name: string;
+ email: string;
+ company: { name: string };
+ }>({
+ name: "",
+ email: "",
+ company: { name: "" },
+ });
+
+ return null;
+}
+```
+
+```svelte Svelte icon="s"
+
+```
+
+
+
+### Nested Data and Arrays
+
+Form types fully support nested objects and arrays. You may access and update nested fields using dot notation, and error keys are automatically typed to match.
+
+```ts
+import { useForm } from "@inertiajs/react";
+
+const form = useForm<{
+ user: { name: string; email: string };
+ tags: { id: number; label: string }[];
+}>({
+ user: { name: "", email: "" },
+ tags: [],
+});
+```
+
+## Form Component
+
+The `