Files for my website
bwc9876.dev
1---
2title: Welcome to my blog!
3date: 2023-10-15
4summary: How I built my blog
5cowsay: Hello World!
6---
7
8import CowSay from "@components/blog/CowSay.astro";
9
10## Hey there
11
12I decided to make this blog as a way to track my progress in learning new things.
13I hope you enjoy your stay!
14
15This first post is going into a bit of detail about how I made this blog.
16
17## Making The Basic Blog
18
19Astro has a wonderful feature called the [content framework](https://docs.astro.build/en/guides/content-collections/), an extremely powerful way to easily
20create many pages with simple markdown and some frontmatter.
21
22First thing you have to to do is create a folder called `content` in the src directory.
23I already had some content setup because of the projects parts of this site.
24
25Inside the content folder you place a `config.ts` which will contain the schemas
26for your content's frontmatter. I'll just focus on my blog posts for now.
27
28```ts
29const blogPostsCollection = defineCollection({
30 schema: z.object({
31 title: z.string(),
32 date: z.date(),
33 summary: z.string(),
34 cowsay: z.string()
35 })
36});
37```
38
39This contains the metadata each blog post will need to have in order for my site
40to render it.
41
42Then, we export an object named `collections` which Astro will pick
43up and generate TS bindings for.
44
45```ts
46export const collections = {
47 posts: blogPostsCollection
48};
49```
50
51Now we can get to writing some content! Make a folder with the
52same name as the _key_ of the collection you want to write for. In this case, `posts`.
53
54Then create a markdown file and start writing! Here's a little excerpt
55of what [this page looks like](https://github.com/Bwc9876/portfolio-site/tree/main/src/content/posts/hello_world.mdx):
56
57```md
58---
59title: Welcome to my blog!
60date: 2023-10-15
61summary: How I built my blog.
62cowsay: Hello World!
63---
64
65## Hey there
66```
67
68The frontmatter is the part between the `---` and `---`. This is where you put
69metadata for the post. The `cowsay` field is a special one that I made up. It
70changes what the cow says in the header of the page. I'll get to that later.
71
72Now that we have some content, we can start writing some code to render it!
73
74I start off by making a `blog` folder in the `src/pages` directory. This is where
75all my blog related pages will go. I then make a `index.astro` file which will
76be a directory of all posts on the site.
77
78```astro
79---
80import Layout from "@layouts/Layout.astro";
81import { getCollection } from "astro:content";
82
83const blogEntries = await getCollection("posts");
84---
85
86<Layout title="The Cowsay - Ben C's Blog">
87 <h1>The Cowsay - Ben C's Blog</h1>
88 <p>Here you'll find my blog posts, most recent first</p>
89 {
90 blogEntries.map((p, i) => (
91 <>
92 {i === 0 && <hr />}
93 <hgroup>
94 <h2>
95 <a href={`/blog/posts/${p.id}`}>{p.data.title}</a>
96 </h2>
97 <h3>
98 {p.data.date.toLocaleDateString("en-us", {
99 weekday: "long",
100 year: "numeric",
101 month: "short",
102 day: "numeric"
103 })}
104 </h3>
105 </hgroup>
106 <p>
107 {p.data.summary} <a href={`/blog/posts/${p.id}`}>Read More</a>
108 </p>
109 <hr />
110 </>
111 ))
112 }
113</Layout>
114```
115
116Great! 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.
117To make my URLs look nice I'm going to create a subfolder within `blog` called `posts` and then place a `[...id].astro` in there.
118This will allow me to use `getStaticPaths()` to define the paths for each post.
119
120```astro
121---
122import Layout from "@layouts/Layout.astro";
123import { CollectionEntry, getCollection } from "astro:content";
124export const getStaticPaths = async () => {
125 const posts = await getCollection("posts");
126 return posts.map((entry) => ({
127 params: { slug: entry.id },
128 props: { entry }
129 }));
130};
131
132const { entry } = Astro.props as { entry: CollectionEntry<"posts"> };
133const { Content } = await entry.render();
134---
135
136<Layout title={entry.data.title} description={entry.data.summary}>
137 <h1>{entry.data.title}</h1>
138 <Content />
139</Layout>
140
141<style is:global>
142 main img {
143 border: solid 1px var(--text) !important;
144 border-radius: 5px;
145 }
146</style>
147```
148
149Amazing! I'll spare you the image this time since... you're... look at it. But now we have a blog post page!
150Now 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.
151
152## An Outline
153
154Now 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.
155This should be pretty easy to do. Astro automatically parses all the headers for us and lets us access them in the `entry` object.
156
157First we need to grab the headings from when we rendered the page:
158
159```js
160const { Content, headings } = await entry.render();
161```
162
163Then we need to map those to HTML
164
165```astro
166<div class="toc">
167 <!-- Extra div so we can make it sticky -->
168 <div>
169 <span>On This Page</span>
170 <ul>
171 {
172 headings.map((h) => (
173 <li>
174 <a href={`#${h.id}`}>{h.text}</a>
175 </li>
176 ))
177 }
178 </ul>
179 </div>
180</div>
181```
182
183Finally some simple styles and layout
184
185```astro
186<style>
187 /** Wrapper is going around everything to make the
188 table of contents appear on the right of the page **/
189 div.wrapper {
190 display: flex;
191 flex-direction: row;
192 gap: 4rem;
193 }
194
195 div.toc {
196 width: 100%;
197 }
198
199 div.toc div {
200 top: 5rem;
201 margin-top: 1rem;
202 position: sticky;
203 }
204
205 div.toc ul li {
206 list-style-type: none;
207 }
208</style>
209```
210
211Finally, 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.
212
213```css
214div.wrapper {
215 display: flex;
216 flex-direction: column-reverse;
217 gap: 1rem;
218}
219
220@media (min-width: 1200px) {
221 div.wrapper {
222 flex-direction: row;
223 gap: 4rem;
224 }
225}
226```
227
228## The Cowsay
229
230Now 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.
231I'll also provide a CowSay component that will render the cow and the text. This way I can use it MDX for admonitions.
232
233First I need to make a component that will render the cow. I'm going to use the [cowsay](https://www.npmjs.com/package/cowsay) package to do this.
234
235```astro
236---
237import * as cowsay from "cowsay";
238
239type Props = {
240 color?: "warn" | "info";
241} & cowsay.IOptions;
242
243const { color, ...cowOptions } = Astro.props;
244
245const cowText = cowsay.say(cowOptions);
246---
247
248<pre class={color}>
249{cowText}
250</pre>
251
252<style>
253 pre {
254 padding: 1rem;
255 }
256
257 pre.warn {
258 color: yellow;
259 background-color: rgb(25, 25, 0);
260 }
261
262 pre.info {
263 color: cyan;
264 background-color: rgb(0, 25, 25);
265 }
266</style>
267```
268
269Now I can link it up in my blog post page!
270
271```astro
272<CowSay text={entry.data.cowsay} />
273```
274
275Et 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.
276
277I can also use it for admonitions in MDX!
278
279```astro
280<CowSay color="warn" e="><" text="Warning!" />
281<CowSay color="info" e="^^" T="U" text="Info!" />
282```
283
284<CowSay color="warn" e="><" text="Warning!" />
285<CowSay color="info" e="^^" T="U" text="Info!" />
286
287## Conclusion
288
289I'm really happy with how this blog turned out. I'm going to keep working on it and adding new features as I go.
290I'm also going to try and write more posts in the future. I hope you enjoyed this one!
291
292<CowSay e="^^" text="Adiós!" />