Laravel XML Sitemap Tutorial: Build a Custom System with Filament

Build a custom XML sitemap system in Laravel with a Filament admin panel, scheduled generation, and automatic submission to Google Search Console.

Laravel XML Sitemap Tutorial

You’ve likely struggled to create a custom XML sitemap for your Laravel application, especially if you’re using Filament as your admin interface. Manually configuring routes and cache settings can be tedious, not to mention the hassle of dealing with XML generation.

If you’re using Filament to manage your app’s content, it’s frustrating when generating an up-to-date XML sitemap becomes a chore. You’ll build a robust and dynamic XML sitemap system that integrates seamlessly with your existing application, allowing for easy customization and management through Filament’s CRUD interface. By the end of this tutorial, you’ll have defined sitemap URLs using Eloquent relationships and implemented automated XML generation using a service class.

Installing Filament in Your Laravel Project

To create a custom XML sitemap for your Laravel application with Filament, you’ll first need to install it in your project.

Step 1: Installing Filament via Composer

Open your terminal and navigate to your project directory. Run the following command to add Filament as a dependency:

composer require filament/filament:"^3.3" -W

This installs Filament v3 (3.3.x), which is the version the code in this tutorial is written for. Filament 3.3 supports Laravel 10 through 13. Newer major versions (Filament 4 and 5) change several resource APIs, so if you install one of those, adapt the resource code below to its documentation.

Step 2: Publishing Filament Assets

After installation, run the following Artisan commands to install the admin panel, publish Filament’s assets and create your first admin user:

php artisan filament:install --panels
php artisan make:filament-user

This will create and register a panel provider at app/Providers/Filament/AdminPanelProvider.php and publish the assets required for Filament.

Step 3: Registering Filament Middleware

Finally, check the middleware of your panel. Filament doesn’t use the global kernel.php middleware stack; each panel registers its own middleware in its provider (app/Providers/Filament/AdminPanelProvider.php), and the authentication middleware that protects the panel is already in place:

use Filament\Http\Middleware\Authenticate;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        ->default()
        ->id('admin')
        ->path('admin')
        ->login()
        // ...
        ->authMiddleware([
            Authenticate::class,
        ]);
}

That’s it for this step! You should now have Filament installed and ready to use in your Laravel project. In the next section, we’ll create a sitemap model using Filament’s CRUD interface.

Creating a Sitemap Model with Filament’s CRUD Interface

Now that we’ve installed Filament, let’s create our sitemap model using its built-in CRUD interface. This will allow us to easily manage and customize our sitemap data.

First, run the following commands in your terminal:

php artisan make:model Sitemap -m
php artisan make:filament-resource Sitemap

This will generate a Sitemap model with a migration, plus a Filament resource. In the generated migration, add two fields, title and url, then run php artisan migrate. We can adjust these fields as needed later on:

Schema::create('sitemaps', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('url');
    $table->timestamps();
});

Next, open the generated file app\Models\Sitemap.php and add the following code:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Sitemap extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'url'
    ];
}

Then open the generated resource, app/Filament/Resources/SitemapResource.php, and define the form fields and table columns for Filament’s CRUD interface:

namespace App\Filament\Resources;

use App\Filament\Resources\SitemapResource\Pages;
use App\Models\Sitemap;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

class SitemapResource extends Resource
{
    protected static ?string $model = Sitemap::class;

    protected static ?string $navigationIcon = 'heroicon-o-map';

    public static function form(Form $form): Form
    {
        return $form->schema([
            Forms\Components\TextInput::make('title')->required(),
            Forms\Components\TextInput::make('url')->label('URL')->url()->required(),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                Tables\Columns\TextColumn::make('id')->label('ID')->sortable(),
                Tables\Columns\TextColumn::make('title')->label('Title')->searchable(),
                Tables\Columns\TextColumn::make('url')->label('URL'),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
            ]);
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListSitemaps::route('/'),
            'create' => Pages\CreateSitemap::route('/create'),
            'edit' => Pages\EditSitemap::route('/{record}/edit'),
        ];
    }
}

