/ astro-integrations / How to Use Payload with Astro: Complete Guide
astro-integrations 5 min read

How to Use Payload with Astro: Complete Guide

Step-by-step guide to integrating Payload with your Astro website.

Payload is an open-source TypeScript headless CMS and application framework that gives you full control over your backend. Unlike hosted CMS platforms, Payload runs on your own infrastructure, which means zero vendor lock-in and complete data ownership. It generates both REST and GraphQL APIs from your config, comes with a powerful admin panel, and supports authentication, access control, and file uploads out of the box.

For Astro developers, Payload is the option when you want a CMS you fully own and can extend without limits. The trade-off is more setup and infrastructure management, but the payoff is a backend that scales with you.

Prerequisites

  • Node.js 18+
  • An Astro project (npm create astro@latest)
  • A running Payload instance (self-hosted or Payload Cloud)
  • MongoDB or PostgreSQL for Payload's database
  • Basic familiarity with TypeScript

Installation

On the Astro side, you just need a way to call Payload's REST API. No special client library is required:

npm install ky

You can also use the native fetch API. Ky is just a convenience wrapper that handles JSON parsing and errors more cleanly.

If your Payload instance runs separately, you only need the HTTP client. If you want to run Payload in the same project (monorepo setup), install Payload alongside Astro:

npx create-payload-app@latest payload-backend

Configuration

Create a client helper for your Payload API:

// src/lib/payload.ts
import ky from "ky";

const payload = ky.create({
  prefixUrl: import.meta.env.PAYLOAD_API_URL,
  headers: {
    ...(import.meta.env.PAYLOAD_API_KEY && {
      Authorization: `users API-Key ${import.meta.env.PAYLOAD_API_KEY}`,
    }),
  },
});

export async function getCollection<T>(
  collection: string,
  params: Record<string, string> = {}
): Promise<{ docs: T[]; totalDocs: number }> {
  const searchParams = new URLSearchParams(params);
  return payload.get(`api/${collection}?${searchParams}`).json();
}

export async function getDocument<T>(
  collection: string,
  idOrSlug: string,
  field = "slug"
): Promise<T> {
  const result = await payload
    .get(`api/${collection}?where[${field}][equals]=${idOrSlug}&limit=1`)
    .json<{ docs: T[] }>();
  return result.docs[0];
}

Add your Payload connection details to .env:

PAYLOAD_API_URL=http://localhost:3000
PAYLOAD_API_KEY=your-api-key-here

On the Payload side, define your collections. Here is a typical blog post collection config:

// payload-backend/src/collections/Posts.ts
import type { CollectionConfig } from "payload";

export const Posts: CollectionConfig = {
  slug: "posts",
  admin: {
    useAsTitle: "title",
  },
  fields: [
    { name: "title", type: "text", required: true },
    { name: "slug", type: "text", required: true, unique: true },
    { name: "excerpt", type: "textarea" },
    { name: "content", type: "richText" },
    { name: "featuredImage", type: "upload", relationTo: "media" },
    { name: "publishedAt", type: "date" },
    { name: "tags", type: "array", fields: [{ name: "tag", type: "text" }] },
    { name: "status", type: "select", options: ["draft", "published"] },
  ],
};

Basic Usage

Fetch posts from Payload and render them in Astro:

---
// src/pages/blog/index.astro
import { getCollection } from "../../lib/payload";
import BaseLayout from "../../layouts/BaseLayout.astro";

interface Post {
  id: string;
  title: string;
  slug: string;
  excerpt: string;
  featuredImage?: { url: string };
  publishedAt: string;
}

const { docs: posts } = await getCollection<Post>("posts", {
  "where[status][equals]": "published",
  sort: "-publishedAt",
  limit: "50",
});
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {posts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        {post.featuredImage && (
          <img src={post.featuredImage.url} alt={post.title} loading="lazy" />
        )}
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </a>
    </article>
  ))}
</BaseLayout>

Dynamic post pages:

---
// src/pages/blog/[slug].astro
import { getCollection, getDocument } from "../../lib/payload";
import BaseLayout from "../../layouts/BaseLayout.astro";

export async function getStaticPaths() {
  const { docs: posts } = await getCollection("posts", {
    "where[status][equals]": "published",
    limit: "1000",
  });

  return posts.map((post) => ({
    params: { slug: post.slug },
  }));
}

const { slug } = Astro.params;
const post = await getDocument("posts", slug);
---

<BaseLayout title={post.title}>
  <article>
    <h1>{post.title}</h1>
    <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
    {post.featuredImage && (
      <img src={post.featuredImage.url} alt={post.title} />
    )}
    <div set:html={post.content_html} />
  </article>
</BaseLayout>

Production Tips

  1. Use Payload's access control. Define granular access rules per collection and field. This is especially important if you expose the API publicly. Lock down create/update/delete operations and only allow read access for your frontend API key.

  • Enable Payload's draft system. Use the versions and drafts features on your collections. This lets editors work on draft content that only publishes when approved, which is critical for production content workflows.

  • Set up a webhook for static rebuilds. Payload supports afterChange hooks on collections. Use these to trigger a rebuild on your hosting platform whenever a post is published or updated.

  • Use Payload's built-in image resizing. Configure image sizes in your media collection to generate thumbnails and responsive variants automatically. This saves bandwidth and speeds up page loads.

  • Deploy Payload separately. Run Payload on its own server or container, separate from your Astro frontend. This lets each scale independently. Payload works well on Railway, Render, or your own Docker setup.

  • Alternatives to Consider

    • Strapi if you want a similar self-hosted CMS with a more visual admin panel builder and a larger plugin ecosystem.
    • Directus if you prefer a CMS that wraps any existing SQL database with an instant REST and GraphQL API.
    • Sanity if you want a hosted solution with real-time collaboration and do not want to manage CMS infrastructure.

    Wrapping Up

    Payload is the CMS for developers who want full control. The TypeScript-first approach means your content schema, access control, and custom logic are all type-safe and versioned in code. Paired with Astro, you get a static frontend that is fast and cheap to host, backed by a CMS that can grow into a full application framework. The learning curve is steeper than hosted alternatives, but if you need a CMS that bends to your requirements instead of the other way around, Payload delivers.