SEO & AI Engine Optimization Framework · May 2026

Headless CMS SEO: Contentful, Sanity, Strapi, Storyblok

A comprehensive reference for SEO implementation with headless CMS architectures. Headless CMS decouples content management from presentation, enabling content reuse across web, mobile, voice, and…

Architecture Patterns, Content Modeling, Schema in Decoupled Stacks, Build vs Runtime Decisions, and SEO for the Headless Era

A comprehensive reference for SEO implementation with headless CMS architectures. Headless CMS decouples content management from presentation, enabling content reuse across web, mobile, voice, and emerging interfaces. The architecture brings flexibility but requires deliberate SEO implementation patterns.


1. Document Purpose

Headless architectures have grown substantially in 2026. Modern marketing sites, e-commerce stores, and content platforms increasingly use headless patterns: content stored in a CMS like Contentful, Sanity, Strapi, or Storyblok; rendered by a frontend framework like Next.js, Astro, or Nuxt; deployed to a CDN.

The benefits are real:

The SEO challenges are also real:

This framework specifies SEO patterns for headless architectures.

1.1 Required Tools


2. Headless Architecture Patterns

2.1 Architecture Components

headless_architecture:
  
  content_layer:
    role: "Stores and manages content"
    options:
      - Contentful (commercial, mature)
      - Sanity (commercial, flexible)
      - Strapi (open source, self-hostable)
      - Storyblok (commercial, visual editing)
      - Hygraph (formerly GraphCMS)
      - Prismic (commercial)
      - Payload (open source, modern)
  
  api_layer:
    role: "Content delivery"
    typical_protocols:
      - REST API
      - GraphQL
    consideration: "GraphQL often preferred for selective data fetching"
  
  build_layer:
    role: "Generate static or hybrid output"
    options:
      - Next.js with ISR
      - Astro with content fetching
      - Nuxt with prerendering
      - Gatsby (declining popularity but still used)
  
  delivery_layer:
    role: "Serve to users"
    options:
      - Vercel
      - Netlify
      - Cloudflare Pages
      - Custom CDN
  
  search_engine:
    interaction: "Crawls the rendered output"
    requirement: "Rendered HTML must contain SEO-critical content"

2.2 Common Stack Combinations

common_stacks:
  
  contentful_nextjs:
    pattern: "Contentful + Next.js + Vercel"
    strengths: "Mature, well-supported, scalable"
    typical_use: "Mid-to-large marketing sites"
  
  sanity_nextjs:
    pattern: "Sanity + Next.js + Vercel"
    strengths: "Flexible content modeling, real-time preview"
    typical_use: "Editorial sites, e-commerce"
  
  strapi_nuxt:
    pattern: "Self-hosted Strapi + Nuxt"
    strengths: "Open source, full control, cost-effective"
    typical_use: "Cost-sensitive, technical teams"
  
  sanity_astro:
    pattern: "Sanity + Astro"
    strengths: "Performance + flexibility"
    typical_use: "Content-heavy sites prioritizing performance"
  
  storyblok_nuxt:
    pattern: "Storyblok + Nuxt"
    strengths: "Visual editing for non-technical users"
    typical_use: "Marketing teams without dev support"

3. Content Modeling for SEO

3.1 SEO Fields per Content Type

Every content type needs SEO fields:

seo_fields_per_content_type:
  
  required_fields:
    - meta_title (override)
    - meta_description
    - canonical_url (override; usually self)
    - og_image
    - og_title (override; default to page title)
    - og_description (override; default to meta description)
    - schema_type (Article, Product, Service, etc.)
    - noindex (boolean)
    - nofollow (boolean)
  
  conditional_fields:
    article:
      - author (reference)
      - publish_date
      - updated_date
      - main_image
      - excerpt
      - reading_time
    
    product:
      - sku
      - price
      - availability
      - images (multiple)
      - reviews (reference)
    
    location:
      - address
      - hours
      - phone
      - service_area

3.2 Content Modeling Principles

content_modeling_principles:
  
  content_types_per_purpose:
    rule: "Distinct content types for distinct purposes"
    examples: "Blog Post, Case Study, Service Page, Product"
  
  shared_components:
    pattern: "Reusable components across types"
    examples: "Hero, CTA, Testimonial, FAQ"
  
  references_for_relationships:
    pattern: "Use references vs duplication"
    examples: "Author referenced in Article; Category referenced in Product"
  
  seo_components:
    pattern: "SEO fields as embedded structure"
    benefit: "Consistent across content types"
  
  taxonomy_modeling:
    options:
      - Categories as references
      - Tags as multi-references
      - Custom taxonomies as separate types

