Make and deploy your own blog in under 30 mins

21 min read

Yes you read that right. Under 30 minutes. Let's not waste time and get right into it.

Tech Stack

First let's look at the stack that we're going to be using:

  • Remix which is a full stack React framework.
  • TailwindCSS for styling.
  • MDX for writing the blog posts.
  • Vercel to deploy our website.

Prerequisites

  • Good understanding of React.
  • Writing and formatting with Markdown

Coding

Alright let's start coding! First, navigate to your projects directory and bootstrap a Remix project using

terminal
npx create-remix@latest
terminal
? Where would you like to create your app? ./remix-blog
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix if you're unsure; it's easy to change deployment targets. Vercel
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes

You can name it whatever you want, I just used remix-blog. You can select JavaScript if you want, I like TypeScript more so I'm going to be using that. And of course we're going to use Vercel to deploy our project so pick that. After you have bootstrapped the project, open it in your favorite code editor.

Next, start the application using

terminal
npm run dev

You will see a very basic app like this bootstrapped-app

You can see that that's being rendered from the index.tsx file inside the app/routes directory. index.tsx is always the root route.

app/routes/index.tsx
export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
      <ul>
        <li>
          <a
            target="_blank"
            href="https://remix.run/tutorials/blog"
            rel="noreferrer"
          >
            15m Quickstart Blog Tutorial
          </a>
        </li>
        <li>
          <a
            target="_blank"
            href="https://remix.run/tutorials/jokes"
            rel="noreferrer"
          >
            Deep Dive Jokes App Tutorial
          </a>
        </li>
        <li>
          <a target="_blank" href="https://remix.run/docs" rel="noreferrer">
            Remix Docs
          </a>
        </li>
      </ul>
    </div>
  );
}

We don't really need all of this so go ahead and remove all of the link. Let's add a h1 tag to render a nice heading.

app/routes/index.tsx
export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>nexxel's blog</h1>
    </div>
  );
}

Let's understand how routing works in Remix. Routing in Remix is file based, and just how you can create route modules using JavaScript, Remix also allows us to make route modules using MDX.

So make a sub directory inside the app/routes directory called blog. This will be our route module for the /blog. Inside the blog directory make a MDX file, let's call it first-blog.mdx. Inside it lets render a heading.

app/routes/blog/first-blog.mdx
# First Blog post

Now if you navigate to http://localhost:3000/blog/first-blog, you should see the markdown being rendered there. first-blog

Now let's add some attributes to our markdown. We can add attributes like this:

app/routes/blog/first-blog.mdx
---
title: "title of the blog"
date: 2022-04-13
meta:
  title: title of the blog
  description: first ever blog post
---

Let's try to access these attributes by rendering the title. We can do this like this:

app/routes/blog/first-blog.mdx
# {attributes.title}

{attributes.date.toDateString()}

Now navigate to /blog/first-blog and you should see the title and date being rendered. attributes Also notice how the meta tag that we added to out markdown gave the page a title.

Now let's paste an actual blog post in here. You can write your own blog. If you don't have a blog prepared, for now you can just copy this blog to follow along.

So you should have a whole blog being rendered like this. blog-without-styling

As you can see, we already have a working blog in like 7 minutes of work! But obviously this looks really bad. The typography sucks and there's no syntax highlighting for code blocks.

Let's add some syntax highlighting first. For this we're going to use hightlight.js as it's the most popular.

In MDX we can add plugins to all sorts of stuff. There are two types of plugins: remark plugins and rehype plugins. We are going to to use a rehype plugin called rehype-highlight which is using highlight.js. So open your terminal and install it.

terminal
npm i rehype-highlight highlight.js

Now open remix.config.js and add an mdx key with this configuration:

remix.config.js
mdx: async (filename) => {
    const [rehypeHighlight] = await Promise.all([
      import("rehype-highlight").then((module) => module.default),
    ]);
    return {
      rehypePlugins: [rehypeHighlight],
    };
  },

Here we are importing rehype-highlight and the adding it to our list of rehypePlugins. So now your remix.config.js should look something like this:

remix.config.js
/**
 * @type {import('@remix-run/dev').AppConfig}
 */
module.exports = {
  serverBuildTarget: "vercel",
  // When running locally in development mode, we use the built in remix
  // server. This does not understand the vercel lambda module format,
  // so we default back to the standard build output.
  server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
  ignoredRouteFiles: [".*"],
  appDirectory: "app",
  assetsBuildDirectory: "public/build",
  serverBuildPath: "api/index.js",
  publicPath: "/build/",
  mdx: async (filename) => {
    const [rehypeHighlight] = await Promise.all([
      import("rehype-highlight").then((module) => module.default),
    ]);
    return {
      rehypePlugins: [rehypeHighlight],
    };
  },
};

