in

Frosty CMS: Image Uploads 📷

Frosty CMS Logo

Up until now we have been serving images by hotlinking to images on other servers (specifically, this server, my blog server, not great). Now we are going to change that by implementing an actual image upload.

Packages

This is going to take two packages. Multer and Cloudinary.

Multer is the middleware that will handle the multipart/form-data submissions. Cloudinary is the package for the Cloudinary media hosting service. There are other ways to accomplish this, this is just what I’m going for. We get our packages setup and then we move on.

Upload Media Form

We need to update our form with an enctype for multipart/form-data. Thank you multer for handling this.

<form action="/blogs" method="POST" enctype="multipart/form-data">
            
            <div class="form-group">
                <label for="featuredImageUpload">
                    <h4>Featured Image</h4>
                </label>
                <input type="file" id="featuredImageUpload" name="blog[image]" accept="image/*" required>
            </div>

We have also changed our input type to file instead of string.

Schema Changes

In our schema for blogs we still have image set as a string, because we are going to be sending our images to Cloudinary and then they are going to be responding with a URL, which we can store in a string, along with an image ID, which will remain constant when we update our images.

const blogSchema = new mongoose.Schema({
    image: String,
    imageId: String,
    title: String,
    author: {
        id: {
            type: mongoose.Schema.Types.ObjectID,
            ref: "User"
        },
        username: String,
        firstname: String,
        lastname: String
    },
    date: {type: Date, default: Date.now},
    short: String,
    content: String,
    comments: [
      {
         type: mongoose.Schema.Types.ObjectId,
         ref: "Comment"
      }
    ]
});

Package Configuration

There is a decent amount of package configuration that we need to go through here.

// ***************************
// IMPORT PACKAGES
// ***************************
const express          = require("express"),
      router           = express.Router({mergeParams: true}),
      middleware       = require('../middleware'),
      moment           = require('moment'),
      multer           = require('multer'),
      storage          = multer.diskStorage({
          filename: (req, file, callback)=>{
              callback(null, Date.now() + file.originalname);
          }
      }),
      imageFilter      = (req, file, cb)=>{
        // accept image files only
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/i)) {
            return cb(new Error('Only image files are allowed!'), false);
        }
        cb(null, true);
        },
      upload           = multer({storage: storage, fileFilter: imageFilter}),    
      cloudinary       = require('cloudinary'),
      Blog             = require('../models/blogs');

// ***************************
// Cloudinary Config
// *************************** 
 cloudinary.config({ 
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 
  api_key: process.env.CLOUDINARY_API_KEY, 
  api_secret: process.env.CLOUDINARY_API_SECRET
});     

One particular thing to note is that we need to store our cloudinary config information in process.env variables with dotenv, which I covered in the linked post.

Route Changes

And of course we have big changes coming into our routes. We have a new middleware that sends our files to Cloudinary and then gets back our secure URL and imageID that we store as strings with our blog object. Here is the route for new blogs.

//----------------------------
// .POST routes
//----------------------------

// new blog: receive and save
router.post("/", middleware.isLoggedIn, upload.single('blog[image]'), (req, res) => {
    // sanitize inputs
    req.body.blog.title   = req.sanitize(req.body.blog.title);
    req.body.blog.short   = req.sanitize(req.body.blog.short);
    req.body.blog.content = req.sanitize(req.body.blog.content);
    
    // assign variables to incoming data
    let title   = req.body.blog.title,
        short   = req.body.blog.short,
        content = req.body.blog.content,
        date    = req.body.blog.date;
    
    // retriever user data
    let author = {
        id: req.user._id,
        username: req.user.username,
        firstname: req.user.firstname,
        lastname: req.user.lastname
    };
    
    // add cloudinary url for the image to the blog object under image property
    cloudinary.v2.uploader.upload(req.file.path, (error, result)=> {
        console.log(result, error);
        
        let image   = result.secure_url,
            imageId = result.public_id;
        
        // combine all data into new variable
        let newBlog = {title: title, image: image, imageId: imageId, short: short, content: content, date: date, author: author};
        
        // save combined data to new blog
        Blog.create(newBlog,(err, newDatabaseRecord) => {
            if(err){
                console.log(err);
                req.flash('error', "Failed to write post to database.");
                res.redirect('back');
                return;
            } else {
                console.log("Blog successfully saved to database.");
                console.log(newDatabaseRecord);
                req.flash('success', 'New blog saved to database.');
                // redirect back to blogs page
                 res.redirect("/");
                 return;
            }
        });
    });
});

and then our route for blog updating needs to first check if the image was changed, and only then delete the old image and upload a new one.

//----------------------------
// .PUT routes
//----------------------------

// edit blog
router.put("/:id",middleware.isLoggedIn, upload.single('blog[image]'), (req, res) => {
    // only upload new photo if requested
    Blog.findById(req.params.id, async function(err, blog){
       if(err){
           req.flash('error', err.message);
           res.redirect('back');
           return;
       } else {
           if (req.file) {
               try {
                   await cloudinary.v2.uploader.destroy(blog.imageId);
                   let newImage = await cloudinary.v2.uploader.upload(req.file.path);
                   blog.imageId = newImage.public_id;
                   blog.image = newImage.secure_url;
                   
               } catch(err) {
                   req.flash('error', err.message);
                   res.redirect('back');
                   return;
               }
           }
            // sanitize inputs
            req.body.blog.title = req.sanitize(req.body.blog.title);
            req.body.blog.short = req.sanitize(req.body.blog.short);
            req.body.blog.content = req.sanitize(req.body.blog.content);
            
            // assign variables to incoming data
            blog.title   = req.body.blog.title;
            blog.author  = blog.author;
            blog.short   = req.body.blog.short;
            blog.content = req.body.blog.content;
            blog.date    = req.body.blog.date;
            blog.imageId = blog.imageId;
            blog.image   = blog.image;
            
            blog.save();
            req.flash('success', "Blog updated.");
            res.redirect('/blogs/' + blog._id);
            return;
       }
    });
});    

And we now have the ability to upload files for our blogs.

Git Repo

Dark Mode

frosty (this link opens in a new window) by ncoughlin (this link opens in a new window)

Frosty is a simple Javascript only blog CMS built with Node, Express and Mongo

dotenv logo

Environment Variables With dotenv 🙊

Javascript logo

Javascript: This Keyword and Object Oriented Programming