Skip to content

Hashnode & Astro

Hashnode is a hosted CMS that allows you to create a blog or publication.

The Hashnode Public API is a GraphQL API that allows you to interact with Hashnode. This guide uses graphql-request, a minimal GraphQL client that works well with Astro, to bring your Hashnode data into your Astro project.

To get started you will need to have the following:

  1. An Astro project - If you don’t have an Astro project yet, our Installation guide will get you up and running in no time.

  2. A Hashnode site - You can create free personal site by visiting Hashnode.

Install the graphql-request package using the package manager of your choice:

Terminal window
npm install graphql-request

Making a blog with Astro and Hashnode

Section titled Making a blog with Astro and Hashnode

This guide uses graphql-request, a minimal GraphQL client that works well with Astro, to bring your Hashnode data into your Astro project.

  1. A Hashnode Blog
  2. An Astro project integrated with the graphql-request package installed.

This example will create an index page that lists posts with links to dynamically-generated individual post pages.

  1. To fetch your site’s data with the graphql-request package, make a src/lib directory and create two new files client.ts & schema.ts:

    • Directorysrc/
      • Directorylib/
        • client.ts
        • schema.ts
      • Directorypages/
        • index.astro
    • astro.config.mjs
    • package.json
  2. Initialize an API instance with the GraphQLClient using the URL from your Hashnode Website.

    src/lib/client.ts
    import { gql, GraphQLClient } from "graphql-request";
    import type { AllPostsData, PostData } from "./schema";
    export const getClient = () => {
    return new GraphQLClient("https://gql.hashnode.com")
    }
    const myHashnodeURL = "astroplayground.hashnode.dev";
    export const getAllPosts = async () => {
    const client = getClient();
    const allPosts = await client.request<AllPostsData>(
    gql`
    query allPosts {
    publication(host: "${myHashnodeURL}") {
    title
    posts(first: 20) {
    pageInfo{
    hasNextPage
    endCursor
    }
    edges {
    node {
    author{
    name
    profilePicture
    }
    title
    subtitle
    brief
    slug
    coverImage {
    url
    }
    tags {
    name
    slug
    }
    publishedAt
    readTimeInMinutes
    }
    }
    }
    }
    }
    `
    );
    return allPosts;
    };
    export const getPost = async (slug: string) => {
    const client = getClient();
    const data = await client.request<PostData>(
    gql`
    query postDetails($slug: String!) {
    publication(host: "${myHashnodeURL}") {
    post(slug: $slug) {
    author{
    name
    profilePicture
    }
    publishedAt
    title
    subtitle
    readTimeInMinutes
    content{
    html
    }
    tags {
    name
    slug
    }
    coverImage {
    url
    }
    }
    }
    }
    `,
    { slug: slug }
    );
    return data.publication.post;
    };
  3. Configure schema.ts to define the shape of the data returned from the Hashnode API.

    src/lib/schema.ts
    import { z } from "astro/zod";
    export const PostSchema = z.object({
    author: z.object({
    name: z.string(),
    profilePicture: z.string(),
    }),
    publishedAt: z.string(),
    title: z.string(),
    subtitle: z.string(),
    brief: z.string(),
    slug: z.string(),
    readTimeInMinutes: z.number(),
    content: z.object({
    html: z.string(),
    }),
    tags: z.array(z.object({
    name: z.string(),
    slug: z.string(),
    })),
    coverImage: z.object({
    url: z.string(),
    }),
    })
    export const AllPostsDataSchema = z.object({
    publication: z.object({
    title: z.string(),
    posts: z.object({
    pageInfo: z.object({
    hasNextPage: z.boolean(),
    endCursor: z.string(),
    }),
    edges: z.array(z.object({
    node: PostSchema,
    })),
    }),
    }),
    })
    export const PostDataSchema = z.object({
    publication: z.object({
    title: z.string(),
    post: PostSchema,
    }),
    })
    export type Post = z.infer<typeof PostSchema>
    export type AllPostsData = z.infer<typeof AllPostsDataSchema>
    export type PostData = z.infer<typeof PostDataSchema>

Fetching via getAllPosts() returns an array of objects containing the properties for each post such as:

  • title - the title of the post
  • brief - the HTML rendering of the content of the post
  • coverImage.url - the source URL of the featured image of the post
  • slug - the slug of the post

Use the posts array returned from the fetch to display a list of blog posts on the page.

src/pages/index.astro
---
import { getAllPosts } from '../lib/client';
const data = await getAllPosts();
const allPosts = data.publication.posts.edges;
---
<html lang="en">
<head>
<title>Astro + Hashnode</title>
</head>
<body>
{
allPosts.map((post) => (
<div>
<h2>{post.node.title}</h2>
<p>{post.node.brief}</p>
<img src={post.node.coverImage.url} alt={post.node.title} />
<a href={`/post/${post.node.slug}`}>Read more</a>
</div>
))
}
</body>
</html>
  1. Create the page src/pages/post/[slug].astro to dynamically generate a page for each post.

    • Directorysrc/
    • Directorylib/
      • client.ts
      • schema.ts
      • Directorypages/
        • index.astro
        • Directorypost/
          • [slug].astro
    • astro.config.mjs
    • package.json
  2. Import and use getAllPosts() and getPost() to fetch the data from Hashnode and generate individual page routes for each post.

    src/pages/post/[slug].astro
    ---
    import { getAllPosts, getPost } from '../../lib/client';
    export async function getStaticPaths() {
    const data = await getAllPosts();
    const allPosts = data.publication.posts.edges;
    return allPosts.map((post) => {
    return {
    params: { slug: post.node.slug },
    }
    })
    }
    const { slug } = Astro.params;
    const post = await getPost(slug);
    ---
  3. Create the template for each page using the properties of each post object. The example below shows the post title and reading time, then the full post content:

    src/pages/post/[slug].astro
    ---
    import { getAllPosts, getPost } from '../../lib/client';
    export async function getStaticPaths() {
    const data = await getAllPosts();
    const allPosts = data.publication.posts.edges;
    return allPosts.map((post) => {
    return {
    params: { slug: post.node.slug },
    }
    })
    }
    const { slug } = Astro.params;
    const post = await getPost(slug);
    ---
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <title>{post.title}</title>
    </head>
    <body>
    <img src={post.coverImage.url} alt={post.title} />
    <h1>{post.title}</h1>
    <p>{post.readTimeInMinutes} min read</p>
    <Fragment set:html={post.content.html} />
    </body>
    </html>

To deploy your site visit our deployment guide and follow the instructions for your preferred hosting provider.

More CMS guides

Contribute

What’s on your mind?

Create GitHub Issue

Quickest way to alert our team of a problem.

Community