How to Create a Landing Page with Laravel, Vue.js, and Twilio SendGrid

How to Create a Landing Page with Laravel, Vue.js, and Twilio SendGrid

This tutorial originally appeared on the Twilio blog.

Landing pages are everywhere in modern business. 

Businesses use them to offer a free giveaway in return for someone joining a mailing list, to sell a product or service, and as glorified resumes.

Landing pages are a great solution for these kinds of business needs as you can rapidly create and deploy them. Knowing how to create them can also be a great skill for developers to have, whether creating landing pages for others or for personal projects.

In this tutorial, I’ll show you how to create a landing page using a combination of Laravel 8, Vue.js, and Twilio SendGrid. It’s a reasonably long tutorial, and by the end of it, I’m confident that you’ll have learned a lot.


To complete the tutorial, you'll need the following 4 things in your local development environment:

Application overview

Before we dive in and start building the application, I want to give you a broad overview of the application's user flow, which you can see in the image below. 

The landing page will allow users to sign up for notifications about upcoming offers from the fictitious online store that I created—The Little PHP Shop—in a recent Twilio SendGrid tutorial.

The application will have two routes, / and /signup, both handled by a single controller named LandingPageController

When the user requests the default route (/), they'll see a form asking them to provide their email address to sign up for notifications from The Little PHP Shop

After submission, the client- and server-side will validate the form. If client-side validation is successful, the server will receive an email address in the form of a JSON object. If the server-side validation passes, the user is registered and will receive an email confirming their registration.

At this point, the client will receive a JSON response confirming sign-up success. When the client receives this, it will hide the form and display a message confirming that everything went well.

If server-side validation fails or if the user cannot successfully subscribe, the user will also receive a JSON response. This JSON response will indicate both that the process failed and why.

Create the back-end application

Bootstrap the Laravel application

The first thing we need to do is to bootstrap a new Laravel application. To do that, run the command below. Feel free to use any of the other methods of bootstrapping Laravel applications if you prefer them.

The bootstrapped application will be created in a new directory named landing-page. Change to the directory and start the application to check that everything's working by running the commands below. The app will be available on localhost on port 8000.

If the application is running, you will see a page similar to the screenshot below. Stop the application running by pressing CTRL+C.

Install the required packages

With the application bootstrapped, we now need to install 2 external packages: To install them, run the command below in the root directory of the project.

Create an API key

Next, you need to supply the application with your Twilio SendGrid API key. To do that, after logging into Twilio SendGrid, navigate to "Settings -> API Keys." Once there:
    1. Click “Create API Key” to create an API key.
    2. Give the new API key a name.
    3. Accept the default API Key Permission of “Full Access.
    4. Click “Create and View.
After you create the API key, copy it so you can use it in a later step. 

Then, open the .env file in the root directory of the project and add the key/value pair below to the end of the file. Replace YOUR-KEY-HERE with the API key that you created and copied. In addition, update 2 existing keys in the file: MAIL_MAILER to sendgrid and MAIL_FROM_ADDRESS to an email address of your choice.

Note: The email address must be listed as “Verified” in the “Single Sender Verification” table in Sender Authentication.

Create a mailable

We now need to create a mailable class that will store the email's subject, set the view to render for the email's body, and specify the mail transport to use. To do that, run the command below in the root directory of the project.

A new file, named Subscribed.php, will be created in app/Mail. Copy and paste the code below in place of the file's existing code.

Most of the action in a mailable happens in the build method, which calls a series of other methods to set:
    • The template to render to create the email's body (view)
    • The email's subject (subject)
The build method finishes by calling sendgrid, which ensures that the Twilio SendGrid transport sends the email.

Create the landing page controller

Now it's time to create a controller to handle requests to the application's 2 routes. To do that, run the command below. When the command is complete, a new file, named LandingPageController.php, will have been created in app/Http/Controllers.

With the file created, open app/Http/Controllers/LandingPageController.php and add 2 methods to it. Firstly, the show method from the example below, then the sign-up method. I've linked to the signup method as it's a bit too long to directly include in the article.

