How to Build a Blog

Note: Although this image is AI-generated, the rest of this post is not.
December 30, 20237 minute readprogramming · react · deno · fresh
Motivation
Let's say you've always wanted to build your own website. Maybe you're interested in learning React. Perhaps you're tired of complicated Node JS web frameworks and don't want to spend an exorbitant amount of time learning one on top of React. Possibly you're a bit sick of Node JS in general.
I have good news! Many other people feel the same way. Even the creator of Node. So much so, that he's created a new JavaScript (and TypeScript) runtime called Deno. It fixes many of the things about Node JS that probably leave a bitter taste in your mouth. Such as:
- out-of-the-box TypeScript support
- built-in formatting
- built-in linting
- top-level async/await
- no node_modules/
- ECMAScript Module support (no CommonJS 😎)
- ...
These same people, who are tired of cumbersome Node JS web frameworks, have created some slick, minimal, web frameworks for it. One such framework is Fresh. If you're thinking: "No! Not another web framework!", hear me out. Fresh is great because right out of the box it has:
- no build step
- no configuration
- speed
- simplicity
Fresh does all of those things and more. It's so clean and simple that it "makes web development fun again." Deno and Fresh complement each other like sunshine and beaches.
The cherry on top is that Deno and Fresh have both been designed for deployment on Deno Deploy, which is a serverless hosting service with zero config. Serverless hosting means that a service is only running when it is needed. It also means that it's very cheap to run and better for the environment! This site is currently hosted on it for free.
Implementation
Maybe I've piqued your interest, but designing a blog still seems like a daunting task. Again, I have great news! We can keep it very simple with these design requirements:
- posts are written in Markdown
- posts are added by committing them to your project
- no additional code needs to be written to publish each post
These might sound pretty basic, but it's important to start somewhere, and starting simple is the best way to ensure something gets done. Another popular tactic is to use a reference when building something new for the first time. It turns out, that Fresh's docs are designed this same way, so we can just refer to its code! 😀
Rendering Markdown
To display a post on our site we'll need to convert (render) each markdown file into HTML. There happens to be a page in the Fresh docs about rendering Markdown.
Let's start by building a component for rendering Markdown into HTML so that we can use it anywhere on our site:
// components/Markdown.tsx
import { CSS, render } from "$gfm/mod.ts";
export type MarkdownProps = {
markdown: string;
};
export default (props: MarkdownProps) => (
<>
<Head>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
</Head>
<div
data-color-mode="auto"
data-light-theme="light"
data-dark-theme="dark"
class="markdown-body"
dangerouslySetInnerHTML={{ __html: render(props.markdown) }}
/>
</>
);
TODO
- You may want to add properties to MarkdownProps for things such as render
options, custom styles, and classes. I had some trouble getting the rendered
markdown to look right with the
CSS
fromgfm
, so I created a CSS file to patch some of the styles. You can see it here.
Creating a Dynamic Route for Posts
Now that we have a component that can be used to display Markdown, we need to
use it to render our posts. The Fresh docs explain that we can use a
dynamic route to
render a page using a part of the path. For example, if someone goes to
www.oursite.com/posts/title-of-a-post
, then we can load the corresponding post
and use it to render the page.
Let's go ahead and create a file for a dynamic route like mentioned above. The Fresh docs give us a basic example that we can use to get started, so let's begin with something similar:
// routes/posts/[name].tsx
import { PageProps } from "$fresh/server.ts";
export default (props: PageProps) => (
<>
{props.params.name}
</>
);
This gives us a route that just responds with whatever path we put after
/posts
.
Handling Post Requests
What do we need next? Since the name
param represents a path to a post, we
should use it to find a corresponding Markdown file, and render the post if it
exists, or a 404 page if it doesn't. The docs explain that we can create a
handler to
fetch data before
rendering a page.
Following the examples in the docs, let's go ahead and create a handler for our page and use it to pass data to the page component:
// routes/posts/[name].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import Markdown from "../../components/Markdown.tsx";
// A map of post paths to Markdown. We'll leave it empty for now
const postMap = new Map<string, string>();
export const handler: Handlers<string> = {
GET: (_, ctx) => {
const { name } = ctx.params;
// Check if the post exists
const post = postMap.get(name);
if (post === undefined) { // Render a 404 page if it doesn't
return ctx.renderNotFound();
}
return ctx.render(post); // Render a post page if it does
},
};
// Let's modify our render function to use the Markdown component with
// the Markdown passed from our handler.
export default (props: PageProps<string>) => (
<>
<Markdown markdown={props.data} />
</>
);
TODO
- You will probably want to add more to this page. See the Post Frontmatter section below to learn about adding things like a title, date, or tags to each post.
- Fresh provides default error pages, but if you would like to customize them be sure to check out the docs here.
Loading Posts from a Directory
Next, we need to load the posts we've written into postMap
. Let's use Deno to
read our Markdown files from a directory. I would also recommend creating a new
file to hold this code so that routes/posts/[name].tsx
doesn't get too
cluttered.
// utils/posts.ts
import { basename, extname } from "$std/path/mod.ts";
// Let's move postMap here
export const postMap = new Map<string, string>();
// Let's create a constant for the directory to our posts so that if it
// ever needs to be changed, we can do it in one location.
const POSTS_DIR_PATH = "./static/posts/";
// Load the posts from the file system
for await (const dirEntry of Deno.readDir(POSTS_DIR_PATH)) {
const { name: filename } = dirEntry;
if (dirEntry.isFile && filename.endsWith(".md")) {
const postPath = `${POSTS_DIR_PATH}${filename}`;
const fileContents = await Deno.readTextFile(postPath);
// We'll use the basename of each filename as the key in the postMap
const postBasename = basename(filename, extname(filename));
postMap.set(postBasename, fileContents);
}
}
Now we have nearly everything we need! I'd recommend creating some test posts to see how they look.
Post Frontmatter
Although we can render posts, we don't have a great way to browse through them or display each one uniformly. In other words, right now we don't have a way to display information like the post title, date, tags, etc. in a common location.
One way to accomplish this is to add parsable
frontmatter to the
beginning of each Markdown file. The Deno std library supports Frontmatter and
is used by Fresh for extracting metadata on each page in the docs. You can check
out the docs for the front_matter
module
here.
You can refer to my
utils/posts.ts
to see how I extract frontmatter from Markdown files. It involves just a few
modifications to the example code above.
Improvements
I hope you've found this guide useful! It should help you get started with Fresh and understand the basic design of a blog. You will need to make some important improvements such as setting up styles and adding things like a home page and a list of posts. You can take a look at the code of my blog, or if you like it, you can fork it and customize it to your needs.
A few things I plan on adding to this site later are:
- comments on posts
- likes on posts
- a way to search posts
- post thumbnails