Astro
Complete guide for integrating TentoCMS with Astro.
Verified against Astro 5.
Installation
pnpm add @tentocms/client
Environment Variables
Create .env:
TENTO_API_KEY=tento_pk_1234567890
TENTO_PREVIEW_KEY=preview_abc123def456789...
TENTO_BASE_URL=https://tento-api.intelligentlending.co.uk
PREVIEW_SECRET=your-random-secret-string
Setup
Create src/lib/tento.ts:
import { TentoClient } from '@tentocms/client'
export const tento = new TentoClient({
apiKey: import.meta.env.TENTO_API_KEY,
baseUrl: import.meta.env.TENTO_BASE_URL,
})
export const tentoPreview = new TentoClient({
apiKey: import.meta.env.TENTO_API_KEY,
previewKey: import.meta.env.TENTO_PREVIEW_KEY,
baseUrl: import.meta.env.TENTO_BASE_URL,
})
Basic Usage
Static Pages
---
// src/pages/index.astro
import { tento } from '../lib/tento'
const page = await tento.pages.getBySlug('homepage')
---
<html>
<head>
<title>{page.fields.title}</title>
</head>
<body>
<h1>{page.fields.title}</h1>
<div set:html={page.fields.content} />
</body>
</html>
Dynamic Routes
---
// src/pages/[slug].astro
import { tento } from '../lib/tento'
export async function getStaticPaths() {
const pages = await tento.pages.list({ limit: 100 })
return pages.data.map((page) => ({
params: { slug: page.slug },
props: { page },
}))
}
const { page } = Astro.props
---
<html>
<body>
<article>
<h1>{page.fields.title}</h1>
<div set:html={page.fields.content} />
</article>
</body>
</html>
Collections
---
// src/pages/products.astro
import { tento } from '../lib/tento'
const products = await tento.collections.list('products', {
limit: 20,
sort: '-createdAt'
})
---
<html>
<body>
<h1>Products</h1>
<div class="grid">
{products.data.map((product) => (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
</body>
</html>
Blog Posts
---
// src/pages/blog/index.astro
import { tento } from '../../lib/tento'
const posts = await tento.blog.posts.list({
limit: 10,
sort: '-publishedAt'
})
---
<html>
<body>
<h1>Blog</h1>
{posts.data.map((post) => (
<article>
<a href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</a>
<p>{post.excerpt}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</article>
))}
</body>
</html>
Dynamic Blog Routes
---
// src/pages/blog/[slug].astro
import { tento } from '../../lib/tento'
export async function getStaticPaths() {
// `list()` omits `content` by default, so build the paths from the list
// and fetch each full post (with body content) in the page frontmatter.
const posts = await tento.blog.posts.list({ limit: 100 })
return posts.data.map((post) => ({
params: { slug: post.slug },
}))
}
// getBySlug returns the full post, including `content`
const post = await tento.blog.posts.getBySlug(Astro.params.slug!)
---
<html>
<body>
<article>
<h1>{post.title}</h1>
{post.featuredImage && (
<img src={post.featuredImage._url} alt={post.featuredImage._altText} />
)}
<div set:html={post.content} />
<div class="meta">
{post.author && <p>By {post.author.name}</p>}
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
{post.category && (
<a href={`/blog/category/${post.category.slug}`}>
{post.category.name}
</a>
)}
{post.tags.map((tag) => (
<a href={`/blog/tag/${tag.slug}`}>{tag.name}</a>
))}
</article>
</body>
</html>
Preview Mode (SSR)
Astro requires on-demand (server) rendering for preview mode. Astro 5 removed the output: 'hybrid' mode: keep the default output: 'static' and opt individual routes into server rendering with export const prerender = false, plus a server adapter in astro.config.mjs:
import node from '@astrojs/node'
export default defineConfig({
output: 'static', // default; per-route `export const prerender = false` renders on demand
adapter: node({ mode: 'standalone' }), // or vercel(), netlify(), etc.
})
Create Preview Route
---
// src/pages/preview/[...slug].astro
export const prerender = false // Disable prerendering for this route
import { tentoPreview } from '../../lib/tento'
const { slug } = Astro.params
const secret = Astro.url.searchParams.get('secret')
// Validate secret
if (secret !== import.meta.env.PREVIEW_SECRET) {
return Astro.redirect('/404')
}
// Fetch draft content
const page = await tentoPreview.pages.getBySlug(slug!)
---
<html>
<body>
<div class="preview-banner">
⚠️ Preview Mode - Viewing Draft Content
</div>
<article>
<h1>{page.fields.title}</h1>
<div set:html={page.fields.content} />
</article>
</body>
</html>
Usage
Access preview mode:
https://yoursite.com/preview/homepage?secret=your-secret
TypeScript
Type-Safe Content
---
import { tento } from '../lib/tento'
import type { Page } from '@tentocms/client'
interface HomepageContent {
heroTitle: string
heroSubtitle: string
features: Array<{
title: string
description: string
}>
}
const page = await tento.pages.getBySlug<HomepageContent>('homepage')
---
<html>
<body>
<h1>{page.fields.heroTitle}</h1>
<p>{page.fields.heroSubtitle}</p>
<div>
{page.fields.features.map((feature) => (
<div>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
))}
</div>
</body>
</html>
Import Types
import type {
Page,
CollectionItem,
BlogPost,
MediaItem,
} from '@tentocms/client'
Components
Create reusable Astro components:
---
// src/components/PageContent.astro
import type { Page } from '@tentocms/client'
interface Props {
page: Page
}
const { page } = Astro.props
---
<article>
<h1>{page.fields.title}</h1>
<div set:html={page.fields.content} />
</article>
Use in pages:
---
import { tento } from '../lib/tento'
import PageContent from '../components/PageContent.astro'
const page = await tento.pages.getBySlug('about')
---
<html>
<body>
<PageContent page={page} />
</body>
</html>
Error Handling
---
import { TentoNotFoundError } from '@tentocms/client'
import { tento } from '../lib/tento'
let page
try {
page = await tento.pages.getBySlug(Astro.params.slug!)
} catch (error) {
if (error instanceof TentoNotFoundError) {
return Astro.redirect('/404')
}
throw error
}
---
<html>
<body>
<h1>{page.fields.title}</h1>
</body>
</html>
Image Optimization
Use Astro's Image component with TentoCMS media:
---
import { Image } from 'astro:assets'
import { tento } from '../lib/tento'
const media = await tento.media.getById('media-id')
---
<Image
src={tento.media.getImageUrl(media.id, {
width: 800,
format: 'webp',
quality: 90
})}
width={800}
height={600}
alt={media.altText || ''}
/>
Pagination
---
// src/pages/blog/[...page].astro
import { tento } from '../../lib/tento'
export async function getStaticPaths({ paginate }) {
const posts = await tento.blog.posts.list({ limit: 100 })
return paginate(posts.data, { pageSize: 10 })
}
const { page } = Astro.props
---
<html>
<body>
<h1>Blog - Page {page.currentPage}</h1>
{page.data.map((post) => (
<article>
<h2>{post.title}</h2>
<a href={`/blog/${post.slug}`}>Read More</a>
</article>
))}
<div class="pagination">
{page.url.prev && <a href={page.url.prev}>Previous</a>}
{page.url.next && <a href={page.url.next}>Next</a>}
</div>
</body>
</html>
Server-Side Rendering
For dynamic content, use SSR mode:
// astro.config.mjs
export default defineConfig({
output: 'server', // Full SSR
adapter: node(), // or vercel(), netlify(), etc.
})
Then disable prerendering per page:
---
export const prerender = false
import { tento } from '../lib/tento'
// This fetches on every request
const page = await tento.pages.getBySlug('dynamic-page')
---
Content Collections
Integrate TentoCMS with Astro Content Collections:
// src/content/config.ts
import { defineCollection, z } from 'astro:content'
const blogCollection = defineCollection({
type: 'data',
schema: z.object({
title: z.string(),
excerpt: z.string(),
publishedAt: z.string(),
}),
})
export const collections = {
blog: blogCollection,
}
Fetch and use:
---
import { getCollection } from 'astro:content'
import { tento } from '../lib/tento'
// Fetch from TentoCMS
const tentoPosts = await tento.blog.posts.list({ limit: 10 })
// Or use Astro content collections
const localPosts = await getCollection('blog')
---
<html>
<body>
<h1>All Posts</h1>
{/* Render TentoCMS posts */}
{tentoPosts.data.map(post => (
<article>
<h2>{post.title}</h2>
</article>
))}
</body>
</html>
Best Practices
1. Use Static Generation by Default
---
// ✅ Good: Static generation (fast, cacheable)
const page = await tento.pages.getBySlug('homepage')
---
2. Enable SSR Only When Needed
---
// Only disable prerendering for truly dynamic content
export const prerender = false
---
3. Optimize Images
---
import { tento } from '../lib/tento'
const imageUrl = tento.media.getImageUrl('image-id', {
width: 800,
format: 'webp',
quality: 90
})
---
<img src={imageUrl} alt="Optimized image" />
4. Use Environment Variables
---
// ✅ Good
const apiKey = import.meta.env.TENTO_API_KEY
// ❌ Bad
const apiKey = 'tento_pk_1234567890'
---
5. Implement Error Boundaries
Always handle errors gracefully to prevent build failures.
Build Hooks
Configure build hooks for automatic rebuilds:
# In your hosting provider (Netlify, Vercel, etc.)
curl -X POST 'https://api.netlify.com/build_hooks/YOUR_HOOK_ID'
Configure webhook in TentoCMS admin to trigger builds on content changes.

