Mirror of
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: update pages, scripts and schema

+130 -54
+14 -7
scripts/generate-digest.ts
··· 22 22 return topics[0]; 23 23 } 24 24 25 - function getPostType(date: Date): "daily" | "nightly" { 26 - const hour = date.getUTCHours() + 1; 27 - const isMorning = hour >= 6 && hour <= 12; 28 - return isMorning ? "daily" : "nightly"; 25 + function getPostType(date: Date): "daily" | "midday" | "nightly" { 26 + const hour = date.getUTCHours() + 1; // Basic UTC+1 adjustment 27 + 28 + // Shifted 1h early to protect against early GitHub Action triggers 29 + const isDaily = hour >= 5 && hour < 13; // Target 6am 30 + const isMidday = hour >= 13 && hour < 21; // Target 2pm 31 + 32 + if (isDaily) return "daily"; 33 + if (isMidday) return "midday"; 34 + return "nightly"; // Target 10pm 29 35 } 30 36 31 37 async function run() { 32 38 console.log("\n\x1b[1m🚀 Generating Intelligent Topic Digest\x1b[0m"); 33 39 34 40 const now = new Date(); 35 - const twelveHoursAgo = new Date(now.getTime() - 12 * 60 * 60 * 1000); 41 + const windowSize = 8 * 60 * 60 * 1000; 42 + const startTime = new Date(now.getTime() - windowSize); 36 43 37 44 try { 38 45 const [gh, bs] = await Promise.all([ 39 - fetchGitHubEvents(twelveHoursAgo), 40 - fetchBlueskyEvents(twelveHoursAgo), 46 + fetchGitHubEvents(startTime), 47 + fetchBlueskyEvents(startTime), 41 48 ]); 42 49 43 50 const allEvents = [...gh, ...bs];
src/content/posts/2026-01-22-daily.json

This is a binary file and will not be displayed.

+18
src/content/posts/2026-01-22-midday.json
··· 1 + { 2 + "title": "npmx Launch 🚀", 3 + "date": "2026-01-22T06:00:00.000Z", 4 + "type": "midday", 5 + "topics": [ 6 + { 7 + "title": "chore: init", 8 + "summary": "danielroe committed the beginning of an awesome community", 9 + "relevanceScore": 10, 10 + "sources": [ 11 + { 12 + "platform": "github", 13 + "url": "https://github.com/npmx-dev/npmx.dev/commit/e39e56c08fd1e7bdb556c8565c6b11b3c34c8934" 14 + } 15 + ] 16 + } 17 + ] 18 + }
src/content/posts/2026-01-23-daily.json

This is a binary file and will not be displayed.

src/content/posts/2026-01-23-midday.json

This is a binary file and will not be displayed.

+1 -1
src/content/posts/2026-02-02-daily.json
··· 1 1 { 2 2 "title": "npmx Adopts Canonical Routing and Fast Meta Acceleration", 3 3 "date": "2026-02-02T06:00:00.000Z", 4 - "type": "nightly", 4 + "type": "daily", 5 5 "topics": [ 6 6 { 7 7 "title": "Routing Architecture: Canonical Schema & Redirects",
+34 -5
src/lib/events.ts
··· 156 156 const token = getRequiredEnv("GITHUB_TOKEN"); 157 157 if (!token || events.length === 0) return []; 158 158 159 - LOG.ai(`Clustering ${events.length} signals into topics...`); 159 + LOG.ai( 160 + `Clustering ${events.length} signals into topics (Prioritizing Bluesky)...`, 161 + ); 160 162 163 + // Directive to the AI: Use Bluesky as community anchors 161 164 const prompt = `You are a technical analyst for npmx. Group the following events into 5-6 logical "Topics". 165 + 166 + STRATEGY: 167 + 1. Community Focus: Treat "bluesky" events as high-signal community interests. 168 + 2. Inclusive Clustering: Ensure that "bluesky" posts are not sidelined; weave them into relevant technical topics where possible. 169 + 3. Topic Weight: If a topic includes a "bluesky" post, it should generally have a higher relevanceScore. 170 + 162 171 Sort by relevanceScore (1-10). Refer to the project strictly as "npmx" (lowercase). 163 172 Return ONLY a JSON array with this structure: { "topics": Topic[] }. 173 + 164 174 Events: ${JSON.stringify(events)}`; 165 175 166 176 try { ··· 175 185 body: JSON.stringify({ 176 186 messages: [{ role: "user", content: prompt }], 177 187 model: "gpt-4o-mini", 178 - temperature: 0.3, 188 + temperature: 0.3, // Lower temp for more stable clustering 179 189 response_format: { type: "json_object" }, 180 190 }), 181 191 }, ··· 183 193 184 194 if (response.ok) { 185 195 const data = await response.json(); 186 - const parsed = JSON.parse(data.choices[0].message.content); 196 + const content = data.choices[0].message.content; 197 + const parsed = JSON.parse(content); 187 198 188 - const topics = parsed.topics.map((t: any) => { 199 + // Re-ranking Logic: Slightly boost topics that contain Bluesky content 200 + const rawTopics = parsed.topics.map((t: any) => { 189 201 const validated = TopicSchema.parse(t); 202 + const hasBluesky = events.some( 203 + (e) => 204 + e.source === "bluesky" && 205 + t.summary.includes(e.title.substring(0, 20)), 206 + ); 207 + 190 208 return { 191 209 ...validated, 192 210 title: sanitizeBrand(validated.title), 193 211 summary: sanitizeBrand(validated.summary), 212 + // Subtle priority boost for community-facing topics 213 + relevanceScore: hasBluesky 214 + ? Math.min(10, validated.relevanceScore + 1) 215 + : validated.relevanceScore, 194 216 }; 195 217 }); 196 218 197 - LOG.success(`Successfully clustered into ${topics.length} topics.`); 219 + // Sort by the new boosted score 220 + const topics = rawTopics.sort( 221 + (a: Topic, b: Topic) => b.relevanceScore - a.relevanceScore, 222 + ); 223 + 224 + LOG.success( 225 + `Successfully clustered into ${topics.length} topics with Bluesky priority.`, 226 + ); 198 227 return topics; 199 228 } 200 229 } catch {
+1 -1
src/lib/schema.ts
··· 15 15 export const PostSchema = z.object({ 16 16 title: z.string(), 17 17 date: z.coerce.date(), 18 - type: z.enum(["daily", "nightly"]), 18 + type: z.enum(["daily", "midday", "nightly"]), 19 19 topics: z.array(TopicSchema), 20 20 }); 21 21
+12 -1
src/pages/archive.astro
··· 25 25 <span class="date">{displayDate}</span> 26 26 <span class="title">{data.title}</span> 27 27 <span class="dot" /> 28 - <span class="type">{data.type}</span> 28 + <span class={`type ${data.type}`}>{data.type}</span> 29 29 </a> 30 30 ); 31 31 }) ··· 74 74 font-size: 0.75rem; 75 75 text-transform: capitalize; 76 76 margin-left: 1rem; 77 + font-weight: 600; 78 + } 79 + 80 + .daily { 81 + color: #fbbf24; 82 + } 83 + .midday { 84 + color: #f97316; 85 + } 86 + .nightly { 87 + color: #818cf8; 77 88 } 78 89 79 90 .dot {
+50 -39
src/pages/index.astro
··· 7 7 (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(), 8 8 ); 9 9 10 - const latestPosts = posts.slice(0, 5); 10 + const MAX_DISPLAY_POSTS = 5; 11 + const TOPIC_LIMIT = 3; 12 + 13 + const latestPosts = posts.slice(0, MAX_DISPLAY_POSTS); 14 + const hasArchive = posts.length > MAX_DISPLAY_POSTS; 11 15 --- 12 16 13 17 <Layout title="npmx.digest"> ··· 21 25 22 26 <div class="post-list"> 23 27 { 24 - latestPosts.map((post) => ( 25 - <a href={`/posts/${post.id}`} class="post-entry"> 26 - <div class="post-meta"> 27 - <time datetime={post.data.date.toString()}> 28 - {new Date(post.data.date).toLocaleDateString( 29 - "en-US", 30 - { 31 - month: "short", 32 - day: "numeric", 33 - }, 34 - )} 35 - </time> 36 - <span class={`type-tag ${post.data.type}`}> 37 - {post.data.type} 38 - </span> 39 - </div> 40 - <div class="post-content"> 41 - <h2>{post.data.title}</h2> 42 - <div class="topic-preview"> 43 - {post.data.topics.slice(0, 3).map((topic, i) => ( 44 - <span> 45 - {topic.title} 46 - {i < 47 - Math.min(post.data.topics.length, 3) - 1 48 - ? " • " 49 - : ""} 50 - </span> 51 - ))} 52 - {post.data.topics.length > 3 && ( 53 - <span class="more"> 54 - {" "} 55 - +{post.data.topics.length - 3} more 56 - </span> 57 - )} 28 + latestPosts.map((post) => { 29 + const postDate = new Date(post.data.date); 30 + const displayDate = postDate.toLocaleDateString("en-US", { 31 + month: "short", 32 + day: "numeric", 33 + }); 34 + 35 + const visibleTopics = post.data.topics.slice(0, TOPIC_LIMIT); 36 + const remainingCount = post.data.topics.length - TOPIC_LIMIT; 37 + 38 + return ( 39 + <a href={`/posts/${post.id}`} class="post-entry"> 40 + <div class="post-meta"> 41 + <time datetime={post.data.date.toString()}> 42 + {displayDate} 43 + </time> 44 + <span class={`type-tag ${post.data.type}`}> 45 + {post.data.type} 46 + </span> 47 + </div> 48 + <div class="post-content"> 49 + <h2>{post.data.title}</h2> 50 + <div class="topic-preview"> 51 + {visibleTopics.map((topic, i) => ( 52 + <span> 53 + {topic.title} 54 + {i < visibleTopics.length - 1 55 + ? " • " 56 + : ""} 57 + </span> 58 + ))} 59 + {remainingCount > 0 && ( 60 + <span class="more"> 61 + {" "} 62 + +{remainingCount} more 63 + </span> 64 + )} 65 + </div> 58 66 </div> 59 - </div> 60 - </a> 61 - )) 67 + </a> 68 + ); 69 + }) 62 70 } 63 71 </div> 64 72 65 73 { 66 - posts.length > 5 && ( 74 + hasArchive && ( 67 75 <a href="/archive" class="archive-link"> 68 76 Explore the archives → 69 77 </a> ··· 125 133 126 134 .daily { 127 135 color: #fbbf24; 136 + } 137 + .midday { 138 + color: #f97316; 128 139 } 129 140 .nightly { 130 141 color: #818cf8;