Skip to content

Commit 3361f86

Browse files
tnyleataylorotwell
authored andcommitted
Fixing Inertia SSR errors (laravel#74)
* Adding initial SSR fixes * fixing warning messages in SSR * Adding updates to ssr file to include global route var * Adding dev:ssr command * Adding light/dark mode fixes to support cookies and localstorage * updating the intertia return array * formatting * run formatter --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 3498538 commit 3361f86

File tree

15 files changed

+173
-11
lines changed

15 files changed

+173
-11
lines changed
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\View;
8+
use Symfony\Component\HttpFoundation\Response;
9+
10+
class HandleAppearance
11+
{
12+
/**
13+
* Handle an incoming request.
14+
*
15+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
16+
*/
17+
public function handle(Request $request, Closure $next): Response
18+
{
19+
View::share('appearance', $request->cookie('appearance') ?? 'system');
20+
21+
return $next($request);
22+
}
23+
}

app/Http/Middleware/HandleInertiaRequests.php

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Foundation\Inspiring;
66
use Illuminate\Http\Request;
77
use Inertia\Middleware;
8+
use Tighten\Ziggy\Ziggy;
89

910
class HandleInertiaRequests extends Middleware
1011
{
@@ -45,6 +46,10 @@ public function share(Request $request): array
4546
'auth' => [
4647
'user' => $request->user(),
4748
],
49+
'ziggy' => [
50+
...(new Ziggy)->toArray(),
51+
'location' => $request->url(),
52+
],
4853
];
4954
}
5055
}

