Skip to content

Commit 9292652

Browse files
committed
feat(rss): add highlights feed; remove feeds popup
1 parent aad1149 commit 9292652

4 files changed

Lines changed: 65 additions & 89 deletions

File tree

src/client/styles/header.scss

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -51,51 +51,6 @@
5151
}
5252
}
5353

54-
.rssMenu {
55-
position: relative;
56-
57-
summary {
58-
display: block;
59-
list-style: none;
60-
cursor: pointer;
61-
}
62-
63-
summary::-webkit-details-marker {
64-
display: none;
65-
}
66-
}
67-
68-
.rssMenuPopover {
69-
position: absolute;
70-
top: calc(100% + var(--size-2));
71-
right: 0;
72-
z-index: 10;
73-
74-
display: grid;
75-
min-width: 13rem;
76-
padding: var(--size-2);
77-
border: 1px solid var(--orange-200);
78-
border-radius: var(--radius-lg);
79-
background: var(--bg);
80-
box-shadow: 0 10px 30px rgb(0 0 0 / 0.08);
81-
}
82-
83-
.rssMenuLink,
84-
.rssMenuIndex {
85-
padding: var(--size-2);
86-
border-radius: var(--radius-md);
87-
transition: background-color 100ms;
88-
89-
&:hover {
90-
background: var(--orange-100);
91-
}
92-
}
93-
94-
.rssMenuIndex {
95-
margin-bottom: var(--size-1);
96-
font-weight: 600;
97-
}
98-
9954
.iconSystem,
10055
.iconLight,
10156
.iconDark {
@@ -118,16 +73,6 @@
11873
.iconButton:hover {
11974
background: var(--neutral-800);
12075
}
121-
122-
.rssMenuPopover {
123-
border-color: var(--neutral-800);
124-
box-shadow: 0 10px 30px rgb(0 0 0 / 0.24);
125-
}
126-
127-
.rssMenuLink:hover,
128-
.rssMenuIndex:hover {
129-
background: var(--neutral-800);
130-
}
13176
}
13277

