If LinkedIn won't make it easy, I'll do it myself!
Jeremy Duffy
Tech and Process Stability SME for BECU | M.S. CS, CISSP, DCELP | Award winning speaker & instructor
(I'm going for "playfully scornful" and mean no disrespect to the LinkedIn folks. That said, how about making my changes (described below) a permanent part of the service?)
The problem
There are many jobs openings at LinkedIn for UX designers right now... at least I hope so because they have some work to do. For example, I've noticed (and previously written about) the frustration of seeing jobs that don't match my requirements of location, job title, and so on.
It's just odd that, even after paying for #linkedinpremium and getting these fancy "top applicant", and "in your network" listings, they do me next to no good because they're often out of state, not remote, or titles that I clearly don't want or qualify for.
Given there's no telling when (or if) those vacant UX positions will be filled and LinkedIn gets around to fixing this, I took it on myself to write a TamperMonkey script to improve the experience now.
(If you're not familiar with TamperMonkey, here's a short tutorial on installing and using it: (6) How to install and use Tampermonkey - YouTube)
Title remover
The first culprit are the various listings that show titles I most definitely don't qualify for (or don't want). For me, this includes "engineer", "director", and "principal". For LinkedIn to show me these is not a good use of my time or their electrons. I can't do anything about their electrons, but I can at least block it off my screen and save my brainpower:
In this photo, you can see that non-applicable jobs are greyed out and minimized, though I eventually changed it so it highlight bad ones in red instead (so you can still see and click them if you're worried about my script getting too "excited".
Location remover
In my entire career, nothing has been more important to my job selection than where that job is physically located. As a family man, I can't just pack up and fly to Iceland on a whim. I'd rather not work somewhere more than 40 minutes away (The Cat's in the Cradle...)
Ergo, anything that's not either remote or in my current state is automatically out:
领英推荐
The above view, "Hiring in my network" is supposed to be a useful way to find jobs related to connections that I have, but ends up being near useless thanks to listing almost exclusively jobs in states I no longer (or never) lived in. Cleaning up that view is a HUGE time saver.
After giving my script a few seconds to work through the duds, it reduced an entire page of results down to the four that might actually be relevant!
Pay Remover
I understand companies have a pay range they can afford, but the same goes for us. If they can't/won't pay enough for me to make my rent, it's not a good use of my time to see them:
Saving time and energy when there isn't much to go around
This is a rough market with tons of skilled amazing people competing for jobs. That means I spend day after day on LinkedIn looking for treasure, but having to sift through dross which is doing no one any favors.
With my script in place, I'm no longer have to worry about things that LinkedIn shouldn't be showing us in the first place!
The script
Here's the script which you can cut and paste into a "new script" in Tampermonkey. You'll need to edit the const values below (there are several near the top) to match the titles you don't want to see, the state you currently live in, pay limits, etc.
Danger!
The point of my script is to clear off as much obvious fluff as possible, but it might trigger on something you'd rather have seen. I did my best and it worked well for me, but LinkedIn could change something at any time so who knows what will happen? Regardless, it doesn't remove anything - just minimizes the "bad" ones. You can still click the job title to see the details if you want. Use at your discretion.
// ==UserScript==
// @name? ? ? ? ?Linked in fixer
// @namespace? ? https://tampermonkey.net/
// @version? ? ? 0.1
// @description? LinkedIn lacks very basic features that would help so I'm going to write them myself.
// @author? ? ? ?Jeremy Duffy
// @match? ? ? ? https://www.dhirubhai.net/*
// @icon? ? ? ? ?https://www.google.com/s2/favicons?sz=64&domain=glassdoor.com
// @grant? ? ? ? none
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
(function() {
? ? 'use strict';
? ? const badTitles = ['engineer','principal','director']; // lowercase titles you wouldn't want. Erase or add as needed
? ? const myStateCode = 'wa'; // Your state two letter code - lower case
? ? const badCompanies = ['coinbase','talentify'];
? ? const minWageHr = "72"; // SET TO 0 TO DISABLE THIS CHECK! hourly wage min - if it's listed and less then this, it gets culled
? ? const minWageYr = "150000"; // SET TO 0 TO DISABLE THIS CHECK! if it's listed and less than this, it gets culled.
? ? const testOnly = false; // when true, bad entries are dimmed and outlined in red instead of removed. Helpful if you worry that the script isn't working properly and will remove the wrong things. Change to "false" if you want to remove the entries instead
? ? function noGood(toTest){
? ? ? ? var testTitle = '';
? ? ? ? var testComp = '';
? ? ? ? var i;
? ? ? ? // Mark this so we don't constantly rehash it.
? ? ? ? // EDIT: nevermind. Details are added dynamically and the script misses them if it was pre-tagged as "done" before that operation finished
? ? ? ? // With no effective way to tell if Linked in is done loading information, we have to redo all visible options every time :(
? ? ? ? //$(toTest).addClass('fixChecked');
? ? ? ? testTitle = $(toTest).find('.job-card-list__title').text().toLowerCase().trim();
? ? ? ? if (testOnly) console.log('checking '+testTitle+'\n');
? ? ? ? if (!testTitle.length) return 0;
? ? ? ? for (i=0; i< badTitles.length; i++){
? ? ? ? ? ? if (testTitle.indexOf(badTitles[i]) != -1) {
? ? ? ? ? ? ? ? dumpJob(toTest,'Blacklisted title '+badTitles[i]);
? ? ? ? ? ? ? ? return 1;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? testComp = $(toTest).find('.artdeco-entity-lockup__subtitle').text().toLowerCase().trim();
? ? ? ? for (i=0; i< badCompanies.length; i++)
? ? ? ? ? ? if (testComp.indexOf(badCompanies[i]) != -1) {
? ? ? ? ? ? ? ? dumpJob(toTest,'Blacklisted company '+badCompanies[i]);
? ? ? ? ? ? ? ? return 1;
? ? ? ? ? ? }
? ? ? ? // If survived title, check location
? ? ? ? var city = null;
? ? ? ? var state = null;
? ? ? ? var remote = null;
? ? ? ? var company = null;
? ? ? ? var cityState = null;
? ? ? ? $(toTest).find('.job-card-container__metadata-item').each(function() {
? ? ? ? ? ? var jobItem = this;
? ? ? ? ? ? // Only check for this once. If we already resolved it, other fields aren't relevant for location
? ? ? ? ? ? if (!remote && !city) {
? ? ? ? ? ? ? ? remote = $(jobItem).text().toLowerCase().trim().match(/\((remote)\)/m);
? ? ? ? ? ? ? ? // If it's remote, we're good. Only check state if it's not
? ? ? ? ? ? ? ? if (!remote) {
? ? ? ? ? ? ? ? ? ? cityState = $(jobItem).text().toLowerCase().trim().match(/^([a-z ]+), ([a-z]{2})$/m);
? ? ? ? ? ? ? ? ? ? if (!cityState) // one more format to try
? ? ? ? ? ? ? ? ? ? ? ? cityState = $(jobItem).text().toLowerCase().trim().match(/^([a-z ]+), ([a-z]{2})[^a-z]/m);
? ? ? ? ? ? ? ? ? ? //console.log(cityState);
? ? ? ? ? ? ? ? ? ? // We only test location if we recognized it's pattern. If we didn't, just leave it alone to be safe
? ? ? ? ? ? ? ? ? ? if (cityState) {
? ? ? ? ? ? ? ? ? ? ? ? city = cityState[1];
? ? ? ? ? ? ? ? ? ? ? ? state = cityState[2];
? ? ? ? ? ? ? ? ? ? ? ? if ((state && state != myStateCode)) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? dumpJob(toTest,'Not remote or in my state ('+myStateCode+' vs '+state+')');
? ? ? ? ? ? ? ? ? ? ? ? ? ? return 1;
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? if (minWageHr) {
? ? ? ? ? ? ? ? var thePay = 0;
? ? ? ? ? ? ? ? var payHr = $(jobItem).text().toLowerCase().trim().match(/\$(.+?)\/hr/g);
? ? ? ? ? ? ? ? if (payHr) {
? ? ? ? ? ? ? ? ? ? // Last matching number is the high range. If solo, it will just be the wage
? ? ? ? ? ? ? ? ? ? // Capture groups are ignored when using the global flag. so stupid...
? ? ? ? ? ? ? ? ? ? thePay = payHr.pop().replace(/[\$\/hr]/g,'');
? ? ? ? ? ? ? ? ? ? thePay = parseFloat(thePay);
? ? ? ? ? ? ? ? ? ? if (thePay < minWageHr) {
? ? ? ? ? ? ? ? ? ? ? ? dumpJob(toTest,"Hourly too low! Desired: "+minWageHr+" vs actual: "+thePay);
? ? ? ? ? ? ? ? ? ? ? ? return 1;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ? if (minWageYr) {
? ? ? ? ? ? ? ? var payYr = $(jobItem).text().toLowerCase().trim().match(/\$(.+?k?)\/yr/g);
? ? ? ? ? ? ? ? var temp;
? ? ? ? ? ? ? ? if (payYr) {
? ? ? ? ? ? ? ? ? ? // Last matching number is the high range. If solo, it will just be the wage
? ? ? ? ? ? ? ? ? ? // Capture groups are ignored when using the global flag. so stupid...
? ? ? ? ? ? ? ? ? ? thePay = payYr.pop().replace(/[\$\/yr]/gi,'');
? ? ? ? ? ? ? ? ? ? if (thePay.indexOf('k')!=-1) {
? ? ? ? ? ? ? ? ? ? ? ? thePay = thePay.replace('k','');
? ? ? ? ? ? ? ? ? ? ? ? thePay = parseFloat(thePay)*1000;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? else
? ? ? ? ? ? ? ? ? ? ? ? thePay = parseFloat(thePay);
? ? ? ? ? ? ? ? ? ? console.log(thePay);
? ? ? ? ? ? ? ? ? ? if (thePay < minWageYr) {
? ? ? ? ? ? ? ? ? ? ? ? dumpJob(toTest,"Yearly too low! Desired: "+minWageYr+" vs actual: "+thePay);
? ? ? ? ? ? ? ? ? ? ? ? return 1;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? });
? ? ? ? return 0;
? ? }
? ? // In a function to more easily switch between testing and actual removing.
? ? function dumpJob(toDump,reason) {
? ? ? ? var title = $(toDump).find('.job-card-list__title').text().toLowerCase().trim();
? ? ? ? console.log('Dumping '+title+'\n::Reason: '+reason);
? ? ? ? if (testOnly) {
? ? ? ? ? ? // no sense in dumping it twice.
? ? ? ? ? ? if (!$(toDump).hasClass('ddisabled'))
? ? ? ? ? ? ? ? $(toDump).addClass('ddisabled').append('Removal reason: '+reason); // For testing - just make it smaller and hard to see
? ? ? ? }
? ? ? ? else {
? ? ? ? ? ? $(toDump).addClass('rremoved');
? ? ? ? ? ? var grabLink = $(toDump).find('.job-card-container__link');
? ? ? ? ? ? $(toDump).html('Dumping '+grabLink[0].outerHTML+'<br>Removal reason: '+reason); // For real, just get rid of it!
? ? ? ? }
? ? }
? ? $(document).ready(function(){
? ? ? ? $("body").append(`
? ? ? ? <style>
? ? ? ? .ddisabled {
? ? ? ? ? ? opacity: .5;
? ? ? ? ? ? border: 1px solid red;
? ? ? ? }
? ? ? ? .rremoved {
? ? ? ? ? ? font-size: 11px;
? ? ? ? ? ? padding: 10px !important;
? ? ? ? ? ? border: 1px solid rgba(8,8,8,.35);
? ? ? ? }
? ? ? ? </style>
? ? ? ? `);
? ? ? ? function fixLinkedIn(){
? ? ? ? ? ? //if (testOnly) console.log('fixing');
? ? ? ? ? ? $(document).find('.jobs-search-results__list-item').each(function(){
? ? ? ? ? ? ? ? // Check for my custom class so we're not thrashing on the same stuff we already checked
? ? ? ? ? ? ? ? if ($(this).hasClass('fixChecked')) return;
? ? ? ? ? ? ? ? noGood(this);
? ? ? ? ? ? });
? ? ? ? }
? ? ? ? var jobCatcher;
? ? ? ? var scrollT;
? ? ? ? jobCatcher = setInterval(function(){
? ? ? ? ? ? $('.jobs-search-results-list').each(function(){
// We've already dealt with this one
if ($(this).hasClass('scrollFixer')) return;
if ($(document).find('.jobs-search-results__list-item').length) {
//if (testOnly) console.log('attaching');
// Do it for the first set of things on screen
? ? ? ? ? ? ? ? ? ? // but there's a race condition because there's no way to know when the dynamic data is loaded. ugh... who designs this way?
? ? ? ? ? ? ? ? ? ? setTimeout(function(){fixLinkedIn();},200);
// Attach scroll event
$('.jobs-search-results-list').on('scroll',function(){
// Scroll is called mutliple times in a single scrolling event
// Prevent it from overcalling the fixer by killing any pending calls
clearTimeout(scrollT);
// Set a new call. Eventually, if this is the last one and scroll isn't called again, it will launch the fixer function
scrollT = setTimeout(function(){
? ? ? ? ? ? ? ? ? ? ? ? ? ? fixLinkedIn();
? ? ? ? ? ? ? ? ? ? ? ? ? ? if (!testOnly)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // The act of removing entries makes some others come into view that weren't there before.
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // Run it again to catch those.
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? setTimeout(function(){fixLinkedIn();},1000);
? ? ? ? ? ? ? ? ? ? ? ? },100);
});
}
// Mark it as done
$(this).addClass('scrollFixer');
? ? ? ? ? ? });
? ? ? ? },2000);
? ? });
})();
Senior Contract Specialist @ SBA | Government Contracting Expert
1 年Thanks for sharing! I will try this!