Build an Enhanced Blog Site
Hi there. I guess this might be the first post of this site(except some archives picked from my former blogs). It took a few days to build this site and I can't wait to share the joy I had walking through it.
toc
Why Next.js and MDX
The goal is to build a responsive blog site looks nice on laptop/tablet/mobile phone. If you ask me why, it's for fun. Though most blog sites are being unnecessary complicated, there's no better chance to research and practice semantic HTML and typography than building a blog site from scratch.
First of all, this site is a part of my progressive personal site matrix along with www.somarl.com and moment.somarl.com, it makes sense to use a consistent framework.
And, is GREAT! It covers most of things we had to concern about from coding to deployment.
Then, for blog posts, is SPECTACULAR! You see, we get an svg in a paragraph, with [<MDXIcon />](https://mdxjs.com)
in the source of this post. I considered between markdown and LaTex, but the former lacks ability to insert customized components as the later lacks simple but extensible building tools for Web sites and hard to export to other platforms. I even thoughted about manually writing HTMLs. MDX wiped all my concerns.
What I did to build this site
To work with Next.js, I chose next-mdx-remote as the adapter for MDX posts and build the basic workspace structure under their instructions.
blog
├── posts
│ └── hello.mdx
├── public
│ └── images
│ └── hello
│ └── cover.jpg
└── src
├── components
├── lib
├── styles
└── pages
└── post
└── [slug].tsx
In src/pages/post/[slug].tsx
we build the mdx files in posts
like
import {postFileSlugsSync, serializePost} from 'src/lib'
export default function PostPage ({compiledSource, scope}) {
return (
<article>
<MDXRemote compiledSource={compiledSource} scope={scope} />
</article>
)
}
export const getStaticProps = async ({params: {slug}}) => {
const {compiledSource, scope} = await serializePost(slug)
return {
props: {
compiledSource,
scope,
},
}
}
export const getStaticPaths = async ctx => {
return {
paths: postFileSlugsSync.map(slug => ({
params: {
slug,
},
})),
fallback: false,
}
}
It makes everthing work. We enhance it from here. First of all, style the post rendered.
Let's first check some details of the basic components supported in markdown. How do they look like and what I did with them.
Headings
I added the heading anchor like Github with the help of remark-slug and remark-autolink-headings.
Try hover on the headings if you are viewing this post by a device with a pointer on screen and the anchors will show up.
h2
h3
h4
h5
h6
And let's just try and see how does a very loooooooooooooooooooooooooooooooong heading looks like
Inline texts
There are emphasis, strong and strikethrough.
And we have ABBR.
Thematic break
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
Code and Syntax Highlighting
Both code block and inline code use the background color as Github markdown code blocks.
The issue was about how to implement syntax highlighting. There are choices like highlightjs
and prism
but I chose a syntax highlighter called "shiki".
The reason is simple, it does syntax parsing and apply TextMate grammar like text editors(VS Code, for example), that's what I call "highlighting". A simple thing explains what does it mean - it highlights mdx
files without any extra configurations.
The only thing I needed to do is to find a proper remark plugin, it turned out to be stefanprobst/remark-shiki with a little patches.
Code block
// Sample javascript code
var s = "JavaScript syntax highlighting";
alert(s);
# Sample python code
s = "Python syntax highlighting"
print(s)
We will see much more code blocks in this post, let's leave it here.
Inline code
Inline code
looks like this.
List
Simply style them and then
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
- Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Task lists included.
- nested unordered lists
- ordered lists
- task lists
- nested task lists
Some thing worth mentioning is that MDX render task list item into
<li><input type="checkbox" checked disabled /> task lists</li>
The problem is disabled
, I don't like its grey accent color in most browsers ;(
So I overrided the customized component like, though readOnly
does not make any sense here :)
export default function PostLi ({children}: IProps) {
if (Array.isArray(children)) {
const [checkbox, ...restChildren] = children
if (isValidElement(checkbox) && checkbox.props.type === 'checkbox') return (
<li>
<input type="checkbox" checked={checkbox.props.checked} readOnly />
{restChildren}
</li>
)
}
return <li>{children}</li>
}
Yet another interesting thing to metion is that task lists selected as ul.contains-task-list
was the only class name used through out the whole site style sheets. I overrided it as ul[role="listbox"]
to avoid this.
Images
The image syntax in markdown is like ![alt](src)
or ![alt](src "title")
. I'd like to display the title as caption if present.
On default, MDX transform ![alt](src "title")
to
<img src={src} alt={alt} title={title} />
Which means the title is almost not working, even Google spiders ignore title
attribute in images. So, I decided to use the figure
element to render images which has figcaption
suitable for the title.
Then I need to override the default component in MDXRemote
with a custom PostImage
react component like
export default function PostImage ({src, alt, title}) {
return (
<figure>
<img src={src} alt={alt} title={title} />
<figcaption>{title}</figcaption>
</figure>
)
}
And pass them to the MDXRemote
like
import PostImage from 'src/components/PostImage'
const components = {
img: PostImage
}
<MDXRemote compiledSource={compiledSource} scope={scope} components={components} />
Unfortunately, content in mdx like below
![Placeholder image](https://picsum.photos/800/400)
![Image with caption](https://picsum.photos/500/300 "Image with caption")
will be rendered into
<p>
<figure>
<img src="https://picsum.photos/800/400" alt="Placeholder image">
<figcaption></figcaption>
</figure>
</p>
<p>
<figure>
<img
src="https://picsum.photos/500/300"
alt="Image with caption"
title="Image with caption"
>
<figcaption>Image with caption</figcaption>
</figure>
</p>
The problem is that p
elements only accept inline elements (more precisely, phrasing contents) as children, which means put a figure
in p
might cause unpredictable behaviors.
The solution is to unwrap the images with a remark plugin called remark-unwrap-images.
It makes sense to put figures directly into articles. Then we style it like
article {
figure {
text-align: center;
}
figcaption {
font-size: smaller;
opacity: 0.5;
}
}
and it looks like below
Table
Take the tabel of components from MDX document as an example.
It has a sticky header, try scroll around the table. It's really tricky to style a responsive table, maybe we can talk about the details later.
Tag | Name | Syntax |
---|---|---|
p | Paragraph | |
h1 | Heading 1 | # |
h2 | Heading 2 | ## |
h3 | Heading 3 | ### |
h4 | Heading 4 | #### |
h5 | Heading 5 | ##### |
h6 | Heading 6 | ###### |
blockquote | Blockquote | > |
ul | List | - |
ol | Ordered list | 1. |
li | List item | |
table | Table | |
thead | Table head | |
tbody | Table body | |
tr | Table row | |
td/th | Table cell | |
code | Code | ```code``` |
inlineCode | InlineCode | `inlineCode` |
pre | Code | ```code``` |
em | Emphasis | _emphasis_ |
strong | Strong | **strong** |
del | Delete | ~~strikethrough~~ |
hr | Thematic break | --- or *** |
a | Link | <https://mdxjs.com> or [MDX](https://mdxjs.com) |
img | Image | ![alt](https://mdx-logo.now.sh) |
Blockquote
MDX makes blockquote footer easier to apply, it just takes a little tricks in styling and then
> Lorem Ipsum is simply dummy text of the printing and typesetting industry.
> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
> Lorem Ipsum is simply dummy text of the printing and typesetting industry.
>
> Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
>
> <footer>Lorem Ipsum<cite>Lorem Ipsum</cite></footer>
turns out to be like
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
That's all about typography by now.