bootstrap/app.php

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\HandleAppearance;
34
use App\Http\Middleware\HandleInertiaRequests;
45
use Illuminate\Foundation\Application;
56
use Illuminate\Foundation\Configuration\Exceptions;
@@ -13,7 +14,10 @@
1314
health: '/up',
1415
)
1516
->withMiddleware(function (Middleware $middleware) {
17+
$middleware->encryptCookies(except: ['appearance']);
18+
1619
$middleware->web(append: [
20+
HandleAppearance::class,
1721
HandleInertiaRequests::class,
1822
AddLinkHeadersForPreloadedAssets::class,
1923
]);

composer.json

+5
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
"dev": [
5656
"Composer\\Config::disableProcessTimeout",
5757
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
58+
],
59+
"dev:ssr": [
60+
"npm run build:ssr",
61+
"Composer\\Config::disableProcessTimeout",
62+
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr"
5863
]
5964
},
6065
"extra": {

resources/js/components/TextLink.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ defineProps<Props>();
1717
:tabindex="tabindex"
1818
:method="method"
1919
:as="as"
20-
class="hover:!decoration-current text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out dark:decoration-neutral-500"
20+
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:!decoration-current dark:decoration-neutral-500"
2121
>
2222
<slot />
2323
</Link>

resources/js/composables/useAppearance.ts

+46-5
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,63 @@ import { onMounted, ref } from 'vue';
33
type Appearance = 'light' | 'dark' | 'system';
44

55
export function updateTheme(value: Appearance) {
6+
if (typeof window === 'undefined') {
7+
return;
8+
}
9+
610
if (value === 'system') {
7-
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
11+
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
12+
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
13+
814
document.documentElement.classList.toggle('dark', systemTheme === 'dark');
915
} else {
1016
document.documentElement.classList.toggle('dark', value === 'dark');
1117
}
1218
}
1319

14-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
20+
const setCookie = (name: string, value: string, days = 365) => {
21+
if (typeof document === 'undefined') {
22+
return;
23+
}
24+
25+
const maxAge = days * 24 * 60 * 60;
26+
27+
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
28+
};
29+
30+
const mediaQuery = () => {
31+
if (typeof window === 'undefined') {
32+
return null;
33+
}
34+
35+
return window.matchMedia('(prefers-color-scheme: dark)');
36+
};
37+
38+
const getStoredAppearance = () => {
39+
if (typeof window === 'undefined') {
40+
return null;
41+
}
42+
43+
return localStorage.getItem('appearance') as Appearance | null;
44+
};
1545

1646
const handleSystemThemeChange = () => {
17-
const currentAppearance = localStorage.getItem('appearance') as Appearance | null;
47+
const currentAppearance = getStoredAppearance();
48+
1849
updateTheme(currentAppearance || 'system');
1950
};
2051

2152
export function initializeTheme() {
53+
if (typeof window === 'undefined') {
54+
return;
55+
}
56+
2257
// Initialize theme from saved preference or default to system...
23-
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
58+
const savedAppearance = getStoredAppearance();
2459
updateTheme(savedAppearance || 'system');
2560

2661
// Set up system theme change listener...
27-
mediaQuery.addEventListener('change', handleSystemThemeChange);
62+
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
2863
}
2964

3065
export function useAppearance() {
@@ -42,7 +77,13 @@ export function useAppearance() {
4277

4378
function updateAppearance(value: Appearance) {
4479
appearance.value = value;
80+
81+
// Store in localStorage for client-side persistence...
4582
localStorage.setItem('appearance', value);
83+
84+
// Store in cookie for SSR...
85+
setCookie('appearance', value);
86+
4687
updateTheme(value);
4788
}
4889

resources/js/layouts/settings/Layout.vue

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Heading from '@/components/Heading.vue';
33
import { Button } from '@/components/ui/button';
44
import { Separator } from '@/components/ui/separator';
55
import { type NavItem } from '@/types';
6-
import { Link } from '@inertiajs/vue3';
6+
import { Link, usePage } from '@inertiajs/vue3';
77
88
const sidebarNavItems: NavItem[] = [
99
{
@@ -20,7 +20,9 @@ const sidebarNavItems: NavItem[] = [
2020
},
2121
];
2222
23-
const currentPath = window.location.pathname;
23+
const page = usePage();
24+
25+
const currentPath = page.props.ziggy?.location ? new URL(page.props.ziggy.location).pathname : '';
2426
</script>
2527

2628
<template>

resources/js/pages/settings/Profile.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const submit = () => {
7777
:href="route('verification.send')"
7878
method="post"
7979
as="button"
80-
class="hover:!decoration-current text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out dark:decoration-neutral-500"
80+
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:!decoration-current dark:decoration-neutral-500"
8181
>
8282
Click here to resend the verification email.
8383
</Link>

resources/js/ssr.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createInertiaApp } from '@inertiajs/vue3';
2+
import createServer from '@inertiajs/vue3/server';
3+
import { renderToString } from '@vue/server-renderer';
4+
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
5+
import { createSSRApp, h } from 'vue';
6+
import { route as ziggyRoute } from 'ziggy-js';
7+
8+
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
9+
10+
createServer((page) =>
11+
createInertiaApp({
12+
page,
13+
render: renderToString,
14+
title: (title) => `${title} - ${appName}`,
15+
resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob('./pages/**/*.vue')),
16+
setup({ App, props, plugin }) {
17+
const app = createSSRApp({ render: () => h(App, props) });
18+
19+
// Configure Ziggy for SSR...
20+
const ziggyConfig = {
21+
...page.props.ziggy,
22+
location: new URL(page.props.ziggy.location),
23+
};
24+
25+
// Create route function...
26+
const route = (name: string, params?: any, absolute?: boolean) => ziggyRoute(name, params, absolute, ziggyConfig);
27+
28+
// Make route function available globally...
29+
app.config.globalProperties.route = route;
30+
31+
// Make route function available globally for SSR...
32+
if (typeof window === 'undefined') {
33+
global.route = route;
34+
}
35+
36+
app.use(plugin);
37+
38+
return app;
39+
},
40+
}),
41+
);

resources/js/types/globals.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { route as routeFn } from 'ziggy-js';
2+
3+
declare global {
4+
const route: typeof routeFn;
5+
}

resources/js/types/index.ts renamed to resources/js/types/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PageProps } from '@inertiajs/core';
22
import type { LucideIcon } from 'lucide-vue-next';
3+
import type { Config } from 'ziggy-js';
34

45
export interface Auth {
56
user: User;
@@ -21,6 +22,7 @@ export interface SharedData extends PageProps {
2122
name: string;
2223
quote: { message: string; author: string };
2324
auth: Auth;
25+
ziggy: Config & { location: string };
2426
}
2527

2628
export interface User {
File renamed without changes.

resources/views/app.blade.php

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
11
<!DOCTYPE html>
2-
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
2+
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => ($appearance ?? 'system') == 'dark'])>
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66

7+
{{-- Inline script to detect system dark mode preference and apply it immediately --}}
8+
<script>
9+
(function() {
10+
const appearance = '{{ $appearance ?? "system" }}';
11+
12+
if (appearance === 'system') {
13+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
14+
15+
if (prefersDark) {
16+
document.documentElement.classList.add('dark');
17+
}
18+
}
19+
})();
20+
</script>
21+
22+
{{-- Inline style to set the HTML background color based on our theme in app.css --}}
23+
<style>
24+
html {
25+
background-color: oklch(1 0 0);
26+
}
27+
28+
html.dark {
29+
background-color: oklch(0.145 0 0);
30+
}
31+
</style>
32+
733
<title inertia>{{ config('app.name', 'Laravel') }}</title>
834

935
<link rel="preconnect" href="https://fonts.bunny.net">

tsconfig.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,10 @@
119119
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
120120
"skipLibCheck": true /* Skip type checking all .d.ts files. */
121121
},
122-
"include": ["resources/js/**/*.ts", "resources/js/**/*.d.ts", "resources/js/**/*.tsx", "resources/js/**/*.vue"]
122+
"include": [
123+
"resources/js/**/*.ts",
124+
"resources/js/**/*.d.ts",
125+
"resources/js/**/*.tsx",
126+
"resources/js/**/*.vue"
127+
]
123128
}

vite.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import autoprefixer from 'autoprefixer';
33
import laravel from 'laravel-vite-plugin';
44
import path from 'path';
55
import tailwindcss from 'tailwindcss';
6+
import { resolve } from 'node:path';
67
import { defineConfig } from 'vite';
78

89
export default defineConfig({
910
plugins: [
1011
laravel({
1112
input: ['resources/js/app.ts'],
13+
ssr: 'resources/js/ssr.ts',
1214
refresh: true,
1315
}),
1416
vue({
@@ -23,6 +25,7 @@ export default defineConfig({
2325
resolve: {
2426
alias: {
2527
'@': path.resolve(__dirname, './resources/js'),
28+
'ziggy-js': resolve(__dirname, 'vendor/tightenco/ziggy'),
2629
},
2730
},
2831
css: {

0 commit comments

Comments
 (0)