Skip to content

Commit 708e634

Browse files
authored
feat: filter feed directory by topic/language and export OPML (#1233)
* feat: filter feed directory by topic/language and export OPML Surface catalog directory.topics and channel.language in the feed directory UI so visitors can AND-filter and export the visible set. * chore(deps): point html2rss gems at feature branches for CI Replace local path pins with git branch refs so CI can resolve unreleased directory.topics from the core and configs PRs. * style: lintfix
1 parent 85b13a1 commit 708e634

6 files changed

Lines changed: 473 additions & 92 deletions

File tree

‎Gemfile.lock‎

Lines changed: 19 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
GIT
22
remote: https://github.com/html2rss/html2rss-configs.git
3-
revision: 7f40e65325f33383a8bdabfc5261f8cff33a3a9e
3+
revision: d9e3dc55b38514eb9cd4a17cb0285d9ef73bc9d5
44
specs:
55
html2rss-configs (0.2.0)
66
html2rss
77

88
GIT
99
remote: https://github.com/html2rss/html2rss.git
10-
revision: 644c7f06ccd70f8ed231a26ffc9d8d3279a2617b
10+
revision: aa974733c6e88a5c934f445516d9752dfa316067
1111
specs:
1212
html2rss (0.25.0)
1313
addressable (~> 2.7)
@@ -17,50 +17,28 @@ GIT
1717
faraday-follow_redirects
1818
faraday-gzip (~> 3)
1919
kramdown
20+
mcp (~> 1.0)
2021
mime-types (> 3.0)
2122
nokogiri (>= 1.10, < 2.0)
22-
puppeteer-ruby
23+
rack (~> 3.0)
24+
rackup (~> 2.0)
2325
regexp_parser
2426
reverse_markdown (~> 3.0)
2527
rss
2628
sanitize
2729
thor
2830
tzinfo
31+
webrick (~> 1.9)
2932
zeitwerk
3033

3134
GEM
3235
remote: https://rubygems.org/
3336
specs:
3437
addressable (2.9.0)
3538
public_suffix (>= 2.0.2, < 8.0)
36-
async (2.44.1)
37-
console (~> 1.29)
38-
fiber-annotation
39-
io-event (~> 1.11)
40-
async-http (0.99.0)
41-
async (>= 2.35.1)
42-
async-pool (~> 0.11)
43-
io-endpoint (~> 0.14)
44-
io-stream (~> 0.14)
45-
protocol-http (~> 0.66)
46-
protocol-http1 (~> 0.39)
47-
protocol-http2 (~> 0.26)
48-
protocol-url (~> 0.2)
49-
async-pool (0.11.2)
50-
async (>= 2.0)
51-
async-websocket (0.30.1)
52-
async-http (~> 0.76)
53-
protocol-http (~> 0.34)
54-
protocol-rack (~> 0.7)
55-
protocol-websocket (~> 0.17)
56-
base64 (0.3.0)
5739
bigdecimal (4.1.2)
5840
brotli (0.8.0)
5941
concurrent-ruby (1.3.8)
60-
console (1.37.0)
61-
fiber-annotation
62-
fiber-local (~> 1.1)
63-
json
6442
crass (1.0.7)
6543
dry-configurable (1.4.0)
6644
dry-core (~> 1.0)
@@ -108,18 +86,18 @@ GEM
10886
zlib (~> 3.0)
10987
faraday-net_http (3.4.4)
11088
net-http (~> 0.5)
111-
fiber-annotation (0.2.0)
112-
fiber-local (1.1.0)
113-
fiber-storage
114-
fiber-storage (1.0.1)
115-
io-endpoint (0.17.2)
116-
io-event (1.19.5)
117-
io-stream (0.14.0)
118-
openssl (>= 3.3)
89+
hana (1.3.7)
11990
json (2.21.2)
91+
json_schemer (2.5.0)
92+
bigdecimal
93+
hana (~> 1.3)
94+
regexp_parser (~> 2.0)
95+
simpleidn (~> 0.2)
12096
kramdown (2.5.2)
12197
rexml (>= 3.4.4)
12298
logger (1.7.0)
99+
mcp (1.2.0)
100+
json_schemer (>= 2.4)
123101
mime-types (3.7.0)
124102
logger
125103
mime-types-data (~> 3.2025, >= 3.2025.0507)
@@ -130,31 +108,11 @@ GEM
130108
racc (~> 1.4)
131109
nokogiri (1.19.4-x86_64-linux-gnu)
132110
racc (~> 1.4)
133-
openssl (4.0.2)
134-
protocol-hpack (1.5.1)
135-
protocol-http (0.70.0)
136-
protocol-http1 (0.40.2)
137-
protocol-http (~> 0.68)
138-
protocol-http2 (0.26.2)
139-
protocol-hpack (~> 1.4)
140-
protocol-http (~> 0.62)
141-
protocol-rack (0.22.1)
142-
io-stream (>= 0.10)
143-
protocol-http (~> 0.58)
144-
rack (>= 1.0)
145-
protocol-url (0.18.0)
146-
protocol-websocket (0.21.1)
147-
protocol-http (~> 0.2)
148111
public_suffix (7.0.5)
149-
puppeteer-ruby (0.53.0)
150-
async (>= 2.35.1, < 3.0)
151-
async-http (>= 0.60, < 1.0)
152-
async-websocket (>= 0.27, < 1.0)
153-
base64
154-
mime-types (>= 3.0)
155-
urlpattern (~> 0.1.1)
156112
racc (1.8.1)
157113
rack (3.2.7)
114+
rackup (2.3.1)
115+
rack (>= 3)
158116
regexp_parser (2.12.0)
159117
reverse_markdown (3.0.2)
160118
nokogiri
@@ -164,16 +122,17 @@ GEM
164122
sanitize (7.0.0)
165123
crass (~> 1.0.2)
166124
nokogiri (>= 1.16.8)
125+
simpleidn (0.2.3)
167126
thor (1.5.0)
168127
tzinfo (2.0.6)
169128
concurrent-ruby (~> 1.0)
170129
uri (1.1.1)
171-
urlpattern (0.1.1-arm64-darwin)
172-
urlpattern (0.1.1-x86_64-linux)
130+
webrick (1.9.2)
173131
zeitwerk (2.8.3)
174132
zlib (3.2.3)
175133

176134
PLATFORMS
135+
arm64-darwin-23
177136
arm64-darwin-24
178137
arm64-darwin-25
179138
x86_64-linux

‎bin/data-update‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ output = file_names.map do |file_name|
5252
'valid_channel_url' => valid_url(config['channel']['url']),
5353
'url_parameters' => string_formatting_references(config['channel']['url']),
5454
'default_parameters' => default_parameters,
55-
'channel' => config['channel']
55+
'channel' => config['channel'],
56+
'directory' => config['directory']
5657
}
5758
end
5859

