Creating a blog has never been easier, whether that is a standalone blog or an accompaniment to a product. Creating a blog alongside a product is a great way to advertise. It will improve your domain authority and drive traffic through Search Engine Optimisation.
Additionally, it will allow interact with your audience, which is crucial when starting out. They may arrive at your site for a blog post and then find the product. Therefore the blog becomes another product in itself, diversifying your online offering.
This tutorial will be utilizing Next.js App Router (14.2.1), alongside the Material UI component library that has already been installed. Each post will be written in Markdown with accompanying metadata to be used elsewhere.
Firstly, we are going to create a new blog folder, which will be our route for all of the blog pages. Alongside a layout that will be used for all of the nested pages.
This layout declares the metadata titles with placeholder suffixes. Also, the container layout that all blog-related pages will live within.
Let's start by installing the necessary packages that are required.
Package | Use |
---|---|
gray-matter | for loading any metadata from the markdown post |
next-mdx-remote | Loading the content from a mdx file |
remarkGfm | for adding Github compatible markdown functionality |
rehype-autolink-headings | Auto linking for the blog post headings |
remark-toc | Ability to create a table of contents |
rehype-slug | Creates the URL slug from the post filename |
All of the posts will be written in Markdown and stored in a folder named Posts in the root directory of the application.
Next, we need to load all of the posts from the Post folder. We created a BlogService that loads all posts and individual posts based on a slug.
We use FS to load all files from the Post folder, then we use gray-matter per file to load the metadata and content for each post. This loading returns the content, the metadata, and the slug which is dictated by the filename.
The metadata contains a published date value, not only is this surfaced on the index and the post itself, but this allows for scheduling of posts. The lists of posts are filtered by this value so only posts in the past appear to the client.
We use React’s Cache function, as this data is very static, so we can reduce the compute needed to gain each post. The frequency of this return changing is only if a post was created, updated or deleted.
Now onto creating the index itself. We use the getPosts method to load all possible current posts. Then it's a simple rendering of the post titles, some additional metadata and along with the slug as the URL.
Now we add a method to load a single post to the BlogService. This getPost essentially sits upon the aforementioned getPosts which just finds the single post based on the slug.
We need to create a new route named [slug] that allows us to have a dynamic slug where we can access the value.
We then pass this dynamic value to getPost to load the content. For instance http://localhost:3000/blog/initial-technical-architecture would have a value of initial-technical-architecture. Should a null object be returned then we return a NotFound result.
Then rendering some of the metadata at the top including the author and published date.
We then pass the content to the MDXRemote which renders the content for us.
As we are using Material UI as our component library we want a consistent theme throughout the blog also. We pass the components to the MDXRemote rendering.
We specify the overrides where we declare the mapping between the HTML elements and the Material UI components that will override them.
As this is primarily a technical blog, diagrams are a must. I am a big fan of Mermaid. It allows you to create simple diagrams of different types. GitHub even allows you to render these diagrams, so they’re familiar to the development world.
Lets create a component to render a Mermaid diagram. This is because the diagrams must be rendered only on the client side, rather than the rest of content which is rendered on the server side.
Then we use the Dynamic function to load this component on the client side. Then add the component itself to further the previously created mapping, so that any Mermaid charts are replaced by a Mermaid component.
1- posts // new folder created for markdown
2- public
3- src
4 - app
5package.json
6next.config.json
7.env
1- posts // new folder created for markdown
2- public
3- src
4 - app
5 - blog
6 - [slug] // new folder
7 page.tsx // new page
8 layout.tsx
9 page.tsx
10package.json
11next.config.json
12.env
1
2import * as React from 'react';
3import Container from '@mui/material/Container';
4
5export const metadata = {
6 title: {
7 template: '%s | BetaBud - Blog',
8 default: 'BetaBud - Blog',
9 },
10};
11
12export default function BlogLayout({
13 children }: { children: React.ReactNode }) {
14 return (
15 <Container
16 component="main"
17 maxWidth='sm'>
18 {children}
19 </Container>
20 );
21}
1
2import matter from 'gray-matter';
3import path from 'path';
4import fs from 'fs';
5import { cache } from 'react';
6
7export const getPosts = cache(() => {
8 const postsDirectory = path.join(process.cwd(), 'posts');
9
10 const files = fs.readdirSync(path.join(postsDirectory));
11
12 const currentDate = new Date();
13
14 return files.map(filename => {
15
16 const fileContent = fs.readFileSync(
17 path.join(postsDirectory, filename), 'utf-8');
18
19 const { data, content } = matter(fileContent);
20
21 return {
22 meta: data,
23 slug: filename.replace('.mdx', ''),
24 content: content
25 };
26 }).filter(item => {
27 const itemDate = new Date(item.meta["publishedDate"]);
28
29 return itemDate <= currentDate;
30 });
31});
1
2export default function BlogHome() {
3 const blogs = getPosts();
4
5 return (
6 <Container maxWidth="sm" sx={{ overflow: 'auto' }}>
7 <Typography variant='h2'>
8 BetaBud Blog Articles
9 </Typography>
10 <Stack sx={{ my: 4 }} spacing={2}>
11 {blogs.map(blog => (
12 <Link href={'/blog/' + blog.slug} key={blog.slug} underline='none'>
13 <Card>
14 <CardHeader title={blog.meta.title}
15 subheader={`${blog.meta.author} - ${blog.meta.publishedDate}`} />
16 <CardContent sx={{pt: 0}}>
17 {blog.meta.description}
18 </CardContent>
19 </Card>
20 </Link>
21 ))}
22 </Stack>
23 </Container>
24 )
25}
1// further addtion
2
3export const getPost = (slug: string) => {
4 const posts = getPosts();
5 return posts.find((post) => post.slug === slug);
6}
1
2export async function generateMetadata({
3 params,
4}): Promise<Metadata> {
5 const post = getPost(params.slug);
6 return {
7 title: post?.meta?.title,
8 description: post?.meta?.title,
9 };
10}
11
12
13export default async function BlogPage({ params }) {
14 const post = getPost(params.slug);
15
16 if (!post)
17 return notFound();
18
19 return (<>
20 <NoSsr>
21 <Typography variant='subtitle2'>
22 {post?.meta?.author} - {new Date(post?.meta?.publishedDate).toLocaleDateString()}
23 </Typography>
24 </NoSsr>
25 <Link href='/blog' underline='none'>Blog Home</Link>
26 <MDXRemote
27 source={post?.content}
28 options={{
29 mdxOptions: {
30 remarkPlugins: [
31 remarkGfm,
32 remarkToc
33 ],
34 rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
35 },
36 }}
37 components={mdxComponents}
38 />
39 <Link href='/blog' underline='none'>Blog Home</Link>
40 </>
41 );
42}
1
2export const mdxComponents: MDXComponents = {
3 h1: ({ children }) => <Typography variant='h3' component='h1'>{children}</Typography>,
4 h2: ({ children }) => <Typography variant='h4' component='h2'>{children}</Typography>,
5 h3: ({ children }) => <Typography variant='h5' component='h3'>{children}</Typography>,
6 h4: ({ children }) => <Typography variant='h6' component='h4'>{children}</Typography>,
7 p: ({ children }) => <Typography variant='body1' sx={{ py: 1 }}>{children}</Typography>,
8 table: ({ children }) => <TableContainer component={Paper} sx={{ my: 3 }}><Table>{children}</Table></TableContainer>,
9 thead: ({ children }) => <TableHead>{children}</TableHead>,
10 tr: ({ children }) => <TableRow>{children}</TableRow>,
11 th: ({ children }) => <TableCell>{children}</TableCell>,
12 tbody: ({ children }) => <TableBody>{children}</TableBody>,
13 td: ({ children }) => <TableCell>{children}</TableCell>,
14 code: ({children}) => <code style={{display: 'inline-block', width: '100%', overflowX: 'auto'}}>{children}</code>
15}
1
2'use client'
3import React, { useEffect } from "react";
4import mermaid from "mermaid";
5
6mermaid.initialize({
7 startOnLoad: true,
8});
9
10type MermaidProps = {
11 readonly chart: string;
12};
13
14const Mermaid = ({ chart }: MermaidProps): JSX.Element => {
15 useEffect(() => mermaid.contentLoaded(), []);
16
17 return <div className="mermaid">{chart}</div>;
18};
19
20export default Mermaid;
1
2import dynamic from "next/dynamic";
3const Mermaid = dynamic(() => import("@/components/Mermaid"), {
4 ssr: false,
5});
6
7export const mdxComponents: MDXComponents = {
8 // from above
9 Mermaid: (props) => <Paper sx={{ textAlign: 'center', my: 3 }}><Mermaid {...props} /></Paper>
10}