How to Use TinaCMS with Astro: Complete Guide
Step-by-step guide to integrating TinaCMS with your Astro website.
TinaCMS is a Git-backed headless CMS that gives you visual editing on your actual site while storing all content as Markdown or MDX files in your repository. The key differentiator here is that your content lives in Git, not in a third-party database. Edits made through the visual editor create real commits in your repo. For developers who want version-controlled content with a friendly editing interface, Tina is a compelling option.
With Astro, TinaCMS provides a GraphQL API layer over your local Markdown/MDX files. You define your content schema in code, and Tina generates a type-safe API plus an admin UI that editors can use to modify content without touching files directly.
Versions referenced in this guide (checked on 2026-05-29 against registry.npmjs.org): astro 6.4.2, tinacms 3.8.3, @tinacms/cli 2.4.1, @tinacms/astro 0.4.0, @astrojs/node 10.1.2. Pin these in your package.json so a future major release does not change behavior under you.
Prerequisites
- Node.js v22.12.0 or newer (the
engines.nodefield onastro6.4.2 requires>=22.12.0) - An Astro project (
npm create astro@latest) - A GitHub repository for your project (Tina commits directly to your repo)
- A Tina Cloud account for the hosted editing experience (free tier available; pricing is published on the Tina pricing page rather than fixed here so it stays current)
Installation
Initialize TinaCMS in your existing Astro project:
npx @tinacms/cli@latest init
This command scaffolds the configuration and adds the necessary dependencies. It creates a tina/ directory with your schema configuration. When the init flow prompts for your public assets directory, answer public (Astro's default static directory).
If you want the official Astro starter instead of bolting Tina onto an existing project, scaffold it directly:
npx create-tina-app@latest --template tina-astro-starter
If you prefer manual setup, install the runtime, the CLI, and the Astro integration:
npm install tinacms @tinacms/cli @tinacms/astro
The @tinacms/astro package is the official integration that powers contextual visual editing. Pin it explicitly; it is versioned separately from the core tinacms runtime (0.4.0 at the time of writing).
Rendering Mode in Astro 6
A common point of confusion: in Astro 6 the old output: 'hybrid' value no longer exists. It was removed back in Astro 5. Astro now has exactly two output modes. static is the default and prerenders everything at build time. server renders pages on demand and requires an adapter. You mix the two per route with export const prerender = true (force static) or export const prerender = false (force on-demand). In static mode pages are prerendered unless you opt out; in server mode they render on demand unless you opt in.
This matters for two different Tina workflows:
- Pure published output (the public blog) can stay fully static. Tina reads your Markdown/MDX at build time and your pages prerender like any other Astro site.
- Tina's contextual visual editing (the in-context editor that updates the page as you type) needs server-rendered routes, so it expects an adapter and
output: 'server'.
If you only want the admin form interface at /admin and are happy with a static public site, you can skip the integration and keep the default static output. If you want the live in-context editor, add the @tinacms/astro integration and an adapter. The official Astro integration setup looks like this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tina from '@tinacms/astro/integration';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
integrations: [tina()],
});
Swap @astrojs/node for @astrojs/vercel, @astrojs/netlify, or @astrojs/cloudflare depending on where you deploy. The integration auto-injects the request-scoped middleware that visual editing needs, so there is no extra head-tag wiring to add yourself.
Configuration
The init command creates tina/config.ts. Define your content schema here:
// tina/config.ts
import { defineConfig } from "tinacms";
export default defineConfig({
branch: process.env.TINA_BRANCH || "main",
clientId: process.env.TINA_CLIENT_ID || "",
token: process.env.TINA_TOKEN || "",
build: {
outputFolder: "admin",
publicFolder: "public",
},
media: {
tina: {
mediaRoot: "blog-images",
publicFolder: "public",
},
},
schema: {
collections: [
{
name: "post",
label: "Blog Posts",
path: "src/content/posts",
format: "mdx",
fields: [
{
type: "string",
name: "title",
label: "Title",
isTitle: true,
required: true,
},
{
type: "string",
name: "description",
label: "Description",
},
{
type: "datetime",
name: "publishDate",
label: "Publish Date",
},
{
type: "image",
name: "heroImage",
label: "Hero Image",
},
{
type: "string",
name: "tags",
label: "Tags",
list: true,
},
{
type: "rich-text",
name: "body",
label: "Body",
isBody: true,
},
],
},
],
},
});
Add your Tina Cloud credentials to .env:
TINA_CLIENT_ID=your-client-id
TINA_TOKEN=your-read-only-token
TINA_BRANCH=main
Get these from the Tina Cloud dashboard after connecting your GitHub repository.
Basic Usage
TinaCMS provides a GraphQL client that reads your content. Use it to fetch posts in your Astro pages:
---
// src/pages/blog/index.astro
import { client } from "../../tina/__generated__/client";
import BaseLayout from "../../layouts/BaseLayout.astro";
const postsResponse = await client.queries.postConnection({
sort: "publishDate",
last: 50,
});
const posts = postsResponse.data.postConnection.edges || [];
---
<BaseLayout title="Blog">
<h1>Blog</h1>
{posts.map(({ node: post }) => (
<article>
<a href={`/blog/${post._sys.filename}`}>
<h2>{post.title}</h2>
<p>{post.description}</p>
</a>
</article>
))}
</BaseLayout>
For individual post pages:
---
// src/pages/blog/[slug].astro
import { client } from "../../tina/__generated__/client";
import { TinaMarkdown } from "tinacms/dist/rich-text";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const postsResponse = await client.queries.postConnection();
const posts = postsResponse.data.postConnection.edges || [];
return posts.map(({ node }) => ({
params: { slug: node._sys.filename },
}));
}
const { slug } = Astro.params;
const postResponse = await client.queries.post({
relativePath: `${slug}.mdx`,
});
const post = postResponse.data.post;
---
<BaseLayout title={post.title}>
<article>
<h1>{post.title}</h1>
{post.heroImage && (
<img src={post.heroImage} alt={post.title} />
)}
<div set:html={post.body} />
</article>
</BaseLayout>
For rendering Tina rich-text inside server-rendered, visually-editable regions, use the TinaMarkdown component from @tinacms/astro rather than rendering raw HTML with set:html. The set:html approach shown above is fine for fully static published pages; the component approach is what enables the in-context editor to highlight and edit individual blocks.
Run the dev server with Tina's admin panel:
npx tinacms dev -c "astro dev"
This starts both Astro's dev server and the Tina admin. If you need a non-default port, pass it through to Astro, for example npx tinacms dev -c "astro dev --port 8080". The admin interface is served at /admin/index.html (for example http://localhost:4321/admin/index.html). Editors can now create and modify content visually.
Production Tips
Use Tina's local mode during development. Run
npx tinacms devfor local development, which reads directly from your filesystem without needing Tina Cloud. This is faster and works offline.Set up branch-based editing. Tina supports editing on different Git branches. Point your staging environment at a
draftbranch so editors can preview changes without affecting production content.
Define custom components for rich text. If your MDX uses custom components, register them in Tina's rich-text field configuration. This lets editors insert components like callouts, code blocks, or embeds through the visual editor.
Keep your generated client up to date. Run npx tinacms build after schema changes to regenerate the GraphQL client. The generated types ensure your queries stay in sync with your content model.
Use Tina's media manager. Configure the media field to upload images to your repo's public folder. This keeps all assets version-controlled alongside your content.
Alternatives to Consider
- Keystatic (
@keystatic/astro5.1.0) if you want a similar Git-based CMS with an even simpler setup and a first-party Astro integration. - Prismic (
@prismicio/client7.21.8) if you need a hosted CMS with more advanced slice-based content modeling and scheduled publishing. - Decap CMS (
decap-cms-app3.12.2, formerly Netlify CMS) if you want a free, open-source Git-based CMS with a simpler feature set.
Common Errors and Fixes
You set
output: 'hybrid'and the build fails with an unknown value. That mode was removed in Astro 5 and is gone in Astro 6. Useoutput: 'static'(the default) oroutput: 'server', and control individual routes withexport const prerender = trueorexport const prerender = false.Visual editing does not load, or
/admin404s the in-context editor. Contextual editing is server-rendered. Confirm@tinacms/astrois installed,integrations: [tina()]is present,output: 'server'is set, and an adapter such as@astrojs/nodeis configured. A purely static site can still use the admin form interface but not the live in-context editor.tinacms buildordeverrors on the Node version. Astro 6.4.2 requires Node>=22.12.0. Node 18.x, 20.x, and earlier 22.x point releases are not supported; upgrade Node before debugging Tina itself.The generated client import path cannot be found.
tina/__generated__/clientonly exists after a generate step. Runnpx tinacms build(or startnpx tinacms dev) at least once after any schema change so the GraphQL client and its types are regenerated before your Astro pages import them.relativePathqueries return null. TherelativePathpassed to a single-document query must match the on-disk file relative to the collection'spath, including the extension. For anmdxcollection that means${slug}.mdx, not just${slug}.Wrong public folder during init. If you accept a non-
publicanswer when the init prompt asks for the assets directory, the admin build lands somewhere Astro will not serve. Re-run init and answerpublic, or fixbuild.publicFolderintina/config.ts.
Official Docs and Examples
- TinaCMS Astro integration guide: https://tina.io/docs/frameworks/astro
- TinaCMS getting started: https://tina.io/docs/introduction/using-starter/
- Official Astro starter repo: https://github.com/tinacms/tina-astro-starter (scaffold with
npx create-tina-app@latest --template tina-astro-starter) - Astro on-demand rendering and output modes: https://docs.astro.build/en/guides/on-demand-rendering/
Wrapping Up
TinaCMS gives you the best of both worlds: a visual editing experience for content creators and Git-backed content for developers. The schema-as-code approach means your content model is versioned and reviewable in pull requests. For Astro sites where you want to keep content in your repository while still providing a proper CMS interface, Tina is one of the strongest options available. The free tier handles small teams, and the editing experience is smooth enough that non-technical collaborators can contribute content without learning Markdown.
Sources
All versions and facts below were checked on 2026-05-29.
- tinacms on npm (3.8.3)
- @tinacms/cli on npm (2.4.1)
- @tinacms/astro on npm (0.4.0)
- create-tina-app on npm (2.1.5)
- astro on npm (6.4.2)
- @astrojs/node on npm (10.1.2)
- @keystatic/astro on npm (5.1.0)
- decap-cms-app on npm (3.12.2)
- @prismicio/client on npm (7.21.8)
- TinaCMS Astro integration docs
- TinaCMS getting started
- Official TinaCMS Astro starter repo
- Astro on-demand rendering and output modes
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.