Astro Integration
Build ultra-fast, zero-JavaScript blogs and websites with Astro and Headless Bridge
Why Astro + Headless Bridge?
- Zero JavaScript by default - Only send what you need to the browser
- Static site generation - Pre-render pages at build time for lightning speed
- Island Architecture - Minimal JavaScript for interactive components
- Perfect for blogs - Combine Astro's speed with WordPress's content management
- Built-in optimizations - Image optimization, code splitting, bundling
Setup
1. Create Astro Project
npm create astro@latest my-blog -- --template blog
2. Environment Variables
Create .env:
PUBLIC_WP_API_URL=https://yoursite.com/wp-json/bridge/v1 WP_API_KEY=hb_your_key_here
3. Create WordPress Client
Create src/lib/wordpress.ts:
const API_URL = import.meta.env.PUBLIC_WP_API_URL;
interface Post {
uuid: string;
slug: string;
title: string;
excerpt: string;
content: string;
publishedAt: string;
modifiedAt: string;
author: {
id: number;
name: string;
avatar: string;
};
seo: {
title: string;
description: string;
canonical: string;
};
featuredImage?: {
url: string;
alt: string;
srcset: string;
sizes: string;
};
categories: Array<{
id: number;
name: string;
slug: string;
}>;
tags: Array<{
id: number;
name: string;
slug: string;
}>;
acf?: Record<string, any>;
}
export async function getPost(slug: string): Promise<Post> {
const response = await fetch(`${API_URL}/page?slug=${slug}`);
if (!response.ok) {
throw new Error(`Failed to fetch post: ${slug}`);
}
return response.json();
}
export async function getPosts(limit = 20, offset = 0) {
const response = await fetch(
`${API_URL}/pages?type=post&limit=${limit}&offset=${offset}`
);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
}
export async function getAllPostSlugs(): Promise<string[]> {
const data = await getPosts(100);
return data.items.map((post: Post) => post.slug);
}File-Based Routing Examples
Blog Post Page
Create src/pages/blog/[slug].astro:
---
import { getPost, getAllPostSlugs } from '@/lib/wordpress';
import Layout from '@/layouts/Layout.astro';
export async function getStaticPaths() {
const slugs = await getAllPostSlugs();
return slugs.map((slug) => ({
params: { slug },
}));
}
const { slug } = Astro.params;
const post = await getPost(slug);
---
<Layout title={post.seo.title} description={post.seo.description}>
<article class="max-w-4xl mx-auto px-4 py-16">
<header class="mb-8">
<h1 class="text-5xl font-bold mb-4">{post.title}</h1>
<div class="flex items-center gap-4 text-gray-600">
<span>{post.author.name}</span>
<span>•</span>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</div>
</header>
{post.featuredImage && (
<img
src={post.featuredImage.url}
alt={post.featuredImage.alt}
srcSet={post.featuredImage.srcset}
sizes={post.featuredImage.sizes}
class="w-full rounded-lg mb-8"
/>
)}
<div
class="prose prose-lg max-w-none"
set:html={post.content}
/>
{post.tags && post.tags.length > 0 && (
<div class="flex flex-wrap gap-2 pt-8 border-t">
<span class="text-gray-600 font-medium">Tags:</span>
{post.tags.map((tag) => (
<a
href={'/tag/' + tag.slug}
class="px-3 py-1 bg-gray-100 text-gray-700 rounded text-sm hover:bg-gray-200"
>
#{tag.name}
</a>
))}
</div>
)}
</article>
</Layout>Blog Index Page
Create src/pages/blog/index.astro:
---
import { getPosts } from '@/lib/wordpress';
import Layout from '@/layouts/Layout.astro';
const { items } = await getPosts(20);
---
<Layout title="Blog" description="Latest articles and posts">
<div class="max-w-6xl mx-auto px-4 py-16">
<h1 class="text-5xl font-bold mb-12">Blog</h1>
<div class="grid md:grid-cols-3 gap-8">
{items.map((post) => (
<a
href={'/blog/' + post.slug}
class="border rounded-lg overflow-hidden hover:shadow-lg transition"
>
{post.featuredImage && (
<img
src={post.featuredImage.url}
alt={post.featuredImage.alt}
class="w-full h-48 object-cover"
/>
)}
<div class="p-6">
<h2 class="text-xl font-bold mb-2">{post.title}</h2>
<p class="text-gray-600">{post.excerpt}</p>
<div class="mt-4 text-sm text-gray-500">
{new Date(post.publishedAt).toLocaleDateString()}
</div>
</div>
</a>
))}
</div>
</div>
</Layout>Dynamic Routes
Category Pages
Create src/pages/category/[slug].astro:
---
import { getPosts } from '@/lib/wordpress';
import Layout from '@/layouts/Layout.astro';
export async function getStaticPaths() {
const { items } = await getPosts(100);
const categories = new Set();
items.forEach((post) => {
post.categories?.forEach((cat) => {
categories.add(cat.slug);
});
});
return Array.from(categories).map((slug) => ({
params: { slug },
}));
}
const { slug } = Astro.params;
const { items } = await getPosts(100);
const posts = items.filter((post) =>
post.categories?.some((cat) => cat.slug === slug)
);
const categoryName = posts[0]?.categories?.find((cat) => cat.slug === slug)?.name;
---
<Layout title={categoryName} description={categoryName}>
<div class="max-w-6xl mx-auto px-4 py-16">
<h1 class="text-5xl font-bold mb-12">{categoryName}</h1>
{/* Posts grid here */}
</div>
</Layout>Building & Deployment
Build for Static Output
Update astro.config.mjs:
import { defineConfig } from 'astro/config';
export default defineConfig({
// Static output mode (pre-render all pages at build time)
output: 'static',
// Optional: Add integrations
integrations: [
// @astrojs/image for image optimization
// @astrojs/react for interactive components if needed
],
});Build and Deploy
# Build static site npm run build # Output is in dist/ directory # Deploy to Vercel, Netlify, GitHub Pages, etc. netlify deploy --prod --dir=dist
Advanced: Island Architecture
Add Interactive Components
Create src/components/Comments.tsx (React island):
import { useState } from 'react';
interface CommentsProps {
postId: string;
}
export default function Comments({ postId }: CommentsProps) {
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
// Submit comment logic here
setLoading(false);
};
return (
<section className="mt-12">
<h3 className="text-2xl font-bold mb-8">Comments</h3>
<form onSubmit={handleSubmit} className="mb-8 space-y-4">
<input
type="text"
placeholder="Your name"
className="w-full px-4 py-2 border rounded"
required
/>
<textarea
placeholder="Your comment"
className="w-full px-4 py-2 border rounded"
rows={4}
required
/>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{loading ? 'Posting...' : 'Post Comment'}
</button>
</form>
</section>
);
}Use in Astro
---
import Comments from '@/components/Comments.tsx';
import { getPost } from '@/lib/wordpress';
const post = await getPost(slug);
---
<article>
{/* Post content */}
{/* Only load React for comments - rest is static HTML */}
<Comments client:load postId={post.uuid} />
</article>Performance Tips
- ✓Use Static Generation: Pre-render posts at build time with
getStaticPaths() - ✓Optimize Images: Use Astro's Image component for automatic optimization
- ✓Island Architecture: Only load JavaScript for truly interactive components
- ✓Incremental Static Regeneration: Use on-demand revalidation with webhooks
- ✓Deploy Globally: Static files deploy instantly to CDNs worldwide