3.3 Example Schema (Sanity)

// schemas/article.ts
export default {
  name: 'article',
  title: 'Article',
  type: 'document',
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: Rule => Rule.required().max(70)
    },
    {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title', maxLength: 96 }
    },
    {
      name: 'seo',
      title: 'SEO',
      type: 'object',
      fields: [
        { name: 'metaTitle', title: 'Meta Title', type: 'string', validation: Rule => Rule.max(60) },
        { name: 'metaDescription', title: 'Meta Description', type: 'text', validation: Rule => Rule.max(155) },
        { name: 'ogImage', title: 'OG Image', type: 'image' },
        { name: 'noindex', title: 'No Index', type: 'boolean' },
        { name: 'canonicalUrl', title: 'Canonical URL', type: 'url' }
      ]
    },
    {
      name: 'author',
      title: 'Author',
      type: 'reference',
      to: [{ type: 'author' }]
    },
    {
      name: 'publishedAt',
      title: 'Published At',
      type: 'datetime'
    },
    {
      name: 'updatedAt',
      title: 'Updated At',
      type: 'datetime'
    },
    {
      name: 'mainImage',
      title: 'Main Image',
      type: 'image',
      fields: [
        { name: 'alt', title: 'Alt Text', type: 'string' }
      ]
    },
    {
      name: 'excerpt',
      title: 'Excerpt',
      type: 'text'
    },
    {
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [{ type: 'block' }, { type: 'image' }]
    },
    {
      name: 'categories',
      title: 'Categories',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'category' }] }]
    }
  ]
};

4. Build vs Runtime Strategy

4.1 Decision Framework

build_vs_runtime:
  
  build_time_rendering:
    pattern: "Pages generated at build time"
    pros:
      - Fastest possible delivery
      - No server load per request
      - Fully cacheable
    cons:
      - Content updates require rebuild
      - Build times grow with content
      - Less suitable for personalized content
    use_for:
      - Marketing pages
      - Most content (with ISR)
      - Static sites
  
  isr_incremental_static_regeneration:
    pattern: "Built at first request, then cached, regenerated periodically"
    pros:
      - Combines static performance with freshness
      - Background regeneration
      - Webhook-triggered for immediate updates
    cons:
      - Slightly more complex than pure SSG
      - First request may be slower
    use_for:
      - Blogs and articles
      - Product catalogs
      - Most use cases
  
  ssr_server_side_rendering:
    pattern: "Rendered on each request"
    pros:
      - Always fresh
      - Personalization possible
    cons:
      - Server load
      - Slower than cached
      - Higher hosting cost
    use_for:
      - User-personalized pages
      - Real-time data
      - Search results
  
  csr_client_side_rendering:
    pattern: "Rendered in browser via JavaScript"
    seo_concern: "Often poor for crawlers"
    use_for:
      - Behind-auth pages only
      - Interactive components in otherwise rendered pages

4.2 Webhook-Triggered Rebuilds

For build-time stacks, content updates trigger rebuilds via webhooks:

webhook_workflow:
  
  contentful_to_vercel:
    setup: "Contentful webhook → Vercel deployment hook"
    trigger: "Content publish, update, or unpublish"
    result: "Rebuild deployed automatically"
  
  sanity_to_vercel:
    setup: "Sanity webhook → Vercel deployment"
    consideration: "Configure for relevant document types"
  
  considerations:
    - Build time impact (large sites slow to rebuild)
    - On-demand revalidation (Next.js) for selective updates
    - Preview workflow separate from production

4.3 On-Demand Revalidation (Next.js)

For Next.js with Contentful or similar:

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  // Verify webhook secret
  const secret = request.nextUrl.searchParams.get('secret');
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
  }
  
  const body = await request.json();
  
  // Determine path to revalidate based on webhook payload
  const path = `/blog/${body.slug}`;
  
  revalidatePath(path);
  
  return NextResponse.json({ revalidated: true, path });
}

CMS webhook calls this endpoint when content publishes; specific page rebuilds without full site rebuild.


5. Schema Implementation

5.1 Schema Generation Patterns

