Discuss your project

Building a Slick Blog with Supabase, React, Astro, and Cloudflare – Part 4: Creating the Frontend with React and Tailwind

/* by Tirth Bodawala - August 17, 2024 */

Welcome to Part 4 of our blog-building series! With the backend and admin interface all set up, it’s time to bring your blog to life with a beautiful and responsive frontend. In this part, we’ll focus on displaying blog posts using Astro, React, and Tailwind CSS. You’ll learn how to set up Tailwind, create the necessary components for listing and viewing posts, and connect everything to Supabase.

Table of Contents

  1. Initializing Tailwind CSS
  2. Creating the Blog Layout
  3. Building the Blog List and Post Components
  4. Setting Up the Homepage
  5. Creating the Blog Post Route

1. Initializing Tailwind CSS

First, we need to set up Tailwind CSS to style our blog.

Step 1: Initialize Tailwind CSS

Run the following command to initialize Tailwind CSS in your project:

npx tailwindcss init

Step 2: Create the Global CSS File

Create a global CSS file at src/styles/global.css and add the following content:

@tailwind base;
@tailwind components;
@tailwind utilities;

This file imports the base styles, components, and utilities provided by Tailwind.

Step 3: Update the Tailwind Configuration

Update your tailwind.config.js file with the following configuration:

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{js,jsx,ts,tsx,astro}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

This configuration ensures that Tailwind scans all relevant files for class names to include in the final CSS.

Step 4: Ensure Tailwind is Integrated in Astro

Make sure your astro.config.mjs includes the following setup:

import { defineConfig } from 'astro/config';

import react from "@astrojs/react";
import cloudflare from "@astrojs/cloudflare";
import tailwind from '@astrojs/tailwind';

// https://astro.build/config
export default defineConfig({
  integrations: [react(), tailwind()],
  output: "server",
  adapter: cloudflare()
});

This setup integrates React, Tailwind CSS, and Cloudflare Workers with your Astro project.


2. Creating the Blog Layout

Next, let’s create a basic layout that we can use across different pages of the blog.

Step 1: Create the Layout File

Create a new file src/layouts/BaseLayout.astro and add the following content:

---
const { title } = Astro.props;
---
<html lang="en" class="min-h-screen">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{title ?? 'Quick blog with Supabase'}</title>
  </head>
  <body class="min-h-screen">
    <h1>{title}</h1>
    <article>
      <slot /> <!-- your content is injected here -->
    </article>
    <footer class="sticky top-[100vh] text-center">
      <p class="text-grey">
        Made with ❤️ by <a href="https://www.atyantik.com">Atyantik Technologies</a>
      </p>
    </footer>
  </body>
</html>

This layout provides a consistent structure across your blog pages, including a header, content area, and footer.


3. Building the Blog List and Post Components

Let’s create the components that will display individual blog posts and the list of posts.

Step 1: Create the PostItem Component

This component will be used to display individual blog posts. Create src/components/PostItem.tsx:

import dayjs from "dayjs";

interface IPostItem {
  title: string;
  slug: string;
  unique_id: string;
  excerpt: string;
  publish_date: string;
  content: string;
  featured_images: { src: string; title: string }[];
}

