Create your account

Already have an account? Login here

Note: By joining, you will receive periodic emails from Coursetro. You can unsubscribe from these emails.

Create account

Adonis 4 Tutorial - Learn Adonis 4 in this Crash Course

By Gary simon - Sep 11, 2018

AdonisJS is basically a clone of the popular PHP-based Laravel framework. The difference however comes when you consider Adonis is based on Node.js. If you're familiar with Laravel and the MVC approach to development, learning Adonis will be a breeze.

In this crash course, you're going to learn Adonis 4.1 by creating a project from scratch, where we will demonstrate all of the basics associated with it.

Let's get started!

If you prefer watching a video..

Be sure to Subscribe to the Official Coursetro Youtube Channel for more videos.

Installation

First, you're going to need to install the Adonis CLI in your command line or console:

> npm i --global @adonisjs/cli

Then, hop into your preferred projects folder and run the following command:

> adonis new jobpostr

jobpostr is the name of our app. 

After completion, issue the following commands:

> cd jobpostr
> adonis serve --dev

Great! You should now be able to visit your project in the browser at http://127.0.0.1:3333/

Adonis Templating (The View in MVC)

Open up the jobpostr folder your preferred code editor. 

First, you might wonder where the template and graphics are coming from when you viewed the site in the browser. We know from the URL that the route is the home route / so, let's find out where that's taken care of.

Open up /start/routes.js and you will notice the following code:

const Route = use('Route')
Route.on('/').render('welcome')

This is telling Adonis that when the root of the site is loaded, render a template/view called welcome.

That welcome template can be found in /resources/views/welcome.edge. Here, you will find the associated HTML for the page you see in your browser.

Also notice on line 6 we have this code:

  {{ style('style') }}

That's a quick and easy Adonis helper function that allows you to reference CSS files with the style() function. It's referencing a CSS file called style, which can be found in /public/style.css.

For now, let's focus on the view and structuring out how our views will be situated with a navigation and content section.

Rename welcome.edge to index.edge and create a new folder in /resources/views called layouts with a file inside of it called main.edge.

main.edge will be responsible for storing the head information and the basic layout that we're going to use. This will help us avoid html duplication.

Inside main.edge, paste the following contents:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    {{ style('style') }}

    @!section('extracss')

    <title>
        @!section('title')
    </title>
</head>
<body>
    <header>
        <a href="/" id="logo">JobPostr</a>
        <nav>
            <ul>
                <li><a href="/">Jobs</a></li>
                <li><a href="/login">Login</a></li>
                <li><a href="/signup">Signup</a></li>
            </ul>
        </nav>
    </header>

    <div class="container">
        @!section('content')
    </div>
</body>
</html>

A few things are happening here, but most importantly, we see @!section('title') and content. Both of these are placeholders, and are therefore dynamic. We can define these content sections in the other edge templates, and their contents will be placed inside of these placeholders.

The extracss section will be used later when we create additional edge templates that require additional CSS imports.

Open our index.edge file and paste the following:

@layout('layouts.main')

@section('title')
JobPostr - Post your Jobs 
@endsection

@section('content')
  <h1>All Jobs</h1>

  <div class="job-container">
    <div class="blank"></div>
    <div class="job-info">
      <h3>Job Title</h3>
      <p>Future job description can go here.</p>
    </div>
  </div>
@endsection

First, we tell the edge system to use the layouts.main edge file, and then we define our sections that are defined within main.edge. For now, we don't have anything dynamic, so the job listing information is simply static. But we'll come back to that later.

Next, visit the /start/routes.js file and update line 18 to:

Route.on('/').render('index')

Refresh the browser, and you will see our content. But everything looks really ugly right now, so let's fix that by stepping into public/style.css and pasting the following content:

@import url('https://fonts.googleapis.com/css?family=Montserrat:300,700');

html, body {
  height: 100%;
  width: 100%;
}

body {
  font-family: 'Montserrat', sans-serif;
  font-weight: 300;
  background-color: #EFEFEF;
}

* {
  margin: 0;
  padding: 0;
}
ul {
  list-style-type: none;
}

