Setting up Velite for Processing Markdown Content Through MDX

Adding Velite to the NextJS, React with Radix UI and Shadcn UI project and configuring blog source for markdown using MDX


Required dependencies

We use velite for accessing the content of the MDX files in JavaScript.

# npm i velite --save

Creating the content directory

We store the MDX content for the blog read by Velite in a /content/blog directory. Each year has its own directory. The file name starts with the month and day. This structure is not mandatory, but rather to help organize and manage the blog. The directory structure looks like this:

...
├── components.json
├── content
│   └── blog
│       ├── 2023
│       │   └── 06-21-summer-solstice.mdx
│       └── 2024
│           └── 06-21-summer-solstice.mdx
├── next-env.d.ts
...

In order to allow the code to access the cotent directory, the following is added in tsconfig.json:

{
  "compilerOptions": {
    ...
    "paths": {
      "@/*": [
        "./src/*"
      ],
      "#site/content": [
        "./.velite"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

Configuring Velite

Velite configuration is placed in next.config.mjs:

/** @type {import('next').NextConfig} */
import { build } from 'velite';
 
class VeliteWebpackPlugin {
  static started = false;
  constructor(/** @type {import('velite').Options} */ options = {}) {
    this.options = options;
  }
  apply(/** @type {import('webpack').Compiler} */ compiler) {
    // executed three times in nextjs !!!
    // twice for the server (nodejs / edge runtime) and once for the client
    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); // start velite
    });
  }
}
 
const nextConfig = {
  output: 'export',
  // other next config here...
  webpack: (config) => {
    config.plugins.push(new VeliteWebpackPlugin());
    return config;
  },
};
 
export default nextConfig;

Velite creates temporary and cache files as it works. Those files do not need to be stored in git, so we amend /.gitignore to include:

...
# local env files
.env*.local
 
# vercel
.vercel
 
# velite files
.velite
 
# typescript
*.tsbuildinfo
next-env.d.ts
 

Creating the Velite configuration

The source content for the blog and the frontmatter properties that are being retrieved are specified in velite.config.ts:

import { defineConfig, defineCollection, s } from 'velite';
 
const computedFields = <T extends { slug: string }>(data: T) => ({
  ...data,
  slugAsParams: data.slug.split('/').slice(1).join('/'),
});
 
const posts = defineCollection({
  name: 'Post',
  pattern: 'blog/**/*.mdx',
  schema: s
    .object({
      slug: s.path(),
      title: s.string().max(99),
      description: s.string().max(999).optional(),
      date: s.isodate(),
      published: s.boolean().default(true),
      body: s.mdx(),
    })
    .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: [],
  },
});

Running in development with Velite

When we run

# npm run dev

we see that Velite is running:

  ▲ Next.js 14.2.4
  - Local:        http://localhost:3000
 
 Starting...
[VELITE] build finished in 140.94ms
[VELITE] watching for changes in 'content'
 Ready in 1269ms
<w> [webpack.cache.PackFileCacheStrategy/webpack.FileSystemInfo] Parsing of /private/var/prj/static-nextjs/node_modules/velite/dist/velite-DLg1KhQl.js for build dependencies failed at 'import(configUrl.href)'.
<w> Build dependencies behind this expression are ignored and might cause incorrect cache invalidation.

Velite creates a working directory .velite where it stores cached and temporary files:

...
├── .gitignore
├── .velite
│   ├── index.d.ts
│   ├── index.js
│   └── posts.json
├── README.md
├── components.json
...


Last Updated: