Steal This Blog! Part 3: Partial Components and Reusable UI

An overview of the seven most used components from this theme, with full source code of each component, and discussion of their most interesting features.

This post is part three of the Steal This Blog! Series.
Steal This Blog! Part 3: Partial Components and Reusable UI

While the Piano Pronto Blog isn’t primarily a tech blog, I still find it’s a good place for me to share some of the tech we use at Piano Pronto Publishing, Inc. In this article series, I aim to document the codebase that powers this blog, and some of the possibly unique things I’ve done with Astro framework. View the demo.



Table of Contents:


💎 Introduction

Back in part one of the Steal This Blog! Series, I had gone over layout and post components, but there’s a third type of component that I didn’t mention, partial components. In this article I’ll go over a few of the partial components I’ve created in this starter theme in effort to increase reusability.


🏷️ Tag.astro

---
interface Props {
  term?: string;
  text: string;
  count?: number;
  link?: boolean;
}

import { slug as slugify } from "github-slugger";

const { term = "tags", text, count, link = false } = Astro.props;
const slug = slugify(text);
---

{
  link ? (
    <a href={`/${term}/${slug}/`} class="tag">
      {text}
      {count !== void 0 && <span>{count}</span>}
    </a>
  ) : (
    <div class="tag">
      {text}
      {count !== void 0 && <span>{count}</span>}
    </div>
  )
}

We’re creating a blog, so we definitely need a tag component. The most obvious prop above is text, which is what we are going to display in the tag, i.e. “Popular” or “Guides”. We also have a count prop, which can either be undefined or a number. If it is defined, we’ll display the count in a small badge next to the text.

You’ll notice that we have a prop called term. If you’ve read the previous article, you’ll recall that we created an abstracted page component to let us filter by many different fields. So in this tag component, I wanted to also support many different terms. By default though, I’ve set it to tags since that’s how we’ll use it most often.

Finally, we have a link prop. I didn’t want the tags to be clickable everywhere, so I gave the option to render it as a <div> or <a> tag. Notice that we need to slugify the text, since we might want to display something like “New Posts”, but need to link to /tags/new-posts/.

Source:


⌚ ReadingTime.astro

---
interface Props {
  post: Object;
}

import * as consts from "@/consts";
import readingTime from "reading-time";

const { post } = Astro.props;
const { text } = readingTime(post.body, { wordsPerMinute: consts.READING_WORDS_PER_MINUTE });
---

<span>{text}</span>

Another must have for a blog, this component is very simple because we make use of the reading-time package. Just pass this component the post object as a prop, and it will return a human friendly estimation of the reading time, such as “5 min read”.

Notice that we import the READING_WORDS_PER_MINUTE value from our src/consts.ts file. So if you want to tweak the estimated reading time, go ahead and change the value there.

Source:


📅 FormattedDate.astro

---
interface Props {
  date: Date;
}

const { date } = Astro.props;
---

<time datetime={date.toISOString()}>
  {
    date.toLocaleDateString("en-us", {
      year: "numeric",
      month: "short",
      day: "numeric",
    })
  }
</time>

This component actually comes from the Official Blog Astro Starter Kit, but it’s worth talking about here. This component leverages the fact that we’re sure we have a Date object when fetching posts, so we can easily use the toLocaleDateString method. Recall from our Zod schema, the pubDate field can accept a string value, but is always transformed to a Date object:

const blog = defineCollection({
  schema: ({ image }) =>
    z
      .object({
        // ...
        pubDate: z
          .union([z.string(), z.date()])
          .transform((val) => new Date(val)),
        updatedDate: z
          .union([z.string(), z.date()])
          .optional()
          .transform((val) => new Date(val)),
        // ...
      })
});

A possible future enhancement could be for us to define the locale in src/consts.ts and import it here, as to not hardcode en-us.

Source:


💭 Blurb.astro

---
interface Props {
  href?: string;
  image: Promise<object> | object;
  title: string;
  rounded?: boolean;
}

import { Image } from "astro:assets";

if (Astro.props.image instanceof Promise) {
  Astro.props.image = (await Astro.props.image).default;
}

const { href, image, title, rounded } = Astro.props;

const Element = href ? "a" : "div";
const props = href ? { href } : {};
---