This code defines our sitemap model and specifies the columns for Filament’s CRUD interface. Now, navigate to http://your-app-url/admin in your browser and sign in with your admin credentials. Click on “Sitemaps” in the sidebar navigation to access the new interface.

With this setup, we can now manage our sitemap data using Filament’s intuitive interface. In the next section, we’ll define the relationships between our sitemap URLs.

Defining Sitemap URLs with Eloquent Relationships

Now that our sitemap model is set up, we need to define how it relates to other models in our application. This will allow us to generate URLs for each item in the sitemap.

First, let’s create a migration to add the relationship columns:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddRelationshipColumnsToSitemapsTable extends Migration
{
    public function up()
    {
        Schema::table('sitemaps', function (Blueprint $table) {
            $table->unsignedBigInteger('page_id')->nullable();
            $table->foreign('page_id')->references('id')->on('pages');
            $table->unsignedBigInteger('category_id')->nullable();
            $table->foreign('category_id')->references('id')->on('categories');
        });
    }

    public function down()
    {
        Schema::table('sitemaps', function (Blueprint $table) {
            $table->dropForeign(['page_id']);
            $table->dropColumn('page_id');
            $table->dropForeign(['category_id']);
            $table->dropColumn('category_id');
        });
    }
}

This migration adds page_id and category_id columns to the sitemaps table, which will reference the id of a Page or Category model.

Next, we’ll define these relationships in our Sitemap model:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Category;
use App\Models\Page;

class Sitemap extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'url',
        'page_id',
        'category_id',
    ];

    public function page()
    {
        return $this->belongsTo(Page::class);
    }

    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

With these relationships defined, we can now fetch the relevant URLs for each item in our sitemap. In the next section, we’ll use this data to generate an XML file that Google Search Console can parse.

Implementing XML Generation Using a Service Class

Now that we have defined our Sitemap model and related it to other models in our application, let’s create a service class responsible for generating the XML sitemap.

Create a new file app/Services/SitemapGenerator.php:

namespace App\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class SitemapGenerator
{
    public function generateSitemap(): string
    {
        $sitemaps = DB::table('sitemaps')
            ->select(
                'id',
                'title',
                'url'
            )
            ->get();

        $xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');

        foreach ($sitemaps as $sitemap) {
            $urlSet = $xml->addChild('url');
            $urlSet->addChild('loc', htmlspecialchars($sitemap->url));
            $lastmod = date('Y-m-d');
            $changefreq = 'daily';
            $priority = '0.5';

            $urlSet->addChild('lastmod', $lastmod);
            $urlSet->addChild('changefreq', $changefreq);
            $urlSet->addChild('priority', $priority);
        }

        if (!File::exists(public_path('sitemaps'))) {
            File::makeDirectory(public_path('sitemaps'));
        }

        $filename = 'sitemap-' . date('Y-m-d') . '.xml';
        $filePath = public_path('sitemaps/' . $filename);
        $xml->asXML($filePath);

        return $filePath;
    }
}

This SitemapGenerator class will generate the XML sitemap based on our Sitemap model. We’ll use this service class to create the sitemap dynamically in the next section.

The generated sitemap is saved in the public/sitemaps directory, and we’re returning the file path for further use.

Configuring Route and Cache Settings for the Sitemap

To make our sitemap accessible via a URL, we need to register a new route in Laravel’s routes/web.php file.

use App\Models\Sitemap;

Route::get('/sitemap.xml', function () {
    return response()
        ->view('sitemaps.sitemap', ['sitemaps' => Sitemap::all()])
        ->header('Content-Type', 'application/xml');
})->name('sitemap');

This route will display our sitemap XML data using a Blade view named sitemap. Create it as resources/views/sitemaps/sitemap.blade.php. The XML declaration is echoed as a string so that PHP doesn’t try to parse <? as an opening tag:

