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