Skip to content

Building a Blog Site with Astro and Sanity

Building a Blog Site with Astro and Sanity

Today, I'm going to walk you through how I built this blog site.

Our tech stack will include Astro, a highly performant static site generator and Sanity - a headless CMS.

Let's dive right in!

Step 1: Set up Sanity Studio

Sanity Studio is a web interface for Sanity that we will use to manage your blog’s content.

Create a new Sanity Studio project:

bash Copy Code
npm create sanity@latest -- --template blog --create-project "Blogster" --dataset production

Feel free to replace "Blogster" with your own unique project name.

Launch your Sanity Studio at localhost:3333 with the following command:

bash Copy Code
npm run dev

Step 2: Add Markdown Support for Sanity Studio

Markdown is a simple and user-friendly way of formatting content, making writing and editing blog posts easier. Let's enable Markdown support for Sanity Studio.

In your terminal, navigate to the Sanity project directory and install the required plugins:

bash Copy Code
npm install --save sanity-plugin-markdown easymde@2

Update the sanity.config.ts file to enable Markdown support:

typescript Copy Code
// sanity.config.ts
import { markdownSchema } from "sanity-plugin-markdown";

export default defineConfig({
	// ...
	plugins: [
		markdownSchema(),
	] 
})

Inside the schemas folder, update the post.ts file to use the Markdown editor instead of the rich text editor:

Replace this:

typescript Copy Code
// schemas/post.ts
defineField({
	name: "body",
	title: "Body",
	type: "blockContent",
}),
// ...preview config

With this:

typescript Copy Code
// schemas/post.ts
defineField({
	name: "body",
	title: "Body",
	type: "markdown",
}),
// ...preview config 

With these changes, your Sanity Studio will now support Markdown for writing blog posts.

Step 3: Create a Sample Blog Post

Create a sample blog post. Let's head over to ChatGPT and ask it to write a sample blog post for us.

Here's a prompt to get you started:

text Copy Code
Write me a blog post about [insert topic here] in markdown code

Paste the results from ChatGPT into the body of your blog post.

Make sure you fill out your Title, Description, Slug, Author and Category too.

Step 4: Hosting with Sanity

Run the following command to deploy your Sanity Studio:

bash Copy Code
npx sanity deploy

You will be asked to create a unique hostname. Once deployment is complete, your live Sanity Studio should be accessible from this URL:

https://<unique-hostname>.sanity.studio)

If you haven't done so already, push the project up to a GitHub repository before moving on to the next step.

Step 5: Continuous Deployment of Sanity Studio

Now, let's set up continuous deployment for your Sanity Studio using GitHub Actions. This will automate the process of deploying changes to your studio whenever you push updates to your GitHub repository.

1: Obtain a Sanity API token

  • Head over to your Sanity project's dashboard
  • Click on the API tab in the top-right corner.
  • Select Tokens on the left panel.
  • Scroll down and click Add API Token
  • Give the token a name (e.g., "GitHub Actions Deployment").
  • Select the Deploy Studio (Token only) for permissions.
  • Click Save
  • Copy the generated API token. You'll use this token in the GitHub Actions workflow.

2: Add your Sanity API token to your Github secrets:

  • Go to your GitHub repository
  • Click on Settings > Secrets > New repository secret
  • Name the secret SANITY_API_TOKEN
  • Paste the Sanity API token as the value
  • Click Add secret

3: Set up the GitHub Actions workflow:

  • In your Sanity project folder, create a new directory named .github/workflows.
  • Inside the .github/workflows directory, create a YAML file and name it sanity-deploy.yml
  • Open the sanity-deploy.yml file in your code editor and add the following content:
yaml Copy Code
name: Deploy Sanity Studio
on:
  push:
    branches: [main]
jobs:
  sanity-deploy:
    runs-on: ubuntu-latest
    name: Deploy Sanity
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Install, build, and upload your site
        uses: withastro/action@v0
      - name: Install dependencies
        run: yarn install
      - name: Deploy Sanity Studio
        run: |
          npx @sanity/cli deploy
        env:
          SANITY_API_TOKEN: ${{ secrets.SANITY_API_TOKEN }}

4: Commit and push your changes:

  • Save the sanity-deploy.yml file in your code editor.
  • Commit the new file to your GitHub repository and push the changes.

After pushing the changes, go to the Actions tab in your GitHub repository. You should see the workflow Deploy Sanity Studio running.

By following these steps, your Sanity Studio will now be automatically deployed whenever you push updates to your GitHub repository. This saves you time and effort, ensuring that your live content studio is always in sync with your pushed changes in GitHub.

Step 6: Setting up the Astro Project

Now that we have our content ready in Sanity, it's time to set up the Astro project for our blog site. To speed things along, let's use a pre-built Astro template

*special thanks to flexdinesh for creating this beautiful template

Create your Astro project:

bash Copy Code
npx create-blogster@latest --theme newspaper

cd into your project folder and start the development server:

bash Copy Code
cd my-blogster-blog && npm run dev

Step 7: Integrating Sanity with Astro

Now, we'll integrate Sanity with your Astro project to fetch blog posts and display them on your site.

  • Create a new folder called sanity inside src/lib. We're going to add two files:client.ts and image.ts:
typescript Copy Code
// src/lib/sanity/client.ts
import { createClient } from "@sanity/client";

export const client = createClient({
  projectId: "YOUR PROJECT ID",
  dataset: "production",
  apiVersion: "2023-03-20",
  useCdn: true,
});

Replace projectId with the value from your own Sanity project.

typescript Copy Code
// src/lib/sanity/image.ts
import imageUrlBuilder from "@sanity/image-url";
import { client } from "../sanity/client";