export const PostItem = (props: IPostItem) => {
  const publishDate = dayjs(props.publish_date);
  const publishDateTime = publishDate.format("MMMM D, YYYY");
  return (
    <div>
      <div className="flex text-left max-w-4xl flex-col mx-auto">
        <div className="mx-auto">
          <a
            href="/"
            className="block my-4 text-xl leading-7 text-indigo-600"
          >
            ← Go Back
          </a>
        </div>
      </div>
      <img
        style={{ maxHeight: "60dvh" }}
        src={props.featured_images?.[0]?.src ?? ""}
        alt={props.featured_images?.[0]?.title ?? props.title}
        className="w-full object-cover object-top"
      />
      <div className="flex justify-center max-w-4xl flex-col mx-auto">
        <div className="mx-auto">
          <div>
            <p className="mt-3 text-xl font-semibold leading-7 text-indigo-600">
              {publishDateTime}
            </p>
            <h1 className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
              {props.title}
            </h1>
            <div
              className="mt-6 text-xl leading-8 text-gray-700"
              dangerouslySetInnerHTML={{
                __html: props.content,
              }}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

This component displays a single blog post, including its title, publication date, content, and the featured image.

Step 2: Create the PostListItem Component

This component will display each post in a list format on the homepage. Create src/components/PostListItem.tsx:

import dayjs from "dayjs";

interface IPostListItem {
  title: string;
  slug: string;
  unique_id: string;
  excerpt: string;
  publish_date: string;
  featured_images: { src: string; title: string }[];
};

export const PostListItem = (props: IPostListItem) => {
  const publishDate = dayjs(props.publish_date);
  const publishDateTime = publishDate.format("MMMM D, YYYY");
  return (
    <article className="flex max-w-xl flex-col items-start justify-between">
      <div className="group relative">
        <a href={`/${props.slug}-${props.unique_id}/`}>
          <img
            className="h-40 w-full object-cover object-top rounded-md"
            src={props.featured_images?.[0]?.src ?? ""}
            alt={props.featured_images?.[0]?.title ?? ""}
          />
        </a>
        <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
          <a href={`/${props.slug}-${props.unique_id}/`}>
            <span className="absolute inset-0"></span>
            {props.title}
          </a>
        </h3>
        <div className="flex items-center gap-x-4 text-xs">
          <time dateTime={props.publish_date} className="text-gray-500">
            {publishDateTime}
          </time>
        </div>
        <p className="mt-5 line-clamp-3 text-sm leading-6 text-gray-600">
          {props.excerpt}
        </p>
      </div>
    </article>
  );
};

This component lists each blog post with a thumbnail image, title, and publication date.


4. Setting Up the Homepage

Now that we have our components, let’s set up the homepage to list all the blog posts.

Step 1: Create the Homepage File

Create the homepage file at src/pages/index.astro:

---
import BaseLayout from "../layouts/BaseLayout.astro";
import { PostListItem } from "../components/PostListItem";

import { supabaseClient } from "../utils/supabase";
const { data } = await supabaseClient
  .from("posts")
  .select("title, slug, unique_id, publish_date, featured_images, excerpt");
---

<BaseLayout>
  <div class="bg-white py-24 sm:py-32">
    <div class="mx-auto max-w-7xl px-6 lg:px-8">
      <div class="mx-auto max-w-2xl lg:mx-0">
        <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
          The Supabase blog
        </h2>
      </div>
      <div
        class="mx-auto mt-10 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 border-t border-gray-200 pt-10 sm:mt-16 sm:pt-16 lg:mx-0 lg:max-w-none lg:grid-cols-3"
      >
        {(data ?? []).map((post) => <PostListItem {...post} />)}
      </div>
    </div>
  </div>
</BaseLayout>

This page will display all blog posts in a grid format using the PostListItem component.


5. Creating the Blog Post Route

Finally, let’s create the route to display individual blog posts.

Step 1: Create the Blog Route

Create the blog route at src/pages/[...slug].astro:

---
import BaseLayout from "../layouts/BaseLayout.astro";
import { PostItem } from "../components/PostItem";
import { supabaseClient } from "../utils/supabase";

const { slug } = Astro.params;
const uniqueId = slug?.split("-")?.pop();
if (!slug || !uniqueId) {
  return Astro.redirect("/404");
}

const { data } = await supabaseClient
  .from("posts")
  .select("title, slug, unique_id, publish_date, content, featured_images, excerpt")
  .eq("unique_id", uniqueId)
  .single();

if (!data?.title) {
  return Astro.redirect("/404");
}
---

<BaseLayout>
  <PostItem {...data} />
</BaseLayout>

This route will dynamically render individual blog posts based on their slug and unique ID.


Wrapping Up Part 4

Congratulations! You’ve now built the frontend for your blog using Astro, React, and Tailwind CSS. With these components and pages in place, your blog is fully functional and ready for visitors. In the next part, we’ll focus on deploying your blog with Cloudflare, ensuring it’s fast, secure, and globally accessible.


Next Up: Part 5 – Deploying Your Blog with Cloudflare

With the frontend complete, it’s time to make your blog live. We’ll guide you through deploying your Astro blog with Cloudflare in the next part of this series!


Further Learning

For more on building with Astro, React, and Tailwind CSS, check out the Astro Documentation and Tailwind CSS Documentation.