Easy image management for MDX blogs

by
@_indyman

Writing a blog should be a seamless and enjoyable process. However, I found myself constantly frustrated with the high-friction workflow involved in adding images to my MD(X) blog files. The process typically involved several tedious steps:

  1. Copy the image to the clipboard.
  2. Use the VSCode "paste image" extension to paste the image into the repository root
  3. Move the file to the public/static directory.
  4. Rename the file.
  5. Go to the MDX file, use the Image component, and point it to /static/imagename.

This process was not only time-consuming but also detracted from the pleasure of writing articles. So I went looking for CMS solutions, which I didn't know much about but felt promising.

Exploring CMS Solutions

I explored several CMS solutions, including:

  • Ghost
  • Strapi
  • CraftCMS

While these platforms offered a lot of functionality, they introduced new challenges. For instance, you often need to pay for cloud hosting or manage self-hosting, which involves dealing with backups for both content (usually in a database) and media files (images and videos). Additionally, integrating the CMS content into a Next.js app requires making API calls, which adds complexity to the build process and necessitates environment variable configuration and deployment triggers.

My Experience with Ghost

Ghost provided an excellent WYSIWYG editor and robust metadata management, but it had significant drawbacks for my use case:

  1. Headless CMS Limitations: Ghost is designed to expose a website, and while you can set it to "private mode," this causes the API to stop returning metadata (keywords, images) for articles. This seems like a bug and was a deal-breaker for me.
  2. Inconsistent API Responses: Occasionally, the API would return an empty list of blogs, resulting in builds with an empty "Blog" section. Rebuilding the app would sometimes fix this, but after several failed builds, I lost confidence in this approach.

Ultimately, I missed the simplicity of having content and assets colocated with my app. A straightforward GitHub repository setup, without the need for complex environment variables, API calls or backups felt better suited to my needs.

Automating the Image Management Process

Fed up with the manual steps involved in image management, I decided to automate the process. My goal was to create a simple script that would:

  1. Run directly from my Next.js project using npm run <myscript>.
  2. Prompt for an image name and path, with default values.
  3. Paste the image from the clipboard to the desired destination.
  4. Generate the JSX code boilerplate pointing to the image.
  5. Copy the generated JSX to my clipboard.

Developing the Script

I created a scripts/pasteimg.ts file and began writing the script, leveraging GPT to expedite the process. One challenge was getting GPT to handle "taking the image from the clipboard," as it initially suggested using clipboardy, which isn't compatible with images.

Fortunately, I discovered the npm package save-clipboard-image, which uses OSX native commands and perfectly fit my needs.

After spending a few minutes troubleshooting errors related to ESM modules while trying to run the script with ts-node, a recommendation to use tsx resolved the issue instantly.

The Final Result

Here's a demonstration of the script in action:

And the code behind it:

Note: Only works on MacOS, you'll need to adjust the script for Windows or Linux.

import { saveClipboardImage } from "save-clipboard-image";
import inquirer from "inquirer";
import * as path from "path";
import * as fs from "fs";
import clipboardy from "clipboardy";

const NEXT_STATIC_DIR = "/static";
const NEXT_STATIC_PATH = path.join(process.cwd(), "/public", NEXT_STATIC_DIR);

async function run() {
  // Ask for the image name
  const { imageName } = await inquirer.prompt({
    name: "imageName",
    type: "input",
    message: "Enter the name of the image (without extension):",
    validate: (input: string) =>
      input.trim() !== "" ? true : "Image name cannot be empty",
  });

  // Ask for the path with a default value
  const { subdir } = await inquirer.prompt({
    name: "subdir",
    type: "input",
    message: "Enter the target directory:",
    default: "",
  });

  const fullTargetDirectory = path.join(NEXT_STATIC_PATH, subdir);

  // Ensure the directory exists
  if (!fs.existsSync(fullTargetDirectory)) {
    fs.mkdirSync(fullTargetDirectory, { recursive: true });
  }

  // Save the image from the clipboard
  try {
    await saveClipboardImage(fullTargetDirectory, imageName);
  } catch (error) {
    // No image in clipboard, explain and exit
    console.error("No image found in clipboard, please copy an image first.");
    return;
  }

  // Construct the full path
  const fullImagePath = path.join(fullTargetDirectory, `${imageName}.png`);

  const relativeImagePath = path.join(NEXT_STATIC_DIR, imageName + ".png");

  const jsxCode = `
<Image
  src="${relativeImagePath}"
  alt="${imageName}"
  width={500}
  height={200}
  className="text-center shadow rounded-md"
/>`;

  // Copy the generated JSX code to the clipboard using clipboardy
  clipboardy.writeSync(jsxCode);

  // Log success in terminal + summary (image path and JSX code)
  console.log("Image saved successfully!");
  console.log(`Image path: ${fullImagePath}`);
  console.log(`JSX code copied to clipboard: ${jsxCode}`);
}

run();

Conclusion

I'm thrilled with this custom script. It significantly simplifies the process of adding images to my blog, allowing me to focus on writing. Next.js takes care of optimizing the images for various screen sizes, and by committing both the content and media to the same repository, I no longer worry about backups or synchronization issues.

Want to learn how to self-host a CMS or a Next.js app?
Stay tuned for the upcoming course! 🚀