Today, we're diving into how to create a blog using a combination of HubSpot CMS React, Tailwind CSS, and GraphQL for fetching posts on the Blog Listing Page. Let's walk through the process step by step, illustrating how these technologies work together to build this very blog.
HubSpot released HubSpot CMS React (built atop Vite), which gives us the ability to write React to render on both the server and client. They've included loads of DX improvements (dev server, HMR, TypeScript support, tree shaking and more) that have really ironed out some of the pain points in HubSpot development. Check out their examples to learn more!
For this project, I started with cms-react boilerplate, a starter kit I've made to streamline the development process using HubSpot CMS React. This boilerplate includes optional theme files, serverless function examples and a sensible project structure to make us of Islands, Partials and Modules. Tailwind is included along with `@tailwindcss/typography
` too.
To access our blog post data GraphQL is used in the blog listing template, in place of the built in HubL tag {{ contents
}}. Here's the GraphQL query for this specific blog, created within the theme folder:
// personal_blog_posts.graphql
# label: "Personal Blog Posts"
# description: "Returns published blog posts from personal blog"
# $limit: 10
# $offset: "{{request.query_dict.offset|multiply(10) || 0 }}"
query PersonalBlogInfo($limit: Int!, $offset: Int!) {
BLOG {
post_collection(
filter: { content_group_id__eq: "160568063405", state__eq: "PUBLISHED" }
limit: $limit
offset: $offset
) {
items {
name
post_body
post_summary
slug
url
updated
author_name
}
hasMore
}
}
}
Using GraphQL allows us to easily support pagination and sorting via query string by using dynamic HubL variables like `request.query_dict`. It's easy to build these queries using GraphiQL, try it out in your portal! The query is added in listing page's template notation and the results are passed into a JS Partial `BlogListing.tsx` as props:
// blog-listing.hubl.html
<!--
templateType: "blog_listing"
isAvailableForNewContent: true
label: Blog Listing
dataQueryPath: ./data-queries/personal_blog_posts
-->
{% set blogPosts = data_query.data.BLOG.post_collection.items %}
{% set hasMore = data_query.data.BLOG.post_collection.hasMore %}
{% extends "./layouts/base.hubl.html" %}
{% block body %}
{% js_partial
path="@projects/personal-blog/js-package/components/partials/BlogListing.tsx",
posts={{ blogPosts }},
hasMore={{ hasMore }}
%}
{% endblock body %}
Finally, we can loop through the results and build the post listing using JSX:
// BlogListing.tsx
{props.posts.map((post) => (
<a
className="block py-4 hover:scale-[1.005] scale-100 active:scale-100"
href={post.slug}
>
<article key={post.slug}>
<dl>
<dt className="sr-only">Published on</dt>
<dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
<time>{formatDate(post.updated)}</time>
</dd>
</dl>
<h2 className="text-2xl font-bold leading-8 tracking-tight">
{post.name}
</h2>
<p className="mt-1">
{createExcerpt(post.post_summary, 200)}
</p>
</article>
</a>
))}
The blog post template can incorporate React modules and partials but the post content itself requires the HubL tag `{{ content.post_body }}` to work correctly in the post editor.
In order to use Tailwind within React components, we use PostCSS and a Tailwind config file. Here's an example of those file used in this project.
// postcss.config.mjs
import tailwind from "tailwindcss";
import autoprefixer from "autoprefixer";
import postcssNested from "postcss-nested";
import tailwindConfig from "./tailwind.config.js";
export default {
plugins: [tailwind(tailwindConfig), postcssNested, autoprefixer()],
};
// tailwind.config.js
import { fileURLToPath } from 'url';
import typography from '@tailwindcss/typography';
const componentsDir = fileURLToPath(new URL('./components', import.meta.url));
export default {
content: [`${componentsDir}/**/*.{js,ts,jsx,tsx}`],
theme: {
extend: {
fontFamily: {
serif: ['SpaceGrotesk', 'serif'],
},
},
},
plugins: [typography],
};
As you can see, we need to use `fileURLToPath` to ensure Tailwind is added in the correct location during the build step. Once this has been implemented, the final step is to create a CSS file to import Tailwind base, components and utilities.
/* tailwind.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
Now, `tailwind.css` can be imported in our React modules and partials. It's that easy!
We've been able to see how easy it is to use React modules and partials within HubSpot. It's a massive improvement to be able to leverage third party dependencies from the JS/React ecosystem. We've been able to use server rendering to ensure SEO is maintained, which is important for blogs in particular. GraphQL makes data fetching a breeze. As a long time HubSpot developer, the improvements to DX have been a game changer!
Would you consider building your blog using these new features? If you're looking to chat about these new features, join the conversation in the #cms-react channel within the HubSpot Developer Slack server. Sign up to join the slack server here.