skip to content

Astro + Ghost + Tailwind CSS

Creating a Blog with Astro, Ghost, and Tailwind CSS

I currently run a company blog on Ghost, and I wanted to try out Astro on my personal blog. The twist is that I wanted to use Ghost as a Headless CMS only for my posts, and override the default ghost theme with Astro.

It's actually what's used to power this blog. Some posts are brought in from ghost. Others (like this one) are written directly for Astro (in MDX).

Ghost Previews

The first step Is showing previews of ghost content and then linking to the original content

Ghost has an sdk https://ghost.org/docs/content-api/javascript/ we can install using:

npm install @tryghost/content-api

Then we can use the sdk to get the posts

---

// index.astro
import GhostContentAPI from '@tryghost/content-api'
const ghost = new GhostContentAPI({
  url: 'https://blog.mysite.com',
  key: 'my-ghost-content-key',
  version: "v5.0"
});



const ghostPosts = await ghost.posts
    .browse({limit: 5, include: 'tags,authors', filter: 'author:andre'})

---
		<ul>

			{
				ghostPosts.map((post) => (
					<li>
						<time {datetime} class="min-w-[120px] text-gray-500">{postDate}</time>
							<Element>
								<a href={post.url} class="inline-block cactus-link line-clamp-1">
									{post.title}
								</a>
							</Element>
							{
								withDesc && (
									<q class="block italic line-clamp-3">{post.excerpt}</q>
								)
							}
					</li>
				))
			}
        </ul>

That's really it if you want to show previews of your ghost content. it will link to the the original content on your ghost blog.

Embedding Ghost Content and styling with tailwind

The html content from ghost comes with classes. We can use tailwind to implement those classes.

All that's needed is a dynamic route and components to render the content.

The Dynamic route:

---
// [slug].astro
import BlogPost from "@/layouts/BlogPost";
import ghost from "@/ghost";

export async function getStaticPaths() {
	const ghostPosts = await ghost.posts
		.browse({ limit: 5, include: "tags,authors", filter: "author:andre" })

	const paths = ghostPosts.map((post) => {
		return {
			params: { slug: post.slug },
			props: { post },
		};
	});
	return paths;
}
const { slug } = Astro.params;
const postContent = await ghost.posts.read(
	{ slug },
	{ formats: ["html", "plaintext"] }
);
const { post } = Astro.props;
const blogContent = { ...post, description: undefined };
---

<BlogPost content={blogContent} headings={post.headings}>
	<Fragment set:html={postContent.html} />
</BlogPost>

Then implement any ghost classes with the tailwind components layer.

/* global.css */
... @layer components {
	.kg-bookmark-container {
		@apply flex;
	}
	.kg-bookmark-content {
		@apply grow;
	}
	.kg-bookmark-thumbnail {
		@apply h-40 flex-initial basis-1/4;
	}
	.kg-bookmark-description {
		@apply hidden;
	}
	.kg-bookmark-metadata {
		@apply flex h-20 w-full;
	}
}

That will accomplish the styling of the ghost content. But what if we want to use a custom component for a specific element?

Embedding Ghost Content w/ remote-astro

remote-astro is a plugin that allows you to render remote html or markdown with custom components. One thing to look out for is that it's just a proof of concept and is not production ready. The biggest issue i've found is some content is not rendered correctly. 🤷🏽‍♂️ So not the best solution at the moment. When it's more stable it could help customize the ghost content even more.

it can be installed with:

npm install -D astro-remote

They dynamic route will then look like this:

---
// [slug].astro
import BlogPost from "@/layouts/BlogPost";
import ghost from "@/ghost";
// https://stackblitz.com/edit/github-scvgee?file=src%2Fpages%2Findex.astro
import { Markup } from "astro-remote";
import { components, sanitize } from "src/components/base/components";

export async function getStaticPaths() {
	const ghostPosts = await ghost.posts
		.browse({ limit: 5, include: "tags,authors", filter: "author:andre" })

	const paths = ghostPosts.map((post) => {
		return {
			params: { slug: post.slug },
			props: { post },
		};
	});
	return paths;
}
const { slug } = Astro.params;
const postContent = await ghost.posts.read(
	{ slug },
	{ formats: ["html", "plaintext"] }
);
const { post } = Astro.props;
const blogContent = { ...post, description: undefined };
---

<BlogPost content={blogContent} headings={post.headings}>
	<Markup content={postContent.html} {sanitize} {components} />
</BlogPost>

The Components:

// components.ts
import Paragraph from "./Paragraph.astro";
import Title from "./Title.astro";
import Span from "./Span.astro";
import A from "./A.astro";
import Div from "./Div.astro";
import Img from "./Img.astro";

export const sanitize = {
	dropElements: ["head"],
	blockElements: ["html", "body"],
};

export const components = {
	h1: Title,
	p: Paragraph,
	span: Span,
	a: A,
	div: Div,
	img: Img,
};

Notice that I import each component so that I can override the default behavior of the components. For example: I could use Astro's Image component to render properly sized images.

---
// Img.astro
import { Image } from "@astrojs/image/components";

const props = Astro.props;
---

<Image src={props.src} alt={props.alt} />

Conclusion

Ideally I would use both methods to style and render my content. For now this is enough for a personal blog. Although I might look into contributing to remote-astro to fix that issue at some point.

That's about it.

Take note that the blog will need to be rebuilt every time a change is made to the ghost content, though that can be automated.

All the code for this blog can be found on the github repo: https://github.com/andr-ec/landing-blog

Someone made a starter project for ghost and astro. Though it doesn't use tailwind. https://github.com/PhilDL/astro-starter-ghost

Thanks for reading.