After that, add the following use statements for the signup method.

I'll skip over the show method, as it should be self-explanatory, and dive into the signup method. The method starts off using Laravel's validator facade to validate the data sent in the request, retrieved by calling $request->all(), against a validation ruleset with the result stored in $validator.

To pass validation, the request body needs to contain an element named email whose value is a legitimate email address. In addition, I have added the email-specific Request For Comment (RFC) and Domain Name System (DNS) validation rules because:
    • RFC validation ensures that the email is valid according to the email RFC. However, even if it passes that validation, the email may not be universally routable, such as matthew or matthew@lan.
    • DNS validation ensures that the email address doesn't contain reserved top-level DNS names, or mDNS, and private DNS namespaces, such as test, local, lan, intranet, and internal.
Note: To learn more about Laravel's validators, check out the official documentation.

If the result of calling the fails method returns false, a JSON object returns that contains a list of form validation errors. These are retrievable by calling the $validator's errors method. In addition, the response's status code is set to 422 to show that the request was not processable.

If validation succeeded, however, it is time to add the email address to our contact list. To do that, you need to initiate a new SendGrid object, which requires our Twilio SendGrid API key that you retrieved from the 'SENDGRID_API_KEY' environment variable.

After that, a PUT request is sent to the Twilio SendGrid API /marketing/contacts/ endpoint. To that request, an array of contacts passes, albeit with only one contact where we specify the new contact’s email address.

If the response's status code is not 202, then we know that something went wrong. If this happens, a JSON response returns to the client containing 3 properties:
    • status: Set to false
    • message: Set to "subscription failed"
    • reason: Initialized with the errors returned from the Twilio SendGrid API call

If the user was successfully added to our contacts list, it's time to send them a confirmation email. To do that, the code makes use of two methods on Laravel's mail facade to to set the recipient and send to send the email.

Retrieve the recipient from the email address sent in the request body, handily retrieved by calling $request's input method. The remainder of the email's properties are in Subscribed, the mailable object we created previously, and passed to the mail facade's send method.

The attempt to send the email is wrapped in a try/catch block, just in case there is a problem sending the email, such as attempting to send from an email address that is not listed as “Verified” in the “Single Sender Verification” table.

If there is an error, a JSON response returns to the client containing 3 properties, similar to before:
    • status: Set to false
    • message: Set to "registration failed"
    • reason: Initialized with the exception's message

At this point everything has succeeded, so it is time to let the user know that. The code does that by returning a JSON response again, but this time with only 2 properties: status set to true and message set to "registration is completed." 

It is small but effective!

Note: There are more maintainable ways to code the controller method, but for the purposes of an example, I have included all of the required calls in the body of the signup method.

Create the required templates

Now it is time to create the templates that our app will use. Under resources/views, we are going to create 2 directories (email and layouts) and 3 files (landing.blade.php, email/subscribed.blade.php, and layouts/app.blade.php). 

Here is a quick visual representation of the file and directory structure that we will create.

I have chosen this structure primarily because I love the Two-Step View pattern. If you are not familiar with it, it essentially splits views into 2 parts. There is one part for any content that is consistent across all requests (layouts/app.blade.php), and one part for content that is request-specific (landing.blade.php).

It might seem like overkill on such a small application, but I have found that this approach makes it easier to create more maintainable templates.

Run the commands below to create the file and directory structure.

Note: If you are using Microsoft Windows, the -p flag is not necessary, and the touch command doe not exist, so try the following commands instead.

Update resources/views/layouts/app.blade.php

Open resources/views/layouts/app.blade.php and paste the code below into it. Most of it is pretty standard Laravel template code, which you can find in resources/views/welcome.blade.php.

The final 2 tags in the head section are worth noting, however. Here, we link the CSS stylesheet that we will create later in the tutorial and store a CSRF token, which Laravel will generate for us (more on that shortly) as a meta tag.

We are not going to touch resources/views/landing.blade.php and resources/views/email/subscribed.blade.php now, as we cover them later in the tutorial.

Update the routing table

