SEO
Stacks.js provides tools and patterns for optimizing your application for search engines.
Meta Tags
Basic Meta Tags
<!-- resources/views/layouts/app.stx -->
<head
<metacharset="
<metaname="content="
<title{{ $title ?? config('app.name') }}</title
<metaname="content="description??'app.description'
<!-- Canonical URL -->
<linkrel="href="->
<!-- Robots -->
<metaname="content="robots??'index, follow'
</head
SEO Component
<!-- components/Seo.stx -->
<template>
<title{{ title }} | {{ siteName }}</title
<metaname=":content="description
<linkrel=":href="canonical
<!-- Open Graph -->
<metaproperty=":content="title
<metaproperty=":content="description
<metaproperty=":content="canonical
<metaproperty=":content="type
<metaproperty=":content="image
<metaproperty=":content="siteName
<!-- Twitter -->
<metaname=":content="twitterCard
<metaname=":content="title
<metaname=":content="description
<metaname=":content="image
@if(twitterSite
<metaname=":content="twitterSite
</template
<script>
export default {
props: {
title: { type: String, required: true },
description: { type: String, required: true },
canonical: { type: String, default: null },
type: { type: String, default: 'website' },
image: { type: String, default: null },
twitterCard: { type: String, default: 'summary_large_image' },
twitterSite: { type: String, default: null },
},
computed: {
siteName() {
return config('app.name')
}
}
}
</script
Page Usage
<!-- resources/views/pages/blog/[slug].stx -->
<x-layout
<x-seo
:title="post.title"
:description="post.excerpt"
:canonical="route('blog.show', post.slug)"
type="article"
:image="post.featured_image"
/>
<article
<h1{{ post.title }}</h1
{{ post.content }}
</article
</x-layout
Structured Data
JSON-LD Schema
<!-- Organization -->
<script>
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "{{ config('app.name') }}",
"url": "{{ config('app.url') }}",
"logo": "{{ asset('images/logo.png') }}",
"sameAs": [
"https://twitter.com/yourcompany",
"https://github.com/yourcompany"
]
}
</script
<!-- Article -->
<script>
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "",
"description": "",
"image": "",
"author": {
"@type": "Person",
"name": ""
},
"publisher": {
"@type": "Organization",
"name": "{{ config('app.name') }}",
"logo": {
"@type": "ImageObject",
"url": "{{ asset('images/logo.png') }}"
}
},
"datePublished": "{{ post.published_at.toISOString() }}",
"dateModified": "{{ post.updated_at.toISOString() }}"
}
</script
Schema Component
<!-- components/Schema.stx -->
<template>
<script>jsonLd
</template
<script>
export default {
props: {
type: { type: String, required: true },
data: { type: Object, required: true },
},
computed: {
jsonLd() {
return JSON.stringify({
'@context': 'https://schema.org',
'@type': this.type,
...this.data,
})
}
}
}
</script
<!-- Usage -->
<x-schema
type="Product"
:data="{
name: product.name,
description: product.description,
image: product.images,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: 'https://schema.org/InStock'
}
}"
/>
Breadcrumb Schema
<x-schema
type="BreadcrumbList"
:data="{
itemListElement: breadcrumbs.map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: crumb.url
}))
}"
/>
Sitemaps
Automatic Sitemap Generation
// config/seo.ts
export default {
sitemap: {
enabled: true,
path: '/sitemap.xml',
// Static routes
routes: [
{ url: '/', changefreq: 'daily', priority: 1.0 },
{ url: '/about', changefreq: 'monthly', priority: 0.8 },
{ url: '/contact', changefreq: 'monthly', priority: 0.8 },
],
// Dynamic routes
dynamic: [
{
model: 'Post',
url: (post) => `/blog/${post.slug}`,
lastmod: (post) => post.updated_at,
changefreq: 'weekly',
priority: 0.6,
},
{
model: 'Product',
url: (product) => `/products/${product.slug}`,
lastmod: (product) => product.updated_at,
changefreq: 'daily',
priority: 0.7,
},
],
}
}
Generate Sitemap
# Generate sitemap
buddy seo:sitemap
# Auto-generate on deploy
# (configured in deployment pipeline)
Sitemap Index (Large Sites)
// config/seo.ts
export default {
sitemap: {
index: true, // Generate sitemap index for large sites
maxUrls: 50000, // Max URLs per sitemap file
}
}
Robots.txt
Configuration
// config/seo.ts
export default {
robots: {
enabled: true,
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin', '/api', '/private'],
},
{
userAgent: 'Googlebot',
allow: '/',
crawlDelay: 1,
},
],
sitemap: 'https://yoursite.com/sitemap.xml',
}
}
Dynamic Robots.txt
// routes/web.ts
router.get('/robots.txt', (request) => {
const rules = [
'User-agent: *',
'Allow: /',
'Disallow: /admin',
'Disallow: /api',
'',
`Sitemap: ${config('app.url')}/sitemap.xml`,
]
return new Response(rules.join('\n'), {
headers: { 'Content-Type': 'text/plain' },
})
})
URL Structure
Clean URLs
// routes/web.ts
// GOOD: Clean, descriptive URLs
router.get('/products/:category/:slug', ProductController.show)
// /products/electronics/wireless-headphones
// BAD: Parameter-based URLs
router.get('/product', ProductController.show)
// /product?id=123&cat=5
Slug Generation
// app/Models/Post.ts
import { slugify } from '@stacksjs/strings'
export default class Post extends Model {
static boot() {
this.creating((post) => {
post.slug = slugify(post.title)
})
}
}
Canonical URLs
<!-- Prevent duplicate content -->
<linkrel="href="->
<!-- For paginated content -->
@if(page>1
<linkrel="href="'blog.index'
Performance for SEO
Core Web Vitals
// config/seo.ts
export default {
performance: {
// Optimize for Core Web Vitals
preconnect: [
'https://fonts.googleapis.com',
'https://cdn.example.com',
],
prefetch: [
'/api/critical-data',
],
}
}
<head
<!-- Preconnect to critical origins -->
<linkrel="href="
<linkrel="href="crossorigin
<!-- Preload critical assets -->
<linkrel="href="as="type="crossorigin
<!-- DNS prefetch for third parties -->
<linkrel="href="
</head
Image Optimization
<!-- Responsive images with proper attributes -->
<img
src="{{ $image->url('medium') }}"
srcset="
{{ $image->url('small') }} 400w,
{{ $image->url('medium') }} 800w,
{{ $image->url('large') }} 1200w
"
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
alt="{{ $image->alt }}"
loading="lazy"
decoding="async"
width="800"
height="600"
/>
Server-Side Rendering
Stacks renders pages server-side by default, ensuring search engines see full content:
// All STX pages are SSR by default
// No additional configuration needed
// For dynamic content, ensure it's rendered server-side
export async function getServerSideProps() {
const posts = await Post.published().get()
return { posts }
}
Social Sharing
Open Graph
<!-- Open Graph meta tags -->
<metaproperty="content="title
<metaproperty="content="description
<metaproperty="content="image
<metaproperty="content="
<metaproperty="content="
<metaproperty="content="->
<metaproperty="content="
<metaproperty="content="
Twitter Cards
<!-- Twitter Card meta tags -->
<metaname="content="
<metaname="content="
<metaname="content="title
<metaname="content="description
<metaname="content="image
OG Image Generation
// routes/api.ts
router.get('/og-image/:slug', async (request) => {
const { slug } = request.params
const post = await Post.where('slug', slug).first()
const image = await generateOgImage({
title: post.title,
author: post.author.name,
date: post.published_at,
})
return new Response(image, {
headers: { 'Content-Type': 'image/png' },
})
})
Internationalization SEO
Hreflang Tags
<linkrel="hreflang="href="'home''en'
<linkrel="hreflang="href="'home''es'
<linkrel="hreflang="href="'home''fr'
<linkrel="hreflang="href="'home'
Language-Specific Sitemaps
// config/seo.ts
export default {
sitemap: {
locales: ['en', 'es', 'fr'],
generatePerLocale: true,
}
}
Analytics Integration
Google Analytics 4
<!-- Global site tag (gtag.js) -->
<script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXX');
</script
Google Search Console
# Verify site ownership
# Add verification file or meta tag
# Submit sitemap
buddy seo:submit-sitemap
SEO Checklist
Technical SEO
- Clean, descriptive URLs
- Proper heading hierarchy (H1-H6)
- XML sitemap generated and submitted
- robots.txt configured
- Canonical URLs set
- HTTPS enabled
- Mobile-responsive design
On-Page SEO
- Unique title tags (50-60 characters)
- Meta descriptions (150-160 characters)
- Proper image alt text
- Internal linking strategy
- External links to authoritative sources
Performance
- Core Web Vitals passing
- Images optimized
- CSS/JS minified
- Caching configured
- CDN for static assets
Content
- Structured data implemented
- Fresh, quality content
- Proper keyword usage
- Mobile-friendly formatting