{!! '<?xml version="1.0" encoding="UTF-8"?>' !!}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@foreach ($sitemaps as $sitemap)
    <url>
        <loc>{{ $sitemap->url }}</loc>
        <lastmod>{{ $sitemap->updated_at->toAtomString() }}</lastmod>
    </url>
@endforeach
</urlset>

Blade’s {{ }} escapes each URL, so characters such as & in query strings produce valid XML.

Next, we need to configure caching for our sitemap. We’ll use Laravel’s built-in cache system to store and retrieve the sitemap XML data efficiently.

In your config/cache.php file, add a new store to the stores array:

'stores' => [
    // ...
    'sitemaps' => [
        'driver' => 'file',
        'path'   => storage_path('app/sitemaps'),
    ],
],

This will store our sitemap XML data in a storage/app/sitemaps directory. You can adjust the path and driver settings according to your project’s requirements.

Then update the route so the rendered XML is read through this store and only rebuilt once a day:

use App\Models\Sitemap;
use Illuminate\Support\Facades\Cache;

Route::get('/sitemap.xml', function () {
    $xml = Cache::store('sitemaps')->remember('sitemap.xml', now()->addDay(), function () {
        return view('sitemaps.sitemap', ['sitemaps' => Sitemap::all()])->render();
    });

    return response($xml, 200, ['Content-Type' => 'application/xml']);
})->name('sitemap');

With these changes, you’ll be able to access your custom sitemap at http://your-app.com/sitemap.xml.

Adding Customizable Fields to the Sitemap Model with Filament

To make our sitemap model more flexible and adaptable to various needs, we’ll introduce customizable fields using Filament’s field management system.

Firstly, add the new columns to the sitemaps table with a migration (php artisan make:migration add_custom_fields_to_sitemaps_table):

Schema::table('sitemaps', function (Blueprint $table) {
    $table->string('name')->nullable();
    $table->boolean('enabled')->default(true);
    $table->string('priority')->default('0.5');
});

Add name, enabled and priority to the $fillable array in app/Models/Sitemap.php, then open app/Filament/Resources/SitemapResource.php and extend its form() method:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form->schema([
        Forms\Components\TextInput::make('title')->required(),
        Forms\Components\TextInput::make('url')->label('URL')->url()->required(),

        // Example field: 'name' is the attribute name and 'label' is used for display.
        Forms\Components\TextInput::make('name')
            ->label('Sitemap Name'),

        // Example field: Using a toggle to store boolean value.
        Forms\Components\Toggle::make('enabled')
            ->label('Enabled'),

        // Example field: Using a select dropdown.
        Forms\Components\Select::make('priority')
            ->options([
                '1.0' => 'High Priority',
                '0.5' => 'Normal Priority',
                '0.1' => 'Low Priority',
            ])
            ->default('0.5'),
    ]);
}

This form() method defines the fields that will be displayed in the Filament interface for managing sitemaps. You can customize and extend these field definitions to suit your specific needs.

