MachineLogs.IO

Why it works on my machine

Functional Paper Integrations — Dev Log 2

The second Dev Log for Functional Paper. We discuss the integrations with other tools, or with Astro features directly, that power various functionalities. We will also analyze the remaining source code that makes use of them.

Building Blocks; Photo by Karl Abuid, Unsplash
Building Blocks; Photo by Karl Abuid, Unsplash

Notice: The analysis is done on the v1.3 release of Functional Paper. Some things may change in later releases. The entire source code for the release is available on GitHub.

This is the second part of a series that takes a deep dive into Functional Paper, an Astro theme I developed. I used it as the basis for MachineLogs.IO. You can check out the prior posts in the series:

Giscus

Giscus is the tool used for the comments section. In the back, it uses GitHub Discussions to create the comments thread.

Before going into the implementation details, I want to explain why I went with this integration instead of creating my own comments section for a few reasons. Firstly, it provided a quick and easy way to get it done. Secondly, by using GitHub authentication, it adds some legitimacy to the comments section by having users with history on other websites. This also enables me to leverage the GitHub Discussion features to do the necessary moderation.

Although the GitHub account creates a barrier of entry, and maybe some of you would rather not support GitHub, at this point in the life of the blog, I think this will not have a big impact.

Looking forward, I’ll revisit this topic and look for possible improvements, other available tools, or building from scratch my comments section.

Going back to the Giscus integration in Functional Paper, the source code for the comments section can be found in Comments.astro. The logic is inside an inline script. The loadGiscus function was created using the straightforward setup instructions provided by Giscus on their page. lazy is set for the data-loading attribute to improve the user experience, as there is no need to load the comments until the user reaches that part of the page. For the discussions repo attributes, I chose to use environment variables rather than hard-code connection-specific values.

Besides the loadGiscus function, the additional code is dedicated to applying the correct theme to Giscus and to switching it to the correct theme when the user changes it. The event listener created at line 47 is needed to trigger the initial creation of the Giscus comments section. The astro:page-load event is triggered after the new page is visible to the user. The getTheme function, as the name suggests, retrieves the current set theme option to also apply it to the comments section. In case no option is saved, the fallback is the light theme.

The second case is when the user changes the theme while on a page with a comments section. At line 61 the event listener created is on the custom event dispatched by clicking on the theme toggle. It will trigger a reload of the Giscus comments section with the new theme applied, so it matches the overall site theme.

Pagefind

While doing research for setting up a search functionality for my theme, I came across pagefind. It is a static search library that boasts with the capability to run a full-text search on a 10,000 page site with a total payload under 300kB.

It is installed using npm and requires the built website static files to generate its search indexes. I’ve changed the default npm run build command to also run pagefind as you can see in the code. It adds the necessary scripts to the output directory and generates the search indexes.

Additionally, I’ve added the devindex command. Because npm run dev does not use the dist/ directory, the search functionality is broken in dev mode. The workaround is to copy the files generated by build. When you update content on a page while in dev mode, the search index will not update, a website rebuild and a devindex are needed for this. Moreover, while in dev mode, the images in the post preview in the search results cannot be displayed as the resources used are the ones optimized by Astro at built time, unavailable in dev mode. To fully test search functionality, you need build and run the website in preview mode.

Markdown & MDX

Astro comes with support by default for Markdown. If you haven’t worked before with Markdown, it is a markup language designed to be writable in a plain-text editor. It is popular among bloggers and is used by companies such as GitHub and Reddit. For the former, it is used to format comments and Readme files and for the latter, it is used to format posts and comments.

Besides the user visible content, in Astro, Markdown files use a special section at the start between --- delimiters. This is called the frontmatter. It stores important metadata about the post. For Functional Paper, that is the post title, author, post description, the author, the highlight image used in the post cards, the image alt that also serves as a caption, and finally the tags.

