@@ -35,7 +35,33 @@ export class LinkedInAdapter implements PlatformAdapter {
3535 * Get the unique ID for a post element
3636 */
3737 getPostId ( element : Element ) : string | null {
38- // Try to get the data-urn attribute
38+ // Try componentkey attribute first (new LinkedIn DOM structure)
39+ const componentKey = element . getAttribute ( linkedInDataAttributes . componentKey ) ;
40+ if ( componentKey ) {
41+ const expandedMatch = componentKey . match ( linkedInDataAttributes . componentKeyPattern ) ;
42+ if ( expandedMatch ) {
43+ return expandedMatch [ 1 ] ;
44+ }
45+ // Try simple pattern for non-expanded keys
46+ const simpleMatch = componentKey . match ( linkedInDataAttributes . componentKeySimplePattern ) ;
47+ if ( simpleMatch ) {
48+ return simpleMatch [ 1 ] ;
49+ }
50+ }
51+
52+ // Check parent/ancestor elements for componentkey
53+ const parentWithKey = element . closest ( '[componentkey*="FeedType"]' ) ;
54+ if ( parentWithKey ) {
55+ const parentKey = parentWithKey . getAttribute ( 'componentkey' ) ;
56+ if ( parentKey ) {
57+ const match = parentKey . match ( linkedInDataAttributes . componentKeyPattern ) ;
58+ if ( match ) {
59+ return match [ 1 ] ;
60+ }
61+ }
62+ }
63+
64+ // Legacy: Try to get the data-urn attribute
3965 const urn = element . getAttribute ( linkedInDataAttributes . postUrn ) ;
4066 if ( urn ) {
4167 const activityMatch = urn . match ( linkedInDataAttributes . activityUrnPattern ) ;
@@ -48,7 +74,7 @@ export class LinkedInAdapter implements PlatformAdapter {
4874 }
4975 }
5076
51- // Check parent elements for the URN
77+ // Legacy: Check parent elements for the URN
5278 const parent = element . closest ( '[data-urn]' ) ;
5379 if ( parent ) {
5480 const parentUrn = parent . getAttribute ( 'data-urn' ) ;
@@ -158,6 +184,23 @@ export class LinkedInAdapter implements PlatformAdapter {
158184 * Extract the author's name from a post element
159185 */
160186 private extractAuthorName ( container : Element ) : string | undefined {
187+ // New structure: look for the actor link and find the name in nearby elements
188+ const actorImage = container . querySelector ( '[data-view-name="feed-actor-image"]' ) ;
189+ if ( actorImage ) {
190+ // The name is typically in a sibling anchor element's first paragraph
191+ const nextAnchor = actorImage . nextElementSibling ;
192+ if ( nextAnchor ?. tagName === 'A' ) {
193+ const namePara = nextAnchor . querySelector ( 'p' ) ;
194+ if ( namePara ) {
195+ const text = namePara . textContent ?. trim ( ) ;
196+ if ( text && ! text . includes ( 'followers' ) ) {
197+ return text . replace ( / \s + / g, ' ' ) . split ( '\n' ) [ 0 ] . trim ( ) ;
198+ }
199+ }
200+ }
201+ }
202+
203+ // Try original selectors as fallback
161204 const selectors = this . selectors . authorName . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ;
162205
163206 for ( const selector of selectors ) {
@@ -178,6 +221,23 @@ export class LinkedInAdapter implements PlatformAdapter {
178221 * Extract the author's headline/description
179222 */
180223 private extractAuthorHeadline ( container : Element ) : string | undefined {
224+ // New structure: look for the actor link and find headline in nearby elements
225+ const actorImage = container . querySelector ( '[data-view-name="feed-actor-image"]' ) ;
226+ if ( actorImage ) {
227+ const nextAnchor = actorImage . nextElementSibling ;
228+ if ( nextAnchor ?. tagName === 'A' ) {
229+ const paragraphs = nextAnchor . querySelectorAll ( 'p' ) ;
230+ // Second paragraph is typically the headline/followers
231+ if ( paragraphs . length >= 2 ) {
232+ const text = paragraphs [ 1 ] . textContent ?. trim ( ) ;
233+ if ( text ) {
234+ return text . split ( '\n' ) [ 0 ] . trim ( ) ;
235+ }
236+ }
237+ }
238+ }
239+
240+ // Fallback to original selectors
181241 if ( ! this . selectors . authorHeadline ) return undefined ;
182242 const text = this . extractTextContent ( container , this . selectors . authorHeadline ) ;
183243 if ( text ) {
@@ -191,13 +251,23 @@ export class LinkedInAdapter implements PlatformAdapter {
191251 * Extract the author's profile URL
192252 */
193253 private extractAuthorProfileUrl ( container : Element ) : string | undefined {
254+ // New structure: look for the actor image link
255+ const actorImage = container . querySelector ( '[data-view-name="feed-actor-image"]' ) as HTMLAnchorElement | null ;
256+ if ( actorImage ?. href ) {
257+ const href = actorImage . href ;
258+ if ( href . includes ( '/in/' ) || href . includes ( '/company/' ) ) {
259+ return href . split ( '?' ) [ 0 ] ; // Remove query params
260+ }
261+ }
262+
263+ // Fallback to original selectors
194264 if ( ! this . selectors . authorProfileLink ) return undefined ;
195265
196266 const selectors = this . selectors . authorProfileLink . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ;
197267
198268 for ( const selector of selectors ) {
199269 const link = container . querySelector ( selector ) as HTMLAnchorElement | null ;
200- if ( link ?. href && link . href . includes ( '/in/' ) ) {
270+ if ( link ?. href && ( link . href . includes ( '/in/' ) || link . href . includes ( '/company/' ) ) ) {
201271 return link . href . split ( '?' ) [ 0 ] ; // Remove query params
202272 }
203273 }
@@ -230,15 +300,18 @@ export class LinkedInAdapter implements PlatformAdapter {
230300 // Clone the element to avoid modifying the original
231301 const clone = element . cloneNode ( true ) as Element ;
232302
233- // Remove hidden elements
234- clone . querySelectorAll ( '.visually-hidden, [aria-hidden="true"]' ) . forEach ( ( el ) => {
303+ // Remove hidden elements (new structure uses class patterns for visually-hidden)
304+ clone . querySelectorAll ( '.visually-hidden, [aria-hidden="true"], ._7e570f38 ' ) . forEach ( ( el ) => {
235305 // Keep aria-hidden spans that contain the actual visible text
236- if ( ! el . closest ( '.update-components-text' ) ) {
306+ const parent = el . closest ( '[data-testid="expandable-text-box"]' ) ;
307+ if ( ! parent ) {
237308 el . remove ( ) ;
238309 }
239310 } ) ;
240311
241- // Remove "see more" buttons
312+ // Remove "see more" buttons (new structure)
313+ clone . querySelectorAll ( '[data-testid="expandable-text-button"]' ) . forEach ( ( el ) => el . remove ( ) ) ;
314+ // Legacy "see more" buttons
242315 clone . querySelectorAll ( '.feed-shared-inline-show-more-text__see-more-less-toggle' ) . forEach ( ( el ) => el . remove ( ) ) ;
243316
244317 let text = clone . textContent || '' ;
@@ -251,7 +324,7 @@ export class LinkedInAdapter implements PlatformAdapter {
251324 . trim ( ) ;
252325
253326 // Remove common LinkedIn UI text
254- text = text . replace ( / \s * ( s e e m o r e | s e e l e s s | … m o r e ) \s * / gi, '' ) . trim ( ) ;
327+ text = text . replace ( / \s * ( s e e m o r e | s e e l e s s | … m o r e | … m o r e ) \s * / gi, '' ) . trim ( ) ;
255328
256329 return text ;
257330 }
@@ -329,15 +402,33 @@ export class LinkedInAdapter implements PlatformAdapter {
329402 * Check if the post is a repost
330403 */
331404 private isRepost ( container : Element ) : boolean {
332- if ( ! this . selectors . repostIndicator ) return false ;
405+ // New structure: check for repost header
406+ const headerText = container . querySelector ( '[data-view-name="feed-header-text"]' ) ;
407+ if ( headerText ) {
408+ const text = headerText . textContent ?. toLowerCase ( ) || '' ;
409+ if ( text . includes ( 'repost' ) ) {
410+ return true ;
411+ }
412+ }
333413
334- const indicator = container . querySelector ( this . selectors . repostIndicator ) ;
335- if ( indicator ) {
336- const text = indicator . textContent ?. toLowerCase ( ) || '' ;
337- return text . includes ( 'repost' ) ;
414+ // Also check for the header actor image (indicates someone shared/reposted)
415+ const headerActor = container . querySelector ( '[data-view-name="feed-header-actor-image"]' ) ;
416+ if ( headerActor ) {
417+ return true ;
338418 }
339419
340- // Also check for reshare URN
420+ // Legacy: check repost indicator selector
421+ if ( this . selectors . repostIndicator ) {
422+ const indicator = container . querySelector ( this . selectors . repostIndicator ) ;
423+ if ( indicator ) {
424+ const text = indicator . textContent ?. toLowerCase ( ) || '' ;
425+ if ( text . includes ( 'repost' ) ) {
426+ return true ;
427+ }
428+ }
429+ }
430+
431+ // Legacy: check for reshare URN
341432 const urn = container . getAttribute ( 'data-urn' ) || '' ;
342433 return urn . includes ( 'reshare' ) ;
343434 }
@@ -346,10 +437,23 @@ export class LinkedInAdapter implements PlatformAdapter {
346437 * Check if the post is sponsored
347438 */
348439 private isSponsored ( container : Element ) : boolean {
440+ // New structure: check for sponsored indicator
349441 const indicator = container . querySelector ( linkedInExtraSelectors . sponsoredIndicator ) ;
350442 if ( indicator ) return true ;
351443
352- // Check for "Promoted" or "Sponsored" text
444+ // Check for "Promoted" or "Sponsored" text in the actor area
445+ const actorImage = container . querySelector ( '[data-view-name="feed-actor-image"]' ) ;
446+ if ( actorImage ) {
447+ const nextAnchor = actorImage . nextElementSibling ;
448+ if ( nextAnchor ) {
449+ const text = nextAnchor . textContent ?. toLowerCase ( ) || '' ;
450+ if ( text . includes ( 'promoted' ) || text . includes ( 'sponsored' ) ) {
451+ return true ;
452+ }
453+ }
454+ }
455+
456+ // Legacy: check for "Promoted" or "Sponsored" text
353457 const actorDescription = container . querySelector ( '.update-components-actor__sub-description' ) ;
354458 if ( actorDescription ) {
355459 const text = actorDescription . textContent ?. toLowerCase ( ) || '' ;
0 commit comments