schema_generation_patterns:
  
  per_content_type_schema:
    pattern: "Function generating schema based on content type"
    example_typescript: |
      function generateArticleSchema(article) {
        return {
          "@context": "https://schema.org",
          "@type": "Article",
          "headline": article.title,
          "description": article.excerpt,
          "datePublished": article.publishedAt,
          "dateModified": article.updatedAt || article.publishedAt,
          "author": {
            "@type": "Person",
            "name": article.author.name
          },
          "image": article.mainImage,
          "mainEntityOfPage": {
            "@type": "WebPage",
            "@id": `https://example.com/blog/${article.slug}/`
          }
        };
      }
  
  schema_component:
    pattern: "Reusable component injecting schema"
    benefit: "Consistent implementation across pages"
  
  graph_pattern:
    pattern: "Single schema graph per page with multiple linked types"
    example:
      - WebPage
      - Article
      - Person (author)
      - Organization (publisher)
      - BreadcrumbList

5.2 Sample Schema Component (Next.js + Contentful)

// components/Schema.tsx
import { Article, Author } from '@/types/contentful';

interface ArticleSchemaProps {
  article: Article;
  author: Author;
}

export function ArticleSchema({ article, author }: ArticleSchemaProps) {
  const schema = {
    '@context': 'https://schema.org',
    '@graph': [
      {
        '@type': 'WebPage',
        '@id': `https://thatdeveloperguy.com/blog/${article.fields.slug}/`,
        url: `https://thatdeveloperguy.com/blog/${article.fields.slug}/`,
        name: article.fields.title,
      },
      {
        '@type': 'Article',
        '@id': `https://thatdeveloperguy.com/blog/${article.fields.slug}/#article`,
        headline: article.fields.title,
        description: article.fields.excerpt,
        datePublished: article.fields.publishedAt,
        dateModified: article.fields.updatedAt || article.fields.publishedAt,
        author: { '@id': `https://thatdeveloperguy.com/about/${author.fields.slug}/#person` },
        publisher: { '@id': 'https://thatdeveloperguy.com/#organization' },
        image: article.fields.mainImage?.fields.file.url,
        mainEntityOfPage: { '@id': `https://thatdeveloperguy.com/blog/${article.fields.slug}/` },
      },
      {
        '@type': 'Person',
        '@id': `https://thatdeveloperguy.com/about/${author.fields.slug}/#person`,
        name: author.fields.name,
        url: `https://thatdeveloperguy.com/about/${author.fields.slug}/`,
      },
      {
        '@type': 'Organization',
        '@id': 'https://thatdeveloperguy.com/#organization',
        name: 'ThatDeveloperGuy',
        url: 'https://thatdeveloperguy.com/',
      },
    ],
  };
  
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

6. Sitemap Generation

6.1 Programmatic Sitemap

For headless stacks, sitemap must be generated from CMS data:

// app/sitemap.ts (Next.js)
import { MetadataRoute } from 'next';
import { getAllArticles, getAllPages, getAllProducts } from '@/lib/cms';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const [articles, pages, products] = await Promise.all([
    getAllArticles(),
    getAllPages(),
    getAllProducts(),
  ]);
  
  const articleUrls = articles.map(article => ({
    url: `https://example.com/blog/${article.slug}/`,
    lastModified: new Date(article.updatedAt || article.publishedAt),
    changeFrequency: 'monthly' as const,
    priority: 0.7,
  }));
  
  const pageUrls = pages.map(page => ({
    url: `https://example.com/${page.slug}/`,
    lastModified: new Date(page.updatedAt),
    changeFrequency: 'monthly' as const,
    priority: 0.8,
  }));
  
  const productUrls = products.map(product => ({
    url: `https://example.com/products/${product.slug}/`,
    lastModified: new Date(product.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.9,
  }));
  
  return [...pageUrls, ...articleUrls, ...productUrls];
}

6.2 Multi-File Sitemaps

For very large sites, sitemap index pattern:

// app/sitemap.xml/route.ts
export async function GET() {
  const articleCount = await getArticleCount();
  const productCount = await getProductCount();
  
  const sitemaps = [];
  
  // 50,000 URL limit per sitemap
  for (let i = 0; i < Math.ceil(articleCount / 50000); i++) {
    sitemaps.push(`https://example.com/sitemaps/articles-${i}.xml`);
  }
  for (let i = 0; i < Math.ceil(productCount / 50000); i++) {
    sitemaps.push(`https://example.com/sitemaps/products-${i}.xml`);
  }
  
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemaps.map(loc => `<sitemap><loc>${loc}</loc></sitemap>`).join('\n')}
</sitemapindex>`;
  
  return new Response(xml, {
    headers: { 'Content-Type': 'application/xml' },
  });
}

7. Preview & Editorial Workflow

7.1 Preview Mode

Editors need to preview unpublished content:

preview_mode_implementation:
  
  nextjs_draft_mode:
    pattern: "Next.js Draft Mode for preview"
    setup: "Preview API route + cookie-based auth"
    benefit: "See unpublished content rendered as it would appear"
  
  cms_preview_url:
    setup: "Configure CMS to link to preview URL"
    workflow: "Editor clicks 'Preview' in CMS → opens site in draft mode"
  
  staging_environment:
    alternative: "Deploy preview branches to staging"
    use_case: "More substantial review workflows"

7.2 Editorial Workflow Considerations

editorial_workflow:
  
  content_workflow_states:
    typical: "Draft → In Review → Approved → Published"
    cms_support: "Most modern CMS support workflow states"
  
  approval_process:
    define: "Who can publish vs propose"
    cms_support: "Roles and permissions"
  
  content_localization:
    when: "Multi-language sites"
    cms_support: "Variable across CMS options"
  
  versioning_history:
    benefit: "Rollback capability"
    cms_support: "Most enterprise CMS support"

8. Performance Patterns

8.1 Performance Optimization

headless_performance_optimization:
  
  build_time_optimization:
    - Generate static HTML where possible
    - Optimize images at build
    - Bundle and minify assets
  
  runtime_caching:
    - CDN caching for static assets
    - ISR caching for static pages
    - API response caching
  
  api_efficiency:
    - GraphQL for selective data fetching
    - Avoid N+1 queries
    - Batch related fetches
    - Cache CMS responses where appropriate
  
  client_side_efficiency:
    - Minimize JavaScript on static pages
    - Hydrate selectively
    - Code split per route

8.2 Image Optimization

headless_image_optimization:
  
  cms_image_processing:
    - Most CMS provide image transformation APIs
    - Resize, format conversion at request time
    - Responsive images via URL parameters
  
  cdn_image_processing:
    - Cloudinary, Imgix, similar services
    - On-demand transformation
    - CDN delivery
  
  framework_image_components:
    - Next.js Image component
    - Astro's Image component
    - Auto-optimization with CMS sources

9. Common Headless Mistakes

common_mistakes:
  
  no_seo_fields_in_cms:
    issue: "Editors can't optimize without SEO fields exposed"
    fix: "Add comprehensive SEO fields to each content type"
  
  schema_only_in_template:
    issue: "Schema not editable per content piece"
    fix: "Make schema fields editable where needed"
  
  no_preview_workflow:
    issue: "Editors can't see content before publish"
    fix: "Implement Draft Mode or staging"
  
  no_revalidation_strategy:
    issue: "Content updates take full rebuild"
    fix: "On-demand revalidation; ISR; webhooks"
  
  poor_content_modeling:
    issue: "Content structure doesn't match presentation needs"
    fix: "Iterate models with editorial team input"
  
  client_side_rendering_for_content:
    issue: "Crawlers see empty page"
    fix: "Pre-render HTML; selective hydration only"
  
  sitemap_not_dynamic:
    issue: "Sitemap doesn't include CMS-managed URLs"
    fix: "Generate sitemap programmatically from CMS"
  
  no_editorial_training:
    issue: "Editors don't understand SEO impact"
    fix: "Document SEO field purposes; train team"
  
  ignoring_content_team_workflow:
    issue: "Developer-friendly but editor-hostile"
    fix: "Design CMS for editorial team usability"
  
  missing_cms_role_management:
    issue: "All editors have all permissions"
    fix: "Configure roles per workflow needs"

10. Audit Mode

# Criterion Pass/Fail
HC1 Comprehensive SEO fields per content type
HC2 Schema generated per content type
HC3 Sitemap programmatically generated
HC4 Build/runtime strategy appropriate per content
HC5 On-demand revalidation or webhooks configured
HC6 Preview workflow available
HC7 Editorial roles and permissions configured
HC8 Image optimization implemented
HC9 Performance excellent (CWV)
HC10 Critical content rendered (not CSR)
HC11 Internal linking strategy in templates
HC12 Editor training documented
HC13 Multi-language support (if applicable)
HC14 Backup and rollback capability
HC15 Monitoring of API health and rebuild status

Score: 15. World-class headless implementation: 13+/15.


11. Common Mistakes

(See Section 9 for detailed list)


End of Framework Document

Companion documents:

Want this framework implemented on your site?

ThatDevPro ships these frameworks as productized services. SDVOSB-certified veteran owned. Cassville, Missouri.

See Engine Optimization service ›