To view the customizable fields, navigate to your project’s URL with the /admin route (e.g., http://localhost:8000/admin). There, you should see a list of sitemap models, each with its editable attributes. Make sure to click on the ‘Edit’ button next to each model to explore and manage their properties.

This concludes our customization of the sitemap model fields using Filament’s field management system. In the next section, we’ll build upon this by dynamically generating the sitemap XML file based on these customizable fields.

Generating the Sitemap XML File Dynamically

Dynamically Generating the Sitemap XML File

Now that our sitemap model and service class are in place, we can create a command to generate the sitemap XML file dynamically. This approach allows us to easily regenerate the sitemap whenever necessary without manually editing files.

Create a new command in your app/Console/Commands directory: SitemapGenerateCommand.php. Add the following code:

namespace App\Console\Commands;

use App\Services\SitemapGenerator;
use Illuminate\Console\Command;

class SitemapGenerateCommand extends Command
{
    protected $signature = 'sitemap:generate';
    protected $description = 'Generate sitemap XML file';

    public function handle(SitemapGenerator $generator): int
    {
        // Build the XML and write it to a file
        $filePath = $generator->generateSitemap();

        $this->info("Sitemap generated successfully: $filePath");

        return self::SUCCESS;
    }
}

In this code, Laravel injects our SitemapGenerator service class into the command’s handle() method, and its generateSitemap() method builds the XML content from the sitemap records. It then writes this content to a file in our public directory.

To run the command and generate the sitemap, use the following Artisan command:

php artisan sitemap:generate

This will create or update your sitemap XML file based on your configured routes and relationships.

Testing Your Custom XML Sitemap in Google Search Console

Firstly, ensure your sitemap URL is properly set up and configured for Google’s crawling. You can do this by submitting the URL of your generated sitemap (e.g., https://example.com/sitemap.xml) to Google Search Console under “Sitemaps”. If you’ve correctly followed the previous steps, the XML file should be generated dynamically based on your models.

To test your custom sitemap in action, follow these steps:

  1. In your terminal, run composer require google/apiclient for API support.
  2. Create a new file called SitemapController.php within your project’s main namespace (e.g., App\Http\Controllers). This will hold the logic to interact with Google’s Search Console API.
// app/Http/Controllers/SitemapController.php

namespace App\Http\Controllers;

use Exception;
use Google\Client;
use Google\Service\SearchConsole;
use Illuminate\Support\Facades\Http;

class SitemapController extends Controller
{
    public function index()
    {
        $siteUrl = 'https://www.example.com/';
        $sitemapUrl = 'https://www.example.com/sitemap.xml';

        $sitemap = Http::get($sitemapUrl);

        if ($sitemap->status() !== 200) {
            return response()->json(['error' => 'Sitemap is not reachable'], 502);
        }

        try {
            // Authenticate with a Google Cloud service account key file
            $client = new Client();
            $client->setAuthConfig(storage_path('app/google-service-account.json'));
            $client->addScope(SearchConsole::WEBMASTERS);

            $searchConsole = new SearchConsole($client);
            $searchConsole->sitemaps->submit($siteUrl, $sitemapUrl);

            return response()->json(['submitted' => $sitemapUrl]);
        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 500);
        }
    }
}

Remember to enable the Google Search Console API in your Google Cloud project, download a service account key to storage/app/google-service-account.json, add the service account’s email address as a user with Full permission on your Search Console property, and update the site URL to match your own property.

This concludes our tutorial on creating a custom XML sitemap for your Laravel app using Filament. With these steps, you should now have a functional sitemap that dynamically updates with changes in your models.

Frequently Asked Questions

How do I install Filament in my Laravel project?

To install Filament, run the command composer require filament/filament:"^3.3" -W in your terminal, then set up the admin panel using php artisan filament:install --panels. Finally, create an admin user with php artisan make:filament-user; the panel’s middleware is configured in app/Providers/Filament/AdminPanelProvider.php.

What happens if I don’t configure the sitemap model correctly?

If you don’t configure the sitemap model correctly, it may lead to incorrect or missing sitemap data. Make sure to adjust the fields and relationships as needed in your Sitemap model.

Is there an alternative approach to creating a custom XML sitemap?

Yes, you can use the spatie/laravel-sitemap package, which can crawl your site or build a sitemap from your models (for example, Sitemap::create()->add(Url::create('/blog'))->writeToFile(public_path('sitemap.xml'))), and then serve the generated file or add the necessary routes in your web.php file. However, using Filament provides more flexibility and customization options.

How do I generate an up-to-date XML sitemap automatically?

To generate an up-to-date XML sitemap automatically, create a service class that uses the Sitemap model to retrieve data and then use PHP’s built-in SimpleXMLElement class to generate the XML file, as the SitemapGenerator service and the sitemap:generate command in this tutorial do.

Can I customize the fields in my sitemap model?

Yes, you can customize the fields in your sitemap model by adjusting the $fillable array in your Sitemap model and adding new fields to the form() method of your Filament SitemapResource.

Comments

comments