Coding

Adding a Sitemap to Google with Laravel on the DigitalOcean App Platform

Adding a Sitemap to Google with Laravel on the DigitalOcean App Platform

Posted on Fri, Jan 10, 2025

The issue I faced on a recent project was that a sitemap added to Google Webmaster Tools needs to be stored on the same domain. However, in my setup, the sitemap was being generated and automatically saved to DigitalOcean Spaces, then served via a CDN URL. On hosting platforms like DigitalOcean App Platform or Heroku, you can’t store files directly on the instance or node.

Most of the posts I found suggested adding a new property with the CDN URL and linking the sitemap there. Unfortunately, this didn’t work for me. After some experimentation, I came up with a workaround to solve this issue.

Here’s How I Solved It for This Project

In my Laravel project, I set up a route to generate the sitemap and return it via a view.

routes/web.php

Route::get('sitemap.xml', function () {
    $sitemap = collect(SitemapGenerator::create('https://www.cloud-wales.co.uk')->getSitemap())->flatten();
		
    $posts = Post::where('status', Status::PUBLISHED)
	    ->where('published_at', '<=', Carbon::now())
	    ->pluck('slug', 'published_at');

    $xml = view(
        'sitemap',
        [
            'urls' => $sitemap,
            'posts' => collect($posts)->flatten(),
        ]
    )->render();

    return response($xml)->withHeaders([
        'content-type' => 'text/xml'
    ]);
});

Explanation of the Code

1. Generate the Sitemap

First, I generated the sitemap using the Spatie Laravel Sitemap package. I collected and flattened the returned data for easier processing.

$sitemap = collect(SitemapGenerator::create('{SITE_URL}')
    ->getSitemap())->flatten();

2. Create a View

Next, I passed the required data to a view.

$xml = view(
    'sitemap',
    [
        'urls' => $sitemap,
        'posts' => collect($posts)->flatten(),
    ]
)->render();

3. Return an XML Response

Finally, I set up a response to serve the sitemap as an XML file.

return response($xml)->withHeaders([
    'content-type' => 'text/xml'
]);

The View

Currently, this is a quick and somewhat messy implementation, but I plan to refactor it for better readability and maintainability. For now, here’s the view:

resources/views/sitemap.blade.php

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
    xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
    xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
    @foreach ($urls as $url)
        <url>
            <loc>{{ $url->url }}</loc>
            @if (Str::contains($url->url, 'blog'))
                <news:news>
                    <news:publication>
                        <news:name>{{ env('APP_NAME') }}</news:name>
                        <news:language>en</news:language>
                    </news:publication>
                    <news:title>{{ Str::headline(Str::of($url->url)->afterLast('/')) }}</news:title>
                    <news:publication_date>
                        {{ Carbon\Carbon::parse(App\Models\Post::where('slug', Str::of($url->url)->afterLast('/'))
																								->value('published_at'))->toDateString() }}
                    </news:publication_date>
                </news:news>
            @else
                <changefreq>{{ $url->changeFrequency }}</changefreq>
                <priority>{{ $url->priority }}</priority>
            @endif
        </url>
    @endforeach
</urlset>

Summary

In this implementation, the sitemap is generated and displayed using a custom view. For the <news> XML section, there’s definitely room for improvement. I plan to refactor it to make it cleaner and more modular.

While it’s a rough solution, it works effectively for now!