SEO, Meta Tags & Sitemap

How goilerplate handles SEO, meta tags, social media previews, and dynamic sitemap generation

goilerplate includes a comprehensive SEO system built directly into the base template. It’s simple, effective, and easy to customize.

How It Works

The SEO system is implemented in internal/ui/layouts/base.templ using a clean, component-based approach:

// Pass SEO props to any page
templ Base(props ...SEOProps) {
    // SEO meta tags are automatically included
    if len(props) > 0 {
        @seo(props[0])
    }
}

SEO Props Structure

Each page can define its SEO metadata:

type SEOProps struct {
    Title       string  // Page title (appended with app name)
    Description string  // Meta description for search engines
    Path        string  // Current page path for canonical URL
}

Usage Example

In Your Page Template

// internal/ui/pages/blog.templ
templ BlogPost(post *model.BlogPost) {
    @layouts.Base(layouts.SEOProps{
        Title:       post.Title,
        Description: post.Description,
        Path:        "/blog/" + post.Slug,
    }) {
        <article>
            <h1>{ post.Title }</h1>
            // ... rest of your content
        </article>
    }
}

Dynamic Title Updates

For HTMX requests, titles are updated dynamically using out-of-band swaps:

@templ.Fragment("seo-title") {
    <title id="page-title" hx-swap-oob="true">{ fullTitle }</title>
}

Meta Tags Included

The SEO component automatically generates:

Basic Meta Tags

  • <title> - Page title with app name
  • <meta name="description"> - Search engine description
  • <meta name="author"> - Application author
  • <meta name="robots"> - Search engine instructions
  • <link rel="canonical"> - Canonical URL to prevent duplicate content

OpenGraph Tags (Facebook, LinkedIn)

  • og:title - Social media title
  • og:description - Social media description
  • og:type - Content type (website)
  • og:url - Page URL
  • og:site_name - Application name
  • og:image - Preview image (1200x630)
  • og:image:alt - Image alt text

Twitter Card Tags

  • twitter:card - Card type (summary_large_image)
  • twitter:title - Tweet preview title
  • twitter:description - Tweet preview description
  • twitter:image - Preview image
  • twitter:image:alt - Image alt text

Mobile Optimization

  • <meta name="theme-color"> - Browser theme color
  • Viewport meta tag for responsive design

Configuration

SEO configuration is automatically loaded from your environment variables via the config:

templ seo(props SEOProps) {
    {{ cfg := ctxkeys.Config(ctx) }}
    {{ baseURL := cfg.AppURL }}      // From APP_URL env var
    {{ appName := cfg.AppName }}     // From APP_NAME env var
    {{ appTagline := cfg.AppTagline }} // From APP_TAGLINE env var
}

Set these in your .env file:

APP_URL=https://yourdomain.com
APP_NAME=Your App Name
APP_TAGLINE=Your app tagline

Social Preview Image

Place your social media preview image at:

/assets/img/social-preview.png

Recommended dimensions:

  • Size: 1200x630 pixels
  • Format: PNG or JPG
  • File size: Under 1MB

Best Practices

  • Unique titles and descriptions for every page
  • Keep descriptions 150-160 characters for optimal search display
  • Always provide canonical URLs via the Path field
  • Use descriptive titles instead of generic ones

Testing Your SEO

Validation tools:


Sitemap & Robots.txt

goilerplate includes a smart, dynamic sitemap generator and robots.txt configuration that automatically discovers your content.

Robots.txt

Located at /static/robots.txt:

User-agent: *
Allow: /
Sitemap: /sitemap.xml

Customizing Robots.txt:

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /app/
Crawl-delay: 1

Sitemap: /sitemap.xml

Dynamic Sitemap

How URLs Are Discovered

The sitemap automatically includes:

  1. Static Routes - Defined in publicRoutes slice
  2. Blog Posts - All markdown files in /content/blog/
  3. Blog Tags - Unique tags from all blog posts
  4. Documentation - All markdown files in /content/docs/

Adding Static Routes

Edit the publicRoutes slice in /internal/service/sitemap.go:

var publicRoutes = []struct {
    Path       string
    Priority   string
    ChangeFreq string
}{
    {"/", "1.0", "daily"},
    {"/blog", "0.8", "daily"},
    {"/docs", "0.8", "weekly"},
    {"/login", "0.3", "monthly"},
    {"/register", "0.3", "monthly"},
    // Add new routes here:
    // {"/about", "0.7", "monthly"},
    // {"/contact", "0.5", "monthly"},
}

Priority Guidelines

Priority Use Case
1.0 Homepage only
0.8-0.9 Primary content pages (blog index, docs)
0.7-0.8 Individual blog posts, main docs
0.5-0.6 Category/tag pages, nested docs
0.3-0.4 Utility pages (login, register)
0.1-0.2 Legal pages, rarely updated content

Change Frequency Values

Valid changefreq values:

  • always - Changes every time accessed
  • hourly - Updated hourly
  • daily - Updated daily
  • weekly - Updated weekly
  • monthly - Updated monthly
  • yearly - Updated yearly
  • never - Archived content

Protected Routes

Routes requiring authentication are automatically excluded from the sitemap.

// NOT in sitemap (requires auth)
mux.HandleFunc("GET /dashboard", middleware.RequireAuth(base.DashboardPage))

// IN sitemap (public)
mux.HandleFunc("GET /blog", blog.ListPosts)

Accessing Endpoints

  • Robots.txt: http://localhost:8090/robots.txt
  • Sitemap: http://localhost:8090/sitemap.xml

Submitting to Search Engines

Performance

For typical sites (< 1,000 URLs):

  • Generation time: ~5-10ms
  • Memory usage: Minimal
  • No caching needed

Consider caching if you have:

  • More than 1,000 URLs
  • High crawler traffic (> 100 requests/hour)

Sitemap Limits

According to the sitemap protocol:

  • Maximum 50,000 URLs per sitemap
  • Maximum 50MB uncompressed size
  • Use sitemap index for larger sites

Why Dynamic Generation?

  • ✅ Always current (no stale data)
  • ✅ Less maintenance (no rebuild needed)
  • ✅ Single source of truth (content drives sitemap)
  • ✅ Good performance (fast enough for most sites)
  • ✅ Simpler deployment (no build step)

Common Issues

Sitemap Not Updating:
The sitemap is generated dynamically, so it’s always current. If you don’t see new content:

  1. Check that the content file exists
  2. Verify it has valid frontmatter
  3. Ensure the blog/docs service can read it

URLs Missing from Sitemap:
Check if the route is:

  1. Listed in publicRoutes (for static pages)
  2. Not behind authentication middleware
  3. Has valid content (for blog/docs)