Logo

I moved my blog from Ghost to Astro

Learn about the benefits of publishing with Astro instead of Ghost

Published: 12 Oct, 2024


I’ve been using Ghost for the past year and recently moved over to Astro.

I was hosting the Ghost instance myself instead of buying their hosted solution. I also wasn’t making use of the newsletter functionality because I don’t have a targeted audience to whom I’m selling products/services. I also didn’t like how difficult it was to customize the theme, and add extra functionality to the site. For example, I had to bring it extra Javascript in the form of PrismJS for syntax highlighting on my code snippets. I didn’t like that.

Astro is pleasant to work with. I write my posts in Markdown, commit my changes to Git, and Cloudflare will automatically build my site and deploy it to Pages. I also get free web analytics. Previously, I was also hosting Plausible on the same server as the Ghost instance. I decided to get rid of the server now that I’m getting both hosting and analytics for free from Cloudflare.

I’ve used static site generators before, namely, Jekyll and Hugo. But I didn’t enjoy working with them because both Jekyll and Hugo are opinionated and difficult to customize. I actually didn’t enjoy working with Hugo at all because the documentation often doesn’t make any sense and the template system is complicated.

Astro on the other hand, is completely flexible because I can write Javascript to build any kind of content system I want. To build the UI itself, I simply use JSX which is fun to work with. Support for MDX is a huge win because I can use components in my Markdown files to add custom UI elements to my posts. I’ve been using it for displaying a “callout” like the one below.

This is a callout

One more killer feature is the API routes concept. I’ve been using it to generate OpenGraph images automatically using the code below:

import { getCollection } from "astro:content";
import { ImageResponse } from "@vercel/og";
import path from "node:path";
import { fileURLToPath } from "url";
import { readFile } from "node:fs/promises";
import roboto from "../assets/fonts/Roboto-Regular.ttf";
import logoImg from "../assets/images/logo.png";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export async function GET({ props }) {
  let robotoSrc;

  if (import.meta.env.NODE_ENV === "development") {
    robotoSrc = path.join(__dirname, "..", "..", "..", roboto);
  } else {
    robotoSrc = path.join(__dirname, "..", "..", "dist", roboto);
  }

  let logoSrc;

  if (import.meta.env.NODE_ENV === "development") {
    logoSrc = path.resolve(logoImg.src.replace("/@fs", "").split("?")[0]);
  } else {
    logoSrc = path.join(__dirname, "..", "..", "dist", logoImg.src);
  }

  const robotoRegular = await readFile(path.resolve(robotoSrc));

  const logo = await readFile(path.resolve(logoSrc));

  const html = {
    type: "div",
    props: {
      tw: "flex justify-center items-center h-full w-full bg-white",
      style: { fontFamily: "Roboto" },
      children: [
        {
          type: "div",
          props: {
            tw: "flex flex-col justify-center items-center flex-wrap",
            children: [
              {
                type: "img",
                props: {
                  tw: "mb-12",
                  src: logo.buffer,
                  width: "200px",
                },
              },
              {
                type: "p",
                props: {
                  style: {
                    fontSize: "48px",
                  },
                  children: "Josh Karamuth",
                },
              },
              {
                type: "p",
                props: {
                  style: { fontSize: "20px" },
                  children:
                    "I write about programming and other related topics.",
                },
              },
              {
                type: "div",
                props: {
                  tw: "rounded px-6 py-4 mt-8 font-bold",
                  style: {
                    backgroundColor: "#a06f4e",
                    color: "white",
                  },
                  children: "Read now",
                },
              },
            ],
          },
        },
      ],
    },
  };

  return new ImageResponse(html, {
    fonts: [
      {
        name: "Roboto",
        data: robotoRegular.buffer,
        weight: 400,
        style: "normal",
      },
    ],
  });
}

export async function getStaticPaths() {
  const blogPosts = await getCollection("blog");
  return blogPosts.map((post) => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

This wasn’t possible with Ghost unless I relied on external APIs.

Ghost has a newsletter feature but if I need the same thing for my Astro site, I can rely on newsletter services like ConvertKit instead because it’s free for up to 10,000 subscribers. Although, I doubt I’ll be starting a newsletter anytime soon unless I see the demand for one.

To handle draft posts, I’m keeping a separate Git branch called draft in addition to the master branch. I also have a boolean named draft in my frontmatter so that I can filter out draft posts when building the site. When I’m done writing, I’ll set draft to false and then merge the draft branch into master to deploy the post to production.

Overall, I’m happy with this transition and will continue plublishing with Astro.


Email me if you have any questions about this post.

Subscribe to the RSS feed to keep updated.