‎src/components/FeedDirectory.astro‎

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ function formatDefaultParameters(defaultParameters: Record<string, string> = {})
1111
.join(", ");
1212
}
1313
14+
const availableTopics = [...new Set(configs.flatMap((config) => config.directory?.topics || []))].sort(
15+
(a, b) => a.localeCompare(b),
16+
);
17+
18+
const availableLanguages = [
19+
...new Set(
20+
configs
21+
.map((config) => config.channel?.language)
22+
.filter((language): language is string => Boolean(language)),
23+
),
24+
].sort((a, b) => a.localeCompare(b));
25+
1426
const staticFeedUrls = configs.map((config) => ({
1527
...config,
1628
staticFeedUrl: "#",
@@ -19,6 +31,8 @@ const staticFeedUrls = configs.map((config) => ({
1931
?.replace(/^https?:\/\//, "")
2032
.replace(/^www\./, "")
2133
.replace(/\/$/, ""),
34+
topics: config.directory?.topics || [],
35+
language: config.channel?.language || "",
2236
}));
2337
---
2438

@@ -47,6 +61,38 @@ const staticFeedUrls = configs.map((config) => ({
4761
</p>
4862
</div>
4963

64+
<div class="filter-block">
65+
<div class="filter-group">
66+
<p class="filter-label" id="topic-filter-label">Topics</p>
67+
<div class="topic-chips" role="group" aria-labelledby="topic-filter-label">
68+
{
69+
availableTopics.map((topic) => (
70+
<button type="button" class="topic-chip" data-topic-filter={topic} aria-pressed="false">
71+
{topic}
72+
</button>
73+
))
74+
}
75+
</div>
76+
</div>
77+
78+
<div class="filter-actions">
79+
<div class="language-filter">
80+
<label class="filter-label" for="language-filter">Language</label>
81+
<select
82+
id="language-filter"
83+
class="language-select"
84+
data-language-filter
85+
aria-label="Filter by language"
86+
>
87+
<option value="">All languages</option>
88+
{availableLanguages.map((language) => <option value={language}>{language}</option>)}
89+
</select>
90+
</div>
91+
92+
<button type="button" class="export-opml" data-export-opml> Export OPML </button>
93+
</div>
94+
</div>
95+
5096
<div class="instance-block">
5197
<div class="instance-line">
5298
<span class="instance-label">Using instance:</span>
@@ -114,7 +160,9 @@ const staticFeedUrls = configs.map((config) => ({
114160
class="feed-row"
115161
data-domain={config.domain}
116162
data-name={config.name}
117-
data-searchable={`${config.domain}/${config.name} ${config.channel?.url || ""}`}
163+
data-topics={config.topics.join(" ")}
164+
data-language={config.language}
165+
data-searchable={`${config.domain}/${config.name} ${config.channel?.url || ""} ${config.topics.join(" ")} ${config.language}`}
118166
>
119167
<div class="feed-info">
120168
<div class="feed-mainline">
@@ -272,14 +320,16 @@ const staticFeedUrls = configs.map((config) => ({
272320
}
273321

274322
.search-block,
323+
.filter-block,
275324
.instance-block,
276325
.empty-state,
277326
.feed-row,
278327
.parameter-form {
279328
padding: var(--fd-padding);
280329
}
281330

282-
.search-block {
331+
.search-block,
332+
.filter-block {
283333
display: grid;
284334
gap: var(--fd-gap);
285335
border-bottom: 1px solid var(--fd-border);
@@ -325,6 +375,81 @@ const staticFeedUrls = configs.map((config) => ({
325375
outline: none;
326376
}
327377

378+
.filter-group {
379+
display: grid;
380+
gap: 0.375rem;
381+
}
382+
383+
.filter-label {
384+
margin: 0;
385+
color: var(--fd-muted);
386+
font-size: var(--sl-text-xs);
387+
}
388+
389+
.topic-chips {
390+
display: flex;
391+
flex-wrap: wrap;
392+
gap: 0.375rem;
393+
}
394+
395+
.topic-chip {
396+
appearance: none;
397+
padding: 0.25rem 0.625rem;
398+
border: 1px solid var(--fd-border);
399+
border-radius: calc(var(--fd-radius) - 0.125rem);
400+
background: transparent;
401+
color: var(--fd-muted);
402+
font: inherit;
403+
font-size: var(--sl-text-xs);
404+
cursor: pointer;
405+
}
406+
407+
.topic-chip[aria-pressed="true"] {
408+
border-color: var(--sl-color-accent);
409+
background: var(--sl-color-accent-low);
410+
color: var(--sl-color-white);
411+
}
412+
413+
.filter-actions {
414+
display: flex;
415+
flex-wrap: wrap;
416+
gap: var(--fd-gap);
417+
align-items: end;
418+
justify-content: space-between;
419+
}
420+
421+
.language-filter {
422+
display: grid;
423+
gap: 0.375rem;
424+
min-width: 10rem;
425+
}
426+
427+
.language-select {
428+
min-width: 0;
429+
padding: 0.5rem 0.75rem;
430+
border: 1px solid var(--fd-border);
431+
border-radius: calc(var(--fd-radius) - 0.125rem);
432+
background: var(--fd-surface-alt);
433+
color: var(--sl-color-white);
434+
font: inherit;
435+
font-size: var(--sl-text-sm);
436+
}
437+
438+
.export-opml {
439+
appearance: none;
440+
display: inline-flex;
441+
align-items: center;
442+
justify-content: center;
443+
padding: 0.5rem 0.75rem;
444+
border: 1px solid var(--fd-border);
445+
border-radius: calc(var(--fd-radius) - 0.125rem);
446+
background: transparent;
447+
color: var(--sl-color-white);
448+
font: inherit;
449+
font-size: var(--sl-text-sm);
450+
cursor: pointer;
451+
}
452+
328453
.result-summary,
329454
.instance-feedback,
330455
.empty-title,
@@ -551,6 +676,11 @@ const staticFeedUrls = configs.map((config) => ({
551676
.instance-apply:focus-visible,
552677
.done-button:hover,
553678
.done-button:focus-visible,
679+
.topic-chip:hover,
680+
.topic-chip:focus-visible,
681+
.export-opml:hover,
682+
.export-opml:focus-visible,
683+
.language-select:focus-visible,
554684
.instance-input:focus-visible,
555685
.form-input:focus-visible {
556686
border-color: var(--sl-color-accent);
@@ -651,5 +781,10 @@ const staticFeedUrls = configs.map((config) => ({
651781
.feed-meta {
652782
justify-content: flex-start;
653783
}
784+
785+
.filter-actions {
786+
flex-direction: column;
787+
align-items: stretch;
788+
}
654789
}
655790
</style>

0 commit comments

Comments
 (0)