Build Your Own Chrome Extension to Track Applications and Conquer the Job Hunt

Yesterday I was feeling a little drained from the constant applications. I think the previous day I had sent well over twenty, each being optimised for ATS scanners, individualised personable cover letters and research into the roles/companies. I thought, what the hell is this, I almost never burn out when programming? I certainly didn't get involved in development to get ghosted and roasted.

Im here to explore my creativity and build cool stuff!

So I thought, ok Darrach, Lets procrastinate viciously and build a little chrome extension to ease the pain. Technically its still in the realm of job hunting right if I build something directly related to the process? Right!?

I dunno, but here goes. Today I am going to show you all what I learnt building a basic chrome extension. It is a simple process if you have a semi decent understanding of JS and the DOM.

Step 1 - Setting Up Your Manifest

Every Chrome extension starts with a manifest file. What is this? Well think of it as a package.json. This file outlines the extension's configuration, including background scripts, content scripts, and actions. Think of it as the blueprint for your extension's functionality. Theres alot of configurables but lets just focus on the key ones. background, content, popup.

  1. The background takes a service worker JS file. Think of it as a little micro server that is constantly listening to all the the information being passed around during your chrome session. It remains active throughout your browsing session, even when the extension's pop-up or content scripts are in use.
  2. The content script is like a silent companion that works directly within the web pages you visit. It injects custom JavaScript into pages you are on, this means we can manipulate the DOM on other websites You can also add custom css files.
  3. The popup html/js deal with the interface, typically appears when the user clicks on the extension's icon in the Chrome toolbar. We can render a popup which is written as any old html file where we can link scripts, css and cdns etc.
  4. Permissions and matches: Permissions are an array of extension APIs needed, while matches determine the URLs on which the content scripts will activate. Here, I have set it to "https://.linkedin.com/jobs" as I only want content.js to run on that specific url.

Step 2 - Lets Plan

Ok, this application's premise is simple. I want a few things to happen:

  1. If I have not completed X amount of applications, an annoying popup notification or a redirect to LinkedIn Jobs boards occurs.
  2. If I am actively on LinkedIn Jobs, pop-ups do not appear, and I can apply for jobs as normal.
  3. When I apply for a job, my application is tracked. Its ID and Date are stored.
  4. When I finish my daily goal, I can browse the web as normal.

Now that's out of the way, let me show you the key steps on how to use Chrome's Extension API to achieve this.

Step 3 - Lets Build: Setting the Goal - Storage API

Firstly we need to set a goal to track against. Here we need an input from the user so this will deal directly with our extension and no content pages. So in the popup.html create a normal html input element and attach an event listener to it.

// Update applications goal total

goalInput.addEventListener("change", async () => {
  let goal = goalInput.value;
  if (goal < 0) {
    goal = 0;
    goalInput.value = goal;
  }
  goalCount.textContent = goal;

  chrome.storage.sync.set({ goal });
});

Here I am just listening for changes to my input element and setting the new goal value in Chrome storage for persistence.

Once a goal is set, I need to check the application amount against it. Once again, the storage API is used. In background.js, I employ an immediately invoking function to check the applications. All it's doing is fetching the applications array and mapping the current applications length vs my goal.

async function checkApplications() {
  applications = await fetchApplications();

  const todayApplications = applications.filter((application) =>
    isDateToday(application.date)
  );
  goal = await fetchApplicationGoal();

  if (todayApplications.length < goal) {
    // If the user has not reached their daily goal, create a notification
    createAlarm();
    createNotification();
  }
}

If there are not sufficient goals, alarms and notifications are triggered.

Step 4 - Lets Build: Annoying the User - Notification/Alarm API

OK now we have are goal and applications tracked we need to annoy the user if they havent done what they are supposed to. We must limit procrastination!

We are using the Notifications API and Alarms API.

// Create an alarm to remind the user to apply for more jobs
function createAlarm() {
  chrome.alarms.create("job_hunt", {
    delayInMinutes: 0,
  });
}

// Create LinkedIn Jobs page notification
function createNotification() {
  chrome.notifications.create(
    "job_hunt",
    {
      type: "basic",
      iconUrl: "images/application.png",
      title: "Daily Goal Not Reached!",
      message: "Apply For More Jobs Now!",
      silent: false,
    },
    (notificationId) => {
      console.log(`Notification ${notificationId} created.`);
    }
  );
}

Here, we are creating the alarm and notification with the ID "job_hunt." Now, what if the user clicks the notification? We want to route the user to the job board, right? Click handlers can be added to these notifications. The click handler can create a new tab based on a specified URL. So below, we are routing the user to the LinkedIn jobs board if the notification ID matches.

  // Redirect to LinkedIn Jobs page when notification is clicked
  chrome.notifications.onClicked.addListener((notificationId) => {
    if (notificationId === "job_hunt") {
      chrome.tabs.create({ url: "https://www.linkedin.com/jobs/" });
    }
  });