header {
  background-color: #303C49;
  display: grid;
  grid-template-columns: 30% auto;
}
header a {
  color: white;
  text-decoration: none;
  text-transform: uppercase;
}
#logo {
  font-weight: bold;
  font-size: 1.3em;
  padding: 1.5em 0 0 2em;
  margin: 0;
}
nav {
  justify-self: right;
}
nav ul li {
  display: inline;
}
nav ul li a {
  padding: 2em;
  display: inline-block;
}
nav ul li a:hover {
  background: #455668;
}
.container {
  width: calc(100% - 4em);
  padding: 2em;
  margin-top: 2em;
}
h1 {
  margin-bottom: 1em;
}
.job-container, .job-container2 {
  background: white;
  border-radius: 7px;
  padding: 1em;
  margin-bottom: 10px;
  display: grid;
  grid-template-columns: 120px auto;
}
.blank {
  width: 100px;
  height: 100px;
  border: 1px solid lightgray;
  border-radius: 5px;
}

It's quite a bit, but nothing special is happening here. This is what the result should look like in the browser after refreshing:

Great!

Databases

Now that we've touched on some of the View related stuff, let's talk about the backend stuff. Adonis 5 supports the following databases to store your data:

  • PostgreSQL

  • SQLite

  • MySQL

  • MariaDB

  • Oracle

  • MSSQL

For this tutorial, we're going to use MySQL. This will require installing MySQL on your machine if you plan to develop locally. I won't be covering how to get this set up and running, so, perform some Google searches on installing MySQL if you have a hard time. 

Visit the /.env file and change DB_CONNECTION to mysql: along with setting the database to jobpostr:

DB_CONNECTION=mysql
DB_DATABASE=jobpostr

This will tell Adonis that we want to use MySQL as our database.

Next, let's use the command line to install mysql:

> adonis install mysql

Now, we have to create the jobpostr database in the command line. After installing MySQL and ensuring you have access to the mysql command from the command line, type:

> mysql -u root -p
:: hit enter when prompted for a password ::

mysql> create database jobpostr;
mysql> exit;

After running the final command, our database will be created.

If you installed MySQL Workbench and open it up, you wil see the new jobpostr database listed in your databases!

Next, how do we actually populate this database with tables, data and such? That's done with the help of Adonis Migrations.

Migrations

Migration files allow you to create and delete tables. 

Open up /database/migrations and you will see two migrations already created. Open up the _user.js migration file.

You will see we have both and up() and down() method. Up is for creating or altering a table of some sort, and down is for rolling back the changes made in Up. 

The way we run these migrations is through the console:

> adonis migration:run

output:

migrate: 1503248427885_user.js
migrate: 1503248427886_token.js
Database migrated successfully in 613 ms

If you use MySQL workbench of the mysql command line tool, you will see that our database now has 3 tables. One for users, tokens, and schemas. The schemas table keeps track of which database migrations were ran!

Let's make a new migration with the Adonis CLI for storing our job posting data:

> adonis make:migration jobs
> Choose an action Create table
√ create  database\migrations\1536680712439_jobs_schema.js

When prompted, choose create table and hit enter. 

This results in a new migrations file being created in the /database/migrations folder. Open up the new file and update the following section:

    this.create('jobs', (table) => {
      table.increments()
      table.string('title')
      table.string('link')
      table.string('description')
      table.integer('user_id')
      table.timestamps()
    })

Here, we're adding 4 columns called title, link, description, user_id. The user_id field will allow us to keep track of who posted which job.

Let's run this migration file now:

> adonis migration:run
migrate: 1536680712439_jobs_schema.js
Database migrated successfully in 313 ms

If you view the database, you will now see the new table!

Creating a Model

Now that our database and tables have been created, we need to create a model for handling our jobs table and associated data.

Let's use the Adonis CLI to create a new model for Jobs data:

> adonis make:model Job

Open up /app/Models/Job.js and you will see the following code:

'use strict'

const Model = use('Model')

class Job extends Model {
}

module.exports = Job

Notice the naming convention. I chose Job because that's the naming convention used for a table named in the plural context jobs. You will notice the same thing for the User model and the users table.

At this point, we don't have to do anything else with our Model. As long as it's created, we've done our job.

Creating a Controller

Next, we need to define a controller that's responsible for using the Job model for creating, altering and retreiving jobs from the database.

Let's use the Adonis CLI to create that controller:

