Your Business Name
Documentation · Advanced

Headless CMS Integration

Integrate WordPress, Sanity, Contentful, or any headless CMS with the Astro content layer for flexible content management

CMS Documentation Team · · 8 min read
Table of Contents

Overview

The Astro CMS is designed with a flexible Content Layer API that allows you to swap content sources without changing your templates or components. Start with markdown, migrate to a headless CMS later - your site keeps working.

Supported Platforms

Built-In Support

  • Markdown/MDX: File-based content (default)
  • JSON/YAML: Structured data files

Via Loaders

  • WordPress: REST API or GraphQL
  • Sanity: Sanity.io integration
  • Contentful: Contentful CMS
  • Strapi: Open-source headless CMS
  • Ghost: Ghost CMS
  • Notion: Notion as CMS
  • Any REST API: Custom loader

Architecture

Content Layer Pattern

All content goes through the same interface:

// Your templates always use this:
const entries = await getCollection('cms');

// Content source is configured once:
// src/content/config.ts
const cms = defineCollection({
  loader: /* markdown | wordpress | sanity | custom */,
  schema: cmsSchema
});

Benefits:

  • Change CMS without touching templates
  • Test different CMSs easily
  • Migrate incrementally
  • Use multiple sources simultaneously

WordPress Integration

Setup

1. Install WordPress Loader

npm install @astrojs/wordpress

2. Configure WordPress Endpoint

// src/config/cms.js
export default {
  source: 'wordpress',
  wordpress: {
    endpoint: 'https://your-site.com/wp-json/wp/v2',
    cache: true,
    ttl: 3600, // 1 hour
    auth: {
      // Optional: for private posts
      username: process.env.WP_USERNAME,
      password: process.env.WP_APP_PASSWORD,
    },
  },
};

3. Update Content Collection

// src/content/config.ts
import { defineCollection } from 'astro:content';
import { wordPressLoader } from '@astrojs/wordpress';
import cmsConfig from '../config/cms.js';
import { cmsSchema } from './schemas/cms.js';

const cms = defineCollection({
  loader: wordPressLoader({
    endpoint: cmsConfig.wordpress.endpoint,
    cache: cmsConfig.wordpress.cache,
    ttl: cmsConfig.wordpress.ttl,
  }),
  schema: cmsSchema,
});

export const collections = { cms };

Data Mapping

WordPress data automatically maps to CMS schema:

WordPress FieldCMS FieldNotes
title.renderedtitlePost title
excerpt.renderedexcerptPost excerpt
content.renderedbodyPost content (HTML)
datepubDatePublication date
modifiedupdatedDateLast modified
authorauthorAuthor name
categoriescategoriesCategory names
tagstagsTag names
featured_mediaheroImageFeatured image URL
slugslugPost slug
parentparentParent page slug

Custom Fields

Map WordPress custom fields:

// WordPress Advanced Custom Fields (ACF)
const loader = wordPressLoader({
  endpoint: 'https://your-site.com/wp-json/wp/v2',
  transform: (post) => ({
    title: post.title.rendered,
    body: post.content.rendered,
    // Custom fields
    layout: post.acf.layout_type,
    showToc: post.acf.show_toc,
    featured: post.acf.is_featured,
  }),
});

Hierarchical Pages

WordPress parent-child relationships work automatically:

WordPress Page Hierarchy:
├── Docs (parent: null)
│   ├── Getting Started (parent: docs)
│   └── API Reference (parent: docs)

Becomes:
/docs/
/docs/getting-started/
/docs/api-reference/

Sanity Integration

Setup

1. Install Sanity Loader

npm install @sanity/astro-loader

2. Configure Sanity

// src/config/cms.js
export default {
  source: 'sanity',
  sanity: {
    projectId: process.env.SANITY_PROJECT_ID,
    dataset: process.env.SANITY_DATASET || 'production',
    apiVersion: '2023-05-03',
    useCdn: true,
  },
};

3. Create Sanity Loader

// src/content/config.ts
import { defineCollection } from 'astro:content';
import { sanityLoader } from '@sanity/astro-loader';
import cmsConfig from '../config/cms.js';

