@@ -99,45 +99,116 @@ export class LinkedInAdapter implements PlatformAdapter {
9999
100100 /**
101101 * Generate the permalink URL for a post
102+ * If the postId is already a numeric activity ID, use it directly
103+ * Otherwise, it's a componentKey and we can't generate a valid URL
102104 */
103105 getPostUrl ( postId : string ) : string {
106+ // Check if this is a numeric activity ID
107+ if ( / ^ \d + $ / . test ( postId ) ) {
108+ return `${ linkedInUrlPatterns . postPermalink } urn:li:activity:${ postId } ` ;
109+ }
110+ // For componentKey-based IDs, return a search URL as fallback
111+ // The actual URL will be set during extraction if we find the tracking scope
104112 return `${ linkedInUrlPatterns . postPermalink } urn:li:activity:${ postId } ` ;
105113 }
106114
115+ /**
116+ * Extract the actual activity URN from tracking scope data
117+ * LinkedIn embeds the URN in a data-view-tracking-scope attribute as encoded JSON
118+ */
119+ private extractActivityUrn ( element : Element ) : string | null {
120+ // Find the parent with tracking scope data
121+ const trackingParent = element . closest ( '[data-view-tracking-scope]' ) ;
122+ if ( ! trackingParent ) {
123+ // Try checking if the element itself has tracking scope
124+ const parentContainer = element . parentElement ?. closest ( '[data-view-tracking-scope]' ) ;
125+ if ( ! parentContainer ) {
126+ return null ;
127+ }
128+ return this . parseTrackingScope ( parentContainer ) ;
129+ }
130+ return this . parseTrackingScope ( trackingParent ) ;
131+ }
132+
133+ /**
134+ * Parse the tracking scope attribute to extract the updateUrn
135+ */
136+ private parseTrackingScope ( element : Element ) : string | null {
137+ const trackingScope = element . getAttribute ( 'data-view-tracking-scope' ) ;
138+ if ( ! trackingScope ) return null ;
139+
140+ try {
141+ const parsed = JSON . parse ( trackingScope ) ;
142+ if ( ! Array . isArray ( parsed ) || parsed . length === 0 ) return null ;
143+
144+ // Look for FeedUpdateServedEvent which contains the post URN
145+ for ( const item of parsed ) {
146+ if ( item . breadcrumb ?. content ?. data ) {
147+ // The data is an array of byte values that form a JSON string
148+ const bytes = item . breadcrumb . content . data ;
149+ if ( Array . isArray ( bytes ) ) {
150+ const jsonString = bytes . map ( ( b : number ) => String . fromCharCode ( b ) ) . join ( '' ) ;
151+ try {
152+ const contentData = JSON . parse ( jsonString ) ;
153+ if ( contentData . updateUrn ) {
154+ // Extract the activity ID from "urn:li:activity:1234567890"
155+ const match = contentData . updateUrn . match ( / u r n : l i : a c t i v i t y : ( \d + ) / ) ;
156+ if ( match ) {
157+ return match [ 1 ] ;
158+ }
159+ }
160+ } catch {
161+ // JSON parse failed for content data
162+ continue ;
163+ }
164+ }
165+ }
166+ }
167+ } catch {
168+ // JSON parse failed for tracking scope
169+ console . debug ( LOG_PREFIX , 'Failed to parse tracking scope' ) ;
170+ }
171+ return null ;
172+ }
173+
107174 /**
108175 * Extract a post from a DOM element
109176 */
110177 extractPost ( element : Element ) : ExtractedPost | null {
111178 try {
112179 const postId = this . getPostId ( element ) ;
113180 if ( ! postId ) {
114- console . debug ( ` ${ LOG_PREFIX } Could not extract post ID from element` ) ;
181+ console . debug ( LOG_PREFIX , ' Could not extract post ID from element' ) ;
115182 return null ;
116183 }
117184
118185 // Check if this is a sponsored post and skip it
119186 if ( this . isSponsored ( element ) ) {
120- console . debug ( ` ${ LOG_PREFIX } Skipping sponsored post: ${ postId } ` ) ;
187+ console . debug ( LOG_PREFIX , ' Skipping sponsored post:' , postId ) ;
121188 return null ;
122189 }
123190
124191 const authorName = this . extractAuthorName ( element ) ;
125192 if ( ! authorName ) {
126- console . debug ( ` ${ LOG_PREFIX } Could not extract author name for post: ${ postId } ` ) ;
193+ console . debug ( LOG_PREFIX , ' Could not extract author name for post:' , postId ) ;
127194 return null ;
128195 }
129196
130197 const content = this . extractPostContent ( element ) ;
131198 if ( ! content ) {
132- console . debug ( ` ${ LOG_PREFIX } Could not extract content for post: ${ postId } ` ) ;
199+ console . debug ( LOG_PREFIX , ' Could not extract content for post:' , postId ) ;
133200 return null ;
134201 }
135202
136203 const isRepost = this . isRepost ( element ) ;
137204
205+ // Try to extract the actual activity URN for proper URL generation
206+ const activityUrn = this . extractActivityUrn ( element ) ;
207+ const postUrl = activityUrn ? this . getPostUrl ( activityUrn ) : this . getPostUrl ( postId ) ;
208+
138209 const post : ExtractedPost = {
139210 id : postId ,
140- url : this . getPostUrl ( postId ) ,
211+ url : postUrl ,
141212 authorName,
142213 authorHeadline : this . extractAuthorHeadline ( element ) ,
143214 authorProfileUrl : this . extractAuthorProfileUrl ( element ) ,
@@ -153,10 +224,10 @@ export class LinkedInAdapter implements PlatformAdapter {
153224 extractedAt : Date . now ( ) ,
154225 } ;
155226
156- console . debug ( ` ${ LOG_PREFIX } Extracted post:` , post . id , post . authorName ) ;
227+ console . debug ( LOG_PREFIX , ' Extracted post:' , post . id , post . authorName ) ;
157228 return post ;
158229 } catch ( error ) {
159- console . error ( ` ${ LOG_PREFIX } Error extracting post:` , error ) ;
230+ console . error ( LOG_PREFIX , ' Error extracting post:' , error ) ;
160231 return null ;
161232 }
162233 }
0 commit comments