How to build a blog with Next.js and Velite.js

Written by
auth-avtHieu.BuiMinh
Published onNovember 18, 2024
Views100
Comments100

Preface

Velite is a powerful configuration tool for building type-safe data layer for Next.js with Zod schema, allowing developers to manage their apps with streamlined settings. Here, we will explore how to set up Velite for a Next.js project and enhance the development process.

The packages to be used:

  • next Next.js framework
  • velite Handling and loading mdx content
  • tailwindcss A utility-first CSS framework packed with classes

Why Use Velite in Next.js?

  • Efficiency: Simplifies configuration management with structured files.
  • Scalability: Ideal for projects with complex routing or multilingual setups.
  • Developer-Friendly: Easy to integrate with minimal setup.

Step-by-Step Guide

Getting started with Next js

1. Create a Next js project

Use the following commands to create a Next.js project and navigate to it:

bash
npx create-next-app@latest my-next-app
# or
cd my-next-app

2. Run Development Server

Start the development server:

bash
npm run dev

Visit http://localhost:3000.

Read the Next.js Documentation:

Velite configuration

1. Install Velite

To get started, ensure you have Node.js installed. Then, install Velite using npm or yarn:

bash
npm install velite -D
# or
yarn add velite -D

2. Folder structured

page.tsx
page.tsx
layout.tsx

heading.tsx
mdx.tsx

hello-world.mdx

...velite auto genarating json files
blog.json
package.json
next.config.mjs
velite.config.ts

3. In next.config.mjs

Config for Velite plugins wiht webpack

next.config.mjs
import { build } from 'velite'
 
/** @type {import('next').NextConfig} */
// eslint-disable-next-line import/no-anonymous-default-export
export default {
	webpack: (config) => {
		config.plugins.push(new VeliteWebpackPlugin())
		return config
	},
}
 
class VeliteWebpackPlugin {
	static started = false
	constructor(/** @type {import('velite').Options} */ options = {}) {
		this.options = options
	}
	apply(/** @type {import('webpack').Compiler} */ compiler) {
		compiler.hooks.beforeCompile.tapPromise('VeliteWebpackPlugin', async () => {
			if (VeliteWebpackPlugin.started) return
			VeliteWebpackPlugin.started = true
			const dev = compiler.options.mode === 'development'
			this.options.watch = this.options.watch ?? dev
			this.options.clean = this.options.clean ?? !dev
			await build(this.options)
		})
	}
}

4. Velite configuration and Define Collections in velite.config.ts

After creating a velite.config.ts file in the root directory of your project to define collections config.

Velite uses velite.config.js/ts as the config file. You can see it in the root directory of your project. Now we can put our blog in to velite defineConfig function

velite.config.ts
import { defineCollection, defineConfig, s } from 'velite'
 
export const computedFields = <T extends { slug: string }>(data: T) => {
	return { ...data, slugAsParams: data.slug.split('/').slice(1).join('/') }
	// blog/hello-world => ['blog', 'hello-world'] => ['hello-world] => '/hello-world'
}
 
const posts = defineCollection({
	name: 'Post', // name of colection
	pattern: 'blog/**/*.mdx', // watch all mdx file in blog folder
	schema: s
		.object({
			id: s.string(),
			slug: s.path(), // return blog path
			title: s.string().max(999),
			date: s.isodate(),
			lastUpdated: s.isodate().optional(),
			metadata: s.metadata().optional(),
			description: s.string().max(999).optional(),
			published: s.boolean().default(true),
			hashTags: s
				.object({
					category: s.string(),
					tags: s.array(s.string()),
				})
				.optional(),
			body: s.mdx(), // mdx content
			toc: s.toc({ tight: true, ordered: true, maxDepth: 6 }), // toc stand for table-of-content
			//slugAsParams <=> needed transform
		})
		.transform(computedFields),
})
 
export default defineConfig({
	root: 'content',
	output: {
		data: '.velite',
		assets: 'public/static',
		base: '/static/',
		name: '[name]-[hash:6].[ext]',
		clean: true,
	},
	collections: { posts },
	mdx: {
		rehypePlugins: [],
		remarkPlugins: [],
	},
})

5. Create the first content 🔥

content/blog/hello-world.mdx
---
id: 'xxxxxxxx-xxxx-xxxx-...'
title: 'This is the first post'
date: '2024-11-18T00:00:00Z'
lastUpdated: '2024-11-18T00:00:00Z'
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
published: true
hashTags: { category: 'Blog', tags: ['Lorem', 'Ipsum'] }
---
 
