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
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
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.
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.
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.
# First Blog post
Now if you navigate to http://localhost:3000/blog/first-blog
, you should see the markdown being rendered there.
Now let's add some attributes to our markdown. We can add attributes like this:
---
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:
# {attributes.title}
{attributes.date.toDateString()}
Now navigate to /blog/first-blog
and you should see the title and date being rendered. 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.
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.
npm i rehype-highlight highlight.js
Now open remix.config.js
and add an mdx
key with this configuration:
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:
/**
* @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.
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:
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:
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!
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
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Now go to package.json
and update the scripts.
{
"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.
@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:
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.
npm i -D @tailwindcss/typography
Open tailwind.config.js
and add the typography plugin in the plugins
list.
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:
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!
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.
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.
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.
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.
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:
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.
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:
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>
);
}
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.
Import the repository that you uploaded the code to.
Choose Remix as your framework preset and then click on 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!
Top comments (3)
Yeah its pretty neat. But I'm too in love with next.js to switch to remix for bigger projects.
So I'm still gonna use next for serious projects haha. But you should def check out Remix, its awesome!
Btw I love your blogs