feat(capture): add minimal browser extension

This commit is contained in:
cesnimda
2026-08-31 22:16:16 +02:00
parent b7029faaec
commit caf486dceb
5 changed files with 95 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# Jobjakt Quick Capture
The Manifest V3 extension opens the current page or selected link in Jobjakt's existing reviewed import flow. It does not scrape page content, store credentials, or submit an application automatically.
## Install locally
1. Open `chrome://extensions` (or the equivalent extensions page in Edge).
2. Enable developer mode.
3. Choose **Load unpacked** and select this `browser-extension` folder.
4. Open a vacancy and click the Jobjakt extension, or use **Save vacancy to Jobjakt** from the page/link context menu.
The Jobjakt add-job dialog opens with the vacancy URL and runs the same server-side preview, validation, duplicate checking, and user review used by normal URL imports.
## Test
```sh
node --test browser-extension/tests/*.test.mjs
```
No build or package installation is required. Zip the folder contents for store submission after adding store artwork and completing the browser-store privacy declarations.
+23
View File
@@ -0,0 +1,23 @@
import { buildCaptureUrl } from "./capture-url.mjs";
const menuId = "save-vacancy-to-jobjakt";
function openCapture(pageUrl) {
const captureUrl = buildCaptureUrl(pageUrl);
if (captureUrl) chrome.tabs.create({ url: captureUrl });
}
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: menuId,
title: "Save vacancy to Jobjakt",
contexts: ["page", "link"]
});
});
});
chrome.action.onClicked.addListener((tab) => openCapture(tab.url));
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === menuId) openCapture(info.linkUrl || info.pageUrl || tab?.url);
});
+15
View File
@@ -0,0 +1,15 @@
export const JOBBJAKT_CAPTURE_ORIGIN = "https://jobs.cesnimda.uk";
export function buildCaptureUrl(pageUrl, origin = JOBBJAKT_CAPTURE_ORIGIN) {
let source;
try {
source = new URL(pageUrl);
} catch {
return null;
}
if (source.protocol !== "http:" && source.protocol !== "https:") return null;
const capture = new URL("/", origin);
capture.searchParams.set("add", source.href);
return capture.href;
}
+14
View File
@@ -0,0 +1,14 @@
{
"manifest_version": 3,
"name": "Jobjakt Quick Capture",
"version": "1.0.0",
"description": "Send the current vacancy to Jobjakt's reviewed import flow.",
"permissions": ["activeTab", "contextMenus"],
"action": {
"default_title": "Save vacancy to Jobjakt"
},
"background": {
"service_worker": "background.mjs",
"type": "module"
}
}
@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildCaptureUrl } from "../capture-url.mjs";
test("builds the existing reviewed capture route", () => {
const result = new URL(buildCaptureUrl("https://example.test/jobs/42?from=search"));
assert.equal(result.origin, "https://jobs.cesnimda.uk");
assert.equal(result.pathname, "/");
assert.equal(result.searchParams.get("add"), "https://example.test/jobs/42?from=search");
});
test("rejects privileged and malformed URLs", () => {
assert.equal(buildCaptureUrl("chrome://settings"), null);
assert.equal(buildCaptureUrl("javascript:alert(1)"), null);
assert.equal(buildCaptureUrl("not a url"), null);
});
test("supports a local application origin during development", () => {
assert.equal(
buildCaptureUrl("https://arbeidsplassen.nav.no/stillinger/stilling/abc", "http://localhost:3300"),
"http://localhost:3300/?add=https%3A%2F%2Farbeidsplassen.nav.no%2Fstillinger%2Fstilling%2Fabc"
);
});