2021 - Next.js Build Notes

November - 2021

These notes start from the point when I moved off mdx-bundler and onto next-mdx-remote. I started from scratch.


Initial setup

mkdir alanwsmith.com
cd alanwsmith.com
npx create-next-app .
npm i next-mdx-remote gray-matter
npm i @netlify/plugin-nextjs

That got everything ready to go based off the work done on https://next-mdx-remote-blog-example.netlify.app/


Moved all components into their own files since next-mdx-remote doesn't work with them inside of files.

The data has to be added directly in the component as well, like this:

<Checklist data={`
Start Monster Cat Audio
Set Monster Cat Audio to Shuffle
Mute headphones
Switch OBS to main scene and check that music and vocals are working
`} />

Installed Tailwind from these instructions: via: https://tailwindcss.com/docs/guides/nextjs

Starting with

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Then ran this

npx tailwindcss init -p

which created tailwind.config.js and postcss.config.js

The next step was to put this in tailwind.config.js

purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],

Then replaced the contents of ./styles/global.css with:

@tailwind base;
@tailwind components;
@tailwind utilities;

That gets the basic framework in place.


Setup to use a basic LayoutMain file that has tailwind in it:

file: ./components/LayoutMain.js

export default function LayoutMain(props) {
  return (
    <>
      <div className="bg-gray-800 min-h-screen">
        <main className="pb-16 mx-auto container pt-4 px-6 max-w-screen-md">
          {props.content}
        </main>
      </div>
    </>
  )
}

Then updated the index.js and [slug].js file to use it with:

file: ./pages/index.js

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import Link from 'next/link'
import LayoutMain from '../components/LayoutMain'

export default function HomePage({ posts }) {
  return (
    <>
      <LayoutMain
        content={
          <>
            <h1>This is the home page</h1>
            <p>Stuff broke... I fix</p>
            <ul>
              {posts.map((post) => (
                <li key={post.slug}>
                  <Link href={`/posts/${post.slug}`}>
                    <a>{post.title}</a>
                  </Link>
                </li>
              ))}
            </ul>
          </>
        }
      />
    </>
  )
}

export async function getStaticProps({ params }) {
  const contentDir = path.join(process.cwd(), '_posts')

  const posts = fs
    .readdirSync(contentDir)
    .filter((filePath) => /\.mdx?$/.test(filePath))
    .map((filePath) => {
      const slug = filePath.replace(/\.mdx$/, '')
      const { content, data } = matter(
        fs.readFileSync(path.join(contentDir, filePath))
      )
      return {
        title: data.title,
        slug: slug,
      }
    })

  return {
    props: {
      posts: posts,
    },
  }
}

file: ./pages/posts/[slug].js

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import { MDXRemote } from 'next-mdx-remote'
import Link from 'next/link'

const CONTENT_DIR = path.join(process.cwd(), '_posts')

import LayoutMain from '../../components/LayoutMain'
import Checklist from '../../components/Checklist'
import ReadOnlyChecklist from '../../components/ReadOnlyChecklist'
import YouTubeVideo from '../../components/YouTubeVideo'
import VimeoVideo from '../../components/VimeoVideo'

export default function Post({ source, frontmatter }) {
  return (
    <>
      <div>
        <Link href="/">
          <a>Home Page</a>
        </Link>
      </div>
      <LayoutMain
        content={
          <>
            <h1>{frontmatter.title}</h1>
            <MDXRemote
              {...source}
              components={{
                Checklist,
                ReadOnlyChecklist,
                YouTubeVideo,
                VimeoVideo,
              }}
            />
          </>
        }
      />
    </>
  )
}

export async function getStaticProps({ params }) {
  const { content, data } = matter(
    fs.readFileSync(path.join(CONTENT_DIR, `${params.slug}.mdx`))
  )
  const mdxSource = await serialize(content)
  return { props: { source: mdxSource, frontmatter: data } }
}

export async function getStaticPaths() {
  const paths = fs
    .readdirSync(CONTENT_DIR)
    .filter((path) => /\.mdx?$/.test(path))
    .map((fileName) => fileName.replace(/\.mdx$/, ''))
    .map((slug) => ({ params: { slug: slug } }))
  return {
    paths: paths,
    fallback: false,
  }
}

Installed prismjs with:

npm i prismjs

Added import 'prismjs/themes/prism-tomorrow.css' to _app.js so it looks like this:

import '../styles/globals.css'
import 'prismjs/themes/prism-tomorrow.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

And then updated ./components/LayoutMain.js so it looks like this:

const prism = require('prismjs')
import { useEffect } from 'react'

export default function LayoutMain(props) {
  useEffect(() => {
    prism.highlightAll()
  }, [])

  return (
    <>
      <div className="bg-gray-800 min-h-screen">
        <main className="pb-16 mx-auto container pt-4 px-6 max-w-screen-md">
          {props.content}
        </main>
      </div>
    </>
  )
}

That gets the syntax highlighting in place so that code fences like:

```javascript
const someThing = 'An example'
```

Render as:

const someThing = 'An example'

Added in favicons and header stuff and nav. see the github for details.


Using next/image requires importing the images directly to get the benefits of local functionality. Unfortunately next-mdx-remote doesn't handle that. So, I ended up created a components/Img.js file with:

import Image from 'next/image'

import black_widow_20051026_230734a1 from '../_images/black_widow_20051026_230734a1.jpg'

const imgMap = {
  black_widow_20051026_230734a1: black_widow_20051026_230734a1,
}

export default function Img({ src, alt = 'image alt text unavailable' }) {
  return <Image src={imgMap[src]} alt={alt} />
}

That gets included in the [slug].js file with the other components (via: import Img from '../../components/Img') and passed to MDXRemote like this:

<MDXRemote
  {...source}
  components={{
    Checklist,
    Img,
    ReadOnlyChecklist,
    YouTubeVideo,
    VimeoVideo,
  }}
/>

Then, for every image that's going to be called I create an import like:

import black_widow_20051026_230734a1 from '../_images/black_widow_20051026_230734a1.jpg'

and a reference in a mapping object so I can access it like this:

const imgMap = {
  black_widow_20051026_230734a1: black_widow_20051026_230734a1,
  // other images here
}

I make sure the image filenames work as variable names so I can keep the same name in place across the board.

Then, to use the image, I call it like this in the MDX source files:

<Img
    src="black_widow_20051026_230734a1"
    alt="A dead black widow spider showing the red hour-glass shape on the belly"
/>

I build the Img.js file with a script before I publish that grabs all the images in the _images directory and makes an entry for each one. That way, all I need to do is drop in an image with a valid name and then use it in the <Img> tag. The rest of the work is automated.

I'd prefer to keep the images in sub-directories by year but that's more than I want to tackle at the moment. Getting the images in place in generally was the key. I can optimize later if that becomes necessary.

Something that's cool about this is that it will let me customize image calls further if I want since there's a layer of abstraction between the content and the <Image> call. For example, instead of just sending a style tag I could send an attribute that adds extra divs and puts in captions, etc... In fact, now that I think about it, that's exactly what I'm going to do.

It's a bit of a hack, but it works.

TODO: Figure out if this impacts the performance of the site as the number of images goes up since they are all being imported.