Andreas Ludvigsson

Bridging Code and Commerce

Creating an AI-Powered Resume Maker SaaS with Laravel – Part 1

Hey there! Are you ready to take your Laravel skills to the next level? In this exciting SaaS series, we’ll embark on a journey to create a powerful Resume Maker that harnesses the capabilities of AI to enhance the resume creation process for users. Whether you choose to follow along step by step or simply grab the source code from Gumroad (available for free or at a price you deem fair), this series promises to be an engaging and educational experience. If you find value in the content, don’t hesitate to leave a comment or reach out with any questions you may have.

Now, let’s dive into the impressive tech stack we’ll be utilizing to bring this project to life:

  1. Laravel 11: The latest version of the robust and expressive PHP web application framework, providing a solid foundation for building our SaaS application.
  2. InertiaJS + Vue: A powerful combination that allows us to create smooth and seamless user interfaces. InertiaJS enables us to build single-page applications using classic server-side routing and controllers, while Vue.js provides a reactive and component-based frontend framework.
  3. Laravel JetStream: A beautifully designed application starter kit that offers authentication, registration, email verification, two-factor authentication, session management, API support, and optional team management. JetStream gives us a head start in building our SaaS app.
  4. Laravel Cashier: A library that simplifies subscription billing for our SaaS application. With Cashier, we can easily handle subscription management, invoicing, and payment processing using popular payment gateways like Stripe.
  5. Laravel Reverb: Reverb is a first-party WebSocket server for Laravel applications, bringing real-time communication between client and server 
  6. Browsershot: A handy package that enables us to capture screenshots and generate PDFs from HTML pages. We’ll use Browsershot to create downloadable resume files for our users.

By combining these cutting-edge technologies, we’ll create a Resume Maker SaaS that offers an intuitive user interface, AI-powered resume enhancement, secure authentication, seamless subscription management, and the ability to generate professional-looking resume files.

Get ready to expand your Laravel expertise and witness the fusion of AI and web development in action. Let’s embark on this exciting journey together and create a remarkable Resume Maker SaaS!

Installing Laravel

  1. Make sure you have PHP and Composer installed on your system.
  2. Open your terminal and navigate to the directory where you want to create your Laravel project.
  3. Run the following command to create a new Laravel project:
    composer create-project --prefer-dist laravel/laravel your-project-name Replace your-project-name with the desired name for your project.
  4. Once the installation is complete, navigate into the project directory:
    cd your-project-name

Installing Jetstream with Inertia:

  1. While in your project directory, run the following command to install Jetstream with Inertia: composer require laravel/jetstream
  2. After the installation is complete, run the following command to finalize the Jetstream installation: php artisan jetstream:install inertia This command will install Jetstream with the Inertia stack.
  3. Next, run the following commands to install the necessary dependencies and compile the assets:
    npm install
    npm run dev

Configuring the .env file for MySQL:

  1. In your project directory, locate the .env.example file and make a copy of it. Rename the copy to .env. (if needed)
  2. Open the .env file in a text editor.
  3. Locate the database configuration section that looks like this:
    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=your_database_name
    DB_USERNAME=your_username
    DB_PASSWORD=your_password
  4. Update the following values according to your MySQL database setup:
    • DB_DATABASE: Replace your_database_name with the name of your MySQL database.
    • DB_USERNAME: Replace your_username with your MySQL database username.
    • DB_PASSWORD: Replace your_password with your MySQL database password.
  5. Save the changes to the .env file.

Final Steps:

  1. Run the following command to generate a new application key: php artisan key:generate
  2. Run the database migrations to create the necessary tables: php artisan migrate
  3. Finally, start the development server: php artisan serve

You should now have Laravel installed with Jetstream and Inertia, and your .env file configured for a MySQL database. You can access your Laravel application by visiting http://localhost:8000 in your web browser.

Remember to make sure your MySQL database is running and accessible with the provided credentials before running the migrations.

Our Landing Page

Let’s start by creating an engaging landing page for our application. To ensure a visually appealing and user-friendly design, we’ll draw inspiration from Tailwind UI, a collection of beautifully designed and expertly crafted components and templates created by the makers of Tailwind CSS. Tailwind UI provides the perfect starting point for our project, offering a wide range of pre-built components that we can easily customize to fit our needs.

