Cloudinary Blog

How to build a CMS with Adonis: A Laravel-like MVC framework for Node - Part 2

Building an Adonis-based CMS including image management

Even though Node is fun, easy and cheap to work with, we spend a lot of time writing boilerplate codes because structure and organization is missing.

In part 1, we discussed the basics of Adonis, including how to setup Adonis projects, and create migrations, models, a few routes, and a controller to test the creation of new posts.

Now, let’s extend what we already know to reading posts, updating existing posts, deleting posts, and adding an image upload feature.

Reading existing posts

With the create feature implemented, we must have created posts in our store. These posts will be read from our store and displayed on the browser.

We need to update the index route to point to a controller, rather than sending a view directly as it already does:

// ./app/Http/routes.js
...
// What we had:
// Route.on('/').render('welcome');
// Update to:
Route.get('/', 'PostController.index');
...

Next, we add an action method to our controller named index. This method will be responsible for fetching the data from our model and sending the data to our view:

// ./app/Http/Controllers/PostController.js
...
* index(request, response) {
    const posts = yield Post.all();
    yield response.sendView('post/index', {posts: posts.toJSON()});
}
...

The route points to an existing method, which sends a view with our retrieved data. Let’s create this view:

<h2>Articles <a href="/new" class="ui blue button">New Post</a></h2>
<div class="ards">
  {% for post in posts %}
  <div class="card">
    <div class="content">
        <a class="header">{{post.title}}</a>
        <div class="meta">
            <span class="date">{{post.created_at}}</span>
        </div>
        <div class="description">
        {{post.body.substring(0, 50)}}...
        </div>
    </div>
  </div>
  % else %}
  <h3> No posts found </h3>
  {% endfor %}
</div>

Adonis Post

Adonis Post Title

Reading a single post

One other form of showing posts is reading and displaying a single post, which gives us more room for the details about the post. To do this, you need a route and a controller. Here is the route:

// ./app/Http/routes.js
Route.get('/post/:id', 'PostController.read');

The :id part of the URL is a placeholder for the route parameter. The parameter will be a unique value that points to a given post.

// ./app/Http/Controllers/PostController.js
* read(request, response) {
     // Receive parameter from request
     const id = request.param('id');
     // Find id with request parameter
     const post = yield Post.find(id);
     yield response.sendView('post/read', {post: post});
 }

The controller’s read action method receives the id parameter from the request object. We then use the model’s find method to find a post based on the value passed in.

The view sent is named read in the post folder and we are passing the post data down to the view as well:

<h2>{{post.title}}</h2>
<small>{{post.created_at}}</small>
<p>{{post.body}}</p>

Adonis Post Second Title

You can update the link for each post on the home page to point to the read URL:

<a class="header" href="/post/{{post.id}}">{{post.title}}</a>

Updating posts

What happens when we realize that our post requires an update? We can create a form just like we did for the new post form, but this time we will send the existing post to the form. First, we need to specify some routes, as usual:

// ./app/Http/routes.js
// Edit form
Route.get('/edit/:id', 'PostController.edit');
Route.post('/update', 'PostController.update');

With respect to the routes above, the following action methods will serve as handlers for both routes:

* edit(request, response) {
    const id = request.param('id');
    const post = yield Post.find(id);
    yield response.sendView('post/edit', {post: post});
}
* update(request, response) {
    var postData = request.only('id', 'title', 'body');
    const id = postData.id;
    const post = yield Post.find(id);
    // Update and save post
    post.fill(postData);
    yield post.save();
    // Go home
    response.redirect('/');
}

The edit action method sends a view. This view will hold our form template for editing the selected post:

  <h2>{{post.title}}</h2>
  {{ form.open({action: 'PostController.update'}) }}

    {{ csrfField }}

    <div class="ui form">
        <div class="field">
            {{ form.label('Title') }}
            {{ form.text('title', post.title) }}
        </div>

        <div class="field">
            {{ form.label('Body') }}
            {{ form.textarea('body', post.body) }}
        </div>

        {{form.hidden('id', post.id)}}

        {{ form.submit('Create', 'create', { class: 'ui blue button' }) }}
    </div>

  {{ form.close() }}

The default values are set using the existing values in our store. The post’s id is also available on the form via a hidden input.

For each post listed on the home page, we'll also add an edit button that points to the relevant edit URL:

...
<a class="ui basic green button" href="/edit/{{post.id}}">Edit</a>
...

Deleting posts

The route for deleting post is very similar to the read and edit routes. It takes a parameter for searching the post to be removed:

Route.get('/delete/:id', 'PostController.delete');

Note: Best practices suggest that you do not use GET to update state as we are doing right now. For the sake of this demo’s simplicity, we can overlook that practice.

The action method on the controller is delete and it finds the post based on the id parameter and then deletes the found post:

* delete(request, response) {
    var post = yield Post.find(request.param('id'));
    yield post.delete();
    response.redirect('/');
}

We can add a link to the post list to point to the delete URL:

<a class="ui basic red button" href="/delete/{{post.id}}">Delete</a>

Featured image uploads

Contents are always accompanied with images. In the case of a CMS, these images could be embedded right inside the content or serve as a banner to a given content. The banner is usually known as a featured image.

Cloudinary is the image management back-end for web and mobile developers. With Cloudinary, we can add images to our content with ease, thereby saving as the hassle of managing storage, as well as image uploads, downloads, manipulation, delivery and administration.

With the Cloudinary free account, we can begin using these features in our projects. Let’s see how by adding a featured image to our posts.

After you set up a Cloudinary account, you can create an upload preset. Upload presets enable you to centrally define a set of image upload options instead of specifying them in each upload call. We will use the preset when making an upload request.

Cloudinary provides JavaScript plugins that make image upload to the Cloudinary server very easy. Here’s how we include these scripts:

