Welcome to my blog!

 ______________
< Hello World! >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Hey there

I decided to make this blog as a way to track my progress in learning new things. I hope you enjoy your stay!

This first post is going into a bit of detail about how I made this blog.

Making The Basic Blog

Astro has a wonderful feature called the content framework, an extremely powerful way to easily create many pages with simple markdown and some frontmatter.

First thing you have to to do is create a folder called content in the src directory. I already had some content setup because of the projects parts of this site.

Inside the content folder you place a config.ts which will contain the schemas for your content’s frontmatter. I’ll just focus on my blog posts for now.

const blogPostsCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z.date(),
    summary: z.string(),
    cowsay: z.string()
  })
});

This contains the metadata each blog post will need to have in order for my site to render it.

Then, we export an object named collections which Astro will pick up and generate TS bindings for.

export const collections = {
  posts: blogPostsCollection
};

Now we can get to writing some content! Make a folder with the same name as the key of the collection you want to write for. In this case, posts.

Then create a markdown file and start writing! Here’s a little excerpt of what this page looks like:

---
title: Welcome to my blog!
date: 2023-10-15
summary: How I built my blog.
cowsay: Hello World!
---

## Hey there

The frontmatter is the part between the --- and ---. This is where you put metadata for the post. The cowsay field is a special one that I made up. It changes what the cow says in the header of the page. I’ll get to that later.

Now that we have some content, we can start writing some code to render it!

I start off by making a blog folder in the src/pages directory. This is where all my blog related pages will go. I then make a index.astro file which will be a directory of all posts on the site.

---
import Layout from "@layouts/Layout.astro";
import { getCollection } from "astro:content";

const blogEntries = await getCollection("posts");
---

<Layout title="The Cowsay - Ben C's Blog">
  <h1>The Cowsay - Ben C's Blog</h1>
  <p>Here you'll find my blog posts, most recent first</p>
  {
    blogEntries.map((p, i) => (
      <>
        {i === 0 && <hr />}
        <hgroup>
          <h2>
            <a href={`/blog/posts/${p.slug}`}>{p.data.title}</a>
          </h2>
          <h3>
            {p.data.date.toLocaleDateString("en-us", {
              weekday: "long",
              year: "numeric",
              month: "short",
              day: "numeric"
            })}
          </h3>
        </hgroup>
        <p>
          {p.data.summary}&nbsp;&nbsp;<a href={`/blog/posts/${p.slug}`}>Read More</a>
        </p>
        <hr />
      </>
    ))
  }
</Layout>

Great! I’ll probably fiddle with it in the future but it’s a good start. Now we need to make a page for each post. To make my URLs look nice I’m going to create a subfolder within blog called posts and then place a [...slug].astro in there. This will allow me to use getStaticPaths() to define the paths for each post.

---
import Layout from "@layouts/Layout.astro";
import { CollectionEntry, getCollection } from "astro:content";
export const getStaticPaths = async () => {
  const posts = await getCollection("posts");
  return posts.map((entry) => ({
    params: { slug: entry.slug },
    props: { entry }
  }));
};

const { entry } = Astro.props as { entry: CollectionEntry<"posts"> };
const { Content } = await entry.render();
---

<Layout title={entry.data.title} description={entry.data.summary}>
  <h1>{entry.data.title}</h1>
  <Content />
</Layout>

<style is:global>
  main img {
    border: solid 1px var(--text) !important;
    border-radius: 5px;
  }
</style>

Amazing! I’ll spare you the image this time since… you’re… look at it. But now we have a blog post page! Now anytime I want to make a new post I just have to make a new markdown file and it’ll be rendered on the site.

An Outline

Now that we have a basic blog, I want to add a few more features to it. First I want to add the ability to see all headers in a post. This should be pretty easy to do. Astro automatically parses all the headers for us and lets us access them in the entry object.

First we need to grab the headings from when we rendered the page:

const { Content, headings } = await entry.render();

Then we need to map those to HTML

<div class="toc">
  <!-- Extra div so we can make it sticky -->
  <div>
    <span>On This Page</span>
    <ul>
      {
        headings.map((h) => (
          <li>
            <a href={`#${h.slug}`}>{h.text}</a>
          </li>
        ))
      }
    </ul>
  </div>
</div>

Finally some simple styles and layout

<style>
  /** Wrapper is going around everything to make the
        table of contents appear on the right of the page **/
  div.wrapper {
    display: flex;
    flex-direction: row;
    gap: 4rem;
  }

  div.toc {
    width: 100%;
  }

  div.toc div {
    top: 5rem;
    margin-top: 1rem;
    position: sticky;
  }

  div.toc ul li {
    list-style-type: none;
  }
</style>

Finally, I want to make sure on smaller screen sizes the table of contents appears at the top rather than the side so it doesn’t take up too much space.

div.wrapper {
  display: flex;
  flex-direction: column-reverse;
  gap: 1rem;
}

@media (min-width: 1200px) {
  div.wrapper {
    flex-direction: row;
    gap: 4rem;
  }
}

The Cowsay

Now for the fun part. I want to make a little cow that says something in the header of each post. I’m going to use the cowsay field in the frontmatter to do this. I’ll also provide a CowSay component that will render the cow and the text. This way I can use it MDX for admonitions.

First I need to make a component that will render the cow. I’m going to use the cowsay package to do this.

---
import * as cowsay from "cowsay";

type Props = {
  color?: "warn" | "info";
} & cowsay.IOptions;

const { color, ...cowOptions } = Astro.props;

const cowText = cowsay.say(cowOptions);
---

<pre class={color}>
{cowText}
</pre>

<style>
  pre {
    padding: 1rem;
  }

  pre.warn {
    color: yellow;
    background-color: rgb(25, 25, 0);
  }

  pre.info {
    color: cyan;
    background-color: rgb(0, 25, 25);
  }
</style>

Now I can link it up in my blog post page!

<CowSay text={entry.data.cowsay} />

Et voila! A cow that says something in the header of each post! I’ll probably make it a bit more fancy in the future but for now it’s good enough.

I can also use it for admonitions in MDX!

<CowSay color="warn" e="><" text="Warning!" />
<CowSay color="info" e="^^" T="U" text="Info!" />
 __________
< Warning! >
 ----------
        \   ^__^
         \  (><)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
 _______
< Info! >
 -------
        \   ^__^
         \  (^^)\_______
            (__)\       )\/\
             U ||----w |
                ||     ||

Conclusion

I’m really happy with how this blog turned out. I’m going to keep working on it and adding new features as I go. I’m also going to try and write more posts in the future. I hope you enjoyed this one!

 ________
< Adiós! >
 --------
        \   ^__^
         \  (^^)\_______
            (__)\       )\/\
                ||----w |
                ||     ||