We only need to make 2 changes to the routing table: to change the default route’s handler and add a new route to handle sign-ups. To do that, replace the existing route in routes/web.php with the code below.

Also, add the use statement for the LandingPageController as well:

Update Laravel's configuration

With the routes defined, we now need to update 3 of Laravel's core configuration files: config/cors.php, config/mail.php, and config/services.php.

Update config/cors.php

The first file that we need to update is config/cors.php. This is so that the XHR requests that we make in the Vue.js front end can successfully make requests to the back-end Laravel app. 

To enable that, update the paths element's array in the array returned in config/cors.php so that it matches the example code below.

Update config/mail.php

Next, we need to update config/mail.php to register sendgrid as a valid mail transport. To do that, add the configuration below to the mailers element's array at the end of the existing list of transports.

Update config/services.php

The final change that we have to make is to config/services.php, to register sendgrid as a service with Laravel's Dependency Injection (DI) Container. To do that, add the configuration below at the end of the array returned in the file.

Create the Vue.js application

Now that we have created the back end of the application, it is time to create the front-end Vue.js application. Before we can do that, we need to install several dependencies.

Gladly, there are not that many, just Vue.js and Laravel Mix, with support for Tailwind CSS, PostCSS, and Lodash, to simplify building the front end.

To install them, run the commands below in the root directory of the project.

Update resources/views/landing.blade.php

I have not included the full contents of resources/views/email/landing.blade.php since it is quite long and would take up too much space here in the article. You can find it in the GitHub repository for this project. Copy and paste the contents into the template.

I am going to step through the most relevant parts of the file. First, we will visualize what is happening in the file so that it is easier to appreciate what is going on.

We are creating a small Vue.js application, called app, composed of 2 parts: 
    1. The landing page form that the user sees when they initially land on the page
    2. A post-submission confirmation that appears in place of the form after a successful form submission
Let's start off with the landing page form. It contains 2 parts:
    1. A header and marketing description to convince the reader to provide their email address
    2. A form that the user can fill in and submit, which can render errors when the form submission fails validation or the server-side request fails
The section directly below is part one. There is not much to it, except for the V-show directive, which conditionally displays the element if submitted is true.

The next section uses a custom Vue.js component, error-item, to reduce the amount of code required in the template and to make the error rendering more maintainable. We’ll discuss this component shortly.

This section makes use of a V-if directive to conditionally render the element based on whether there are any errors or not. It uses Vue.js' @submit.prevent attribute to pass control of the normal form submission process to the processForm method. It uses Laravel's Blade CSRF directive to render a CSRF token in a hidden form field.

One other thing worth noting is the V-model directive in the email input field, v-model="". This creates a 2-way binding between the form element and the property in the JavaScript code. We will come back to this shortly.

Note: We will use the namespace to keep the naming more intuitive than plain email.

The final section contains the confirmation message that will display upon successful submission of the form. We can keep it simple by just specifying a header and body text.

Create the JavaScript code

Next, we will work through the JavaScript that will power the front end. It is a little long, so copy the code from the GitHub repository and paste it in place of the existing code in resources/js/app.js. Then, we will go through it.

The code starts off by defining sendGridApp, which forms the basis of our Vue.js application and contains 3 data properties:
    • errors: This is a list of form validation errors
    • This stores the email address that the user supplies
    • submitted: This determines whether the form has been successfully submitted or not. If it is false, the form will display. If it is true, then the confirmation message will display in place of the form

Next up, we define sendGridApp's methods. Starting with processForm, triggered from the form submission, we can check if the email’s set. If it has not, it sets an error message and returns false so that form submission stops. If it has, then it calls subscribeUser to subscribe the user to the list.

subscribeUser makes a POST request to /signup, with a JSON body, containing a JSON-encoded copy of the submitted form.

The request headers are important to note. This is because they ensure that Laravel interprets the request as an XHR request, not a normal form submission (Content-Type and Accept), and that the request is valid because it has a CSRF token (X-CSRF-TOKEN).

If we were building a purely server-side application using only Blade templates, then we would only need to include Blade's CSRF directive, and Blade would do the rest. However, it is not quite so simple with JavaScript.