const builder = imageUrlBuilder(client);

export function urlFor(source) {
  return builder.image(source);
}
  • Finally, create a folder called api at src/lib/sanity and add the file post.api.ts:
typescript Copy Code
// src/lib/sanity/api/post.api.ts
import groq from "groq";
import { client } from "../client";

// Define the Post interface
interface Post {
  _id: string;
  title: string;
  description: string;
  mainImage: {
    _type: "image";
    asset: {
      _ref: string;
      _type: "reference";
    };
  };
  publishedAt: string;
  author: {
    _ref: string;
    _type: "reference";
  };
  _createdAt: string;
  _rev: string;
  _type: "post";
  _updatedAt: string;
  slug: {
    current: string;
    _type: "slug";
  };
  categories: Array<{
    _ref: string;
    _type: "reference";
    _key: string;
  }>;
  body: string;
}

// Fetch all blog posts
export async function getPosts(): Promise<Post[]> {
  return await client.fetch(
    groq`*[_type == "post" && defined(slug.current)]{..., categories[]->{title}} | order(publishedAt desc)`
  );
}

// Fetch a specific blog post by slug
export async function getPost(slug: string): Promise<Post> {
  return await client.fetch(
    groq`*[_type == "post" && slug.current == $slug][0]`,
    {
      slug,
    }
  );
}

// Fetch only specific fields for all blog posts
export async function getFilteredPosts<T extends keyof Post>(
  ...fields: T[]
): Promise<Pick<Post, T>[]> {
  const fieldSelection = fields.join(",");

  return await client.fetch(
    groq`*[_type == "post" && defined(slug.current)]{${fieldSelection}}`
  );
}

Step 8: Fetch Data in Astro

Astro, like Next.js, follows a paged-based routing system. This means any page or folder inside src/pages represents a unique route (e.g. creating a blog folder insrc/pages creates a route accessible at localhost:3000/blog)

Create a blog folder inside src/pages and add an index.astro file:

astro Copy Code
// src/pages/blog/index.astro
---
import PageMeta from "../../components/PageMeta.astro";
import PageLayout from "../../layouts/PageLayout.astro";
import { getFilteredPosts } from "../../lib/sanity/api/post.api";
import { SITE_TITLE } from "../../config";

const last_posts = await getFilteredPosts("title", "slug", "publishedAt");
---

<PageLayout>
  <PageMeta title={`Blog | ${SITE_TITLE}`} slot="meta" />
  <section slot="main">
    <ul>
      {
        last_posts.map((post) => {
          const formattedDate = new Date(post.publishedAt).toLocaleDateString(
            "en-us",
            {
              year: "numeric",
              month: "short",
              day: "numeric",
            }
          );
          return (
            <li class="grid grid-cols-[1fr] md:grid-cols-[1fr_auto] mb-3 md:gap-2 items-start">
              <div class="title">
                <a
                  href={`blog/${post.slug.current}`}
                  class="unset hover:text-text-link"
                >
                  <span>{post.title}</span>
                  <span>
                    <i class="ml-1 mr-1 text-[12px] pb-2 fa-solid fa-up-right-from-square" />
                  </span>
                </a>
              </div>
              <div class="text-text-muted text-sm italic pt-1">
                <time datetime={new Date(post.publishedAt).toISOString()}>
                  {formattedDate}
                </time>
              </div>
            </li>
          );
        })
      }
    </ul>
  </section>
</PageLayout>

This file serves as the entry point for your blog route. We're using it to display a paginated list of blog titles.

Now let's create a dynamic route to display each individual blog post. Each blog post will be fetched by its slug.

astro Copy Code
// src/pages/blog/[slug].astro
---
import matter from "gray-matter";
import Renderer from "src/components/Renderer.astro";
import { parseAndTransform } from "src/lib/markdoc/read";
import BlogPostMeta from "../../components/BlogPostMeta.astro";
import ContentLayout from "../../layouts/ContentLayout.astro";
import { getFilteredPosts, getPost } from "../../lib/sanity/api/post.api";
import { urlFor } from "../../lib/sanity/image";

export async function getStaticPaths() {
  const posts = await getFilteredPosts("slug");
  return posts.map((post) => ({
    params: { slug: post.slug.current },
  }));
}

const { slug } = Astro.params;

if (typeof slug !== "string") {
  throw Error(`slug should be string. Received: ${slug}`);
}

const post = await getPost(slug);

const { content } = matter(post.body);
const transformedContent = await parseAndTransform({ content });
---

<ContentLayout
  title={post.title}
  date={post.publishedAt}
  image={urlFor(post.mainImage).url()}
>
  <BlogPostMeta
    title={post.title}
    description={post.description}
    publishDate={post.publishedAt}
    pagePath={`/blog/${slug}`}
    slot="meta"
  />
  <Renderer content={transformedContent} slot="content" />
</ContentLayout>

Now launch your development environment.

Navigate to localhost:3000/blog and click on one of the blog titles. You should be directed to localhost:3000/blog/[slug] where you will be able to read the individual blog post.

Pretty neat stuff.

Step 9: Deploy to Vercel

Vercel is a cloud platform designed to host modern web applications, and it works well with static site generators like Astro. Deploying a website through the platform is a straightforward process.

  1. Head over to the Vercel and sign in or create an account if you don't have one.
  2. Click the Add New... menu.
  3. Select Project and import your Git repository containing the Astro project.

Vercel will automatically detect the project settings and start the deployment process. Once the deployment is complete, Vercel will provide you with a unique URL for your blog site.

Congratulations! You've successfully built a blog site with Astro and Sanity.

Happy blogging!