Shipping Zero JavaScript Websites with Astro and Contentful

by Ayham, Software Engineer

Have you ever visited a blog or documentation site and wondered why it needed to download hundreds of kilobytes of JavaScript just to display some text? It's a fair question.

Many modern websites ship large JavaScript bundles to the browser, even when most of the content is static. For content-focused sites—blogs, documentation, portfolios—there's often a simpler approach.

This guide explores Astro, a framework designed with a different philosophy, and how to pair it with Contentful for content management.

Choosing the Right Tool for the Job

React and similar frameworks are excellent for building interactive applications. But not every project needs the same toolset.

A blog typically needs:

  • Fast, reliable page loads
  • Content that works even if JavaScript fails to load
  • Good search engine visibility
  • A pleasant reading experience

It doesn't necessarily need:

  • Client-side routing
  • Complex state management
  • A JavaScript framework running in the browser

This led us to explore Astro, and we've been impressed with what it offers.

What Is Astro?

Astro is a web framework designed with an interesting philosophy: it ships zero JavaScript to the browser by default.

Here's what that means in practice. Most frameworks assume your pages need interactivity, so they send JavaScript to the browser to make everything work. Astro takes a different approach—it assumes most of your content is static (which, for many websites, it is), so it renders everything to HTML at build time.

If you think about a typical website, much of it is static: navigation, articles, images, team bios. None of that requires JavaScript to display. Astro recognizes this and only sends JavaScript for the parts that genuinely need it.

Setting Up: Astro with Contentful

For this walkthrough, we'll use:

  • Astro 4.x — The framework
  • Contentful — A headless CMS for managing content
  • TypeScript — For type safety

Let's walk through the setup together.

Step 1: Create Your Project

npm create astro@latest zero-js-blog
cd zero-js-blog
npm install contentful

Astro's CLI will guide you through some options. The minimal template works well if you'd like to build from scratch.

Step 2: Configure Contentful

In Contentful, create a new space and set up a content type called blogPost with these fields:

  • title (Short text)
  • slug (Short text, unique)
  • body (Rich text)
  • excerpt (Long text)
  • publishedDate (Date)

You'll find your Space ID and Content Delivery API token under Settings → API Keys.

Step 3: Set Up Environment Variables

Create a .env file in your project root. Remember to add this to .gitignore to keep your credentials safe:

CONTENTFUL_SPACE_ID=your_space_id_here
CONTENTFUL_ACCESS_TOKEN=your_access_token_here

Step 4: Create the Content Fetcher

This is where we connect to Contentful. Create a file at src/lib/contentful.ts:

import contentful, { type EntryFieldTypes } from 'contentful';

// Define the structure of a blog post
interface BlogPostFields {
  title: EntryFieldTypes.Text;
  slug: EntryFieldTypes.Text;
  body: EntryFieldTypes.RichText;
  excerpt: EntryFieldTypes.Text;
  publishedDate: EntryFieldTypes.Date;
}

const client = contentful.createClient({
  space: import.meta.env.CONTENTFUL_SPACE_ID,
  accessToken: import.meta.env.CONTENTFUL_ACCESS_TOKEN,
});

export async function getAllPosts() {
  const entries = await client.getEntries<BlogPostFields>({
    content_type: 'blogPost',
    order: ['-fields.publishedDate'],
  });

  return entries.items.map((item) => ({
    slug: item.fields.slug,
    title: item.fields.title,
    body: item.fields.body,
    excerpt: item.fields.excerpt,
    publishedDate: item.fields.publishedDate,
  }));
}

export async function getPostBySlug(slug: string) {
  const entries = await client.getEntries<BlogPostFields>({
    content_type: 'blogPost',
    'fields.slug': slug,
    limit: 1,
  });

  if (entries.items.length === 0) {
    return null;
  }

  const item = entries.items[0];
  return {
    slug: item.fields.slug,
    title: item.fields.title,
    body: item.fields.body,
    excerpt: item.fields.excerpt,
    publishedDate: item.fields.publishedDate,
  };
}

This is straightforward TypeScript that fetches content from Contentful. The important thing to understand is that this code runs at build time, not in your visitors' browsers.