Now we're going to make a layout route for /blog. The way to do this in Remix is by create a blog.tsx file at the same level as blog directory. So create a blog.tsx file in the app/routes directory. As this is a layout route, any styling that we add here is added for all the nested routes for /blog.

Let's bring in a theme for syntax highlighting from highlight.js. If you look at node_modules/highlight.js/styles, you will see a lot of themes to choose from. I'm going to use the tokyo-night-dark theme, but feel free to choose whatever you like. Now we need to expose this css to all the nested routes. The way to do this in Remix is by the links function. You can read more about it here. So in app/routes/blog.tsx, let's add all this code.

app/routes/blog.tsx
import type { LinksFunction } from "@remix-run/node";
import styles from "highlight.js/styles/tokyo-night-dark.css";

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

We're just providing it a stylesheet with the css that we imported from highlight.js. While we're here, let's also add some meta tags to this page. To add meta tags we use the meta function. Read more about it here. This is what your file should look like now:

app/routes/blog.tsx
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import styles from "highlight.js/styles/tokyo-night-dark.css";

export const meta: MetaFunction = () => {
  return {
    title: "nexxel's blog",
    description: "here nexxel writes about stuff",
  };
};

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

Feel free to add whatever title and description you want.

Since this is out layout route, we also need to export a default component that returns an <Outlet />. This is a Remix thing, it requires this for nested routes. Read more about it here.

Now your code should look something like this:

app/routes/blog.tsx
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";
import styles from "highlight.js/styles/tokyo-night-dark.css";

export const meta: MetaFunction = () => {
  return {
    title: "nexxel's blog",
    description: "here nexxel writes about stuff",
  };
};

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

export default function Blog() {
  return <Outlet />;
}

Now if you rerun your dev server by using npm run dev, you will see that our syntax highlighting works! syntax-highlighting

Congratulations if you have made this far because we are almost done. If you look at the current state of our blog, it isn't very readable. The typography sucks. So we're going to use Tailwind for this, more specifically the @tailwindcss/typography plugin which will make our blog look super nice. Let's set up Tailwind first.

Kill your dev server and install Tailwind and its peer dependencies, then run the init command to generate tailwind.config.js and postcss.config.js.

npm install -D tailwindcss postcss autoprefixer concurrently
npx tailwindcss init -p

We also need concurrently because we will run two processes at one, one will be our dev server, and another one will compile the Tailwind classes into actual CSS.

Now add all the file paths that will use Tailwind in tailwind.config.js

tailwind.config.js
module.exports = {
  content: ["./app/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

Now go to package.json and update the scripts.

package.json
{
  "scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
    "dev": "concurrently \"npm run dev:css\" \"remix dev\"",
    "dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css"
  }
}

Now create a ./styles/app.css and add the Tailwind directives.

styles/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

This will show you 3 problems in VSCode, just ignore them.

Now go to app/root.tsx and import the compiled css. This is what your code should look like:

app/root.tsx
import type { MetaFunction } from "@remix-run/node";
import styles from "./styles/app.css";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export const meta: MetaFunction = () => ({
  charset: "utf-8",
  title: "New Remix App",
  viewport: "width=device-width,initial-scale=1",
});

export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

This is all documented here

Now that we have Tailwind set up, let's also install the typography plugin.

terminal
npm i -D @tailwindcss/typography

Open tailwind.config.js and add the typography plugin in the plugins list.

tailwind.config.js
module.exports = {
  content: ["./app/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {}
    },
  },
  plugins: [require("@tailwindcss/typography")],
};

Now when you run your dev server using npm run dev, you'll see that it will first give an error because our compiled css file doesn't exist yet, but then it will generate that eventually and it will work.

Now we're gonna see just how powerful this typography plugin is. Open app/routes/blog.tsx which is the blog layout route. Any styling that we add here is added for all the nested routes. So let's wrap the <Outlet /> component with a <div> and add the prose class from the typography plugin. This is what your code should look like:

app/routes/blog.tsx
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";
import styles from "highlight.js/styles/github-dark-dimmed.css";

export const meta: MetaFunction = () => {
  return {
    title: "nexxel's blog",
    description: "here nexxel writes about stuff",
  };
};

export const links: LinksFunction = () => {
  return [
    {
      rel: "stylesheet",
      href: styles,
    },
  ];
};

export default function Blog() {
  return (
    <div className="flex justify-center">
      <div className="prose lg:prose-xl py-10 px-6">
        <Outlet />
      </div>
    </div>
  );
}

We are also centering it using flexbox. Just one prose class and it makes it so much better! typography

If you make another MDX file inside the app/routes/blog directory, you will see that the styles work there too. All because of the blog layout route.

