ARTICLE AD BOX
In my Next.js 15 website I added a LinkedIn feed viewer.
As per my understanding, linkedin has posts FINDER to retrive all posts, but its response is not ideal for rendering as the posts image are returned in a form of urn:li:image:XXXXX instead of the usable image link https://media.licdn.com/dms/image/v2/XXXXX).
To achieve my goal I planned to call the posts FINDER and then retrieve all images ID to then call the images BATCH-GET API.
In my code, I menaged to achieve the posts and image retrieving, but I get some problems with caching. I can call posts FINDER 100 times in 24h but images get only 2 in 24h. I was planning to use caching to avoid calling the API too many times.
With my solution I still call the API more then 300 times a day if a hit fails, and the API stops working after some time. I don't have much error handling in my code.
What is the best solution in this scenario?
import mockData from '@/data/response.json'; export interface LinkedInPost { id: string; author: string; createdAt: number; publishedAt: number; commentary?: string; visibility?: string; lifecycleState?: string; distribution?: { feedDistribution?: string; thirdPartyDistributionChannels?: string[]; }; content?: { media?: { id: string; altText?: string; }; multiImage?: { images: Array<{ id: string; altText?: string; }>; }; article?: { title: string; description?: string; source: string; thumbnail: string; thumbnailAltText?: string; }; }; media: MediaItem[]; } export interface MediaItem { id: string; altText?: string | null; } interface LinkedInFeed { elements: LinkedInPost[]; } const TOKEN = process.env.LINKEDIN_ACCESS_TOKEN; async function fetchFeedRaw(): Promise<LinkedInFeed> { const url = `https://api.linkedin.com/rest/posts?q=author&author=urn%3Ali%3Aorganization%XXXXX&count=100&sortBy=CREATED`; const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}`, 'X-Restli-Protocol-Version': '2.0.0', 'LinkedIn-Version': '202511', 'X-RestLi-Method': 'FINDER', }, next: { revalidate: 21600 }, }); if (!res.ok) { const body = await res.text(); throw new Error(`LinkedIn feed error ${res.status}: ${body}`); } return res.json(); } async function fetchImagesMap( imageUrns: string[] ): Promise<Record<string, { downloadUrl?: string }>> { if (imageUrns.length === 0) return {}; const listParam = `List(${imageUrns.map(encodeURIComponent).join(',')})`; const url = `https://api.linkedin.com/rest/images?ids=${listParam}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}`, 'X-Restli-Protocol-Version': '2.0.0', 'LinkedIn-Version': '202511', }, next: { revalidate: 43200 }, }); if (!res.ok) { const body = await res.text(); throw new Error(`LinkedIn images error ${res.status}: ${body}`); } const json = await res.json(); return json?.results ?? {}; } export async function fetchLinkedInFeed(): Promise<LinkedInFeed> { // Keep your mock switch if (process.env.MOCK_DATA === 'true') { return mockData as LinkedInFeed; } const feed = await fetchFeedRaw(); // 1) Collect all image URNs const imageUrnsSet = new Set<string>(); for (const post of feed.elements ?? []) { const single = post.content?.media?.id; if (single?.startsWith('urn:li:image:')) imageUrnsSet.add(single); for (const img of post.content?.multiImage?.images ?? []) { if (img.id?.startsWith('urn:li:image:')) imageUrnsSet.add(img.id); } } // 2) Fetch downloadUrl map const imageMap = await fetchImagesMap(Array.from(imageUrnsSet)); // 3) Patch each post to have top-level `media: [{ id: downloadUrl, altText }]` for (const post of feed.elements ?? []) { const mapped: MediaItem[] = []; const single = post.content?.media; if (single?.id) { const info = imageMap[single.id]; if (info?.downloadUrl) { mapped.push({ id: info.downloadUrl, altText: single.altText ?? null, }); } } for (const img of post.content?.multiImage?.images ?? []) { const info = imageMap[img.id]; if (info?.downloadUrl) { mapped.push({ id: info.downloadUrl, altText: img.altText ?? null, }); } } post.media = mapped; } return feed; }