Content Pipeline in Vite

AI Generated Content

This post was originally written by human but has been edited by AI to improve writing.

To create a content processing pipeline that runs within Vite's development server and build process, we can build a custom Vite plugin. This plugin leverages the Effect library to manage concurrent workflows, creating a robust and maintainable system.

This document outlines the integration mechanism, covering the plugin's lifecycle, its API, and the hot-reloading process.

API

The pipeline is exposed through a Vite plugin, collections, which you can configure in your vite.config.ts.

Synopsis:

collections({
contentDir: path.resolve(root, "src/contents"),
outDir: path.resolve(root, "data"),
})

The collections function accepts the following options:

  • contentDir: The directory containing the source content files.
  • outDir: The directory where generated files will be placed.

Reloading

When a file is modified during development, the plugin processes the change and notifies the client to reload the relevant parts of the application.

userBrowserfileVite pluginPipeline service edit filefile eventtrigger rebuildreturn routes to updatenotify route updatesdisplay updated content

RouteUpdate event

When a file changes, the plugin sends a RouteUpdate event to the client. This object's structure is defined as follows:

src/dev/types.ts
export interface RouteUpdate {
type: "reload" | "delete"
matchRoute: MatchRoute
}

MatchRoute is a data structure used to identify which application route is affected by the change. In a TanStack Router application, the useMatchRoute hook can use this data to determine if the current page needs to be updated.

Triggering reload

The Vite plugin's handleHotUpdate function triggers a reload by sending a custom HMR event:

function reload(entries: RouteUpdate[]) {
ctx.server.hot.send({
type: "custom",
event: "routes-reload",
data: {
entries,
},
})
}

For example, modifying a blog post source file sends the following payload over the WebSocket. This payload notifies the client to reload the post itself, as well as any pages that list posts:

ctx.server.hot.send({
type: "custom",
event: "routes-reload",
data: {
entries: [
{
type: "reload" as "reload" | "delete",
matchRoute: {
to: "/post/$lang/$slug",
params: {
slug: postMetadata.slug,
lang: postMetadata.language,
},
},
},
{
type: "reload",
matchRoute: {
to: "/post/$lang",
params: {
lang: postMetadata.language,
},
},
},
{
type: "reload",
matchRoute: {
to: "/post",
},
},
],
},
})

Receiving the reload event inside a React application

On the client, a custom React hook, useRouteReload, listens for these HMR events and invalidates the relevant data, prompting TanStack Router to refetch and re-render:

import type { RouteUpdate } from "@/dev/types"
import { useMatchRoute, useRouter } from "@tanstack/react-router"
import { useEffect } from "react"
export function useRouteReload() {
const router = useRouter()
const matchRoute = useMatchRoute()
useEffect(() => {
import.meta.hot!.on("routes-reload", ({ entries }: { entries: RouteUpdate[] }) => {
if (entries.some(entry => entry.type === "reload" && matchRoute(entry.matchRoute))) {
router.invalidate()
}
})
}, [matchRoute, router])
}

Support for remove events is not implemented yet.

Lifecycle Events

The Vite plugin hooks into Vite's lifecycle to manage the content pipeline. The process flow is illustrated below.

Vite Server Startconfig()Initialize runtimeCreate output directorybuildStart()Process all content filesGenerate collection dataDev ServerRunningFile Change EventhandleHotUpdate()Check file existenceProcess changed fileSend HMR eventFile Exists?handleFileChange()Process modified/added filehandleFileDeletion()Cleanup deleted file dataSend HMR EventNotify client of changes YesNo

The key phases are:

  • Initialization: Setting up the Effect runtime and creating necessary directories.
  • Initial Build: Processing all content files and generating collection data.
  • Hot Reload: Watching for file changes and incrementally updating collection data.