Skip to content

Sitemap (Optional) ​

Serve /sitemap.xml from Laravel using the five public routes from Page Stubs. Keep an explicit URL list; no crawler, package, generated file, or scheduled task is needed.

Sitemap Controller ​

bash
php artisan make:controller SitemapController --invokable --no-interaction

File: app/Http/Controllers/SitemapController.php

php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;

class SitemapController extends Controller
{
    public function __invoke(): Response
    {
        return response()
            ->view('sitemap', [
                'urls' => [
                    route('home'),
                    route('about'),
                    route('contact'),
                    route('privacy'),
                    route('terms'),
                ],
            ])
            ->header('Content-Type', 'application/xml; charset=UTF-8');
    }
}

Keep only public, canonical, indexable pages. Leave login, registration, Dashboard, and Settings out. Update the list when adding public pages; sitemap omission is not access control.

XML View ​

Return a standalone Blade view, not an Inertia page or the application shell.

File: resources/views/sitemap.blade.php

bash
touch resources/views/sitemap.blade.php
blade
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    @foreach($urls as $url)
        <url>
            <loc>{{ $url }}</loc>
        </url>
    @endforeach
</urlset>

Keep the escaped Blade output so query-string ampersands become &amp;. The controller sets the XML content type. No lastmod is included; add it only when a real content modification date is available.

Route ​

Add the SitemapController import and named GET route outside the authenticated group. Keep the existing public pages, contact POST route, Dashboard, and settings routes.

File: routes/web.php

php
<?php

use App\Http\Controllers\ContactFormController;
use App\Http\Controllers\SitemapController;
use Illuminate\Support\Facades\Route;
use Laravel\Fortify\Features;

Route::inertia('/', 'Home', [
    'canRegister' => Features::enabled(Features::registration()),
])->name('home');
Route::inertia('about', 'About')->name('about');
Route::inertia('contact', 'Contact')->name('contact');
Route::inertia('privacy', 'Privacy')->name('privacy');
Route::inertia('terms', 'Terms')->name('terms');
Route::get('sitemap.xml', SitemapController::class)->name('sitemap');

Route::post('contact', [ContactFormController::class, 'store'])
    ->middleware('throttle:5,1')
    ->name('contact.store');

Route::middleware(['auth', 'verified'])->group(function () {
    Route::inertia('dashboard', 'Dashboard')->name('dashboard');
});

require __DIR__.'/settings.php';

Do not keep a generated public/sitemap.xml alongside this route: the web server may serve that file instead. The Spatie Sitemap guide is an alternative, not an additional setup step.

Robots ​

Create the robots file:

bash
touch public/robots.txt

Allow all crawlers and add the sitemap URL with the real production domain:

File: public/robots.txt

text
User-agent: *
Disallow:

Sitemap: https://example.com/sitemap.xml

Keep APP_URL set to the canonical production URL. For this HTTP route, URL generation also uses the request host and scheme; check HTTPS/proxy configuration if the output is wrong.

Sitemap Tests ​

Check guest access, the XML content type and namespace, the exact public URL list, and escaping without changing URL values. PHP's SimpleXML extension must be enabled.

File: tests/Feature/SitemapTest.php

bash
touch tests/Feature/SitemapTest.php
php
<?php

test('guests receive a valid XML sitemap containing only the public pages', function () {
    $response = $this->get(route('sitemap'));

    $response->assertOk()
        ->assertHeader('Content-Type', 'application/xml; charset=UTF-8');

    $sitemap = simplexml_load_string($response->getContent());

    expect($sitemap)->toBeInstanceOf(SimpleXMLElement::class);
    expect($sitemap->getName())->toBe('urlset');
    expect($sitemap->getDocNamespaces())->toBe([
        '' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
    ]);
    expect(array_map(
        fn (SimpleXMLElement $location): string => (string) $location,
        $sitemap->xpath('//*[local-name()="loc"]'),
    ))->toBe([
        url('/'),
        url('/about'),
        url('/contact'),
        url('/privacy'),
        url('/terms'),
    ]);
});

test('sitemap URLs are XML escaped without changing their values', function () {
    $url = 'https://example.com/search?category=books&sort=newest';

    $content = view('sitemap', ['urls' => [$url]])->render();

    $sitemap = simplexml_load_string($content);

    expect($sitemap)->toBeInstanceOf(SimpleXMLElement::class);
    expect((string) $sitemap->url->loc)->toBe($url);
});

Run in the Laravel application:

bash
php artisan test --compact tests/Feature/SitemapTest.php
php artisan route:list --path=sitemap
  • Open /sitemap.xml as a guest: XML with exactly the five public URLs, no application layout or login redirect.
  • Check that every URL uses the production host and HTTPS and opens the intended page; do not publish .test or localhost URLs.
  • Open /robots.txt and confirm its sitemap URL matches the live endpoint.

Commit ​

bash
git add -A
git commit -m '09 - Sitemap'

References ​