> adonis make:controller JobController
> Select controller type
> For HTTP requests
  For Websocket channel

Choose "For HTTP requests" and hit enter.

Open up /app/Controllers/Http/JobController.js and specify the following:

 

'use strict'

const Job = use('App/Models/Job')

class JobController {
    async home({view}) {

        // Create a job
        const job = new Job;
        job.title = 'My job title';
        job.link = 'http://google.com';
        job.description = 'My job description';

        await job.save();

        // Fetch a job
        const jobs = await Job.all();

        return view.render('index', { jobs: jobs.toJSON() })
    }
}

module.exports = JobController

Here, we're first creating a job when someone visits the / route. This is just temporary and will allow us to insert a row of data into our table. Then, we're fetching all of the jobs. This is all made possible by referencing our Model.

At the end we're returning the view of index (index.edge) and passing the jobs to the view.

In order for this to work, we need to modify the /start/routes.js file:

Route.get('/', 'JobController.home');

Save this, and then visit the browser and refresh the home page. Then, view the data shown in MySQL and you will see the new data inserted into the table!

Let's modify our /resources/views/index.edge to display the returned data:

  @each(job in jobs)
  <div class="job-container">
    <div class="blank"></div>
    <div class="job-info">
      <h3><a href="{{ job.link }}">{{ job.title }}</a></h3>
      <p>{{ job.description }}</p>
    </div>
  </div>
  @endeach

 Awesome! Now, view the result in the browser:

Let's remove the create command from the controller:

class JobController {
    async home({view}) {

        // Fetch a job
        const jobs = await Job.all();

        return view.render('index', { jobs: jobs.toJSON() })
    }
}

User Authentication

Before we can allow users to submit jobs, we need to create a user authentication system with a signup and login.

Fortunately, our users table and model has already been created for us.

Let's visit the routes.js file and add the following 2 lines to the bottom:

Route.on('/signup').render('auth.signup');
Route.on('/login').render('auth.login');

This is referencing a couple views that don't yet exist, so create the following folder and file structure:

/resources
   /views
      /auth
        login.edge
        signup.edge

 

Inside signup.edge paste the following HTML:

@layout('layouts.main')

@section('extracss')
{{ style('forms') }}
@endsection

@section('title')
JobPostr - Sign up
@endsection

@section('content')
  <h1>Join now</h1>

  <div class="job-container">
      <form action="{{ route('UserController.create') }}" method="POST">
        {{ csrfField() }}

        <label for="username">Username</label>
        <input type="text" name="username" value="{{ old('username', '') }}">
        @if(hasErrorFor('username'))
            <span>
                {{ getErrorFor('username') }}
            </span>
        @endif

        <label for="email">Email</label>
        <input type="email" name="email" value="{{ old('email', '') }}">
        @if(hasErrorFor('email'))
            <span>
                {{ getErrorFor('email') }}
            </span>
        @endif

        <label for="password">Password</label>
        <input type="password" name="password">
        @if(hasErrorFor('password'))
            <span>
                {{ getErrorFor('password') }}
            </span>
        @endif

        <button type="submit">Join now</button>
      </form>
  </div>
@endsection

A few things are happening here:

  • First, notice we're using the style('forms') function at the top. This means we need to create a CSS file, we'll do this in the next step..
  • In the form tag, we're setting the action attribute to an Adonis helper function called Route, which means it will submit the route to a method called Create in our UserController. We will create this method shortly..
  • We're using csrfField() to generate a hidden input to ensure this request is coming from our server.
  • In two of the inputs, we use the Adonis old() function to persist form data if an error shows up during future form validation.
  • We use the Adonis helper function hasErrorFor('field') to display errors returned from form validation.

Let's create a file in: /public/forms.css and paste the following CSS:

.job-container {
    grid-template-columns: auto;
    padding: 2em;
    width: 30%;

  }
label {
    font-weight: bold;
}
input[type='text'], input[type='email'], input[type='password'] {
    width: calc(100% - 4em);
    display: block;
    padding: .5em;
    margin-bottom: 20px;
    border: 2px solid lightgray;
}
/* Change the white to any color ;) */
input:-webkit-autofill {
    -webkit-box-shadow: 0 0 0 30px white inset;
}

