Hello World!

Fx64b

Fx64b

Readtime: 5 mins

2024-10-16

Hello World! This is the first "blog post" on this site.

After procrastinating for close to two years, I finally found the time to replace the "Work in Progress" page with a proper site:

Old WIP page


What I used to build this site


...


Well you might ask yourself now, "Why not use plain HTML and CSS and just build a static site?"


Great question! It's almost like asking a chef why he didn't just microwave a frozen pizza for dinner.

Sure, it's quick and gets the job done but where is the fun in that?


And nothing screams "I'm serious about web development" like having a CI/CD pipeline and automated versioning set up for a blog that I'll probably update twice a year.


So why all this?

  1. Because I can.
  2. Because, honestly, I really wanted to experiment with these technologies, but I don't really have any ideas for actual projects to use them on.

Building the site

The most interesting and difficult part was definitely the MarkdownRenderer.tsx, specifically the codeblock and syntax highlighting part.

After several hours of googling and getting nonsense answers from ChatGPT I ended up with this not so clean solution:

TypeScript
1// components/MarkdownRenderer.tsx 2// ... 3interface MarkdownRendererProps { 4 content: string 5} 6 7interface CodeBlockProps { 8 inline?: boolean 9 className?: string 10 children?: React.ReactNode 11 [key: string]: any 12} 13 14const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content }) => { 15 const CodeBlock: React.FC<CodeBlockProps> = ({ 16 inline, 17 className, 18 children, 19 ...props 20 }) => { 21 const [isCopied, setIsCopied] = useState(false) 22 const match = /language-(\w+)/.exec(className || '') 23 const language = match?.[1] 24 25 const code = String(children).replace(/\n$/, '') 26 27 return !inline && language ? ( 28 <div style={{ position: 'relative' }}> 29 <CopyToClipboard 30 text={code} 31 onCopy={() => { 32 setIsCopied(true) 33 setTimeout(() => setIsCopied(false), 2000) // Reset after 2 seconds 34 }} 35 > 36 <button 37 style={{ 38 position: 'absolute', 39 top: '1.1rem', 40 right: '1rem', 41 background: 'none', 42 border: 'none', 43 cursor: 'pointer', 44 color: isCopied ? '#006fee' : '#fff', 45 }} 46 aria-label="Copy code to clipboard" 47 > 48 {isCopied ? ( 49 <CheckIcon 50 style={{ width: '1.25rem', height: '1.25rem' }} 51 /> 52 ) : ( 53 <ClipboardIcon 54 style={{ width: '1.25rem', height: '1.25rem' }} 55 /> 56 )} 57 </button> 58 </CopyToClipboard> 59 <SyntaxHighlighter 60 style={oneDark} 61 language={language} 62 PreTag="div" 63 {...props} 64 > 65 {code} 66 </SyntaxHighlighter> 67 </div> 68 ) : ( 69 <span {...props}>{children}</span> 70 ) 71 } 72 73 return ( 74 <ReactMarkdown 75 rehypePlugins={[rehypeRaw]} 76 components={{ 77 code: CodeBlock as any, 78 hr: () => <Divider />, 79 a: ({ href, children }) => ( 80 <Link href={href!} isExternal> 81 {children} 82 </Link> 83 ), 84 }} 85 > 86 {content} 87 </ReactMarkdown> 88 ) 89}

Here is how it works

  1. CodeBlock Subcomponent
TypeScript
1// components/MarkdownRenderer.tsx 2const CodeBlock: React.FC<CodeBlockProps> = ({ inline, className, children, ...props }) => { 3 const [isCopied, setIsCopied] = useState(false) 4 const match = /language-(\w+)/.exec(className || '') 5 const language = match?.[1] 6 7 const code = String(children).replace(/\n$/, ''); 8 ...
  • We start off by setting the useState() for the copy button.
  • Then we extract the language. During the markdown processing a class that looks like this is added to the codeblock to later identify the correct language: language-tsx
  • Then the children prop (which contains all the code) is converted into a string and all trailing newline (\n)

The Copy button isn't too interesting, I just use the react-copy-to-clipboard package.

Syntax highlighting is done by the <SyntaxHighlighter> component from react-syntax-highlighter with this relatively simple code block:

TypeScript
1// components/MarkdownRenderer.tsx 2<SyntaxHighlighter style={oneDark} language={language} PreTag="div" {...props}> 3 {code} 4</SyntaxHighlighter>

If the code is inline (for example an npm install command) we just use a basic span element which results in a clean look:

TypeScript
1// components/MarkdownRenderer.tsx 2) : ( 3 <span {...props}>{children}</span> 4)

and looks like this:

Bash
pnpm install is-odd@latest

  1. Hot-gluing it all together

Now we take these two components and patch them together like this:

TypeScript
1// components/MarkdownRenderer.tsx 2return ( 3 <ReactMarkdown 4 rehypePlugins={[rehypeRaw]} 5 components={{ 6 code: CodeBlock as any, 7 hr: () => <Divider />, 8 a: ({ href, children }) => ( 9 <Link href={href!} isExternal> 10 {children} 11 </Link> 12 ), 13 }} 14 > 15 {content} 16 </ReactMarkdown> 17)
  • rehypeRaw is used that things like <br> tags are not rendered as text so that the content renders halfway decently.
  • In the components attribute I specified the html elements I want to override with my own elements, specifically the CodeBlock component


Handling of Markdown files

Posts are stored as simple markdown files in the /content directory.

The filename also acts as the slug in the url /content/hello-world.md -> /blog/hello-world

Here is how postdata and content is fetched:

TS
1// app/lib/posts.ts 2export function getPostBySlug(slug: string): Post | null { 3 const realSlug = slug.replace(/\.md$/, '') 4 const fullPath = path.join(postsDirectory, `${realSlug}.md`) 5 6 if (!fs.existsSync(fullPath)) { 7 return null 8 } 9 10 const fileContents = fs.readFileSync(fullPath, 'utf8') 11 const { data } = matter(fileContents) 12 13 return { 14 ...(data as Post), 15 slug: realSlug, 16 } 17}

This function additionally has a replace for .md in case the function is called by a function that gets all filenames from the content directory like this:

TS
1// app/lib/posts.ts 2export function getPostSlugs(): string[] { 3 return fs.readdirSync(postsDirectory).filter((file) => file.endsWith('.md')) 4}

and then:

TS
1// app/lib/posts.ts 2export function getAllPosts(): Post[] { 3 const slugs = getPostSlugs() 4 return slugs 5 .map((slug) => getPostBySlug(slug)) 6 .filter((post): post is Post => post !== null) 7 .sort((a, b) => (a.date > b.date ? -1 : 1)) 8}


Conclusion

There is much more code I could cover here, but I'm not gonna do that. Check out the code yourself if you want to: Fx64b/fx64b.dev.


If you have some spare time to waste you can help me clean up the code by creating a pull request.


Feel free to copy the code from this site. If you for some reason decide to copy content from my blog posts and publish them yourself, please add a reference to the original post.

Fx64b

About Fx64b

Software engineer from Switzerland building modern web applications with React, Next.js, TypeScript, and Go. Currently exploring cybersecurity fundamentals and sharing my journey through this blog.