Linkedin Job Scanner
Introducing the LinkedIn Job Scanner Chrome Extension!
Tired of sifting through countless job posts on LinkedIn? Let our extension do the heavy lifting for you! The LinkedIn Job Scanner Extension is here to help you find the job opportunities that match your keywords effortlessly.
Chrome Webstore Download Link
Youtube How to use Video Link
Why I Created This Extension
As someone in search of frontend positions in Germany without proficiency in German, I faced the challenge of finding English-language job opportunities on LinkedIn that matched my skills. I quickly realized that sifting through countless job posts was both time-consuming and demotivating. Additionally, there were relatively fewer junior-level English positions available. During my job search, I found myself repeatedly reading job articles and scanning for specific keywords in job titles and descriptions. This gave me the idea to create an application that could automatically find job posts containing these keywords, making the job search process more manageable and less overwhelming.
I developed the extension using React in the Vite environment. In the following content, I'm going to share problems I encountered during development and how I dealt with them.
7 Problems I Encountered While Developing
1. Handling Updates with useFieldArray
in react-hook-form
There is a condition type that has several sub-conditions.
export interface JobCondition {
id: string;
subConditions: {
id: string;
not: boolean;
caseInsensitive: boolean;
target: ConditionTarget;
operator: ConditionOperator;
text: string;
frequency: number;
}[];
}
For example, if you want to look for job posts that contain frontend
or react
in the job title and react
in the job description.
It will look like
const conditions:JobCondition[] = [
{
id: 'jca',
subConditions: [
{
id: 'sca',
not: false,
caseInsensitive: true,
target: "title",
operator: '>=',
text: 'react',
frequency: 1,
},
{
id: 'scb',
not: false,
caseInsensitive: true,
target: "title",
operator: '>=',
text: 'frontend',
frequency: 1,
}
]
},
{
id: 'jcb',
subConditions: [
{
id: 'sca',
not: false,
caseInsensitive: true,
target: "description",
operator: '>=',
text: 'react',
frequency: 1,
},
]
}
];
Each job post should satisfy all conditions, and meeting a condition implies that a job post fulfills at least one of the sub-conditions.
If you click the button highlighted in red within the image, the sub-condition will be included in the list depicted in blue lines.
To manage form data, I opted for react-hook-form
and used the useFieldArray
for handling arrays. However, I faced an issue that updating the sub-conditions updates to the entire condition, causing a visible rendering flicker.
{fields.map((item, index) => (
<li key={item.id}>
<input {...register(`test.${index}.firstName`)} />
<Controller
render={({ field }) => <input {...field} />}
name={`test.${index}.lastName`}
control={control}
/>
<button type="button" onClick={() => remove(index)}>Delete</button>
</li>
))}
Referring to the official document, it was suggested to utilize item.id
. But I found that the id
changes when the update
function is called. I initially expected that changing the sub-conditions field within the item would only update that field.
There might have been other solutions, but I ended up replacing it with useState
.
2. Data loss when the UI is closed
When a user clicks outside of the UI, the extension is closed and the data is gone.
I encountered this issue multiple times during testing, so I addressed it by saving the user's work-in-progress data to chrome.storage
.
export interface TaskFormDraft {
taskId: string | null;
isEdit: boolean;
value: {
taskName: string;
delay: number;
jobConditions: JobCondition[];
};
}
export interface StorageData {
tasks?: JobTask[];
activeTask?: ActiveJobTask;
draft?: TaskFormDraft;
}
//...
export const draftTaskFormData = async (draftData: TaskFormDraft) => {
if (!window.chrome?.storage?.local) return;
const versionData = await chrome.storage.local.get(STORAGE_VERSION);
const data = (versionData[STORAGE_VERSION] ?? {}) as StorageData;
await chrome.storage.local.set({
[STORAGE_VERSION]: {
...data,
draft: draftData,
},
});
};
export const loadDraftTaskFormData =
async (): Promise<TaskFormDraft | null> => {
if (!window.chrome?.storage?.local) return null;
const versionData = await chrome.storage.local.get(STORAGE_VERSION);
const data = (versionData[STORAGE_VERSION] ?? {}) as StorageData;
return data.draft ?? null;
};
3. Job Post Loading Check
This is the loading UI of a job post on Linkedin, but, the disappearance of the loading UI doesn't indicate that the job has finished loading.
I noticed that certain HTML elements have classes like "--loading" during loading, and skeleton elements have the "jobs-ghost-fadein-placeholder" class.
I wrote a function like below to check whether a job post is done loading or not. However, this approach was more of a guess than an analysis.
export const isLoading = () => {
const loading =
document.querySelector('#main *[class*=--loading]') ??
document.querySelector('.jobs-ghost-fadein-placeholder');
return loading !== null ? true : false;
};
I considered an alternative approach, which involved comparing the current job post's contents with the previous one. Fortunately, it seemed to work well.
Now that I think about it, I could have tried to capture all HTML variations to get more details.
4. DOM Test Code
I implemented functions that grab content from job posts. Initially, I thought about fetching the LinkedIn job listing page before testing, but since it doesn't allow us to see job posts without signing in, that approach wasn't feasible.
I downloaded HTML files by myself and loaded the HTML files in the test code.
const jobListHtml = readFileSync(path.join(__dirname, 'html/joblist.html'), {
encoding: 'utf-8',
flag: 'r',
}).toString();
const jobListLoadingHtml = readFileSync(
path.join(__dirname, 'html/joblist-detail-contents-loading.html'),
{
encoding: 'utf-8',
flag: 'r',
}
).toString();
const jobListLastPageHtml = readFileSync(
path.join(__dirname, 'html/joblist-last-page.html'),
{
encoding: 'utf-8',
flag: 'r',
}
).toString();
describe('JobList Loading Page', () => {
beforeAll(() => {
document.documentElement.innerHTML = jobListLoadingHtml;
});
describe('isLoading', () => {
test('should return true.', () => {
expect(isLoading()).toBe(true);
});
});
});
It would be better in the future to have a program that downloads HTML files to update with the latest pages. In the process, signing up logic might be necessary.
5. Chrome Storage Test Code
Actually, I didn't consider testing chrome.storage
as I aimed to complete the project quickly. I am unsure if there are existing methods for testing it. If not, we might be able to test it by creating mock functions for chrome.storage
.
6. Module Error in Chrome Extention Content Script
While I don't have a deep understanding of the Vite
build system, it seemed that sharing code between the React project and the content script, which runs in the background of the web browser, created a new file to optimize code sharing. Just to clarify, there are two entry points to generate two outputs.
It was okay, but the problem was that import
didn't work in the content script. At first, I transpiled the code to a lower version of ES and used commonjs
modules, but it still didn't work.
I found Browserify, which lets you require
in the browser by bundling up all of my dependencies. I solved the content script problem after applying it, but the React project encountered some other issues.
I ended up using a separate configuration file for each entry point to avoid generating the shared code file and browserify the content script.
[vite.config.ts]
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
build: {
rollupOptions: {
input: {
main: './index.html',
},
output: {
entryFileNames: `assets/[name].js`,
},
},
},
});
[vite-script.config.ts]
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import commonjs from '@rollup/plugin-commonjs';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
build: {
emptyOutDir: false,
target: 'ES2015',
rollupOptions: {
plugins: [commonjs()],
input: {
content: './src/content/content.ts',
},
output: {
entryFileNames: `assets/[name].js`,
format: 'cjs',
},
},
},
});
[package.json]
{
"scripts": {
"build": "tsc && vite build && vite build -c vite-script.config.ts && browserify ./dist/assets/content.js -o ./dist/assets/content.js",
}
}
7. Ensuring the stability of the code in the repository
The code in the repository must pass tests and build successfully. To ensure the stability of the code, I added a Github action that runs tests and builds the project when changes are pushed to the main branch.
name: Test
on:
push:
branches: ['main']
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
shell: bash
- name: Run test files
run: pnpm run test
- name: Build
run: pnpm run build
Wrap Up
I found something interesting. Even though there were thousands of job posts, the extension couldn't reach all of them. The last page was 40(1000 job posts).
As I was eager to complete the project quickly while searching for jobs, There is still plenty of room for improvement and potential bugs.
If you have any suggestions or find bugs, please let me know through the github issues. Your contributions to improving and refactoring the code would be appreciated as well.
Getting a developer position is absolutely brutal, much more than I expected. I have heard people saying they have failed hundreds, and even thousands, of applications. I also haven't received any positive responses from over 60 companies I applied to. I haven't even had one interview. I expected to fail some interviews due to my English skills, but I couldn't have expected not even getting a chance to fail. I felt frustrated by the results and thought about going back to my country. However, the people around me kept supporting and encouraging me. I truly appreciate the support from my friends. I am going to start job searching again.
I hope some people find the extension helpful. For me, the happiest moment as a developer is when I find my work is helpful to someone else.
Some of you may be in the same situation as I am. Don't give up, and let's keep trying together. Your efforts will pay.
There is a quote that I saw a few days ago in one of the feeds on LinkedIn.
"You Never Fail Until You Stop Trying"
Thanks for reading this.
Top comments (2)
I think this is a smart idea, especially with the current LinkedIn algorithms. Good luck with your job search!
Thank you very much π