Although simplicity has its appeal, there are cases where you require a bit more options. Here comes MDX, an extension of Markdown that lets you write JSX in your Markdown files. For Astro, this translates into the ability to insert Astro components in your MDX files.

The distinction at first glance between Markdown and MDX files is made by using their respective file extensions. .md for Markdown and .mdx for MDX are the established file extensions that designate each markup language.

The process to add MDX to Astro is straightforward, as it is an officially supported integration. The command to run is:

npx astro add mdx

Then in astro.config.ts the integration needs to be applied to the config.

In Functional Paper, MDX is used to replace the default rendered HTML <img> element with the custom component PostImage.astro. As described in Dev Log 1, this enables the build-time conversion of images to AVIF format and srcset creation for serving optimal-sized images to clients. I also decided to use the alt property as the <figcaption> too. For this use case, the role overlaps, so there is no need to duplicate the text.

Additionally, I’ve created a post in the Functional Paper demo to showcase the various formatting options. You can compare it to the source code to see how each section is rendered.

Tailwind CSS

It is an open-source CSS framework that offers pre-built utility classes to style your elements. From my experience, this accelerates the styling process a lot compared to using vanilla CSS. And instead of having to work with fixed components, as is the case with other CSS frameworks, you have more flexibility to build your own. However, this comes with added complexity, so it’s a decision that should be made on a per-project basis. If you want to go with UI components, there is also the Tailwind UI framework, created by the same team.

Adding Tailwind CSS is easy using the official integration. To add it to the project, you first install it like this:

npx astro tailwind

Then you import the module in astro.config.ts and add it to the config. You also need a tailwind config file named tailwind.config.mjs which requires this line to configure the file paths.

In tailwind.config.mjs I defined custom colors to make them addressable by name. This can be done like this:

theme: {
	extend: {
		colors: {
		    'custom-color': '#01010101',
		},
	},
}

The styling was done in two ways. Firstly, using a global.css file in the styles/ directory. These stylings are shared across all pages, and I made sure the file is loaded by including it in the BaseLayout.astro, which is common to all website pages. Secondly, by applying component-specific styling, for example in ‘Header.astro’. This is only needed for elements in that specific component, so it’s better to keep them within for code clarity, maintainability, and to avoid loading unneeded styling.

It’s important to note that everything mentioned above applies to every element except the Markdown/MDX rendered content. For styling that part, we have the next integration.

Tailwind Typography

To style rendered Markdown, or MDX in this case, we need to use the Tailwind Typography plugin. Astro provides a guide for this integration too, available here. You install the package with npm:

npm install -D @tailwindcss/typography

Then you declare the plugin in the Tailwind config:

plugins: [
	require('@tailwindcss/typography'),
]

To apply the typography styling to the Markdown/MDX content, a wrapper Astro component is required. In Functional Paper, this is the role of Prose.astro. The component is used in [...slug].astro that generates the posts. It wraps the Content component, which is the rendered Markdown/MDX post, within prose and prose-dark classes. You can see the code here. The styling is configured in the Tailwind config file.

Astro Content Collections

In Astro, the best way to manage your posts is by using Content Collections. These enable you to do frontmatter validation, create queries with filters, and generate routes. A detailed documentation from Astro is available on their website.

To create your Content Collection, first you need to add your collection directory in src/content/, for example posts. The next step is to define a frontmatter structure. As described earlier, in Functional Paper, there are a few properties present that hold the important metadata used across the site. The formal definition of the schema is done in the config.ts file, in the parent directory. By using TypeScript, type checking is enforced for each post frontmatter against the defined schema, preventing some incomplete production pushes.

Those were all the prerequisites for creating the content collection. For usage, we will look at each of the 4 cases where the posts Content Collection is accessed.

1. /posts

The first use case is in src/pages/posts/[...slug].astro. By looking at the path, we can see that Astro will generate a /posts/ route which will be filled with dynamically generated pages at build time. [...slug] means a placeholder which will be filled with the slug of every entry used in that component.