To enhance the interactivity and functionality of our landing page, we’ll leverage HeadlessUI for our navigation bar. HeadlessUI is a powerful library that provides accessible and customizable UI components, making it easier to create a seamless and intuitive user experience.

For icons, we’ll utilize HeroIcons, a set of beautiful hand-crafted SVG icons also created by the makers of Tailwind CSS. These icons will add visual appeal and clarity to our landing page, helping users navigate and understand the features of our application.

Creating the Vue Layout

Since we are using InertiaJS and Vue, we’ll create our layout as a Vue file instead of using Blade templates. This approach allows us to take full advantage of Vue’s component-based architecture and seamlessly integrate with InertiaJS.

To get started, create a new file called GuestLayout.vue in the resources/js/layouts directory. This file will serve as our primary layout for non-member related functionality, such as the landing page.

To keep our code organized and maintainable, we’ll divide the layout into smaller, reusable components. Create the following Vue files in the resources/js/components directory:

  • GuestHeader.vue: This component will contain the header section of the landing page, including the navigation bar.
  • GuestHero.vue: This component will showcase the main hero section of the landing page, featuring a prominent headline, a brief description, and a call-to-action button.
  • GuestFunctions.vue: This component will highlight the key functions and features of our application, providing users with an overview of what they can expect.
  • GuestCTA.vue: This component will include a compelling call-to-action section, encouraging users to sign up or take a specific action.
  • GuestFooter.vue: This component will contain the footer section of the landing page, including important links, social media icons, and any necessary legal information.

Once you have created these component files, import them into your GuestLayout.vue file and include them in the template section. This will allow you to compose the landing page layout by combining these reusable components.

By breaking down the layout into smaller components, we can achieve a modular and maintainable structure for our landing page. Each component can be developed and styled independently, making it easier to manage and update the overall design.

GuestLayout.vue

<script setup>
import GuestHeader from "@/Components/GuestHeader.vue";
import GuestHero from "@/Components/GuestHero.vue";
import GuestFunctions from "@/Components/GuestFunctions.vue";
import GuestCTA from "@/Components/GuestCTA.vue";
import GuestFooter from "@/Components/GuestFooter.vue";

defineProps({
    title: String,
});
</script>

<template class="bg-white>
  <GuestHeader/>
<GuestHero/>
    <GuestFunctions/>
    <GuestCTA/>
    <GuestFooter/>
</template>

<style scoped>

</style>

GuestHeader.vue

<template>
    <Disclosure as="nav" class="bg-gray-50 shadow-md sticky top-0 z-50" v-slot="{ open }">
        <div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
            <div class="relative flex h-16 items-center justify-between">
                <div class="flex flex-1 items-center justify-start items-stretch">
                    <div class="flex flex-shrink-0 items-center">
                        <img class="block h-8 w-auto lg:hidden" src="path/to/resume-maker-logo-small.png" alt="ResumeMaker">
                        <img class="hidden h-8 w-auto lg:block" src="path/to/resume-maker-logo-large.png" alt="ResumeMaker">
                    </div>

                </div>

                    <a href="/login" class="text-sm text-white bg-pink-600 hover:bg-pink-900 px-3 py-2 rounded-md">Login</a>
                    <a href="/register" class="text-sm text-white bg-pink-600 hover:bg-pink-900 ml-3 px-3 py-2 rounded-md">Register</a>
            </div>

        </div>

    </Disclosure>
</template>

<script setup>
import { Disclosure } from '@headlessui/vue'
import {usePage} from "@inertiajs/vue3";

const { props } = usePage()


</script>

GuestFunctions.vue

