How I Built My Blog


I’ve been a long-time WordPress user for my blog and thought it was about time for a change. I’ve been a big fan of Next.js for years now and even did a talk about it back at WordCamp Miami 2018, albeit with a non-working demo 😅. Since then, Next.js has come a long way with even more features.

While I am not supplying the entire source code to my blog, that is mainly because I keep draft posts in the repository and on the production environment just filter out the posts that are not published. Instead, I am going to walk you through the basics of getting a blog up and running using MDX files for blog posts, Next.js, and styled components. If you’re only interested in the final product, you can check that out here on GitHub.

Getting Started

# Create the new project.
yarn create next-app mdx-blog --typescript
 
# Change directories into that new project.
cd mdx-blog

From there, let’s start adjusting the directory structure of the project.

# Delete the API routes since we're not going to use them and the styles directory as we're going to use styled-components.
rm -rf pages/api styles
 
# Create the directories we're going to use to store our MDX files.
mkdir -p data/{pages,posts}
 
# Create the directory we're going to use to store our components, hooks, and utilities.
mkdir {components,hooks,utils}

Let’s go ahead and remove the imports for those styles as well within pages/_app.tsx and pages/index.tsx as well as the content in the home route.

I also like to rename the function within my pages/_app.tsx to be a bit more descriptive about what it actually is. I chose BlogApp for this instance here.

Now, our pages/_app.tsx should look like the following:

pages/_app.tsx
import type { AppProps } from "next/app";
 
function BlogApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}
export default BlogApp;

And our pages/index.tsx should look like the following:

pages/index.tsx
import type { NextPage } from "next";
import Head from "next/head";
 
const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Kevin Langley Jr. | Web Engineer</title>
      </Head>
      <div>Home</div>
    </div>
  );
};
 
export default Home;

Customizing tsconfig.json

I like to use path aliases for my different directories, so let’s set that up now in our tsconfig.json.

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "paths": {
      "@components/*": ["./components/*"],
      "@hooks/*": ["./hooks/*"],
      "@utils/*": ["utils/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

ESLint

Now, let’s setup ESLint! We can generate a configuration file for it by just simply running the following.

yarn lint
 
? How would you like to configure ESLint? https://nextjs.org/docs/basic-features/eslint
  Strict (recommended)
   Base
   Cancel

Styled Components

While CSS-in-JS took a bit to win me over, I am now a big fan and I personally love styled-components, so let’s integrate that now.

# Add styled-components and styled-normalize as dependencies.
yarn add styled-components styled-normalize react-is
 
# Add babel-plugin-styled-components and @types/styled-components as dev dependencies.
yarn add -D babel-plugin-styled-components @types/styled-components

Now, we’ll add a bit to our .babelrc file to integrate with styled-components.

.babelrc
{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true }]]
}

Let’s next add the custom pages/_document.tsx file to integrate with styled-components.

pages/_document.tsx
import Document from "next/document";
import { ServerStyleSheet } from "styled-components";
 
export default class BlogDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
 
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });
 
      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }
}

Since we are going to setup a light and dark style mode on the site, let’s also install the deepmerge package to be able to recursively merge JavaScript objects. Specifically, we’ll be using this to merge default styles with the different light and dark mode styles.

yarn add deepmerge

Now, let’s add the styles declaration file, located at styles.d.ts.

styles.d.ts
// import original module declarations
import "styled-components";
 
// and extend them!
declare module "styled-components" {
  export interface DefaultTheme {
    colors: {
      background: string;
      blockquote: {
        text: string;
        background: string;
        accent: string;
        author: string;
      };
      button: {
        color: string;
      };
      code: {
        background: string;
        border: string;
        highlightBg: string;
        text: string;
        title: string;
      };
      headerLinkText: string;
      linkText: string;
      text: string;
      postDate: string;
      postTitle: string;
      themeToggle: {
        light: string;
        dark: string;
      };
    };
  }
}

Next, let’s add the style utility file, located at utils/styles.ts.

utils/styles.ts
import type { DefaultTheme } from "styled-components";
 
import { createGlobalStyle } from "styled-components";
import { normalize } from "styled-normalize";
import merge from "deepmerge";
 
export const GlobalStyle = createGlobalStyle`
  ${normalize}
 
  html {
    height: 100%;
  }
 
  body {
    background-color: var( --color-background );
    color: var( --color-text );
    font-family: 'Inter', sans-serif;
    height: 100%;
    transition: background-color 300ms ease-out, color 300ms ease-out;
  }
 
  #__next {
    min-height: 100%;
    display: grid;
    grid-template-rows: auto 1fr auto;
  }
 
  p,
  ul li {
    line-height: 1.8;
    margin: 1.25rem 0;
  }
 
  code {
    background-color: var( --color-code-background );
    color: var( --color-code-text );
    padding: 0.375rem 0.625rem;
  }
`;
 
const commonStyles = {
  colors: {
    button: {
      color: "#fff",
    },
    code: {
      background: "#011627",
      border: "rgba(51, 183, 255, 0.2)",
      highlightBg: "#2d374c",
      text: "#f9b434",
      title: "#fff",
    },
    themeToggle: {
      light: "#a0aec0",
      dark: "#4a5568",
    },
  },
};
 