The list of pages that are generated is determined by the return value of the function getStaticPaths(), which looks like this for us. Inside, we call getCollection('posts') to get the posts collection. The return of getStaticPaths() is a list of pairs of params and props. params holds the slug for the page name; as we don’t define a custom one in our frontmatter, this will be the filename of the post. props holds the entire collection information.

The post frontmatter, for which we did all those prerequisites, is in entry.data and we pass it to the PostLayout template to be used in the page generation.

To generate the page content from the Markdown/MDX data, we need to call the render() method of the entry. This will be wrapped by the Prose component to have the Tailwind Typography styling applied.

2. /blog

We start with a difference right away. Instead of [...slug].astro, we have a file called [...page].astro. The reason is that instead of having named pages, we have numbered pages, for example: /blog, /blog/2, /blog/3 etc. Important to note is that [...page].astro does not create the number 1 page. Instead, it acts like index.html. If you want to explicitly have the number 1 page, you need to use the filename [page].astro. All these features are possible by using the built-in pagination feature offered by Astro.

For the blog section of the site, we want to have the posts ordered chronologically, from the latest to the oldest. Therefore, the first thing we do to the posts collection is to sort it by the pubDate property in the frontmatter.

The next step is to apply the pagination. The props we receive when calling paginate are not a single entry as we had in /posts, but instead a list of entries of length equal to pageSize, or less for the last page if the number of posts was not divisible by the chosen number. We use this list to generate the post cards on every page. You can view the code here.

I selected the pagination approach because I find it more functional than an infinite scroll. The infinite scroll on most websites I’ve experienced does not work well when you want to get to old posts. Because of the continuous memory size increase with each lazy loaded batch, the page can start feeling sluggish.

3. /tags

For the tagging system, we add another placeholder in the path, src/pages/tags/[tag]/[...page].astro to create nested pagination. Official documentation is available from Astro on how to implement it.

In addition to what we saw previously, we have to create records with each tag coupled with the posts that have it present; the code is in this section. We go through the list of posts and the tags attached to each one. Each tag has an accumulator where we push the current post where we found it.

Another difference is that there is no longer a global pagination. Instead, pagination is applied per tag. We store this data in the returned object, where we have the tag and its paginated list of related posts. You can look at the processing done here. The tag is put in params to be taken automatically by Astro to populate the path placeholder.

Additionally, there is an index.astro file to create the /tags page. There is no dynamic routing present here, only posts collection processing. On this page, I wanted to provide visual feedback for how many times a tag was used. I count the appearances of each tag, and then scale each element proportionally to its counter.

4. index.html

For the home page, we have a combination of /blog and /tags.

As in /blog, we sort the posts chronologically. But instead of pagination, the most recent post becomes the highlighted post, and we then select a few more to populate this page only. The total size is kept the same as for /blog pages, so the “more posts” linking is done directly to /blog/2.

Similar to /tags main page, we also count tag appearances. This lets us sort them by counter, so we can select the first n to display on the website home page.

Vercel

Vercel is a cloud platform for website hosting. I use it for the demo site of Functional Paper and for MachineLogs.IO. Although I do not have specific settings for Vercel in Functional Paper because the theme is platform-agnostic, I’ll present what I’ve done for MachineLogs.IO.

Astro has an officially documented integration with Vercel. I’ve followed the recommended installation with npx astro add vercel. This integration will change the default build directory from dist/ to .vercel/. Therefore, the build and devindex commands defined in package.json need to be updated to .vercel/output/static.

Using the integration, we gain the possibility to use Vercel Web Analytics to gather insights into website usage. To set it up, I used this guide for static output mode.

Closing Remarks

There are multiple tools, but also built-in features in Astro that I made use of to create Functional Paper. Those we’ll surely change over time.

I hope you learned something and gained some ideas for your projects. The next Dev Log will cover configuration and styling options. See you then!