Auto-deploy: 2026-08-05 17:41:16

This commit is contained in:
Hamza-Ayed
2026-08-05 17:41:16 +03:00
parent ccdbd29770
commit 380f3e4811
3 changed files with 66 additions and 19 deletions
+37 -15
View File
@@ -66,11 +66,12 @@
'.topcard__title',
]);
// LinkedIn renders screen-reader-only headings throughout its chrome
// ("0 notifications", "1 new message"). They are real <h2> elements with
// real text, so any document-wide heading scan will happily return one as
// the job title. Require the element to be visibly rendered and to not look
// like navigation furniture.
// LinkedIn's job panel is full of text that is neither navigation nor the
// job title: screen-reader-only headings ("0 notifications"), and product
// furniture inside the detail column itself ("Are these results helpful?",
// "Your profile and resume match...", "Get personalized tips..."). A
// heuristic DOM scan cannot reliably tell those from a job title, so the
// scan is now the LAST resort rather than the second.
function isVisible(el) {
if (!el || !el.offsetParent) return false;
if (el.closest('[aria-hidden="true"]')) return false;
@@ -80,20 +81,48 @@
return !/visually-hidden|sr-only|a11y-text/.test(cls);
}
const NAV_NOISE = /notification|message|invitation|my network|feed update|search|premium|^\d+\s|jobs|people|similar|featured|recommended/i;
const NAV_NOISE = new RegExp([
// chrome / navigation
'notification', 'message', 'invitation', 'my network', 'feed update',
'premium', 'search', 'similar', 'featured', 'recommended',
// in-panel product furniture — the class of string that broke this before
'helpful', 'results', 'feedback', 'your profile', 'match', 'tips',
'people you', 'about the job', 'sign in', 'join now', 'people also',
'jobs based on', 'reach out',
// counters
'^\\d+\\s'
].join('|'), 'i');
function plausibleTitle(text) {
return text.length > 4 && text.length < 100 && !NAV_NOISE.test(text);
}
// Fallback: a heading inside the job detail panel only — never page-wide.
// Preferred fallback: the browser tab title. LinkedIn sets it to
// "(N) Job Title | Company | LinkedIn" on every job page, it is not part of
// the React DOM that keeps getting restructured, and it cannot pick up a
// stray card heading. This is far more reliable than guessing at headings,
// so it runs before the DOM scan, not after it.
if (!data.jobTitle) {
// The "(N)" prefix is LinkedIn's unread counter and can read "(99+)".
const m = document.title.match(/^(?:\(\d+\+?\)\s*)?(.+?)\s*[|·]/);
if (m) {
const candidate = m[1].trim();
if (plausibleTitle(candidate) && !/^linkedin$/i.test(candidate)) {
data.jobTitle = candidate;
}
}
}
// Last resort: a heading inside the job detail panel only — never page-wide.
if (!data.jobTitle) {
const jobPanel = document.querySelector(
'.scaffold-layout__detail-column, .jobs-search__job-details, .job-view-layout, ' +
'.jobs-details, [class*="jobs-unified-top-card"], main'
);
if (jobPanel) {
for (const h of jobPanel.querySelectorAll('h1, h2')) {
// h1 first: the job title is a level-1 heading, while the cards that
// polluted this before ("Are these results helpful?") are h2/h3.
for (const h of [...jobPanel.querySelectorAll('h1'), ...jobPanel.querySelectorAll('h2')]) {
const text = h.textContent.trim();
if (isVisible(h) && plausibleTitle(text)) {
data.jobTitle = text;
@@ -103,13 +132,6 @@
}
}
// Last resort: the browser tab title, which LinkedIn formats as
// "(N) Job Title | Company | LinkedIn". Still better than a nav label.
if (!data.jobTitle) {
const m = document.title.match(/^(?:\(\d+\)\s*)?(.+?)\s*\|/);
if (m && plausibleTitle(m[1].trim())) data.jobTitle = m[1].trim();
}
// ── Company Name
data.company = trySelectors([
'.job-details-jobs-unified-top-card__company-name a',