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:
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 thehref
attribute if needed.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.
Be the first to comment.