Step 5 - Lets Build: Annoying the User - Tab Navigation

Ok we have the notifications set up to trigger when the checkapplications function fires on load, but we want this to annoy the user whenever they navigate around the web if the page is not speficially the linkedin jobs board. Here we can use the Chrome Tabs API.

  // Listens for tab updates and sends a message to content.js
  chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
    if (changeInfo.status === "complete") {
      if (tab.url && !tab.url.includes("linkedin.com/jobs")) {
        checkApplications();
      } else {
        chrome.tabs
          .sendMessage(tabId, { message: "tab_updated" })
          .catch((err) => {
            console.log(err);
          });
      }
    }
  });

Here, we check to see if the changeStatus is completed. If it is, then we check if the tab.url includes linkedin.com/jobs. If it does not, we check the applications again to alert the user if they fail the goal check.

If the page is the LinkedIn jobs board, we want to send a message to the content.js to trigger something. We need to do this for a particular reason.

Step 5 - Lets Build: Submit an application

Ok we have annoyed the user enough for them to stop procrastinating and reach their goals. We must move from the serviceworker/background.js to the content js. We need to attach some event listeners to the DOM so we can update our total application number.

First, let's listen to the message from background.js. Chrome Runtime allows us to listen to messages sent, and here we check to see if the message is tab_updated. If so, we trigger this observer:

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  if (request.message === "tab_updated") {
    const observer = new MutationObserver(callback);
    observer.observe(document.body, { childList: true, subtree: true });
  }
});

Don't be intimidated by this MutationObserver; I also learned about it yesterday. The observer monitors changes in the DOM structure of the webpage. When changes occur, it triggers a callback function, enabling the extension to respond dynamically to these changes.

This is necessary due to the way LinkedIn renders HTML documents. Not all elements are rendered on the server and sent to the client; some hydration occurs. Whether it's ISR or CSR, I don't really know, but the point is that the elements we need to attach click handlers to won't be present on DOMContentLoaded. Therefore, we need to listen for mutations and attach them dynamically.

So here is the callback

const callback = function (mutationsList, observer) {
  const urlParams = new URLSearchParams(window.location.search);
  const currentJobId = urlParams.get("currentJobId");

  for (const mutation of mutationsList) {
    if (mutation.target.classList.contains("jobs-apply-button")) {
      const button = mutation.target;

      if (!button.classList.contains("application-tracker-button")) {
        button.classList.add("application-tracker-button");
        button.addEventListener("click", async () => {
          chrome.runtime.sendMessage({
            message: "applications_incremented",
            id: currentJobId,
          });
        });
      }

      observer.disconnect();
      return;
    }
  }
};

In this code, we're iterating through mutations, specifically targeting the jobs-apply-button classlist as it's present on all application buttons. When found, we add our custom click handler function and adjust the style slightly.

Using the runtime.sendMessage function, we send a message to background.js with the message name and the ID of the current job parsed from the URL.

With this setup, we can update the applications array in our Chrome storage with a new application if the ID is unique.

// Listens for messages from content.js and increments the applications count
  chrome.runtime.onMessage.addListener(async function (
    request,
    sender,
    sendResponse
  ) {
    if (request.message == "applications_incremented") {
      applications = await fetchApplications();

      const newApplication = { date: new Date(), id: request.id };

      if (!applications.some((app) => app.id === newApplication.id)) {
        applications.push(newApplication);

        chrome.storage.sync.set({ applications: JSON.stringify(applications) });

        incrementApplicationsAlarm();
      } else {
        chrome.notifications.create(
          "already_applied",
          {
            type: "basic",
            iconUrl: "images/application.png",
            title: "You have already applied!",
            message: `Choose another job to apply for!`,
            silent: false,
          },
          (notificationId) => {
            console.log(`Notification ${notificationId} created.`);
          }
        );
      }
    }
    sendResponse(() => {
      return false;
    });
  });

Step 6 - Profit

Alright, hopefully, I've outlined enough features present in Google Chrome's Extension API for you to build your own fun little extension.

If you're trudging through the job hunt and could use an extra push, why not download and install the app?

If you want to improve this one or view the full source code, you can find it here!

It only took a day to build, so I'm sure there are a bunch of bugs that need fixing or optimizations to handle, haha. But I can't procrastinate forever!

Avatar for DarrachBarneveld

Written by DarrachBarneveld

Addicted to building

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.