utils.js
1 import { CommandExecutionError } from '@jackwener/opencli/errors'; 2 export function buildMediumTagUrl(topic) { 3 return topic ? `https://medium.com/tag/${encodeURIComponent(topic)}` : 'https://medium.com/tag/technology'; 4 } 5 export function buildMediumSearchUrl(keyword) { 6 return `https://medium.com/search?q=${encodeURIComponent(keyword)}`; 7 } 8 export function buildMediumUserUrl(username) { 9 return username.startsWith('@') ? `https://medium.com/${username}` : `https://medium.com/@${username}`; 10 } 11 export async function loadMediumPosts(page, url, limit) { 12 if (!page) 13 throw new CommandExecutionError('Browser session required for medium posts'); 14 await page.goto(url); 15 await page.wait({ selector: 'article', timeout: 5 }); 16 const data = await page.evaluate(` 17 (async () => { 18 await new Promise((resolve) => setTimeout(resolve, 3000)); 19 20 const limit = ${Math.max(1, Math.min(limit, 50))}; 21 const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim(); 22 const posts = []; 23 const seen = new Set(); 24 25 for (const article of Array.from(document.querySelectorAll('article'))) { 26 try { 27 const titleEl = article.querySelector('h2, h3, h1'); 28 const title = normalize(titleEl?.textContent); 29 if (!title) continue; 30 31 const linkEl = titleEl?.closest('a') || article.querySelector('a[href*="/@"], a[href*="/p/"]'); 32 let url = linkEl?.getAttribute('href') || ''; 33 if (!url) continue; 34 if (!url.startsWith('http')) url = 'https://medium.com' + url; 35 if (seen.has(url)) continue; 36 37 const author = normalize( 38 Array.from(article.querySelectorAll('a[href^="/@"]')) 39 .map((node) => normalize(node.textContent)) 40 .find((text) => text && text !== title), 41 ); 42 43 const allText = normalize(article.textContent); 44 const dateEl = article.querySelector('time'); 45 const date = normalize(dateEl?.textContent) || 46 dateEl?.getAttribute('datetime') || 47 allText.match(/\\b(?:[A-Z][a-z]{2}\\s+\\d{1,2}|\\d+[dhmw]\\s+ago)\\b/)?.[0] || 48 ''; 49 50 const readTime = allText.match(/(\\d+)\\s*min\\s*read/i)?.[0] || ''; 51 const claps = allText.match(/\\b(\\d+(?:\\.\\d+)?[KkMm]?)\\s*claps?\\b/i)?.[1] || ''; 52 53 const description = normalize( 54 Array.from(article.querySelectorAll('h3, p')) 55 .map((node) => normalize(node.textContent)) 56 .find((text) => text && text !== title && text !== author && !/member-only story|response icon/i.test(text)), 57 ); 58 59 seen.add(url); 60 posts.push({ 61 rank: posts.length + 1, 62 title, 63 author, 64 date, 65 readTime, 66 claps, 67 description: description ? description.slice(0, 150) : '', 68 url, 69 }); 70 71 if (posts.length >= limit) break; 72 } catch {} 73 } 74 75 return posts; 76 })() 77 `); 78 return Array.isArray(data) ? data : []; 79 }