export const lightTheme: DefaultTheme = merge(
  {
    colors: {
      background: "#fff",
      text: "#1f1f1f",
      headerLinkText: "#1f1f1f",
      linkText: "#2b6cb0",
      postDate: "#666",
      postTitle: "#1f1f1f",
      blockquote: {
        text: "#555",
        background: "#ededed",
        accent: "#1a202c",
        author: "#333",
      },
    },
  },
  commonStyles,
);
 
export const darkTheme: DefaultTheme = merge(
  {
    colors: {
      background: "#1a202c",
      text: "#fff",
      headerLinkText: "#fff",
      linkText: "#f9b434",
      postDate: "#999",
      postTitle: "#a0aec0",
      blockquote: {
        text: "#ededed",
        background: "#1a2d3c",
        accent: "#f9b434",
        author: "#f9b434",
      },
    },
  },
  commonStyles,
);

Within our utils/styles.ts, we start by importing createGlobalStyle from styled-components, normalize from styled-normalize, and merge from deepmerge. We then created and exported the GlobalStyle which we will use in the Layout component we’re about to make. We also exported the lightTheme and darkTheme which have been deep merged with our commonStyles.

useDarkMode Custom Hook

Since we’re going to be creating a dark and light mode for this project, let’s go ahead and create the useDarkMode custom hook!

hooks/useDarkMode.ts
import { useEffect, useState } from "react";
 
export interface DarkModeProps {
  theme: string;
  toggleTheme: () => void;
  componentMounted: boolean;
}
 
const useDarkMode = (): DarkModeProps => {
  const [theme, setTheme] = useState<string>("dark");
  const [componentMounted, setComponentMounted] = useState<boolean>(false);
 
  const setMode = (mode) => {
    window.localStorage.setItem("theme", mode);
    setTheme(mode);
  };
 
  const toggleTheme = () =>
    theme === "light" ? setMode("dark") : setMode("light");
 
  useEffect(() => {
    const localTheme = window.localStorage.getItem("theme");
 
    if (
      window.matchMedia &&
      window.matchMedia("(prefers-color-scheme: dark)").matches &&
      !localTheme
    ) {
      setMode("dark");
    } else {
      localTheme ? setTheme(localTheme) : setMode("dark");
    }
 
    setComponentMounted(true);
  }, []);
 
  return {
    theme,
    toggleTheme,
    componentMounted,
  };
};
 
export default useDarkMode;

Layout Component

Next, let’s create a Layout component that will wrap all of our pages with our ThemeProvider from styled-components. Soon, we’ll create a Header and Footer components that will wrap our <main> tag within the Layout component.

components/Layout/index.tsx
import type { ReactNode, ReactElement } from "react";
 
import Head from "next/head";
import styled, { ThemeProvider } from "styled-components";
 
import useDarkMode from "@hooks/useDarkMode";
import { GlobalStyle, lightTheme, darkTheme } from "@utils/styles";
 
const Main = styled.main`
  min-width: 100%;
`;
 
type LayoutProps = {
  title?: string;
  children?: ReactNode;
};
 
const Layout = ({ children, title = "Blog" }: LayoutProps) => {
  const { theme, toggleTheme, componentMounted } = useDarkMode();
 
  const themeMode = theme === "light" ? lightTheme : darkTheme;
 
  if (!componentMounted) {
    return <div />;
  }
 
  return (
    <>
      <Head>
        <title>{title}</title>
      </Head>
      <ThemeProvider theme={themeMode}>
        <GlobalStyle />
        <Main>{children}</Main>
      </ThemeProvider>
    </>
  );
};
 
export default Layout;
 
export const getLayout = (page: ReactElement): ReactElement => (
  <Layout>{page}</Layout>
);

Let’s go ahead and create the container, header, and footer components now!

components/Container/index.tsx
import styled from "styled-components";
 
interface ContainerProps {
  condensed?: boolean;
}
 
const Container = styled.div<ContainerProps>`
  margin: 0 auto;
  max-width: ${({ condensed }) => (condensed ? "60rem" : "80rem")};
  padding: 0 1rem;
`;
 
export default Container;

This container element will allow us to have a condensed and non-condensed version of the container depending on the props passed into the component.

Next, we’ll create our Header component.

components/Layout/Header/index.tsx
import type { ReactElement } from "react";
import type { StyledComponent } from "styled-components";
 
import styled from "styled-components";
 
import Container from "@components/Container";
 
const Nav: StyledComponent = styled.nav`
  ul {
    margin: 0;
    padding: 0;
  }
 
  li {
    display: inline-block;
 
    margin: 0 0.5rem;
 
    @media (min-width: 48rem) {
      margin: 0 1rem;
    }
  }
`;
 
const SiteHeader = styled.header`
  text-align: center;
  margin: 1.25rem 0;
`;
 
const Header = (): ReactElement => {
  return (
    <SiteHeader>
      <Container></Container>
    </SiteHeader>
  );
};
 
export default Header;