13378
html[data-theme="system"] .header .iconSystem {

src/components/Header.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useSSRContext } from "~/lib/context";
2-
import { RSS_FEEDS, getRSSFeedPath } from "~/lib/rss";
32
import { Link } from "./Link";
43
import { MagnifyingGlassIcon } from "./icons/MagnifyingGlassIcon";
54
import { MoonIcon } from "./icons/MoonIcon";
@@ -42,27 +41,9 @@ export const Header = () => {
4241
>
4342
<PaperAirplaneIcon />
4443
</button>
45-
<details class="rssMenu">
46-
<summary class="iconButton" title="RSS feeds" aria-label="RSS feeds">
47-
<RssIcon />
48-
</summary>
49-
50-
<div class="rssMenuPopover">
51-
<Link className="rssMenuIndex" href="/rss" isActive={url.pathname === "/rss"}>
52-
RSS index
53-
</Link>
54-
55-
{RSS_FEEDS.map((feed) => {
56-
const feedPath = getRSSFeedPath(feed.slug);
57-
58-
return (
59-
<Link className="rssMenuLink" href={feedPath} key={feed.slug}>
60-
{feed.title}
61-
</Link>
62-
);
63-
})}
64-
</div>
65-
</details>
44+
<Link className="iconButton" href="/rss" title="RSS feeds" aria-label="RSS feeds">
45+
<RssIcon />
46+
</Link>
6647
<a
6748
className="iconButton"
6849
title="Search Hacker News"

src/lib/hnrss.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const parseRSSQuery = (
142142
const count = parsePositiveInt(query.count);
143143
const description = getSingleQueryValue(query.description);
144144
const link = getSingleQueryValue(query.link);
145-
const isStoryFeed = feed.source !== "bestcomments";
145+
const isStoryFeed = feed.source !== "comments";
146146
const linkTo = isStoryFeed && link === "comments" ? "comments" : "article";
147147
const minPoints = isStoryFeed ? parsePositiveInt(query.points) : undefined;
148148
const minComments = isStoryFeed ? parsePositiveInt(query.comments) : undefined;
@@ -205,15 +205,30 @@ const buildPaginatedHNUrl = (path: string, page: number) => {
205205
return `${HN_BASE_URL}${path}${separator}p=${page}`;
206206
};
207207

208-
const scrapeItemIds = async (path: string, page: number) => {
208+
const parseScrapedPage = (html: string) => {
209+
const document = parse(html);
210+
211+
return {
212+
ids: document
213+
.querySelectorAll("tr.athing")
214+
.map((node) => node.getAttribute("id"))
215+
.filter((id): id is string => Boolean(id)),
216+
nextPath: document.querySelector("a.morelink")?.getAttribute("href") ?? undefined,
217+
};
218+
};
219+
220+
const scrapePagedItemIds = async (path: string, page: number) => {
209221
const url = buildPaginatedHNUrl(path, page);
210222
const html = await getCached(url, () => fetchText(url));
211-
const document = parse(html);
212223

213-
return document
214-
.querySelectorAll("tr.athing")
215-
.map((node) => node.getAttribute("id"))
216-
.filter((id): id is string => Boolean(id));
224+
return parseScrapedPage(html).ids;
225+
};
226+
227+
const scrapeCursorItemIds = async (path: string) => {
228+
const url = new URL(path, HN_BASE_URL).toString();
229+
const html = await getCached(url, () => fetchText(url));
230+
231+
return parseScrapedPage(html);
217232
};
218233

219234
const buildNumericFilters = (
@@ -260,7 +275,7 @@ const buildSpecialFeedParams = (
260275

261276
params.set("hitsPerPage", String(ids.length));
262277

263-
if (feed.source === "bestcomments") {
278+
if (feed.source === "comments") {
264279
params.set(
265280
"filters",
266281
ids.map((id) => `objectID:"${id}"`).join(" OR "),
@@ -287,8 +302,33 @@ const fetchSpecialFeedHits = async (
287302

288303
const hits: AlgoliaSearchHit[] = [];
289304

305+
if (feed.slug === "highlights") {
306+
let nextPath = feed.scrapePath;
307+
308+
while (nextPath && hits.length < query.count) {
309+
const scrapedPage = await scrapeCursorItemIds(nextPath);
310+
311+
if (scrapedPage.ids.length === 0) {
312+
break;
313+
}
314+
315+
const params = buildSpecialFeedParams(feed, query, scrapedPage.ids);
316+
const pageHits = await fetchAlgolia(params);
317+
318+
hits.push(...orderHitsByIds(scrapedPage.ids, pageHits));
319+
320+
if (scrapedPage.ids.length < PAGE_SIZE) {
321+
break;
322+
}
323+
324+
nextPath = scrapedPage.nextPath;
325+
}
326+
327+
return hits;
328+
}
329+
290330
for (let page = 1; hits.length < query.count; page += 1) {
291-
const pageIds = await scrapeItemIds(feed.scrapePath, page);
331+
const pageIds = await scrapePagedItemIds(feed.scrapePath, page);
292332

293333
if (pageIds.length === 0) {
294334
break;
@@ -434,7 +474,7 @@ export const buildRSSFeedXML = async (
434474
const visibleHits = hits.slice(0, query.count);
435475
let items: RSSItem[];
436476

437-
if (feed.source === "bestcomments") {
477+
if (feed.source === "comments") {
438478
items = visibleHits.map((hit) => buildCommentItem(hit, requestUrl.origin, query));
439479
} else {
440480
items = visibleHits.map((hit) => buildStoryItem(hit, requestUrl.origin, query));

src/lib/rss.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type RSSFeedSlug =
77
| "classic"
88
| "best"
99
| "active"
10+
| "highlights"
1011
| "bestcomments";
1112

1213
export type RSSFeedCategory =
@@ -21,7 +22,7 @@ export interface RSSFeedDefinition {
2122
summary: string;
2223
category: RSSFeedCategory;
2324
channelLink: string;
24-
source: "algolia" | "special" | "bestcomments";
25+
source: "algolia" | "special" | "comments";
2526
algoliaTags?: string;
2627
scrapePath?: string;
2728
topicName?: string;
@@ -104,13 +105,22 @@ export const RSS_FEEDS: RSSFeedDefinition[] = [
104105
source: "special",
105106
scrapePath: "/active",
106107
},
108+
{
109+
slug: "highlights",
110+
title: "Highlights",
111+
summary: "Hand-curated standout comments and subthreads from Hacker News.",
112+
category: "Comments",
113+
channelLink: "https://news.ycombinator.com/highlights",
114+
source: "comments",
115+
scrapePath: "/highlights",
116+
},
107117
{
108118
slug: "bestcomments",
109119
title: "Best Comments",
110120
summary: "Recent highly voted comments from across Hacker News.",
111121
category: "Comments",
112122
channelLink: "https://news.ycombinator.com/bestcomments",
113-
source: "bestcomments",
123+
source: "comments",
114124
scrapePath: "/bestcomments",
115125
},
116126
];

0 commit comments

Comments
 (0)