<template>
    <div ref="functionsSection" class="bg-gray-50 py-24 sm:py-32">
        <div class="mx-auto max-w-7xl px-6 lg:px-8">
            <div class="mx-auto max-w-2xl lg:text-center">
                <h2 class="text-base font-semibold leading-7 text-purple-500">Build Your Resume</h2>
                <p class="mt-2 text-3xl font-bold tracking-tight text-gray-800 sm:text-4xl">Everything you need to craft your perfect resume</p>
                <p class="mt-6 text-lg leading-8 text-gray-700">Choose from dozens of templates, customize your content in real time, and launch your professional journey with ease.</p>
            </div>
            <div class="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-4xl">
                <dl class="grid max-w-xl grid-cols-1 gap-x-8 gap-y-10 lg:max-w-none lg:grid-cols-2 lg:gap-y-16">
                    <div v-for="feature in features" :key="feature.name" class="relative pl-16">
                        <dt class="text-base font-semibold leading-7 text-gray-900">
                            <div class="absolute left-0 top-0 flex h-10 w-10 items-center justify-center rounded-lg bg-purple-400">
                                <component :is="feature.icon" class="h-6 w-6 text-white" aria-hidden="true" />
                            </div>
                            {{ feature.name }}
                        </dt>
                        <dd class="mt-2 text-base leading-7 text-gray-700">{{ feature.description }}</dd>
                    </div>
                </dl>
            </div>
        </div>
    </div>
</template>

<script setup>
import { ShieldCheckIcon, DocumentIcon, PencilIcon, CloudIcon } from '@heroicons/vue/24/outline'

const features = [
    {
        name: 'Customizable Templates',
        description:
            'Start with professionally designed templates that highlight your skills and experience.',
        icon: DocumentIcon,
    },
    {
        name: 'Real-time Editing',
        description:
            'Edit and format your resume in real-time. Our intuitive editor makes it easy to add, remove, and rearrange sections to suit your needs.',
        icon: PencilIcon,
    },
    {
        name: 'Cloud Storage',
        description:
            'Access your resumes from anywhere at any time.',
        icon: CloudIcon,
    },
    {
        name: 'Privacy Controls',
        description:
            'Your privacy is paramount. Control who sees your resume with advanced privacy settings, including password protection and link sharing.',
        icon: ShieldCheckIcon,
    },
]
</script>

GuestHero.vue

<template>
    <div class="relative isolate overflow-hidden bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500">
        <div class="mx-auto max-w-7xl px-6 pb-24 pt-10 sm:pb-32 lg:flex lg:px-8 lg:py-40">
            <div class="mx-auto max-w-2xl lg:mx-0 lg:max-w-xl lg:flex-shrink-0 lg:pt-8">
                <h1 class="mt-10 text-4xl font-bold tracking-tight text-white sm:text-6xl">Craft your professional resume for free</h1>
                <p class="mt-6 text-lg leading-8 text-gray-200">Build a professional and polished resume with ease. Choose from a variety of templates, customize your content, and showcase your skills to potential employers.</p>
                <div class="mt-10 flex items-center gap-x-6">
                    <Link href="/register" class="animate-pulse rounded-md bg-green-200 px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-lg hover:bg-green-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-400">Create Resume</Link>
                </div>
            </div>
            <div class="mx-auto mt-16 flex max-w-2xl sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 lg:max-w-none lg:flex-none xl:ml-32">
                <div class="max-w-3xl flex-none sm:max-w-5xl lg:max-w-none">
                    <div class="-m-2 rounded-xl bg-gray-900/10 p-2 ring-1 ring-inset ring-gray-900/20 lg:-m-4 lg:rounded-2xl lg:p-4">
                        <img src="https://placehold.co/1024x600" alt="Screenshot" width="2432" height="1442" class="w-[76rem] rounded-md shadow-xl ring-1 ring-gray-900/20" />
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
import {Link} from "@inertiajs/vue3";
</script>

GuestCTA.Vue

<template>
    <div class="bg-white py-24 sm:py-32">
        <div class="mx-auto max-w-7xl px-6 lg:flex lg:items-center lg:justify-between lg:px-8">
            <h2 class="text-3xl font-bold tracking-tight text-gray-800 sm:text-4xl">Ready to dive in?<br /><span class="text-purple-500">Create Your Free Resume Today.</span></h2>
            <div class="mt-10 flex items-center gap-x-6 lg:mt-0 lg:flex-shrink-0">
                <Link href="/register" class="rounded-md bg-purple-500 px-5 py-3 text-sm font-semibold text-white shadow-lg transform transition duration-150 ease-in-out hover:bg-purple-700 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2">Get started</Link>
            </div>
        </div>
    </div>
</template>
<script setup>
import {Link} from "@inertiajs/vue3";
</script>