const cms = defineCollection({
  loader: sanityLoader({
    projectId: cmsConfig.sanity.projectId,
    dataset: cmsConfig.sanity.dataset,
    query: `
      *[_type == "post"] {
        _id,
        title,
        slug,
        body,
        publishedAt,
        author->{name},
        categories[]->title,
        "heroImage": mainImage.asset->url
      }
    `,
    transform: (doc) => ({
      id: doc._id,
      slug: doc.slug.current,
      data: {
        title: doc.title,
        body: doc.body,
        pubDate: doc.publishedAt,
        author: doc.author.name,
        categories: doc.categories,
        heroImage: doc.heroImage,
      },
    }),
  }),
  schema: cmsSchema,
});

GROQ Queries

Customize what content to fetch:

// All published posts
*[_type == "post" && !(_id in path("drafts.**"))]

// Posts with specific category
*[_type == "post" && "documentation" in categories[]->slug.current]

// Hierarchical content
*[_type == "post"] {
  ...,
  parent->{slug}
}

Contentful Integration

Setup

1. Install Contentful Loader

npm install contentful

2. Configure Contentful

// src/config/cms.js
export default {
  source: 'contentful',
  contentful: {
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
    environment: 'master',
  },
};

3. Create Custom Loader

// src/loaders/contentful.js
import { createClient } from 'contentful';

export function contentfulLoader(config) {
  const client = createClient({
    space: config.space,
    accessToken: config.accessToken,
  });

  return {
    name: 'contentful-loader',
    async load() {
      const entries = await client.getEntries({
        content_type: 'post',
      });

      return entries.items.map((item) => ({
        id: item.sys.id,
        slug: item.fields.slug,
        data: {
          title: item.fields.title,
          body: item.fields.body,
          pubDate: item.fields.publishDate,
          author: item.fields.author?.fields.name,
          categories: item.fields.categories || [],
          heroImage: item.fields.heroImage?.fields.file.url,
        },
      }));
    },
  };
}

Hybrid Mode

Run Multiple Sources Simultaneously

Combine local markdown with headless CMS:

// src/content/config.ts
import { glob } from 'astro/loaders';
import { wordPressLoader } from '@astrojs/wordpress';

// Local markdown content
const cmsLocal = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/cms' }),
  schema: cmsSchema,
});

// WordPress content
const cmsWordPress = defineCollection({
  loader: wordPressLoader({ endpoint: '...' }),
  schema: cmsSchema,
});

// Merge both sources
export const collections = {
  cms: mergeSources([cmsLocal, cmsWordPress]),
};

Use Cases for Hybrid Mode

Migration Period:

  • Gradually move content from markdown to CMS
  • Test CMS before full migration
  • Maintain fallback during transition

Content Split:

  • Core pages in markdown (fast, version-controlled)
  • Blog posts in WordPress (client can edit)
  • Documentation in Notion (team collaboration)

Multi-Source Aggregation:

  • Aggregate content from multiple sources
  • Unified search across all content
  • Single consistent design

Custom Loaders

Create Your Own Loader

// src/loaders/custom-api.js
export function customAPILoader(config) {
  return {
    name: 'custom-api-loader',

    async load() {
      const response = await fetch(config.endpoint);
      const data = await response.json();

      return data.items.map((item) => ({
        id: item.id,
        slug: item.slug,
        data: {
          title: item.title,
          body: item.content,
          pubDate: new Date(item.published_at),
          author: item.author.name,
          categories: item.categories,
          tags: item.tags,
          heroImage: item.featured_image,
          parent: item.parent_slug,
        },
      }));
    },

    // Optional: watch for changes during dev
    async watch(onChange) {
      // Implement webhook listener or polling
    },
  };
}

Use Custom Loader

// src/content/config.ts
import { customAPILoader } from '../loaders/custom-api.js';

const cms = defineCollection({
  loader: customAPILoader({
    endpoint: 'https://api.example.com/posts',
  }),
  schema: cmsSchema,
});

Caching & Performance

Build-Time Caching

Cache CMS responses during build:

wordPressLoader({
  endpoint: '...',
  cache: true,
  ttl: 3600, // Cache for 1 hour
});

Incremental Builds

Only fetch changed content:

const loader = wordPressLoader({
  endpoint: '...',
  incremental: true,
  lastBuild: process.env.LAST_BUILD_TIME,
});

CDN Caching

