June 2, 2024
O Wolfson
Let's create a content management system (CMS) for our MDX Blog, where blog posts are stored as static files.
We'll cover and handling form submissions for new blog entries, saving posts to the file system, generating a cache for efficient data retrieval.
Note: this interface will only be relevant in your development environment as you are saving files to the local file system.
Our CMS will:
Caching is crucial for enhancing performance and reducing the load on the file system. When dealing with static files, reading and parsing each file on every request can be inefficient, especially as the number of posts grows. By generating a cache, we can quickly access metadata and content without repeatedly accessing the file system.
We need a function to save the submitted form data as an MDX file. This function will:
Here's the saveFileLocally
function:
javascriptconst fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const { generatePostsCache } = require("./posts-utils");
/**
* Save form data as a local MDX file
* @param {Object} data - The form data to save
* @returns {string} - The generated slug for the new post
*/
export function saveFileLocally(data) {
console.log("Data received:", data.date);
const { date, title, categories, tags, ...rest } = data;
const projectRoot = process.cwd();
// Generate the initial filename and slug
let filename = `${title.toLowerCase().replace(/\s+/g, "-")}.mdx`;
let slug = `${title.toLowerCase().replace(/\s+/g, "-")}`;
let filePath = path.join(projectRoot, "data/posts", filename);
// Ensure the filename is unique
let counter = 1;
while (fs.existsSync(filePath)) {
filename = `${title.toLowerCase().replace(/\s+/g, "-")}-${counter}.mdx`;
filePath = path.join(projectRoot, "data/posts", filename);
counter++;
}
// Format the date
currentDate = (data.);
formattedDate = ;
formattedCategories = categories.( ).();
formattedTags = tags
.()
.( )
.();
fileContent = ;
fs.(filePath, fileContent, {
(err) {
.(, err);
} {
();
(, {
(error) {
.();
;
}
.();
.();
});
}
});
slug;
}
The cache script reads all MDX files, extracts the necessary metadata, and writes it to a JSON file. This cached data can then be quickly accessed, improving performance.
Here's the cachePosts.js
script:
javascriptimport fs from "fs";
import path from "path";
import matter from "gray-matter";
import { startOfDay } from "date-fns";
/**
* Generate a cache of all posts
* @returns {Array} - An array of post metadata
*/
export function generatePostsCache() {
const postsDirectory = path.join(process.cwd(), "data/posts");
const fileNames = fs
.readdirSync(postsDirectory)
.filter(
(fileName) => !fileName.startsWith(".") && fileName.endsWith(".mdx")
);
const currentDate = startOfDay(new Date()); // Get the start of the current day
const posts = fileNames
.map((fileName) => {
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data: frontMatter } = matter(fileContents);
const postDate = startOfDay(new Date(frontMatter.date)); // Get the start of the post's date
(postDate > currentDate) {
;
}
{
: fileName.(, ),
...frontMatter,
};
})
.();
cachePath = path.(process.(), );
fs.(cachePath, .(posts, , ));
posts;
}
We need an API endpoint to handle the POST requests from our form. This endpoint will call the saveFileLocally
function and respond with the path of the saved file or an error message.
Here's the implementation of the POST handler:
javascriptimport { saveFileLocally } from "@/lib/save-file-locally";
/*
* Handle POST requests to save form data as an MDX file
* @param {Request} req - The request object
* @returns {Response} - The response object
*/
export async function POST(req) {
if (req.method === "POST") {
try {
const data = await req.json();
const filePath = saveFileLocally(data); // Save the file and regenerate the cache
return new Response(JSON.stringify({ filePath }), {
headers: { "content-type": "application/json" },
});
} catch (error) {
console.error("Error:", error);
return new Response(
JSON.stringify({ message: "Error processing request" }),
{
status: 500,
headers: { "content-type": "application/json" },
}
);
}
} else {
return new Response(
JSON.stringify({ message: "Only POST requests are accepted" }),
{
headers: { "content-type": },
}
);
}
}
We'll create a form component that captures the user's input and sends it to our API endpoint. We'll use react-hook-form
for form handling and validation, and zod
for schema validation. Additionally, we'll add functionality to update and delete posts. FYI we are using Tailwind CSS for styling and shadcn/ui for most of the UI components.
Here's the CreatePostForm
component code:
jsx"use client";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { v4 as uuidv4 } from "uuid";
import { useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import DatePickerField from "@/components/date-picker";
import { MultiSelect } ;
formSchema = z.({
: z.(),
: z.().(),
: z.().(, { : }),
: z
.()
.(, { : }),
: z
.()
.(, { : }),
: z.(z.()).(),
: z.().(),
});
() {
[selectedValue, setSelectedValue] = ();
form = ({
: (formSchema),
: post || {
: (),
: ,
: ,
: ,
: ,
: [],
: ,
},
});
authorName = ;
router = ();
() {
endpoint = post ? : ;
submissionData = {
...values,
: authorName,
: post ? post. : (),
};
{
response = (endpoint, {
: ,
: { : },
: .(submissionData),
});
(!response.) ();
result = response.();
.(, result);
form.();
router.();
} (error) {
.(, error);
}
}
() {
{
response = (, {
: ,
: { : },
: .({ : post. }),
});
(!response.) ();
.();
router.();
} (error) {
.(, error);
}
}
(
);
}
We'll create a home screen component that fetches the cached posts and displays their titles. Clicking on a title will open the post in the CreatePostForm
.
HomeScreen.jsx
jsximport React, { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
export default function HomeScreen() {
const [posts, setPosts] = useState([]);
const router = useRouter();
useEffect(() => {
async function fetchPosts() {
const res = await fetch("/cache/posts.json");
const data = await res.json();
setPosts(data);
}
fetchPosts();
}, []);
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Blog Posts</h1>
<ul className="space-y-2">
{posts.map((post) => (
<li key={post.slug} className="flex justify-between items-center">
<Link href={`/edit/${}`}>
{post.title}
))}
);
}
We'll create an API endpoint to handle the deletion of posts.
deleteFile.js
javascriptconst fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const { generatePostsCache } = require("./posts-utils");
export async function POST(req) {
if (req.method === "POST") {
try {
const { path: filePath } = await req.json();
const fullPath = path.join(process.cwd(), "data/posts", filePath);
exec(`rm "${fullPath}"`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return new Response(
JSON.stringify({ message: "Error deleting file" }),
{
status: 500,
headers: { "content-type": "application/json" },
}
);
}
console.log("File deleted successfully");
generatePostsCache();
return (.({ : }), {
: { : },
});
});
} (error) {
.(, error);
(
.({ : }),
{
: ,
: { : },
}
);
}
} {
(
.({ : }),
{
: { : },
}
);
}
}
By following these steps, we've built a content management system for our MDX Blog. Users can submit new blog posts through a form, which are then saved as MDX files on the server. The posts cache is regenerated to ensure quick access to the latest posts, enhancing performance. Additionally, we added a home screen to list the titles of the blog posts, along with functionality to update and delete posts. This setup leverages the power of Next.js, MDX, and React to create a seamless and dynamic content creation experience in our local development environment.
If you have any questions or need further assistance, feel free to reach out!