button {
    padding: .5em;
    background: #455668;
    border: 0;
    color: white;
    font-size: 1.2em;
    font-family: 'Montserrat';
    text-transform: uppercase;
    font-weight: bold;
    border-radius: 5px;
    margin-top: 20px;
    cursor: pointer;
}

span {
    display: block;
    background: yellow;
    padding: .3em;
    margin-bottom: 20px;
}

The result in the browser should now look like this:

Visit the routes.js file and add a post line underneath the first line shown here:

Route.on('/signup').render('auth.signup');
Route.post('/signup', 'UserController.create').validator('CreateUser');

Notice that we're adding a .validator() at the end? This will allow us to validate the form data submitted before it reaches the create method in the controller.

Let's install the validator plugin with the Adonis CLI:

> adonis install @adonisjs/validator

Once installed, open up /start/app.js and we'll add the validator as a provider in the providers array:

const providers = [
  '@adonisjs/framework/providers/AppProvider',
  '@adonisjs/framework/providers/ViewProvider',
  '@adonisjs/lucid/providers/LucidProvider',
  '@adonisjs/bodyparser/providers/BodyParserProvider',
  '@adonisjs/cors/providers/CorsProvider',
  '@adonisjs/shield/providers/ShieldProvider',
  '@adonisjs/session/providers/SessionProvider',
  '@adonisjs/auth/providers/AuthProvider',
  '@adonisjs/validator/providers/ValidatorProvider'   // Add here
]

Now, we've gained access to an Adonis CLI command to create a custom validator:

> adonis make:validator CreateUser

This creates a file (open it up) in: /app/Validators/CreateUser.js. Paste the following contents:

'use strict'

class CreateUser {
  get rules () {
    return {
      'username': 'required|unique:users',
      'email': 'required|unique:users',
      'password': 'required'
    }
  }

  get messages() {
    return {
      'required': 'Woah now, {{ field }} is required.',
      'unique': 'Wait a second, the {{ field }} already exists'
    }
  }

  async fails(error) {
    this.ctx.session.withErrors(error)
      .flashAll();
    
    return this.ctx.response.redirect('back');
  }
}

module.exports = CreateUser

First, we define the rules() for each of the form data that's to be submitted. Next, we create validation messages in case any of the fields that are required or unique end up failing.

Then, in the case of a failure (async fails()), we flash a message to the session, which will be displayed by our view.

Save this, and try submitting the form without specifying any data!

We need to create a controller that will allow us to issue commands to the predefined User model, which interacts with the users table in our database:

> adonis make:controller UserController

(Choose HTTP request). 

In the /app/Controllers/UserController.js file, paste the following:

'use strict'

const User = use('App/Models/User');

class UserController {
    async create({ request, response, auth}) {
        const user = await User.create(request.only(['username','email','password']));

        await auth.login(user);
        return response.redirect('/');
    }
}

module.exports = UserController

First, we're defining a constant for the User model, then we create the create() method in which we specify the request, response and auth from the context.

Then we use User.create() to create the user, then log them in, and redirect them back to the home page!

Give it a shot! Visit the signup page, fill out the form correctly, and then view your MySQL Users table. It should show the new user.

Logged in status

Right now, our user is logged in, but it's not being reflected in the view. Let's make an adjustment for that.

Visit the /resources/views/layouts/main.edge file and modify the nav section:

        <nav>
            <ul>
                <li><a href="/">Jobs</a></li>
                @loggedIn
                    <li><a href="/post-a-job">My Jobs</a></li>
                    <li><a href="/logout">Logout</a></li>
                @else
                    <li><a href="/login">Login</a></li>
                    <li><a href="/signup">Signup</a></li>
                @endloggedIn
            </ul>
        </nav>

Simple enough! Refresh the browser and you should see the @loggedIn links.

Logging Out

Logging out is very simple. Visit the routes file and paste the following at the bottom of the file:

Route.get('/logout', async ({ auth, response }) => {
    await auth.logout();
    return response.redirect('/');
});

If you refresh and click logout in the navigation, you will be logged out!

Awesome!

Logging In

Visit the /auth/login.edge file and paste the following contents:

@layout('layouts.main')

@section('extracss')
{{ style('forms') }}
@endsection

@section('title')
JobPostr - Login
@endsection

