How to Use Contentful with Astro: Complete Guide
Step-by-step guide to integrating Contentful with your Astro website. Installation, configuration, and best practices.
Contentful is one of the most popular enterprise headless CMS platforms out there, and it pairs really well with Astro. You get a powerful content modeling API on the backend and lightning-fast static pages on the frontend. If you want structured content without the weight of a traditional CMS, this is a solid combo.
This guide follows the approach in the official Astro Contentful docs and the Contentful JavaScript SDK docs, updated for Astro 6 and the current contentful SDK.
Prerequisites
- Node.js 18 or newer. The
contentfulSDK declaresengines.node >= 18, and Astro 6 also targets modern Node. - An Astro project (
npm create astro@latest). - A Contentful account. The free plan currently includes 10 users, 10,000 records, 100K API calls per month, and 50 GB of CDN bandwidth. Check the pricing page for the current numbers since these change.
Installation
Install the Contentful JavaScript SDK along with the rich text renderer. These are the two packages the official Astro guide installs:
npm install contentful @contentful/rich-text-html-renderer
At the time of writing the current versions are contentful 11.12.3 and @contentful/rich-text-html-renderer 17.2.2. Both package names are current and resolve on the npm registry. Pin whatever versions are current when you install; npm install writes the resolved version into your lockfile.
Configuration
Create a .env file in your project root with your Contentful credentials. You can find these in your Contentful dashboard under Settings then API keys.
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_DELIVERY_TOKEN=your_delivery_api_token
CONTENTFUL_PREVIEW_TOKEN=your_preview_api_token
Astro exposes these on import.meta.env. To get type safety and autocomplete, declare them in src/env.d.ts exactly as the Astro docs show:
// src/env.d.ts
interface ImportMetaEnv {
readonly CONTENTFUL_SPACE_ID: string;
readonly CONTENTFUL_DELIVERY_TOKEN: string;
readonly CONTENTFUL_PREVIEW_TOKEN: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Now create a helper file to initialize the Contentful client. Note the import * as contentful namespace import. The current SDK and the official Astro guide use this form, not a default import, so the EntryFieldTypes helpers and createClient resolve correctly:
// src/lib/contentful.ts
import * as contentful from "contentful";
import type { EntryFieldTypes } from "contentful";
export interface BlogPost {
contentTypeId: "blogPost";
fields: {
title: EntryFieldTypes.Text;
body: EntryFieldTypes.RichText;
publishDate: EntryFieldTypes.Date;
description: EntryFieldTypes.Text;
slug: EntryFieldTypes.Text;
};
}
export const contentfulClient = contentful.createClient({
space: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.DEV
? import.meta.env.CONTENTFUL_PREVIEW_TOKEN
: import.meta.env.CONTENTFUL_DELIVERY_TOKEN,
host: import.meta.env.DEV ? "preview.contentful.com" : "cdn.contentful.com",
});
This setup automatically switches between the preview API during development and the delivery API in production. In the official Astro guide's words, in development mode your content is fetched from the Contentful preview API, and at build time it is fetched from the delivery API.
The BlogPost interface above is a content type skeleton. Passing it as a generic to getEntries gives you typed fields and lets you filter on field-specific query keys. This is the recommended TypeScript pattern from both the Contentful SDK docs and the Astro guide.
Basic Usage
Let's say you have a "Blog Post" content type in Contentful with fields like title, slug, body, and publishDate. Pass the skeleton type as a generic so the items come back typed:
---
// src/pages/blog/index.astro
import { contentfulClient, type BlogPost } from "../../lib/contentful";
import BaseLayout from "../../layouts/BaseLayout.astro";
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
order: ["-fields.publishDate"],
});
const posts = entries.items;
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.fields.slug}`}>
<h2>{post.fields.title}</h2>
<time>{new Date(post.fields.publishDate).toLocaleDateString()}</time>
</a>
</li>
))}
</ul>
</BaseLayout>
The order parameter sorts results. Prefixing a field with a - reverses the sort, so -fields.publishDate gives you newest first. Multiple fields are supported as comma separated values (or as an array in the SDK).
Fetching Data for Dynamic Pages
For individual blog post pages, use getStaticPaths to generate routes at build time. This is the static generation path, which is Astro's default behavior:
---
// src/pages/blog/[slug].astro
import { contentfulClient, type BlogPost } from "../../lib/contentful";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
return entries.items.map((item) => ({
params: { slug: item.fields.slug },
props: {
title: item.fields.title,
content: documentToHtmlString(item.fields.body),
date: new Date(item.fields.publishDate).toLocaleDateString(),
},
}));
}
const { title, content, date } = Astro.props;
---
<BaseLayout title={title}>
<article>
<h1>{title}</h1>
<time>{date}</time>
<div set:html={content} />
</article>
</BaseLayout>
The rich text renderer comes from the package you already installed (@contentful/rich-text-html-renderer). It turns a Contentful rich text Document into an HTML string that you inject with set:html.
Rendering at Request Time Instead of Build Time
The example above generates every post at build time, which is the right default for a content site. If you instead want a route to render on demand (for example to read draft content with the preview API per request), Astro 6 handles this without a separate output mode.
A few facts to get right here, because the older guidance is out of date:
output: 'static'is the default in Astro. Your whole site is prerendered to static HTML unless you opt a route out.output: 'hybrid'was removed in Astro 5. The static and hybrid modes were merged into the default static mode, so if you seeoutput: 'hybrid'in an old tutorial, delete it.- To render any route on demand you add an adapter (for example
@astrojs/node, current version 10.1.2) and setexport const prerender = falseat the top of that page. No other config change is needed.
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
adapter: node({ mode: "standalone" }),
});
---
// src/pages/preview/[slug].astro
export const prerender = false; // render this route on demand
---
If you keep things fully static (the common case for a Contentful blog), you do not need an adapter at all.
Production Tips
Cache aggressively. Contentful's delivery API is fast, but fetching on every build still adds up. Keep Astro on its default static output and rebuild only when content changes via webhooks.
Use the
includeparameter. When fetching entries with linked content (images, references), setincludeto resolve nested entries in a single API call instead of multiple round trips. The default is 1 and the maximum is 10; the API throws aBadRequestErrorfor higher values. A value ofinclude: 2is a sensible starting point for most blogs.Set up webhooks for rebuilds. In Contentful, go to Settings then Webhooks and trigger a rebuild on your hosting platform (Vercel, Netlify, etc.) whenever content is published or unpublished. This pairs perfectly with Astro's static output: content edits trigger a fresh build instead of a per-request fetch.
Type your content models. Beyond the hand-written skeleton above, you can auto-generate TypeScript types from your Contentful content models with contentful-typescript-codegen (current version 3.4.0). It saves you from guessing field names across a large schema.
Optimize images. Contentful's Images API supports on-the-fly resizing and format conversion. The width parameter is w (up to 4000 px), the format parameter is fm (accepts jpg, png, webp, gif, avif, tiff), and quality is q (1 to 100). For example append ?w=800&fm=webp&q=80 to an asset URL for a smaller payload.
Common Errors and Fixes
createClient is not a functionor missingEntryFieldTypes. This usually means a default import. Use the namespace importimport * as contentful from "contentful"(andimport type { EntryFieldTypes } from "contentful") as the official Astro guide does. A default import does not expose the SDK the same way.includerejected with a 400 /BadRequestError. Theincludeparameter caps at 10. Anything higher, or a non-integer, throwsBadRequestError. Drop it to 10 or lower.output: 'hybrid'is not a valid option. Hybrid mode was removed in Astro 5. Remove theoutputline entirely (static is the default) or useoutput: 'server'if you genuinely want every route on demand. Per-route on-demand rendering is done withexport const prerender = falseplus an adapter.On-demand route returns static HTML or fails without an adapter. On-demand rendering requires a server adapter. Add one (such as
@astrojs/node) before settingprerender = false, otherwise Astro has no server runtime to render the route.Empty fields or unresolved links in the response. By default linked entries resolve only one level deep (
includedefaults to 1). Raiseincludeto pull nested references, and remember that linked entries which are unpublished will not resolve. The preview host (preview.contentful.com) is what surfaces draft content during development.undefinedenv vars at build time. Astro readsimport.meta.env.CONTENTFUL_*, notprocess.env, in.astroand client code. Confirm the names match your.envand yoursrc/env.d.tsdeclarations exactly.
Alternatives to Consider
- Sanity if you want real-time collaboration and a more flexible schema builder. It has an official Astro guide.
- Keystatic if you prefer a git-based CMS that stores content directly in your repo. Zero external dependencies.
- Storyblok if you need a visual editor for non-technical content teams. Great drag-and-drop experience.
Official Docs and Examples
- Astro Contentful guide (the source for the client setup and rich text rendering above): https://docs.astro.build/en/guides/cms/contentful/
- Astro on-demand rendering reference (adapters and
prerender): https://docs.astro.build/en/guides/on-demand-rendering/ - Contentful JavaScript SDK on GitHub, including the TYPESCRIPT.md skeleton-type guide: https://github.com/contentful/contentful.js
- Official Contentful Astro starter (real example repo, Tailwind + Contentful): https://github.com/contentful/starter-astro-bookshelf
- Contentful Content Delivery API reference (
order,include): https://www.contentful.com/developers/docs/references/content-delivery-api/ - Contentful Images API reference (
w,fm,q,fit): https://www.contentful.com/developers/docs/references/images-api/
Wrapping Up
Contentful and Astro make a strong pair for content-heavy sites that need structure and performance. Set up the typed client, model your content, fetch it at build time with getStaticPaths, and you have a fast, maintainable site with a proper CMS behind it. Reach for an adapter and prerender = false only when a specific route truly needs to render on demand.
Sources
All versions and facts below were checked on 2026-05-29.
- contentful 11.12.3 (npm registry): https://registry.npmjs.org/contentful/latest
- @contentful/rich-text-html-renderer 17.2.2 (npm registry): https://registry.npmjs.org/@contentful/rich-text-html-renderer/latest
- contentful-typescript-codegen 3.4.0 (npm registry): https://registry.npmjs.org/contentful-typescript-codegen/latest
- astro 6.4.2 (npm registry): https://registry.npmjs.org/astro/latest
- @astrojs/node 10.1.2 (npm registry): https://registry.npmjs.org/@astrojs/node/latest
- Astro Contentful guide (client, env.d.ts, getStaticPaths, rich text): https://docs.astro.build/en/guides/cms/contentful/
- Astro on-demand rendering (adapter requirement,
prerender = false, static default): https://docs.astro.build/en/guides/on-demand-rendering/ - Astro upgrade to v5 (
output: 'hybrid'removed, merged into static): https://docs.astro.build/en/guides/upgrade-to/v5/ - Contentful SDK TypeScript skeleton types and
import * as contentful: https://github.com/contentful/contentful.js - Contentful Content Delivery API (
ordersorting,includemax 10, default 1): https://www.contentful.com/developers/docs/references/content-delivery-api/ - Contentful Images API (
wup to 4000,fmformats incl. webp/avif,q1 to 100,fit): https://www.contentful.com/developers/docs/references/images-api/ - Contentful pricing and free plan limits (10 users, 10k records, 100k API calls/mo, 50 GB CDN): https://www.contentful.com/pricing/
- Official Contentful Astro starter example repo: https://github.com/contentful/starter-astro-bookshelf
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.