Make and deploy your own blog in under 30 mins
Tags: react, tailwindcss, webdev, beginners
Published: 2022-04-13 11:44:09

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](https://remix.run) which is a full stack React framework.
- [TailwindCSS](https://tailwindcss.com) for styling.
- [MDX](https://mdxjs.com) for writing the blog posts.
- [Vercel](https://vercel.com) 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 
```
npx create-remix@latest
```
```
? 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
```
npm run dev
```
You will see a very basic app like this ![bootstrapped-app](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_3mwe2bD2OG.png)

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.

```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.
```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.
```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](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_VoZNWNDC7F.png)

Now let's add some attributes to our markdown. We can add attributes like this:
```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:
```mdx
# {attributes.title}

{attributes.date.toDateString()}
```
Now navigate to `/blog/first-blog` and you should see the title and date being rendered. ![attributes](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_eaPYVVzuR2.png) 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](https://raw.githubusercontent.com/nexxeln/remix-blog/main/app/routes/blog/go-gol.mdx) to follow along.

So you should have a whole blog being rendered like this. ![blog-without-styling](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_n8aPAVElD8.gif)

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](https://highlightjs.org/) 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.
```
npm i rehype-highlight highlight.js
```

Now open `remix.config.js` and add an `mdx` key with this configuration: 
```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:
```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](https://remix.run/docs/en/v1/api/conventions#links). So in `app/routes/blog.tsx`, let's add all this code.
```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](https://remix.run/docs/en/v1/api/conventions#meta).
This is what your file should look like now:
```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](https://remix.run/docs/en/v1/api/remix#outlet-context-).

Now your code should look something like this:
```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](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_sOaQ8ZJoYH.png)

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`](https://tailwindcss.com/docs/typography-plugin) 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`
```js
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Now go to `package.json` and update the scripts.
```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.
```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:
```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](https://tailwindcss.com/docs/guides/remix)

Now that we have Tailwind set up, let's also install the typography plugin.
```
npm i -D @tailwindcss/typography
```

Open `tailwind.config.js` and add the typography plugin in the `plugins` list.
```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:
```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](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_umo6KZ1WSu.gif)

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. 
```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](https://remix.run/docs/en/v1/guides/mdx#modules).

```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](https://remix.run/docs/en/v1/api/conventions#loader). We will just load all our blogs in here.
```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](https://remix.run/docs/en/v1/api/remix#useloaderdata). Now we just map over our posts and render them in an unordered list. I'm also adding some very basic styling.

```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:
```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](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_EsooUIZfZD.png)

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:
```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](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_Tj44rGk8Vx.png)

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](https://github.com). 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](https://vercel.com) and login in with GitHub. Click on new project. ![vercel](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_fusvFkqV31.png)
Import the repository that you uploaded the code to. ![vercel-import](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_wmScEKFQZ4.png)
Choose Remix as your framework preset and then click on deploy!
![deploy](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_uyJ35KjqN3.png)

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!