GuestFooter.Vue

<template>
    <footer class="bg-gray-100">
        <div class="mx-auto max-w-7xl overflow-hidden px-6 py-20 sm:py-24 lg:px-8">
            <nav class="-mb-6 columns-2 sm:flex sm:justify-center sm:space-x-12" aria-label="Footer">
                <div v-for="item in navigation.main" :key="item.name" class="pb-6">
                    <Link :href="item.href" class="text-sm leading-6 text-gray-600 hover:text-gray-900">{{ item.name }}</Link>
                </div>
            </nav>
            <div class="mt-10 flex justify-center space-x-10">
                <a v-for="item in navigation.social" :key="item.name" :href="item.href" class="text-gray-400 hover:text-gray-500">
                    <span class="sr-only">{{ item.name }}</span>
                    <component :is="item.icon" class="h-6 w-6" aria-hidden="true" />
                </a>
            </div>
            <p class="mt-10 text-center text-xs leading-5 text-gray-500">&copy; 2024 <a href="https://aludvigsson.com">Andreas Ludvigsson</a></p>
        </div>
    </footer>
</template>

<script setup>
import { defineComponent, h } from 'vue'
import {Link} from "@inertiajs/vue3";

const navigation = {
    main: [
        { name: 'Sign in', href: '/login' },
        { name: 'Register', href: '/register' },
    ],
    social: [
        {
            name: 'Facebook',
            href: '#',
            icon: defineComponent({
                render: () =>
                    h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
                        h('path', {
                            'fill-rule': 'evenodd',
                            d: 'M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z',
                            'clip-rule': 'evenodd',
                        }),
                    ]),
            }),
        },
        {
            name: 'Instagram',
            href: '#',
            icon: defineComponent({
                render: () =>
                    h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
                        h('path', {
                            'fill-rule': 'evenodd',
                            d: 'M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z',
                            'clip-rule': 'evenodd',
                        }),
                    ]),
            }),
        },
        {
            name: 'X',
            href: '#',
            icon: defineComponent({
                render: () =>
                    h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
                        h('path', {
                            d: 'M13.6823 10.6218L20.2391 3H18.6854L12.9921 9.61788L8.44486 3H3.2002L10.0765 13.0074L3.2002 21H4.75404L10.7663 14.0113L15.5685 21H20.8131L13.6819 10.6218H13.6823ZM11.5541 13.0956L10.8574 12.0991L5.31391 4.16971H7.70053L12.1742 10.5689L12.8709 11.5655L18.6861 19.8835H16.2995L11.5541 13.096V13.0956Z',
                        }),
                    ]),
            }),
        },
        {
            name: 'GitHub',
            href: '#',
            icon: defineComponent({
                render: () =>
                    h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
                        h('path', {
                            'fill-rule': 'evenodd',
                            d: 'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z',
                            'clip-rule': 'evenodd',
                        }),
                    ]),
            }),
        },
        {
            name: 'YouTube',
            href: '#',
            icon: defineComponent({
                render: () =>
                    h('svg', { fill: 'currentColor', viewBox: '0 0 24 24' }, [
                        h('path', {
                            'fill-rule': 'evenodd',
                            d: 'M19.812 5.418c.861.23 1.538.907 1.768 1.768C21.998 8.746 22 12 22 12s0 3.255-.418 4.814a2.504 2.504 0 0 1-1.768 1.768c-1.56.419-7.814.419-7.814.419s-6.255 0-7.814-.419a2.505 2.505 0 0 1-1.768-1.768C2 15.255 2 12 2 12s0-3.255.417-4.814a2.507 2.507 0 0 1 1.768-1.768C5.744 5 11.998 5 11.998 5s6.255 0 7.814.418ZM15.194 12 10 15V9l5.194 3Z',
                            'clip-rule': 'evenodd',
                        }),
                    ]),
            }),
        },
    ],
}
</script>

And with that, we have successfully created a stunning landing page for our AI-powered Resume Maker SaaS! In the next part of this series, we will dive into the backend design and the development of the resume editor. Stay tuned for more exciting updates as we continue to build our application!

saas resume template landing page

Leave a Reply

Your email address will not be published. Required fields are marked *