Skip to content
This repository was archived by the owner on Oct 20, 2025. It is now read-only.

Commit 0701a87

Browse files
authored
Support for handling SEO (#20)
1 parent b9e924d commit 0701a87

File tree

22 files changed

+474
-25
lines changed

22 files changed

+474
-25
lines changed

app/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
APP_NAME=Laravel
1+
APP_NAME="Laravel Splade"
22
APP_ENV=local
33
APP_KEY=base64:TBVjDAO1zLKHWGrb6ZI3JWSoJgUWkB8Wf6GfQdes4h8=
44
APP_DEBUG=true
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use ProtoneMedia\Splade\Facades\SEO;
6+
7+
class ModalController
8+
{
9+
public function base()
10+
{
11+
SEO::title('Modal Base');
12+
13+
return view('modal.base');
14+
}
15+
16+
public function one()
17+
{
18+
SEO::title('Modal One');
19+
20+
return view('modal.one');
21+
}
22+
23+
public function two()
24+
{
25+
SEO::title('Modal Two');
26+
27+
return view('modal.two');
28+
}
29+
30+
public function slideover()
31+
{
32+
return view('modal.slideover');
33+
}
34+
35+
public function validation()
36+
{
37+
return view('modal.validation');
38+
}
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use ProtoneMedia\Splade\Facades\SEO;
6+
7+
class NavigationController
8+
{
9+
public function one()
10+
{
11+
SEO::title('Navigation One')
12+
->description('First Navigation')
13+
->keywords('een, one');
14+
15+
return view('navigation.one');
16+
}
17+
18+
public function two()
19+
{
20+
SEO::title('Navigation Two')
21+
->description('Second Navigation')
22+
->keywords(['twee', 'two']);
23+
24+
return view('navigation.two');
25+
}
26+
27+
public function three()
28+
{
29+
return view('navigation.three');
30+
}
31+
32+
public function form()
33+
{
34+
return view('navigation.form');
35+
}
36+
}

app/resources/views/root.blade.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66

7-
<title>Laravel</title>
8-
97
<!-- Fonts -->
108
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
119

10+
@spladeHead
1211
@vite('resources/js/app.js')
1312
</head>
1413
<body class="antialiased">

app/routes/web.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use App\Events\ToastEvent;
77
use App\Http\Controllers\BackFormController;
88
use App\Http\Controllers\FileFormController;
9+
use App\Http\Controllers\ModalController;
10+
use App\Http\Controllers\NavigationController;
911
use App\Http\Controllers\SimpleFormController;
1012
use App\Http\Controllers\SlowFormController;
1113
use App\Http\Controllers\ToastController;
@@ -69,18 +71,19 @@
6971
Route::view('form/jsonable', 'form.jsonable')->name('form.jsonable');
7072
Route::view('form/jsonSerializable', 'form.jsonSerializable')->name('form.jsonSerializable');
7173

72-
Route::view('navigation/one', 'navigation.one')->name('navigation.one');
73-
Route::view('navigation/two', 'navigation.two')->name('navigation.two');
74-
Route::view('navigation/three', 'navigation.three')->name('navigation.three');
75-
Route::view('navigation/form', 'navigation.form')->name('navigation.form');
74+
Route::get('navigation/one', [NavigationController::class, 'one'])->name('navigation.one');
75+
Route::get('navigation/two', [NavigationController::class, 'two'])->name('navigation.two');
76+
Route::get('navigation/three', [NavigationController::class, 'three'])->name('navigation.three');
77+
Route::get('navigation/form', [NavigationController::class, 'form'])->name('navigation.form');
78+
7679
Route::get('navigation/notFound', fn () => abort(404))->name('navigation.notFound');
7780
Route::get('navigation/serverError', fn () => throw new Exception('Whoops!'))->name('navigation.serverError');
7881

79-
Route::view('modal/base', 'modal.base')->name('modal.base');
80-
Route::view('modal/one', 'modal.one')->name('modal.one');
81-
Route::view('modal/two', 'modal.two')->name('modal.two');
82-
Route::view('modal/slideover', 'modal.slideover')->name('modal.slideover');
83-
Route::view('modal/validation', 'modal.validation')->name('modal.validation');
82+
Route::get('modal/base', [ModalController::class, 'base'])->name('modal.base');
83+
Route::get('modal/one', [ModalController::class, 'one'])->name('modal.one');
84+
Route::get('modal/two', [ModalController::class, 'two'])->name('modal.two');
85+
Route::get('modal/slideover', [ModalController::class, 'slideover'])->name('modal.slideover');
86+
Route::get('modal/validation', [ModalController::class, 'validation'])->name('modal.validation');
8487

8588
Route::post('state', function () {
8689
Splade::share('info', 'This is invalid');

app/tests/Browser.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Tests;
4+
5+
use Laravel\Dusk\Browser as BaseBrowser;
6+
use PHPUnit\Framework\Assert as PHPUnit;
7+
8+
class Browser extends BaseBrowser
9+
{
10+
public function assertMetaByName($name, $content)
11+
{
12+
$driverContent = $this->driver->executeScript('return document.querySelector("meta[name=\"' . $name . '\"]")?.getAttribute("content")');
13+
14+
PHPUnit::assertEquals(
15+
$content,
16+
$driverContent,
17+
"Meta with name [{$name}] expected content [{$content}] does not equal actual title [{$driverContent}]."
18+
);
19+
20+
return $this;
21+
}
22+
}

app/tests/Browser/HeadTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Tests\Browser;
4+
5+
use Tests\Browser;
6+
use Tests\DuskTestCase;
7+
8+
class HeadTest extends DuskTestCase
9+
{
10+
/** @test */
11+
public function it_updates_the_title_and_meta_tags()
12+
{
13+
$this->browse(function (Browser $browser) {
14+
$browser->visit('/navigation/one')
15+
->waitForText('NavigationOne')
16+
->assertTitle('Navigation One')
17+
->assertMetaByName('description', 'First Navigation')
18+
->assertMetaByName('keywords', 'een, one')
19+
->click('@two')
20+
->waitForText('NavigationTwo')
21+
->assertTitle('Navigation Two')
22+
->assertMetaByName('description', 'Second Navigation')
23+
->assertMetaByName('keywords', 'twee, two')
24+
->click('@three')
25+
->waitForText('NavigationThree')
26+
27+
// defaults:
28+
->assertTitle('Laravel Splade')
29+
->assertMetaByName('description', 'Splade provides a super easy way to build Single Page Applications (SPA) using standard Laravel Blade templates, enhanced with renderless Vue 3 components.')
30+
->assertMetaByName('keywords', 'Laravel, Splade');
31+
});
32+
}
33+
34+
/** @test */
35+
public function it_updates_the_head_when_modals_are_opened_and_closed()
36+
{
37+
$this->browse(function (Browser $browser) {
38+
$browser->visit('/modal/base')
39+
->waitForText('ModalComponent')
40+
->assertTitle('Modal Base')
41+
->click('@one')
42+
->waitForText('ModalComponentOne')
43+
->pause(500)
44+
->assertTitle('Modal One')
45+
->click('@two')
46+
->waitForText('ModalComponentTwo')
47+
->pause(500)
48+
->assertTitle('Modal Two')
49+
->click('@close-two')
50+
->waitForText('ModalComponentOne')
51+
->pause(500)
52+
->assertTitle('Modal One')
53+
->click('@close-one')
54+
->pause(500)
55+
->assertTitle('Modal Base');
56+
});
57+
}
58+
}

app/tests/DuskTestCase.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Facebook\WebDriver\Remote\DesiredCapabilities;
77
use Facebook\WebDriver\Remote\RemoteWebDriver;
88
use Illuminate\Support\Arr;
9-
use Laravel\Dusk\Browser;
109
use Laravel\Dusk\TestCase as BaseTestCase;
1110
use Spatie\Snapshots\MatchesSnapshots;
1211

@@ -15,6 +14,17 @@ abstract class DuskTestCase extends BaseTestCase
1514
use CreatesApplication;
1615
use MatchesSnapshots;
1716

17+
/**
18+
* Create a new Browser instance.
19+
*
20+
* @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
21+
* @return \Tests\Browser
22+
*/
23+
protected function newBrowser($driver)
24+
{
25+
return new Browser($driver);
26+
}
27+
1828
/**
1929
* Prepare for Dusk test execution.
2030
*

app/tests/Feature/HeadTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Tests\TestCase;
6+
7+
class HeadTest extends TestCase
8+
{
9+
/** @test */
10+
public function it_renders_the_head()
11+
{
12+
$this->get('/navigation/one')
13+
->assertSee('<title>Navigation One</title>', false)
14+
->assertSee('<meta name="description" content="First Navigation" />', false)
15+
->assertSee('<meta name="keywords" content="een, one" />', false);
16+
}
17+
}

app/tests/Feature/SsrTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ public function it_has_a_server_that_handles_ssr_requests()
2424
$this->assertArrayHasKey('body', $data);
2525

2626
// evaluated form
27-
$this->assertStringContainsString('<form><input dusk="name" value="Splade"></form>', $data['body']);
27+
$this->assertStringContainsString('<form><input dusk="name" value="Splade"></form>', $data['body'] ?? '');
2828

2929
// rendered components
30-
$this->assertStringContainsString('grid grid-cols-3 grid-flow-row-3', $data['body']);
30+
$this->assertStringContainsString('grid grid-cols-3 grid-flow-row-3', $data['body'] ?? '');
3131
}
3232
}

0 commit comments

Comments
 (0)