< Back to Blog

Lessons Learnt from My Latest SEO Audit

September 12th, 2025

Statamic
SEO

From the very start of every project, I build with SEO in mind. I make sure performance, content structure, and metadata are considered throughout the development process, and beyond so that my clients have the control they need to optimise current and future content. Over the years, I’ve contributed to a few open source tools that make optimising images, meta tags, and sitemaps easier for clients and their SEO teams.

Even with all this in place, every audit teaches me something new - which is a good thing! Each lesson makes my next project faster, cleaner, and more SEO-friendly.

Images First

Page speed is one of the most obvious (and important) ranking factors. One of the biggest improvements you can make is by serving images in optimised sizes and next-gen formats.

To make this easier, I built an Open Source Statamic Starter Kit that includes a reusable image partial. This partial serves a webp version of an image (with a fallback if the browser doesn’t support it) and automatically adjusts the size based on screen width.

Here’s an example of how it looks:

<picture> 
  
  <source 
		  type="image/webp" 
		  srcset="
    	  {{ glide :src='url' format='webp' :width='mobile_width' }} 
    	  {{ mobile_width }}w, 
    	  {{ glide :src='url' format='webp' :width='desktop_width' }} 
    	  {{ desktop_width }}w"     
		  sizes="(min-width: 1024px) {{ desktop_size }}, 100vw" > 
  
  <img 
	   src="{{ glide :src='url' :width='desktop_width' }}" 
	   alt="{{ alt }}" 
	   class="w-full h-full object-cover" 
	   loading="{{ loading }}"> 
  
</picture>

How this works is:

srcset = tells the browser the available image widths. In the above example there are two options available: a smaller image for mobile and a larger image for desktop.

sizes = tells the browser what size image will appear at different viewport widths. In this case, if the viewport is at least 1024px wide, the image will display at 1000px (e.g. 800px, 70vw, etc.).

The browser then picks the most appropriate file from the srcset (no JavaScript required).

loading can also "lazy" or "eager" load elements depending on their position within the page (eager for when it's on screen for page load, or lazy if it isn't).

With this I can drop the partial anywhere on the site and know every image will be served in the best format for speed and SEO.

Note that Glide does not work with SVG's.

Giving SEO Teams Full Control

To make sure clients and their SEO agencies can do their best work, I always provide full H1 control in the CMS and add open source Statamic addons like Alt SEO (for meta and social tags) and Alt Sitemap (for custom sitemaps with priorities and exclusions)

I also work with SEO agencies during the build to add any necessary fields for structured data. For example:

  • Using Globals store contact details once and reuse them across the site e.g. footer, contact page, the Organisation schema.

  • Adding fields to Collection blueprints (like blogs or products) to output Article, FAQ, or Product schemas using CMS data.

This means teams can write user-friendly descriptions for the front end while keeping a separate, bot-focused meta description for search engines.

We can loop through the data to output the schema:

<script type="application/ld+json">
    {
        "@context": "https://schema.org",
        "@type": "FAQPage",
        "mainEntity": [
            {{ collection:faqs }}
            {
                "@type": "Question",
                "name": "{{ question }}",
                "acceptedAnswer": {
                    "@type": "Answer",
                    "text": "{{ answer }}"
                }
            }{{ unless last }},{{ /unless }}
            {{ /collection:faqs }}
        ]
    }
</script>

What the Audit Found (and How I Fixed It)

Even with all the preparations, the audit still uncovered a few things to tweak:

1. CSS and JS Not Caching

The fix was to update the Forge Nginx config to enable long-term caching:

location ~* \.(?:css|js)$ {
  expires 1y; 
  add_header Cache-Control "public, immutable"; 
}

After updating, I restarted Nginx and verified the assets were caching in Network > JS > Headers by checking for the Cache-Control header.

Livewire Gotcha

This change caused a 404 for the Livewire JS. Normally, Livewire serves JS dynamically (no physical file), but as Nginx now intercepted the request before Laravel could handle it, it resulted in the 404, even though visiting the (uncached) URL did return the JS. The solution was to publish the Livewire assets to the public folder:

php artisan vendor:publish --tag=livewire:assets --force

2. Sitemap Clean-Up

The audit also flagged some sitemap issues:

  • Unwanted links were appearing - easily excluded using the Alt Sitemap addon.

  • Malformed links (a leftover from development) were leading to 404s - these were tracked down and updated.

3. Content Structure

Finally, a few classic SEO must-haves were missing:

  • No H1 on a one page - fixed by adding a required H1 field in the CMS.

  • Missing Meta Descriptions - added via Alt SEO within the CMS.

  • Duplicate Titles on Paginated Pages - solved by appending the page number to the title on those pages.


Every site teaches me something new, and I actually love that. SEO is one of those things that’s never really “finished,” - there’s always some tweak or trick to be found. The cool part is that each lesson sticks, which means the next site I build is a bit faster, a bit cleaner, and a bit more search-engine friendly than the last.