<script src='https://cdn.jsdelivr.net/jquery.cloudinary/1.0.18/jquery.cloudinary.js' type='text/javascript'></script>
<script src="//widget.cloudinary.com/global/all.js" type="text/javascript"></script>
<script src="/script.js"></script>

The plugin depends on jQuery, so we also added our defined script.js file to the setup.

In the script.js file, we can start implementing the upload logic:

$(function() {
    // Configure Cloudinary
    // with credentials available on
    // your Cloudinary account dashboard
    $.cloudinary.config({ cloud_name: 'YOUR_CLOUD_NAME', api_key: 'YOUR_API_KEY'});

    // Upload button
    var uploadButton = $('#upload-button');
    var canvas = $('#canvas');
    var imageInput = $('#image-input');
    // Upload button event
    uploadButton.on('click', function(e){
        // Initiate upload
        cloudinary.openUploadWidget({ cloud_name: 'christekh', upload_preset: 'idcidr0h', tags: ['cgal']}, 
        function(error, result) { 
            if(error) console.log(error);
            // If NO error, log image data to console
            var id = result[0].public_id;
            canvas.html(procesImage(id));
            imageInput.val($.cloudinary.url(id, {}));
        });
    });
})

function procesImage(id) {
    var options = {
        client_hints: true,
    };
    return '<img src="'+ $.cloudinary.url(id, options) +'" style="width: 100%; height: auto"/>';
}

The above listens to a click event on a button in our view. When the button is clicked, the upload process starts. After an image is uploaded and returned, the image is displayed above the form. The URL is also embedded in a hidden input so it can be sent to the server and stored for future use, as well.

Going back to the posts/new view we already created, we can extend it to handle the logic we have prepared:

<h2>New Post</h2>
    <div class="paint_container" id="canvas">
        <!-- Canvas to drop image after processing -->
    </div>
  {{ form.open({action: 'PostController.create'}) }}

    {{ csrfField }}

    <div class="ui form">
        <div class="field">
            {{ form.label('Title') }}
            {{ form.text('title', null) }}
        </div>

        <div class="field">
            {{ form.label('Body') }}
            {{ form.textarea('body', null) }}
        </div>

        <input type="hidden" id="image-input" name="image">

        <button id="upload-button" class="ui purple button" type="button">Upload Featured Image</button>

        {{ form.submit('Create', 'create', { class: 'ui blue button' }) }}
    </div>

  {{ form.close() }}

Adonis New Post

The image also gets stored in our database, so we can also display it on the home page where we list the posts:

...
<div class="card">
    <div class="image">
        <img src="{{post.image}}" syyle="width: 100%; heigth: auto;">
    </div>
   ...

Adonis Finished Post

Have a look at the complete demo project.

Conclusion

Adonis is an awesome framework that is great to work with because of its simplicity. Media management can cause you sleepless nights, not just with Adonis, but any server-side framework. Cloudinary eliminates the stress and shortens the time you spend on media management, enabling you to focus on building out the other aspects of your application.

Christian Nwamba Christian Nwamba is a code beast, with a passion for instructing computers and understanding it's language. In his next life, Chris hopes to remain a computer programmer.

Recent Blog Posts

Hipcamp Optimizes Images and Improves Page Load Times With Cloudinary

When creating a website that allows campers to discover great destinations, Hipcamp put a strong emphasis on featuring high-quality images that showcased the list of beautiful locations, regardless of whether users accessed the site on a desktop, tablet, or phone. Since 2015, Hipcamp has relied on Cloudinary’s image management solution to automate cropping and image optimization, enabling instant public delivery of photos, automatic tagging based on content recognition, and faster loading of webpages. In addition, Hipcamp was able to maintain the high standards it holds for the look and feel of its website.

Read more
New Image File Format: FUIF: Why Do We Need a New Image Format

In my last post, I introduced FUIF, a new, free, and universal image format I’ve created. In this post and other follow-up pieces, I will explain the why, what, and how of FUIF.

Even though JPEG is still the most widely-used image file format on the web, it has limitations, especially the subset of the format that has been implemented in browsers and that has, therefore, become the de facto standard. Because JPEG has a relatively verbose header, it cannot be used (at least not as is) for low-quality image placeholders (LQIP), for which you need a budget of a few hundred bytes. JPEG cannot encode alpha channels (transparency); it is restricted to 8 bits per channel; and its entropy coding is no longer state of the art. Also, JPEG is not fully “responsive by design.” There is no easy way to find a file’s truncation offsets and it is limited to a 1:8 downscale (the DC coefficients). If you want to use the same file for an 8K UHD display (7,680 pixels wide) and for a smart watch (320 pixels wide), 1:8 is not enough. And finally, JPEG does not work well with nonphotographic images and cannot do fully lossless compression.

Read more
 New Image File Format: FUIF:Lossy, Lossless, and Free

I've been working to create a new image format, which I'm calling FUIF, or Free Universal Image Format. That’s a rather pretentious name, I know. But I couldn’t call it the Free Lossy Image Format (FLIF) because that acronym is not available any more (see below) and FUIF can do lossless, too, so it wouldn’t be accurate either.

Read more
Optimizing Video Streaming and Delivery: Q&A with Doug Sillars

Doug Sillars, a digital nomad and a freelance mobile-performance expert, answers questions about video streaming and delivery, website optimization, and more.

Doug Sillars, a freelance mobile-performance expert and developer advocate, is a Google Developer Expert and the author of O’Reilly’s High Performance Android Apps. Given his extensive travels across the globe—from the UK to Siberia—with his wife, kids, and 11-year-old dog, Max, he has been referred to as a “digital nomad.” So far in 2018, Doug has spoken at more than 75 meetups and conferences!

Read more