The code uses the Promise's then method to retrieve the JSON in the response (if the request was successful) or throws an error (if it was unsuccessful). If the request is successful, the next then method is called. 

Here, it sets submitted to true, which does several things:
    • Hides the form
    • Displays the confirmation message
    • Clears the email address entered in the form
Finally, if something goes wrong, it catches the error and logs it to the Console.

Finally, a new Vue.js app is created, named app, with the const that we just defined. The app creation code defines a small component for rendering form errors and mounts the app.

Create the style sheet

Next, in resources/css/app.css, add the code below. It includes Tailwind's base, components, and utilities styles and creates several additional base styles for some elements that are common across the view templates.

Update resources/views/email/subscribed.blade.php

I have not included the full contents of resources/views/email/subscribed.blade.php, as it is quite long and would take up too much space here in the article. You can find it in the GitHub repository for this project. Copy it and paste the contents into the template. 

Now, we will go through the most relevant parts of the file.

The template extends resources/views/layouts/app.blade.php, by setting the content for the content section when rendered. The content itself is relatively simple, just thanking the user for subscribing and ending by giving the user a link to unsubscribe.

Build the front-end application

At this point, we are just about ready to test the app. However, we need to build the front end and its supporting files before we can do that. Gladly, Laravel Mix makes this pretty trivial. To start, we have to update one configuration file and create a second configuration file.

Update webpack.mix.js

Because Laravel Mix comes bundled with Laravel, its configuration file, webpack.mix.js, is already available and contains a basic configuration. 

However, we need to make 2 additions to it. The first addition supports Laravel Mix for Vue.js single-file components. The second addition supports Tailwind CSS. Add the highlighted changes below to webpack.mix.js.

Create tailwind.config.js

Because we are using Tailwind CSS to style the front end and because we have just added support for it to Laravel Mix's configuration file, we need to supply the tailwind.config.js configuration file to build it properly. 

Create a new file named tailwind.config.js in the root directory of the project, then copy and paste the code below into it.

This instructs PostCSS to parse all PHP, Blade, JavaScript, and Vue.js files in the above directories and to build a list of all Tailwind CSS styles discovered therein. Using that list, it strips out any unused styles from the default Tailwind CSS style sheet, generating a style sheet around 20.5KB in size.

This is handy because the uncompressed default file is 3566.2KB in size. This is far too large for a website that needs to be performant.

With the files in place and configured, in the terminal in the root directory of the project, run the command below.

This command runs Laravel Mix telling it to:
    1. Generate public/js/app.js from resources/js/app.js
    2. Generate public/css/app.css from resources/css/app.css
This should only take a few seconds to complete and render the following to the terminal.

Test the application

With the code in place and all wired up, it is time to test that it works properly. To do that, start the application by running the command below.

Then, open http://localhost:8000 in your browser of choice. Before filling out the form, open up the Developer Tools and change to the Console tab. With everything ready, fill in the form with a valid email address. 

You should see the form hidden and replaced by the confirmation message. Check your inbox for the confirmation email. Then, view your All Contacts list, to confirm that the user was successfully subscribed. If successful, you should see them listed, similar to in the screenshot below. 

Now try again by clicking the "Start over" link and submitting the form without entering an email address or after entering an invalid email address. You should see an applicable error message displayed.

That is how to create a landing page using Laravel 8, Vue.js, and Twilio SendGrid

We have stepped through how to turn a stock-standard Laravel 8 site into a basic landing page, one that can sign up a user to a mailing list when they submit their email address. 

While not placing a lot of emphasis on it, we also made use of Laravel Mix and Tailwind CSS to streamline the process of creating a custom, performant front end for our application.

If you would like to know more about what makes a great landing page, check out this article from Copyblogger. Otherwise, you can find all the code for the application that we build in this series over on GitHub.

Matthew Setter is a PHP Editor in the Twilio Voices team and (naturally) a PHP developer. He’s also the author of Mezzio Essentials. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at, and he's settermjd on Twitter and GitHub.

Recommended For You

Most Popular

Send With Confidence

Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.