Originally published at thatdevpro.com. This framework reference is part of the 14-tier Engine Optimization stack from ThatDevPro, an SDVOSB-certified veteran-owned web + AI engineering studio. You are reading the dev.to mirror; the source-of-truth canonical version with embedded validation tools lives at the link above.
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:
- Content reused across multiple channels
- Frontend technology choices unconstrained by CMS
- Performance often superior to monolithic CMS
- Developer experience improved
The SEO challenges are also real:
- Schema must be deliberately implemented
- Sitemap generation requires programmatic approach
- Preview workflows are more complex
- Build vs runtime decisions affect freshness
- Content workflow requires editorial team training
This framework specifies SEO patterns for headless architectures.
1.1 Required Tools
- Headless CMS — Contentful, Sanity, Strapi, Storyblok, Hygraph, Prismic
- Frontend framework — Next.js, Astro, Nuxt, SvelteKit, Remix
- Hosting — Vercel, Netlify, Cloudflare, self-hosted
- Content team training — workflow education
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:
-
framework-nextjs.md— Next.js as common frontend -
framework-astrohugo.md— Astro/Hugo as alternative frontends -
framework-schema.md— Schema implementation -
framework-international.md— Multi-language patterns -
framework-pageexperience.md— Performance metrics
From the ThatDevPro Engine Optimization framework library. Studio: ThatDevPro (SDVOSB veteran-owned web + AI engineering). Sister property: ThatDeveloperGuy. Source: https://www.thatdevpro.com/insights/framework-headless/.
Top comments (0)