@section('content')
  <h1>Login now</h1>

  <div class="job-container">
    @if(flashMessage('loginError'))
        <span>{{ flashMessage('loginError') }}</span>
    @endif
      <form action="{{ route('UserController.login') }}" method="POST">
        {{ csrfField() }}

        <label for="email">Email</label>
        <input type="email" name="email" value="{{ old('email', '') }}">
        @if(hasErrorFor('email'))
            <span>
                {{ getErrorFor('email') }}
            </span>
        @endif

        <label for="password">Password</label>
        <input type="password" name="password">
        @if(hasErrorFor('password'))
            <span>
                {{ getErrorFor('password') }}
            </span>
        @endif

        <button type="submit">Login now</button>
      </form>
  </div>
@endsection

It's almost identical to the signup.edge with just a few differences.

Visit the routes.js file and add the following route:

Route.post('/login', 'UserController.login').validator('LoginUser');

Now, let's create a new validator with the Adonis CLI:

> adonis make:validator LoginUser

Open up the /app/Validators/LoginUser.js and paste the following:

'use strict'

class CreateUser {
  get rules () {
    return {
      'email': 'required|email',
      'password': 'required'
    }
  }

  get messages() {
    return {
      'required': 'Woah now, {{ field }} is required.',
    }
  }

  async fails(error) {
    this.ctx.session.withErrors(error)
      .flashAll();
    
    return this.ctx.response.redirect('back');
  }
}

module.exports = CreateUser

It's almost identical to the CreateUser.js validator.

In /app/Controllers/Http/UserController.js add the following just underneath the create() {} method:

    async login({ request, auth, response, session }) {
        const { email, password } = request.all();

        try {
            await auth.attempt(email, password);
            return response.redirect('/');
        } catch (error) {
            session.flash({loginError: 'These credentials do not work.'})
            return response.redirect('/login');
        }
    }

This will take the email and password submitted from the request, and auth.attempt() to login. If successful, we return them to the home page. If not, it will flash a session message called loginError which is being displayed by our edge view.

Give it a shot. Try logging in with the user you created before and it should work. Great!

Posting Jobs

Now that we can create users and login, let's take a look at posting jobs. 

Create a new edge view file: /resources/views/jobs.edge and paste the following:

@layout('layouts.main')

@section('title')
JobPostr - Post your Job
@endsection

@section('extracss')
{{ style('forms') }}
@endsection

@section('content')
  <h1>Post a Job</h1>


  <div class="job-container">
    @if(flashMessage('message'))
        <span>{{ flashMessage('message') }}</span>
    @endif
        <form action="{{ route('JobController.create') }}" method="POST">
          {{ csrfField() }}
  
          <label for="title">Job Title</label>
          <input type="text" name="title" value="{{ old('title', '') }}">
          @if(hasErrorFor('title'))
              <span>
                  {{ getErrorFor('title') }}
              </span>
          @endif
  
          <label for="link">Link URL</label>
          <input type="text" name="link" value="{{ old('link', '') }}">
          @if(hasErrorFor('link'))
              <span>
                  {{ getErrorFor('link') }}
              </span>
          @endif
  
          <label for="description">Description</label>
          <input type="text" name="description">
          @if(hasErrorFor('description'))
              <span>
                  {{ getErrorFor('description') }}
              </span>
          @endif
  
          <button type="submit">Submit a Job</button>
        </form>
    </div>

<h2>My jobs</h2>

  @each(job in jobs)
  <div class="job-container2">
    <div class="blank"></div>
    <div class="job-info">
      <h3><a href="{{ job.link }}">{{ job.title }}</a></h3>
      <p>{{ job.description }}</p>
      <ul>
          <li><a href="{{ route('JobController.delete', { id: job.id }) }}">Delete</a></li>
          <li><a href="{{ route('JobController.edit', { id: job.id }) }}">Edit</a></li>
      </ul>
    </div>
  </div>
  @endeach
@endsection

Next, let's create a relationship between the user and jobs. We do this by opening up the app/Models/User.js file and class model, paste:

  jobs() {
    return this.hasMany('App/Models/Job');
  }

This will allow our controller to automatically append the currently logged in user's ID to new job postings.

Let's create a new validator for our jobs:

> adonis make:validator CreateJob

Open up the new CreateJob.js validator and specify the following:

'use strict'

