Technical: How we built our markdown blog w/ Next.js, TypeScript, and Vercel
2024-07-27
We're thrilled to announce the launch of our new blog, built with Next.js, TypeScript, and hosted on Vercel's free tier. Our goal was to create a sleek and efficient platform for sharing updates, insights, and stories about our journey and products. In doing so, we've effectively created a simple, yet powerful blogging solution akin to Hugo or Jekyll but with the modern capabilities of Next.js.
Why We Chose Next.js and TypeScript
Next.js is a powerful React framework that offers a range of features ideal for building static and dynamic websites. Its ability to handle server-side rendering, static site generation, and API routes made it a perfect choice for our blog. By leveraging TypeScript, we ensured our codebase is robust and maintainable, benefiting from static typing and the vast TypeScript ecosystem.
Setting Up on Vercel
Hosting our blog on Vercel was a no-brainer. Vercel provides seamless integration with Next.js, offering automatic deployments and performance optimizations right out of the box. Their free tier is perfect for startups and small projects, providing the necessary features without any cost.
Creating a Next.js Blog: A Simple Hugo/Jekyll Equivalent
Our new blog provides us with the simplicity and functionality of static site generators like Hugo and Jekyll, but with the added flexibility and power of Next.js.
Inspiration from Midjourney: Animation and b00p Art
One of the standout features of our landing page is the animated b00p art. Inspired by Midjourney's creative animations, we aimed to bring a dynamic and engaging visual experience to our visitors. The b00p art animations add a playful touch, enhancing the overall user experience.
Shout Out to Jason Hargrove
We owe a huge thanks to our technical founder, Jason Hargrove, for providing the original b00p art. Jason's art was generated using custom 3D models in Blender, inspired by a handmade ink drawing. This blend of traditional and digital art techniques brings a unique aesthetic to our blog, making it visually appealing and distinctive.
Content Snippet
Here's a sneak peek at one of our recent blog posts:
---
title: 'Hello World! 🌎'
date: '2024-07-20'
image: '/b00ps-noforest-1.png'
---
# Meet Hi: the b00p named Hello World.
*Beep Boop,* World!
![image](/b00ps-noforest-1.png)
I'm Hi, short for Hello World. I'm a b00p, version 2.01. My electronics are hosted on a yellow robot board, with red waves silkscreen.
"What's a b00p?" Well, we're the future of fun at work!
Here's a little secret: I'm the chattiest b00p around. My mission? Spreading smiles, one greeting at a time!
So, welcome to our corner of the web. Let's make the world a little bit brighter together!
*Boop beep*,
from Hi 🤖👋
How We Did It — Quick Start Guide
This guide will help you set up a Next.js blog project with Markdown support and a custom plugin to remove images from content snippets.
Prerequisites
- Node.js (v14 or later)
- npm or yarn
Setup
1. Initialize the Project
npx create-next-app@latest my-blog
cd my-blog
2. Install Dependencies
npm install gray-matter remark remark-html remark-strip-markdown unist-util-visit @tailwindcss/typography @tailwindcss/postcss7-compat
3. Setup Tailwind CSS
Follow the official Tailwind CSS installation guide: https://tailwindcss.com/docs/guides/nextjs
4. Project Structure
Ensure your project structure looks like this:
/my-blog
/posts
first-post.md
second-post.md
/src
/app
/blog
/[id]
page.tsx
page.tsx
/helpers
stripImages.ts
/public
/images
example.jpg
5. Create the Custom Plugin
Create a custom plugin to remove images from Markdown content.
helpers/stripImages.ts
:
import { Plugin } from 'unified';
import { Node } from 'unist';
import { visit } from 'unist-util-visit';
interface ParentNode extends Node {
children: Node[];
}
const stripImages: Plugin = () => {
return (tree: Node) => {
visit(tree, 'image', (node, index, parent) => {
if (parent && (parent as ParentNode).children) {
(parent as ParentNode).children.splice(index, 1);
}
});
};
};
export default stripImages;
6. Create Markdown Posts
Create your Markdown files in the /posts
directory.
posts/first-post.md
:
---
title: '1st Blog from a Startup in web4'
date: '2024-07-26'
image: 'https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0f3e7fe-4b3a-4d7a-b0d8-8ede0f45e789_1586x1038.png'
---
This is the first blog post content.
7. Create the Blog Landing Page
src/app/blog/page.tsx
:
import fs from 'fs';
import path from 'path';
import Link from 'next/link';
import matter from 'gray-matter';
import { remark } from 'remark';
import strip from 'strip-markdown';
import stripImages from '../../helpers/stripImages';
const postsDirectory = path.join(process.cwd(), 'posts');
interface Post {
id: string;
title: string;
date: string;
image: string;
snippet: string;
}
async function getPosts(): Promise<Post[]> {
const filenames = fs.readdirSync(postsDirectory);
const posts = await Promise.all(filenames.map(async (filename) => {
const filePath = path.join(postsDirectory, filename);
const fileContents = fs.readFileSync(filePath, 'utf8');
const { content, data } = matter(fileContents);
// Extract snippet from content, removing images
const processedContent = await remark().use(stripImages).use(strip).process(content);
const snippet = processedContent.toString().split(' ').slice(0, 20).join(' ') + '...';
return {
id: filename.replace(/\.md$/, ''),
title: data.title,
date: data.date,
image: data.image,
snippet,
} as Post;
}));
return posts;
}
const Blog = async () => {
const posts = await getPosts();
return (
<div className="container mx-auto px-4 py-8 max-w-7xl">
<h1 className="text-4xl font-bold mb-8" style={{color: '#fff'}}>the b00ps blog</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => {
const linkProps = { href: `/blog/${post.id}` };
return (
<div key={post.id} className="bg-white shadow-md rounded-lg overflow-hidden">
<Link {...linkProps} className="block">
<img src={post.image} alt={post.title} className="w-full h-64 object-cover"/>
<div className="p-6">
<h2 className="text-2xl font-bold mb-2 text-gray-800">{post.title}</h2>
<p className="text-sm text-gray-500 mb-4">{post.date}</p>
<p className="text-gray-700 mb-4">{post.snippet}</p>
<p className="text-blue-600">Read more...</p>
</div>
</Link>
</div>
);
})}
</div>
</div>
);
};
export default Blog;
8. Create the Blog Post Page
src/app/blog/[id]/page.tsx
:
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import remarkImages from 'remark-images';
import Link from 'next/link';
const postsDirectory = path.join(process.cwd(), 'posts');
interface PostData {
title: string;
date: string;
contentHtml: string;
image: string;
}
interface PostProps {
params: {
id: string;
};
}
async function getPostData(id: string): Promise<PostData> {
const filePath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(filePath, 'utf8');
const { content, data } = matter(fileContents);
const processedContent = await remark().use(html).use(remarkImages).process(content);
const contentHtml = processedContent.toString();
return {
title: data.title,
date: data.date,
contentHtml,
image: data.image,
};
}
const Post = async ({ params }: PostProps) => {
const postData = await getPostData(params.id);
return (
<div className="container mx-auto px-4 py-8 max-w-2xl" style={{color: '#000'}}>
<Link href="/blog" className="text-blue-600 hover:text-blue-800 mb-4 inline-block">← Back to Blog</Link>
<article className="bg-white shadow-md rounded-lg overflow-hidden">
<div className="p-6">
<h1 className="text-3xl font-bold mb-4 text-gray-800">{postData.title}</h1>
<p className="text-sm text-gray-500 mb-6">{postData.date}</p>
<div
className="prose prose-sm sm:prose lg:prose-lg mx-auto"
dangerouslySetInnerHTML={{ __html: postData.contentHtml }}
/>
</div>
</article>
</div>
);
};
export default Post;
Running the Project
Start the development server:
npm run dev
Visit http://localhost:3000/blog
to see your blog landing page with posts.
This README provides a quick start guide to set up your Next.js blog project with support for Markdown content and a custom plugin to remove images from content snippets. Adjust paths and configuration as needed based on your project's structure.
Hope it helps! b33p b00p!