Introduction
App.tsx1import React from "react";23const App: React.FC = () => (4 <div>5 HelloWorld6 </div>7)
Hi, I updated my blog using Gatsby.js.
Here, I will show you how to build it and how I write each posts. The list of what I wanted to do was the following.
- Writing my post in markdown file
- Highlighting source code written in the file.
- Embedding JSX in the markdown file.
I'm writing this post in markdown file. But not just .md file, I used .mdx file. Using this format, I can write JSX in markdown file.
The Introduction section title is written as
# Introduction
By default, the # mark will not apply any styles. To apply styles, such as font size, bold or regular, I had to replace the default tag to my original component.
The line beginning with # mark will be converted to h1 tag, so I replaced the h1 tag to H1 component
HeadingParagraph.tsx1export const H1: React.FC<PropsWithChildren<unknown>> = ({children}) => (2 <h1 className="text-3xl font-bold">{children}</h1>3)
The HeadingParagraph.tsx code is written with the following format.
this_post.mdx
```jsx:H1.tsx
// codes here
```
The list what I wanted to do is written in the markdown file(.mdx) with the following format.
// styled by tailwindcss
<ul className="px-8 py-4 list-disc font-bold">
<li>Writing my post in markdown file</li>
<li>Highlighting source code written in the file.</li>
<li>Embedding JSX in the markdown file.</li>
</ul>
Of course, the list can be written as
- hoge
- fuga
But, I used JSX here.
(By default the list is converted to ul and li tags. As I introduced, default tags can be replaced by components, so If you use markdown format, Ul or Li component may be needed.)
In this way, I could create fully customizable tech blog.
Setup Gatsby.js
First you need to setup these things.
- gatsby-node.js to create page
- gatsby-config.ts to use some plugins
This official tutorial is good to complete them.
These are all plugins I used.
gatsby-config.ts1 plugins: [2 'gatsby-plugin-typescript',3 {4 resolve: 'gatsby-plugin-mdx',5 options: {6 extensions: ['.mdx', '.md']7 }8 },9 {10 resolve: 'gatsby-plugin-postcss',11 options: {12 postCssPlugins: [require('tailwindcss')]13 }14 },15 {16 resolve: 'gatsby-source-filesystem',17 options: {18 name: 'src',19 path: `${__dirname}/src/`20 }21 },22 {23 resolve: 'gatsby-plugin-graphql-codegen',24 options: {25 documentPaths: [26 './src/**/*.{ts,tsx}'27 ]28 }29 }30 ]
Some plugins introduced in official tutorial such as gatsby-transformer-remark are not used here.
Create Page
We create pages. Assuming tree of this project is the following.
- src
- pages
- anything
- index.tsx
- anything
- mdxPages
- anything
- helloworld.mdx
- anything
- templates
- TechPost.tsx
- pages
This is the script to create pages from mdx file.
In onCreateNode, paths are created passing "mdxPages" parameters to basePath. This means the post written in mdxPages/anything/helloworld.mdx can be accessed by "https://YOUR_DOMAIN/anything/helloworld"
In createPages, actual pages are created. Here, page metadata are fetched by graphql this is the feature of Gatsby.js.
"./src/templates/TechPost.tsx" are passed to createPage function. This means the content in mdx are rendered in TechPost.tsx component.
gatsby-node.js1const {createFilePath} = require("gatsby-source-filesystem");2const path = require('path');34exports.onCreateNode = (({node, getNode, actions}) => {5 const {createNodeField} = actions;6 if (node.internal.type === 'Mdx') {7 const slug = createFilePath({node, getNode, basePath: 'mdxPages'})8 createNodeField({9 node,10 name: 'slug',11 value: slug12 })13 }14})1516exports.createPages = async ({graphql, actions}) => {17 const {createPage} = actions;18 const result = await graphql(`19 {20 allMdx(21 sort: { fields: [frontmatter___date], order: DESC }22 ) {23 nodes {24 fields {25 slug26 }27 frontmatter {28 title29 abstract30 }31 }32 }33 }`);3435 const posts = result.data.allMdx.nodes;36 posts.forEach(post => {37 createPage({38 path: post.fields.slug,39 component: path.resolve('./src/templates/TechPost.tsx'),40 context: {41 slug: post.fields.slug42 }43 })44 })45}
In TechPost.tsx, mdx data fetched by graphql are just rendered as children of MDXRenderer.
TechPost.tsx1import React from "react";2import {graphql, PageProps} from "gatsby";3import {TechPostQuery} from "../../graphql-types";4import {BlogLayout} from "../layouts/BlogLayout";5import {MDXRenderer} from "gatsby-plugin-mdx";67export default ({data}: PageProps<TechPostQuery>) => {8 // @ts-ignore9 const {body, frontmatter} = data.mdx;1011 return (12 <BlogLayout>13 <MDXRenderer>{body}</MDXRenderer>14 </BlogLayout>15 )16}1718export const query = graphql`19 query TechPost($slug: String!) {20 mdx(fields: {slug: { eq: $slug } }) {21 body22 frontmatter {23 title24 date25 abstract26 }27 }28 }29`
BlogLayout component appears here. In this component, default tags are replaced to original component.
Code blocks surrounded by "```" in markdown file are converted to code tag.
So, I replaced it to CodeBlock for syntax highlighting.post.jsx1import React from "react";2import {MDXProvider} from "@mdx-js/react";34export const BlogLayout: React.FC<PropsWithChildren<unknown>> = ({children}) => {5 // replacing default tags to component6 const components = {7 h1: H1,8 code: CodeBlock9 }1011 return (12 <div>13 <MDXProvider components={components}>14 {children}15 </MDXProvider>16 </div>17 )18}
H1 component is the component I defined in Introduction.
CodeBlock component is the following component. Basically, I used codes introduced officially. I added some changes to show line number and filename.
CodeBlock.tsx1import React, {PropsWithChildren} from "react";2import Highlight, {defaultProps} from "prism-react-renderer";3import github from "prism-react-renderer/themes/dracula";45type Props = {6 className: string7};89export function CodeBlock({className, children}: PropsWithChildren<Props>) {10 const matches = className.match(/language-([a-zA-Z]+)(:(.+))?/) ?? [];11 const language = matches[1];12 const fileName = matches[3];1314 return (15 <div className="my-4">16 {/*@ts-ignore*/}17 <Highlight {...defaultProps} theme={github} code={children} language={language}>18 {({className, style, tokens, getLineProps, getTokenProps}) => (19 <pre className="px-4 py-4 text-xs bg-black rounded-md">20 {fileName &&21 <span className="block text-white text-center border-b border-gray-800 mb-2 pb-2">{fileName}</span>}22 <div className="overflow-x-scroll overflow-hidden">23 {tokens.map((line, i) => {24 const unnecessaryLine = i === tokens.length - 1 && line.length === 1;25 return unnecessaryLine ? null : (26 <div {...getLineProps({line, key: i})}>27 {<span className="text-gray-500 select-none pr-2">{i + 1}</span>}28 {line.map((token, key) => {29 const props = getTokenProps({token, key});30 props.style = {color: props.style?.color ?? null};31 return <span {...props}/>32 })}33 </div>34 )35 })}36 </div>37 </pre>38 )}39 </Highlight>40 </div>41 )42}
Other languages like swift, go are available.
hello.go1package main23import "fmt"45func main() {6 fmt.Printf("Thanks for\n")7}
main.swift1import Foundation23print("reading my first post!!")
Errors I encountered
gatsby-plugins-mdx: warn The GraphQL query in the non-page component "src/templates/**"
Solution: As mentioned here, I moved .mdx files to "src/mdxPages" directory from "src/pages".
MDXProvider with Typescript: Could not find a declaration file for module '@mdx-js/react'
Solution: Add declaration file to "src/types/mdx-js.d.ts" following this doc.
Memo
- I used v2.0.0-next.8 for "@mdx-js/mdx", "@mdx-js.react" to use typescript.
- To show icons cool using font awesome, I used gatsby-plugin-fontawesome-css plugin.