class CreateJob {
  get rules () {
    return {
      title: 'required',
      link: 'required'
    }
  }
  get messages() {
    return {
      'required': 'Hold up, the {{ field }} is required.'
    }
  }

  async fails(error) {
    this.ctx.session.withErrors(error)
      .flashAll();
    
    return this.ctx.response.redirect('back');
  }
}

module.exports = CreateJob

Next, open up routes.js and add the following routes:

Route.get('/post-a-job', 'JobController.userIndex');
Route.get('/post-a-job/delete/:id', 'JobController.delete');
Route.get('/post-a-job/edit/:id', 'JobController.edit');
Route.post('/post-a-job/update/:id', 'JobController.update').validator('CreateJob');

These routes are all for defining CRUD (Create Read Update and Delete) functionality.

Open up the JobController.js file and add the following methods:

    async userIndex({view, auth}) {

        // Fetch all user's jobs
        const jobs = await auth.user.jobs().fetch();
        console.log(jobs)

        return view.render('jobs', { jobs: jobs.toJSON() })
    }

    async create({ request, response, session, auth}) {
        const job = request.all();

        const posted = await auth.user.jobs().create({
            title: job.title,
            link: job.link,
            description: job.description
        });

        session.flash({ message: 'Your job has been posted!' });
        return response.redirect('back');
    }

    async delete({ response, session, params}) {
        const job = await Job.find(params.id);

        await job.delete();
        session.flash({ message: 'Your job has been removed'});
        return response.redirect('back');
    }

    async edit({ params, view }) {
        const job = await Job.find(params.id);
        return view.render('edit', { job: job });
    }

    async update ({ response, request, session, params }) {
        const job = await Job.find(params.id);

        job.title = request.all().title;
        job.link = request.all().link;
        job.description = request.all().description;

        await job.save();

        session.flash({ message: 'Your job has been updated. '});
        return response.redirect('/post-a-job');
    }

Save this and give it a shot by posting a new job! The new job should be posted directly underneath the form. 

You can also click the Delete link and it should work as well. 

In order to make the edit link work, we have to create a new edge view.

Create the file /resources/views/edit.edge when the following template:

@layout('layouts.main')

@section('title')
JobPostr - Edit your Job
@endsection

@section('extracss')
{{ style('forms') }}
@endsection

@section('content')
  <h1>Edit Job</h1>


  <div class="job-container">
    @if(flashMessage('message'))
        <span>{{ flashMessage('message') }}</span>
    @endif
        <form action="{{ route('JobController.update', { id: job.id }) }}" method="POST">
          {{ csrfField() }}
  
          <label for="title">Job Title</label>
          <input type="text" name="title" value="{{ job.title }}">
          @if(hasErrorFor('title'))
              <span>
                  {{ getErrorFor('title') }}
              </span>
          @endif
  
          <label for="link">Link URL</label>
          <input type="text" name="link" value="{{ job.link }}">
          @if(hasErrorFor('link'))
              <span>
                  {{ getErrorFor('link') }}
              </span>
          @endif
  
          <label for="description">Description</label>
          <input type="text" name="description" value="{{ job.description }}">
          @if(hasErrorFor('description'))
              <span>
                  {{ getErrorFor('description') }}
              </span>
          @endif
  
          <button type="submit">Update Job</button><br>
        </form>
    </div>
@endsection

Save this and try clicking edit on an existing job and then editing it. 

Refactoring the Routes

Right now, this section is rather redundant / verbose:

Route.get('/post-a-job', 'JobController.userIndex');
Route.get('/post-a-job/delete/:id', 'JobController.delete');
Route.get('/post-a-job/edit/:id', 'JobController.edit');
Route.post('/post-a-job/update/:id', 'JobController.update').validator('CreateJob');

We can simplify this a bit by using Route Groups. Modify it as such:

Route.get('/post-a-job', 'JobController.userIndex');

Route.group(() => {
    Route.get('/delete/:id', 'JobController.delete');
    Route.get('/edit/:id', 'JobController.edit');
    Route.post('/update/:id', 'JobController.update').validator('CreateJob');
}).prefix('/post-a-job');

Conclusion

We've just scratched the surface here, but hopefully this should get you going to the point at which you have some solid footing to learn even more!


Share this post




Say something about this awesome post!