Step 5: Build the Blog Post Page

Create src/pages/blog/[slug].astro:

---
import { getAllPosts, getPostBySlug } from '../../lib/contentful';
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import Layout from '../../layouts/Layout.astro';

// This function runs at build time
export async function getStaticPaths() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    params: { slug: post.slug },
  }));
}

const { slug } = Astro.params;
const post = await getPostBySlug(slug);

if (!post) {
  return Astro.redirect('/404');
}

const htmlContent = documentToHtmlString(post.body);
---

<Layout title={post.title}>
  <article class="prose prose-lg mx-auto">
    <h1>{post.title}</h1>
    <time datetime={post.publishedDate}>
      {new Date(post.publishedDate).toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      })}
    </time>
    <div set:html={htmlContent} />
  </article>
</Layout>

You'll notice there's no useState or useEffect here—everything is rendered to HTML before your visitors ever see it.

The Result: Pure HTML

When you run the build command:

npm run build

You'll see something like this in your dist folder:

dist/
├── blog/
│   ├── my-first-post/
│   │   └── index.html
│   └── building-with-astro/
│       └── index.html
└── index.html

These are simple HTML files—no JavaScript bundles, no framework runtime. Your visitors get the content they came for, quickly and reliably.

Adding Interactivity When You Need It

Of course, some parts of a website do need JavaScript. A search feature, a theme toggle, or a contact form might require some interactivity.

Astro handles this thoughtfully with a concept called Islands. The idea is that your page is mostly static HTML, with small "islands" of interactivity only where they're actually needed.

---
import ThemeToggle from '../components/ThemeToggle.jsx';
import ArticleContent from '../components/ArticleContent.astro';
---

<!-- This renders as static HTML -->
<ArticleContent />

<!-- This component receives JavaScript -->
<ThemeToggle client:load />

The client:load directive tells Astro that this particular component needs to be interactive.

You have several options depending on your needs:

  • client:visible — Loads JavaScript when the component scrolls into view
  • client:idle — Loads JavaScript when the browser has spare capacity
  • client:media — Loads JavaScript based on screen size or other media queries

This approach means most of your site stays fast and lightweight, with JavaScript only where it genuinely improves the experience.

Performance Benefits

The difference can be significant. A typical content site built with a traditional JavaScript framework might have:

  • First Contentful Paint around 1.5–2.5 seconds
  • Hundreds of kilobytes of JavaScript to parse
  • Time spent hydrating components that don't need interactivity

With Astro's approach:

  • Pages load as fast as the HTML can be delivered
  • No JavaScript parsing or hydration overhead for static content
  • Better scores on Core Web Vitals

The exact numbers depend on your specific site, but the principle holds: shipping less JavaScript means faster load times.

When This Approach Works Well

We want to be clear that this isn't the right solution for every project. Here's how we think about it:

Astro works well for:

  • Blogs and documentation sites
  • Marketing and landing pages
  • Portfolios
  • E-commerce product pages
  • Any site where content is the focus

You might want a different approach for:

  • Highly interactive applications (dashboards, editors, collaborative tools)
  • Apps where every page is personalized for each user
  • Single-page applications with complex client-side routing

For interactive applications, frameworks like React or Vue are still excellent choices. It's about matching the tool to what you're building.

Getting Started

If you'd like to try this approach, here's a quick summary:

  1. Create a new project with npm create astro@latest
  2. Connect your preferred CMS (Contentful, Sanity, Strapi, or others)
  3. Use getStaticPaths() to fetch content at build time
  4. Deploy to any static hosting service (Netlify, Vercel, Cloudflare Pages)

The Astro documentation is thorough and well-written—it's a great place to start.

Closing Thoughts

Over time, websites have become increasingly complex. Sometimes that complexity is necessary, but often it isn't—especially for content-focused sites.

Astro offers a thoughtful alternative: build with modern tools, but send only what visitors actually need. For content-focused sites, it's worth considering.

Thanks for reading. If you found this helpful or have questions, feel free to reach out.

Tell us about your project

Our location

  • Berlin
    Germany