We're pretty much done here. Now all that is left is to make a /blog page to display all our blog posts. I'm going to keep this very simple and minimal but feel free to explore with the styling and come up with cool designs!

So let's make an index.tsx file inside app/routes/blog which will act as the /blog page.

First let's import all our blog posts in here. I changed the name of the MDX file to make more sense.

app/routes/blog/index.tsx
import * as goGol from "go-gol.mdx";
import * as nexdle from "nexdle.mdx";
import * as genLicense from "gen-license.mdx";

Now that we have all the MDX modules imported, let's write a function to pull out the slug which is the filename without the .mdx, and then we can just provide the rest of the attributes that we're getting from the meta attribute that we had added in out MDX files. This function is straight from the docs. Read more here.

app/routes/blog/index.tsx
function postFromModule(module: any) {
  return {
    slug: module.filename.replace(/\.mdx?$/, ""),
    ...module.attributes.meta,
  };
}

Now let's add a loader function, in Remix the loader function is used to load in data server-side. Read more here. We will just load all our blogs in here.

app/routes/blog/index.tsx
export const loader: LoaderFunction = () => {
  return [
    postFromModule(genLicense),
    postFromModule(nexdle),
    postFromModule(goGol),
  ];
};

Whatever we have loaded here is accessible in client-side by using a hook called useLoaderData which is provided by Remix. Read more about it here. Now we just map over our posts and render them in an unordered list. I'm also adding some very basic styling.

app/routes/blog/index.tsx
export default function BlogIndex() {
  const posts = useLoaderData();
  return (
    <div className="px-6">
      <h2>Posts</h2>

      <ul>
        {posts.map((post: any) => (
          <li key={post.slug}>
            <Link to={`/blog/${post.slug}`}>{post.title}</Link>

            {post.description && (
              <p className="m-0 lg:m-0">{post.description}</p>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

So after adding all this, your code should look like this:

app/routes/blog/index.tsx
import type { LoaderFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import * as goGol from "go-gol.mdx";
import * as nexdle from "nexdle.mdx";
import * as genLicense from "gen-license.mdx";

function postFromModule(module: any) {
  return {
    slug: module.filename.replace(/\.mdx?$/, ""),
    ...module.attributes.meta,
  };
}

export const loader: LoaderFunction = () => {
  return [
    postFromModule(genLicense),
    postFromModule(nexdle),
    postFromModule(goGol),
  ];
};

export default function BlogIndex() {
  const posts = useLoaderData();
  return (
    <div className="px-6">
      <h2>Posts</h2>

      <ul>
        {posts.map((post: any) => (
          <li key={post.slug}>
            <Link to={`/blog/${post.slug}`}>{post.title}</Link>

            {post.description && (
              <p className="m-0 lg:m-0">{post.description}</p>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

Now if you go to /blog you will see, that all our posts are shown there. /blog

Now let's make a nice landing page for our blog. I'm going to keep this very minimal but this is where you can show off your creativity and personality!

Go to app/routes/index.tsx and add your code there. This is what mine looks like:

app/routes/index.tsx
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";

export const meta: MetaFunction = () => {
  return {
    title: "nexxel's blog",
    description: "here nexxel writes about stuff",
  };
};

export default function Index() {
  return (
    <div className="flex justify-center items-center text-center text-4xl px-6 h-screen">
      <div>
        <h1 className="font-bold">Welcome to my bare-bones blog</h1>
        <Link to={"/blog"}>
          <button className="pt-6">
            <span className="font-normal text-xl bg-black text-white px-4 py-2 hover:opacity-90 transition-opacity duration-300 rounded-sm shadow-2xl">
              Go to the blog
            </span>
          </button>
        </Link>
      </div>
    </div>
  );
}

landing

Congratulations!! You have finished building a blog app using Remix, TailwindCSS and MDX. That is actually so cool.

Now let's deploy this thing using Vercel 🚀.

Deploying To Vercel

First, delete the app/styles directory (that was our compiled css that was generated) and then upload this code to GitHub. I'm assuming you know how to do that, if you don't feel free to ask in the comment section or just look it up online.

Then go to Vercel and login in with GitHub. Click on new project. vercel Import the repository that you uploaded the code to. vercel-import Choose Remix as your framework preset and then click on deploy! deploy

And we're done! Congratulations on making a very cool blog for yourself and deploying it to the internet! Now whenever you add new blogs you just have push those changes to your repository on GitHub and Vercel will deploy that for you. It's awesome, I love Vercel.

That's it for today, damn this was a long one. If you made it this far, please do comment and show off your new blog. I would really appreciate it!

Code for this tutorial: https://github.com/nexxeln/remix-blog My Blog: https://blog.nexxel.dev

Thank you for reading!