`##` Hello world
 
`###` This is my first post
 
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
 

components/mdx.tsx
import Link from 'next/link'
import * as runtime from 'react/jsx-runtime'
 
import Heading from '@/components/heading'
 
const useMDXComponent = (code: string) => {
	const fn = new Function(code)
	return fn({ ...runtime }).default
}
 
const components = {
	// overwrite heading or other components here...
	h2: (props: React.ComponentPropsWithoutRef<'h2'>) => <Heading as="h2" {...props} />,
	h3: (props: React.ComponentPropsWithoutRef<'h3'>) => <Heading as="h3" {...props} />,
	h4: (props: React.ComponentPropsWithoutRef<'h4'>) => <Heading as="h4" {...props} />,
	h5: (props: React.ComponentPropsWithoutRef<'h5'>) => <Heading as="h5" {...props} />,
	h6: (props: React.ComponentPropsWithoutRef<'h6'>) => <Heading as="h6" {...props} />,
	a: (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
		const { children, href, ...rest } = props
 
		if (!href) {
			return (
				<span className="text-muted-foreground line-through transition-colors hover:text-foreground" {...rest}>
					{children}
				</span>
			)
		}
 
		return (
			<Link
				className="font-bold text-green-600 no-underline transition-colors hover:text-foreground hover:underline dark:text-green-400"
				href={href}
				{...rest}
			>
				{children}
			</Link>
		)
	},
}
 
interface MdxProps {
	code: string
}
 
export function MDXContent({ code }: MdxProps) {
	const Component = useMDXComponent(code)
	return <Component components={components} />
}

app/blog/page.tsx
import Link from 'next/link'
 
import { posts } from '@/.velite'
 
function BlogPage() {
	return (
		<div className="flex h-screen w-screen items-center justify-center">
			{posts.map((post) => {
				return (
					<Link key={post.id} href={post.slug}>
						<div className="flex h-[200px] w-[400px] flex-col justify-between rounded-md border p-4">
							<p className="text-md font-bold">{post.title}</p>
							<p className="text-sm">{post.description}</p>
							<span>{post.date}</span>
						</div>
					</Link>
				)
			})}
		</div>
	)
}
 
export default BlogPage

app/blog/[...slug]/page.tsx
import { notFound } from 'next/navigation'
 
import { posts } from '@/.velite'
import { MDXContent } from '@/components/mdx'
 
interface PostPageProps {
	params: Promise<{ slug: string[] }>
}
 
async function getPostFromParams(params: PostPageProps['params']) {
	const resolvedParams = await params
	const slug = resolvedParams.slug.join('/')
	const post = posts.find((post) => post.slugAsParams === slug)
 
	return post
}
 
export default async function BlogDetailPageView({ params }: PostPageProps) {
	const post = await getPostFromParams(params)
 
	if (!post || !post.published) {
		notFound()
	}
 
	return (
		<div className="relative flex justify-between gap-10">
			<article>
				<MDXContent code={post.body} />
			</article>
		</div>
	)
}

bash
npm i @tailwindcss/typography
tailwind.config.ts
const config: Config = {
	plugins: [require('@tailwindcss/typography')],
}

Now you can use the prose classes to add sensible typography styles to any vanilla HTML:

tailwind.config.ts
export default async function BlogDetailPageView({ params }: PostPageProps) {
	const post = await getPostFromParams(params)
 
	if (!post || !post.published) {
		notFound()
	}
 
	return (
		<div className="relative flex justify-between gap-10">
			<article className="prose lg:prose-xl">
				<MDXContent code={post.body} />
			</article>
		</div>
	)
}

6. Get all blogs

After creating your first content .mdx, check your folder structured, you would see a folder named .velite, inside it you can see 2 files named index.d.ts and blog.json, that means ours hello-world.mdx have been parsed successfuly.

Inside

Final result

first-post.png

first-post-detail.png

Conclusion

Velite makes your projects easier to maintain and expand, especially for growing applications.

By combining Next.js with Velite.js, you can efficiently build a fast, SEO-friendly blog with minimal configuration. This guide has covered the essential steps to get you started, from creating the Next.js project and installing Velite to defining blog content and rendering posts dynamically. The result is a streamlined, type-safe blog development experience that scales well for future projects.

With Next.js and Velite.js, you gain full control over your content, ensuring a smooth development process and an optimized blog that ranks well on search engines.

Happy coding (づ ̄3 ̄)づ╭❤️~

Last updated: November 18, 2024