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!
Sanity Studio is a web interface for Sanity that we will use to manage your blog’s content.
Create a new Sanity Studio project:
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:
npm run dev
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:
npm install --save sanity-plugin-markdown easymde@2
Update the sanity.config.ts
file to enable Markdown support:
// 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:
// schemas/post.ts
defineField({
name: "body",
title: "Body",
type: "blockContent",
}),
// ...preview config
With this:
// 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.
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:
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.
Run the following command to deploy your Sanity Studio:
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.
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 itsanity-deploy.yml
- Open the
sanity-deploy.yml
file in your code editor and add the following content:
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.
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:
npx create-blogster@latest --theme newspaper
cd
into your project folder and start the development server:
cd my-blogster-blog && npm run dev
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
insidesrc/lib
. We're going to add two files:client.ts
andimage.ts
:
// 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.
// 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
atsrc/lib/sanity
and add the filepost.api.ts
:
// 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}}`
);
}
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:
// 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.
// 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.
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.
- Head over to the Vercel and sign in or create an account if you don't have one.
- Click the
Add New...
menu. - 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!