Astro is a modern web framework that delivers lightning-fast websites by shipping zero JavaScript by default. It supports multiple UI frameworks (React, Vue, Svelte) and integrates seamlessly with headless CMS backends. This guide covers deploying an Astro site with CMS integration on your VPS.
Why Astro?
- Zero JS by default: Pages ship as pure HTML unless you opt into client-side JavaScript
- Islands architecture: Interactive components hydrate independently
- Content collections: Type-safe Markdown/MDX content with schema validation
- Framework agnostic: Use React, Vue, Svelte, or Solid components together
- SSR support: Server-side rendering with Node.js adapter for dynamic content
Create an Astro Project
# Create new project
npm create astro@latest my-astro-site
cd my-astro-site
# Add integrations
npx astro add node # For SSR deployment
npx astro add react # If using React components
npx astro add tailwind # For Tailwind CSS
npx astro add mdx # For MDX support
Content Collections (Local CMS)
// src/content/config.ts
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.date(),
updatedAt: z.date().optional(),
author: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
draft: z.boolean().default(false),
}),
})
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
order: z.number(),
section: z.string(),
}),
})
export const collections = { blog, docs }
Fetching from a Headless CMS
// src/lib/cms.ts
// Example: Fetching from Directus, Strapi, or any REST API
export async function getPosts() {
const res = await fetch(`${import.meta.env.CMS_URL}/items/posts?filter[status][_eq]=published&sort=-published_at`, {
headers: {
'Authorization': `Bearer ${import.meta.env.CMS_TOKEN}`,
},
})
const { data } = await res.json()
return data
}
// src/pages/blog/[slug].astro
---
import { getPosts } from '../../lib/cms'
import Layout from '../../layouts/Layout.astro'
export async function getStaticPaths() {
const posts = await getPosts()
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
---
<Layout title={post.title}>
<article>
<h1>{post.title}</h1>
<time>{new Date(post.published_at).toLocaleDateString()}</time>
<div set:html={post.content} />
</article>
</Layout>
SSR Deployment on VPS
// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
export default defineConfig({
output: 'hybrid', // Static by default, SSR opt-in per page
adapter: node({
mode: 'standalone',
}),
server: { port: 4321, host: '127.0.0.1' },
})
# Build
npm run build
# The output is in dist/
# Entry point: dist/server/entry.mjs
# systemd service
sudo cat > /etc/systemd/system/astro-site.service > /var/log/astro-rebuilds.log
Best Practices
- Use hybrid rendering: Static pages for content, SSR for dynamic pages like search
- Leverage content collections for local Markdown content with type safety
- Ship minimal JavaScript: Use
client:load,client:idle, orclient:visibledirectives wisely - Set up image optimization: Use Astro's built-in
<Image>component for responsive images - Cache aggressively: Astro's hashed asset filenames make long cache headers safe
- Configure webhooks from your CMS to trigger rebuilds on content changes