Static pages cached at CDN:

  • WordPress content fetched once at build
  • Served as static HTML
  • No runtime CMS queries
  • Lightning-fast delivery

Webhooks & Auto-Rebuild

Netlify Build Hooks

Trigger rebuild when content changes:

1. Create Build Hook

Netlify Dashboard → Site Settings → Build & Deploy → Build Hooks

2. Configure WordPress

Install “WP Webhooks” plugin:

// WordPress: Trigger Netlify rebuild on post publish
add_action('publish_post', function($post_id) {
    wp_remote_post('https://api.netlify.com/build_hooks/YOUR_HOOK_ID');
});

3. Configure Sanity

// sanity.config.js
export default defineConfig({
  // ...
  webhooks: [
    {
      name: 'netlify-rebuild',
      url: 'https://api.netlify.com/build_hooks/YOUR_HOOK_ID',
      on: ['create', 'update', 'delete'],
      filter: '_type == "post"',
    },
  ],
});

Content Preview

Draft Content Preview

Enable preview mode for drafts:

// src/pages/preview.astro
---
const { token, id } = Astro.url.searchParams;

// Verify preview token
if (token !== process.env.PREVIEW_TOKEN) {
  return Astro.redirect('/404');
}

// Fetch draft content
const draft = await fetchDraftContent(id);
---

<Layout>
  <article>
    <h1>{draft.title}</h1>
    <div set:html={draft.body} />
  </article>
</Layout>

WordPress/Sanity preview links:

WordPress:
https://yoursite.com/preview?token=SECRET&id=POST_ID

Sanity:
https://yoursite.com/preview?token=SECRET&id=DOCUMENT_ID

Data Transformation

Markdown Conversion

Convert HTML to Markdown:

import TurndownService from 'turndown';

const turndown = new TurndownService();

const loader = wordPressLoader({
  transform: (post) => ({
    ...post,
    body: turndown.turndown(post.content.rendered),
  }),
});

Image Processing

Process CMS images:

transform: (post) => ({
  ...post,
  heroImage: post.featured_media ? `${post.featured_media}?w=1200&h=630&fit=crop` : null,
});

Authentication

Private Content

Access password-protected content:

wordPressLoader({
  endpoint: '...',
  auth: {
    username: process.env.WP_USERNAME,
    password: process.env.WP_APP_PASSWORD, // Application password
  },
});

API Keys

Secure API access:

sanityLoader({
  projectId: '...',
  dataset: '...',
  token: process.env.SANITY_READ_TOKEN, // For private datasets
});

Troubleshooting

Content Not Updating

Issue: Changes in CMS not reflected on site

Solutions:

  1. Trigger manual rebuild
  2. Check webhook configuration
  3. Verify cache TTL settings
  4. Clear build cache

Schema Validation Errors

Issue: CMS data doesn’t match schema

Solutions:

  1. Update schema to match CMS fields
  2. Transform data in loader
  3. Provide default values
  4. Make fields optional

Performance Issues

Issue: Slow builds with large content

Solutions:

  1. Enable caching (cache: true)
  2. Use incremental builds
  3. Limit content fetched
  4. Paginate API requests

Migration Strategies

Gradual Migration

  1. Week 1: Set up headless CMS
  2. Week 2: Import existing content
  3. Week 3: Run hybrid mode (markdown + CMS)
  4. Week 4: Migrate remaining content
  5. Week 5: Remove markdown loader

Content Export

Export markdown to CMS:

// scripts/export-to-cms.js
import { getCollection } from 'astro:content';
import { createPost } from './cms-api.js';

const posts = await getCollection('cms');

for (const post of posts) {
  await createPost({
    title: post.data.title,
    content: post.body,
    // ...
  });
}

Best Practices

  1. Start with Markdown: Build and test with markdown first
  2. Plan Schema Early: Design schema to work with multiple sources
  3. Use Environment Variables: Keep credentials secure
  4. Enable Caching: Speed up builds
  5. Set Up Webhooks: Auto-rebuild on content changes
  6. Test Locally: Use .env.local for local CMS testing
  7. Monitor Build Times: Track performance with different CMSs
  8. Version Your Content: Use git for markdown, CMS versioning for headless

Resources

◆ ◆ ◆
headless cms integration wordpress sanity

If you enjoyed this article, please share it:

Written by

CMS Documentation Team