<div class="not-prose blurb">
  <Element {...props}>
    <div>
      <Image
        src={image}
        width="150"
        alt={title}
        class:list={[rounded && "rounded-full"]}
      />
    </div>
    <div>
      <h4 class="blurb__title">{title}</h4>
      <slot />
    </div>
  </Element>
</div>

This little component lets us create uniform sections of text next to an image. You can see an example of it at the top of this page with the “Fork me on GitHub” section, or in the src/content/blog/steal-this-blog.mdx page in the theme.

There are some self-explanatory props, such as title which controls the text in the <h4> and rounded which makes the image a circle. However, there’s a couple of interesting things going on here, too:

  1. We can optionally make it a link by passing the href prop. We use Astro’s dynamic tags feature in order to render either a <div> or <a> tag, along with the href attribute if needed.

  2. The image prop can be either an object, or a promise, which makes its use a bit cleaner:

// import() return a promise, so we can use:
<Blurb image={import("./image.png")}>

// instead of:
import myImage from "./image.png";
<Blurb image={myImage}>

💡 Tip: You can use this component in any MDX file under the src/content/blog directory. That’s because we inject it into Astro’s <Content /> component in the src/components/partials/Content.astro file. We’ll be diving deeper into MDX components in a future post.

💡 Tip: There’s also some responsive design, so make sure to check out the CSS file. I’ve made it so that on extra small screens, the image is set to float: left instead of displayed as a flex row.

Source:


👤 AboutAuthor.astro

---
interface Props {
  author: string | object;
}

import { Image } from "astro:assets";
import { getAuthor } from "@/content/utils.js";
import placeholder from "@/assets/authors/default.png";

const author =
  typeof Astro.props.author === "string"
    ? await getAuthor(Astro.props.author)
    : Astro.props.author;
const title = Astro.props.title ?? `About ${author.name}`;
---

<div class="about-author not-prose">
  <div>
    {
      author.data.avatar ? (
        <Image src={author.data.avatar} alt={author.data.name} />
      ) : (
        <Image src={placeholder} alt={author.data.name} />
      )
    }
  </div>

  <div>
    {title && <div class="about-author__name">{title}</div>}
    <div>
      {author.data.bio}
    </div>
  </div>
</div>

Last but not least, we have a component to display an author’s picture and bio. It’s similar to the Blurb.astro component, but tailored for one specific purpose. We can optionally override the title prop, otherwise it will by default output “About Houston”. If we didn’t give the author an image, we’ll use a placeholder.

The one interesting feature is that we accept either an author object that’s been loaded with getEntry(), or just the author’s slug. Since we’ve already loaded the author in the src/components/post/Page.astro component, this allows us to avoid loading it twice.


💡 Conclusion

These were just a few components that I found useful when creating this starter theme. There’s some more that I’ve used on the Piano Pronto Blog, and I plan to add those in a future article when we talk about MDX components.

Even if you’re not using this starter theme to build your blog, perhaps you can use some of these components to make your life a bit easier.



Justin Beaty
Written by Justin Beaty
As the sole developer at Piano Pronto, Justin is a master of bringing life to our website and ensuring it's the best possible experience of shopping for sheet music online. In a world of algorithms and APIs, Justin is our digital maestro, orchestrating every line of code to perfection. He's a transplant to Southern California from the East Coast, and never complains about the weather.

Comments

User's avatar


No comments yet
Be the first to comment.

Steal This Blog! Part 2: Filtering Collections by Tags, Authors, and More
Nerd Corner

Steal This Blog! Part 2: Filtering Collections by Tags, Authors, and More

Creating a reusable way to filter blog posts and other content collections by various fields in Astro with pagination and meta tags.

  • 17 min read
Read more
Steal This Blog! Part 1: Overview, Content Collections, Pages, and Components
Nerd Corner

Steal This Blog! Part 1: Overview, Content Collections, Pages, and Components

Recreating Ghost's Casper theme for Astro, with support for tags, author pages, scheduled posts, tailwind, and more.

  • 17 min read
Read more
Christmas Cheer: Volume 1 Songbook
Christmas Songbooks

Christmas Cheer: Volume 1 Songbook

Light up your holiday season and delight audiences with ten thoughtfully arranged intermediate solos by Jennifer Eklund. Includes favorites like "Christmas Time Is Here," "River (It's Comin' on Christmas)," "Wonderful Christmastime," and "Last Christmas."

  • 6 min read
Read more